All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.islandtime.DateTime.kt Maven / Gradle / Ivy

The newest version!
package io.islandtime

import dev.erikchristensen.javamath2kmp.floorDiv
import dev.erikchristensen.javamath2kmp.floorMod
import io.islandtime.internal.*
import io.islandtime.measures.*
import io.islandtime.parser.*
import io.islandtime.ranges.DateTimeInterval

/**
 * A date and time of day in an ambiguous region.
 *
 * @constructor Creates a [DateTime] by combining a [Date] and [Time].
 * @param date the date
 * @param time the time
 */
class DateTime(
    /** The date. */
    val date: Date,
    /** The time of day. */
    val time: Time
) : Comparable {

    /**
     * Creates a [DateTime].
     * @throws DateTimeException if the date-time is invalid
     */
    constructor(
        year: Int,
        month: Month,
        day: Int,
        hour: Int,
        minute: Int,
        second: Int = 0,
        nanosecond: Int = 0
    ) : this(Date(year, month, day), Time(hour, minute, second, nanosecond))

    /**
     * Creates a [DateTime].
     * @throws DateTimeException if the date-time is invalid
     */
    constructor(
        year: Int,
        monthNumber: Int,
        day: Int,
        hour: Int,
        minute: Int,
        second: Int = 0,
        nanosecond: Int = 0
    ) : this(year, monthNumber.toMonth(), day, hour, minute, second, nanosecond)

    /**
     * Creates a [DateTime].
     * @throws DateTimeException if the date-time is invalid
     */
    constructor(
        year: Int,
        dayOfYear: Int,
        hour: Int,
        minute: Int,
        second: Int,
        nanosecond: Int
    ) : this(Date(year, dayOfYear), Time(hour, minute, second, nanosecond))

    /**
     * The hour of the day.
     */
    inline val hour: Int get() = time.hour

    /**
     * The minute of the hour.
     */
    inline val minute: Int get() = time.minute

    /**
     * The second of the minute.
     */
    inline val second: Int get() = time.second

    /**
     * The nanosecond of the second.
     */
    inline val nanosecond: Int get() = time.nanosecond

    /**
     * The month of the year.
     */
    inline val month: Month get() = date.month

    /**
     * The ISO month number, from 1-12.
     */
    inline val monthNumber: Int get() = month.number

    /**
     * The day of the week.
     */
    inline val dayOfWeek: DayOfWeek get() = date.dayOfWeek

    /**
     * The day of the month.
     */
    inline val dayOfMonth: Int get() = date.dayOfMonth

    /**
     * The day of the year.
     */
    inline val dayOfYear: Int get() = date.dayOfYear

    /**
     * The year.
     */
    inline val year: Int get() = date.year

    @Deprecated(
        "Use toYearMonth() instead.",
        ReplaceWith("this.toYearMonth()"),
        DeprecationLevel.ERROR
    )
    inline val yearMonth: YearMonth
        get() = toYearMonth()

    /**
     * Returns this date-time with [period] added to it.
     *
     * Years are added first, then months, then days. If the day exceeds the maximum month length at any step, it will
     * be coerced into the valid range.
     */
    operator fun plus(period: Period): DateTime {
        return if (period.isZero()) {
            this
        } else {
            copy(date = date + period)
        }
    }

    operator fun plus(duration: Duration): DateTime {
        return this + duration.seconds + duration.nanosecondAdjustment
    }

    operator fun plus(years: IntYears) = plus(years.toLongYears())

    operator fun plus(years: LongYears): DateTime {
        return if (years.value == 0L) {
            this
        } else {
            copy(date = date + years)
        }
    }

    operator fun plus(months: IntMonths) = plus(months.toLongMonths())

    operator fun plus(months: LongMonths): DateTime {
        return if (months.value == 0L) {
            this
        } else {
            copy(date = date + months)
        }
    }

    operator fun plus(weeks: IntWeeks) = plus(weeks.toLongWeeks().inDaysUnchecked)

    operator fun plus(weeks: LongWeeks): DateTime {
        return if (weeks.value == 0L) {
            this
        } else {
            copy(date = date + weeks)
        }
    }

    operator fun plus(days: IntDays) = plus(days.toLongDays())

    operator fun plus(days: LongDays): DateTime {
        return if (days.value == 0L) {
            this
        } else {
            copy(date = date + days)
        }
    }

    operator fun plus(hours: IntHours) = plus(hours.toLongHours())

    operator fun plus(hours: LongHours): DateTime {
        return if (hours.value == 0L) {
            this
        } else {
            var daysToAdd = hours.inDays
            val wrappedHours = (hours % HOURS_PER_DAY).toInt()
            var newHour = time.hour + wrappedHours
            daysToAdd += (newHour floorDiv HOURS_PER_DAY).days
            newHour = newHour floorMod HOURS_PER_DAY

            val newDate = date + daysToAdd
            val newTime = time.copy(hour = newHour)
            DateTime(newDate, newTime)
        }
    }

    operator fun plus(minutes: IntMinutes) = plus(minutes.toLongMinutes())

    operator fun plus(minutes: LongMinutes): DateTime {
        return if (minutes.value == 0L) {
            this
        } else {
            var daysToAdd = minutes.inDays
            val currentMinuteOfDay = time.hour * MINUTES_PER_HOUR + minute
            val wrappedMinutes = (minutes % MINUTES_PER_DAY).toInt()
            var newMinuteOfDay = currentMinuteOfDay + wrappedMinutes
            daysToAdd += (newMinuteOfDay floorDiv MINUTES_PER_DAY).days
            newMinuteOfDay = newMinuteOfDay floorMod MINUTES_PER_DAY

            val newDate = date + daysToAdd

            val newTime = if (currentMinuteOfDay == newMinuteOfDay) {
                time
            } else {
                val newHour = newMinuteOfDay / MINUTES_PER_HOUR
                val newMinute = newMinuteOfDay % MINUTES_PER_HOUR
                Time(newHour, newMinute, time.second, time.nanosecond)
            }

            DateTime(newDate, newTime)
        }
    }

    operator fun plus(seconds: IntSeconds) = plus(seconds.toLongSeconds())

    operator fun plus(seconds: LongSeconds): DateTime {
        return if (seconds.value == 0L) {
            this
        } else {
            var daysToAdd = seconds.inDays
            val currentSecondOfDay = time.secondOfDay
            val wrappedSeconds = (seconds % SECONDS_PER_DAY).toInt()
            var newSecondOfDay = currentSecondOfDay + wrappedSeconds
            daysToAdd += (newSecondOfDay floorDiv SECONDS_PER_DAY).days
            newSecondOfDay = newSecondOfDay floorMod SECONDS_PER_DAY

            val newDate = date + daysToAdd

            val newTime = if (currentSecondOfDay == newSecondOfDay) {
                time
            } else {
                Time.fromSecondOfDay(newSecondOfDay, time.nanosecond)
            }

            DateTime(newDate, newTime)
        }
    }

    operator fun plus(milliseconds: IntMilliseconds) = plus(milliseconds.toLongMilliseconds())

    operator fun plus(milliseconds: LongMilliseconds): DateTime {
        return if (milliseconds.value == 0L) {
            this
        } else {
            plus(milliseconds.inDays, (milliseconds % MILLISECONDS_PER_DAY).inNanosecondsUnchecked)
        }
    }

    operator fun plus(microseconds: IntMicroseconds) = plus(microseconds.toLongMicroseconds())

    operator fun plus(microseconds: LongMicroseconds): DateTime {
        return if (microseconds.value == 0L) {
            this
        } else {
            plus(microseconds.inDays, (microseconds % MICROSECONDS_PER_DAY).inNanosecondsUnchecked)
        }
    }

    operator fun plus(nanoseconds: IntNanoseconds) = plus(nanoseconds.toLongNanoseconds())

    operator fun plus(nanoseconds: LongNanoseconds): DateTime {
        return if (nanoseconds.value == 0L) {
            this
        } else {
            plus(nanoseconds.inDays, nanoseconds % NANOSECONDS_PER_DAY)
        }
    }

    private fun plus(days: LongDays, wrappedNanoseconds: LongNanoseconds): DateTime {
        val currentNanosecondOfDay = time.nanosecondOfDay
        var newNanosecondOfDay = currentNanosecondOfDay + wrappedNanoseconds.value
        val daysToAdd = (days.value + (newNanosecondOfDay floorDiv NANOSECONDS_PER_DAY)).days
        newNanosecondOfDay = newNanosecondOfDay floorMod NANOSECONDS_PER_DAY

        val newDate = date + daysToAdd

        val newTime = if (currentNanosecondOfDay == newNanosecondOfDay) {
            time
        } else {
            Time.fromNanosecondOfDay(newNanosecondOfDay)
        }

        return DateTime(newDate, newTime)
    }

    /**
     * Returns this date-time with [period] subtracted from it.
     *
     * Years are added first, then months, then days. If the day exceeds the maximum month length at any step, it will
     * be coerced into the valid range.
     */
    operator fun minus(period: Period) = plus(-period)

    operator fun minus(duration: Duration): DateTime {
        return this - duration.seconds - duration.nanosecondAdjustment
    }

    operator fun minus(years: IntYears) = plus(years.toLongYears().negateUnchecked())

    operator fun minus(years: LongYears): DateTime {
        return if (years.value == 0L) {
            this
        } else {
            copy(date = date - years)
        }
    }

    operator fun minus(months: IntMonths) = plus(months.toLongMonths().negateUnchecked())

    operator fun minus(months: LongMonths): DateTime {
        return if (months.value == 0L) {
            this
        } else {
            copy(date = date - months)
        }
    }

    operator fun minus(weeks: IntWeeks) = plus(weeks.toLongWeeks().inDaysUnchecked.negateUnchecked())

    operator fun minus(weeks: LongWeeks): DateTime {
        return if (weeks.value == 0L) {
            this
        } else {
            copy(date = date - weeks)
        }
    }

    operator fun minus(days: IntDays) = plus(days.toLongDays().negateUnchecked())

    operator fun minus(days: LongDays): DateTime {
        return if (days.value == 0L) {
            this
        } else {
            copy(date = date - days)
        }
    }

    operator fun minus(hours: IntHours) = plus(hours.toLongHours().negateUnchecked())

    operator fun minus(hours: LongHours): DateTime {
        return if (hours.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.hours + 1.hours
        } else {
            plus(hours.negateUnchecked())
        }
    }

    operator fun minus(minutes: IntMinutes) = plus(minutes.toLongMinutes().negateUnchecked())

    operator fun minus(minutes: LongMinutes): DateTime {
        return if (minutes.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.minutes + 1.minutes
        } else {
            plus(minutes.negateUnchecked())
        }
    }

    operator fun minus(seconds: IntSeconds) = plus(seconds.toLongSeconds().negateUnchecked())

    operator fun minus(seconds: LongSeconds): DateTime {
        return if (seconds.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.seconds + 1.seconds
        } else {
            plus(seconds.negateUnchecked())
        }
    }

    operator fun minus(milliseconds: IntMilliseconds) = plus(milliseconds.toLongMilliseconds().negateUnchecked())

    operator fun minus(milliseconds: LongMilliseconds): DateTime {
        return if (milliseconds.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.milliseconds + 1.milliseconds
        } else {
            plus(milliseconds.negateUnchecked())
        }
    }

    operator fun minus(microseconds: IntMicroseconds) = plus(microseconds.toLongMicroseconds().negateUnchecked())

    operator fun minus(microseconds: LongMicroseconds): DateTime {
        return if (microseconds.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.microseconds + 1.microseconds
        } else {
            plus(microseconds.negateUnchecked())
        }
    }

    operator fun minus(nanoseconds: IntNanoseconds) = plus(nanoseconds.toLongNanoseconds().negateUnchecked())

    operator fun minus(nanoseconds: LongNanoseconds): DateTime {
        return if (nanoseconds.value == Long.MIN_VALUE) {
            this + Long.MAX_VALUE.nanoseconds + 1.nanoseconds
        } else {
            plus(nanoseconds.negateUnchecked())
        }
    }

    operator fun component1(): Date = date
    operator fun component2(): Time = time

    operator fun rangeTo(other: DateTime) = DateTimeInterval.withInclusiveEnd(this, other)

    override fun compareTo(other: DateTime): Int {
        val dateDiff = date.compareTo(other.date)

        return if (dateDiff != 0) {
            dateDiff
        } else {
            time.compareTo(other.time)
        }
    }

    /**
     * Converts this date-time to a string in ISO-8601 extended format. For example, `2012-04-15T17:31:45.923452091` or
     * `2020-02-13T02:30`.
     */
    override fun toString(): String = buildString(MAX_DATE_TIME_STRING_LENGTH) {
        appendDateTime(this@DateTime)
    }

    override fun equals(other: Any?): Boolean {
        return this === other || (other is DateTime && date == other.date && time == other.time)
    }

    override fun hashCode(): Int {
        return 31 * date.hashCode() + time.hashCode()
    }

    /**
     * Returns a copy of this date-time with the values of any individual components replaced by the new values
     * specified.
     * @throws DateTimeException if the resulting date-time is invalid
     */
    fun copy(
        date: Date = this.date,
        time: Time = this.time
    ): DateTime = DateTime(date, time)

    /**
     * Returns a copy of this date-time with the values of any individual components replaced by the new values
     * specified.
     * @throws DateTimeException if the resulting date-time is invalid
     */
    fun copy(
        year: Int = this.year,
        dayOfYear: Int = this.dayOfYear,
        hour: Int = this.hour,
        minute: Int = this.minute,
        second: Int = this.second,
        nanosecond: Int = this.nanosecond
    ): DateTime = DateTime(date.copy(year, dayOfYear), time.copy(hour, minute, second, nanosecond))

    /**
     * Returns a copy of this date-time with the values of any individual components replaced by the new values
     * specified.
     * @throws DateTimeException if the resulting date-time is invalid
     */
    fun copy(
        year: Int = this.year,
        month: Month = this.month,
        dayOfMonth: Int = this.dayOfMonth,
        hour: Int = this.hour,
        minute: Int = this.minute,
        second: Int = this.second,
        nanosecond: Int = this.nanosecond
    ): DateTime = DateTime(date.copy(year, month, dayOfMonth), time.copy(hour, minute, second, nanosecond))

    /**
     * Returns a copy of this date-time with the values of any individual components replaced by the new values
     * specified.
     * @throws DateTimeException if the resulting date-time is invalid
     */
    fun copy(
        year: Int = this.year,
        monthNumber: Int,
        dayOfMonth: Int = this.dayOfMonth,
        hour: Int = this.hour,
        minute: Int = this.minute,
        second: Int = this.second,
        nanosecond: Int = this.nanosecond
    ): DateTime = DateTime(date.copy(year, monthNumber, dayOfMonth), time.copy(hour, minute, second, nanosecond))

    /**
     * The number of seconds relative to the Unix epoch of `1970-01-01T00:00Z` at a particular offset. This is a "floor"
     * value, so 1 nanosecond before the Unix epoch will be at a distance of 1 second.
     *
     * @param offset the offset from UTC
     * @see additionalNanosecondsSinceUnixEpoch
     */
    fun secondsSinceUnixEpochAt(offset: UtcOffset): LongSeconds {
        return (date.daysSinceUnixEpoch.inSecondsUnchecked.value +
            time.secondsSinceStartOfDay.value -
            offset.totalSeconds.value).seconds
    }

    /**
     * The number of additional nanoseconds that should be applied on top of the number of seconds since the Unix epoch
     * returned by [secondsSinceUnixEpochAt].
     * @see secondsSinceUnixEpochAt
     */
    val additionalNanosecondsSinceUnixEpoch: IntNanoseconds
        get() = nanosecond.nanoseconds

    /**
     * The number of milliseconds relative to the Unix epoch of `1970-01-01T00:00Z` at a particular offset. This is a
     * "floor" value, so 1 nanosecond before the Unix epoch will be at a distance of 1 millisecond.
     * @param offset the offset from UTC
     */
    fun millisecondsSinceUnixEpochAt(offset: UtcOffset): LongMilliseconds {
        return (date.daysSinceUnixEpoch.inMillisecondsUnchecked.value +
            time.nanosecondsSinceStartOfDay.inMilliseconds.value -
            offset.totalSeconds.inMilliseconds.value).milliseconds
    }

    /**
     * The second of the Unix epoch.
     *
     * @param offset the offset from UTC
     * @see additionalNanosecondsSinceUnixEpoch
     */
    fun secondOfUnixEpochAt(offset: UtcOffset): Long = secondsSinceUnixEpochAt(offset).value

    /**
     * The millisecond of the Unix epoch.
     * @param offset the offset from UTC
     */
    fun millisecondOfUnixEpochAt(offset: UtcOffset): Long = millisecondsSinceUnixEpochAt(offset).value

    @Deprecated(
        "Use additionalNanosecondsSinceUnixEpoch instead.",
        ReplaceWith("this.additionalNanosecondsSinceUnixEpoch"),
        DeprecationLevel.ERROR
    )
    val nanoOfSecondsSinceUnixEpoch: IntNanoseconds
        get() = additionalNanosecondsSinceUnixEpoch

    @Deprecated(
        "Use secondOfUnixEpochAt() instead.",
        ReplaceWith("this.secondOfUnixEpochAt(offset)"),
        DeprecationLevel.ERROR
    )
    fun unixEpochSecondAt(offset: UtcOffset): Long = secondOfUnixEpochAt(offset)

    @Deprecated(
        "Use nanosecond instead.",
        ReplaceWith("this.nanosecond"),
        DeprecationLevel.ERROR
    )
    val unixEpochNanoOfSecond: Int
        get() = nanosecond

    @Deprecated(
        "Use millisecondOfUnixEpoch() instead.",
        ReplaceWith("this.millisecondOfUnixEpochAt(offset)"),
        DeprecationLevel.ERROR
    )
    fun unixEpochMillisecondAt(offset: UtcOffset): Long = millisecondOfUnixEpochAt(offset)

    @Deprecated(
        "Use toInstantAt() instead.",
        ReplaceWith("this.toInstantAt(offset)"),
        DeprecationLevel.ERROR
    )
    fun instantAt(offset: UtcOffset): Instant = toInstantAt(offset)

    companion object {
        /**
         * The earliest supported [DateTime], which can be used as a "far past" sentinel.
         */
        val MIN = DateTime(Date.MIN, Time.MIN)

        /**
         * The latest supported [DateTime], which can be used as a "far future" sentinel.
         */
        val MAX = DateTime(Date.MAX, Time.MAX)

        /**
         * Creates a [DateTime] from a duration of milliseconds relative to the Unix epoch at [offset].
         */
        fun fromMillisecondsSinceUnixEpoch(millisecondsSinceUnixEpoch: LongMilliseconds, offset: UtcOffset): DateTime {
            val localMilliseconds = millisecondsSinceUnixEpoch + offset.totalSeconds
            val localEpochDay = localMilliseconds.value floorDiv MILLISECONDS_PER_DAY
            val nanosecondOfDay =
                (localMilliseconds.value floorMod MILLISECONDS_PER_DAY).milliseconds.inNanoseconds.value
            val date = Date.fromDayOfUnixEpoch(localEpochDay)
            val time = Time.fromNanosecondOfDay(nanosecondOfDay)
            return DateTime(date, time)
        }

        /**
         * Creates a [DateTime] from a duration of seconds relative to the Unix epoch at [offset], optionally, with some
         * number of additional nanoseconds added to it.
         */
        fun fromSecondsSinceUnixEpoch(
            secondsSinceUnixEpoch: LongSeconds,
            nanosecondAdjustment: IntNanoseconds = 0.nanoseconds,
            offset: UtcOffset
        ): DateTime {
            val adjustedSeconds =
                secondsSinceUnixEpoch + (nanosecondAdjustment.value floorDiv NANOSECONDS_PER_SECOND).seconds
            val nanosecond = nanosecondAdjustment.value floorMod NANOSECONDS_PER_SECOND
            val localSeconds = adjustedSeconds + offset.totalSeconds
            val localEpochDay = (localSeconds.value floorDiv SECONDS_PER_DAY)
            val secondOfDay = (localSeconds.value floorMod SECONDS_PER_DAY).toInt()
            val date = Date.fromDayOfUnixEpoch(localEpochDay)
            val time = Time.fromSecondOfDay(secondOfDay, nanosecond)
            return DateTime(date, time)
        }

        /**
         * Creates a [DateTime] from the millisecond of the Unix epoch at [offset].
         */
        fun fromMillisecondOfUnixEpoch(millisecond: Long, offset: UtcOffset): DateTime {
            return fromMillisecondsSinceUnixEpoch(millisecond.milliseconds, offset)
        }

        /**
         * Creates a [DateTime] from the second of the Unix epoch at [offset] and optionally, the nanosecond of the
         * second.
         */
        fun fromSecondOfUnixEpoch(second: Long, nanosecond: Int = 0, offset: UtcOffset): DateTime {
            return fromSecondsSinceUnixEpoch(second.seconds, nanosecond.nanoseconds, offset)
        }

        @Deprecated(
            "Use fromMillisecondOfUnixEpoch() instead.",
            ReplaceWith("DateTime.fromMillisecondOfUnixEpoch(millisecond, offset)"),
            DeprecationLevel.ERROR
        )
        fun fromUnixEpochMillisecond(millisecond: Long, offset: UtcOffset): DateTime {
            return fromMillisecondOfUnixEpoch(millisecond, offset)
        }

        @Deprecated(
            "Use fromSecondOfUnixEpoch() instead.",
            ReplaceWith("DateTime.fromSecondOfUnixEpoch(second, nanosecondAdjustment, offset)"),
            DeprecationLevel.ERROR
        )
        fun fromUnixEpochSecond(second: Long, nanosecondAdjustment: Int = 0, offset: UtcOffset): DateTime {
            return fromSecondOfUnixEpoch(second, nanosecondAdjustment, offset)
        }
    }
}

/**
 * Convert a string to a [DateTime].
 *
 * The string is assumed to be an ISO-8601 date-time representation in extended format. For example, `2019-08-22T18:00`
 * or `2019-08-22 18:00:30.123456789`. The output of [DateTime.toString] can be safely parsed using this method.
 *
 * @throws DateTimeParseException if parsing fails
 * @throws DateTimeException if the parsed date-time is invalid
 */
fun String.toDateTime() = toDateTime(DateTimeParsers.Iso.Extended.DATE_TIME)

/**
 * Converts a string to a [DateTime] using a specific parser.
 *
 * A set of predefined parsers can be found in [DateTimeParsers].
 *
 * Any custom parser must be capable of supplying the fields necessary to resolve both a [Date] and [Time].
 *
 * @throws DateTimeParseException if parsing fails
 * @throws DateTimeException if the parsed date-time is invalid
 */
fun String.toDateTime(
    parser: DateTimeParser,
    settings: DateTimeParserSettings = DateTimeParserSettings.DEFAULT
): DateTime {
    val result = parser.parse(this, settings)
    return result.toDateTime() ?: throwParserFieldResolutionException(this)
}

internal fun DateTimeParseResult.toDateTime(): DateTime? {
    val date = this.toDate()
    val time = this.toTime()

    return if (date != null && time != null) {
        DateTime(date, time)
    } else {
        null
    }
}

internal const val MAX_DATE_TIME_STRING_LENGTH = MAX_DATE_STRING_LENGTH + 1 + MAX_TIME_STRING_LENGTH

internal fun StringBuilder.appendDateTime(dateTime: DateTime): StringBuilder {
    return dateTime.run { appendDate(date).append('T').appendTime(time) }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy