From 3f9ad822c08bbe14fa5744d4fdd68b8f0ccc8ef5 Mon Sep 17 00:00:00 2001 From: Lukellmann Date: Wed, 25 Aug 2021 20:54:28 +0200 Subject: [PATCH] Snowflake: tests, checks in constructor that takes instant, endOfTime constant, more documentation --- common/src/main/kotlin/entity/Snowflake.kt | 35 +++++++++++- .../src/test/kotlin/entity/SnowflakeTest.kt | 57 +++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 common/src/test/kotlin/entity/SnowflakeTest.kt diff --git a/common/src/main/kotlin/entity/Snowflake.kt b/common/src/main/kotlin/entity/Snowflake.kt index f98d108ab1a2..f397d328dd80 100644 --- a/common/src/main/kotlin/entity/Snowflake.kt +++ b/common/src/main/kotlin/entity/Snowflake.kt @@ -33,13 +33,31 @@ class Snowflake(val value: Long) : Comparable { /** * Creates a Snowflake from a given [instant]. + * + * If the given [instant] is too far in the past / future, this constructor will create + * an instance with a [timeStamp] equal to [Snowflake.min] / [Snowflake.max]. */ - constructor(instant: Instant) : this((instant.toEpochMilliseconds() - discordEpochLong) shl 22) + constructor(instant: Instant) : this( + instant.toEpochMilliseconds() + .coerceAtLeast(discordEpochLong) // time before is unknown to Snowflakes + .minus(discordEpochLong) + .coerceAtMost(maxMillisecondsSinceDiscordEpoch) // time after is unknown to Snowflakes + .shl(22) + ) + /** + * A [String] representation of this Snowflake's [value]. + */ val asString get() = value.toString() + /** + * The point in time this Snowflake represents. + */ val timeStamp: Instant get() = Instant.fromEpochMilliseconds(discordEpochLong + (value ushr 22)) + /** + * A [TimeMark] for the point in time this Snowflake represents. + */ val timeMark: TimeMark get() = SnowflakeMark(timeStamp) override fun compareTo(other: Snowflake): Int = value.ushr(22).compareTo(other.value.ushr(22)) @@ -53,14 +71,25 @@ class Snowflake(val value: Long) : Comparable { } companion object { - private const val discordEpochLong = 1420070400000L + private const val discordEpochLong = 1420070400000L // 42 one bits + private const val maxMillisecondsSinceDiscordEpoch = 0b111111111111111111111111111111111111111111L + + /** + * The point in time that marks the Discord Epoch (the first second of 2015). + */ val discordEpochStart: Instant = Instant.fromEpochMilliseconds(discordEpochLong) + /** + * The last point in time a Snowflake can represent. + */ + val endOfTime: Instant = + Instant.fromEpochMilliseconds(discordEpochLong + maxMillisecondsSinceDiscordEpoch) + /** * The maximum value a Snowflake can hold. * Useful when requesting paginated entities. */ - val max: Snowflake = Snowflake(Long.MAX_VALUE) + val max: Snowflake = Snowflake(-1) /** * The minimum value a Snowflake can hold. diff --git a/common/src/test/kotlin/entity/SnowflakeTest.kt b/common/src/test/kotlin/entity/SnowflakeTest.kt new file mode 100644 index 000000000000..47356140bd54 --- /dev/null +++ b/common/src/test/kotlin/entity/SnowflakeTest.kt @@ -0,0 +1,57 @@ +package entity + +import dev.kord.common.entity.Snowflake +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit.Companion.MILLISECOND +import kotlinx.datetime.Instant +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlin.test.* + +class SnowflakeTest { + + @Test + fun `Snowflake with value 0 has timeStamp equal to the timeStamp of discordEpochStart`() { + val snowflake = Snowflake(0) + assertEquals(Snowflake.discordEpochStart, snowflake.timeStamp) + } + + @Test + fun `Snowflake with value -1 has timeStamp equal to the timeStamp of endOfTime`() { + val snowflake = Snowflake(-1) + assertEquals(Snowflake.endOfTime, snowflake.timeStamp) + } + + @Test + fun `Snowflake created from instant far in the past has timeStamp equal the timeStamp of Snowflake min`() { + val snowflake = Snowflake(Instant.DISTANT_PAST) + assertEquals(Snowflake.min.timeStamp, snowflake.timeStamp) + } + + @Test + fun `Snowflake created from instant far in the future has timeStamp equal the timeStamp of Snowflake max`() { + val snowflake = Snowflake(Instant.DISTANT_FUTURE) + assertEquals(Snowflake.max.timeStamp, snowflake.timeStamp) + } + + @Test + fun `Snowflake's timeStamp calculates an Instant close to the Instant the Snowflake was created from`() { + val instant = Clock.System.now() + val snowflake = Snowflake(instant) + + // snowflake timestamps have a millisecond accuracy -> allow +/-1 millisecond from original instant + val validTimeRange = instant.minus(1, MILLISECOND)..instant.plus(1, MILLISECOND) + + assertContains(validTimeRange, snowflake.timeStamp) + } + + @Test + fun `min Snowflake's timeMark has passed`() { + assertTrue(Snowflake.min.timeMark.hasPassedNow()) + } + + @Test + fun `max Snowflake's timeMark has not passed`() { + assertFalse(Snowflake.max.timeMark.hasPassedNow()) + } +}