Skip to content

Commit

Permalink
Update Year and UtcOffset to take advantage of Kotlin 1.4.30 features (
Browse files Browse the repository at this point in the history
…#168)

* Update Year and UtcOffset to take advantage of Kotlin 1.4.30 features

* Minor fixes
  • Loading branch information
erikc5000 authored Feb 26, 2021
1 parent 0300571 commit 2581be5
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 110 deletions.
13 changes: 6 additions & 7 deletions core/src/commonMain/kotlin/io/islandtime/OffsetDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ class OffsetDateTime(
val offset: UtcOffset
) : TimePoint<OffsetDateTime> {

init {
offset.validate()
}

/**
* Creates an [OffsetDateTime].
* @throws DateTimeException if the offset is invalid
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions core/src/commonMain/kotlin/io/islandtime/OffsetTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ class OffsetTime(
val offset: UtcOffset
) {

init {
offset.validate()
}

/**
* Creates an [OffsetTime].
* @throws DateTimeException if the time or offset is invalid
Expand Down
4 changes: 0 additions & 4 deletions core/src/commonMain/kotlin/io/islandtime/TimeZone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,6 @@ sealed class TimeZone : Comparable<TimeZone> {
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)
Expand Down
56 changes: 18 additions & 38 deletions core/src/commonMain/kotlin/io/islandtime/UtcOffset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<UtcOffset> {

/**
* 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.
Expand Down Expand Up @@ -66,31 +64,13 @@ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable<UtcOffset> {
}
}

/**
* 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)
}
}

Expand All @@ -100,7 +80,7 @@ inline class UtcOffset(val totalSeconds: IntSeconds) : Comparable<UtcOffset> {
* @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(
Expand Down Expand Up @@ -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]
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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")
}
}
Expand Down
72 changes: 40 additions & 32 deletions core/src/commonMain/kotlin/io/islandtime/Year.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Year> {

/**
* 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
Expand All @@ -59,11 +64,10 @@ inline class Year(val value: Int) : Comparable<Year> {
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) {
Expand All @@ -73,23 +77,11 @@ inline class Year(val value: Int) : Comparable<Year> {
}
}

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

/**
Expand All @@ -113,22 +105,22 @@ inline class Year(val value: Int) : Comparable<Year> {
/**
* 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)
}
}

Expand Down Expand Up @@ -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)) {
Expand All @@ -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
}
Expand Down
24 changes: 6 additions & 18 deletions core/src/commonTest/kotlin/io/islandtime/UtcOffsetTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,34 +67,22 @@ 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() }
assertFalse { UtcOffset((-1).seconds).isZero() }
}

@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<DateTimeException> { UtcOffset(UtcOffset.MAX_TOTAL_SECONDS + 1.seconds).validated() }
assertFailsWith<DateTimeException> { UtcOffset(UtcOffset.MIN_TOTAL_SECONDS - 1.seconds).validated() }
fun `construction throws an exception if the offset is outside the valid range`() {
assertFailsWith<DateTimeException> { UtcOffset(UtcOffset.MAX_TOTAL_SECONDS + 1.seconds) }
assertFailsWith<DateTimeException> { UtcOffset(UtcOffset.MIN_TOTAL_SECONDS - 1.seconds) }
}

@Test
Expand Down
Loading

0 comments on commit 2581be5

Please sign in to comment.