Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Year and UtcOffset to take advantage of Kotlin 1.4.30 features #168

Merged
merged 3 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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