diff --git a/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt b/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt index 1e04a5c03..e6fd01375 100644 --- a/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt +++ b/core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt @@ -24,10 +24,6 @@ class OffsetDateTime( val offset: UtcOffset ) : TimePoint { - init { - offset.validate() - } - /** * Creates an [OffsetDateTime]. * @throws DateTimeException if the offset is invalid @@ -143,21 +139,24 @@ class OffsetDateTime( ReplaceWith("this.toYearMonth()"), DeprecationLevel.ERROR ) - inline val yearMonth: YearMonth get() = toYearMonth() + inline val yearMonth: YearMonth + get() = toYearMonth() @Deprecated( "Use toOffsetTime() instead.", ReplaceWith("this.toOffsetTime()"), DeprecationLevel.ERROR ) - inline val offsetTime: OffsetTime get() = toOffsetTime() + inline val offsetTime: OffsetTime + get() = toOffsetTime() @Deprecated( "Use toInstant() instead.", ReplaceWith("this.toInstant()"), DeprecationLevel.ERROR ) - inline val instant: Instant get() = toInstant() + inline val instant: Instant + get() = toInstant() override val secondsSinceUnixEpoch: LongSeconds get() = dateTime.secondsSinceUnixEpochAt(offset) diff --git a/core/src/commonMain/kotlin/io/islandtime/OffsetTime.kt b/core/src/commonMain/kotlin/io/islandtime/OffsetTime.kt index 0f3cd7308..44eb0b1cc 100644 --- a/core/src/commonMain/kotlin/io/islandtime/OffsetTime.kt +++ b/core/src/commonMain/kotlin/io/islandtime/OffsetTime.kt @@ -15,10 +15,6 @@ class OffsetTime( val offset: UtcOffset ) { - init { - offset.validate() - } - /** * Creates an [OffsetTime]. * @throws DateTimeException if the time or offset is invalid diff --git a/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt b/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt index cbcac87eb..4384a3001 100644 --- a/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt +++ b/core/src/commonMain/kotlin/io/islandtime/TimeZone.kt @@ -152,10 +152,6 @@ sealed class TimeZone : Comparable { val offset: UtcOffset ) : TimeZone() { - init { - offset.validate() - } - override val id: String get() = offset.toString() override val isValid: Boolean get() = true override val rules: TimeZoneRules get() = FixedTimeZoneRules(offset) diff --git a/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt b/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt index 79271f11f..b5c553ea9 100644 --- a/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt +++ b/core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt @@ -13,18 +13,16 @@ import kotlin.math.sign /** * The time shift between a local time and UTC. * - * To ensure that the offset is within the valid supported range, you must explicitly call [validate] or [validated]. - * * @param totalSeconds the total number of seconds to offset by - * @see validate - * @see validated + * @throws DateTimeException if the offset is outside the supported range */ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable { - /** - * Checks if this offset is within the supported range. - */ - val isValid: Boolean get() = totalSeconds in MIN_TOTAL_SECONDS..MAX_TOTAL_SECONDS + init { + if (totalSeconds !in MIN_TOTAL_SECONDS..MAX_TOTAL_SECONDS) { + throw DateTimeException("'$totalSeconds' is outside the valid offset range of +/-18:00") + } + } /** * Checks if this is the UTC offset of +00:00. @@ -66,31 +64,13 @@ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable { } } - /** - * Checks if the offset is valid and throws an exception if it isn't. - * @throws DateTimeException if the offset is outside the supported range - * @see isValid - */ - fun validate() { - if (!isValid) { - throw DateTimeException("'$totalSeconds' is outside the valid offset range of +/-18:00") - } - } - - /** - * Ensures that the offset is valid, throwing an exception if it isn't. - * @throws DateTimeException if the offset is outside the supported range - * @see isValid - */ - fun validated(): UtcOffset = apply { validate() } - companion object { - val MAX_TOTAL_SECONDS = 18.hours.inSecondsUnchecked - val MIN_TOTAL_SECONDS = (-18).hours.inSecondsUnchecked + val MAX_TOTAL_SECONDS: IntSeconds = 18.hours.inSecondsUnchecked + val MIN_TOTAL_SECONDS: IntSeconds = (-18).hours.inSecondsUnchecked - val MIN = UtcOffset(MIN_TOTAL_SECONDS) - val MAX = UtcOffset(MAX_TOTAL_SECONDS) - val ZERO = UtcOffset(0.seconds) + val MIN: UtcOffset = UtcOffset(MIN_TOTAL_SECONDS) + val MAX: UtcOffset = UtcOffset(MAX_TOTAL_SECONDS) + val ZERO: UtcOffset = UtcOffset(0.seconds) } } @@ -100,7 +80,7 @@ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable { * @param hours hours to offset by, within +/-18 * @param minutes minutes to offset by, within +/-59 * @param seconds seconds to offset by, within +/-59 - * @throws DateTimeException if any of the individual components is outside the valid range + * @throws DateTimeException if the offset or any of its components are invalid * @return a [UtcOffset] */ fun UtcOffset( @@ -167,7 +147,7 @@ internal fun DateTimeParseResult.toUtcOffset(): UtcOffset? { val totalSeconds = fields[DateTimeField.UTC_OFFSET_TOTAL_SECONDS] if (totalSeconds != null) { - return UtcOffset(totalSeconds.toIntExact().seconds).validated() + return UtcOffset(totalSeconds.toIntExact().seconds) } val sign = fields[DateTimeField.UTC_OFFSET_SIGN] @@ -178,9 +158,9 @@ internal fun DateTimeParseResult.toUtcOffset(): UtcOffset? { val seconds = (fields[DateTimeField.UTC_OFFSET_SECONDS]?.toIntExact() ?: 0).seconds return if (sign < 0L) { - UtcOffset(-hours, -minutes, -seconds).validated() + UtcOffset(-hours, -minutes, -seconds) } else { - UtcOffset(hours, minutes, seconds).validated() + UtcOffset(hours, minutes, seconds) } } @@ -210,13 +190,13 @@ internal fun StringBuilder.appendUtcOffset(offset: UtcOffset): StringBuilder { private fun validateUtcOffsetComponents(hours: IntHours, minutes: IntMinutes, seconds: IntSeconds) { when { - hours.isPositive() -> if (minutes.isNegative() || seconds.isNegative()) { + hours.value > 0 -> if (minutes.value < 0 || seconds.value < 0) { throw DateTimeException("Time offset minutes and seconds must be positive when hours are positive") } - hours.isNegative() -> if (minutes.isPositive() || seconds.isPositive()) { + hours.value < 0 -> if (minutes.value > 0 || seconds.value > 0) { throw DateTimeException("Time offset minutes and seconds must be negative when hours are negative") } - else -> if ((minutes.isNegative() && seconds.isPositive()) || (minutes.isPositive() && seconds.isNegative())) { + else -> if ((minutes.value < 0 && seconds.value > 0) || (minutes.value > 0 && seconds.value < 0)) { throw DateTimeException("Time offset minutes and seconds must have the same sign") } } diff --git a/core/src/commonMain/kotlin/io/islandtime/Year.kt b/core/src/commonMain/kotlin/io/islandtime/Year.kt index f5aadad93..ab6acae0d 100644 --- a/core/src/commonMain/kotlin/io/islandtime/Year.kt +++ b/core/src/commonMain/kotlin/io/islandtime/Year.kt @@ -11,31 +11,36 @@ import kotlin.math.absoluteValue * A year as defined by ISO-8601. * @constructor Creates a [Year]. * @param value the year + * @throws DateTimeException if the year is invalid * @property value The year value. */ inline class Year(val value: Int) : Comparable { /** - * Checks if this year is within the supported range. + * Creates a [Year]. + * @param value the year + * @throws DateTimeException if the year is invalid */ - val isValid: Boolean get() = value in MIN_VALUE..MAX_VALUE + constructor(value: Long) : this(checkValidYear(value)) + + init { + checkValidYear(value) + } /** * Checks if this is a leap year. */ - val isLeap: Boolean - get() = value % 4 == 0 && (value % 100 != 0 || value % 400 == 0) + val isLeap: Boolean get() = isLeapYear(value) /** * The length of the year in days. */ - val length: IntDays - get() = if (isLeap) 366.days else 365.days + val length: IntDays get() = lengthOfYear(value) /** * The last day of the year. This will be either `365` or `366` depending on whether this is a common or leap year. */ - val lastDay: Int get() = length.value + val lastDay: Int get() = lastDayOfYear(value) /** * The day range of the year. This will be either `1..365` or `1.366` depending on whether this is a common or leap @@ -59,11 +64,10 @@ inline class Year(val value: Int) : Comparable { val endDate: Date get() = Date(value, Month.DECEMBER, 31) operator fun plus(years: LongYears): Year { - val newValue = checkValidYear(value + years.value) - return Year(newValue) + return Year(value + years.value) } - operator fun plus(years: IntYears) = plus(years.toLongYears()) + operator fun plus(years: IntYears): Year = plus(years.toLongYears()) operator fun minus(years: LongYears): Year { return if (years.value == Long.MIN_VALUE) { @@ -73,23 +77,11 @@ inline class Year(val value: Int) : Comparable { } } - operator fun minus(years: IntYears) = plus(years.toLongYears().negateUnchecked()) + operator fun minus(years: IntYears): Year = plus(years.toLongYears().negateUnchecked()) operator fun contains(yearMonth: YearMonth): Boolean = yearMonth.year == value operator fun contains(date: Date): Boolean = date.year == value - /** - * Ensures that this year is valid, throwing an exception if it isn't. - * @throws DateTimeException if the year is invalid - * @see isValid - */ - fun validated(): Year { - if (!isValid) { - throw DateTimeException(getInvalidYearMessage(value.toLong())) - } - return this - } - override fun compareTo(other: Year): Int = value - other.value /** @@ -113,22 +105,22 @@ inline class Year(val value: Int) : Comparable { /** * The earliest supported year value. */ - const val MIN_VALUE = -999_999_999 + const val MIN_VALUE: Int = -999_999_999 /** * The latest supported year value. */ - const val MAX_VALUE = 999_999_999 + const val MAX_VALUE: Int = 999_999_999 /** * The earliest supported [Year], which can be used as a "far past" sentinel. */ - val MIN = Year(MIN_VALUE) + val MIN: Year = Year(MIN_VALUE) /** * The latest supported [Year], which can be used as a "far future" sentinel. */ - val MAX = Year(MAX_VALUE) + val MAX: Year = Year(MAX_VALUE) } } @@ -165,16 +157,28 @@ internal fun DateTimeParseResult.toYear(): Year? { val value = fields[DateTimeField.YEAR] return if (value != null) { - Year(checkValidYear(value)) + Year(value) } else { null } } -internal fun isLeapYear(year: Int) = Year(year).isLeap -internal fun lengthOfYear(year: Int) = Year(year).length -internal fun lastDayOfYear(year: Int): Int = Year(year).lastDay -internal fun checkValidYear(year: Int) = Year(year).validated().value +internal fun isLeapYear(year: Int): Boolean { + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) +} + +internal fun lengthOfYear(year: Int): IntDays { + return if (isLeapYear(year)) 366.days else 365.days +} + +internal fun lastDayOfYear(year: Int): Int = lengthOfYear(year).value + +internal fun checkValidYear(year: Int): Int { + if (!isValidYear(year)) { + throw DateTimeException(getInvalidYearMessage(year.toLong())) + } + return year +} internal fun checkValidYear(year: Long): Int { if (!isValidYear(year)) { @@ -183,6 +187,10 @@ internal fun checkValidYear(year: Long): Int { return year.toInt() } +internal fun isValidYear(year: Int): Boolean { + return year in Year.MIN_VALUE..Year.MAX_VALUE +} + internal fun isValidYear(year: Long): Boolean { return year in Year.MIN_VALUE..Year.MAX_VALUE } diff --git a/core/src/commonTest/kotlin/io/islandtime/UtcOffsetTest.kt b/core/src/commonTest/kotlin/io/islandtime/UtcOffsetTest.kt index 7aae2bb9f..c0dd8971a 100644 --- a/core/src/commonTest/kotlin/io/islandtime/UtcOffsetTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/UtcOffsetTest.kt @@ -67,18 +67,6 @@ class UtcOffsetTest : AbstractIslandTimeTest() { assertEquals(1.seconds, 1.seconds.asUtcOffset().totalSeconds) } - @Test - fun `isValid property returns true is offset is inside the valid range`() { - assertTrue { UtcOffset.MAX.isValid } - assertTrue { UtcOffset.MIN.isValid } - } - - @Test - fun `isValid property returns false if offset is outside of +-18_00`() { - assertFalse { UtcOffset(UtcOffset.MAX_TOTAL_SECONDS + 1.seconds).isValid } - assertFalse { UtcOffset(UtcOffset.MIN_TOTAL_SECONDS - 1.seconds).isValid } - } - @Test fun `isZero() returns true only when the offset is zero`() { assertTrue { UtcOffset(0.seconds).isZero() } @@ -86,15 +74,15 @@ class UtcOffsetTest : AbstractIslandTimeTest() { } @Test - fun `validated() returns the unmodified offset when within the valid range`() { - assertEquals(UtcOffset.MAX, UtcOffset.MAX.validated()) - assertEquals(UtcOffset.MIN, UtcOffset.MIN.validated()) + fun `construction succeeds when the offset is within the valid range`() { + assertEquals(UtcOffset.MAX_TOTAL_SECONDS, UtcOffset.MAX.totalSeconds) + assertEquals(UtcOffset.MIN_TOTAL_SECONDS, UtcOffset.MIN.totalSeconds) } @Test - fun `validated() throws an exception if the offset is outside the valid range`() { - assertFailsWith { UtcOffset(UtcOffset.MAX_TOTAL_SECONDS + 1.seconds).validated() } - assertFailsWith { UtcOffset(UtcOffset.MIN_TOTAL_SECONDS - 1.seconds).validated() } + fun `construction throws an exception if the offset is outside the valid range`() { + assertFailsWith { UtcOffset(UtcOffset.MAX_TOTAL_SECONDS + 1.seconds) } + assertFailsWith { UtcOffset(UtcOffset.MIN_TOTAL_SECONDS - 1.seconds) } } @Test diff --git a/core/src/commonTest/kotlin/io/islandtime/YearTest.kt b/core/src/commonTest/kotlin/io/islandtime/YearTest.kt index 608836d6c..09f4ad700 100644 --- a/core/src/commonTest/kotlin/io/islandtime/YearTest.kt +++ b/core/src/commonTest/kotlin/io/islandtime/YearTest.kt @@ -11,27 +11,48 @@ import io.islandtime.test.AbstractIslandTimeTest import kotlin.test.* class YearTest : AbstractIslandTimeTest() { - private val invalidYears = listOf( + private val invalidIntYears = listOf( Year.MIN_VALUE - 1, Year.MAX_VALUE + 1, Int.MAX_VALUE, Int.MIN_VALUE ) + private val invalidLongYears = listOf( + Year.MIN_VALUE - 1L, + Year.MAX_VALUE + 1L, + Long.MAX_VALUE, + Long.MIN_VALUE + ) + @Test - fun `isValid can be used to check if the Year was initialized with an invalid value`() { - invalidYears.forEach { - assertFalse { Year(it).isValid } + fun `Int constructor throws an exception if the value is invalid`() { + invalidIntYears.forEach { + assertFailsWith { Year(it) } } } @Test - fun `validated() throws an exception if the current value is invalid`() { - invalidYears.forEach { - assertFailsWith { Year(it).validated() } + fun `Long constructor throws an exception if the value is invalid`() { + invalidLongYears.forEach { + assertFailsWith { Year(it) } } } + @Test + fun `Int construction succeeds with valid years`() { + assertEquals(2000, Year(2000).value) + assertEquals(Year.MIN_VALUE, Year.MIN.value) + assertEquals(Year.MAX_VALUE, Year.MAX.value) + } + + @Test + fun `Long construction succeeds with valid years`() { + assertEquals(2000, Year(2000L).value) + assertEquals(Year.MIN_VALUE, Year(Year.MIN_VALUE.toLong()).value) + assertEquals(Year.MAX_VALUE, Year(Year.MAX_VALUE.toLong()).value) + } + @Test fun `isLeap property returns false for common years`() { assertFalse { Year(2001).isLeap }