Skip to content

Commit

Permalink
Use ULong for Snowflake value
Browse files Browse the repository at this point in the history
  • Loading branch information
lukellmann committed Aug 25, 2021
1 parent ab3a2f3 commit c889ff9
Show file tree
Hide file tree
Showing 23 changed files with 104 additions and 82 deletions.
3 changes: 1 addition & 2 deletions common/src/main/kotlin/entity/Interactions.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.kord.common.entity

import dev.kord.common.annotation.KordExperimental
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.OptionalSnowflake
Expand Down Expand Up @@ -625,7 +624,7 @@ fun CommandArgument<*>.boolean(): Boolean {


fun CommandArgument<*>.snowflake(): Snowflake {
val id = string().toLongOrNull() ?: error("$value wasn't a Snowflake")
val id = string().toULongOrNull() ?: error("$value wasn't a Snowflake")
return Snowflake(id)
}

Expand Down
33 changes: 18 additions & 15 deletions common/src/main/kotlin/entity/Snowflake.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package dev.kord.common.entity

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
Expand All @@ -16,20 +16,20 @@ import kotlin.time.TimeMark
* A unique identifier for entities [used by discord](https://discord.com/developers/docs/reference#snowflakes).
*
* Note: this class has a natural ordering that is inconsistent with [equals],
* since [compareTo] only compares the first 42 bits of the Long [value] (comparing the timestamp),
* whereas [equals] uses all bits of the Long [value].
* since [compareTo] only compares the first 42 bits of the ULong [value] (comparing the timestamp),
* whereas [equals] uses all bits of the ULong [value].
* [compareTo] can return `0` even if [equals] returns `false`,
* but [equals] only returns `true` if [compareTo] returns `0`.
*
* @constructor Creates a Snowflake from a given Long [value].
* @constructor Creates a Snowflake from a given ULong [value].
*/
@Serializable(with = Snowflake.Serializer::class)
class Snowflake(val value: Long) : Comparable<Snowflake> {
class Snowflake(val value: ULong) : Comparable<Snowflake> {

/**
* Creates a Snowflake from a given String [value], parsing it as a [Long] value.
* Creates a Snowflake from a given String [value], parsing it as a [ULong] value.
*/
constructor(value: String) : this(value.toLong())
constructor(value: String) : this(value.toULong())

/**
* Creates a Snowflake from a given [instant].
Expand All @@ -42,6 +42,7 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {
.coerceAtLeast(discordEpochLong) // time before is unknown to Snowflakes
.minus(discordEpochLong)
.coerceAtMost(maxMillisecondsSinceDiscordEpoch) // time after is unknown to Snowflakes
.toULong()
.shl(22)
)

Expand All @@ -53,14 +54,14 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {
/**
* The point in time this Snowflake represents.
*/
val timeStamp: Instant get() = Instant.fromEpochMilliseconds(discordEpochLong + (value ushr 22))
val timeStamp: Instant get() = Instant.fromEpochMilliseconds(discordEpochLong + (value shr 22).toLong())

/**
* 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))
override fun compareTo(other: Snowflake): Int = value.shr(22).compareTo(other.value.shr(22))

override fun toString(): String = "Snowflake(value=$value)"

Expand Down Expand Up @@ -89,23 +90,25 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {
* The maximum value a Snowflake can hold.
* Useful when requesting paginated entities.
*/
val max: Snowflake = Snowflake(-1)
val max: Snowflake = Snowflake(ULong.MAX_VALUE)

/**
* The minimum value a Snowflake can hold.
* Useful when requesting paginated entities.
*/
val min: Snowflake = Snowflake(0)
val min: Snowflake = Snowflake(ULong.MIN_VALUE)
}

@OptIn(ExperimentalSerializationApi::class)
internal class Serializer : KSerializer<Snowflake> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("Kord.Snowflake", PrimitiveKind.LONG)
get() = @OptIn(ExperimentalUnsignedTypes::class) ULong.serializer().descriptor

override fun deserialize(decoder: Decoder): Snowflake = Snowflake(decoder.decodeLong())
override fun deserialize(decoder: Decoder): Snowflake =
Snowflake(decoder.decodeInline(descriptor).decodeLong().toULong())

override fun serialize(encoder: Encoder, value: Snowflake) {
encoder.encodeLong(value.value)
encoder.encodeInline(descriptor).encodeLong(value.value.toLong())
}
}
}
Expand Down
23 changes: 12 additions & 11 deletions common/src/main/kotlin/entity/optional/OptionalSnowflake.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.kord.common.entity.optional

import dev.kord.common.entity.Snowflake
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
Expand Down Expand Up @@ -66,13 +66,13 @@ sealed class OptionalSnowflake {
* Represents a field that was assigned a non-null value in the serialized entity.
* Equality and hashcode is implemented through its [value].
*
* @param longValue the value this optional wraps.
* @param uLongValue the value this optional wraps.
*/
class Value(private val longValue: Long) : OptionalSnowflake() {
class Value(private val uLongValue: ULong) : OptionalSnowflake() {

constructor(value: Snowflake) : this(value.value)

override val value: Snowflake get() = Snowflake(longValue)
override val value: Snowflake get() = Snowflake(uLongValue)

/**
* Destructures this optional to its [value].
Expand All @@ -89,16 +89,17 @@ sealed class OptionalSnowflake {
override fun hashCode(): Int = value.hashCode()
}

@OptIn(ExperimentalSerializationApi::class)
internal object Serializer : KSerializer<OptionalSnowflake> {

override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("Kord.OptionalSnowflake", PrimitiveKind.LONG)
get() = @OptIn(ExperimentalUnsignedTypes::class) ULong.serializer().descriptor

override fun deserialize(decoder: Decoder): OptionalSnowflake = Value(decoder.decodeLong())
override fun deserialize(decoder: Decoder): OptionalSnowflake =
Value(decoder.decodeInline(descriptor).decodeLong().toULong())

override fun serialize(encoder: Encoder, value: OptionalSnowflake) = when (value) {
Missing -> Unit//ignore value
is Value -> encoder.encodeLong(value.value.value)
Missing -> Unit // ignore value
is Value -> encoder.encodeInline(descriptor).encodeLong(value.value.value.toLong())
}

}
Expand All @@ -122,4 +123,4 @@ fun Snowflake?.optionalSnowflake(): OptionalSnowflake.Value? = this?.optionalSno
inline fun <T : Any> OptionalSnowflake.map(mapper: (Snowflake) -> T): Optional<T> = when (this) {
OptionalSnowflake.Missing -> Optional.Missing()
is OptionalSnowflake.Value -> Optional.Value(mapper(value))
}
}
8 changes: 4 additions & 4 deletions common/src/test/kotlin/entity/SnowflakeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import kotlin.test.*
class SnowflakeTest {

@Test
fun `Snowflake with value 0 has timeStamp equal to discordEpochStart`() {
val snowflake = Snowflake(0)
fun `Snowflake with value ULong MIN_VALUE has timeStamp equal to discordEpochStart`() {
val snowflake = Snowflake(ULong.MIN_VALUE)
assertEquals(Snowflake.discordEpochStart, snowflake.timeStamp)
}

@Test
fun `Snowflake with value -1 has timeStamp equal to endOfTime`() {
val snowflake = Snowflake(-1)
fun `Snowflake with value ULong MAX_VALUE has timeStamp equal to endOfTime`() {
val snowflake = Snowflake(ULong.MAX_VALUE)
assertEquals(Snowflake.endOfTime, snowflake.timeStamp)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal class OptionalSnowflakeTest {
val entity = Json.decodeFromString<ValueOptionalEntity>(json)
require(entity.value is OptionalSnowflake.Value)

Assertions.assertEquals(Snowflake(5), entity.value.value)
Assertions.assertEquals(Snowflake(5u), entity.value.value)
}

}
6 changes: 3 additions & 3 deletions core/src/main/kotlin/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal fun String?.toSnowflakeOrNull(): Snowflake? = when {
else -> Snowflake(this)
}

internal fun Long?.toSnowflakeOrNull(): Snowflake? = when {
internal fun ULong?.toSnowflakeOrNull(): Snowflake? = when {
this == null -> null
else -> Snowflake(this)
}
Expand Down Expand Up @@ -215,7 +215,7 @@ internal fun <C : Collection<T>, T : KordEntity> paginateForwards(
* Selects the [Position.Before] the oldest item in the batch.
*/
internal fun <C : Collection<T>, T> paginateBackwards(
start: Snowflake = Snowflake(Long.MAX_VALUE),
start: Snowflake = Snowflake.max,
batchSize: Int,
idSelector: (T) -> Snowflake,
request: suspend (position: Position) -> C
Expand All @@ -226,7 +226,7 @@ internal fun <C : Collection<T>, T> paginateBackwards(
* Selects the [Position.Before] the oldest item in the batch.
*/
internal fun <C : Collection<T>, T : KordEntity> paginateBackwards(
start: Snowflake = Snowflake(Long.MAX_VALUE),
start: Snowflake = Snowflake.max,
batchSize: Int,
request: suspend (position: Position) -> C
): Flow<T> =
Expand Down
3 changes: 1 addition & 2 deletions core/src/main/kotlin/behavior/GuildBehavior.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package dev.kord.core.behavior
import dev.kord.cache.api.query
import dev.kord.common.annotation.DeprecatedSinceKord
import dev.kord.common.annotation.KordExperimental
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.DiscordUser
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.Optional
Expand Down Expand Up @@ -201,7 +200,7 @@ interface GuildBehavior : KordEntity, Strategizable {
*/
val gateway: Gateway?
get() {
val shard = id.value.shr(22) % kord.resources.shards.totalShards.coerceAtLeast(1)
val shard = id.value.shr(22).toLong() % kord.resources.shards.totalShards.coerceAtLeast(1)
return kord.gateway.gateways[shard.toInt()]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import dev.kord.rest.service.RestClient
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
Expand Down Expand Up @@ -67,7 +69,7 @@ interface MessageChannelBehavior : ChannelBehavior, Strategizable {
* The returned flow is lazily executed, any [RequestException] will be thrown on
* [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
*/
val messages: Flow<Message> get() = getMessagesAfter(Snowflake(0))
val messages: Flow<Message> get() = getMessagesAfter(Snowflake.min)

/**
* Requests to get the pinned messages in this channel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ interface PrivateThreadParentChannelBehavior : ThreadParentChannelBehavior {
* [terminal operators](https://kotlinlang.org/docs/reference/coroutines/flow.html#terminal-flow-operators) instead.
*/
fun getJoinedPrivateArchivedThreads(
before: Snowflake = Snowflake(Long.MAX_VALUE),
before: Snowflake = Snowflake.max,
limit: Int = Int.MAX_VALUE
): Flow<ThreadChannel> {
return supplier.getJoinedPrivateArchivedThreads(id, before, limit)
Expand Down
17 changes: 8 additions & 9 deletions core/src/test/kotlin/UtilKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dev.kord.core

import dev.kord.common.entity.Snowflake
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
Expand All @@ -13,13 +12,13 @@ internal class UtilKtTest {
@ExperimentalStdlibApi
fun `paginate forwards selects the right id`() = runBlockingTest {

val flow = paginateForwards(start = Snowflake(0), batchSize = 100, idSelector = { it }) {
val flow = paginateForwards(start = Snowflake(0u), batchSize = 100, idSelector = { it }) {
var value = it.value.value
if (value >= 1000) return@paginateForwards emptyList<Snowflake>()
value += 1 //don't include the position id
if (value >= 1000u) return@paginateForwards emptyList<Snowflake>()
value += 1u //don't include the position id

buildList(100) {
(value until (value + 100)).reversed().forEach { snowflake -> //biggest/youngest -> smallest/oldest
(value until (value + 100u)).reversed().forEach { snowflake -> //biggest/youngest -> smallest/oldest
add(Snowflake(snowflake))
}
}
Expand All @@ -32,13 +31,13 @@ internal class UtilKtTest {
@ExperimentalStdlibApi
fun `paginate backwards selects the right id`() = runBlockingTest {

val flow = paginateBackwards(start = Snowflake(1000), batchSize = 100, idSelector = { it }) {
val flow = paginateBackwards(start = Snowflake(1000u), batchSize = 100, idSelector = { it }) {
var value = it.value.value
if (value <= 0) return@paginateBackwards emptyList<Snowflake>()
value -= 1 //don't include the position id
if (value <= 0u) return@paginateBackwards emptyList<Snowflake>()
value -= 1u //don't include the position id

buildList(100) {
((value - 99 /*reverse until, don't count the lowest value*/)..value).reversed().forEach { snowflake -> //biggest/youngest -> smallest/oldest
((value - 99u /*reverse until, don't count the lowest value*/)..value).reversed().forEach { snowflake -> //biggest/youngest -> smallest/oldest
add(Snowflake(snowflake))
}
}
Expand Down
4 changes: 2 additions & 2 deletions core/src/test/kotlin/behavior/MessageBehaviorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import mockKord

internal class MessageBehaviorTest : EntityEqualityTest<MessageBehavior> by EntityEqualityTest({
val kord = mockKord()
MessageBehavior(it, Snowflake(0), kord)
})
MessageBehavior(it, Snowflake(0u), kord)
})
5 changes: 2 additions & 3 deletions core/src/test/kotlin/behavior/RoleBehaviorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dev.kord.core.behavior

import dev.kord.common.entity.Snowflake
import equality.GuildEntityEqualityTest
import io.mockk.mockk
import mockKord
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
Expand All @@ -16,10 +15,10 @@ internal class RoleBehaviorTest : GuildEntityEqualityTest<RoleBehavior> by Guild
fun `everyone role mention is properly formatted`(){
val kord = mockKord()

val id = Snowflake(1337)
val id = Snowflake(1337u)
val behavior = RoleBehavior(id, id, kord)

assertEquals("@everyone", behavior.mention)
}

}
}
8 changes: 4 additions & 4 deletions core/src/test/kotlin/entity/MemberTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ internal class MemberTest : GuildEntityEqualityTest<Member> by GuildEntityEquali
fun `members equal users with the same ID`() {
val kord = mockKord()
val memberData = mockk<MemberData>()
every { memberData.userId } returns Snowflake(0L)
every { memberData.guildId } returns Snowflake(1L)
every { memberData.userId } returns Snowflake(0u)
every { memberData.guildId } returns Snowflake(1u)

val userData = mockk<UserData>()
every { userData.id } returns Snowflake(0L)
every { userData.id } returns Snowflake(0u)
val member = Member(memberData, userData, kord)
val user = User(userData, kord)

Expand All @@ -42,4 +42,4 @@ internal class MemberTest : GuildEntityEqualityTest<Member> by GuildEntityEquali
}


}
}
5 changes: 3 additions & 2 deletions core/src/test/kotlin/equality/EntityEqualityTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package equality
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.KordEntity
import kotlin.random.Random
import kotlin.random.nextULong
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

val ids = generateSequence { Random.nextLong() }.distinct().iterator()
val ids = generateSequence { Random.nextULong() }.distinct().iterator()
fun randomId() = Snowflake(ids.next())

interface EntityEqualityTest<T : KordEntity> {
Expand Down Expand Up @@ -35,4 +36,4 @@ interface EntityEqualityTest<T : KordEntity> {
override fun newEntity(id: Snowflake): T = supplier(id)
}
}
}
}
Loading

0 comments on commit c889ff9

Please sign in to comment.