From 68bb1da3d3221773314c7e01d1759d96138242ea Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:20:09 +0000 Subject: [PATCH 01/12] Update InstantWithDuration --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index 37a4b67..7c29cee 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -80,15 +80,17 @@ object DateTimeKit { } -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonValue import klib.base.ShortString.asShortString import klib.base.ShortString.shortStringAsLong + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonValue import java.time.Duration import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import kotlin.math.pow -private val dateTimeFormatterUTC = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC) @Suppress("MagicNumber") object ShortString { @@ -101,7 +103,7 @@ object ShortString { value class InstantWithDuration(private val packedValue: Long) { init { - require(packedValue >= 0) { "PackedValue must be non-negative" } +// require(packedValue >= 0) { "PackedValue must be non-negative" } require(packedValue shr (63 - BITS_FOR_DURATION) == 0L) { "StartEpochSeconds out of range" } require(durationMinutes <= MAX_DURATION_MINUTES) { "Duration must be at most $MAX_DURATION_MINUTES" } } @@ -109,33 +111,18 @@ value class InstantWithDuration(private val packedValue: Long) { val startEpochSeconds: Long get() = EPOCH_2020 + (packedValue shr BITS_FOR_DURATION) - val startInstant - get() = Instant.ofEpochSecond(startEpochSeconds) - val durationMinutes: UShort get() = (packedValue and DURATION_MASK).toUShort() - val duration - get() = Duration.ofMinutes(durationMinutes.toLong()) - - val endEpochSeconds: Long - get() = startEpochSeconds + durationMinutes.toLong() * 60 - - val endInstant - get() = Instant.ofEpochSecond(endEpochSeconds) - - val startFormatted get() = dateTimeFormatterUTC.format(startInstant) - val endFormatted get() = dateTimeFormatterUTC.format(endInstant) - - override fun toString() = "${durationMinutes}m @ $startFormatted" - @get:JsonValue val asShortString get() = packedValue.asShortString + override fun toString() = "${durationMinutes}m @ $startFormatted" + companion object { const val EPOCH_2020 = 1577836800L const val BITS_FOR_DURATION = 11 - val MAX_DURATION_MINUTES: UShort get() = (2.0.pow(BITS_FOR_DURATION).toUInt() - 1u).toUShort() + val MAX_DURATION_MINUTES: UShort = (2.0.pow(BITS_FOR_DURATION).toUInt() - 1u).toUShort() const val DURATION_MASK = (1L shl BITS_FOR_DURATION) - 1 const val MAX_SECONDS = (1L shl (63 - BITS_FOR_DURATION)) - 1 @@ -143,6 +130,9 @@ value class InstantWithDuration(private val packedValue: Long) { @JvmStatic fun fromShortString(value: String) = InstantWithDuration(value.shortStringAsLong) + fun fromStartAndEnd(start: String, end: String) = + fromStartAndEnd(Instant.parse(start), Instant.parse(end)) + fun fromStartAndEnd(start: Instant, end: Instant): InstantWithDuration { require(end >= start) { "End time must not be before start time" } val durationMinutes = Duration.between(start, end).toMinutes().toUShort() @@ -162,3 +152,22 @@ value class InstantWithDuration(private val packedValue: Long) { } } } + +val InstantWithDuration.startInstant + get() = Instant.ofEpochSecond(startEpochSeconds) + +val InstantWithDuration.duration + get() = Duration.ofMinutes(durationMinutes.toLong()) + +@Suppress("MagicNumber") +val InstantWithDuration.endEpochSeconds: Long + get() = startEpochSeconds + durationMinutes.toLong() * 60 + +val InstantWithDuration.endInstant + get() = Instant.ofEpochSecond(endEpochSeconds) + +private val dateTimeFormatterUTC = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") + .withZone(ZoneOffset.UTC) + +val InstantWithDuration.startFormatted get() = dateTimeFormatterUTC.format(startInstant) +val InstantWithDuration.endFormatted get() = dateTimeFormatterUTC.format(endInstant) From bb320fb332e73fa962dc6e680e9a750c2115cfee Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:17:08 +0000 Subject: [PATCH 02/12] Update InstantWithDuration --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index 7c29cee..4b81fd0 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -89,7 +89,6 @@ import java.time.Duration import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter -import kotlin.math.pow @Suppress("MagicNumber") @@ -100,10 +99,9 @@ object ShortString { @Suppress("MagicNumber") @JvmInline -value class InstantWithDuration(private val packedValue: Long) { +value class InstantWithDuration(internal val packedValue: Long) : Comparable { init { -// require(packedValue >= 0) { "PackedValue must be non-negative" } require(packedValue shr (63 - BITS_FOR_DURATION) == 0L) { "StartEpochSeconds out of range" } require(durationMinutes <= MAX_DURATION_MINUTES) { "Duration must be at most $MAX_DURATION_MINUTES" } } @@ -112,19 +110,21 @@ value class InstantWithDuration(private val packedValue: Long) { get() = EPOCH_2020 + (packedValue shr BITS_FOR_DURATION) val durationMinutes: UShort - get() = (packedValue and DURATION_MASK).toUShort() + get() = (packedValue and DURATION_MINUTES_MASK).toUShort() @get:JsonValue val asShortString get() = packedValue.asShortString override fun toString() = "${durationMinutes}m @ $startFormatted" + override fun compareTo(other: InstantWithDuration): Int = packedValue.compareTo(other.packedValue) + companion object { const val EPOCH_2020 = 1577836800L const val BITS_FOR_DURATION = 11 - val MAX_DURATION_MINUTES: UShort = (2.0.pow(BITS_FOR_DURATION).toUInt() - 1u).toUShort() - const val DURATION_MASK = (1L shl BITS_FOR_DURATION) - 1 - const val MAX_SECONDS = (1L shl (63 - BITS_FOR_DURATION)) - 1 + const val DURATION_MINUTES_MASK: Long = (1L shl BITS_FOR_DURATION) - 1 + val MAX_DURATION_MINUTES: UShort = DURATION_MINUTES_MASK.toUShort() + const val INSTANT_SECONDS_MASK = (1L shl (63 - BITS_FOR_DURATION)) - 1 @JsonCreator @JvmStatic @@ -146,8 +146,14 @@ value class InstantWithDuration(private val packedValue: Long) { fromStartAndDuration(start.epochSecond, durationMinutes) fun fromStartAndDuration(startEpochSeconds: Long, durationMinutes: UShort = 0u): InstantWithDuration { + require(durationMinutes <= MAX_DURATION_MINUTES) { + "Max duration minutes exceeded by ${durationMinutes - MAX_DURATION_MINUTES}" + } + require(startEpochSeconds >= EPOCH_2020) { + "Min startEpochSeconds should be increased by ${EPOCH_2020 - startEpochSeconds}" + } return InstantWithDuration( - ((startEpochSeconds - EPOCH_2020).toInt().toLong() shl BITS_FOR_DURATION) or durationMinutes.toLong() + ((startEpochSeconds - EPOCH_2020) shl BITS_FOR_DURATION) or durationMinutes.toLong() ) } } From 9282bfdc8fcf54baaa06fd531f797be6b6631287 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:19:17 +0000 Subject: [PATCH 03/12] Create InstantWithDurationTest.kt --- .../klib/base/InstantWithDurationTest.kt | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 base/src/test/kotlin/klib/base/InstantWithDurationTest.kt diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt new file mode 100644 index 0000000..c74da69 --- /dev/null +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -0,0 +1,134 @@ +package klib.base + +import klib.base.InstantWithDuration.Companion.DURATION_MINUTES_MASK +import klib.base.InstantWithDuration.Companion.EPOCH_2020 +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import kotlin.test.assertEquals + +class InstantWithDurationTest { + + @Test + fun `constructor with startEpochSeconds and durationMinutes creates correct packedValue`() { + val instantWithDuration = InstantWithDuration + .fromStartAndDuration(EPOCH_2020, InstantWithDuration.MAX_DURATION_MINUTES) + assertEquals("2020-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") + assertEquals("2020-01-02T10:07:00Z", instantWithDuration.endFormatted, "endFormatted") + assertEquals( + 60 * DURATION_MINUTES_MASK, + instantWithDuration.endEpochSeconds - instantWithDuration.startEpochSeconds, + "Max duration" + ) + assertEquals(DURATION_MINUTES_MASK, instantWithDuration.packedValue and DURATION_MINUTES_MASK, "packedValue") + assertEquals(0x7FF, instantWithDuration.packedValue, "packedValue should be 2047") + } + + @Test + fun `constructor with packedValue creates correct startEpochSeconds and durationMinutes`() { + val instantWithDuration = InstantWithDuration(0x7ffL or DURATION_MINUTES_MASK) + assertEquals(EPOCH_2020, instantWithDuration.startEpochSeconds) + assertEquals(InstantWithDuration.MAX_DURATION_MINUTES, instantWithDuration.durationMinutes) + } + + @ParameterizedTest + @CsvSource( + "1630444800, 60, 1dhubhoc", + "1630448400, 120, 1dhypim0", + "1630448400, 121, 1dhypim1", + "1630448401, 120, 1dhypk6w", + "1630452000, 180, 1di33jjo" + ) + fun `roundtrip conversion maintains values`( + startEpochSeconds: Long, + durationMinutes: UShort, + expectedShortString: String + ) { + val instantWithDuration1 = InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes) + assertEquals(expectedShortString, instantWithDuration1.asShortString) + + val instantWithDuration2 = InstantWithDuration.fromShortString(expectedShortString) + assertEquals(startEpochSeconds, instantWithDuration2.startEpochSeconds) + assertEquals(durationMinutes, instantWithDuration2.durationMinutes) + } + + @Test + fun `endEpochSeconds - minimum valid values`() { + val instantWithDuration = InstantWithDuration.fromStartAndDuration(EPOCH_2020) + assertEquals( + EPOCH_2020, + instantWithDuration.endEpochSeconds, + "endEpochSeconds" + ) + } + + @Test + fun `endEpochSeconds - Year 2120, max duration`() { + val epoch2120 = 4733510400 + val instantWithDuration = InstantWithDuration.fromStartAndDuration( + epoch2120, + InstantWithDuration.MAX_DURATION_MINUTES + ) + assertEquals("2120-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") + assertEquals("2120-01-02T10:07:00Z", instantWithDuration.endFormatted, "endFormatted") + assertEquals( + epoch2120, + instantWithDuration.startEpochSeconds, + "startEpochSeconds" + ) + assertEquals( + epoch2120 + InstantWithDuration.MAX_DURATION_MINUTES.toLong() * 60, + instantWithDuration.endEpochSeconds, + "endEpochSeconds" + ) + } + + @ParameterizedTest + @CsvSource( + "$EPOCH_2020, 0, '0m @ 2020-01-01T00:00:00Z'", + "$EPOCH_2020, 1, '1m @ 2020-01-01T00:00:00Z'", + "$EPOCH_2020, 2047, '2047m @ 2020-01-01T00:00:00Z'", + "${EPOCH_2020 + 60}, 0, '0m @ 2020-01-01T00:01:00Z'", + ) + fun `toShortString() for various inputs`( + startEpochSeconds: Long, + durationMinutes: UShort, + expected: String + ) { + assertEquals( + expected, + InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes).toString() + ) + } + + @ParameterizedTest + @CsvSource( + "$EPOCH_2020, 0, '2020-01-01T00:00:00Z'", + "$EPOCH_2020, 1, '2020-01-01T00:01:00Z'", + "${EPOCH_2020 + 60}, 0, '2020-01-01T00:01:00Z'", + "$EPOCH_2020, 2047, '2020-01-02T10:07:00Z'", + ) + fun `'endFormatted' for various inputs`( + startEpochSeconds: Long, + durationMinutes: UShort, + expected: String + ) { + assertEquals( + expected, + InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes).endFormatted + ) + } + + @Test + fun `compareTo works correctly`() { + val earlier = InstantWithDuration.fromStartAndDuration(EPOCH_2020, 30u) + val later = InstantWithDuration.fromStartAndDuration(EPOCH_2020, 60u) + val sameEndAsLater = InstantWithDuration.fromStartAndDuration(EPOCH_2020 + 1800, 30u) + + assertEquals(later.endFormatted , sameEndAsLater.endFormatted) + assert(earlier < later) + assert(later > earlier) + assert(earlier < sameEndAsLater) + assert(later < sameEndAsLater) + } +} From 92a5727a01f5bd1009ee420b90a211a4a17fc09d Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:46:24 +0000 Subject: [PATCH 04/12] Use 26 bits for BITS_FOR_DURATION --- .../klib/base/InstantWithDurationTest.kt | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt index c74da69..11bedd1 100644 --- a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -12,36 +12,35 @@ class InstantWithDurationTest { @Test fun `constructor with startEpochSeconds and durationMinutes creates correct packedValue`() { val instantWithDuration = InstantWithDuration - .fromStartAndDuration(EPOCH_2020, InstantWithDuration.MAX_DURATION_MINUTES) + .fromStartAndDuration(EPOCH_2020, MAX_DURATION_MINUTES) assertEquals("2020-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") - assertEquals("2020-01-02T10:07:00Z", instantWithDuration.endFormatted, "endFormatted") + assertEquals("2147-08-06T09:03:00Z", instantWithDuration.endFormatted, "endFormatted") assertEquals( 60 * DURATION_MINUTES_MASK, instantWithDuration.endEpochSeconds - instantWithDuration.startEpochSeconds, "Max duration" ) assertEquals(DURATION_MINUTES_MASK, instantWithDuration.packedValue and DURATION_MINUTES_MASK, "packedValue") - assertEquals(0x7FF, instantWithDuration.packedValue, "packedValue should be 2047") + assertEquals(0x3ffffff, instantWithDuration.packedValue, "packedValue should be 67108863") } @Test fun `constructor with packedValue creates correct startEpochSeconds and durationMinutes`() { val instantWithDuration = InstantWithDuration(0x7ffL or DURATION_MINUTES_MASK) assertEquals(EPOCH_2020, instantWithDuration.startEpochSeconds) - assertEquals(InstantWithDuration.MAX_DURATION_MINUTES, instantWithDuration.durationMinutes) + assertEquals(MAX_DURATION_MINUTES, instantWithDuration.durationMinutes) } @ParameterizedTest @CsvSource( - "1630444800, 60, 1dhubhoc", - "1630448400, 120, 1dhypim0", - "1630448400, 121, 1dhypim1", - "1630448401, 120, 1dhypk6w", - "1630452000, 180, 1di33jjo" + "$EPOCH_2020, 0, 000000", + "$EPOCH_2020, 1, 000001", + "$EPOCH_2020, $DURATION_MINUTES_MASK, 13ydj3", + "${EPOCH_2020 + 60}, 0, 1ulajuo", ) fun `roundtrip conversion maintains values`( startEpochSeconds: Long, - durationMinutes: UShort, + durationMinutes: UInt, expectedShortString: String ) { val instantWithDuration1 = InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes) @@ -64,20 +63,20 @@ class InstantWithDurationTest { @Test fun `endEpochSeconds - Year 2120, max duration`() { - val epoch2120 = 4733510400 + val epoch2120 = 4733510400L val instantWithDuration = InstantWithDuration.fromStartAndDuration( epoch2120, - InstantWithDuration.MAX_DURATION_MINUTES + MAX_DURATION_MINUTES ) assertEquals("2120-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") - assertEquals("2120-01-02T10:07:00Z", instantWithDuration.endFormatted, "endFormatted") + assertEquals("2247-08-06T09:03:00Z", instantWithDuration.endFormatted, "endFormatted") assertEquals( epoch2120, instantWithDuration.startEpochSeconds, "startEpochSeconds" ) assertEquals( - epoch2120 + InstantWithDuration.MAX_DURATION_MINUTES.toLong() * 60, + epoch2120 + MAX_DURATION_MINUTES.toLong() * 60, instantWithDuration.endEpochSeconds, "endEpochSeconds" ) @@ -88,11 +87,12 @@ class InstantWithDurationTest { "$EPOCH_2020, 0, '0m @ 2020-01-01T00:00:00Z'", "$EPOCH_2020, 1, '1m @ 2020-01-01T00:00:00Z'", "$EPOCH_2020, 2047, '2047m @ 2020-01-01T00:00:00Z'", + "$EPOCH_2020, $DURATION_MINUTES_MASK, '67108863m @ 2020-01-01T00:00:00Z'", "${EPOCH_2020 + 60}, 0, '0m @ 2020-01-01T00:01:00Z'", ) fun `toShortString() for various inputs`( startEpochSeconds: Long, - durationMinutes: UShort, + durationMinutes: UInt, expected: String ) { assertEquals( @@ -103,14 +103,16 @@ class InstantWithDurationTest { @ParameterizedTest @CsvSource( - "$EPOCH_2020, 0, '2020-01-01T00:00:00Z'", - "$EPOCH_2020, 1, '2020-01-01T00:01:00Z'", + "$EPOCH_2020, 0, '2020-01-01T00:00:00Z'", + "$EPOCH_2020, 1, '2020-01-01T00:01:00Z'", "${EPOCH_2020 + 60}, 0, '2020-01-01T00:01:00Z'", - "$EPOCH_2020, 2047, '2020-01-02T10:07:00Z'", + "$EPOCH_2020, 2047, '2020-01-02T10:07:00Z'", + "$EPOCH_2020, $DURATION_MINUTES_MASK, '2147-08-06T09:03:00Z'", + "${EPOCH_2020 + DURATION_MINUTES_MASK * 60}, 0, '2147-08-06T09:03:00Z'", ) fun `'endFormatted' for various inputs`( startEpochSeconds: Long, - durationMinutes: UShort, + durationMinutes: UInt, expected: String ) { assertEquals( From 0a319884d466a37c0f35ee96e118b3d5acd51c63 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:48:07 +0000 Subject: [PATCH 05/12] Use 26 bits for BITS_FOR_DURATION --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index 4b81fd0..8ee4fcc 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -102,15 +102,19 @@ object ShortString { value class InstantWithDuration(internal val packedValue: Long) : Comparable { init { - require(packedValue shr (63 - BITS_FOR_DURATION) == 0L) { "StartEpochSeconds out of range" } - require(durationMinutes <= MAX_DURATION_MINUTES) { "Duration must be at most $MAX_DURATION_MINUTES" } + require((packedValue shr BITS_FOR_DURATION) <= INSTANT_SECONDS_MASK) { + "StartEpochSeconds out of range (${(packedValue shr BITS_FOR_DURATION) + EPOCH_2020})" + } + require(durationMinutes <= MAX_DURATION_MINUTES) { + "Max duration minutes exceeded by ${durationMinutes - MAX_DURATION_MINUTES}" + } } val startEpochSeconds: Long get() = EPOCH_2020 + (packedValue shr BITS_FOR_DURATION) - val durationMinutes: UShort - get() = (packedValue and DURATION_MINUTES_MASK).toUShort() + val durationMinutes: UInt + get() = (packedValue and DURATION_MINUTES_MASK).toUInt() @get:JsonValue val asShortString get() = packedValue.asShortString @@ -121,10 +125,10 @@ value class InstantWithDuration(internal val packedValue: Long) : Comparable= start) { "End time must not be before start time" } - val durationMinutes = Duration.between(start, end).toMinutes().toUShort() + val durationMinutes = Duration.between(start, end).toMinutes().toUInt() return fromStartAndDuration(start, durationMinutes) } - fun fromStartAndDuration(start: String, durationMinutes: UShort = 0u) = + fun fromStartAndDuration(start: String, durationMinutes: UInt = 0u) = fromStartAndDuration(Instant.parse(start), durationMinutes) - fun fromStartAndDuration(start: Instant, durationMinutes: UShort = 0u) = + fun fromStartAndDuration(start: Instant, durationMinutes: UInt = 0u) = fromStartAndDuration(start.epochSecond, durationMinutes) - fun fromStartAndDuration(startEpochSeconds: Long, durationMinutes: UShort = 0u): InstantWithDuration { + fun fromStartAndDuration(startEpochSeconds: Long, durationMinutes: UInt = 0u): InstantWithDuration { require(durationMinutes <= MAX_DURATION_MINUTES) { "Max duration minutes exceeded by ${durationMinutes - MAX_DURATION_MINUTES}" } require(startEpochSeconds >= EPOCH_2020) { "Min startEpochSeconds should be increased by ${EPOCH_2020 - startEpochSeconds}" } + require(startEpochSeconds <= EPOCH_2020 + INSTANT_SECONDS_MASK) { + "Max startEpochSeconds exceeded by ${startEpochSeconds - (EPOCH_2020 + INSTANT_SECONDS_MASK)}" + } return InstantWithDuration( ((startEpochSeconds - EPOCH_2020) shl BITS_FOR_DURATION) or durationMinutes.toLong() ) From b783f6fc24df8e5133ec1ddabc6bd8fca63d68ea Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:58:36 +0000 Subject: [PATCH 06/12] Update DateTimeKit.kt --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index 8ee4fcc..cfa7c7e 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -90,7 +90,6 @@ import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter - @Suppress("MagicNumber") object ShortString { val Long.asShortString get() = toString(36).padStart(6, '0') @@ -128,7 +127,7 @@ value class InstantWithDuration(internal val packedValue: Long) : Comparable Date: Fri, 30 Aug 2024 19:59:16 +0000 Subject: [PATCH 07/12] Update InstantWithDurationTest.kt --- .../klib/base/InstantWithDurationTest.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt index 11bedd1..690de05 100644 --- a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -62,24 +62,31 @@ class InstantWithDurationTest { } @Test - fun `endEpochSeconds - Year 2120, max duration`() { - val epoch2120 = 4733510400L + fun `Maximum representable values`() { val instantWithDuration = InstantWithDuration.fromStartAndDuration( - epoch2120, - MAX_DURATION_MINUTES - ) - assertEquals("2120-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") - assertEquals("2247-08-06T09:03:00Z", instantWithDuration.endFormatted, "endFormatted") - assertEquals( - epoch2120, - instantWithDuration.startEpochSeconds, - "startEpochSeconds" - ) - assertEquals( - epoch2120 + MAX_DURATION_MINUTES.toLong() * 60, - instantWithDuration.endEpochSeconds, - "endEpochSeconds" + EPOCH_2020 + INSTANT_SECONDS_MASK, MAX_DURATION_MINUTES ) + assertEquals("6375-04-08T15:04:31Z", instantWithDuration.startFormatted, "startFormatted") + assertEquals("6502-11-12T00:07:31Z", instantWithDuration.endFormatted, "endFormatted") + assertEquals(EPOCH_2020 + INSTANT_SECONDS_MASK, instantWithDuration.startEpochSeconds, "startEpochSeconds") + assertEquals(143043322051, instantWithDuration.endEpochSeconds, "endEpochSeconds") + } + + @Test + fun `constructor throws IllegalArgumentException for values exceeding maximum`() { + assertThrows("Start") { + InstantWithDuration.fromStartAndDuration( + EPOCH_2020 + INSTANT_SECONDS_MASK + 1, + 0u + ) + } + + assertThrows("Duration") { + InstantWithDuration.fromStartAndDuration( + EPOCH_2020, + MAX_DURATION_MINUTES + 1u + ) + } } @ParameterizedTest From d8c41ec913586fed16b79df0a0219fb6329cff50 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:31:45 +0000 Subject: [PATCH 08/12] Update InstantWithDurationTest.kt: Use @DisplayName --- .../klib/base/InstantWithDurationTest.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt index 690de05..25a72ad 100644 --- a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -7,10 +7,11 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import kotlin.test.assertEquals +@DisplayName("InstantWithDuration") class InstantWithDurationTest { @Test - fun `constructor with startEpochSeconds and durationMinutes creates correct packedValue`() { + fun `Constructor with startEpochSeconds and durationMinutes creates correct packedValue`() { val instantWithDuration = InstantWithDuration .fromStartAndDuration(EPOCH_2020, MAX_DURATION_MINUTES) assertEquals("2020-01-01T00:00:00Z", instantWithDuration.startFormatted, "startFormatted") @@ -25,7 +26,7 @@ class InstantWithDurationTest { } @Test - fun `constructor with packedValue creates correct startEpochSeconds and durationMinutes`() { + fun `Constructor with packedValue creates correct startEpochSeconds and durationMinutes`() { val instantWithDuration = InstantWithDuration(0x7ffL or DURATION_MINUTES_MASK) assertEquals(EPOCH_2020, instantWithDuration.startEpochSeconds) assertEquals(MAX_DURATION_MINUTES, instantWithDuration.durationMinutes) @@ -37,22 +38,24 @@ class InstantWithDurationTest { "$EPOCH_2020, 1, 000001", "$EPOCH_2020, $DURATION_MINUTES_MASK, 13ydj3", "${EPOCH_2020 + 60}, 0, 1ulajuo", + "${EPOCH_2020 + INSTANT_SECONDS_MASK}, $DURATION_MINUTES_MASK, 1y2p0ij32e8e7", ) - fun `roundtrip conversion maintains values`( + @DisplayName("Roundtrip conversion maintains values") + fun roundTrip( startEpochSeconds: Long, durationMinutes: UInt, expectedShortString: String ) { - val instantWithDuration1 = InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes) - assertEquals(expectedShortString, instantWithDuration1.asShortString) - - val instantWithDuration2 = InstantWithDuration.fromShortString(expectedShortString) - assertEquals(startEpochSeconds, instantWithDuration2.startEpochSeconds) - assertEquals(durationMinutes, instantWithDuration2.durationMinutes) + val encodedString = InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes).asShortString + InstantWithDuration.fromShortString(encodedString).also { decoded -> + assertEquals(startEpochSeconds, decoded.startEpochSeconds) + assertEquals(durationMinutes, decoded.durationMinutes) + } + assertEquals(expectedShortString, encodedString) } @Test - fun `endEpochSeconds - minimum valid values`() { + fun `'endEpochSeconds' - minimum valid values`() { val instantWithDuration = InstantWithDuration.fromStartAndDuration(EPOCH_2020) assertEquals( EPOCH_2020, @@ -73,7 +76,7 @@ class InstantWithDurationTest { } @Test - fun `constructor throws IllegalArgumentException for values exceeding maximum`() { + fun `Constructor throws IllegalArgumentException for values exceeding maximum`() { assertThrows("Start") { InstantWithDuration.fromStartAndDuration( EPOCH_2020 + INSTANT_SECONDS_MASK + 1, @@ -97,7 +100,8 @@ class InstantWithDurationTest { "$EPOCH_2020, $DURATION_MINUTES_MASK, '67108863m @ 2020-01-01T00:00:00Z'", "${EPOCH_2020 + 60}, 0, '0m @ 2020-01-01T00:01:00Z'", ) - fun `toShortString() for various inputs`( + @DisplayName("'toShortString()' for various inputs") + fun `toShortString()`( startEpochSeconds: Long, durationMinutes: UInt, expected: String @@ -117,19 +121,20 @@ class InstantWithDurationTest { "$EPOCH_2020, $DURATION_MINUTES_MASK, '2147-08-06T09:03:00Z'", "${EPOCH_2020 + DURATION_MINUTES_MASK * 60}, 0, '2147-08-06T09:03:00Z'", ) - fun `'endFormatted' for various inputs`( + @DisplayName("'endFormatted' for various inputs") + fun endFormatted( startEpochSeconds: Long, durationMinutes: UInt, - expected: String + expectedEnd: String ) { assertEquals( - expected, + expectedEnd, InstantWithDuration.fromStartAndDuration(startEpochSeconds, durationMinutes).endFormatted ) } @Test - fun `compareTo works correctly`() { + fun `'compareTo' works correctly`() { val earlier = InstantWithDuration.fromStartAndDuration(EPOCH_2020, 30u) val later = InstantWithDuration.fromStartAndDuration(EPOCH_2020, 60u) val sameEndAsLater = InstantWithDuration.fromStartAndDuration(EPOCH_2020 + 1800, 30u) From 06c748a1a221babfec14f4bceaea42b17fc515d6 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:41:20 +0000 Subject: [PATCH 09/12] Update DateTimeKit.kt --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index cfa7c7e..e45ff65 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -124,7 +124,7 @@ value class InstantWithDuration(internal val packedValue: Long) : Comparable Date: Mon, 2 Sep 2024 11:42:25 +0000 Subject: [PATCH 10/12] Update InstantWithDurationTest.kt --- .../klib/base/InstantWithDurationTest.kt | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt index 25a72ad..3b245f0 100644 --- a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -32,6 +32,39 @@ class InstantWithDurationTest { assertEquals(MAX_DURATION_MINUTES, instantWithDuration.durationMinutes) } + @ParameterizedTest + @CsvSource( + "2020-01-01T00:00:00Z, 2020-01-01T00:00:00Z, 0", + "2025-08-01T10:00:00Z, 2025-08-01T10:05:00Z, 5", + "2025-08-01T10:00:00Z, 2025-08-01T10:30:00Z, 30", + "2025-08-01T10:00:00Z, 2025-08-01T11:00:00Z, 60", + "6375-04-08T15:04:31Z, 6502-11-12T00:07:31Z, $DURATION_MINUTES_MASK" + ) + @DisplayName("Factory methods") + fun testFromStartFactory(start: String, end: String, expectedDurationMinutes: UInt) { + InstantWithDuration.fromStartAndEnd(start, end).run { + assertEquals(start, startFormatted) + assertEquals(end, endFormatted) + assertEquals(expectedDurationMinutes, durationMinutes, "fromStartAndEnd") + } + InstantWithDuration.fromStartAndDuration(start, expectedDurationMinutes).run { + assertEquals(start, startFormatted) + assertEquals(end, endFormatted) + assertEquals(expectedDurationMinutes, durationMinutes, "fromStartAndDuration") + } + } + + @Test + @DisplayName("'fromStartAndEnd' throws IllegalArgumentException when end is before start") + fun testFromStartAndEndThrowsException() { + val start = "2025-08-01T09:00:01Z" + val end = "2025-08-01T09:00:00Z" + + assertThrows { + InstantWithDuration.fromStartAndEnd(start, end) + } + } + @ParameterizedTest @CsvSource( "$EPOCH_2020, 0, 000000", @@ -40,7 +73,7 @@ class InstantWithDurationTest { "${EPOCH_2020 + 60}, 0, 1ulajuo", "${EPOCH_2020 + INSTANT_SECONDS_MASK}, $DURATION_MINUTES_MASK, 1y2p0ij32e8e7", ) - @DisplayName("Roundtrip conversion maintains values") + @DisplayName("Roundtrip (asShortString -> fromShortString)") fun roundTrip( startEpochSeconds: Long, durationMinutes: UInt, @@ -73,6 +106,8 @@ class InstantWithDurationTest { assertEquals("6502-11-12T00:07:31Z", instantWithDuration.endFormatted, "endFormatted") assertEquals(EPOCH_2020 + INSTANT_SECONDS_MASK, instantWithDuration.startEpochSeconds, "startEpochSeconds") assertEquals(143043322051, instantWithDuration.endEpochSeconds, "endEpochSeconds") + assertEquals(DURATION_MINUTES_MASK.toUInt(), instantWithDuration.durationMinutes, "durationMinutes") + assertEquals(DURATION_MINUTES_MASK, instantWithDuration.duration.toMinutes(), "duration") } @Test @@ -139,7 +174,7 @@ class InstantWithDurationTest { val later = InstantWithDuration.fromStartAndDuration(EPOCH_2020, 60u) val sameEndAsLater = InstantWithDuration.fromStartAndDuration(EPOCH_2020 + 1800, 30u) - assertEquals(later.endFormatted , sameEndAsLater.endFormatted) + assertEquals(later.endFormatted, sameEndAsLater.endFormatted) assert(earlier < later) assert(later > earlier) assert(earlier < sameEndAsLater) From 4bf6557b824fea10f431f467968da8eeb162ba54 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:08:58 +0000 Subject: [PATCH 11/12] Update InstantWithDurationTest.kt --- .../klib/base/InstantWithDurationTest.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt index 3b245f0..9be44cd 100644 --- a/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt +++ b/base/src/test/kotlin/klib/base/InstantWithDurationTest.kt @@ -34,6 +34,7 @@ class InstantWithDurationTest { @ParameterizedTest @CsvSource( + "2019-12-31T23:58:00Z, 2019-12-31T23:59:00Z, 1", "2020-01-01T00:00:00Z, 2020-01-01T00:00:00Z, 0", "2025-08-01T10:00:00Z, 2025-08-01T10:05:00Z, 5", "2025-08-01T10:00:00Z, 2025-08-01T10:30:00Z, 30", @@ -67,6 +68,7 @@ class InstantWithDurationTest { @ParameterizedTest @CsvSource( + "${EPOCH_2020 - 1}, 0, -13ydj4", "$EPOCH_2020, 0, 000000", "$EPOCH_2020, 1, 000001", "$EPOCH_2020, $DURATION_MINUTES_MASK, 13ydj3", @@ -88,7 +90,7 @@ class InstantWithDurationTest { } @Test - fun `'endEpochSeconds' - minimum valid values`() { + fun `'endEpochSeconds' for EPOCH_2020`() { val instantWithDuration = InstantWithDuration.fromStartAndDuration(EPOCH_2020) assertEquals( EPOCH_2020, @@ -97,6 +99,18 @@ class InstantWithDurationTest { ) } + @Test + fun `'endEpochSeconds' for EPOCH_2020 minus 2 minutes`() { + val instantWithDuration = InstantWithDuration.fromStartAndDuration( + EPOCH_2020 - 120, 1u + ) + assertEquals( + EPOCH_2020 - 60, + instantWithDuration.endEpochSeconds, + "endEpochSeconds" + ) + } + @Test fun `Maximum representable values`() { val instantWithDuration = InstantWithDuration.fromStartAndDuration( @@ -149,7 +163,7 @@ class InstantWithDurationTest { @ParameterizedTest @CsvSource( - "$EPOCH_2020, 0, '2020-01-01T00:00:00Z'", + "${EPOCH_2020 - 1}, 0, '2019-12-31T23:59:59Z'", "$EPOCH_2020, 1, '2020-01-01T00:01:00Z'", "${EPOCH_2020 + 60}, 0, '2020-01-01T00:01:00Z'", "$EPOCH_2020, 2047, '2020-01-02T10:07:00Z'", From cca4d74b5480acbf78cd86c904026724d0ee61f3 Mon Sep 17 00:00:00 2001 From: "Elifarley C." <519940+elifarley@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:09:55 +0000 Subject: [PATCH 12/12] Update DateTimeKit.kt --- base/src/main/kotlin/klib/base/DateTimeKit.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base/src/main/kotlin/klib/base/DateTimeKit.kt b/base/src/main/kotlin/klib/base/DateTimeKit.kt index e45ff65..2817b7f 100644 --- a/base/src/main/kotlin/klib/base/DateTimeKit.kt +++ b/base/src/main/kotlin/klib/base/DateTimeKit.kt @@ -152,8 +152,10 @@ value class InstantWithDuration(internal val packedValue: Long) : Comparable= EPOCH_2020) { - "Min startEpochSeconds should be increased by ${EPOCH_2020 - startEpochSeconds}" + require(startEpochSeconds >= EPOCH_2020 - INSTANT_SECONDS_MASK + 1) { + "Min startEpochSeconds should be increased by ${ + EPOCH_2020 - INSTANT_SECONDS_MASK + 1 - startEpochSeconds + }" } require(startEpochSeconds <= EPOCH_2020 + INSTANT_SECONDS_MASK) { "Max startEpochSeconds exceeded by ${startEpochSeconds - (EPOCH_2020 + INSTANT_SECONDS_MASK)}"