diff --git a/library/api/api.txt b/library/api/api.txt index 372c9d8..136c6a5 100644 --- a/library/api/api.txt +++ b/library/api/api.txt @@ -4,14 +4,18 @@ package me.saket.bytesize { @kotlin.jvm.JvmInline public final value class BinaryByteSize implements me.saket.bytesize.ByteSize { ctor public BinaryByteSize(Number bytes); ctor public BinaryByteSize(@kotlin.PublishedApi long bytes); + method public inline operator int compareTo(long other); method public inline int compareTo(me.saket.bytesize.ByteSize other); method public inline long div(Number other); + method public inline operator double div(long other); method public inline double div(me.saket.bytesize.ByteSize other); method public inline long inWholeBytes(); method public inline long inWholeGibibytes(); method public inline long inWholeKibibytes(); method public inline long inWholeMebibytes(); + method public inline operator long minus(long other); method public inline operator me.saket.bytesize.ByteSize minus(me.saket.bytesize.ByteSize other); + method public inline operator long plus(long other); method public inline operator long plus(me.saket.bytesize.ByteSize other); method public inline long times(Number other); method public inline String toString(); @@ -48,15 +52,19 @@ package me.saket.bytesize { @kotlin.jvm.JvmInline public final value class DecimalBitSize implements me.saket.bytesize.ByteSize { ctor public DecimalBitSize(Number bits); ctor public DecimalBitSize(@kotlin.PublishedApi long bits); + method public inline operator int compareTo(long other); method public inline int compareTo(me.saket.bytesize.ByteSize other); method public inline long div(Number other); + method public inline operator double div(long other); method public inline double div(me.saket.bytesize.ByteSize other); method public inline long inWholeBits(); method public inline long inWholeBytes(); method public inline long inWholeGigabits(); method public inline long inWholeKilobits(); method public inline long inWholeMegabits(); + method public inline operator long minus(long other); method public inline long minus(me.saket.bytesize.ByteSize other); + method public inline operator long plus(long other); method public inline long plus(me.saket.bytesize.ByteSize other); method public inline long times(Number other); method public inline String toString(); @@ -75,14 +83,18 @@ package me.saket.bytesize { @kotlin.jvm.JvmInline public final value class DecimalByteSize implements me.saket.bytesize.ByteSize { ctor public DecimalByteSize(Number bytes); ctor public DecimalByteSize(@kotlin.PublishedApi long bytes); + method public inline operator int compareTo(long other); method public inline int compareTo(me.saket.bytesize.ByteSize other); method public inline long div(Number other); + method public inline operator double div(long other); method public inline double div(me.saket.bytesize.ByteSize other); method public inline long inWholeBytes(); method public inline long inWholeGigabytes(); method public inline long inWholeKilobytes(); method public inline long inWholeMegabytes(); + method public inline operator long minus(long other); method public inline operator long minus(me.saket.bytesize.ByteSize other); + method public inline operator long plus(long other); method public inline operator long plus(me.saket.bytesize.ByteSize other); method public inline long times(Number other); method public inline String toString(); diff --git a/library/src/commonMain/kotlin/me/saket/bytesize/BinaryByteSize.kt b/library/src/commonMain/kotlin/me/saket/bytesize/BinaryByteSize.kt index bbdf1d1..057a8eb 100644 --- a/library/src/commonMain/kotlin/me/saket/bytesize/BinaryByteSize.kt +++ b/library/src/commonMain/kotlin/me/saket/bytesize/BinaryByteSize.kt @@ -2,6 +2,9 @@ package me.saket.bytesize +import dev.erikchristensen.javamath2kmp.minusExact +import dev.erikchristensen.javamath2kmp.plusExact +import dev.erikchristensen.javamath2kmp.timesExact import kotlin.jvm.JvmInline import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic @@ -24,6 +27,16 @@ import me.saket.bytesize.internal.toStringAsFixed inline val Number.binaryBytes: BinaryByteSize get() = BinaryByteSize(this) +/** Allocation-free overload of [binaryBytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.binaryBytes: BinaryByteSize + get() = BinaryByteSize(bytes = toLong()) + +/** Allocation-free overload of [binaryBytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.binaryBytes: BinaryByteSize + get() = BinaryByteSize(bytes = this) + /** * Returns a [BinaryByteSize] equal to this number of kibibytes. * @@ -33,6 +46,16 @@ inline val Number.binaryBytes: BinaryByteSize inline val Number.kibibytes: BinaryByteSize get() = BinaryByteSize(BytesPerKiB) * this +/** Allocation-free overload of [kibibytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.kibibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerKiB.timesExact(toLong())) + +/** Allocation-free overload of [kibibytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.kibibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerKiB.timesExact(this)) + /** * Returns a [BinaryByteSize] equal to this number of mebibytes. * @@ -42,6 +65,16 @@ inline val Number.kibibytes: BinaryByteSize inline val Number.mebibytes: BinaryByteSize get() = BinaryByteSize(BytesPerMiB) * this +/** Allocation-free overload of [mebibytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.mebibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerMiB.timesExact(toLong())) + +/** Allocation-free overload of [mebibytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.mebibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerMiB.timesExact(this)) + /** * Returns a [BinaryByteSize] equal to this number of gibibytes. * @@ -51,6 +84,16 @@ inline val Number.mebibytes: BinaryByteSize inline val Number.gibibytes: BinaryByteSize get() = BinaryByteSize(BytesPerGiB) * this +/** Allocation-free overload of [gibibytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.gibibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerGiB.timesExact(toLong())) + +/** Allocation-free overload of [gibibytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.gibibytes: BinaryByteSize + get() = BinaryByteSize(bytes = BytesPerGiB.timesExact(this)) + /** Returns this size without its sign. */ @get:JvmSynthetic inline val BinaryByteSize.absoluteValue: BinaryByteSize @@ -91,15 +134,27 @@ value class BinaryByteSize( override inline operator fun plus(other: ByteSize): BinaryByteSize = BinaryByteSize(commonPlus(other)) + /** Allocation-free overload of [plus] for same-precision operands. */ + inline operator fun plus(other: BinaryByteSize): BinaryByteSize = + BinaryByteSize(bytes.plusExact(other.bytes)) + override inline operator fun minus(other: ByteSize): ByteSize = BinaryByteSize(commonMinus(other)) + /** Allocation-free overload of [minus] for same-precision operands. */ + inline operator fun minus(other: BinaryByteSize): BinaryByteSize = + BinaryByteSize(bytes.minusExact(other.bytes)) + override inline fun times(other: Number): BinaryByteSize = BinaryByteSize(commonTimes(other)) override inline fun div(other: ByteSize): Double = commonDiv(other) + /** Allocation-free overload of [div] for same-precision operands. */ + inline operator fun div(other: BinaryByteSize): Double = + bytes.toDouble() / other.bytes + override inline fun div(other: Number): BinaryByteSize = BinaryByteSize(commonDiv(other)) @@ -109,6 +164,10 @@ value class BinaryByteSize( override inline fun compareTo(other: ByteSize): Int = commonCompareTo(other) + /** Allocation-free overload of [compareTo] for same-precision operands. */ + inline operator fun compareTo(other: BinaryByteSize): Int = + bytes.compareTo(other.bytes) + override inline fun toString(): String { val sign = if (inWholeBytes < 0) "-" else "" val bytes = abs(inWholeBytes) diff --git a/library/src/commonMain/kotlin/me/saket/bytesize/ByteSize.kt b/library/src/commonMain/kotlin/me/saket/bytesize/ByteSize.kt index 85f681c..7df1a42 100644 --- a/library/src/commonMain/kotlin/me/saket/bytesize/ByteSize.kt +++ b/library/src/commonMain/kotlin/me/saket/bytesize/ByteSize.kt @@ -9,6 +9,12 @@ import me.saket.bytesize.internal.BitsPerByte * Represents a size that can be expressed in bytes. * * A [ByteSize] is one of [BinaryByteSize], [DecimalByteSize], or [DecimalBitSize]. + * + * The operators declared here accept this interface so that sizes of different precisions can be + * mixed. That comes at a cost: a `value class` is only kept unboxed while its static type _is_ the + * value class, so passing one as a [ByteSize] boxes it. Each subtype therefore also declares + * same-precision overloads of these operators, which the compiler prefers when both operands have + * the same concrete type. Those overloads read the backing `Long` directly and allocate nothing. */ sealed interface ByteSize : Comparable { @get:JvmName("inWholeBytes") diff --git a/library/src/commonMain/kotlin/me/saket/bytesize/DecimalBitSize.kt b/library/src/commonMain/kotlin/me/saket/bytesize/DecimalBitSize.kt index 735c4a5..591b381 100644 --- a/library/src/commonMain/kotlin/me/saket/bytesize/DecimalBitSize.kt +++ b/library/src/commonMain/kotlin/me/saket/bytesize/DecimalBitSize.kt @@ -2,6 +2,9 @@ package me.saket.bytesize +import dev.erikchristensen.javamath2kmp.minusExact +import dev.erikchristensen.javamath2kmp.plusExact +import dev.erikchristensen.javamath2kmp.timesExact import kotlin.jvm.JvmInline import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic @@ -25,6 +28,16 @@ import me.saket.bytesize.internal.toStringAsFixed inline val Number.decimalBits: DecimalBitSize get() = DecimalBitSize(this) +/** Allocation-free overload of [decimalBits] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.decimalBits: DecimalBitSize + get() = DecimalBitSize(bits = toLong()) + +/** Allocation-free overload of [decimalBits] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.decimalBits: DecimalBitSize + get() = DecimalBitSize(bits = this) + /** * Returns a [DecimalBitSize] equal to this number of kilobits. * @@ -34,6 +47,16 @@ inline val Number.decimalBits: DecimalBitSize inline val Number.kilobits: DecimalBitSize get() = DecimalBitSize(BitsPerKb) * this +/** Allocation-free overload of [kilobits] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.kilobits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerKb.timesExact(toLong())) + +/** Allocation-free overload of [kilobits] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.kilobits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerKb.timesExact(this)) + /** * Returns a [DecimalBitSize] equal to this number of megabits. * @@ -43,6 +66,16 @@ inline val Number.kilobits: DecimalBitSize inline val Number.megabits: DecimalBitSize get() = DecimalBitSize(BitsPerMb) * this +/** Allocation-free overload of [megabits] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.megabits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerMb.timesExact(toLong())) + +/** Allocation-free overload of [megabits] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.megabits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerMb.timesExact(this)) + /** * Returns a [DecimalBitSize] equal to this number of gigabits. * @@ -52,6 +85,16 @@ inline val Number.megabits: DecimalBitSize inline val Number.gigabits: DecimalBitSize get() = DecimalBitSize(BitsPerGb) * this +/** Allocation-free overload of [gigabits] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.gigabits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerGb.timesExact(toLong())) + +/** Allocation-free overload of [gigabits] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.gigabits: DecimalBitSize + get() = DecimalBitSize(bits = BitsPerGb.timesExact(this)) + /** Returns this size without its sign. */ @get:JvmSynthetic inline val DecimalBitSize.absoluteValue: DecimalBitSize @@ -98,10 +141,20 @@ value class DecimalBitSize( return DecimalBitSize(bits = commonPlus(other)) } + /** Allocation-free overload of [plus] for same-precision operands. */ + inline operator fun plus(other: DecimalBitSize): DecimalBitSize { + return DecimalBitSize(bits = bits.plusExact(other.bits)) + } + override inline fun minus(other: ByteSize): DecimalBitSize { return DecimalBitSize(bits = commonMinus(other)) } + /** Allocation-free overload of [minus] for same-precision operands. */ + inline operator fun minus(other: DecimalBitSize): DecimalBitSize { + return DecimalBitSize(bits = bits.minusExact(other.bits)) + } + override inline fun times(other: Number): DecimalBitSize { return DecimalBitSize(bits = commonTimes(other)) } @@ -110,6 +163,11 @@ value class DecimalBitSize( return commonDiv(other) } + /** Allocation-free overload of [div] for same-precision operands. */ + inline operator fun div(other: DecimalBitSize): Double { + return bits.toDouble() / other.bits + } + override inline fun div(other: Number): DecimalBitSize { return DecimalBitSize(bits = commonDiv(other)) } @@ -121,6 +179,11 @@ value class DecimalBitSize( return commonCompareTo(other) } + /** Allocation-free overload of [compareTo] for same-precision operands. */ + inline operator fun compareTo(other: DecimalBitSize): Int { + return bits.compareTo(other.bits) + } + override inline fun toString(): String { val sign = if (inWholeBits < 0) "-" else "" val bits = abs(inWholeBits) diff --git a/library/src/commonMain/kotlin/me/saket/bytesize/DecimalByteSize.kt b/library/src/commonMain/kotlin/me/saket/bytesize/DecimalByteSize.kt index 0f8e303..0eb6ba7 100644 --- a/library/src/commonMain/kotlin/me/saket/bytesize/DecimalByteSize.kt +++ b/library/src/commonMain/kotlin/me/saket/bytesize/DecimalByteSize.kt @@ -2,6 +2,9 @@ package me.saket.bytesize +import dev.erikchristensen.javamath2kmp.minusExact +import dev.erikchristensen.javamath2kmp.plusExact +import dev.erikchristensen.javamath2kmp.timesExact import kotlin.jvm.JvmInline import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic @@ -24,6 +27,16 @@ import me.saket.bytesize.internal.toStringAsFixed inline val Number.decimalBytes: DecimalByteSize get() = DecimalByteSize(this) +/** Allocation-free overload of [decimalBytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.decimalBytes: DecimalByteSize + get() = DecimalByteSize(bytes = toLong()) + +/** Allocation-free overload of [decimalBytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.decimalBytes: DecimalByteSize + get() = DecimalByteSize(bytes = this) + /** * Returns a [DecimalByteSize] equal to this number of kilobytes. * @@ -33,6 +46,16 @@ inline val Number.decimalBytes: DecimalByteSize inline val Number.kilobytes: DecimalByteSize get() = DecimalByteSize(BytesPerKB) * this +/** Allocation-free overload of [kilobytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.kilobytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerKB.timesExact(toLong())) + +/** Allocation-free overload of [kilobytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.kilobytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerKB.timesExact(this)) + /** * Returns a [DecimalByteSize] equal to this number of megabytes. * @@ -42,6 +65,16 @@ inline val Number.kilobytes: DecimalByteSize inline val Number.megabytes: DecimalByteSize get() = DecimalByteSize(BytesPerMB) * this +/** Allocation-free overload of [megabytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.megabytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerMB.timesExact(toLong())) + +/** Allocation-free overload of [megabytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.megabytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerMB.timesExact(this)) + /** * Returns a [DecimalByteSize] equal to this number of gigabytes. * @@ -51,6 +84,16 @@ inline val Number.megabytes: DecimalByteSize inline val Number.gigabytes: DecimalByteSize get() = DecimalByteSize(BytesPerGB) * this +/** Allocation-free overload of [gigabytes] for [Int] receivers. */ +@get:JvmSynthetic +inline val Int.gigabytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerGB.timesExact(toLong())) + +/** Allocation-free overload of [gigabytes] for [Long] receivers. */ +@get:JvmSynthetic +inline val Long.gigabytes: DecimalByteSize + get() = DecimalByteSize(bytes = BytesPerGB.timesExact(this)) + /** Returns this size without its sign. */ @get:JvmSynthetic inline val DecimalByteSize.absoluteValue: DecimalByteSize @@ -91,15 +134,27 @@ value class DecimalByteSize( override inline operator fun plus(other: ByteSize): DecimalByteSize = DecimalByteSize(commonPlus(other)) + /** Allocation-free overload of [plus] for same-precision operands. */ + inline operator fun plus(other: DecimalByteSize): DecimalByteSize = + DecimalByteSize(bytes.plusExact(other.bytes)) + override inline operator fun minus(other: ByteSize): DecimalByteSize = DecimalByteSize(commonMinus(other)) + /** Allocation-free overload of [minus] for same-precision operands. */ + inline operator fun minus(other: DecimalByteSize): DecimalByteSize = + DecimalByteSize(bytes.minusExact(other.bytes)) + override inline fun times(other: Number): DecimalByteSize = DecimalByteSize(commonTimes(other)) override inline fun div(other: ByteSize): Double = commonDiv(other) + /** Allocation-free overload of [div] for same-precision operands. */ + inline operator fun div(other: DecimalByteSize): Double = + bytes.toDouble() / other.bytes + override inline fun div(other: Number): DecimalByteSize = DecimalByteSize(commonDiv(other)) @@ -109,6 +164,10 @@ value class DecimalByteSize( override inline fun compareTo(other: ByteSize): Int = commonCompareTo(other) + /** Allocation-free overload of [compareTo] for same-precision operands. */ + inline operator fun compareTo(other: DecimalByteSize): Int = + bytes.compareTo(other.bytes) + override inline fun toString(): String { val sign = if (inWholeBytes < 0) "-" else "" val bytes = abs(inWholeBytes) diff --git a/library/src/commonTest/kotlin/me/saket/bytesize/BinaryByteSizeTest.kt b/library/src/commonTest/kotlin/me/saket/bytesize/BinaryByteSizeTest.kt index 90b644b..6ff8722 100644 --- a/library/src/commonTest/kotlin/me/saket/bytesize/BinaryByteSizeTest.kt +++ b/library/src/commonTest/kotlin/me/saket/bytesize/BinaryByteSizeTest.kt @@ -130,4 +130,34 @@ class BinaryByteSizeTest { val positiveTwelve = -negativeTwelve assertThat(positiveTwelve).isEqualTo(twelve) } + + @Test fun maths_with_same_units() { + // Resolves to the same-precision operator overloads, which allocate nothing. + // See AllocationFreeCallSitesTest. + assertThat(3.mebibytes + 512.kibibytes).isEqualTo(3.5.mebibytes) + assertThat(7.gibibytes - 512.mebibytes).isEqualTo(6.5.gibibytes) + assertThat(5.mebibytes - 5.mebibytes).isEqualTo(0.binaryBytes) + assertThat(1.mebibytes / 2.binaryBytes).isEqualTo(524_288.0) + + assertThat(3.mebibytes + 1.mebibytes).isInstanceOf() + assertThat(3.mebibytes - 1.mebibytes).isInstanceOf() + } + + @Test fun comparison_with_same_units() { + assertThat(2.mebibytes > 1.mebibytes).isTrue() + assertThat(1.mebibytes >= 1.mebibytes).isTrue() + assertThat(1.mebibytes < 2.mebibytes).isTrue() + assertThat(2.mebibytes < 1.mebibytes).isFalse() + } + + @Test fun throw_an_error_if_same_unit_addition_or_subtraction_will_cause_an_overflow() { + assertFailure { + BinaryByteSize(Long.MAX_VALUE) + 1.binaryBytes + }.isInstanceOf() + + assertFailure { + BinaryByteSize(Long.MIN_VALUE) - 1.binaryBytes + }.isInstanceOf() + } + } diff --git a/library/src/commonTest/kotlin/me/saket/bytesize/ByteSizeTest.kt b/library/src/commonTest/kotlin/me/saket/bytesize/ByteSizeTest.kt index c895c49..d9bcd4f 100644 --- a/library/src/commonTest/kotlin/me/saket/bytesize/ByteSizeTest.kt +++ b/library/src/commonTest/kotlin/me/saket/bytesize/ByteSizeTest.kt @@ -1,5 +1,6 @@ package me.saket.bytesize +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf @@ -20,4 +21,50 @@ class ByteSizeTest { assertThat(value.absoluteValue).isEqualTo(expected) } } + + @Test fun int_and_long_receivers_agree_with_number_receivers() { + // The Int and Long overloads of these properties exist only to keep the receiver unboxed, so + // they must produce exactly what the Number overload they shadow would have produced. + // See AllocationFreeCallSitesTest. + for (n in listOf(0, 1, -1, 7, 345, 1_000_000, -1_636_186_211)) { + val number: Number = n + val long: Long = n.toLong() + + assertThat(n.decimalBytes).isEqualTo(number.decimalBytes) + assertThat(n.kilobytes).isEqualTo(number.kilobytes) + assertThat(n.megabytes).isEqualTo(number.megabytes) + assertThat(n.gigabytes).isEqualTo(number.gigabytes) + + assertThat(n.binaryBytes).isEqualTo(number.binaryBytes) + assertThat(n.kibibytes).isEqualTo(number.kibibytes) + assertThat(n.mebibytes).isEqualTo(number.mebibytes) + assertThat(n.gibibytes).isEqualTo(number.gibibytes) + + assertThat(n.decimalBits).isEqualTo(number.decimalBits) + assertThat(n.kilobits).isEqualTo(number.kilobits) + assertThat(n.megabits).isEqualTo(number.megabits) + assertThat(n.gigabits).isEqualTo(number.gigabits) + + assertThat(long.decimalBytes).isEqualTo(number.decimalBytes) + assertThat(long.kilobytes).isEqualTo(number.kilobytes) + assertThat(long.megabytes).isEqualTo(number.megabytes) + assertThat(long.gigabytes).isEqualTo(number.gigabytes) + + assertThat(long.binaryBytes).isEqualTo(number.binaryBytes) + assertThat(long.kibibytes).isEqualTo(number.kibibytes) + assertThat(long.mebibytes).isEqualTo(number.mebibytes) + assertThat(long.gibibytes).isEqualTo(number.gibibytes) + + assertThat(long.decimalBits).isEqualTo(number.decimalBits) + assertThat(long.kilobits).isEqualTo(number.kilobits) + assertThat(long.megabits).isEqualTo(number.megabits) + assertThat(long.gigabits).isEqualTo(number.gigabits) + } + } + + @Test fun int_and_long_receivers_reject_overflowing_unit_multiples() { + assertFailure { Long.MAX_VALUE.kilobytes }.isInstanceOf() + assertFailure { Long.MAX_VALUE.gibibytes }.isInstanceOf() + assertFailure { Long.MIN_VALUE.megabits }.isInstanceOf() + } } diff --git a/library/src/commonTest/kotlin/me/saket/bytesize/DecimalBitSizeTest.kt b/library/src/commonTest/kotlin/me/saket/bytesize/DecimalBitSizeTest.kt index ec485fa..423a07d 100644 --- a/library/src/commonTest/kotlin/me/saket/bytesize/DecimalBitSizeTest.kt +++ b/library/src/commonTest/kotlin/me/saket/bytesize/DecimalBitSizeTest.kt @@ -128,4 +128,34 @@ class DecimalBitSizeTest { val positiveTwelve = -negativeTwelve assertThat(positiveTwelve).isEqualTo(twelve) } + + @Test fun maths_with_same_units() { + // Resolves to the same-precision operator overloads, which allocate nothing. + // See AllocationFreeCallSitesTest. + assertThat(3.megabits + 200.kilobits).isEqualTo(3.2.megabits) + assertThat(7.gigabits - 500.megabits).isEqualTo(6_500.megabits) + assertThat(5.megabits - 5.megabits).isEqualTo(0.decimalBits) + assertThat(1.megabits / 2.decimalBits).isEqualTo(500_000.0) + + assertThat(3.megabits + 1.megabits).isInstanceOf() + assertThat(3.megabits - 1.megabits).isInstanceOf() + } + + @Test fun comparison_with_same_units() { + assertThat(2.megabits > 1.megabits).isTrue() + assertThat(1.megabits >= 1.megabits).isTrue() + assertThat(1.megabits < 2.megabits).isTrue() + assertThat(2.megabits < 1.megabits).isFalse() + } + + @Test fun throw_an_error_if_same_unit_addition_or_subtraction_will_cause_an_overflow() { + assertFailure { + DecimalBitSize(Long.MAX_VALUE) + 1.decimalBits + }.isInstanceOf() + + assertFailure { + DecimalBitSize(Long.MIN_VALUE) - 1.decimalBits + }.isInstanceOf() + } + } diff --git a/library/src/commonTest/kotlin/me/saket/bytesize/DecimalByteSizeTest.kt b/library/src/commonTest/kotlin/me/saket/bytesize/DecimalByteSizeTest.kt index c22d0e9..ac87d13 100644 --- a/library/src/commonTest/kotlin/me/saket/bytesize/DecimalByteSizeTest.kt +++ b/library/src/commonTest/kotlin/me/saket/bytesize/DecimalByteSizeTest.kt @@ -175,4 +175,34 @@ class DecimalByteSizeTest { val positiveTwelve = -negativeTwelve assertThat(positiveTwelve).isEqualTo(twelve) } + + @Test fun maths_with_same_units() { + // Resolves to the same-precision operator overloads, which allocate nothing. + // See AllocationFreeCallSitesTest. + assertThat(3.megabytes + 200.kilobytes).isEqualTo(3.2.megabytes) + assertThat(7.gigabytes - 500.megabytes).isEqualTo(6_500.megabytes) + assertThat(5.megabytes - 5.megabytes).isEqualTo(0.decimalBytes) + assertThat(1.megabytes / 2.decimalBytes).isEqualTo(500_000.0) + + assertThat(3.megabytes + 1.megabytes).isInstanceOf() + assertThat(3.megabytes - 1.megabytes).isInstanceOf() + } + + @Test fun comparison_with_same_units() { + assertThat(2.megabytes > 1.megabytes).isTrue() + assertThat(1.megabytes >= 1.megabytes).isTrue() + assertThat(1.megabytes < 2.megabytes).isTrue() + assertThat(2.megabytes < 1.megabytes).isFalse() + } + + @Test fun throw_an_error_if_same_unit_addition_or_subtraction_will_cause_an_overflow() { + assertFailure { + DecimalByteSize(Long.MAX_VALUE) + 1.decimalBytes + }.isInstanceOf() + + assertFailure { + DecimalByteSize(Long.MIN_VALUE) - 1.decimalBytes + }.isInstanceOf() + } + } diff --git a/library/src/jvmTest/kotlin/me/saket/bytesize/AllocationFreeCallSitesTest.kt b/library/src/jvmTest/kotlin/me/saket/bytesize/AllocationFreeCallSitesTest.kt new file mode 100644 index 0000000..a79f04e --- /dev/null +++ b/library/src/jvmTest/kotlin/me/saket/bytesize/AllocationFreeCallSitesTest.kt @@ -0,0 +1,103 @@ +package me.saket.bytesize + +import assertk.assertThat +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import kotlin.test.Test + +/** + * A `value class` is only kept unboxed while its static type _is_ the value class. Every operator + * declared on [ByteSize] necessarily accepts the sealed interface, so passing a size to one boxes + * it — and the internal helpers those operators delegate to are extensions on an interface too, so + * the receiver boxes as well. Two allocations to perform one addition. + * + * The same-precision operator overloads and the [Int]/[Long] receiver overloads of the unit + * properties exist to avoid that. This test guards them by checking compiled call sites for + * `box-impl`, the static factory Kotlin generates to box a value class. That name is written to a + * class file's constant pool if and only if the class boxes a value class somewhere, so its + * absence is proof that a call site allocates nothing. + * + * See https://github.com/saket/byte-size/issues/13 for the related companion object problem. + */ +class AllocationFreeCallSitesTest { + + @Test fun same_precision_operators_and_primitive_receivers_do_not_box() { + assertThat(boxesAValueClass()).isFalse() + } + + /** + * Guards the test above: if boxing detection ever stops working, this fails too rather than + * letting [AllocationFree] pass vacuously. Mixing precisions has to go through [ByteSize], so + * these call sites are expected to box. + */ + @Test fun mixing_precisions_still_boxes() { + assertThat(boxesAValueClass()).isTrue() + } + + private inline fun boxesAValueClass(): Boolean { + val resource = T::class.java.name.replace('.', '/') + ".class" + val bytecode = checkNotNull(T::class.java.classLoader.getResourceAsStream(resource)) { + "Cannot find $resource on the test classpath" + }.use { it.readBytes() } + return bytecode.containsUtf8("box-impl") + } + + private fun ByteArray.containsUtf8(text: String): Boolean { + val needle = text.encodeToByteArray() + return (0..size - needle.size).any { start -> + needle.indices.all { this[start + it] == needle[it] } + } + } + + /** + * Call sites that must compile down to primitive arithmetic. Deliberately free of assertions and + * of anything else that could box a value class, because the check is on the whole class file. + */ + @Suppress("unused") + object AllocationFree { + // The pattern that motivated these overloads: accumulate sizes over a hot loop. + fun sumDecimal(sizes: LongArray): DecimalByteSize { + var total = DecimalByteSize(0L) + for (size in sizes) total += size.decimalBytes + return total + } + + fun sumBinary(sizes: IntArray): BinaryByteSize { + var total = BinaryByteSize(0L) + for (size in sizes) total += size.binaryBytes + return total + } + + fun sumBits(sizes: IntArray): DecimalBitSize { + var total = DecimalBitSize(0L) + for (size in sizes) total += size.decimalBits + return total + } + + fun decimalOperators(a: DecimalByteSize, b: DecimalByteSize): Double = + if (a > b) (a - b) / b else (a + b) / b + + fun binaryOperators(a: BinaryByteSize, b: BinaryByteSize): Double = + if (a > b) (a - b) / b else (a + b) / b + + fun bitOperators(a: DecimalBitSize, b: DecimalBitSize): Double = + if (a > b) (a - b) / b else (a + b) / b + + fun fromIntReceivers(n: Int): Long = + n.decimalBytes.inWholeBytes + n.kilobytes.inWholeBytes + n.megabytes.inWholeBytes + + n.gigabytes.inWholeBytes + n.binaryBytes.inWholeBytes + n.kibibytes.inWholeBytes + + n.mebibytes.inWholeBytes + n.gibibytes.inWholeBytes + n.decimalBits.inWholeBits + + n.kilobits.inWholeBits + n.megabits.inWholeBits + n.gigabits.inWholeBits + + fun fromLongReceivers(n: Long): Long = + n.decimalBytes.inWholeBytes + n.kilobytes.inWholeBytes + n.megabytes.inWholeBytes + + n.gigabytes.inWholeBytes + n.binaryBytes.inWholeBytes + n.kibibytes.inWholeBytes + + n.mebibytes.inWholeBytes + n.gibibytes.inWholeBytes + n.decimalBits.inWholeBits + + n.kilobits.inWholeBits + n.megabits.inWholeBits + n.gigabits.inWholeBits + } + + @Suppress("unused") + object MixedPrecision { + fun add(a: DecimalByteSize, b: BinaryByteSize): ByteSize = a + b + } +}