diff --git a/core/src/commonMain/kotlin/io/islandtime/Conversions.kt b/core/src/commonMain/kotlin/io/islandtime/Conversions.kt new file mode 100644 index 000000000..1aca37c55 --- /dev/null +++ b/core/src/commonMain/kotlin/io/islandtime/Conversions.kt @@ -0,0 +1,136 @@ +@file:JvmMultifileClass +@file:JvmName("DateTimesKt") + +package io.islandtime + +import io.islandtime.base.TimePoint +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Returns this date with the precision reduced to the year. + */ +fun YearMonth.toYear(): Year = Year(year) + +/** + * Returns this date with the precision reduced to the year. + */ +fun Date.toYear(): Year = Year(year) + +/** + * Returns this date-time with the precision reduced to the year. + */ +fun DateTime.toYear(): Year = date.toYear() + +/** + * Returns this date-time with the precision reduced to the year. + */ +fun OffsetDateTime.toYear(): Year = date.toYear() + +/** + * Returns this date-time with the precision reduced to the year. + */ +fun ZonedDateTime.toYear(): Year = date.toYear() + +/** + * Returns this date with the precision reduced to the year-month. + */ +fun Date.toYearMonth(): YearMonth = YearMonth(year, month) + +/** + * Returns this date-time with the precision reduced to the year-month. + */ +fun DateTime.toYearMonth(): YearMonth = date.toYearMonth() + +/** + * Returns this date-time with the precision reduced to the year-month. + */ +fun OffsetDateTime.toYearMonth(): YearMonth = dateTime.toYearMonth() + +/** + * Returns this date-time with the precision reduced to the year-month. + */ +fun ZonedDateTime.toYearMonth(): YearMonth = dateTime.toYearMonth() + +/** + * Returns the combined time and UTC offset. + */ +fun OffsetDateTime.toOffsetTime(): OffsetTime = OffsetTime(time, offset) + +/** + * Returns the combined time and UTC offset. + */ +fun ZonedDateTime.toOffsetTime(): OffsetTime = OffsetTime(time, offset) + +/** + * Returns the combined date, time, and UTC offset. + * + * While similar to `ZonedDateTime`, an `OffsetDateTime` representation is unaffected by time zone rule changes or + * database differences between systems, making it better suited for use cases involving persistence or network + * transfer. + */ +fun ZonedDateTime.toOffsetDateTime(): OffsetDateTime = OffsetDateTime(dateTime, offset) + +/** + * Converts this instant to the corresponding [DateTime] in [zone]. + */ +fun Instant.toDateTimeAt(zone: TimeZone): DateTime = toDateTimeAt(zone.rules.offsetAt(this)) + +/** + * Strategy to use when converting a local date-time accompanied by a [UtcOffset] to a date and time that are valid + * according to the rules of a [TimeZone]. + */ +enum class OffsetConversionStrategy { + /** + * Preserve the instant on the timeline, ignoring the local time. + */ + PRESERVE_INSTANT, + + /** + * Preserve the local date and time in the new time zone (if possible), adjusting the offset if needed. + */ + PRESERVE_LOCAL_TIME +} + +/** + * Converts this [OffsetDateTime] to a [ZonedDateTime] using the specified [strategy] to adjust it to a valid date, + * time, and offset in [zone]. + * + * - [OffsetConversionStrategy.PRESERVE_INSTANT] - Preserve the instant captured by the date, time, and offset, + * ignoring the local time. + * + * - [OffsetConversionStrategy.PRESERVE_LOCAL_TIME] - Preserve the local date and time in the new time zone, adjusting + * the offset if needed. + * + * Alternatively, you can use [asZonedDateTime] to convert to a [ZonedDateTime] with an equivalent fixed-offset zone. + * However, this comes with the caveat that a fixed-offset zone lacks knowledge of any region and will not respond to + * daylight savings time changes. + * + * @see asZonedDateTime + */ +fun OffsetDateTime.toZonedDateTime(zone: TimeZone, strategy: OffsetConversionStrategy): ZonedDateTime { + return when (strategy) { + OffsetConversionStrategy.PRESERVE_INSTANT -> ZonedDateTime.fromInstant(dateTime, offset, zone) + OffsetConversionStrategy.PRESERVE_LOCAL_TIME -> ZonedDateTime.fromLocal(dateTime, zone, offset) + } +} + +/** + * Converts this date-time to the corresponding [Instant] at [offset]. + * @param offset the offset from UTC + */ +fun DateTime.toInstantAt(offset: UtcOffset): Instant { + return Instant.fromSecondOfUnixEpoch(secondOfUnixEpochAt(offset), nanosecond) +} + +/** + * Converts this date-time to the [Instant] representing the same time point. + */ +fun OffsetDateTime.toInstant(): Instant = (this as TimePoint<*>).toInstant() + +/** + * Converts this date-time to the [Instant] representing the same time point. + */ +fun ZonedDateTime.toInstant(): Instant = (this as TimePoint<*>).toInstant() + +internal fun TimePoint<*>.toInstant(): Instant = Instant.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond) \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/islandtime/Date.kt b/core/src/commonMain/kotlin/io/islandtime/Date.kt index 42cbe0d93..6ae00cbfc 100644 --- a/core/src/commonMain/kotlin/io/islandtime/Date.kt +++ b/core/src/commonMain/kotlin/io/islandtime/Date.kt @@ -1,3 +1,5 @@ +@file:Suppress("FunctionName") + package io.islandtime import io.islandtime.base.DateTimeField @@ -90,10 +92,13 @@ class Date( */ val lengthOfYear: IntDays get() = lengthOfYear(year) - /** - * The combined year and month. - */ - inline val yearMonth: YearMonth get() = YearMonth(year, month) + @Deprecated( + "Use toYearMonth() instead.", + ReplaceWith("this.toYearMonth()"), + DeprecationLevel.WARNING + ) + inline val yearMonth: YearMonth + get() = toYearMonth() /** * The number of days away from the Unix epoch (`1970-01-01T00:00Z`) that this date falls. @@ -331,7 +336,6 @@ class Date( * @param dayOfYear the day of the calendar year * @throws DateTimeException if the year or day of year are invalid */ -@Suppress("FunctionName") fun Date(year: Int, dayOfYear: Int): Date { checkValidYear(year) checkValidDayOfYear(year, dayOfYear) diff --git a/core/src/commonMain/kotlin/io/islandtime/DateTime.kt b/core/src/commonMain/kotlin/io/islandtime/DateTime.kt index b237afe82..b2d417c38 100644 --- a/core/src/commonMain/kotlin/io/islandtime/DateTime.kt +++ b/core/src/commonMain/kotlin/io/islandtime/DateTime.kt @@ -130,10 +130,12 @@ class DateTime( */ inline val lengthOfYear: IntDays get() = date.lengthOfYear - /** - * The combined year and month. - */ - inline val yearMonth: YearMonth get() = date.yearMonth + @Deprecated( + "Use toYearMonth() instead.", + ReplaceWith("this.toYearMonth()"), + DeprecationLevel.WARNING + ) + inline val yearMonth: YearMonth get() = toYearMonth() /** * Return a [DateTime] with [period] added to it. @@ -579,13 +581,12 @@ class DateTime( */ fun millisecondOfUnixEpochAt(offset: UtcOffset): Long = millisecondsSinceUnixEpochAt(offset).value - /** - * The [Instant] represented by this date-time at a particular offset from UTC. - * @param offset the offset from UTC - */ - fun instantAt(offset: UtcOffset): Instant { - return Instant.fromSecondOfUnixEpoch(secondOfUnixEpochAt(offset), nanosecond) - } + @Deprecated( + "Use toInstantAt() instead.", + ReplaceWith("this.toInstantAt(offset)"), + DeprecationLevel.WARNING + ) + fun instantAt(offset: UtcOffset): Instant = toInstantAt(offset) companion object { /** @@ -679,7 +680,7 @@ fun Date.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): D } /** - * Convert to a [DateTime] at a particular offset from UTC. + * Converts this instant to the corresponding [DateTime] at [offset]. */ fun Instant.toDateTimeAt(offset: UtcOffset): DateTime { return DateTime.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond, offset) diff --git a/core/src/commonMain/kotlin/io/islandtime/Instant.kt b/core/src/commonMain/kotlin/io/islandtime/Instant.kt index f7d4149d4..832b5914d 100644 --- a/core/src/commonMain/kotlin/io/islandtime/Instant.kt +++ b/core/src/commonMain/kotlin/io/islandtime/Instant.kt @@ -1,3 +1,5 @@ +@file:Suppress("FunctionName") + package io.islandtime import io.islandtime.base.DateTimeField @@ -290,14 +292,12 @@ class Instant private constructor( /** * Create the [Instant] represented by a number of seconds relative to the Unix epoch of 1970-01-01T00:00Z. */ -@Suppress("FunctionName") fun Instant(secondsSinceUnixEpoch: LongSeconds) = Instant.fromSecondOfUnixEpoch(secondsSinceUnixEpoch.value) /** * Create the [Instant] represented by a number of seconds and additional nanoseconds relative to the Unix epoch of * 1970-01-01T00:00Z. */ -@Suppress("FunctionName") fun Instant(secondsSinceUnixEpoch: LongSeconds, nanosecondAdjustment: IntNanoseconds): Instant { return Instant.fromSecondOfUnixEpoch(secondsSinceUnixEpoch.value, nanosecondAdjustment.value) } @@ -306,7 +306,6 @@ fun Instant(secondsSinceUnixEpoch: LongSeconds, nanosecondAdjustment: IntNanosec * Create the [Instant] represented by a number of seconds and additional nanoseconds relative to the Unix epoch of * 1970-01-01T00:00Z. */ -@Suppress("FunctionName") fun Instant(secondsSinceUnixEpoch: LongSeconds, nanosecondAdjustment: LongNanoseconds): Instant { return Instant.fromSecondOfUnixEpoch(secondsSinceUnixEpoch.value, nanosecondAdjustment.value) } @@ -314,7 +313,6 @@ fun Instant(secondsSinceUnixEpoch: LongSeconds, nanosecondAdjustment: LongNanose /** * Create the [Instant] represented by a number of milliseconds relative to the Unix epoch of 1970-01-01T00:00Z. */ -@Suppress("FunctionName") fun Instant(millisecondsSinceUnixEpoch: LongMilliseconds): Instant { return Instant.fromMillisecondOfUnixEpoch(millisecondsSinceUnixEpoch.value) } @@ -362,7 +360,7 @@ internal fun DateTimeParseResult.toInstant(): Instant? { return if (dateTime != null && offset != null) { val secondOfEpoch = dateTime.secondOfUnixEpochAt(offset) + - ((parsedYear / 10_000L) timesExact SECONDS_PER_10000_YEARS) + ((parsedYear / 10_000L) timesExact SECONDS_PER_10000_YEARS) Instant.fromSecondOfUnixEpoch(secondOfEpoch, dateTime.nanosecond) } else { null diff --git a/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt b/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt index 3a20e2354..3be9891f8 100644 --- a/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt +++ b/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt @@ -158,20 +158,26 @@ class OffsetDateTime( */ inline val lengthOfYear: IntDays get() = dateTime.lengthOfYear - /** - * The combined year and month. - */ - inline val yearMonth: YearMonth get() = dateTime.yearMonth - - /** - * The combined time of day and offset. - */ - inline val offsetTime: OffsetTime get() = OffsetTime(time, offset) - - /** - * The [Instant] representing the same time point. - */ - inline val instant: Instant get() = Instant.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond) + @Deprecated( + "Use toYearMonth() instead.", + ReplaceWith("this.toYearMonth()"), + DeprecationLevel.WARNING + ) + inline val yearMonth: YearMonth get() = toYearMonth() + + @Deprecated( + "Use toOffsetTime() instead.", + ReplaceWith("this.toOffsetTime()"), + DeprecationLevel.WARNING + ) + inline val offsetTime: OffsetTime get() = toOffsetTime() + + @Deprecated( + "Use toInstant() instead.", + ReplaceWith("this.toInstant()"), + DeprecationLevel.WARNING + ) + inline val instant: Instant get() = toInstant() override val secondsSinceUnixEpoch: LongSeconds get() = dateTime.secondsSinceUnixEpochAt(offset) @@ -410,11 +416,11 @@ infix fun Date.at(offsetTime: OffsetTime) = OffsetDateTime(this, offsetTime.time infix fun Instant.at(offset: UtcOffset) = OffsetDateTime(this.toDateTimeAt(offset), offset) @Deprecated( - "Use the 'offsetDateTime' property on ZonedDateTime instead.", - ReplaceWith("this.offsetDateTime"), + "Use 'toOffsetDateTime()' instead.", + ReplaceWith("this.toOffsetDateTime()"), DeprecationLevel.WARNING ) -fun ZonedDateTime.asOffsetDateTime() = offsetDateTime +fun ZonedDateTime.asOffsetDateTime() = toOffsetDateTime() /** * Convert a string to an [OffsetDateTime]. diff --git a/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt b/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt index da9e9b759..2e18cc2f5 100644 --- a/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt +++ b/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt @@ -1,3 +1,5 @@ +@file:Suppress("FunctionName") + package io.islandtime import io.islandtime.format.TimeZoneTextProvider @@ -173,8 +175,10 @@ sealed class TimeZone : Comparable { */ val UTC: TimeZone = FixedOffset(UtcOffset.ZERO) - @Suppress("FunctionName") - fun FixedOffset(id: String): TimeZone { + /** + * Create a fixed-offset [TimeZone] from an identifier in the form of `+01:00`. + */ + fun FixedOffset(id: String): FixedOffset { return try { FixedOffset(id.toUtcOffset(FIXED_TIME_ZONE_PARSER)) } catch (e: DateTimeParseException) { @@ -187,7 +191,6 @@ sealed class TimeZone : Comparable { /** * Create a [TimeZone] from an identifier. */ -@Suppress("FunctionName") fun TimeZone(id: String): TimeZone { return when { id == "Z" -> TimeZone.UTC @@ -198,7 +201,7 @@ fun TimeZone(id: String): TimeZone { } /** - * Convert a UTC offset into a [TimeZone] with a fixed offset. + * Converts this [UtcOffset] into a fixed-offset [TimeZone]. */ fun UtcOffset.asTimeZone(): TimeZone = TimeZone.FixedOffset(this) diff --git a/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt b/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt index 5548417f6..3e0e75a64 100644 --- a/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt +++ b/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt @@ -1,3 +1,5 @@ +@file:Suppress("FunctionName") + package io.islandtime import io.islandtime.base.DateTimeField @@ -98,7 +100,6 @@ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable { * @throws DateTimeException if any of the individual components is outside the valid range * @return a [UtcOffset] */ -@Suppress("FunctionName") fun UtcOffset( hours: IntHours, minutes: IntMinutes = 0.minutes, @@ -109,19 +110,19 @@ fun UtcOffset( } /** - * Convert a duration of hours into a UTC time offset of the same length. + * Converts a duration of hours into a UTC time offset of the same length. * @throws ArithmeticException if overflow occurs */ fun IntHours.asUtcOffset() = UtcOffset(this.inSeconds) /** - * Convert a duration of minutes into a UTC time offset of the same length. + * Converts a duration of minutes into a UTC time offset of the same length. * @throws ArithmeticException if overflow occurs */ fun IntMinutes.asUtcOffset() = UtcOffset(this.inSeconds) /** - * Convert a duration of seconds into a UTC time offset of the same length. + * Converts a duration of seconds into a UTC time offset of the same length. */ fun IntSeconds.asUtcOffset() = UtcOffset(this) diff --git a/core/src/commonMain/kotlin/io/islandtime/ZonedDateTime.kt b/core/src/commonMain/kotlin/io/islandtime/ZonedDateTime.kt index 9c1eacd5f..e4a068c1f 100644 --- a/core/src/commonMain/kotlin/io/islandtime/ZonedDateTime.kt +++ b/core/src/commonMain/kotlin/io/islandtime/ZonedDateTime.kt @@ -1,3 +1,5 @@ +@file:Suppress("FunctionName") + package io.islandtime import io.islandtime.base.TimePoint @@ -99,29 +101,43 @@ class ZonedDateTime private constructor( */ inline val lengthOfYear: IntDays get() = dateTime.lengthOfYear - /** - * The combined year and month. - */ - inline val yearMonth: YearMonth get() = dateTime.yearMonth + @Deprecated( + "Use toYearMonth() instead.", + ReplaceWith("this.toYearMonth()"), + DeprecationLevel.WARNING + ) + inline val yearMonth: YearMonth + get() = toYearMonth() /** * The combined time of day and offset. */ - inline val offsetTime: OffsetTime get() = OffsetTime(time, offset) + @Deprecated( + "Use toOffsetTime() instead.", + ReplaceWith("this.toOffsetTime()"), + DeprecationLevel.WARNING + ) + inline val offsetTime: OffsetTime + get() = toOffsetTime() - /** - * The combined date, time, and offset. - * - * While similar to `ZonedDateTime`, an `OffsetDateTime` representation is unaffected by time zone rule changes or - * database differences between systems, making it better suited for use cases involving persistence or network - * transfer. - */ - inline val offsetDateTime: OffsetDateTime get() = OffsetDateTime(dateTime, offset) + @Deprecated( + "Use toOffsetDateTime() instead.", + ReplaceWith("this.toOffsetDateTime()"), + DeprecationLevel.WARNING + ) + inline val offsetDateTime: OffsetDateTime + get() = toOffsetDateTime() /** * The [Instant] representing the same time point. */ - inline val instant: Instant get() = Instant.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond) + @Deprecated( + "Use toInstant() instead.", + ReplaceWith("this.toInstant()"), + DeprecationLevel.WARNING + ) + inline val instant: Instant + get() = toInstant() override val secondsSinceUnixEpoch: LongSeconds get() = dateTime.secondsSinceUnixEpochAt(offset) @@ -504,7 +520,6 @@ class ZonedDateTime private constructor( * gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it falls within an overlap * (meaning the local time exists twice), the earlier offset will be used. */ -@Suppress("FunctionName") fun ZonedDateTime( year: Int, month: Month, @@ -523,7 +538,6 @@ fun ZonedDateTime( * gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it falls within an overlap * (meaning the local time exists twice), the earlier offset will be used. */ -@Suppress("FunctionName") fun ZonedDateTime( year: Int, monthNumber: Int, @@ -542,7 +556,6 @@ fun ZonedDateTime( * gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it falls within an overlap * (meaning the local time exists twice), the earlier offset will be used. */ -@Suppress("FunctionName") fun ZonedDateTime( year: Int, dayOfYear: Int, @@ -560,7 +573,6 @@ fun ZonedDateTime( * gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it falls within an overlap * (meaning the local time exists twice), the earlier offset will be used. */ -@Suppress("FunctionName") fun ZonedDateTime(date: Date, time: Time, zone: TimeZone) = ZonedDateTime.fromLocal(DateTime(date, time), zone) /** @@ -570,7 +582,6 @@ fun ZonedDateTime(date: Date, time: Time, zone: TimeZone) = ZonedDateTime.fromLo * gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it falls within an overlap * (meaning the local time exists twice), the earlier offset will be used. */ -@Suppress("FunctionName") fun ZonedDateTime(dateTime: DateTime, zone: TimeZone) = ZonedDateTime.fromLocal(dateTime, zone) /** @@ -625,46 +636,60 @@ fun Date.endOfDayAt(zone: TimeZone): ZonedDateTime { } @Deprecated( - "Renamed to 'dateTimeAt'.", - ReplaceWith("this.dateTimeAt(zone)"), + "Use toZonedDateTime() instead.", + ReplaceWith( + "this.toZonedDateTime(zone, PRESERVE_LOCAL_TIME)", + "io.islandtime.OffsetConversionStrategy.PRESERVE_LOCAL_TIME" + ), DeprecationLevel.WARNING ) fun OffsetDateTime.similarLocalTimeAt(zone: TimeZone): ZonedDateTime { - return dateTimeAt(zone) + return toZonedDateTime(zone, OffsetConversionStrategy.PRESERVE_LOCAL_TIME) } -/** - * The [ZonedDateTime] with the same date and time at [zone]. The offset will be preserved if possible, but may require - * adjustment. - * @see instantAt - * @see asZonedDateTime - */ +@Deprecated( + "Use toZonedDateTime() instead.", + ReplaceWith( + "this.toZonedDateTime(zone, PRESERVE_LOCAL_TIME)", + "io.islandtime.OffsetConversionStrategy.PRESERVE_LOCAL_TIME" + ), + DeprecationLevel.WARNING +) fun OffsetDateTime.dateTimeAt(zone: TimeZone): ZonedDateTime { - return ZonedDateTime.fromInstant(dateTime, offset, zone) + return toZonedDateTime(zone, OffsetConversionStrategy.PRESERVE_LOCAL_TIME) } @Deprecated( - "Renamed to 'instantAt'.", - ReplaceWith("this.instantAt(zone)"), + "Use toZonedDateTime() instead.", + ReplaceWith( + "this.toZonedDateTime(zone, PRESERVE_INSTANT)", + "io.islandtime.OffsetConversionStrategy.PRESERVE_INSTANT" + ), DeprecationLevel.WARNING ) fun OffsetDateTime.sameInstantAt(zone: TimeZone): ZonedDateTime { - return instantAt(zone) + return toZonedDateTime(zone, OffsetConversionStrategy.PRESERVE_INSTANT) } -/** - * The [ZonedDateTime] representing the same instant in time at [zone]. The local date, time, and offset may differ. - * @see dateTimeAt - * @see asZonedDateTime - */ +@Deprecated( + "Use toZonedDateTime() instead.", + ReplaceWith( + "this.toZonedDateTime(zone, PRESERVE_INSTANT)", + "io.islandtime.OffsetConversionStrategy.PRESERVE_INSTANT" + ), + DeprecationLevel.WARNING +) fun OffsetDateTime.instantAt(zone: TimeZone): ZonedDateTime { - return ZonedDateTime.fromInstant(dateTime, offset, zone) + return toZonedDateTime(zone, OffsetConversionStrategy.PRESERVE_INSTANT) } /** - * Convert to a [ZonedDateTime] with a fixed offset time zone. - * @see instantAt - * @see dateTimeAt + * Converts this [OffsetDateTime] to an equivalent [ZonedDateTime] using a fixed-offset time zone. + * + * This comes with the caveat that a fixed-offset zone lacks knowledge of any region and will not respond to daylight + * savings time changes. To convert to a region-based zone, use [toZonedDateTime] instead. + * + * @see toZonedDateTime */ fun OffsetDateTime.asZonedDateTime(): ZonedDateTime { return ZonedDateTime.fromLocal(dateTime, offset.asTimeZone(), offset) diff --git a/core/src/commonMain/kotlin/io/islandtime/clock/Now.kt b/core/src/commonMain/kotlin/io/islandtime/clock/Now.kt index 151ce30e7..252ae648c 100644 --- a/core/src/commonMain/kotlin/io/islandtime/clock/Now.kt +++ b/core/src/commonMain/kotlin/io/islandtime/clock/Now.kt @@ -26,7 +26,7 @@ fun Year.Companion.now() = now(SystemClock()) /** * Get the current [Year] from the specified clock. */ -fun Year.Companion.now(clock: Clock) = Year(Date.now(clock).year) +fun Year.Companion.now(clock: Clock) = Date.now(clock).toYear() /** * Get the current [YearMonth] from the system clock. @@ -36,7 +36,7 @@ fun YearMonth.Companion.now() = now(SystemClock()) /** * Get the current [YearMonth] from the specified clock. */ -fun YearMonth.Companion.now(clock: Clock) = Date.now(clock).yearMonth +fun YearMonth.Companion.now(clock: Clock) = Date.now(clock).toYearMonth() /** * Get the current [Date] from the system clock. diff --git a/core/src/commonMain/kotlin/io/islandtime/ranges/Builders.kt b/core/src/commonMain/kotlin/io/islandtime/ranges/Builders.kt new file mode 100644 index 000000000..7ec489aa1 --- /dev/null +++ b/core/src/commonMain/kotlin/io/islandtime/ranges/Builders.kt @@ -0,0 +1,96 @@ +@file:JvmMultifileClass +@file:JvmName("RangesKt") + +package io.islandtime.ranges + +import io.islandtime.* +import io.islandtime.measures.days +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Combines this [DateRange] with a [TimeZone] to create a [ZonedDateTimeInterval] between the start of the first day + * and the end of the last day in [zone]. + */ +infix fun DateRange.at(zone: TimeZone): ZonedDateTimeInterval { + return when { + isEmpty() -> ZonedDateTimeInterval.EMPTY + isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED + start == endInclusive -> { + val zonedStart = start.startOfDayAt(zone) + val zonedEnd = zonedStart + 1.days + zonedStart until zonedEnd + } + else -> { + val start = if (hasUnboundedStart()) { + DateTime.MIN at zone + } else { + start.startOfDayAt(zone) + } + + val end = if (hasUnboundedEnd()) { + DateTime.MAX at zone + } else { + endInclusive.endOfDayAt(zone) + } + + start..end + } + } +} + +/** + * Combines this [DateTimeInterval] with a [TimeZone] to create a [ZonedDateTimeInterval] where both endpoints are in + * [zone]. + * + * Due to daylight savings time transitions, there a few complexities to be aware of. If the local time of either + * endpoint falls within a gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it + * falls within an overlap (meaning the local time exists twice), the earlier offset will be used. + */ +infix fun DateTimeInterval.at(zone: TimeZone): ZonedDateTimeInterval { + return when { + isEmpty() -> ZonedDateTimeInterval.EMPTY + isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED + else -> { + val start = if (hasUnboundedStart()) { + DateTime.MIN at zone + } else { + start at zone + } + + val end = if (hasUnboundedEnd()) { + DateTime.MAX at zone + } else { + endExclusive at zone + } + + start until end + } + } +} + +/** + * Combines this [InstantInterval] with a [TimeZone] to create an equivalent [ZonedDateTimeInterval] where both + * endpoints are in [zone]. + */ +infix fun InstantInterval.at(zone: TimeZone): ZonedDateTimeInterval { + return when { + isEmpty() -> ZonedDateTimeInterval.EMPTY + isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED + else -> { + val start = if (hasUnboundedStart()) { + DateTime.MIN at zone + } else { + start at zone + } + + val end = if (hasUnboundedEnd()) { + DateTime.MAX at zone + } else { + endExclusive at zone + } + + start until end + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/islandtime/ranges/Conversions.kt b/core/src/commonMain/kotlin/io/islandtime/ranges/Conversions.kt new file mode 100644 index 000000000..1376cfb0d --- /dev/null +++ b/core/src/commonMain/kotlin/io/islandtime/ranges/Conversions.kt @@ -0,0 +1,217 @@ +@file:JvmMultifileClass +@file:JvmName("RangesKt") + +package io.islandtime.ranges + +import io.islandtime.* +import io.islandtime.base.TimePoint +import io.islandtime.measures.days +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Returns this interval with the precision reduced to just the date. + */ +fun DateTimeInterval.toDateRange(): DateRange = toDateRange { this } + +/** + * Returns this interval with the precision reduced to just the date. + */ +fun OffsetDateTimeInterval.toDateRange(): DateRange = toDateRange(OffsetDateTime::dateTime) + +/** + * Returns this interval with the precision reduced to just the date. + */ +fun ZonedDateTimeInterval.toDateRange(): DateRange = toDateRange(ZonedDateTime::dateTime) + +/** + * Converts this interval to the equivalent [DateRange] when both endpoints are in [zone]. + */ +fun InstantInterval.toDateRangeAt(zone: TimeZone): DateRange = toDateRange { toDateTimeOrUnboundedAt(zone) } + +/** + * Returns this interval with the precision reduced to only the local date and time. + */ +fun OffsetDateTimeInterval.toDateTimeInterval(): DateTimeInterval = toDateTimeInterval(OffsetDateTime::dateTime) + +/** + * Returns this interval with the precision reduced to only the local date and time. + */ +fun ZonedDateTimeInterval.toDateTimeInterval(): DateTimeInterval = toDateTimeInterval(ZonedDateTime::dateTime) + +/** + * Converts this interval to the equivalent [DateTimeInterval] when both endpoints are in [zone]. + */ +fun InstantInterval.toDateTimeIntervalAt(zone: TimeZone): DateTimeInterval { + return toDateTimeInterval { toDateTimeOrUnboundedAt(zone) } +} + +/** + * Converts this interval to an [OffsetDateTimeInterval]. + * + * While similar to `ZonedDateTime`, an `OffsetDateTime` representation is unaffected by time zone rule changes or + * database differences between systems, making it better suited for use cases involving persistence or network + * transfer. + */ +fun ZonedDateTimeInterval.toOffsetDateTimeInterval(): OffsetDateTimeInterval { + return mapEndpointsOdt { it.toOffsetDateTime() } +} + +/** + * Converts this interval to an equivalent [ZonedDateTimeInterval] where both endpoints are given a fixed-offset time + * zone. + * + * This comes with the caveat that a fixed-offset zone lacks knowledge of any region and will not respond to daylight + * savings time changes. To convert each endpoint to a region-based zone, use [toZonedDateTimeInterval] instead. + * + * @see toZonedDateTimeInterval + */ +fun OffsetDateTimeInterval.asZonedDateTimeInterval(): ZonedDateTimeInterval { + return mapEndpointsZdt { it.asZonedDateTime() } +} + +/** + * Converts this interval to a [ZonedDateTimeInterval] using the specified [strategy] to adjust each endpoint to a valid + * date, time, and offset in [zone]. + * + * - [OffsetConversionStrategy.PRESERVE_INSTANT] - Preserve the instant captured by the date, time, and offset, + * ignoring the local time. + * + * - [OffsetConversionStrategy.PRESERVE_LOCAL_TIME] - Preserve the local date and time in the new time zone, adjusting + * the offset if needed. + * + * Alternatively, you can use [asZonedDateTimeInterval] to convert each endpoint to a [ZonedDateTime] with an equivalent + * fixed-offset zone. However, this comes with the caveat that a fixed-offset zone lacks knowledge of any region and + * will not respond to daylight savings time changes. + * + * @see asZonedDateTimeInterval + */ +fun OffsetDateTimeInterval.toZonedDateTimeInterval( + zone: TimeZone, + strategy: OffsetConversionStrategy +): ZonedDateTimeInterval { + return mapEndpointsZdt { + when (it.dateTime) { + // FIXME: Need to move away from using MIN and MAX in ranges. This is awkward. + DateTime.MIN -> DateTime.MIN at zone + DateTime.MAX -> DateTime.MAX at zone + else -> it.toZonedDateTime(zone, strategy) + } + } +} + +/** + * Converts this range to an [InstantInterval] between the start of the first day and the end of the last day in [zone]. + */ +fun DateRange.toInstantIntervalAt(zone: TimeZone): InstantInterval { + return when { + isEmpty() -> InstantInterval.EMPTY + isUnbounded() -> InstantInterval.UNBOUNDED + else -> { + val start = if (hasUnboundedStart()) Instant.MIN else start.startOfDayAt(zone).toInstant() + val end = if (hasUnboundedEnd()) Instant.MAX else endInclusive.endOfDayAt(zone).toInstant() + start..end + } + } +} + +/** + * Converts this interval to an [InstantInterval] where both endpoints are in [zone]. + * + * Due to daylight savings time transitions, there a few complexities to be aware of. If the local time of either + * endpoint falls within a gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it + * falls within an overlap (meaning the local time exists twice), the earlier offset will be used. + */ +fun DateTimeInterval.toInstantIntervalAt(zone: TimeZone): InstantInterval { + return when { + isEmpty() -> InstantInterval.EMPTY + isUnbounded() -> InstantInterval.UNBOUNDED + else -> { + val start = if (hasUnboundedStart()) Instant.MIN else (start at zone).toInstant() + val end = if (hasUnboundedEnd()) Instant.MAX else (endExclusive at zone).toInstant() + start until end + } + } +} + +/** + * Converts this interval to an [InstantInterval]. + */ +fun OffsetDateTimeInterval.toInstantInterval(): InstantInterval { + return (this as TimePointInterval<*>).toInstantInterval() +} + +/** + * Converts this interval to an [InstantInterval]. + */ +fun ZonedDateTimeInterval.toInstantInterval(): InstantInterval { + return (this as TimePointInterval<*>).toInstantInterval() +} + +private inline fun TimeInterval.toDateRange(toDateTime: T.() -> DateTime): DateRange { + return when { + isEmpty() -> DateRange.EMPTY + isUnbounded() -> DateRange.UNBOUNDED + else -> { + val endDateTime = toDateTime(endExclusive) + + val endDate = if (endDateTime.time == Time.MIDNIGHT) { + endDateTime.date - 1.days + } else { + endDateTime.date + } + + toDateTime(start).date..endDate + } + } +} + +private inline fun > TimePointInterval.toDateTimeInterval( + toDateTime: T.() -> DateTime +): DateTimeInterval { + return when { + isEmpty() -> DateTimeInterval.EMPTY + isUnbounded() -> DateTimeInterval.UNBOUNDED + else -> toDateTime(start) until toDateTime(endExclusive) + } +} + +private fun TimePointInterval<*>.toInstantInterval(): InstantInterval { + return when { + isEmpty() -> InstantInterval.EMPTY + isUnbounded() -> InstantInterval.UNBOUNDED + else -> { + val startInstant = if (hasUnboundedStart()) Instant.MIN else start.toInstant() + val endInstant = if (hasUnboundedEnd()) Instant.MAX else endExclusive.toInstant() + startInstant until endInstant + } + } +} + +private inline fun > TimePointInterval.mapEndpointsZdt( + transform: (T) -> ZonedDateTime +): ZonedDateTimeInterval { + return when { + isEmpty() -> ZonedDateTimeInterval.EMPTY + isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED + else -> transform(start) until transform(endExclusive) + } +} + +private inline fun > TimePointInterval.mapEndpointsOdt( + transform: (T) -> OffsetDateTime +): OffsetDateTimeInterval { + return when { + isEmpty() -> OffsetDateTimeInterval.EMPTY + isUnbounded() -> OffsetDateTimeInterval.UNBOUNDED + else -> transform(start) until transform(endExclusive) + } +} + +private fun Instant.toDateTimeOrUnboundedAt(zone: TimeZone): DateTime { + return when (this) { + Instant.MIN -> DateTime.MIN + Instant.MAX -> DateTime.MAX + else -> toDateTimeAt(zone) + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/islandtime/ranges/DateTimeInterval.kt b/core/src/commonMain/kotlin/io/islandtime/ranges/DateTimeInterval.kt index 02abf323f..7dcdf29cc 100644 --- a/core/src/commonMain/kotlin/io/islandtime/ranges/DateTimeInterval.kt +++ b/core/src/commonMain/kotlin/io/islandtime/ranges/DateTimeInterval.kt @@ -231,8 +231,7 @@ class DateTimeInterval( * @throws DateTimeParseException if parsing fails * @throws DateTimeException if the parsed time is invalid */ -fun String.toDateTimeInterval() = - toDateTimeInterval(DateTimeParsers.Iso.Extended.DATE_TIME_INTERVAL) +fun String.toDateTimeInterval() = toDateTimeInterval(DateTimeParsers.Iso.Extended.DATE_TIME_INTERVAL) /** * Convert a string to a [DateTimeInterval] using a specific parser. diff --git a/core/src/commonMain/kotlin/io/islandtime/ranges/InstantInterval.kt b/core/src/commonMain/kotlin/io/islandtime/ranges/InstantInterval.kt index c35eafeff..c6732183b 100644 --- a/core/src/commonMain/kotlin/io/islandtime/ranges/InstantInterval.kt +++ b/core/src/commonMain/kotlin/io/islandtime/ranges/InstantInterval.kt @@ -150,48 +150,16 @@ fun InstantInterval.randomOrNull(random: Random): Instant? { */ infix fun Instant.until(to: Instant) = InstantInterval(this, to) -/** - * Convert a range of dates into an [InstantInterval] between the starting and ending instants in a particular time - * zone. - */ -fun DateRange.toInstantIntervalAt(zone: TimeZone): InstantInterval { - return when { - isEmpty() -> InstantInterval.EMPTY - isUnbounded() -> InstantInterval.UNBOUNDED - else -> { - val start = if (hasUnboundedStart()) Instant.MIN else start.startOfDayAt(zone).instant - val end = if (hasUnboundedEnd()) Instant.MAX else endInclusive.endOfDayAt(zone).instant - start..end - } - } -} - -/** - * Convert an [OffsetDateTimeInterval] into an [InstantInterval]. - */ -fun OffsetDateTimeInterval.asInstantInterval(): InstantInterval { - return when { - isEmpty() -> InstantInterval.EMPTY - isUnbounded() -> InstantInterval.UNBOUNDED - else -> { - val startInstant = if (hasUnboundedStart()) Instant.MIN else start.instant - val endInstant = if (hasUnboundedEnd()) Instant.MAX else endExclusive.instant - startInstant until endInstant - } - } -} - -/** - * Convert a [ZonedDateTimeInterval] until an [InstantInterval]. - */ -fun ZonedDateTimeInterval.asInstantInterval(): InstantInterval { - return when { - isEmpty() -> InstantInterval.EMPTY - isUnbounded() -> InstantInterval.UNBOUNDED - else -> { - val startInstant = if (hasUnboundedStart()) Instant.MIN else start.instant - val endInstant = if (hasUnboundedEnd()) Instant.MAX else endExclusive.instant - startInstant until endInstant - } - } -} \ No newline at end of file +@Deprecated( + "Use toInstantInterval() instead.", + ReplaceWith("this.toInstantInterval()"), + DeprecationLevel.WARNING +) +fun OffsetDateTimeInterval.asInstantInterval(): InstantInterval = toInstantInterval() + +@Deprecated( + "Use toInstantInterval() instead.", + ReplaceWith("this.toInstantInterval()"), + DeprecationLevel.WARNING +) +fun ZonedDateTimeInterval.asInstantInterval(): InstantInterval = toInstantInterval() \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/islandtime/ranges/ZonedDateTimeInterval.kt b/core/src/commonMain/kotlin/io/islandtime/ranges/ZonedDateTimeInterval.kt index 1e70af600..7b234e5d9 100644 --- a/core/src/commonMain/kotlin/io/islandtime/ranges/ZonedDateTimeInterval.kt +++ b/core/src/commonMain/kotlin/io/islandtime/ranges/ZonedDateTimeInterval.kt @@ -25,8 +25,7 @@ class ZonedDateTimeInterval( /** * Convert this interval to a string in ISO-8601 extended format. */ - override fun toString() = - buildIsoString(MAX_ZONED_DATE_TIME_STRING_LENGTH, StringBuilder::appendZonedDateTime) + override fun toString() = buildIsoString(MAX_ZONED_DATE_TIME_STRING_LENGTH, StringBuilder::appendZonedDateTime) /** * Convert the interval into a [Period] of the same length. @@ -135,8 +134,7 @@ class ZonedDateTimeInterval( * @throws DateTimeParseException if parsing fails * @throws DateTimeException if the parsed time is invalid */ -fun String.toZonedDateTimeInterval() = - toZonedDateTimeInterval(DateTimeParsers.Iso.Extended.ZONED_DATE_TIME_INTERVAL) +fun String.toZonedDateTimeInterval() = toZonedDateTimeInterval(DateTimeParsers.Iso.Extended.ZONED_DATE_TIME_INTERVAL) /** * Convert a string to a [ZonedDateTimeInterval] using a specific parser. @@ -221,32 +219,12 @@ infix fun ZonedDateTime.until(to: ZonedDateTime) = ZonedDateTimeInterval(this, t * Convert a range of dates into a [ZonedDateTimeInterval] between the starting and ending instants in a particular * time zone. */ -fun DateRange.toZonedDateTimeIntervalAt(zone: TimeZone): ZonedDateTimeInterval { - return when { - isEmpty() -> ZonedDateTimeInterval.EMPTY - isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED - start == endInclusive -> { - val zonedStart = start.startOfDayAt(zone) - val zonedEnd = zonedStart + 1.days - zonedStart until zonedEnd - } - else -> { - val start = if (hasUnboundedStart()) { - DateTime.MIN at zone - } else { - start.startOfDayAt(zone) - } - - val end = if (hasUnboundedEnd()) { - DateTime.MAX at zone - } else { - endInclusive.endOfDayAt(zone) - } - - start..end - } - } -} +@Deprecated( + "Use 'at' instead.", + ReplaceWith("this at zone"), + DeprecationLevel.WARNING +) +fun DateRange.toZonedDateTimeInterval(zone: TimeZone): ZonedDateTimeInterval = this at zone /** * Get the [Period] between two zoned date-times, adjusting the time zone of [endExclusive] if necessary to match the diff --git a/core/src/commonTest/kotlin/io/islandtime/ConversionsTest.kt b/core/src/commonTest/kotlin/io/islandtime/ConversionsTest.kt new file mode 100644 index 000000000..e43fe10ee --- /dev/null +++ b/core/src/commonTest/kotlin/io/islandtime/ConversionsTest.kt @@ -0,0 +1,169 @@ +package io.islandtime + +import io.islandtime.measures.hours +import io.islandtime.test.AbstractIslandTimeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConversionsTest : AbstractIslandTimeTest() { + private val nyZone = TimeZone("America/New_York") + private val denverZone = TimeZone("America/Denver") + + @Test + fun `YearMonth_toYear() converts to Year`() { + assertEquals(Year(2018), YearMonth(2018, Month.JULY).toYear()) + } + + @Test + fun `Date_toYear() converts to Year`() { + assertEquals(Year(2018), Date(2018, Month.JULY, 4).toYear()) + } + + @Test + fun `DateTime_toYear() converts to Year`() { + val dateTime = Date(2019, 3, 3) at Time(7, 0) + assertEquals(Year(2019), dateTime.toYear()) + } + + @Test + fun `OffsetDateTime_toYear() converts to Year`() { + val offsetDateTime = + Date(2019, 3, 3) at Time(7, 0) at (-7).hours.asUtcOffset() + + assertEquals(Year(2019), offsetDateTime.toYear()) + } + + @Test + fun `ZonedDateTime_toYear() converts to Year`() { + val zonedDateTime = DateTime(2019, 3, 3, 7, 0) at denverZone + assertEquals(Year(2019), zonedDateTime.toYear()) + } + + @Test + fun `Date_toYearMonth() converts to YearMonth`() { + assertEquals(YearMonth(2018, Month.JULY), Date(2018, Month.JULY, 4).toYearMonth()) + } + + @Test + fun `DateTime_toYearMonth() converts to YearMonth`() { + val dateTime = Date(2019, 3, 3) at Time(7, 0) + assertEquals(YearMonth(2019, 3), dateTime.toYearMonth()) + } + + @Test + fun `OffsetDateTime_toYearMonth() converts to YearMonth`() { + val offsetDateTime = + Date(2019, 3, 3) at Time(7, 0) at (-7).hours.asUtcOffset() + + assertEquals(YearMonth(2019, 3), offsetDateTime.toYearMonth()) + } + + @Test + fun `ZonedDateTime_toYearMonth() converts to YearMonth`() { + val zonedDateTime = DateTime(2019, 3, 3, 7, 0) at denverZone + assertEquals(YearMonth(2019, 3), zonedDateTime.toYearMonth()) + } + + @Test + fun `Instant_toDateTimeAt() converts to DateTime at zone`() { + val instant = "1980-09-10T14:30Z".toInstant() + + assertEquals( + Date(1980, 9, 10) at Time(10, 30), + instant.toDateTimeAt(nyZone) + ) + } + + @Test + fun `OffsetDateTime_toOffsetTime() converts to OffsetTime`() { + val offsetDateTime = + DateTime(2019, 3, 3, 7, 0) at (-7).hours.asUtcOffset() + + assertEquals(OffsetTime(Time(7, 0), UtcOffset((-7).hours)), offsetDateTime.toOffsetTime()) + } + + @Test + fun `ZonedDateTime_toOffsetTime() converts to OffsetTime`() { + val zonedDateTime = DateTime(2019, 3, 3, 7, 0) at denverZone + assertEquals(OffsetTime(Time(7, 0), UtcOffset((-7).hours)), zonedDateTime.toOffsetTime()) + } + + @Test + fun `ZonedDateTime_toOffsetDateTime() converts to OffsetDateTime`() { + assertEquals( + "1970-01-01T00:00Z".toOffsetDateTime(), + "1970-01-01T00:00Z".toZonedDateTime().toOffsetDateTime() + ) + + assertEquals( + "2017-02-28T14:00:00.123456789-07:00".toOffsetDateTime(), + "2017-02-28T14:00:00.123456789-07:00[America/Denver]".toZonedDateTime().toOffsetDateTime() + ) + } + + @Test + fun `OffsetDateTime_toZonedDateTime() converts to ZonedDateTime preserving local time`() { + val offsetDateTime = + Date(2019, 3, 3) at Time(1, 0) at UtcOffset((-5).hours) + + assertEquals( + ZonedDateTime( + 2019, + 3, + 3, + 1, + 0, + 0, + 0, + denverZone + ), + offsetDateTime.toZonedDateTime(denverZone, OffsetConversionStrategy.PRESERVE_LOCAL_TIME) + ) + } + + @Test + fun `OffsetDateTime_toZonedDateTime() converts to ZonedDateTime preserving instant`() { + val offsetDateTime = + Date(2019, 3, 3) at Time(1, 0) at UtcOffset((-5).hours) + + assertEquals( + ZonedDateTime( + 2019, + 3, + 2, + 23, + 0, + 0, + 0, + denverZone + ), + offsetDateTime.toZonedDateTime(denverZone, OffsetConversionStrategy.PRESERVE_INSTANT) + ) + } + + @Test + fun `OffsetDateTime_toInstant() returns an equivalent Instant`() { + assertEquals( + "1970-01-01T00:00Z".toInstant(), + "1970-01-01T00:00Z".toOffsetDateTime().toInstant() + ) + + assertEquals( + "2017-02-28T21:00:00.123456789Z".toInstant(), + "2017-02-28T14:00:00.123456789-07:00".toOffsetDateTime().toInstant() + ) + } + + @Test + fun `ZonedDateTime_toInstant() returns an equivalent Instant`() { + assertEquals( + "1970-01-01T00:00Z".toInstant(), + "1970-01-01T00:00Z".toZonedDateTime().toInstant() + ) + + assertEquals( + "2017-02-28T21:00:00.123456789Z".toInstant(), + "2017-02-28T14:00:00.123456789-07:00[America/Denver]".toZonedDateTime().toInstant() + ) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/io/islandtime/DateTest.kt b/core/src/commonTest/kotlin/io/islandtime/DateTest.kt index 4aeb5438d..829dd85da 100644 --- a/core/src/commonTest/kotlin/io/islandtime/DateTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/DateTest.kt @@ -633,11 +633,6 @@ class DateTest : AbstractIslandTimeTest() { ) } - @Test - fun `yearMonth property returns a YearMonth with the same month and year`() { - assertEquals(YearMonth(2018, Month.JULY), Date(2018, Month.JULY, 4).yearMonth) - } - @Test fun `daysSinceUnixEpoch property works correctly`() { assertEquals(0L.days, Date(1970, Month.JANUARY, 1).daysSinceUnixEpoch) diff --git a/core/src/commonTest/kotlin/io/islandtime/DateTimeTest.kt b/core/src/commonTest/kotlin/io/islandtime/DateTimeTest.kt index 0772d7e4d..7cafd79e9 100644 --- a/core/src/commonTest/kotlin/io/islandtime/DateTimeTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/DateTimeTest.kt @@ -201,7 +201,7 @@ class DateTimeTest : AbstractIslandTimeTest() { } @Test - fun `unixEpochMillisecondAt() returns the millisecond of the unix epoch`() { + fun `millisecondOfUnixEpochAt() returns the millisecond of the unix epoch`() { assertEquals( 1L, (Date(1970, Month.JANUARY, 1) at Time(1, 0, 0, 1_999_999)) diff --git a/core/src/commonTest/kotlin/io/islandtime/OffsetDateTimeTest.kt b/core/src/commonTest/kotlin/io/islandtime/OffsetDateTimeTest.kt index 1c8053a8e..3d42b6af7 100644 --- a/core/src/commonTest/kotlin/io/islandtime/OffsetDateTimeTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/OffsetDateTimeTest.kt @@ -204,19 +204,6 @@ class OffsetDateTimeTest : AbstractIslandTimeTest() { assertEquals(4, testOffset.nanosecond) } - @Test - fun `instant property returns an equivalent Instant`() { - assertEquals( - "1970-01-01T00:00Z".toInstant(), - "1970-01-01T00:00Z".toOffsetDateTime().instant - ) - - assertEquals( - "2017-02-28T21:00:00.123456789Z".toInstant(), - "2017-02-28T14:00:00.123456789-07:00".toOffsetDateTime().instant - ) - } - @Test fun `adjustedTo() changes the offset while preserving the instant represented by it`() { assertEquals( diff --git a/core/src/commonTest/kotlin/io/islandtime/ZonedDateTimeTest.kt b/core/src/commonTest/kotlin/io/islandtime/ZonedDateTimeTest.kt index 73540f6ad..1c9ea9f13 100644 --- a/core/src/commonTest/kotlin/io/islandtime/ZonedDateTimeTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/ZonedDateTimeTest.kt @@ -8,8 +8,8 @@ import io.islandtime.zone.TimeZoneRulesException import kotlin.test.* class ZonedDateTimeTest : AbstractIslandTimeTest() { - private val nyZone = "America/New_York".toTimeZone() - private val denverZone = "America/Denver".toTimeZone() + private val nyZone = TimeZone("America/New_York") + private val denverZone = TimeZone("America/Denver") @Test fun `throws an exception when constructed with a TimeZone that has no rules`() { @@ -170,46 +170,6 @@ class ZonedDateTimeTest : AbstractIslandTimeTest() { } } - @Test - fun `OffsetDateTime_dateTimeAt() returns a ZonedDateTime with a similar local date and time`() { - val offsetDateTime = - Date(2019, 3, 3) at Time(1, 0) at UtcOffset((-5).hours) - - assertEquals( - ZonedDateTime( - 2019, - 3, - 2, - 23, - 0, - 0, - 0, - denverZone - ), - offsetDateTime.dateTimeAt(denverZone) - ) - } - - @Test - fun `OffsetDateTime_instantAt() returns a ZonedDateTime with the same instant`() { - val offsetDateTime = - Date(2019, 3, 3) at Time(1, 0) at UtcOffset((-5).hours) - - assertEquals( - ZonedDateTime( - 2019, - 3, - 3, - 1, - 0, - 0, - 0, - nyZone - ), - offsetDateTime.instantAt(nyZone) - ) - } - @Test fun `at infix creates a ZonedDateTime from a DateTime`() { assertEquals( @@ -269,8 +229,7 @@ class ZonedDateTimeTest : AbstractIslandTimeTest() { 821_000_000, nyZone ), - Instant(1566256047821L.milliseconds) - at nyZone + Instant(1566256047821L.milliseconds) at nyZone ) } @@ -361,14 +320,12 @@ class ZonedDateTimeTest : AbstractIslandTimeTest() { val zonedDateTime = DateTime(2019, 3, 3, 7, 0) at denverZone assertEquals(2019, zonedDateTime.year) - assertEquals(YearMonth(2019, 3), zonedDateTime.yearMonth) assertEquals(Month.MARCH, zonedDateTime.month) assertEquals(3, zonedDateTime.monthNumber) assertEquals(3, zonedDateTime.dayOfMonth) assertEquals(Date(2019, 3, 3), zonedDateTime.date) assertEquals(Time(7, 0), zonedDateTime.time) assertEquals(DateTime(2019, 3, 3, 7, 0), zonedDateTime.dateTime) - assertEquals(OffsetTime(Time(7, 0), UtcOffset((-7).hours)), zonedDateTime.offsetTime) } @Test @@ -582,32 +539,6 @@ class ZonedDateTimeTest : AbstractIslandTimeTest() { ) } - @Test - fun `instant property returns an equivalent Instant`() { - assertEquals( - "1970-01-01T00:00Z".toInstant(), - "1970-01-01T00:00Z".toZonedDateTime().instant - ) - - assertEquals( - "2017-02-28T21:00:00.123456789Z".toInstant(), - "2017-02-28T14:00:00.123456789-07:00[America/Denver]".toZonedDateTime().instant - ) - } - - @Test - fun `offsetDateTime property returns an equivalent OffsetDateTime`() { - assertEquals( - "1970-01-01T00:00Z".toOffsetDateTime(), - "1970-01-01T00:00Z".toZonedDateTime().offsetDateTime - ) - - assertEquals( - "2017-02-28T14:00:00.123456789-07:00".toOffsetDateTime(), - "2017-02-28T14:00:00.123456789-07:00[America/Denver]".toZonedDateTime().offsetDateTime - ) - } - @Test fun `add period of zero`() { val zonedDateTime = DateTime(2016, Month.FEBRUARY, 29, 13, 0) at nyZone diff --git a/core/src/commonTest/kotlin/io/islandtime/operators/RoundDownTest.kt b/core/src/commonTest/kotlin/io/islandtime/operators/RoundDownTest.kt index aa01d5bce..3bed92be9 100644 --- a/core/src/commonTest/kotlin/io/islandtime/operators/RoundDownTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/operators/RoundDownTest.kt @@ -499,8 +499,8 @@ class RoundDownTest { assertEquals(outDateTime.time at testOffset, inDateTime.time.at(testOffset).roundedDownTo(unit)) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownTo(unit) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownTo(unit) ) } } @@ -519,8 +519,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } @@ -539,8 +539,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } @@ -559,8 +559,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } @@ -579,8 +579,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } @@ -599,8 +599,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } @@ -619,8 +619,8 @@ class RoundDownTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedDownToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedDownToNearest(increment) ) } } diff --git a/core/src/commonTest/kotlin/io/islandtime/operators/RoundTest.kt b/core/src/commonTest/kotlin/io/islandtime/operators/RoundTest.kt index cadf59a27..85b37ca76 100644 --- a/core/src/commonTest/kotlin/io/islandtime/operators/RoundTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/operators/RoundTest.kt @@ -523,8 +523,8 @@ class RoundTest { assertEquals(outDateTime.time at testOffset, inDateTime.time.at(testOffset).roundedTo(unit)) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedTo(unit) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedTo(unit) ) } } @@ -543,8 +543,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } @@ -563,8 +563,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } @@ -583,8 +583,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } @@ -603,8 +603,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } @@ -623,8 +623,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } @@ -643,8 +643,8 @@ class RoundTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedToNearest(increment) ) } } diff --git a/core/src/commonTest/kotlin/io/islandtime/operators/RoundUpTest.kt b/core/src/commonTest/kotlin/io/islandtime/operators/RoundUpTest.kt index 13e6304f0..8c67930ef 100644 --- a/core/src/commonTest/kotlin/io/islandtime/operators/RoundUpTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/operators/RoundUpTest.kt @@ -514,8 +514,8 @@ class RoundUpTest { assertEquals(outDateTime.time at testOffset, inDateTime.time.at(testOffset).roundedUpTo(unit)) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpTo(unit) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpTo(unit) ) } } @@ -534,8 +534,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } @@ -554,8 +554,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } @@ -574,8 +574,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } @@ -594,8 +594,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } @@ -614,8 +614,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } @@ -634,8 +634,8 @@ class RoundUpTest { ) assertEquals( - outDateTime.instantAt(UtcOffset.ZERO), - inDateTime.instantAt(UtcOffset.ZERO).roundedUpToNearest(increment) + outDateTime.toInstantAt(UtcOffset.ZERO), + inDateTime.toInstantAt(UtcOffset.ZERO).roundedUpToNearest(increment) ) } } diff --git a/core/src/commonTest/kotlin/io/islandtime/ranges/BuildersTest.kt b/core/src/commonTest/kotlin/io/islandtime/ranges/BuildersTest.kt new file mode 100644 index 000000000..c7ad423b7 --- /dev/null +++ b/core/src/commonTest/kotlin/io/islandtime/ranges/BuildersTest.kt @@ -0,0 +1,159 @@ +package io.islandtime.ranges + +import io.islandtime.* +import io.islandtime.test.AbstractIslandTimeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class BuildersTest : AbstractIslandTimeTest() { + private val nyZone = TimeZone("America/New_York") + + @Test + fun `convert empty DateRange to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.EMPTY, DateRange.EMPTY at nyZone) + } + + @Test + fun `convert unbounded DateRange to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.UNBOUNDED, DateRange.UNBOUNDED at nyZone) + } + + @Test + fun `convert half-bounded DateRange to ZonedDateTimeInterval`() { + val dateRange1 = Date.MIN..Date(2019, Month.MARCH, 12) + + assertEquals( + ZonedDateTimeInterval( + DateTime.MIN at nyZone, + Date(2019, Month.MARCH, 13) at Time.MIDNIGHT at nyZone + ), + dateRange1 at nyZone + ) + + val dateRange2 = Date(2019, Month.MARCH, 10)..Date.MAX + + assertEquals( + ZonedDateTimeInterval( + Date(2019, Month.MARCH, 10) at Time.MIDNIGHT at nyZone, + DateTime.MAX at nyZone + ), + dateRange2 at nyZone + ) + } + + @Test + fun `convert bounded DateRange to ZonedDateTimeInterval`() { + val dateRange1 = Date(2019, Month.MARCH, 10)..Date(2019, Month.MARCH, 12) + + assertEquals( + ZonedDateTimeInterval( + Date(2019, Month.MARCH, 10) at Time.MIDNIGHT at nyZone, + Date(2019, Month.MARCH, 13) at Time.MIDNIGHT at nyZone + ), + dateRange1 at nyZone + ) + + val dateRange2 = Date(2019, Month.MARCH, 10)..Date(2019, Month.MARCH, 10) + + assertEquals( + ZonedDateTimeInterval( + Date(2019, Month.MARCH, 10) at Time.MIDNIGHT at nyZone, + Date(2019, Month.MARCH, 11) at Time.MIDNIGHT at nyZone + ), + dateRange2 at nyZone + ) + } + + @Test + fun `convert empty DateTimeInterval to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.EMPTY, DateTimeInterval.EMPTY at nyZone) + } + + @Test + fun `convert unbounded DateTimeInterval to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.UNBOUNDED, DateTimeInterval.UNBOUNDED at nyZone) + } + + @Test + fun `convert half-bounded DateTimeInterval to ZonedDateTimeInterval`() { + val interval1 = DateTime.MIN until DateTime(2019, Month.MARCH, 12, 2, 0) + + assertEquals( + ZonedDateTimeInterval( + DateTime.MIN at nyZone, + DateTime(2019, Month.MARCH, 12, 2, 0) at nyZone + ), + interval1 at nyZone + ) + + val interval2 = DateTime(2019, Month.MARCH, 10, 2, 0) until DateTime.MAX + + assertEquals( + ZonedDateTimeInterval( + DateTime(2019, Month.MARCH, 10, 2, 0) at nyZone, + DateTime.MAX at nyZone + ), + interval2 at nyZone + ) + } + + @Test + fun `convert bounded DateTimeInterval to ZonedDateTimeInterval`() { + val interval = DateTime(2019, Month.MARCH, 10, 2, 0) until + DateTime(2019, Month.MARCH, 12, 14, 0) + + assertEquals( + ZonedDateTimeInterval( + DateTime(2019, Month.MARCH, 10, 2, 0) at nyZone, + DateTime(2019, Month.MARCH, 12, 14, 0) at nyZone + ), + interval at nyZone + ) + } + + @Test + fun `convert empty InstantInterval to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.EMPTY, InstantInterval.EMPTY at nyZone) + } + + @Test + fun `convert unbounded InstantInterval to ZonedDateTimeInterval`() { + assertEquals(ZonedDateTimeInterval.UNBOUNDED, InstantInterval.UNBOUNDED at nyZone) + } + + @Test + fun `convert half-bounded InstantInterval to ZonedDateTimeInterval`() { + val interval1 = Instant.MIN until "2019-03-12T06:00Z".toInstant() + + assertEquals( + ZonedDateTimeInterval( + DateTime.MIN at nyZone, + DateTime(2019, Month.MARCH, 12, 2, 0) at nyZone + ), + interval1 at nyZone + ) + + val interval2 = "2019-03-10T07:00Z".toInstant() until Instant.MAX + + assertEquals( + ZonedDateTimeInterval( + DateTime(2019, Month.MARCH, 10, 2, 0) at nyZone, + DateTime.MAX at nyZone + ), + interval2 at nyZone + ) + } + + @Test + fun `convert bounded InstantInterval to ZonedDateTimeInterval`() { + val interval = "2019-03-10T07:00Z/2019-03-12T18:00Z".toInstantInterval() + + assertEquals( + ZonedDateTimeInterval( + DateTime(2019, Month.MARCH, 10, 2, 0) at nyZone, + DateTime(2019, Month.MARCH, 12, 14, 0) at nyZone + ), + interval at nyZone + ) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/io/islandtime/ranges/ConversionsTest.kt b/core/src/commonTest/kotlin/io/islandtime/ranges/ConversionsTest.kt new file mode 100644 index 000000000..0eea0ad7d --- /dev/null +++ b/core/src/commonTest/kotlin/io/islandtime/ranges/ConversionsTest.kt @@ -0,0 +1,562 @@ +package io.islandtime.ranges + +import io.islandtime.* +import io.islandtime.OffsetConversionStrategy.PRESERVE_INSTANT +import io.islandtime.OffsetConversionStrategy.PRESERVE_LOCAL_TIME +import io.islandtime.test.AbstractIslandTimeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConversionsTest : AbstractIslandTimeTest() { + private val nyZone = TimeZone("America/New_York") + + @Test + fun `convert empty DateTimeInterval to DateRange`() { + assertEquals(DateRange.EMPTY, DateTimeInterval.EMPTY.toDateRange()) + } + + @Test + fun `convert unbounded DateTimeInterval to DateRange`() { + assertEquals(DateRange.UNBOUNDED, DateTimeInterval.UNBOUNDED.toDateRange()) + } + + @Test + fun `convert half-bounded DateTimeInterval to DateRange`() { + val dateTimeInterval1 = "1968-10-05T05:00/..".toDateTimeInterval() + + assertEquals( + "1968-10-05".toDate()..Date.MAX, + dateTimeInterval1.toDateRange() + ) + + val dateTimeInterval2 = "../2000-01-03T10:00".toDateTimeInterval() + + assertEquals( + Date.MIN.."2000-01-03".toDate(), + dateTimeInterval2.toDateRange() + ) + } + + @Test + fun `convert bounded DateTimeInterval to DateRange`() { + val dateTimeInterval = "1968-10-05T05:00/2000-01-03T10:00".toDateTimeInterval() + assertEquals("1968-10-05".toDate().."2000-01-03".toDate(), dateTimeInterval.toDateRange()) + } + + @Test + fun `convert empty OffsetDateTimeInterval to DateRange`() { + assertEquals(DateRange.EMPTY, OffsetDateTimeInterval.EMPTY.toDateRange()) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to DateRange`() { + assertEquals(DateRange.UNBOUNDED, OffsetDateTimeInterval.UNBOUNDED.toDateRange()) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to DateRange`() { + val offsetDateTimeInterval1 = "1968-10-05T05:00-04:00/..".toOffsetDateTimeInterval() + assertEquals("1968-10-05".toDate()..Date.MAX, offsetDateTimeInterval1.toDateRange()) + + val offsetDateTimeInterval2 = "../2000-01-03T10:00-05:00".toOffsetDateTimeInterval() + assertEquals(Date.MIN.."2000-01-03".toDate(), offsetDateTimeInterval2.toDateRange()) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to DateRange`() { + val offsetDateTimeInterval = "1968-10-05T05:00-04:00/2000-01-03T10:00-05:00".toOffsetDateTimeInterval() + + assertEquals( + "1968-10-05".toDate().."2000-01-03".toDate(), + offsetDateTimeInterval.toDateRange() + ) + } + + @Test + fun `convert empty ZonedDateTimeInterval to DateRange`() { + assertEquals(DateRange.EMPTY, ZonedDateTimeInterval.EMPTY.toDateRange()) + } + + @Test + fun `convert unbounded ZonedDateTimeInterval to DateRange`() { + assertEquals(DateRange.UNBOUNDED, ZonedDateTimeInterval.UNBOUNDED.toDateRange()) + } + + @Test + fun `convert half-bounded ZonedDateTimeInterval to DateRange`() { + val zonedDateTimeInterval1 = "1968-10-05T05:00-04:00[America/New_York]/..".toZonedDateTimeInterval() + + assertEquals( + "1968-10-05".toDate()..Date.MAX, + zonedDateTimeInterval1.toDateRange() + ) + + val zonedDateTimeInterval2 = "../2000-01-03T10:00-05:00[America/New_York]".toZonedDateTimeInterval() + + assertEquals( + Date.MIN.."2000-01-03".toDate(), + zonedDateTimeInterval2.toDateRange() + ) + } + + @Test + fun `convert bounded ZonedDateTimeInterval to DateRange`() { + val zonedDateTimeInterval = "1968-10-05T05:00-04:00[America/New_York]/2000-01-03T10:00-05:00[America/New_York]" + .toZonedDateTimeInterval() + + assertEquals( + "1968-10-05".toDate().."2000-01-03".toDate(), + zonedDateTimeInterval.toDateRange() + ) + } + + @Test + fun `convert empty InstantInterval to DateRange`() { + assertEquals(DateRange.EMPTY, InstantInterval.EMPTY.toDateRangeAt(nyZone)) + } + + @Test + fun `convert unbounded InstantInterval to DateRange`() { + assertEquals(DateRange.UNBOUNDED, InstantInterval.UNBOUNDED.toDateRangeAt(nyZone)) + } + + @Test + fun `convert half-bounded InstantInterval to DateRange`() { + val instantInterval1 = "1968-10-05T04:00Z/..".toInstantInterval() + assertEquals("1968-10-05".toDate()..Date.MAX, instantInterval1.toDateRangeAt(nyZone)) + + val instantInterval2 = "../2000-01-04T04:59:59.999999999Z".toInstantInterval() + assertEquals(Date.MIN.."2000-01-03".toDate(), instantInterval2.toDateRangeAt(nyZone)) + } + + @Test + fun `convert bounded InstantInterval to DateRange`() { + val instantInterval = "1968-10-05T04:00Z/2000-01-04T04:59:59.999999999Z".toInstantInterval() + + assertEquals( + "1968-10-05".toDate().."2000-01-03".toDate(), + instantInterval.toDateRangeAt(nyZone) + ) + } + + @Test + fun `convert empty OffsetDateTimeInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.EMPTY, OffsetDateTimeInterval.EMPTY.toDateTimeInterval()) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.UNBOUNDED, OffsetDateTimeInterval.UNBOUNDED.toDateTimeInterval()) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to DateTimeInterval`() { + val offsetDateTimeInterval1 = "1968-10-05T05:00-04:00/..".toOffsetDateTimeInterval() + + assertEquals( + "1968-10-05T05:00".toDateTime() until DateTime.MAX, + offsetDateTimeInterval1.toDateTimeInterval() + ) + + val offsetDateTimeInterval2 = "../2000-01-03T10:00-05:00".toOffsetDateTimeInterval() + + assertEquals( + DateTime.MIN until "2000-01-03T10:00".toDateTime(), + offsetDateTimeInterval2.toDateTimeInterval() + ) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to DateTimeInterval`() { + val offsetDateTimeInterval = "1968-10-05T05:00-04:00/2000-01-03T10:00-05:00" + .toOffsetDateTimeInterval() + + assertEquals( + "1968-10-05T05:00".toDateTime() until "2000-01-03T10:00".toDateTime(), + offsetDateTimeInterval.toDateTimeInterval() + ) + } + + @Test + fun `convert empty ZonedDateTimeInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.EMPTY, ZonedDateTimeInterval.EMPTY.toDateTimeInterval()) + } + + @Test + fun `convert unbounded ZonedDateTimeInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.UNBOUNDED, ZonedDateTimeInterval.UNBOUNDED.toDateTimeInterval()) + } + + @Test + fun `convert half-bounded ZonedDateTimeInterval to DateTimeInterval`() { + val zonedDateTimeInterval1 = "1968-10-05T05:00-04:00[America/New_York]/..".toZonedDateTimeInterval() + + assertEquals( + "1968-10-05T05:00".toDateTime() until DateTime.MAX, + zonedDateTimeInterval1.toDateTimeInterval() + ) + + val zonedDateTimeInterval2 = "../2000-01-03T10:00-05:00[America/New_York]".toZonedDateTimeInterval() + + assertEquals( + DateTime.MIN until "2000-01-03T10:00".toDateTime(), + zonedDateTimeInterval2.toDateTimeInterval() + ) + } + + @Test + fun `convert bounded ZonedDateTimeInterval to DateTimeInterval`() { + val zonedDateTimeInterval = "1968-10-05T05:00-04:00[America/New_York]/2000-01-03T10:00-05:00[America/New_York]" + .toZonedDateTimeInterval() + + assertEquals( + "1968-10-05T05:00".toDateTime() until "2000-01-03T10:00".toDateTime(), + zonedDateTimeInterval.toDateTimeInterval() + ) + } + + @Test + fun `convert empty InstantInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.EMPTY, InstantInterval.EMPTY.toDateTimeIntervalAt(nyZone)) + } + + @Test + fun `convert unbounded InstantInterval to DateTimeInterval`() { + assertEquals(DateTimeInterval.UNBOUNDED, InstantInterval.UNBOUNDED.toDateTimeIntervalAt(nyZone)) + } + + @Test + fun `convert half-bounded InstantInterval to DateTimeInterval`() { + val instantInterval1 = "1968-10-05T04:00Z/..".toInstantInterval() + + assertEquals( + "1968-10-05T00:00".toDateTime() until DateTime.MAX, + instantInterval1.toDateTimeIntervalAt(nyZone) + ) + + val instantInterval2 = "../2000-01-04T04:59:59.999999999Z".toInstantInterval() + + assertEquals( + DateTime.MIN until "2000-01-03T23:59:59.999999999".toDateTime(), + instantInterval2.toDateTimeIntervalAt(nyZone) + ) + } + + @Test + fun `convert bounded InstantInterval to DateTimeInterval`() { + val instantInterval = "1968-10-05T04:00Z/2000-01-04T04:59:59.999999999Z".toInstantInterval() + + assertEquals( + "1968-10-05T00:00".toDateTime() until "2000-01-03T23:59:59.999999999".toDateTime(), + instantInterval.toDateTimeIntervalAt(nyZone) + ) + } + + @Test + fun `convert empty ZonedDateTimeInterval to OffsetDateTimeInterval`() { + assertEquals(OffsetDateTimeInterval.EMPTY, ZonedDateTimeInterval.EMPTY.toOffsetDateTimeInterval()) + } + + @Test + fun `convert unbounded ZonedDateTimeInterval to OffsetDateTimeInterval`() { + assertEquals(OffsetDateTimeInterval.UNBOUNDED, ZonedDateTimeInterval.UNBOUNDED.toOffsetDateTimeInterval()) + } + + @Test + fun `convert half-bounded ZonedDateTimeInterval to OffsetDateTimeInterval`() { + val zonedDateTimeInterval1 = "1968-10-05T05:00-04:00[America/New_York]/..".toZonedDateTimeInterval() + + assertEquals( + "1968-10-05T05:00-04:00".toOffsetDateTime() until OffsetDateTime.MAX, + zonedDateTimeInterval1.toOffsetDateTimeInterval() + ) + + val zonedDateTimeInterval2 = "../2000-01-03T10:00-05:00[America/New_York]".toZonedDateTimeInterval() + + assertEquals( + OffsetDateTime.MIN until "2000-01-03T10:00-05:00".toOffsetDateTime(), + zonedDateTimeInterval2.toOffsetDateTimeInterval() + ) + } + + @Test + fun `convert bounded ZonedDateTimeInterval to OffsetDateTimeInterval`() { + val zonedDateTimeInterval = "1968-10-05T05:00-04:00[America/New_York]/2000-01-03T10:00-05:00[America/New_York]" + .toZonedDateTimeInterval() + + assertEquals( + "1968-10-05T05:00-04:00".toOffsetDateTime() until "2000-01-03T10:00-05:00".toOffsetDateTime(), + zonedDateTimeInterval.toOffsetDateTimeInterval() + ) + } + + @Test + fun `convert empty OffsetDateTimeInterval to ZonedDateTimeInterval with fixed-offset zone`() { + assertEquals(ZonedDateTimeInterval.EMPTY, OffsetDateTimeInterval.EMPTY.asZonedDateTimeInterval()) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to ZonedDateTimeInterval with fixed-offset zone`() { + assertEquals(ZonedDateTimeInterval.UNBOUNDED, OffsetDateTimeInterval.UNBOUNDED.asZonedDateTimeInterval()) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to ZonedDateTimeInterval with fixed-offset zone`() { + assertEquals( + "../1991-06-23T14:00-04:00".toZonedDateTimeInterval(), + "../1991-06-23T14:00-04:00".toOffsetDateTimeInterval().asZonedDateTimeInterval() + ) + assertEquals( + "1991-02-15T12:00-05:00/..".toZonedDateTimeInterval(), + "1991-02-15T12:00-05:00/..".toOffsetDateTimeInterval().asZonedDateTimeInterval() + ) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to ZonedDateTimeInterval with fixed-offset zone`() { + assertEquals( + "1991-02-15T12:00-05:00/1991-06-23T14:00-04:00".toZonedDateTimeInterval(), + "1991-02-15T12:00-05:00/1991-06-23T14:00-04:00".toOffsetDateTimeInterval().asZonedDateTimeInterval() + ) + } + + @Test + fun `convert empty OffsetDateTimeInterval to ZonedDateTimeInterval preserving instant`() { + assertEquals( + ZonedDateTimeInterval.EMPTY, + OffsetDateTimeInterval.EMPTY.toZonedDateTimeInterval(nyZone, PRESERVE_INSTANT) + ) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving instant`() { + assertEquals( + ZonedDateTimeInterval.UNBOUNDED, + OffsetDateTimeInterval.UNBOUNDED.toZonedDateTimeInterval(nyZone, PRESERVE_INSTANT) + ) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving instant`() { + assertEquals( + "../1991-06-23T15:00-04:00[America/New_York]".toZonedDateTimeInterval(), + "../1991-06-23T14:00-05:00".toOffsetDateTimeInterval().toZonedDateTimeInterval(nyZone, PRESERVE_INSTANT) + ) + assertEquals( + "1991-02-15T11:00-05:00[America/New_York]/..".toZonedDateTimeInterval(), + "1991-02-15T12:00-04:00/..".toOffsetDateTimeInterval().toZonedDateTimeInterval(nyZone, PRESERVE_INSTANT) + ) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving instant`() { + assertEquals( + "1991-02-15T11:00-05:00[America/New_York]/1991-06-23T15:00-04:00[America/New_York]" + .toZonedDateTimeInterval(), + "1991-02-15T12:00-04:00/1991-06-23T14:00-05:00" + .toOffsetDateTimeInterval() + .toZonedDateTimeInterval(nyZone, PRESERVE_INSTANT) + ) + } + + @Test + fun `convert empty OffsetDateTimeInterval to ZonedDateTimeInterval preserving local time`() { + assertEquals( + ZonedDateTimeInterval.EMPTY, + OffsetDateTimeInterval.EMPTY.toZonedDateTimeInterval(nyZone, PRESERVE_LOCAL_TIME) + ) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving local time`() { + assertEquals( + ZonedDateTimeInterval.UNBOUNDED, + OffsetDateTimeInterval.UNBOUNDED.toZonedDateTimeInterval(nyZone, PRESERVE_LOCAL_TIME) + ) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving local time`() { + assertEquals( + "../1991-06-23T14:00-04:00[America/New_York]".toZonedDateTimeInterval(), + "../1991-06-23T14:00-05:00".toOffsetDateTimeInterval().toZonedDateTimeInterval(nyZone, PRESERVE_LOCAL_TIME) + ) + assertEquals( + "1991-02-15T12:00-05:00[America/New_York]/..".toZonedDateTimeInterval(), + "1991-02-15T12:00-04:00/..".toOffsetDateTimeInterval().toZonedDateTimeInterval(nyZone, PRESERVE_LOCAL_TIME) + ) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to ZonedDateTimeInterval preserving local time`() { + assertEquals( + "1991-02-15T12:00-05:00[America/New_York]/1991-06-23T14:00-04:00[America/New_York]" + .toZonedDateTimeInterval(), + "1991-02-15T12:00-04:00/1991-06-23T14:00-05:00" + .toOffsetDateTimeInterval() + .toZonedDateTimeInterval(nyZone, PRESERVE_LOCAL_TIME) + ) + } + + @Test + fun `convert empty DateRange to InstantInterval`() { + assertEquals(InstantInterval.EMPTY, DateRange.EMPTY.toInstantIntervalAt(TimeZone.UTC)) + } + + @Test + fun `convert unbounded DateRange to InstantInterval`() { + assertEquals( + InstantInterval.UNBOUNDED, + DateRange.UNBOUNDED.toInstantIntervalAt(TimeZone.UTC) + ) + } + + @Test + fun `convert half-bounded DateRange to InstantInterval`() { + val dateRange1 = Date(1968, 10, 5)..Date.MAX + + assertEquals( + "1968-10-05T04:00Z".toInstant() until Instant.MAX, + dateRange1.toInstantIntervalAt(nyZone) + ) + + val dateRange2 = Date.MIN..Date(2000, 1, 3) + + assertEquals( + Instant.MIN until "2000-01-04T05:00Z".toInstant(), + dateRange2.toInstantIntervalAt(nyZone) + ) + } + + @Test + fun `convert bounded DateRange to InstantInterval`() { + val dateRange = Date(1968, 10, 5)..Date(2000, 1, 3) + + assertEquals( + "1968-10-05T04:00Z".toInstant() until "2000-01-04T05:00Z".toInstant(), + dateRange.toInstantIntervalAt(nyZone) + ) + } + + @Test + fun `convert empty DateTimeInterval to InstantInterval`() { + assertEquals(InstantInterval.EMPTY, DateTimeInterval.EMPTY.toInstantIntervalAt(TimeZone.UTC)) + } + + @Test + fun `convert unbounded DateTimeInterval to InstantInterval`() { + assertEquals( + InstantInterval.UNBOUNDED, + DateTimeInterval.UNBOUNDED.toInstantIntervalAt(TimeZone.UTC) + ) + } + + @Test + fun `convert half-bounded DateTimeInterval to InstantInterval`() { + val dateTimeInterval1 = DateTime(1968, 10, 5, 10, 0) until DateTime.MAX + + assertEquals( + "1968-10-05T14:00Z".toInstant() until Instant.MAX, + dateTimeInterval1.toInstantIntervalAt(nyZone) + ) + + val dateTimeInterval2 = DateTime.MIN until DateTime(2000, 1, 3, 10, 0) + + assertEquals( + Instant.MIN until "2000-01-03T15:00Z".toInstant(), + dateTimeInterval2.toInstantIntervalAt(nyZone) + ) + } + + @Test + fun `convert bounded DateTimeInterval to InstantInterval`() { + val dateTimeInterval = DateTime(1968, 10, 5, 10, 0) until + DateTime(2000, 1, 3, 10, 0) + + assertEquals( + "1968-10-05T14:00Z".toInstant() until "2000-01-03T15:00Z".toInstant(), + dateTimeInterval.toInstantIntervalAt(nyZone) + ) + } + + @Test + fun `convert empty OffsetDateTimeInterval to InstantInterval`() { + assertEquals(InstantInterval.EMPTY, OffsetDateTimeInterval.EMPTY.toInstantInterval()) + } + + @Test + fun `convert unbounded OffsetDateTimeInterval to InstantInterval`() { + assertEquals( + InstantInterval.UNBOUNDED, + OffsetDateTimeInterval.UNBOUNDED.toInstantInterval() + ) + } + + @Test + fun `convert half-bounded OffsetDateTimeInterval to InstantInterval`() { + val offsetDateTimeInterval1 = "1968-10-05T05:00-05:00".toOffsetDateTime() until OffsetDateTime.MAX + + assertEquals( + "1968-10-05T10:00Z".toInstant() until Instant.MAX, + offsetDateTimeInterval1.toInstantInterval() + ) + + val offsetDateTimeInterval2 = OffsetDateTime.MIN until "2000-01-03T10:00-05:00".toOffsetDateTime() + + assertEquals( + Instant.MIN until "2000-01-03T15:00Z".toInstant(), + offsetDateTimeInterval2.toInstantInterval() + ) + } + + @Test + fun `convert bounded OffsetDateTimeInterval to InstantInterval`() { + val offsetDateTimeInterval = + "1968-10-05T05:00-05:00".toOffsetDateTime() until "2000-01-03T10:00-05:00".toOffsetDateTime() + + assertEquals( + "1968-10-05T10:00Z".toInstant() until "2000-01-03T15:00Z".toInstant(), + offsetDateTimeInterval.toInstantInterval() + ) + } + + @Test + fun `convert empty ZonedDateTimeInterval to InstantInterval`() { + assertEquals(InstantInterval.EMPTY, ZonedDateTimeInterval.EMPTY.toInstantInterval()) + } + + @Test + fun `convert unbounded ZonedDateTimeInterval to InstantInterval`() { + assertEquals(InstantInterval.UNBOUNDED, ZonedDateTimeInterval.UNBOUNDED.toInstantInterval()) + } + + @Test + fun `convert half-bounded ZonedDateTimeInterval to InstantInterval`() { + val zonedDateTimeInterval1 = + "1968-10-05T05:00-05:00".toZonedDateTime() until ZonedDateTimeInterval.UNBOUNDED.endExclusive + + assertEquals( + "1968-10-05T10:00Z".toInstant() until Instant.MAX, + zonedDateTimeInterval1.toInstantInterval() + ) + + val zonedDateTimeInterval2 = + ZonedDateTimeInterval.UNBOUNDED.start until "2000-01-03T10:00-05:00".toZonedDateTime() + + assertEquals( + Instant.MIN until "2000-01-03T15:00Z".toInstant(), + zonedDateTimeInterval2.toInstantInterval() + ) + } + + @Test + fun `convert bounded ZonedDateTimeInterval to InstantInterval`() { + val zonedDateTimeInterval = + "1968-10-05T05:00-05:00".toZonedDateTime() until "2000-01-03T10:00-05:00".toZonedDateTime() + + assertEquals( + "1968-10-05T10:00Z".toInstant() until "2000-01-03T15:00Z".toInstant(), + zonedDateTimeInterval.toInstantInterval() + ) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/io/islandtime/ranges/InstantIntervalTest.kt b/core/src/commonTest/kotlin/io/islandtime/ranges/InstantIntervalTest.kt index 91f460076..58387a3bd 100644 --- a/core/src/commonTest/kotlin/io/islandtime/ranges/InstantIntervalTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/ranges/InstantIntervalTest.kt @@ -230,129 +230,6 @@ class InstantIntervalTest : AbstractIslandTimeTest() { assertEquals(1L.nanoseconds, (instant..instant).lengthInNanoseconds) } - @Test - fun `convert empty DateRange to InstantInterval`() { - assertEquals(InstantInterval.EMPTY, DateRange.EMPTY.toInstantIntervalAt(TimeZone.UTC)) - } - - @Test - fun `convert unbounded DateRange to InstantInterval`() { - assertEquals( - InstantInterval.UNBOUNDED, - DateRange.UNBOUNDED.toInstantIntervalAt(TimeZone.UTC) - ) - } - - @Test - fun `convert half-bounded DateRange to InstantInterval`() { - val dateRange1 = Date(1968, 10, 5)..Date.MAX - - assertEquals( - "1968-10-05T05:00Z".toInstant() until Instant.MAX, - dateRange1.toInstantIntervalAt((-5).hours.asUtcOffset().asTimeZone()) - ) - - val dateRange2 = Date.MIN..Date(2000, 1, 3) - - assertEquals( - Instant.MIN until "2000-01-04T05:00Z".toInstant(), - dateRange2.toInstantIntervalAt((-5).hours.asUtcOffset().asTimeZone()) - ) - } - - @Test - fun `convert bounded DateRange to InstantInterval`() { - val dateRange = Date(1968, 10, 5)..Date(2000, 1, 3) - - assertEquals( - "1968-10-05T05:00Z".toInstant() until "2000-01-04T05:00Z".toInstant(), - dateRange.toInstantIntervalAt((-5).hours.asUtcOffset().asTimeZone()) - ) - } - - @Test - fun `convert empty OffsetDateTimeInterval to InstantInterval`() { - assertEquals(InstantInterval.EMPTY, OffsetDateTimeInterval.EMPTY.asInstantInterval()) - } - - @Test - fun `convert unbounded OffsetDateTimeInterval to InstantInterval`() { - assertEquals( - InstantInterval.UNBOUNDED, - OffsetDateTimeInterval.UNBOUNDED.asInstantInterval() - ) - } - - @Test - fun `convert half-bounded OffsetDateTimeInterval to InstantInterval`() { - val offsetDateTimeInterval1 = - "1968-10-05T05:00-05:00".toOffsetDateTime() until OffsetDateTime.MAX - - assertEquals( - "1968-10-05T10:00Z".toInstant() until Instant.MAX, - offsetDateTimeInterval1.asInstantInterval() - ) - - val offsetDateTimeInterval2 = - OffsetDateTime.MIN until "2000-01-03T10:00-05:00".toOffsetDateTime() - - assertEquals( - Instant.MIN until "2000-01-03T15:00Z".toInstant(), - offsetDateTimeInterval2.asInstantInterval() - ) - } - - @Test - fun `convert bounded OffsetDateTimeInterval to InstantInterval`() { - val offsetDateTimeInterval = - "1968-10-05T05:00-05:00".toOffsetDateTime() until "2000-01-03T10:00-05:00".toOffsetDateTime() - - assertEquals( - "1968-10-05T10:00Z".toInstant() until "2000-01-03T15:00Z".toInstant(), - offsetDateTimeInterval.asInstantInterval() - ) - } - - @Test - fun `convert empty ZonedDateTimeInterval to InstantInterval`() { - assertEquals(InstantInterval.EMPTY, ZonedDateTimeInterval.EMPTY.asInstantInterval()) - } - - @Test - fun `convert unbounded ZonedDateTimeInterval to InstantInterval`() { - assertEquals(InstantInterval.UNBOUNDED, ZonedDateTimeInterval.UNBOUNDED.asInstantInterval()) - } - - @Test - fun `convert half-bounded ZonedDateTimeInterval to InstantInterval`() { - val zonedDateTimeInterval1 = - "1968-10-05T05:00-05:00".toZonedDateTime() until ZonedDateTimeInterval.UNBOUNDED.endExclusive - - assertEquals( - "1968-10-05T10:00Z".toInstant() until Instant.MAX, - zonedDateTimeInterval1.asInstantInterval() - ) - - val zonedDateTimeInterval2 = - ZonedDateTimeInterval.UNBOUNDED.start until "2000-01-03T10:00-05:00".toZonedDateTime() - - assertEquals( - Instant.MIN until "2000-01-03T15:00Z".toInstant(), - zonedDateTimeInterval2.asInstantInterval() - ) - } - - @Test - fun `convert bounded ZonedDateTimeInterval to InstantInterval`() { - val zonedDateTimeInterval = - "1968-10-05T05:00-05:00".toZonedDateTime() until "2000-01-03T10:00-05:00".toZonedDateTime() - - assertEquals( - "1968-10-05T10:00Z".toInstant() until "2000-01-03T15:00Z".toInstant(), - zonedDateTimeInterval.asInstantInterval() - ) - } - @Test fun `durationBetween() returns the duration between two instants`() { assertEquals( diff --git a/core/src/commonTest/kotlin/io/islandtime/ranges/ZonedDateTimeIntervalTest.kt b/core/src/commonTest/kotlin/io/islandtime/ranges/ZonedDateTimeIntervalTest.kt index 71747582a..f8475d0d5 100644 --- a/core/src/commonTest/kotlin/io/islandtime/ranges/ZonedDateTimeIntervalTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/ranges/ZonedDateTimeIntervalTest.kt @@ -7,6 +7,8 @@ import io.islandtime.test.AbstractIslandTimeTest import kotlin.test.* class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { + private val nyZone = TimeZone("America/New_York") + @Test fun `EMPTY returns an empty interval`() { assertTrue { ZonedDateTimeInterval.EMPTY.isEmpty() } @@ -31,9 +33,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `inclusive end creation handles unbounded correctly`() { - val timeZone = "America/New_York".toTimeZone() - val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at timeZone - val max = DateTime.MAX at timeZone + val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone + val max = DateTime.MAX at nyZone assertTrue { (start..max).hasUnboundedEnd() } assertFailsWith { start..max - 1.nanoseconds } @@ -42,8 +43,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `contains() returns true for dates within bounded range`() { - val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() - val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at "America/New_York".toTimeZone() + val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone + val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at nyZone assertTrue { start in start..end } assertTrue { end in start..end } @@ -53,8 +54,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `contains() returns true for dates within range with unbounded end`() { - val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() - val end = DateTime.MAX at "America/New_York".toTimeZone() + val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone + val end = DateTime.MAX at nyZone assertTrue { start in start..end } assertTrue { DateTime.MAX at "Etc/UTC".toTimeZone() in start..end } @@ -64,8 +65,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `contains() returns true for dates within range with unbounded start`() { - val start = DateTime.MIN at "America/New_York".toTimeZone() - val end = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() + val start = DateTime.MIN at nyZone + val end = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone assertTrue { start in start..end } assertTrue { end in start..end } @@ -76,8 +77,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `contains() returns false for out of range dates`() { - val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() - val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at "America/New_York".toTimeZone() + val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone + val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at nyZone assertFalse { start - 1.nanoseconds in start..end } assertFalse { end + 1.nanoseconds in start..end } @@ -87,8 +88,8 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `until infix operator constructs an interval with non-inclusive end`() { - val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() - val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at "America/New_York".toTimeZone() + val start = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone + val end = Date(2019, Month.MARCH, 12) at MIDNIGHT at nyZone val range = start until end assertEquals(start, range.start) @@ -98,12 +99,12 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `random() returns a date-time within the interval`() { - val start = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at TimeZone("America/New_York") - val end = Date(2019, Month.NOVEMBER, 2) at MIDNIGHT at TimeZone("America/New_York") + val start = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at nyZone + val end = Date(2019, Month.NOVEMBER, 2) at MIDNIGHT at nyZone val interval = start until end val randomZonedDateTime = interval.random() assertTrue { randomZonedDateTime in interval } - assertEquals(TimeZone("America/New_York"), randomZonedDateTime.zone) + assertEquals(nyZone, randomZonedDateTime.zone) } @Test @@ -113,7 +114,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `random() throws an exception when the interval is not bounded`() { - val dateTime = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at TimeZone("America/New_York") + val dateTime = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at nyZone assertFailsWith { ZonedDateTimeInterval.UNBOUNDED.random() } assertFailsWith { ZonedDateTimeInterval(start = dateTime).random() } assertFailsWith { ZonedDateTimeInterval(endExclusive = dateTime).random() } @@ -126,7 +127,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `randomOrNull() returns null when the interval is not bounded`() { - val dateTime = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at TimeZone("America/New_York") + val dateTime = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at nyZone assertNull(ZonedDateTimeInterval.UNBOUNDED.randomOrNull()) assertNull(ZonedDateTimeInterval(start = dateTime).randomOrNull()) assertNull(ZonedDateTimeInterval(endExclusive = dateTime).randomOrNull()) @@ -134,12 +135,12 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `randomOrNull() returns a date-time within the interval`() { - val start = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at TimeZone("America/New_York") + val start = Date(2019, Month.NOVEMBER, 1) at MIDNIGHT at nyZone val end = (start + 1.nanoseconds).adjustedTo(TimeZone("Europe/London")) val interval = start until end val randomZonedDateTime = interval.randomOrNull()!! assertTrue { randomZonedDateTime in interval } - assertEquals(TimeZone("America/New_York"), randomZonedDateTime.zone) + assertEquals(nyZone, randomZonedDateTime.zone) } @Test @@ -170,13 +171,13 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInNanoseconds returns 1 in an inclusive interval where the start and end instant are the same`() { - val instant = Date(2019, Month.MARCH, 10) at MIDNIGHT at "America/New_York".toTimeZone() + val instant = Date(2019, Month.MARCH, 10) at MIDNIGHT at nyZone assertEquals(1L.nanoseconds, (instant..instant).lengthInNanoseconds) } @Test fun `period of months during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10) at Time(1, 0) at zone val now = Date(2019, 4, 10) at Time(1, 0) at zone @@ -190,7 +191,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `period of days during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10) at Time(1, 0) at zone val now = Date(2019, 3, 11) at Time(1, 0) at zone @@ -204,7 +205,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `duration during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10) at Time(1, 0) at zone val now = Date(2019, 3, 11) at Time(1, 0) at zone @@ -216,7 +217,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `period of days during daylight savings overlap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 11, 3) at Time(1, 0) at zone val now = Date(2019, 11, 4) at Time(1, 0) at zone @@ -228,7 +229,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `duration during daylight savings overlap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 11, 3) at Time(1, 0) at zone val now = Date(2019, 11, 4) at Time(1, 0) at zone @@ -239,7 +240,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInYears property`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10).startOfDayAt(zone) val now = Date(2020, 3, 10).startOfDayAt(zone) @@ -251,7 +252,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInMonths property during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10).startOfDayAt(zone) val now = Date(2019, 4, 10).startOfDayAt(zone) @@ -263,7 +264,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInWeeks property during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10).startOfDayAt(zone) val now = Date(2019, 3, 17).startOfDayAt(zone) @@ -275,7 +276,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInDays property during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10).startOfDayAt(zone) val now = Date(2019, 3, 11).startOfDayAt(zone) @@ -287,7 +288,7 @@ class ZonedDateTimeIntervalTest : AbstractIslandTimeTest() { @Test fun `lengthInHours property during daylight savings gap`() { - val zone = "America/New_York".toTimeZone() + val zone = nyZone val then = Date(2019, 3, 10).startOfDayAt(zone) val now = Date(2019, 3, 10) at Time(5, 0) at zone diff --git a/docs/basics/intervals.md b/docs/basics/intervals.md index 42b56d4e1..46a44bcb1 100644 --- a/docs/basics/intervals.md +++ b/docs/basics/intervals.md @@ -64,6 +64,11 @@ A `DateRange` can be converted directly to an interval representing the period f val today: Date = Date.now() val dateRange: DateRange = today - 1.weeks until today val zone: TimeZone = TimeZone.systemDefault() + +// Convert to a ZonedDateTimeInterval +val zonedDateTimeInterval: ZonedDateTimeInterval = dateRange at zone + +// Convert to an InstantInterval val instantInterval: InstantInterval = dateRange.toInstantIntervalAt(zone) ``` @@ -74,7 +79,7 @@ Only `InstantInterval` allows iteration, though the other interval types can be ```kotlin val now: ZonedDateTime = ZonedDateTime.now() val zonedDateTimeInterval: ZonedDateTimeInterval = now until now + 1.weeks -val instantInterval: InstantInterval = zonedDateTimeInterval.asInstantInterval() +val instantInterval: InstantInterval = zonedDateTimeInterval.toInstantInterval() ``` Unlike with date ranges, the `step` is necessary to create a progression. @@ -126,8 +131,8 @@ val isoDateRangeString = dateRange.toString() val readDateRange = isoDateRangeString.toDateRange() val zone = TimeZone("America/New_York") -val zonedInterval: ZonedDateTimeInterval = dateRange.toZonedDateTimeIntervalAt(zone) -val isoZonedIntervalString = instantInterval.toString() +val zonedInterval: ZonedDateTimeInterval = dateRange at zone +val isoZonedIntervalString = zonedInterval.toString() // Output: 2020-03-01T00:00-05:00[America/New_York]/2020-05-13T23:59:59.999999999-04:00[America/New_York] val readZonedInterval = isoZonedIntervalString.toZonedDateTimeInterval()