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

commonMain.io.islandtime.ranges.DateTimeInterval.kt Maven / Gradle / Ivy

The newest version!
package io.islandtime.ranges

import io.islandtime.*
import io.islandtime.base.DateTimeField
import io.islandtime.measures.*
import io.islandtime.measures.internal.minusWithOverflow
import io.islandtime.parser.*
import io.islandtime.ranges.internal.*

/**
 * An interval between two date-times, assumed to be at the same offset from UTC.
 *
 * [DateTime.MIN] and [DateTime.MAX] are used as sentinels to indicate an unbounded (ie. infinite) start or end.
 */
class DateTimeInterval(
    override val start: DateTime = UNBOUNDED.start,
    override val endExclusive: DateTime = UNBOUNDED.endExclusive
) : Interval {

    override val endInclusive: DateTime
        get() = if (hasUnboundedEnd()) endExclusive else endExclusive - 1.nanoseconds

    override fun hasUnboundedStart(): Boolean = start == DateTime.MIN
    override fun hasUnboundedEnd(): Boolean = endExclusive == DateTime.MAX

    /**
     * Checks if this interval contains [value].
     * @param value a date-time, assumed to be in the same time zone
     */
    override fun contains(value: DateTime): Boolean {
        return value >= start && (value < endExclusive || hasUnboundedEnd())
    }

    override fun isEmpty(): Boolean {
        return start >= endExclusive
    }

    override fun equals(other: Any?): Boolean {
        return other is DateTimeInterval && (isEmpty() && other.isEmpty() ||
            start == other.start && endExclusive == other.endExclusive)
    }

    override fun hashCode(): Int {
        return if (isEmpty()) -1 else (31 * start.hashCode() + endExclusive.hashCode())
    }

    /**
     * Converts this interval to a string in ISO-8601 extended format.
     */
    override fun toString(): String = buildIsoString(
        maxElementSize = MAX_DATE_TIME_STRING_LENGTH,
        inclusive = false,
        appendFunction = StringBuilder::appendDateTime
    )

    /**
     * Converts this interval to the [Duration] between the start and end date-time, assuming they're in the same time
     * zone. In general, it's more appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight
     * savings rules won't be taken into account when working with [DateTime] directly.
     *
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    fun asDuration(): Duration {
        return when {
            isEmpty() -> Duration.ZERO
            isBounded() -> durationBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }
    }

    /**
     * Converts this interval into a [Period] of the same length.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    fun asPeriod(): Period {
        return when {
            isEmpty() -> Period.ZERO
            isBounded() -> periodBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }
    }

    /**
     * The number of whole years in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInYears: IntYears
        get() = when {
            isEmpty() -> 0.years
            isBounded() -> yearsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole months in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInMonths: IntMonths
        get() = when {
            isEmpty() -> 0.months
            isBounded() -> monthsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole weeks in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInWeeks: LongWeeks
        get() = when {
            isEmpty() -> 0L.weeks
            isBounded() -> weeksBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole days in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInDays: LongDays
        get() = when {
            isEmpty() -> 0L.days
            isBounded() -> daysBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole hours in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInHours: LongHours
        get() = when {
            isEmpty() -> 0L.hours
            isBounded() -> hoursBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole minutes in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInMinutes: LongMinutes
        get() = when {
            isEmpty() -> 0L.minutes
            isBounded() -> minutesBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole seconds in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInSeconds: LongSeconds
        get() = when {
            isEmpty() -> 0L.seconds
            isBounded() -> secondsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole milliseconds in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInMilliseconds: LongMilliseconds
        get() = when {
            isEmpty() -> 0L.milliseconds
            isBounded() -> millisecondsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of whole microseconds in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInMicroseconds: LongMicroseconds
        get() = when {
            isEmpty() -> 0L.microseconds
            isBounded() -> microsecondsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    /**
     * The number of nanoseconds in this interval.
     * @throws UnsupportedOperationException if the interval isn't bounded
     */
    val lengthInNanoseconds: LongNanoseconds
        get() = when {
            isEmpty() -> 0L.nanoseconds
            isBounded() -> nanosecondsBetween(start, endExclusive)
            else -> throwUnboundedIntervalException()
        }

    companion object {
        /**
         * An empty interval.
         */
        val EMPTY = DateTimeInterval(
            DateTime.fromSecondOfUnixEpoch(0L, 0, UtcOffset.ZERO),
            DateTime.fromSecondOfUnixEpoch(0L, 0, UtcOffset.ZERO)
        )

        /**
         * An unbounded (ie. infinite) interval.
         */
        val UNBOUNDED = DateTimeInterval(DateTime.MIN, DateTime.MAX)

        internal fun withInclusiveEnd(start: DateTime, endInclusive: DateTime): DateTimeInterval {
            val endExclusive = when {
                endInclusive == DateTime.MAX -> endInclusive
                endInclusive > MAX_INCLUSIVE_END_DATE_TIME ->
                    throw DateTimeException("The end of the interval can't be represented")
                else -> endInclusive + 1.nanoseconds
            }

            return DateTimeInterval(start, endExclusive)
        }
    }
}

/**
 * Converts a string to a [DateTimeInterval].
 *
 * The string is assumed to be an ISO-8601 time interval representation in extended format. The output of
 * [DateTimeInterval.toString] can be safely parsed using this method.
 *
 * Examples:
 * - `1990-01-04T03/1991-08-30T15:30:05.123`
 * - `../1991-08-30T15:30:05.123`
 * - `1990-01-04T03/..`
 * - `../..`
 * - (empty string)
 *
 * @throws DateTimeParseException if parsing fails
 * @throws DateTimeException if the parsed time is invalid
 */
fun String.toDateTimeInterval(): DateTimeInterval = toDateTimeInterval(DateTimeParsers.Iso.Extended.DATE_TIME_INTERVAL)

/**
 * Converts a string to a [DateTimeInterval] using a specific parser.
 *
 * A set of predefined parsers can be found in [DateTimeParsers].
 *
 * @throws DateTimeParseException if parsing fails
 * @throws DateTimeException if the parsed interval is invalid
 */
fun String.toDateTimeInterval(
    parser: GroupedDateTimeParser,
    settings: DateTimeParserSettings = DateTimeParserSettings.DEFAULT
): DateTimeInterval {
    val results = parser.parse(this, settings).expectingGroupCount(2, this)

    val start = when {
        results[0].isEmpty() -> null
        results[0].fields[DateTimeField.IS_UNBOUNDED] == 1L -> DateTimeInterval.UNBOUNDED.start
        else -> results[0].toDateTime() ?: throwParserFieldResolutionException(this)
    }

    val end = when {
        results[1].isEmpty() -> null
        results[1].fields[DateTimeField.IS_UNBOUNDED] == 1L -> DateTimeInterval.UNBOUNDED.endExclusive
        else -> results[1].toDateTime() ?: throwParserFieldResolutionException(this)
    }

    return when {
        start != null && end != null -> start until end
        start == null && end == null -> DateTimeInterval.EMPTY
        else -> throw DateTimeParseException("Intervals with unknown start or end are not supported")
    }
}

/**
 * Creates a [DateTimeInterval] from this date-time up to, but not including the nanosecond represented by [to].
 */
infix fun DateTime.until(to: DateTime): DateTimeInterval = DateTimeInterval(this, to)

/**
 * Gets the [Period] between two date-times, assuming they're in the same time zone.
 */
fun periodBetween(start: DateTime, endExclusive: DateTime): Period {
    return periodBetween(start.date, adjustedEndDate(start, endExclusive))
}

/**
 * Gets the number of whole years between two date-times, assuming they're in the same time zone.
 */
fun yearsBetween(start: DateTime, endExclusive: DateTime): IntYears {
    return yearsBetween(start.date, adjustedEndDate(start, endExclusive))
}

/**
 * Gets the number of whole months between two date-times, assuming they're in the same time zone.
 */
fun monthsBetween(start: DateTime, endExclusive: DateTime): IntMonths {
    return monthsBetween(start.date, adjustedEndDate(start, endExclusive))
}

/**
 * Gets the number whole weeks between two date-times, assuming they're in the same time zone.
 */
fun weeksBetween(start: DateTime, endExclusive: DateTime): LongWeeks {
    return daysBetween(start, endExclusive).inWeeks
}

/**
 * Gets the number whole days between two date-times, assuming they're in the same time zone.
 */
fun daysBetween(start: DateTime, endExclusive: DateTime): LongDays {
    return secondsBetween(start, endExclusive).inDays
}

/**
 * Gets the [Duration] between two date-times, assuming they have the same UTC offset. In general, it's more appropriate
 * to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be taken into account
 * when working with [DateTime] directly.
 */
fun durationBetween(start: DateTime, endExclusive: DateTime): Duration {
    val secondDiff =
        endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO) - start.secondsSinceUnixEpochAt(UtcOffset.ZERO)

    val nanoDiff =
        endExclusive.additionalNanosecondsSinceUnixEpoch minusWithOverflow start.additionalNanosecondsSinceUnixEpoch

    return durationOf(secondDiff, nanoDiff)
}

/**
 * Gets the number of whole hours between two date-times, assuming they have the same UTC offset. In general, it's more
 * appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be taken
 * into account when working with [DateTime] directly.
 */
fun hoursBetween(start: DateTime, endExclusive: DateTime): LongHours {
    return secondsBetween(start, endExclusive).inHours
}

/**
 * Gets the number of whole minutes between two date-times, assuming they have the same UTC offset. In general, it's
 * more appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be
 * taken into account when working with [DateTime] directly.
 */
fun minutesBetween(start: DateTime, endExclusive: DateTime): LongMinutes {
    return secondsBetween(start, endExclusive).inMinutes
}

/**
 * Gets the number of whole seconds between two date-times, assuming they have the same UTC offset. In general, it's
 * more appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be
 * taken into account when working with [DateTime] directly.
 *
 * @throws ArithmeticException if the result overflows
 */
fun secondsBetween(start: DateTime, endExclusive: DateTime): LongSeconds {
    return secondsBetween(
        start.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        start.additionalNanosecondsSinceUnixEpoch,
        endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        endExclusive.additionalNanosecondsSinceUnixEpoch
    )
}

/**
 * Gets the number of whole milliseconds between two date-times, assuming they have the same UTC offset. In general,
 * it's more appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be
 * taken into account when working with [DateTime] directly.
 *
 * @throws ArithmeticException if the result overflows
 */
fun millisecondsBetween(start: DateTime, endExclusive: DateTime): LongMilliseconds {
    return millisecondsBetween(
        start.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        start.additionalNanosecondsSinceUnixEpoch,
        endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        endExclusive.additionalNanosecondsSinceUnixEpoch
    )
}

/**
 * Gets the number of whole microseconds between two date-times, assuming they have the same UTC offset. In general,
 * it's more appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be
 * taken into account when working with [DateTime] directly.
 *
 *  @throws ArithmeticException if the result overflows
 */
fun microsecondsBetween(start: DateTime, endExclusive: DateTime): LongMicroseconds {
    return microsecondsBetween(
        start.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        start.additionalNanosecondsSinceUnixEpoch,
        endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        endExclusive.additionalNanosecondsSinceUnixEpoch
    )
}

/**
 * Gets the number of nanoseconds between two date-times, assuming they have the same UTC offset. In general, it's more
 * appropriate to calculate duration using [Instant] or [ZonedDateTime] as any daylight savings rules won't be taken
 * into account when working with [DateTime] directly.
 *
 * @throws ArithmeticException if the result overflows
 */
fun nanosecondsBetween(start: DateTime, endExclusive: DateTime): LongNanoseconds {
    return nanosecondsBetween(
        start.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        start.additionalNanosecondsSinceUnixEpoch,
        endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO),
        endExclusive.additionalNanosecondsSinceUnixEpoch
    )
}

internal fun adjustedEndDate(start: DateTime, endExclusive: DateTime): Date {
    return when {
        endExclusive.date > start.date && endExclusive.time < start.time -> endExclusive.date - 1.days
        endExclusive.date < start.date && endExclusive.time > start.time -> endExclusive.date + 1.days
        else -> endExclusive.date
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy