commonMain.io.islandtime.UtcOffset.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-metadata Show documentation
Show all versions of core-metadata Show documentation
A multiplatform library for working with dates and times
The newest version!
@file:Suppress("FunctionName")
package io.islandtime
import dev.erikchristensen.javamath2kmp.toIntExact
import io.islandtime.base.DateTimeField
import io.islandtime.internal.appendZeroPadded
import io.islandtime.measures.*
import io.islandtime.parser.*
import kotlin.math.absoluteValue
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
*/
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
/**
* Checks if this is the UTC offset of +00:00.
*/
fun isZero(): Boolean = this == ZERO
/**
* Breaks a UTC offset down into components. The sign will indicate whether the offset is positive or negative while
* each component will be positive.
*/
inline fun toComponents(
action: (sign: Int, hours: IntHours, minutes: IntMinutes, seconds: IntSeconds) -> T
): T {
return totalSeconds.value.absoluteValue.seconds.toComponents { hours, minutes, seconds ->
action(totalSeconds.value.sign, hours, minutes, seconds)
}
}
/**
* Breaks a UTC offset down into components. If the offset is negative, each component will be negative.
*/
inline fun toComponents(
action: (hours: IntHours, minutes: IntMinutes, seconds: IntSeconds) -> T
): T {
return totalSeconds.toComponents(action)
}
override fun compareTo(other: UtcOffset): Int = totalSeconds.compareTo(other.totalSeconds)
/**
* Converts this offset to a string in ISO-8601 extended format. For example, `-04:00` or `Z`.
*/
override fun toString(): String {
return if (isZero()) {
"Z"
} else {
buildString(MAX_UTC_OFFSET_STRING_LENGTH) { appendUtcOffset(this@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 MIN = UtcOffset(MIN_TOTAL_SECONDS)
val MAX = UtcOffset(MAX_TOTAL_SECONDS)
val ZERO = UtcOffset(0.seconds)
}
}
/**
* Creates a UTC offset of hours, minutes, and seconds. Each component must be within its valid range and without any
* mixed positive and negative values.
* @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
* @return a [UtcOffset]
*/
fun UtcOffset(
hours: IntHours,
minutes: IntMinutes = 0.minutes,
seconds: IntSeconds = 0.seconds
): UtcOffset {
validateUtcOffsetComponents(hours, minutes, seconds)
return UtcOffset(hours + minutes + seconds)
}
/**
* Converts a duration of hours into a [UtcOffset] of the same length.
* @throws ArithmeticException if overflow occurs
*/
fun IntHours.asUtcOffset(): UtcOffset = UtcOffset(this.inSeconds)
/**
* Converts a duration of minutes into a [UtcOffset] of the same length.
* @throws ArithmeticException if overflow occurs
*/
fun IntMinutes.asUtcOffset(): UtcOffset = UtcOffset(this.inSeconds)
/**
* Converts a duration of seconds into a [UtcOffset] of the same length.
*/
fun IntSeconds.asUtcOffset(): UtcOffset = UtcOffset(this)
/**
* Converts a string to a [UtcOffset].
*
* The string is assumed to be an ISO-8601 UTC offset representation in extended format. For example, `Z`, `+05`, or
* `-04:30`. The output of [UtcOffset.toString] can be safely parsed using this method.
*
* @throws DateTimeParseException if parsing fails
* @throws DateTimeException if the parsed UTC offset is invalid
*/
fun String.toUtcOffset(): UtcOffset = toUtcOffset(DateTimeParsers.Iso.Extended.UTC_OFFSET)
/**
* Converts a string to a [UtcOffset] using a specific parser.
*
* A set of predefined parsers can be found in [DateTimeParsers].
*
* @throws DateTimeParseException if parsing fails
* @throws DateTimeException if the parsed UTC offset is invalid
*/
fun String.toUtcOffset(
parser: DateTimeParser,
settings: DateTimeParserSettings = DateTimeParserSettings.DEFAULT
): UtcOffset {
val result = parser.parse(this, settings)
return result.toUtcOffset() ?: throwParserFieldResolutionException(this)
}
/**
* Resolves a parser result into a [UtcOffset].
*
* Required fields are [DateTimeField.UTC_OFFSET_TOTAL_SECONDS] or [DateTimeField.UTC_OFFSET_SIGN] in conjunction with
* any combination of [DateTimeField.UTC_OFFSET_HOURS], [DateTimeField.UTC_OFFSET_MINUTES], and
* [DateTimeField.UTC_OFFSET_SECONDS].
*/
internal fun DateTimeParseResult.toUtcOffset(): UtcOffset? {
val totalSeconds = fields[DateTimeField.UTC_OFFSET_TOTAL_SECONDS]
if (totalSeconds != null) {
return UtcOffset(totalSeconds.toIntExact().seconds).validated()
}
val sign = fields[DateTimeField.UTC_OFFSET_SIGN]
if (sign != null) {
val hours = (fields[DateTimeField.UTC_OFFSET_HOURS]?.toIntExact() ?: 0).hours
val minutes = (fields[DateTimeField.UTC_OFFSET_MINUTES]?.toIntExact() ?: 0).minutes
val seconds = (fields[DateTimeField.UTC_OFFSET_SECONDS]?.toIntExact() ?: 0).seconds
return if (sign < 0L) {
UtcOffset(-hours, -minutes, -seconds).validated()
} else {
UtcOffset(hours, minutes, seconds).validated()
}
}
return null
}
internal const val MAX_UTC_OFFSET_STRING_LENGTH = 9
internal fun StringBuilder.appendUtcOffset(offset: UtcOffset): StringBuilder {
if (offset.isZero()) {
append('Z')
} else {
offset.toComponents { sign, hours, minutes, seconds ->
append(if (sign < 0) '-' else '+')
appendZeroPadded(hours.value, 2)
append(':')
appendZeroPadded(minutes.value, 2)
if (seconds.value != 0) {
append(':')
appendZeroPadded(seconds.value, 2)
}
}
}
return this
}
private fun validateUtcOffsetComponents(hours: IntHours, minutes: IntMinutes, seconds: IntSeconds) {
when {
hours.isPositive() -> if (minutes.isNegative() || seconds.isNegative()) {
throw DateTimeException("Time offset minutes and seconds must be positive when hours are positive")
}
hours.isNegative() -> if (minutes.isPositive() || seconds.isPositive()) {
throw DateTimeException("Time offset minutes and seconds must be negative when hours are negative")
}
else -> if ((minutes.isNegative() && seconds.isPositive()) || (minutes.isPositive() && seconds.isNegative())) {
throw DateTimeException("Time offset minutes and seconds must have the same sign")
}
}
if (hours.value !in -18..18) {
throw DateTimeException("Time offset hours must be within +/-18, got '${hours.value}'")
}
if (minutes.value !in -59..59) {
throw DateTimeException("Time offset minutes must be within +/-59, got '${minutes.value}'")
}
if (seconds.value !in -59..59) {
throw DateTimeException("Time offset seconds must be within +/-59, got '${seconds.value}'")
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy