diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccc74208fd..eea6d6eed02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.20.23 +* `Klock`: + * Inited as copypaste from [korlibs/korge](https://github.com/korlibs/korge) and [korlibs/korlibs4](https://github.com/korlibs/korlibs4) + ## 0.20.22 * `Common`: diff --git a/README.md b/README.md index b813b2c758d..64253d241b3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # MicroUtils +--- + +**`Klock` module initial commit based on [korlibs/korge](https://github.com/korlibs/korge) and [korlibs/korlibs4](https://github.com/korlibs/korlibs4)** + +--- + This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies: * [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines) diff --git a/klock/LICENSE b/klock/LICENSE new file mode 100644 index 00000000000..bb512118833 --- /dev/null +++ b/klock/LICENSE @@ -0,0 +1,47 @@ +MIT License + +Copyright (c) 2023 Ovsiannikov Aleksei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------- + +MIT License + +Copyright (c) 2017-2019 Carlos Ballesteros Velasco and contributors +* https://github.com/korlibs/korge/graphs/contributors +* https://github.com/korlibs-archive/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/klock/build.gradle b/klock/build.gradle new file mode 100644 index 00000000000..d425197852e --- /dev/null +++ b/klock/build.gradle @@ -0,0 +1,7 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" diff --git a/klock/src/androidMain/kotlin/korlibs/time/Time.internal.jvm.kt b/klock/src/androidMain/kotlin/korlibs/time/Time.internal.jvm.kt new file mode 100644 index 00000000000..ca4ea81dc00 --- /dev/null +++ b/klock/src/androidMain/kotlin/korlibs/time/Time.internal.jvm.kt @@ -0,0 +1,37 @@ +@file:Suppress("PackageDirectoryMismatch") +package korlibs.time.internal + +import korlibs.time.* +import java.util.* + +internal actual object KlockInternal { + actual val currentTime: Double get() = CurrentKlockInternalJvm.currentTime + actual val now: TimeSpan get() = CurrentKlockInternalJvm.hrNow + actual fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = CurrentKlockInternalJvm.localTimezoneOffsetMinutes(time) + actual fun sleep(time: TimeSpan) { + val nanos = time.nanoseconds.toLong() + Thread.sleep(nanos / 1_000_000, (nanos % 1_000_000).toInt()) + } +} + +inline fun TemporalKlockInternalJvm(impl: KlockInternalJvm, callback: () -> T): T { + val old = CurrentKlockInternalJvm + CurrentKlockInternalJvm = impl + try { + return callback() + } finally { + CurrentKlockInternalJvm = old + } +} + +var CurrentKlockInternalJvm = object : KlockInternalJvm { +} + +interface KlockInternalJvm { + val currentTime: Double get() = (System.currentTimeMillis()).toDouble() + val microClock: Double get() = hrNow.microseconds + val hrNow: TimeSpan get() = TimeSpan.fromNanoseconds(System.nanoTime().toDouble()) + fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = TimeZone.getDefault().getOffset(time.unixMillisLong).milliseconds +} + +actual typealias Serializable = java.io.Serializable diff --git a/klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt b/klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt new file mode 100644 index 00000000000..7bf88bc45bf --- /dev/null +++ b/klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt @@ -0,0 +1,8 @@ +@file:Suppress("PackageDirectoryMismatch") +package korlibs.time.internal + +import korlibs.time.* +import java.util.Date + +fun Date.toDateTime() = DateTime(this.time) +fun DateTime.toDate() = Date(this.unixMillisLong) diff --git a/klock/src/commonMain/kotlin/korlibs/time/Date.kt b/klock/src/commonMain/kotlin/korlibs/time/Date.kt new file mode 100644 index 00000000000..d965035e212 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Date.kt @@ -0,0 +1,72 @@ +package korlibs.time + +import korlibs.time.internal.Serializable +import kotlin.jvm.JvmInline +import kotlin.math.abs + +/** + * Represents a triple of [year], [month] and [day]. + * + * It is packed in a value class wrapping an Int to prevent allocations. + */ +@JvmInline +value class Date(val encoded: Int) : Comparable, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + /** Constructs a new [Date] from the [year], [month] and [day] components. */ + operator fun invoke(year: Int, month: Int, day: Int) = Date((year shl 16) or (month shl 8) or (day shl 0)) + /** Constructs a new [Date] from the [year], [month] and [day] components. */ + operator fun invoke(year: Int, month: Month, day: Int) = Date(year, month.index1, day) + /** Constructs a new [Date] from the [year], [month] and [day] components. */ + operator fun invoke(year: Year, month: Month, day: Int) = Date(year.year, month.index1, day) + /** Constructs a new [Date] from the [yearMonth] and [day] components. */ + operator fun invoke(yearMonth: YearMonth, day: Int) = Date(yearMonth.yearInt, yearMonth.month1, day) + } + + /** The [year] part as [Int]. */ + val year: Int get() = encoded shr 16 + /** The [month] part as [Int] where [Month.January] is 1. */ + val month1: Int get() = (encoded ushr 8) and 0xFF + /** The [month] part. */ + val month: Month get() = Month[month1] + /** The [day] part. */ + val day: Int get() = (encoded ushr 0) and 0xFF + /** The [year] part as [Year]. */ + val yearYear: Year get() = Year(year) + + /** A [DateTime] instance representing this date and time from the beginning of the [day]. */ + val dateTimeDayStart get() = DateTime(year, month, day) + + /** The [dayOfYear] part. */ + val dayOfYear get() = dateTimeDayStart.dayOfYear + /** The [dayOfWeek] part. */ + val dayOfWeek get() = dateTimeDayStart.dayOfWeek + /** The [dayOfWeek] part as [Int]. */ + val dayOfWeekInt get() = dateTimeDayStart.dayOfWeekInt + + /** Converts this date to String using [format] for representing it. */ + fun format(format: String) = dateTimeDayStart.format(format) + /** Converts this date to String using [format] for representing it. */ + fun format(format: DateFormat) = dateTimeDayStart.format(format) + + /** Converts this date to String formatting it like "2020-01-01", "2020-12-31" or "-2020-12-31" if the [year] is negative */ + override fun toString(): String = "${if (year < 0) "-" else ""}${abs(year).toString()}-${abs(month1).toString().padStart(2, '0')}-${abs(day).toString().padStart(2, '0')}" + + override fun compareTo(other: Date): Int = this.encoded.compareTo(other.encoded) +} + +operator fun Date.plus(time: TimeSpan) = (this.dateTimeDayStart + time).date +operator fun Date.plus(time: MonthSpan) = (this.dateTimeDayStart + time).date +operator fun Date.plus(time: DateTimeSpan) = (this.dateTimeDayStart + time).date +operator fun Date.plus(time: Time) = DateTime.createAdjusted(year, month1, day, time.hour, time.minute, time.second, time.millisecond) + +operator fun Date.minus(time: TimeSpan) = (this.dateTimeDayStart - time).date +operator fun Date.minus(time: MonthSpan) = (this.dateTimeDayStart - time).date +operator fun Date.minus(time: DateTimeSpan) = (this.dateTimeDayStart - time).date +operator fun Date.minus(time: Time) = DateTime.createAdjusted(year, month1, day, -time.hour, -time.minute, -time.second, -time.millisecond) + +fun Date.inThisWeek(dayOfWeek: DayOfWeekWithLocale): Date = + this + (dayOfWeek.index0 - this.dayOfWeek.withLocale(dayOfWeek.locale).index0).days +fun Date.inThisWeek(dayOfWeek: DayOfWeek, locale: KlockLocale = KlockLocale.default): Date = inThisWeek(dayOfWeek.withLocale(locale)) diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateException.kt b/klock/src/commonMain/kotlin/korlibs/time/DateException.kt new file mode 100644 index 00000000000..a1a5d022722 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateException.kt @@ -0,0 +1,6 @@ +package korlibs.time + +/** + * An exception for Date operations. + */ +class DateException(msg: String) : RuntimeException(msg) diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt b/klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt new file mode 100644 index 00000000000..d1ab3215e90 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt @@ -0,0 +1,43 @@ +package korlibs.time + +/** Allows to [format] and [parse] instances of [Date], [DateTime] and [DateTimeTz] */ +interface DateFormat { + fun format(dd: DateTimeTz): String + fun tryParse(str: String, doThrow: Boolean = false, doAdjust: Boolean = true): DateTimeTz? + + companion object { + val DEFAULT_FORMAT = DateFormat("EEE, dd MMM yyyy HH:mm:ss z") + val FORMAT1 = DateFormat("yyyy-MM-dd'T'HH:mm:ssXXX") + val FORMAT2 = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + val FORMAT_DATE = DateFormat("yyyy-MM-dd") + + val FORMATS = listOf(DEFAULT_FORMAT, FORMAT1, FORMAT2, FORMAT_DATE) + + fun parse(date: String): DateTimeTz { + var lastError: Throwable? = null + for (format in FORMATS) { + try { + return format.parse(date) + } catch (e: Throwable) { + lastError = e + } + } + throw lastError!! + } + + operator fun invoke(pattern: String) = PatternDateFormat(pattern) + } +} + +fun DateFormat.parse(str: String, doAdjust: Boolean = true): DateTimeTz = + tryParse(str, doThrow = true, doAdjust = doAdjust) ?: throw DateException("Not a valid format: '$str' for '$this'") +fun DateFormat.parseDate(str: String): Date = parse(str).local.date + +fun DateFormat.parseUtc(str: String): DateTime = parse(str).utc +fun DateFormat.parseLocal(str: String): DateTime = parse(str).local + +fun DateFormat.format(date: Double): String = format(DateTime.fromUnixMillis(date)) +fun DateFormat.format(date: Long): String = format(DateTime.fromUnixMillis(date)) + +fun DateFormat.format(dd: DateTime): String = format(dd.toOffsetUnadjusted(0.minutes)) +fun DateFormat.format(dd: Date): String = format(dd.dateTimeDayStart) diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTime.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTime.kt new file mode 100644 index 00000000000..5b2b98d39b8 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTime.kt @@ -0,0 +1,458 @@ +package korlibs.time + +import korlibs.time.DateTime.Companion.EPOCH +import korlibs.time.internal.* +import kotlin.jvm.JvmInline +import kotlin.math.* + +/** + * Represents a Date in UTC (GMT+00) with millisecond precision. + * + * It is internally represented as an inlined double, thus doesn't allocate in any target including JS. + * It can represent without loss dates between (-(2 ** 52) and (2 ** 52)): + * - Thu Aug 10 -140744 07:15:45 GMT-0014 (Central European Summer Time) + * - Wed May 23 144683 18:29:30 GMT+0200 (Central European Summer Time) + */ +@JvmInline +value class DateTime( + /** Number of milliseconds since UNIX [EPOCH] */ + val unixMillis: Double +) : Comparable, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + /** It is a [DateTime] instance representing 00:00:00 UTC, Thursday, 1 January 1970. */ + val EPOCH = DateTime(0.0) + + /** + * Constructs a new [DateTime] from date and time information. + * + * This might throw a [DateException] on invalid dates. + */ + operator fun invoke( + year: Year, + month: Month, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime = DateTime( + DateTime.dateToMillis(year.year, month.index1, day) + DateTime.timeToMillis( + hour, + minute, + second + ) + milliseconds + ) + + /** + * Constructs a new [DateTime] from date and time information. + * + * This might throw a [DateException] on invalid dates. + */ + operator fun invoke( + date: Date, + time: Time = Time(0.milliseconds) + ): DateTime = DateTime( + date.year, date.month1, date.day, + time.hour, time.minute, time.second, time.millisecond + ) + + /** + * Constructs a new [DateTime] from date and time information. + * + * This might throw a [DateException] on invalid dates. + */ + operator fun invoke( + year: Int, + month: Month, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime = DateTime( + DateTime.dateToMillis(year, month.index1, day) + DateTime.timeToMillis( + hour, + minute, + second + ) + milliseconds + ) + + /** + * Constructs a new [DateTime] from date and time information. + * + * This might throw a [DateException] on invalid dates. + */ + operator fun invoke( + year: Int, + month: Int, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime = DateTime( + DateTime.dateToMillis(year, month, day) + DateTime.timeToMillis( + hour, + minute, + second + ) + milliseconds + ) + + /** + * Constructs a new [DateTime] from date and time information. + * + * On invalid dates, this function will try to adjust the specified invalid date to a valid one by clamping components. + */ + fun createClamped( + year: Int, + month: Int, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime { + val clampedMonth = month.coerceIn(1, 12) + return createUnchecked( + year = year, + month = clampedMonth, + day = day.coerceIn(1, Month(month).days(year)), + hour = hour.coerceIn(0, 23), + minute = minute.coerceIn(0, 59), + second = second.coerceIn(0, 59), + milliseconds = milliseconds + ) + } + + /** + * Constructs a new [DateTime] from date and time information. + * + * On invalid dates, this function will try to adjust the specified invalid date to a valid one by adjusting other components. + */ + fun createAdjusted( + year: Int, + month: Int, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime { + var dy = year + var dm = month + var dd = day + var th = hour + var tm = minute + var ts = second + + tm += ts.cycleSteps(0, 59); ts = ts.cycle(0, 59) // Adjust seconds, adding minutes + th += tm.cycleSteps(0, 59); tm = tm.cycle(0, 59) // Adjust minutes, adding hours + dd += th.cycleSteps(0, 23); th = th.cycle(0, 23) // Adjust hours, adding days + + while (true) { + val dup = Month(dm).days(dy) + + dm += dd.cycleSteps(1, dup); dd = dd.cycle(1, dup) // Adjust days, adding months + dy += dm.cycleSteps(1, 12); dm = dm.cycle(1, 12) // Adjust months, adding years + + // We have already found a day that is valid for the adjusted month! + if (dd.cycle(1, Month(dm).days(dy)) == dd) { + break + } + } + + return createUnchecked(dy, dm, dd, th, tm, ts, milliseconds) + } + + /** + * Constructs a new [DateTime] from date and time information. + * + * On invalid dates, this function will have an undefined behaviour. + */ + fun createUnchecked( + year: Int, + month: Int, + day: Int, + hour: Int = 0, + minute: Int = 0, + second: Int = 0, + milliseconds: Int = 0 + ): DateTime { + return DateTime( + DateTime.dateToMillisUnchecked(year, month, day) + DateTime.timeToMillisUnchecked( + hour, + minute, + second + ) + milliseconds + ) + } + + /** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */ + operator fun invoke(unix: Long) = fromUnixMillis(unix) + /** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */ + operator fun invoke(unix: Double) = fromUnixMillis(unix) + + /** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */ + fun fromUnixMillis(unix: Double): DateTime = DateTime(unix) + /** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */ + fun fromUnixMillis(unix: Long): DateTime = fromUnixMillis(unix.toDouble()) + + /** Constructs a new [DateTime] by parsing the [str] using standard date formats. */ + fun fromString(str: String) = DateFormat.parse(str) + /** Constructs a new [DateTime] by parsing the [str] using standard date formats. */ + fun parse(str: String) = DateFormat.parse(str) + + /** Returns the current time as [DateTime]. Note that since [DateTime] is inline, this property doesn't allocate on JavaScript. */ + fun now(): DateTime = DateTime(KlockInternal.currentTime) + /** Returns the current local time as [DateTimeTz]. */ + fun nowLocal(): DateTimeTz = DateTimeTz.nowLocal() + + /** Returns the total milliseconds since unix epoch. The same as [nowUnixMillisLong] but as double. To prevent allocation on targets without Long support. */ + fun nowUnixMillis(): Double = KlockInternal.currentTime + /** Returns the total milliseconds since unix epoch. */ + fun nowUnixMillisLong(): Long = KlockInternal.currentTime.toLong() + + internal const val EPOCH_INTERNAL_MILLIS = + 62135596800000.0 // Millis since 00-00-0000 00:00 UTC to UNIX EPOCH + + internal enum class DatePart { Year, DayOfYear, Month, Day } + + internal fun dateToMillisUnchecked(year: Int, month: Int, day: Int): Double = + (Year(year).daysSinceOne + Month(month).daysToStart(year) + day - 1) * MILLIS_PER_DAY.toDouble() - EPOCH_INTERNAL_MILLIS + + private fun timeToMillisUnchecked(hour: Int, minute: Int, second: Int): Double = + hour.toDouble() * MILLIS_PER_HOUR + minute.toDouble() * MILLIS_PER_MINUTE + second.toDouble() * MILLIS_PER_SECOND + + private fun dateToMillis(year: Int, month: Int, day: Int): Double { + //Year.checked(year) + Month.checked(month) + if (day !in 1..Month(month).days(year)) throw DateException("Day $day not valid for year=$year and month=$month") + return dateToMillisUnchecked(year, month, day) + } + + private fun timeToMillis(hour: Int, minute: Int, second: Int): Double { + if (hour !in 0..23) throw DateException("Hour $hour not in 0..23") + if (minute !in 0..59) throw DateException("Minute $minute not in 0..59") + if (second !in 0..59) throw DateException("Second $second not in 0..59") + return timeToMillisUnchecked(hour, minute, second) + } + + // millis are 00-00-0000 based. + internal fun getDatePart(millis: Double, part: DatePart): Int { + val totalDays = (millis / MILLIS_PER_DAY).toInt2() + + // Year + val year = Year.fromDays(totalDays) + if (part == DatePart.Year) return year.year + + // Day of Year + val isLeap = year.isLeap + val startYearDays = year.daysSinceOne + val dayOfYear = 1 + ((totalDays - startYearDays) umod year.days) + if (part == DatePart.DayOfYear) return dayOfYear + + // Month + val month = Month.fromDayOfYear(dayOfYear, isLeap) + ?: error("Invalid dayOfYear=$dayOfYear, isLeap=$isLeap") + if (part == DatePart.Month) return month.index1 + + // Day + val dayOfMonth = dayOfYear - month.daysToStart(isLeap) + if (part == DatePart.Day) return dayOfMonth + + error("Invalid DATE_PART") + } + } + + /** Number of milliseconds since the 00:00:00 UTC, Monday, 1 January 1 */ + val yearOneMillis: Double get() = EPOCH_INTERNAL_MILLIS + unixMillis + + /** The local offset for this date for the timezone of the device */ + val localOffset: TimezoneOffset get() = TimezoneOffset.local(DateTime(unixMillisDouble)) + + /** Number of milliseconds since UNIX [EPOCH] as [Double] */ + val unixMillisDouble: Double get() = unixMillis + + /** Number of milliseconds since UNIX [EPOCH] as [Long] */ + val unixMillisLong: Long get() = unixMillisDouble.toLong() + + /** The [Year] part */ + val year: Year get() = Year(yearInt) + /** The [Year] part as [Int] */ + val yearInt: Int get() = getDatePart(yearOneMillis, DatePart.Year) + + /** The [Month] part */ + val month: Month get() = Month[month1] + /** The [Month] part as [Int] where January is represented as 0 */ + val month0: Int get() = month1 - 1 + /** The [Month] part as [Int] where January is represented as 1 */ + val month1: Int get() = getDatePart(yearOneMillis, DatePart.Month) + + /** Represents a couple of [Year] and [Month] that has leap information and thus allows to get the number of days of that month */ + val yearMonth: YearMonth get() = YearMonth(year, month) + + /** The [dayOfMonth] part */ + val dayOfMonth: Int get() = getDatePart(yearOneMillis, DatePart.Day) + + /** The [dayOfWeek] part */ + val dayOfWeek: DayOfWeek get() = DayOfWeek[dayOfWeekInt] + /** The [dayOfWeek] part as [Int] */ + val dayOfWeekInt: Int get() = (yearOneMillis / MILLIS_PER_DAY + 1).toIntMod(7) + + /** The [dayOfYear] part */ + val dayOfYear: Int get() = getDatePart(yearOneMillis, DatePart.DayOfYear) + + /** The [hours] part */ + val hours: Int get() = (yearOneMillis / MILLIS_PER_HOUR).toIntMod(24) + /** The [minutes] part */ + val minutes: Int get() = (yearOneMillis / MILLIS_PER_MINUTE).toIntMod(60) + /** The [seconds] part */ + val seconds: Int get() = (yearOneMillis / MILLIS_PER_SECOND).toIntMod(60) + /** The [milliseconds] part */ + val milliseconds: Int get() = (yearOneMillis).toIntMod(1000) + + /** Returns a new local date that will match these components. */ + val localUnadjusted: DateTimeTz get() = DateTimeTz.local(this, localOffset) + /** Returns a new local date that will match these components but with a different [offset]. */ + fun toOffsetUnadjusted(offset: TimeSpan) = toOffsetUnadjusted(offset.offset) + /** Returns a new local date that will match these components but with a different [offset]. */ + fun toOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this, offset) + + /** Returns this date with the local offset of this device. Components might change because of the offset. */ + val local: DateTimeTz get() = DateTimeTz.utc(this, localOffset) + /** Returns this date with a local offset. Components might change because of the [offset]. */ + fun toOffset(offset: TimeSpan) = toOffset(offset.offset) + /** Returns this date with a local offset. Components might change because of the [offset]. */ + fun toOffset(offset: TimezoneOffset) = DateTimeTz.utc(this, offset) + /** Returns this date with a local offset. Components might change because of the [timeZone]. */ + fun toTimezone(timeZone: Timezone) = toOffset(timeZone.offset) + /** Returns this date with a 0 offset. Components are equal. */ + val utc: DateTimeTz get() = DateTimeTz.utc(this, TimezoneOffset(0.minutes)) + + /** Returns a [DateTime] of [this] day with the hour at 00:00:00 */ + val dateDayStart get() = DateTime(year, month, dayOfMonth, 0, 0, 0, 0) + /** Returns a [DateTime] of [this] day with the hour at 23:59:59.999 */ + val dateDayEnd get() = DateTime(year, month, dayOfMonth, 23, 59, 59, 999) + + /** Returns the quarter 1, 2, 3 or 4 */ + val quarter get() = (month0 / 3) + 1 + + // startOf + + val startOfYear get() = DateTime(year, Month.January, 1) + val startOfMonth get() = DateTime(year, month, 1) + val startOfQuarter get() = DateTime(year, Month[(quarter - 1) * 3 + 1], 1) + fun startOfDayOfWeek(day: DayOfWeek): DateTime { + for (n in 0 until 7) { + val date = (this - n.days) + if (date.dayOfWeek == day) return date.startOfDay + } + error("Shouldn't happen") + } + val startOfWeek: DateTime get() = startOfDayOfWeek(DayOfWeek.Sunday) + val startOfIsoWeek: DateTime get() = startOfDayOfWeek(DayOfWeek.Monday) + val startOfDay get() = DateTime(year, month, dayOfMonth) + val startOfHour get() = DateTime(year, month, dayOfMonth, hours) + val startOfMinute get() = DateTime(year, month, dayOfMonth, hours, minutes) + val startOfSecond get() = DateTime(year, month, dayOfMonth, hours, minutes, seconds) + + // endOf + + val endOfYear get() = DateTime(year, Month.December, 31, 23, 59, 59, 999) + val endOfMonth get() = DateTime(year, month, month.days(year), 23, 59, 59, 999) + val endOfQuarter get() = DateTime(year, Month[(quarter - 1) * 3 + 3], month.days(year), 23, 59, 59, 999) + fun endOfDayOfWeek(day: DayOfWeek): DateTime { + for (n in 0 until 7) { + val date = (this + n.days) + if (date.dayOfWeek == day) return date.endOfDay + } + error("Shouldn't happen") + } + val endOfWeek: DateTime get() = endOfDayOfWeek(DayOfWeek.Monday) + val endOfIsoWeek: DateTime get() = endOfDayOfWeek(DayOfWeek.Sunday) + val endOfDay get() = DateTime(year, month, dayOfMonth, 23, 59, 59, 999) + val endOfHour get() = DateTime(year, month, dayOfMonth, hours, 59, 59, 999) + val endOfMinute get() = DateTime(year, month, dayOfMonth, hours, minutes, 59, 999) + val endOfSecond get() = DateTime(year, month, dayOfMonth, hours, minutes, seconds, 999) + + val date get() = Date(yearInt, month1, dayOfMonth) + val time get() = Time(hours, minutes, seconds, milliseconds) + + operator fun plus(delta: MonthSpan): DateTime = this.add(delta.totalMonths, 0.0) + operator fun plus(delta: DateTimeSpan): DateTime = this.add(delta.totalMonths, delta.totalMilliseconds) + operator fun plus(delta: TimeSpan): DateTime = add(0, delta.milliseconds) + + operator fun minus(delta: MonthSpan): DateTime = this + -delta + operator fun minus(delta: DateTimeSpan): DateTime = this + -delta + operator fun minus(delta: TimeSpan): DateTime = this + (-delta) + + operator fun minus(other: DateTime): TimeSpan = (this.unixMillisDouble - other.unixMillisDouble).milliseconds + + override fun compareTo(other: DateTime): Int = this.unixMillis.compareTo(other.unixMillis) + + /** Constructs a new [DateTime] after adding [deltaMonths] and [deltaMilliseconds] */ + fun add(deltaMonths: Int, deltaMilliseconds: Double): DateTime = when { + deltaMonths == 0 && deltaMilliseconds == 0.0 -> this + deltaMonths == 0 -> DateTime(this.unixMillis + deltaMilliseconds) + else -> { + var year = this.year + var month = this.month.index1 + var day = this.dayOfMonth + val i = month - 1 + deltaMonths + + if (i >= 0) { + month = i % Month.Count + 1 + year += i / Month.Count + } else { + month = Month.Count + (i + 1) % Month.Count + year += (i - (Month.Count - 1)) / Month.Count + } + //Year.checked(year) + val days = Month(month).days(year) + if (day > days) day = days + + DateTime(dateToMillisUnchecked(year.year, month, day) + (yearOneMillis % MILLIS_PER_DAY) + deltaMilliseconds) + } + } + + /** Constructs a new [DateTime] after adding [dateSpan] and [timeSpan] */ + fun add(dateSpan: MonthSpan, timeSpan: TimeSpan): DateTime = add(dateSpan.totalMonths, timeSpan.milliseconds) + + fun copyDayOfMonth( + year: Year = this.year, + month: Month = this.month, + dayOfMonth: Int = this.dayOfMonth, + hours: Int = this.hours, + minutes: Int = this.minutes, + seconds: Int = this.seconds, + milliseconds: Int = this.milliseconds + ) = DateTime(year, month, dayOfMonth, hours, minutes, seconds, milliseconds) + + /** Converts this date to String using [format] for representing it */ + fun format(format: DateFormat): String = format.format(this) + /** Converts this date to String using [format] for representing it */ + fun format(format: String): String = DateFormat(format).format(this) + + /** Converts this date to String using [format] for representing it */ + fun toString(format: String): String = DateFormat(format).format(this) + /** Converts this date to String using [format] for representing it */ + fun toString(format: DateFormat): String = format.format(this) + + /** Converts this date to String using the [DateFormat.DEFAULT_FORMAT] for representing it */ + fun toStringDefault(): String = DateFormat.DEFAULT_FORMAT.format(this) + //override fun toString(): String = DateFormat.DEFAULT_FORMAT.format(this) + override fun toString(): String = "DateTime($unixMillisLong)" +} + +fun max(a: DateTime, b: DateTime): DateTime = + DateTime.fromUnixMillis(max(a.unixMillis, b.unixMillis)) +fun min(a: DateTime, b: DateTime): DateTime = + DateTime.fromUnixMillis(min(a.unixMillis, b.unixMillis)) +fun DateTime.clamp(min: DateTime, max: DateTime): DateTime = when { + this < min -> min + this > max -> max + else -> this +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt new file mode 100644 index 00000000000..e77175db774 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt @@ -0,0 +1,154 @@ +package korlibs.time + +import korlibs.time.internal.Serializable + +/** + * Represents a right-opened range between two dates. + */ +data class DateTimeRange(val from: DateTime, val to: DateTime) : Comparable, Serializable { + val valid get() = from <= to + + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + operator fun invoke(base: Date, from: Time, to: Time): DateTimeRange = DateTimeRange(base + from, base + to) + } + + val size: TimeSpan get() = to - from + + val min get() = from + val max get() = to + /** + * Duration [TimeSpan] without having into account actual months/years. + */ + val duration: TimeSpan get() = to - from + + /** + * [DateTimeSpan] distance between two dates, month and year aware. + */ + val span: DateTimeSpan by lazy { + val reverse = to < from + val rfrom = if (!reverse) from else to + val rto = if (!reverse) to else from + + var years = 0 + var months = 0 + + var pivot = rfrom + + // Compute years + val diffYears = (rto.year - pivot.year) + pivot += diffYears.years + years += diffYears + if (pivot > rto) { + pivot -= 1.years + years-- + } + + // Compute months (at most an iteration of 12) + while (true) { + val t = pivot + 1.months + if (t <= rto) { + months++ + pivot = t + } else { + break + } + } + + val out = DateTimeSpan(years.years + months.months, rto - pivot) + if (reverse) -out else out + } + + /** + * Checks if a date is contained in this range. + */ + operator fun contains(date: DateTime): Boolean { + val unix = date.unixMillisDouble + val from = from.unixMillisDouble + val to = to.unixMillisDouble + return if (unix < from) false else unix < to + } + + operator fun contains(other: DateTimeRange): Boolean { + return other.min >= this.min && other.max <= this.max + } + + private inline fun _intersectionWith(that: DateTimeRange, rightOpen: Boolean, handler: (from: DateTime, to: DateTime, matches: Boolean) -> T): T { + val from = max(this.from, that.from) + val to = min(this.to, that.to) + return handler(from, to, if (rightOpen) from < to else from <= to) + } + + /** + * Returns new [DateTimeRange] or null - the result of intersection of this and [that] DateTimeRanges. + */ + fun intersectionWith(that: DateTimeRange, rightOpen: Boolean = true): DateTimeRange? { + return _intersectionWith(that, rightOpen) { from, to, matches -> + when { + matches -> DateTimeRange(from, to) + else -> null + } + } + } + + /** + * Returns true if this and [that] DateTimeRanges have intersection otherwise false. + */ + fun intersectsWith(that: DateTimeRange, rightOpen: Boolean = true): Boolean = _intersectionWith(that, rightOpen) { _, _, matches -> matches } + + /** + * Returns true if this and [that] DateTimeRanges have intersection or at least a common end otherwise false. + */ + fun intersectsOrInContactWith(that: DateTimeRange): Boolean = intersectsWith(that, rightOpen = false) + + /** + * Returns new [DateTimeRange] or null - the result of merging this and [that] DateTimeRanges if they have intersection. + */ + fun mergeOnContactOrNull(that: DateTimeRange): DateTimeRange? { + if (!intersectsOrInContactWith(that)) return null + val min = min(this.min, that.min) + val max = max(this.max, that.max) + return DateTimeRange(min, max) + } + + /** + * Returns a [List] of 0, 1 or 2 [DateTimeRange]s - the result of removing [that] DateTimeRange from this one + */ + fun without(that: DateTimeRange): List = when { + // Full remove + (that.min <= this.min) && (that.max >= this.max) -> listOf() + // To the right or left, nothing to remove + (that.min >= this.max) || (that.max <= this.min) -> listOf(this) + // In the middle + else -> { + val p0 = this.min + val p1 = that.min + val p2 = that.max + val p3 = this.max + val c1 = if (p0 < p1) DateTimeRange(p0, p1) else null + val c2 = if (p2 < p3) DateTimeRange(p2, p3) else null + listOfNotNull(c1, c2) + } + } + + fun toString(format: DateFormat): String = "${min.toString(format)}..${max.toString(format)}" + fun toStringLongs(): String = "${min.unixMillisLong}..${max.unixMillisLong}" + fun toStringDefault(): String = toString(DateFormat.FORMAT1) + //override fun toString(): String = toString(DateFormat.FORMAT1) + override fun toString(): String = "$min..$max" + + override fun compareTo(other: DateTime): Int { + if (this.max <= other) return -1 + if (this.min > other) return +1 + return 0 + } +} + +fun List.toStringLongs() = this.map { it.toStringLongs() }.toString() + +/** + * Generates a right-opened range between two [DateTime]s + */ +infix fun DateTime.until(other: DateTime) = DateTimeRange(this, other) diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt new file mode 100644 index 00000000000..bd5e5bb7d9c --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt @@ -0,0 +1,270 @@ +package korlibs.time + +import korlibs.time.internal.BSearchResult +import korlibs.time.internal.Serializable +import korlibs.time.internal.fastForEach +import korlibs.time.internal.genericBinarySearch + +// Properties: +// - ranges are sorted +// - ranges do not overlap/intersect between each other (they are merged and normalized) +// These properties allows to do some tricks and optimizations like binary search and a lot of O(n) operations. +data class DateTimeRangeSet private constructor(val dummy: Boolean, val ranges: List) : Serializable { + + /** [DateTimeRange] from the beginning of the first element to the end of the last one. */ + val bounds = DateTimeRange( + ranges.firstOrNull()?.from ?: DateTime.EPOCH, + ranges.lastOrNull()?.to ?: DateTime.EPOCH + ) + + /** Total time of all [ranges]. */ + val size: TimeSpan by lazy { + var out = 0.seconds + ranges.fastForEach { out += it.size } + out + } + + constructor(ranges: List) : this(false, Fast.combine(ranges)) + constructor(range: DateTimeRange) : this(listOf(range)) + constructor(vararg ranges: DateTimeRange) : this(ranges.toList()) + + operator fun plus(range: DateTimeRange): DateTimeRangeSet = this + DateTimeRangeSet(range) + operator fun plus(right: DateTimeRangeSet): DateTimeRangeSet = DateTimeRangeSet(this.ranges + right.ranges) + + operator fun minus(range: DateTimeRange): DateTimeRangeSet = this - DateTimeRangeSet(range) + operator fun minus(right: DateTimeRangeSet): DateTimeRangeSet = Fast.minus(this, right) + + operator fun contains(time: DateTime): Boolean = Fast.contains(time, this) + operator fun contains(time: DateTimeRange): Boolean = Fast.contains(time, this) + + fun intersection(range: DateTimeRange): DateTimeRangeSet = this.intersection(DateTimeRangeSet(range)) + fun intersection(vararg range: DateTimeRange): DateTimeRangeSet = this.intersection(DateTimeRangeSet(*range)) + fun intersection(right: DateTimeRangeSet): DateTimeRangeSet = Fast.intersection(this, right) + + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + fun toStringLongs(ranges: List): String = "${ranges.map { it.toStringLongs() }}" + } + + object Fast { + internal fun combine(ranges: List): List { + if (ranges.isEmpty()) return ranges + + val sorted = ranges.sortedBy { it.from.unixMillis } + val out = arrayListOf() + var pivot = sorted.first() + for (n in 1 until sorted.size) { + val current = sorted[n] + val result = pivot.mergeOnContactOrNull(current) + pivot = if (result != null) { + result + } else { + out.add(pivot) + current + } + } + return out + listOf(pivot) + } + + internal fun minus(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet { + if (left.ranges.isEmpty() || right.ranges.isEmpty()) return left + + val ll = left.ranges + val rr = right.ranges.filter { it.intersectsWith(left.bounds) } + var lpos = 0 + var rpos = 0 + var l = ll.getOrNull(lpos++) + var r = rr.getOrNull(rpos++) + val out = arrayListOf() + //debug { "-----------------" } + //debug { "Minus:" } + //debug { " - ll=${toStringLongs(ll)}" } + //debug { " - rr=${toStringLongs(rr)}" } + while (l != null && r != null) { + val result = l.without(r) + //debug { "Minus ${l!!.toStringLongs()} with ${r!!.toStringLongs()} -- ${toStringLongs(result)}" } + when (result.size) { + 0 -> { + //debug { " - Full remove" } + l = ll.getOrNull(lpos++) + } + 1 -> { + //debug { " - Result 1" } + when { + r.from >= l.to -> { + //debug { " - Move left. Emit ${result[0].toStringLongs()}" } + out.add(result[0]) + l = ll.getOrNull(lpos++) + } + l == result[0] -> { + //debug { " - Move right. Change l from ${l!!.toStringLongs()} to ${result[0].toStringLongs()}" } + r = rr.getOrNull(rpos++) + } + else -> { + //debug { " - Use this l=${result[0].toStringLongs()} from ${l!!.toStringLongs()}" } + l = result[0] + } + } + } + else -> { + //debug { " - One chunk removed: ${result.map { it.toStringLongs() }}" } + //debug { " - Emit: ${result[0].toStringLongs()}" } + //debug { " - Keep: ${result[1].toStringLongs()}" } + out.add(result[0]) + l = result[1] + } + } + } + if (l != null) { + out.add(l) + } + while (lpos < ll.size) out.add(ll[lpos++]) + + //debug { toStringLongs(out) } + return DateTimeRangeSet(out) + } + + fun intersection(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet { + if (left.ranges.isEmpty() || right.ranges.isEmpty()) return DateTimeRangeSet(listOf()) + + val ll = left.ranges.filter { it.intersectsWith(right.bounds) } + val rr = right.ranges.filter { it.intersectsWith(left.bounds) } + val out = arrayListOf() + //debug { "-----------------" } + //debug { "Intersection:" } + //debug { " - ll=${toStringLongs(ll)}" } + //debug { " - rr=${toStringLongs(rr)}" } + var rpos = 0 + for (l in ll) { + rpos = 0 + // We should be able to do this because the time ranges doesn't intersect each other + //while (rpos > 0) { + // val r = rr.getOrNull(rpos) ?: break + // if ((r.from < l.from) && (r.to < l.from)) break // End since we are already + // rpos-- + //} + while (rpos < rr.size) { + val r = rr.getOrNull(rpos) ?: break + if (r.min > l.max) break // End since the rest are going to be farther + val res = l.intersectionWith(r) + if (res != null) { + out.add(res) + } + rpos++ + } + } + + //debug { toStringLongs(out) } + return DateTimeRangeSet(out) + } + + fun contains(time: DateTime, rangeSet: DateTimeRangeSet): Boolean { + if (time !in rangeSet.bounds) return false // Early guard clause + val ranges = rangeSet.ranges + val result = BSearchResult(genericBinarySearch(0, ranges.size) { index -> ranges[index].compareTo(time) }) + return result.found + } + + fun contains(time: DateTimeRange, rangeSet: DateTimeRangeSet): Boolean { + if (time !in rangeSet.bounds) return false // Early guard clause + val ranges = rangeSet.ranges + val result = BSearchResult(genericBinarySearch(0, ranges.size) { index -> + val range = ranges[index] + when { + time in range -> 0 + time.min < range.min -> +1 + else -> -1 + } + }) + return result.found + } + //private inline fun debug(gen: () -> String) { println(gen()) } + } + + object Slow { + // @TODO: Optimize + internal fun minus(l: DateTimeRangeSet, r: DateTimeRangeSet): DateTimeRangeSet { + val rightList = r.ranges + var out = l.ranges.toMutableList() + restart@ while (true) { + for ((leftIndex, left) in out.withIndex()) { + for (right in rightList) { + val result = left.without(right) + if (result.size != 1 || result[0] != left) { + out = (out.slice(0 until leftIndex) + result + out.slice(leftIndex + 1 until out.size)).toMutableList() + continue@restart + } + } + } + break + } + return DateTimeRangeSet(out) + } + + internal fun combine(ranges: List): List { + // @TODO: Improve performance and verify fast combiner + val ranges = ranges.toMutableList() + restart@ while (true) { + for (i in ranges.indices) { + for (j in ranges.indices) { + if (i == j) continue + val ri = ranges[i] + val rj = ranges[j] + val concat = ri.mergeOnContactOrNull(rj) + if (concat != null) { + //println("Combining $ri and $rj : $concat") + ranges.remove(rj) + ranges[i] = concat + continue@restart + } + } + } + break + } + return ranges + } + + fun intersection(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet { + val leftList = left.ranges + val rightList = right.ranges + val out = arrayListOf() + for (l in leftList) { + for (r in rightList) { + if (r.min > l.max) break + val result = l.intersectionWith(r) + if (result != null) { + out.add(result) + } + } + //val chunks = rightList.mapNotNull { r -> l.intersectionWith(r) } + //out.addAll(DateTimeRangeSet(chunks).ranges) + } + return DateTimeRangeSet(out) + } + + fun contains(time: DateTime, rangeSet: DateTimeRangeSet): Boolean { + if (time !in rangeSet.bounds) return false // Early guard clause + // @TODO: Fast binary search, since the ranges doesn't intersect each other + rangeSet.ranges.fastForEach { range -> + if (time in range) return true + } + return false + } + + fun contains(time: DateTimeRange, rangeSet: DateTimeRangeSet): Boolean { + if (time !in rangeSet.bounds) return false // Early guard clause + // @TODO: Fast binary search, since the ranges doesn't intersect each other + rangeSet.ranges.fastForEach { range -> + if (time in range) return true + } + return false + } + } + + fun toStringLongs(): String = "${ranges.map { it.toStringLongs() }}" + override fun toString(): String = "$ranges" +} + +fun Iterable.toRangeSet() = DateTimeRangeSet(this.toList()) diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt new file mode 100644 index 00000000000..513f6c33fb0 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt @@ -0,0 +1,143 @@ +package korlibs.time + +import korlibs.time.internal.MILLIS_PER_DAY +import korlibs.time.internal.MILLIS_PER_HOUR +import korlibs.time.internal.MILLIS_PER_MINUTE +import korlibs.time.internal.MILLIS_PER_SECOND +import korlibs.time.internal.MILLIS_PER_WEEK +import korlibs.time.internal.Moduler +import korlibs.time.internal.Serializable + +/** + * Immutable structure representing a set of a [monthSpan] and a [timeSpan]. + * This structure loses information about which months are included, that makes it impossible to generate a real [TimeSpan] including months. + * You can use [DateTimeRange.duration] to get this information from two real [DateTime]. + */ +data class DateTimeSpan( + /** The [MonthSpan] part */ + val monthSpan: MonthSpan, + /** The [TimeSpan] part */ + val timeSpan: TimeSpan +) : Comparable, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + } + + constructor( + years: Int = 0, + months: Int = 0, + weeks: Int = 0, + days: Int = 0, + hours: Int = 0, + minutes: Int = 0, + seconds: Int = 0, + milliseconds: Double = 0.0 + ) : this( + years.years + months.months, + weeks.weeks + days.days + hours.hours + minutes.minutes + seconds.seconds + milliseconds.milliseconds + ) + + operator fun unaryMinus() = DateTimeSpan(-monthSpan, -timeSpan) + operator fun unaryPlus() = DateTimeSpan(+monthSpan, +timeSpan) + + operator fun plus(other: TimeSpan) = DateTimeSpan(monthSpan, timeSpan + other) + operator fun plus(other: MonthSpan) = DateTimeSpan(monthSpan + other, timeSpan) + operator fun plus(other: DateTimeSpan) = DateTimeSpan(monthSpan + other.monthSpan, timeSpan + other.timeSpan) + + operator fun minus(other: TimeSpan) = this + -other + operator fun minus(other: MonthSpan) = this + -other + operator fun minus(other: DateTimeSpan) = this + -other + + operator fun times(times: Double) = DateTimeSpan((monthSpan * times), (timeSpan * times)) + operator fun times(times: Int) = this * times.toDouble() + operator fun times(times: Float) = this * times.toDouble() + + operator fun div(times: Double) = times(1.0 / times) + operator fun div(times: Int) = this / times.toDouble() + operator fun div(times: Float) = this / times.toDouble() + + /** From the date part, all months represented as a [totalYears] [Double] */ + val totalYears: Double get() = monthSpan.totalYears + + /** From the date part, all months including months and years */ + val totalMonths: Int get() = monthSpan.totalMonths + + /** From the time part, all the milliseconds including milliseconds, seconds, minutes, hours, days and weeks */ + val totalMilliseconds: Double get() = timeSpan.milliseconds + + /** The [years] part as an integer. */ + val years: Int get() = monthSpan.years + /** The [months] part as an integer. */ + val months: Int get() = monthSpan.months + + /** The [weeks] part as an integer. */ + val weeks: Int get() = computed.weeks + + val daysNotIncludingWeeks: Int get() = days + + /** The [daysIncludingWeeks] part as an integer including days and weeks. */ + val daysIncludingWeeks: Int get() = computed.days + (computed.weeks * DayOfWeek.Count) + + /** The [days] part as an integer. */ + val days: Int get() = computed.days + + /** The [hours] part as an integer. */ + val hours: Int get() = computed.hours + + /** The [minutes] part as an integer. */ + val minutes: Int get() = computed.minutes + + /** The [seconds] part as an integer. */ + val seconds: Int get() = computed.seconds + + /** The [milliseconds] part as a double. */ + val milliseconds: Double get() = computed.milliseconds + + /** The [secondsIncludingMilliseconds] part as a doble including seconds and milliseconds. */ + val secondsIncludingMilliseconds: Double get() = computed.seconds + computed.milliseconds / MILLIS_PER_SECOND + + /** + * Note that if milliseconds overflow months this could not be exactly true. But probably will work in most cases. + * This structure doesn't have information about which months are counted. So some months could have 28-31 days and thus can't be done. + * You can use [DateTimeRange.duration] to compare this with real precision using a range between two [DateTime]. + */ + override fun compareTo(other: DateTimeSpan): Int { + if (this.totalMonths != other.totalMonths) return this.monthSpan.compareTo(other.monthSpan) + return this.timeSpan.compareTo(other.timeSpan) + } + + /** + * Represents this [DateTimeSpan] as a string like `50Y 10M 3W 6DH 30m 15s`. + * Parts that are zero, won't be included. You can omit weeks and represent them + * as days by adjusting the [includeWeeks] parameter. + */ + fun toString(includeWeeks: Boolean): String = arrayListOf().apply { + if (years != 0) add("${years}Y") + if (months != 0) add("${months}M") + if (includeWeeks && weeks != 0) add("${weeks}W") + if (days != 0 || (!includeWeeks && weeks != 0)) add("${if (includeWeeks) days else daysIncludingWeeks}D") + if (hours != 0) add("${hours}H") + if (minutes != 0) add("${minutes}m") + if (seconds != 0 || milliseconds != 0.0) add("${secondsIncludingMilliseconds}s") + if (monthSpan == 0.years && ((timeSpan == 0.seconds) || (timeSpan == (-0).seconds))) add("0s") + }.joinToString(" ") + + override fun toString(): String = toString(includeWeeks = true) + + private class ComputedTime(val weeks: Int, val days: Int, val hours: Int, val minutes: Int, val seconds: Int, val milliseconds: Double) { + companion object { + operator fun invoke(time: TimeSpan): ComputedTime = Moduler(time.milliseconds).run { + val weeks = int(MILLIS_PER_WEEK) + val days = int(MILLIS_PER_DAY) + val hours = int(MILLIS_PER_HOUR) + val minutes = int(MILLIS_PER_MINUTE) + val seconds = int(MILLIS_PER_SECOND) + val milliseconds = double(1) + return ComputedTime(weeks, days, hours, minutes, seconds, milliseconds) + } + } + } + + private val computed by lazy { ComputedTime(timeSpan) } +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpanFormat.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpanFormat.kt new file mode 100644 index 00000000000..9259a413d78 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTimeSpanFormat.kt @@ -0,0 +1,12 @@ +package korlibs.time + +interface DateTimeSpanFormat { + fun format(dd: DateTimeSpan): String + fun tryParse(str: String, doThrow: Boolean): DateTimeSpan? +} + +fun DateTimeSpanFormat.format(dd: TimeSpan): String = format(dd + 0.months) +fun DateTimeSpanFormat.format(dd: MonthSpan): String = format(dd + 0.seconds) + +fun DateTimeSpanFormat.parse(str: String): DateTimeSpan = + tryParse(str, doThrow = true) ?: throw DateException("Not a valid format: '$str' for '$this'") diff --git a/klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt b/klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt new file mode 100644 index 00000000000..bfca9fb9578 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt @@ -0,0 +1,125 @@ +package korlibs.time + +import korlibs.time.internal.Serializable + +/** [DateTime] with an associated [TimezoneOffset] */ +class DateTimeTz private constructor( + /** The [adjusted] part */ + private val adjusted: DateTime, + /** The [offset] part */ + val offset: TimezoneOffset +) : Comparable, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + /** Creates a new [DateTimeTz] with the [utc] date and an [offset]. The [utc] components will be the same as this independently on the [offset]. */ + fun local(local: DateTime, offset: TimezoneOffset) = DateTimeTz(local, offset) + + /** Creates a new [DateTimeTz] with the [utc] date and an [offset]. The [utc] components might be different depending on the [offset]. */ + fun utc(utc: DateTime, offset: TimezoneOffset) = DateTimeTz(utc + offset.time, offset) + + /** Creates a new local [DateTimeTz] from a [unix] time */ + fun fromUnixLocal(unix: Long): DateTimeTz = DateTime(unix).localUnadjusted + + /** Creates a new local [DateTimeTz] from a [unix] time applied*/ + fun fromUnix(unix: Long): DateTimeTz { + val unixDateTime = DateTime(unix) + return utc(unixDateTime, TimezoneOffset.local(unixDateTime)) + } + + /** Returns the current local [DateTimeTz] */ + fun nowLocal(): DateTimeTz = DateTime.now().local + } + + /** Returns a new UTC date that will match these components without being the same time */ + val local: DateTime get() = adjusted + + /** Returns a new UTC date that might not match these components, but it is the same time as UTC */ + val utc: DateTime get() = (adjusted - offset.time) + + /** The [Year] part */ + val year: Year get() = adjusted.year + /** The [Year] part as [Int] */ + val yearInt: Int get() = adjusted.yearInt + + /** The [Month] part */ + val month: Month get() = adjusted.month + /** The [Month] part as [Int] where January is represented as 0 */ + val month0: Int get() = adjusted.month0 + /** The [Month] part as [Int] where January is represented as 1 */ + val month1: Int get() = adjusted.month1 + + /** Represents a couple of [Year] and [Month] that has leap information and thus allows to get the number of days of that month */ + val yearMonth: YearMonth get() = adjusted.yearMonth + + /** The [dayOfMonth] part */ + val dayOfMonth: Int get() = adjusted.dayOfMonth + + /** The [dayOfWeek] part */ + val dayOfWeek: DayOfWeek get() = adjusted.dayOfWeek + /** The [dayOfWeek] part as [Int] */ + val dayOfWeekInt: Int get() = adjusted.dayOfWeekInt + + /** The [dayOfYear] part */ + val dayOfYear: Int get() = adjusted.dayOfYear + + /** The [hours] part */ + val hours: Int get() = adjusted.hours + /** The [minutes] part */ + val minutes: Int get() = adjusted.minutes + /** The [seconds] part */ + val seconds: Int get() = adjusted.seconds + /** The [milliseconds] part */ + val milliseconds: Int get() = adjusted.milliseconds + + /** Constructs this local date with a new [offset] without changing its components */ + fun toOffsetUnadjusted(offset: TimeSpan) = toOffsetUnadjusted(offset.offset) + /** Constructs this local date with a new [offset] without changing its components */ + fun toOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this.local, offset) + + /** Constructs this local date by adding an additional [offset] without changing its components */ + fun addOffsetUnadjusted(offset: TimeSpan) = addOffsetUnadjusted(offset.offset) + /** Constructs this local date by adding an additional [offset] without changing its components */ + fun addOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this.local, (this.offset.time + offset.time).offset) + + /** Constructs the UTC part of this date with a new [offset] */ + fun toOffset(offset: TimeSpan) = toOffset(offset.offset) + /** Constructs the UTC part of this date with a new [offset] */ + fun toOffset(offset: TimezoneOffset) = DateTimeTz.utc(this.utc, offset) + + /** Constructs the UTC part of this date by adding an additional [offset] */ + fun addOffset(offset: TimeSpan) = addOffset(offset.offset) + /** Constructs the UTC part of this date by adding an additional [offset] */ + fun addOffset(offset: TimezoneOffset) = DateTimeTz.utc(this.utc, (this.offset.time + offset.time).offset) + + /** Constructs a new [DateTimeTz] after adding [dateSpan] and [timeSpan] */ + fun add(dateSpan: MonthSpan, timeSpan: TimeSpan): DateTimeTz = DateTimeTz(adjusted.add(dateSpan, timeSpan), offset) + + operator fun plus(delta: MonthSpan) = add(delta, 0.milliseconds) + operator fun plus(delta: DateTimeSpan) = add(delta.monthSpan, delta.timeSpan) + operator fun plus(delta: TimeSpan) = add(0.months, delta) + + operator fun minus(delta: MonthSpan) = this + (-delta) + operator fun minus(delta: DateTimeSpan) = this + (-delta) + operator fun minus(delta: TimeSpan) = this + (-delta) + + operator fun minus(other: DateTimeTz) = (this.utc.unixMillisDouble - other.utc.unixMillisDouble).milliseconds + + override fun hashCode(): Int = this.local.hashCode() + offset.totalMinutesInt + override fun equals(other: Any?): Boolean = other is DateTimeTz && this.utc.unixMillisDouble == other.utc.unixMillisDouble + override fun compareTo(other: DateTimeTz): Int = this.utc.unixMillis.compareTo(other.utc.unixMillis) + + /** Converts this date to String using [format] for representing it */ + fun format(format: DateFormat): String = format.format(this) + /** Converts this date to String using [format] for representing it */ + fun format(format: String): String = DateFormat(format).format(this) + /** Converts this date to String using [format] for representing it */ + fun toString(format: DateFormat): String = format.format(this) + /** Converts this date to String using [format] for representing it */ + fun toString(format: String): String = DateFormat(format).format(this) + /** Converts this date to String using the [DateFormat.DEFAULT_FORMAT] for representing it */ + fun toStringDefault(): String = DateFormat.DEFAULT_FORMAT.format(this) + + override fun toString(): String = "DateTimeTz($adjusted, $offset)" +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt b/klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt new file mode 100644 index 00000000000..77b5dd85af1 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt @@ -0,0 +1,95 @@ +package korlibs.time + +import korlibs.time.DayOfWeek.Friday +import korlibs.time.DayOfWeek.Monday +import korlibs.time.DayOfWeek.Saturday +import korlibs.time.DayOfWeek.Sunday +import korlibs.time.DayOfWeek.Thursday +import korlibs.time.DayOfWeek.Tuesday +import korlibs.time.DayOfWeek.Wednesday +import korlibs.time.internal.* + +/** Represents the day of the week. [Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday]. */ +enum class DayOfWeek( + /** 0: [Sunday], 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday] */ + val index0: Int +) : Serializable { + Sunday(0), + Monday(1), + Tuesday(2), + Wednesday(3), + Thursday(4), + Friday(5), + Saturday(6); + + /** + * 1: [Sunday], 2: [Monday], 3: [Tuesday], 4: [Wednesday], 5: [Thursday], 6: [Friday], 7: [Saturday] + */ + val index1 get() = index0 + 1 + + val index0Sunday get() = index0 + val index1Sunday get() = index1 + + /** 0: [Monday], 1: [Tuesday], 2: [Wednesday], 3: [Thursday], 4: [Friday], 5: [Saturday], 6: [Sunday] */ + val index0Monday get() = (index0 - 1) umod 7 + + /** 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday], 7: [Sunday] */ + val index1Monday get() = index0Monday + 1 + + fun index0Locale(locale: KlockLocale): Int = (index0 - locale.firstDayOfWeek.index0) umod 7 + fun index1Locale(locale: KlockLocale): Int = index0Locale(locale) + 1 + + /** Returns if this day of the week is weekend for a specific [locale] */ + fun isWeekend(locale: KlockLocale = KlockLocale.default) = locale.isWeekend(this) + + val localName get() = localName(KlockLocale.default) + fun localName(locale: KlockLocale) = locale.daysOfWeek[index0] + + val localShortName get() = localShortName(KlockLocale.default) + fun localShortName(locale: KlockLocale) = locale.daysOfWeekShort[index0] + + val prev get() = DayOfWeek[index0 - 1] + val next get() = DayOfWeek[index0 + 1] + + fun prev(offset: Int = 1) = DayOfWeek[index0 - offset] + fun next(offset: Int = 1) = DayOfWeek[index0 + offset] + + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + /** + * Number of days in a wekk. + */ + const val Count = 7 + + private val BY_INDEX0 = values() + + /** + * 0: [Sunday], 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday] + */ + operator fun get(index0: Int) = BY_INDEX0[index0 umod 7] + + fun get0(index0: Int, locale: KlockLocale = KlockLocale.default): DayOfWeek = DayOfWeek[index0 + locale.firstDayOfWeek.index0] + fun get1(index1: Int, locale: KlockLocale = KlockLocale.default): DayOfWeek = get0((index1 - 1) umod 7, locale) + + /** + * Returns the first day of the week for a specific [locale]. + */ + fun firstDayOfWeek(locale: KlockLocale = KlockLocale.default) = locale.firstDayOfWeek + + fun comparator(locale: KlockLocale = KlockLocale.default) = locale.daysOfWeekComparator + } +} + +fun DayOfWeek.withLocale(locale: KlockLocale) = locale.localizedDayOfWeek(this) + +data class DayOfWeekWithLocale(val dayOfWeek: DayOfWeek, val locale: KlockLocale) : Comparable { + val index0: Int get() = dayOfWeek.index0Locale(locale) + val index1: Int get() = dayOfWeek.index1Locale(locale) + + override fun compareTo(other: DayOfWeekWithLocale): Int { + if (other.locale != this.locale) error("Can't compare two day of weeks with different locales") + return locale.daysOfWeekComparator.compare(dayOfWeek, other.dayOfWeek) + } +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/Frequency.kt b/klock/src/commonMain/kotlin/korlibs/time/Frequency.kt new file mode 100644 index 00000000000..395ef17b7eb --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Frequency.kt @@ -0,0 +1,42 @@ +package korlibs.time + +import korlibs.time.internal.* +import kotlin.jvm.* + +val TimeSpan.hz: Frequency get() = timesPerSecond +val Int.hz: Frequency get() = timesPerSecond +val Double.hz: Frequency get() = timesPerSecond + +fun TimeSpan.toFrequency(): Frequency = timesPerSecond + +val TimeSpan.timesPerSecond get() = Frequency(1.0 / this.seconds) +val Int.timesPerSecond get() = Frequency(this.toDouble()) +val Double.timesPerSecond get() = Frequency(this) + +@JvmInline +value class Frequency(val hertz: Double) : Comparable, Serializable { + companion object { + fun from(timeSpan: TimeSpan) = timeSpan.toFrequency() + } + + override fun compareTo(other: Frequency): Int = this.hertz.compareTo(other.hertz) + + operator fun unaryMinus() = Frequency(-this.hertz) + operator fun unaryPlus() = this + + operator fun plus(other: Frequency): Frequency = Frequency(this.hertz + other.hertz) + operator fun minus(other: Frequency): Frequency = Frequency(this.hertz - other.hertz) + + operator fun times(scale: Int): Frequency = Frequency(this.hertz * scale) + operator fun times(scale: Float): Frequency = Frequency(this.hertz * scale) + operator fun times(scale: Double): Frequency = Frequency(this.hertz * scale) + + operator fun div(scale: Int): Frequency = Frequency(this.hertz / scale) + operator fun div(scale: Float): Frequency = Frequency(this.hertz / scale) + operator fun div(scale: Double): Frequency = Frequency(this.hertz / scale) + + operator fun rem(other: Frequency): Frequency = Frequency(this.hertz % other.hertz) + infix fun umod(other: Frequency): Frequency = Frequency(this.hertz umod other.hertz) + + val timeSpan get() = (1.0 / this.hertz).seconds +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt b/klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt new file mode 100644 index 00000000000..319873067da --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt @@ -0,0 +1,446 @@ +package korlibs.time + +import korlibs.time.internal.MicroStrReader +import korlibs.time.internal.fastForEach +import korlibs.time.internal.padded +import korlibs.time.internal.readTimeZoneOffset +import kotlin.math.absoluteValue + +// https://en.wikipedia.org/wiki/ISO_8601 +object ISO8601 { + data class BaseIsoTimeFormat(val format: String) : TimeFormat { + companion object { + private val ref = DateTime(1900, 1, 1) + } + private val dateTimeFormat = BaseIsoDateTimeFormat(format) + + override fun format(dd: TimeSpan): String = dateTimeFormat.format(ref + dd) + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? = + dateTimeFormat.tryParse(str, doThrow, doAdjust)?.let { it.utc - ref } + } + + data class BaseIsoDateTimeFormat(val format: String, val twoDigitBaseYear: Int = 1900) : DateFormat { + override fun format(dd: DateTimeTz): String = buildString { + val d = dd.local + val s = d.copyDayOfMonth(hours = 0, minutes = 0, seconds = 0, milliseconds = 0) + val time = d - s + val fmtReader = MicroStrReader(format) + while (fmtReader.hasMore) { + when { + fmtReader.tryRead("Z") -> { + //if (dd.offset != TimezoneOffset.UTC) { + if (dd.offset != TimezoneOffset.UTC) { + dd.offset.deltaHoursAbs + append(if (dd.offset.positive) "+" else "-") + append(dd.offset.deltaHoursAbs.padded(2)) + append(":") + append(dd.offset.deltaMinutesAbs.padded(2)) + } else { + append("Z") + } + } + fmtReader.tryRead("YYYYYY") -> append(d.yearInt.absoluteValue.padded(6)) + fmtReader.tryRead("YYYY") -> append(d.yearInt.absoluteValue.padded(4)) + fmtReader.tryRead("YY") -> append((d.yearInt.absoluteValue % 100).padded(2)) + fmtReader.tryRead("MM") -> append(d.month1.padded(2)) + fmtReader.tryRead("DD") -> append(d.dayOfMonth.padded(2)) + fmtReader.tryRead("DDD") -> append(d.dayOfWeekInt.padded(3)) + fmtReader.tryRead("ww") -> append(d.weekOfYear1.padded(2)) + fmtReader.tryRead("D") -> append(d.dayOfWeek.index1Monday) + fmtReader.tryRead("hh") -> { + val nextComma = fmtReader.tryRead(',') + val result = if (nextComma || fmtReader.tryRead('.')) { + var decCount = 0 + while (fmtReader.tryRead('h')) decCount++ + time.hours.padded(2, decCount) + } else { + d.hours.padded(2) + } + append(if (nextComma) result.replace('.', ',') else result) + } + fmtReader.tryRead("mm") -> { + val nextComma = fmtReader.tryRead(',') + val result = if (nextComma || fmtReader.tryRead('.')) { + var decCount = 0 + while (fmtReader.tryRead('m')) decCount++ + (time.minutes % 60.0).padded(2, decCount) + } else { + d.minutes.padded(2) + } + append(if (nextComma) result.replace('.', ',') else result) + } + fmtReader.tryRead("ss") -> { + val nextComma = fmtReader.tryRead(',') + val result = if (nextComma || fmtReader.tryRead('.')) { + var decCount = 0 + while (fmtReader.tryRead('s')) decCount++ + (time.seconds % 60.0).padded(2, decCount) + } else { + d.seconds.padded(2) + } + append(if (nextComma) result.replace('.', ',') else result) + } + fmtReader.tryRead("±") -> append(if (d.yearInt < 0) "-" else "+") + else -> append(fmtReader.readChar()) + } + } + } + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? { + return _tryParse(str, doAdjust).also { + if (doThrow && it == null) throw DateException("Can't parse $str with $format") + } + } + + private fun reportParse(reason: String): DateTimeTz? { + //println("reason: $reason") + return null + } + + private fun _tryParse(str: String, doAdjust: Boolean): DateTimeTz? { + var sign = +1 + var tzOffset: TimeSpan? = null + var year = twoDigitBaseYear + var month = 1 + var dayOfMonth = 1 + + var dayOfWeek = -1 + var dayOfYear = -1 + var weekOfYear = -1 + + var hours = 0.0 + var minutes = 0.0 + var seconds = 0.0 + + val reader = MicroStrReader(str) + val fmtReader = MicroStrReader(format) + + while (fmtReader.hasMore) { + when { + fmtReader.tryRead("Z") -> tzOffset = reader.readTimeZoneOffset() + fmtReader.tryRead("YYYYYY") -> year = reader.tryReadInt(6) ?: return reportParse("YYYYYY") + fmtReader.tryRead("YYYY") -> year = reader.tryReadInt(4) ?: return reportParse("YYYY") + //fmtReader.tryRead("YY") -> year = twoDigitBaseYear + (reader.tryReadInt(2) ?: return null) // @TODO: Kotlin compiler BUG? + fmtReader.tryRead("YY") -> { + val base = reader.tryReadInt(2) ?: return reportParse("YY") + year = twoDigitBaseYear + base + } + fmtReader.tryRead("MM") -> month = reader.tryReadInt(2) ?: return reportParse("MM") + fmtReader.tryRead("DD") -> dayOfMonth = reader.tryReadInt(2) ?: return reportParse("DD") + fmtReader.tryRead("DDD") -> dayOfYear = reader.tryReadInt(3) ?: return reportParse("DDD") + fmtReader.tryRead("ww") -> weekOfYear = reader.tryReadInt(2) ?: return reportParse("ww") + fmtReader.tryRead("D") -> dayOfWeek = reader.tryReadInt(1) ?: return reportParse("D") + + fmtReader.tryRead("hh") -> { + val nextComma = fmtReader.tryRead(',') + hours = if (nextComma || fmtReader.tryRead('.')) { + var count = 3 + while (fmtReader.tryRead('h')) count++ + reader.tryReadDouble(count) ?: return reportParse("incorrect hours") + } else { + reader.tryReadDouble(2) ?: return reportParse("incorrect hours") + } + } + fmtReader.tryRead("mm") -> { + val nextComma = fmtReader.tryRead(',') + minutes = if (nextComma || fmtReader.tryRead('.')) { + var count = 3 + while (fmtReader.tryRead('m')) count++ + reader.tryReadDouble(count) ?: return reportParse("incorrect minutes") + } else { + reader.tryReadDouble(2) ?: return reportParse("incorrect seconds") + } + } + fmtReader.tryRead("ss") -> { + val nextComma = fmtReader.tryRead(',') + seconds = if (nextComma || fmtReader.tryRead('.')) { + var count = 3 + while (fmtReader.tryRead('s')) count++ + reader.tryReadDouble(count) ?: return reportParse("incorrect seconds") + } else { + reader.tryReadDouble(2) ?: return reportParse("incorrect seconds") + } + } + fmtReader.tryRead("±") -> { + sign = when (reader.readChar()) { + '+' -> +1 + '-' -> -1 + else -> return reportParse("±") + } + } + else -> if (fmtReader.readChar() != reader.readChar()) return reportParse("separator") + } + } + if (reader.hasMore) return reportParse("uncomplete") + + val dateTime = when { + dayOfYear >= 0 -> DateTime(year, 1, 1) + (dayOfYear - 1).days + weekOfYear >= 0 -> { + val reference = Year(year).first(DayOfWeek.Thursday) - 3.days + val days = ((weekOfYear - 1) * 7 + (dayOfWeek - 1)) + reference + days.days + } + else -> DateTime(year, month, dayOfMonth) + } + + val baseDateTime = dateTime + hours.hours + minutes.minutes + seconds.seconds + return if (tzOffset != null) DateTimeTz.local(baseDateTime, TimezoneOffset(tzOffset)) else baseDateTime.local + } + + fun withTwoDigitBaseYear(twoDigitBaseYear: Int = 1900) = BaseIsoDateTimeFormat(format, twoDigitBaseYear) + } + + class IsoIntervalFormat(val format: String) : DateTimeSpanFormat { + override fun format(dd: DateTimeSpan): String = buildString { + val fmtReader = MicroStrReader(format) + var time = false + while (fmtReader.hasMore) { + when { + fmtReader.tryRead("T") -> append('T').also { time = true } + fmtReader.tryRead("nnY") -> append(dd.years).append('Y') + fmtReader.tryRead("nnM") -> append(if (time) dd.minutes else dd.months).append('M') + fmtReader.tryRead("nnD") -> append(dd.daysIncludingWeeks).append('D') + fmtReader.tryRead("nnH") -> append(dd.hours).append('H') + fmtReader.tryRead("nnS") -> append(dd.seconds).append('S') + else -> append(fmtReader.readChar()) + } + } + } + + override fun tryParse(str: String, doThrow: Boolean): DateTimeSpan? { + var time = false + var years = 0.0 + var months = 0.0 + var days = 0.0 + var hours = 0.0 + var minutes = 0.0 + var seconds = 0.0 + + val reader = MicroStrReader(str) + val fmtReader = MicroStrReader(format) + + while (fmtReader.hasMore) { + when { + fmtReader.tryRead("nn,nnY") || fmtReader.tryRead("nnY") -> { + years = reader.tryReadDouble() ?: return null + if (!reader.tryRead("Y")) return null + } + fmtReader.tryRead("nn,nnM") || fmtReader.tryRead("nnM") -> { + if (time) { + minutes = reader.tryReadDouble() ?: return null + } else { + months = reader.tryReadDouble() ?: return null + } + if (!reader.tryRead("M")) return null + } + fmtReader.tryRead("nn,nnD") || fmtReader.tryRead("nnD") -> { + days = reader.tryReadDouble() ?: return null + if (!reader.tryRead("D")) return null + } + fmtReader.tryRead("nn,nnH") || fmtReader.tryRead("nnH") -> { + hours = reader.tryReadDouble() ?: return null + if (!reader.tryRead("H")) return null + } + fmtReader.tryRead("nn,nnS") || fmtReader.tryRead("nnS") -> { + seconds = reader.tryReadDouble() ?: return null + if (!reader.tryRead("S")) return null + } + else -> { + val char = fmtReader.readChar() + if (char != reader.readChar()) return null + if (char == 'T') time = true + } + } + } + return ((years * 12) + months).toInt().months + (days.days + hours.hours + minutes.minutes + seconds.seconds) + } + } + + + data class IsoTimeFormat(val basicFormat: String?, val extendedFormat: String?) : TimeFormat { + val basic = BaseIsoTimeFormat(basicFormat ?: extendedFormat ?: TODO()) + val extended = BaseIsoTimeFormat(extendedFormat ?: basicFormat ?: TODO()) + + override fun format(dd: TimeSpan): String = extended.format(dd) + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? = + basic.tryParse(str, false, doAdjust) ?: extended.tryParse(str, false, doAdjust) + ?: (if (doThrow) throw DateException("Invalid format $str") else null) + } + + data class IsoDateTimeFormat(val basicFormat: String?, val extendedFormat: String?) : DateFormat { + val basic = BaseIsoDateTimeFormat(basicFormat ?: extendedFormat ?: TODO()) + val extended = BaseIsoDateTimeFormat(extendedFormat ?: basicFormat ?: TODO()) + + override fun format(dd: DateTimeTz): String = extended.format(dd) + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? = null + ?: basic.tryParse(str, false, doAdjust) + ?: extended.tryParse(str, false, doAdjust) + ?: (if (doThrow) throw DateException("Invalid format $str") else null) + } + + // Date Calendar Variants + val DATE_CALENDAR_COMPLETE = IsoDateTimeFormat("YYYYMMDD", "YYYY-MM-DD") + val DATE_CALENDAR_REDUCED0 = IsoDateTimeFormat(null, "YYYY-MM") + val DATE_CALENDAR_REDUCED1 = IsoDateTimeFormat("YYYY", null) + val DATE_CALENDAR_REDUCED2 = IsoDateTimeFormat("YY", null) + val DATE_CALENDAR_EXPANDED0 = IsoDateTimeFormat("±YYYYYYMMDD", "±YYYYYY-MM-DD") + val DATE_CALENDAR_EXPANDED1 = IsoDateTimeFormat("±YYYYYYMM", "±YYYYYY-MM") + val DATE_CALENDAR_EXPANDED2 = IsoDateTimeFormat("±YYYYYY", null) + val DATE_CALENDAR_EXPANDED3 = IsoDateTimeFormat("±YYY", null) + + // Date Ordinal Variants + val DATE_ORDINAL_COMPLETE = IsoDateTimeFormat("YYYYDDD", "YYYY-DDD") + val DATE_ORDINAL_EXPANDED = IsoDateTimeFormat("±YYYYYYDDD", "±YYYYYY-DDD") + + // Date Week Variants + val DATE_WEEK_COMPLETE = IsoDateTimeFormat("YYYYWwwD", "YYYY-Www-D") + val DATE_WEEK_REDUCED = IsoDateTimeFormat("YYYYWww", "YYYY-Www") + val DATE_WEEK_EXPANDED0 = IsoDateTimeFormat("±YYYYYYWwwD", "±YYYYYY-Www-D") + val DATE_WEEK_EXPANDED1 = IsoDateTimeFormat("±YYYYYYWww", "±YYYYYY-Www") + + val DATE_ALL = listOf( + DATE_CALENDAR_COMPLETE, DATE_CALENDAR_REDUCED0, DATE_CALENDAR_REDUCED1, DATE_CALENDAR_REDUCED2, + DATE_CALENDAR_EXPANDED0, DATE_CALENDAR_EXPANDED1, DATE_CALENDAR_EXPANDED2, DATE_CALENDAR_EXPANDED3, + DATE_ORDINAL_COMPLETE, DATE_ORDINAL_EXPANDED, + DATE_WEEK_COMPLETE, DATE_WEEK_REDUCED, DATE_WEEK_EXPANDED0, DATE_WEEK_EXPANDED1 + ) + + // Time Variants + val TIME_LOCAL_COMPLETE = IsoTimeFormat("hhmmss", "hh:mm:ss") + val TIME_LOCAL_REDUCED0 = IsoTimeFormat("hhmm", "hh:mm") + val TIME_LOCAL_REDUCED1 = IsoTimeFormat("hh", null) + val TIME_LOCAL_FRACTION0 = IsoTimeFormat("hhmmss,ss", "hh:mm:ss,ss") + val TIME_LOCAL_FRACTION1 = IsoTimeFormat("hhmm,mm", "hh:mm,mm") + val TIME_LOCAL_FRACTION2 = IsoTimeFormat("hh,hh", null) + + // Time UTC Variants + val TIME_UTC_COMPLETE = IsoTimeFormat("hhmmssZ", "hh:mm:ssZ") + val TIME_UTC_REDUCED0 = IsoTimeFormat("hhmmZ", "hh:mmZ") + val TIME_UTC_REDUCED1 = IsoTimeFormat("hhZ", null) + val TIME_UTC_FRACTION0 = IsoTimeFormat("hhmmss,ssZ", "hh:mm:ss,ssZ") + val TIME_UTC_FRACTION1 = IsoTimeFormat("hhmm,mmZ", "hh:mm,mmZ") + val TIME_UTC_FRACTION2 = IsoTimeFormat("hh,hhZ", null) + + // Time Relative Variants + val TIME_RELATIVE0 = IsoTimeFormat("±hhmm", "±hh:mm") + val TIME_RELATIVE1 = IsoTimeFormat("±hh", null) + + val TIME_ALL = listOf( + TIME_LOCAL_COMPLETE, + TIME_LOCAL_REDUCED0, + TIME_LOCAL_REDUCED1, + TIME_LOCAL_FRACTION0, + TIME_LOCAL_FRACTION1, + TIME_LOCAL_FRACTION2, + TIME_UTC_COMPLETE, + TIME_UTC_REDUCED0, + TIME_UTC_REDUCED1, + TIME_UTC_FRACTION0, + TIME_UTC_FRACTION1, + TIME_UTC_FRACTION2, + TIME_RELATIVE0, + TIME_RELATIVE1 + ) + + // Date + Time Variants + val DATETIME_COMPLETE = IsoDateTimeFormat("YYYYMMDDThhmmss", "YYYY-MM-DDThh:mm:ss") + val DATETIME_UTC_COMPLETE = IsoDateTimeFormat("YYYYMMDDThhmmssZ", "YYYY-MM-DDThh:mm:ssZ") + val DATETIME_UTC_COMPLETE_FRACTION = IsoDateTimeFormat("YYYYMMDDThhmmss.sssZ", "YYYY-MM-DDThh:mm:ss.sssZ") + + // Interval Variants + val INTERVAL_COMPLETE0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnMnnS") + val INTERVAL_COMPLETE1 = IsoIntervalFormat("PnnYnnW") + + val INTERVAL_REDUCED0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnM") + val INTERVAL_REDUCED1 = IsoIntervalFormat("PnnYnnMnnDTnnH") + val INTERVAL_REDUCED2 = IsoIntervalFormat("PnnYnnMnnD") + val INTERVAL_REDUCED3 = IsoIntervalFormat("PnnYnnM") + val INTERVAL_REDUCED4 = IsoIntervalFormat("PnnY") + + val INTERVAL_DECIMAL0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnMnn,nnS") + val INTERVAL_DECIMAL1 = IsoIntervalFormat("PnnYnnMnnDTnnHnn,nnM") + val INTERVAL_DECIMAL2 = IsoIntervalFormat("PnnYnnMnnDTnn,nnH") + val INTERVAL_DECIMAL3 = IsoIntervalFormat("PnnYnnMnn,nnD") + val INTERVAL_DECIMAL4 = IsoIntervalFormat("PnnYnn,nnM") + val INTERVAL_DECIMAL5 = IsoIntervalFormat("PnnYnn,nnW") + val INTERVAL_DECIMAL6 = IsoIntervalFormat("PnnY") + + val INTERVAL_ZERO_OMIT0 = IsoIntervalFormat("PnnYnnDTnnHnnMnnS") + val INTERVAL_ZERO_OMIT1 = IsoIntervalFormat("PnnYnnDTnnHnnM") + val INTERVAL_ZERO_OMIT2 = IsoIntervalFormat("PnnYnnDTnnH") + val INTERVAL_ZERO_OMIT3 = IsoIntervalFormat("PnnYnnD") + + val INTERVAL_ALL = listOf( + INTERVAL_COMPLETE0, INTERVAL_COMPLETE1, + INTERVAL_REDUCED0, INTERVAL_REDUCED1, INTERVAL_REDUCED2, INTERVAL_REDUCED3, INTERVAL_REDUCED4, + INTERVAL_DECIMAL0, INTERVAL_DECIMAL1, INTERVAL_DECIMAL2, INTERVAL_DECIMAL3, INTERVAL_DECIMAL4, + INTERVAL_DECIMAL5, INTERVAL_DECIMAL6, + INTERVAL_ZERO_OMIT0, INTERVAL_ZERO_OMIT1, INTERVAL_ZERO_OMIT2, INTERVAL_ZERO_OMIT3 + ) + + // Detects and parses all the variants + val DATE = object : DateFormat { + override fun format(dd: DateTimeTz): String = DATE_CALENDAR_COMPLETE.format(dd) + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? { + DATE_ALL.fastForEach { format -> + val result = format.extended.tryParse(str, false, doAdjust) + if (result != null) return result + } + DATE_ALL.fastForEach { format -> + val result = format.basic.tryParse(str, false, doAdjust) + if (result != null) return result + } + return if (doThrow) throw DateException("Invalid format") else null + } + } + val TIME = object : TimeFormat { + override fun format(dd: TimeSpan): String = TIME_LOCAL_FRACTION0.format(dd) + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? { + TIME_ALL.fastForEach { format -> + val result = format.extended.tryParse(str, false, doAdjust) + if (result != null) return result + } + TIME_ALL.fastForEach { format -> + val result = format.basic.tryParse(str, false, doAdjust) + if (result != null) return result + } + return if (doThrow) throw DateException("Invalid format") else null + } + } + val INTERVAL = object : DateTimeSpanFormat { + override fun format(dd: DateTimeSpan): String = INTERVAL_DECIMAL0.format(dd) + + override fun tryParse(str: String, doThrow: Boolean): DateTimeSpan? { + INTERVAL_ALL.fastForEach { format -> + val result = format.tryParse(str, false) + if (result != null) return result + } + return if (doThrow) throw DateException("Invalid format") else null + } + } +} + +// ISO 8601 (first week is the one after 1 containing a thursday) +fun Year.first(dayOfWeek: DayOfWeek): DateTime { + val start = DateTime(this.year, 1, 1) + var n = 0 + while (true) { + val time = (start + n.days) + if (time.dayOfWeek == dayOfWeek) return time + n++ + } +} + +val DateTime.weekOfYear0: Int + get() { + val firstThursday = year.first(DayOfWeek.Thursday) + val offset = firstThursday.dayOfMonth - 3 + return (dayOfYear - offset) / 7 + } + +val DateTime.weekOfYear1: Int get() = weekOfYear0 + 1 +val DateTimeTz.weekOfYear0: Int get() = local.weekOfYear0 +val DateTimeTz.weekOfYear1: Int get() = local.weekOfYear1 diff --git a/klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt b/klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt new file mode 100644 index 00000000000..c20846a574e --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt @@ -0,0 +1,122 @@ +package korlibs.time + +import korlibs.time.internal.substr +import kotlin.native.concurrent.ThreadLocal + +private var KlockLocale_default: KlockLocale? = null + +abstract class KlockLocale { + abstract val ISO639_1: String + abstract val daysOfWeek: List + abstract val months: List + abstract val firstDayOfWeek: DayOfWeek + open val monthsShort: List get() = months.map { it.substr(0, 3) } + open val daysOfWeekShort: List get() = daysOfWeek.map { it.substr(0, 3) } + + //private val daysOfWeekWithLocaleList: Array = Array(7) { DayOfWeekWithLocale(DayOfWeek[it], this) } + + //fun localizedDayOfWeek(dayOfWeek: DayOfWeek) = daysOfWeekWithLocaleList[dayOfWeek.index0] + fun localizedDayOfWeek(dayOfWeek: DayOfWeek) = DayOfWeekWithLocale(DayOfWeek[dayOfWeek.index0], this) + + val daysOfWeekComparator get() = Comparator { a, b -> + a.index0Locale(this).compareTo(b.index0Locale(this)) + } + + open val ordinals get() = Array(32) { + if (it in 11..13) { + "${it}th" + } else { + when (it % 10) { + 1 -> "${it}st" + 2 -> "${it}nd" + 3 -> "${it}rd" + else -> "${it}th" + } + } + } + + open fun getOrdinalByDay(day: Int, context: KlockLocaleContext = KlockLocaleContext.Default): String = ordinals[day] + + open fun getDayByOrdinal(ordinal: String): Int = ordinals.indexOf(ordinal) + + //open val monthsShort: List by klockAtomicLazy { months.map { it.substr(0, 3) } } + //open val daysOfWeekShort: List by klockAtomicLazy { daysOfWeek.map { it.substr(0, 3) } } + /* + private val _lock = KlockLock() + private val _monthsShort = KlockAtomicRef?>(null) + private val _daysOfWeekShort = KlockAtomicRef?>(null) + //open val monthsShort by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { months.map { it.substr(0, 3) } } + open val monthsShort: List get() = _lock { + if (_monthsShort.value == null) { + _monthsShort.value = months.map { it.substr(0, 3) } + } + _monthsShort.value!! + } + open val daysOfWeekShort: List get() = _lock { + if (_daysOfWeekShort.value == null) { + _daysOfWeekShort.value = daysOfWeek.map { it.substr(0, 3) } + } + _daysOfWeekShort.value!! + } + */ + + open val h12Marker: List get() = listOf("am", "pm") + + // This might be required for some languages like chinese? + open fun intToString(value: Int) = "$value" + + open fun isWeekend(dayOfWeek: DayOfWeek): Boolean = dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday + + protected fun format(str: String) = PatternDateFormat(str, this) + + open val formatDateTimeMedium get() = format("MMM d, y h:mm:ss a") + open val formatDateTimeShort get() = format("M/d/yy h:mm a") + + open val formatDateFull get() = format("EEEE, MMMM d, y") + open val formatDateLong get() = format("MMMM d, y") + open val formatDateMedium get() = format("MMM d, y") + open val formatDateShort get() = format("M/d/yy") + + open val formatTimeMedium get() = format("HH:mm:ss") + open val formatTimeShort get() = format("HH:mm") + + companion object { + val english get() = English + + var default: KlockLocale + set(value) { KlockLocale_default = value } + get() = KlockLocale_default ?: English + + inline fun setTemporarily(locale: KlockLocale, callback: () -> R): R { + val old = default + default = locale + try { + return callback() + } finally { + default = old + } + } + } + + open class English : KlockLocale() { + companion object : English() + + override val ISO639_1 get() = "en" + + override val firstDayOfWeek: DayOfWeek get() = DayOfWeek.Sunday + + override val daysOfWeek: List get() = listOf( + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ) + override val months: List get() = listOf( + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ) + + override val formatTimeMedium get() = format("h:mm:ss a") + override val formatTimeShort get() = format("h:mm a") + } +} + +fun DateTime.format(format: String, locale: KlockLocale): String = DateFormat(format).withLocale(locale).format(this) +fun DateTimeTz.format(format: String, locale: KlockLocale): String = DateFormat(format).withLocale(locale).format(this) diff --git a/klock/src/commonMain/kotlin/korlibs/time/KlockLocaleContext.kt b/klock/src/commonMain/kotlin/korlibs/time/KlockLocaleContext.kt new file mode 100644 index 00000000000..5f9cd9debf3 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/KlockLocaleContext.kt @@ -0,0 +1,14 @@ +package korlibs.time + +data class KlockLocaleContext(val gender: KlockLocaleGender = KlockLocaleGender.Neuter) { + + companion object { + + val Default = KlockLocaleContext() + } +} + +enum class KlockLocaleGender { + Neuter, + Masculine, +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/Measure.kt b/klock/src/commonMain/kotlin/korlibs/time/Measure.kt new file mode 100644 index 00000000000..0461a58f023 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Measure.kt @@ -0,0 +1,37 @@ +package korlibs.time + +/** + * Executes a [callback] and measure the time it takes to complete. + */ +inline fun measureTime(callback: () -> Unit): TimeSpan { + val start = PerformanceCounter.microseconds + callback() + val end = PerformanceCounter.microseconds + return (end - start).microseconds +} + +inline fun measureTime(callback: () -> T, handleTime: (TimeSpan) -> Unit): T { + val start = PerformanceCounter.microseconds + val result = callback() + val end = PerformanceCounter.microseconds + val elapsed = (end - start).microseconds + handleTime(elapsed) + return result +} + +/** + * Executes the [callback] measuring the time it takes to complete. + * Returns a [TimedResult] with the time and the return value of the callback. + */ +inline fun measureTimeWithResult(callback: () -> T): TimedResult { + val start = PerformanceCounter.microseconds + val result = callback() + val end = PerformanceCounter.microseconds + val elapsed = (end - start).microseconds + return TimedResult(result, elapsed) +} + +/** + * Represents a [result] associated to a [time]. + */ +data class TimedResult(val result: T, val time: TimeSpan) diff --git a/klock/src/commonMain/kotlin/korlibs/time/Month.kt b/klock/src/commonMain/kotlin/korlibs/time/Month.kt new file mode 100644 index 00000000000..ce84b33d8b5 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Month.kt @@ -0,0 +1,131 @@ +package korlibs.time + +import korlibs.time.Month.* +import korlibs.time.internal.* +import kotlin.math.* + +/** Represents one of the twelve months of the year. */ +enum class Month( + /** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */ + val index1: Int, + /** Number of days of this month in a common year */ + val daysCommon: Int, + /** Number of days of this month in a leap year */ + val daysLeap: Int = daysCommon +) : Serializable { + January(1, daysCommon = 31), + February(2, daysCommon = 28, daysLeap = 29), + March(3, daysCommon = 31), + April(4, daysCommon = 30), + May(5, daysCommon = 31), + June(6, daysCommon = 30), + July(7, daysCommon = 31), + August(8, daysCommon = 31), + September(9, daysCommon = 30), + October(10, daysCommon = 31), + November(11, daysCommon = 30), + December(12, daysCommon = 31); + + /** 0: [January], 1: [February], 2: [March], 3: [April], 4: [May], 5: [June], 6: [July], 7: [August], 8: [September], 9: [October], 10: [November], 11: [December] */ + val index0: Int get() = index1 - 1 + + /** Number of days in a specific month (28-31) depending whether the year is [leap] or not. */ + fun days(leap: Boolean): Int = if (leap) daysLeap else daysCommon + /** Number of days in a specific month (28-31) depending whether the [year] or not. */ + fun days(year: Int): Int = days(Year(year).isLeap) + /** Number of days in a specific month (28-31) depending whether the [year] or not. */ + fun days(year: Year): Int = days(year.isLeap) + + /** Number of days since the start of the [leap] year to reach this month. */ + fun daysToStart(leap: Boolean): Int = YEAR_DAYS(leap)[index0] + /** Number of days since the start of the [year] to reach this month. */ + fun daysToStart(year: Int): Int = daysToStart(Year(year).isLeap) + /** Number of days since the start of the [year] to reach this month. */ + fun daysToStart(year: Year): Int = daysToStart(year.isLeap) + + /** Number of days since the start of the [leap] year to reach next month. */ + fun daysToEnd(leap: Boolean): Int = YEAR_DAYS(leap)[index1] + /** Number of days since the start of the [year] to reach next month. */ + fun daysToEnd(year: Int): Int = daysToEnd(Year(year).isLeap) + /** Number of days since the start of the [year] to reach next month. */ + fun daysToEnd(year: Year): Int = daysToEnd(year.isLeap) + + /** Previous [Month]. */ + val previous: Month get() = this - 1 + /** Next [Month]. */ + val next: Month get() = this + 1 + + operator fun plus(delta: Int): Month = Month[index1 + delta] + operator fun minus(delta: Int): Month = Month[index1 - delta] + + operator fun minus(other: Month): Int = abs(this.index0 - other.index0) + + val localName get() = localName(KlockLocale.default) + fun localName(locale: KlockLocale) = locale.months[index0] + + val localShortName get() = localShortName(KlockLocale.default) + fun localShortName(locale: KlockLocale) = locale.monthsShort[index0] + + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + /** + * Number of months in a year (12). + */ + const val Count = 12 + + /** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */ + operator fun invoke(index1: Int) = adjusted(index1) + /** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */ + operator fun get(index1: Int) = adjusted(index1) + + /** + * Gets the [Month] from a month index where [January]=1 wrapping the index to valid values. + * + * For example 0 and 12=[December], 1 and 13=[January], -1 and 11=[November]. + */ + fun adjusted(index1: Int) = BY_INDEX0[(index1 - 1) umod 12] + + /** + * Gets the [Month] from a month index where [January]=1 checking that the provided [index1] is valid between 1..12. + */ + fun checked(index1: Int) = BY_INDEX0[index1.also { if (index1 !in 1..12) throw DateException("Month $index1 not in 1..12") } - 1] + + /** + * Gets the [Month] of a [dayOfYear] in a [leap] year. + * + * Returns null if the year doesn't contain that [dayOfYear]. + */ + fun fromDayOfYear(dayOfYear: Int, leap: Boolean): Month? { + val days = YEAR_DAYS(leap) + val day0 = dayOfYear - 1 + val guess = day0 / 32 + + if (guess in 0..11 && day0 in days[guess] until days[guess + 1]) return Month[guess + 1] + if (guess in 0..10 && day0 in days[guess + 1] until days[guess + 2]) return Month[guess + 2] + + return null + } + + /** + * Gets the [Month] of a [dayOfYear] in the specified [year]. + * + * Returns null if the year doesn't contain that [dayOfYear]. + */ + fun fromDayOfYear(dayOfYear: Int, year: Year): Month? = fromDayOfYear(dayOfYear, year.isLeap) + + private val BY_INDEX0 = values() + private fun YEAR_DAYS(isLeap: Boolean): IntArray = if (isLeap) YEAR_DAYS_LEAP else YEAR_DAYS_COMMON + private val YEAR_DAYS_LEAP = generateDaysToStart(leap = true) + private val YEAR_DAYS_COMMON = generateDaysToStart(leap = false) + + private fun generateDaysToStart(leap: Boolean): IntArray { + var total = 0 + return IntArray(13) { + total += if (it == 0) 0 else BY_INDEX0[it - 1].days(leap) + total + } + } + } +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt b/klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt new file mode 100644 index 00000000000..835de3777b0 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt @@ -0,0 +1,66 @@ +package korlibs.time + +import korlibs.time.internal.Serializable +import kotlin.jvm.JvmInline + +/** + * Creates a [MonthSpan] representing these years. + */ +inline val Int.years get() = MonthSpan(12 * this) + +/** + * Creates a [MonthSpan] representing these months. + */ +inline val Int.months get() = MonthSpan(this) + +/** + * Represents a number of years and months temporal distance. + */ +@JvmInline +value class MonthSpan( + /** Total months of this [MonthSpan] as integer */ + val totalMonths: Int +) : Comparable, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + } + + operator fun unaryMinus() = MonthSpan(-totalMonths) + operator fun unaryPlus() = MonthSpan(+totalMonths) + + operator fun plus(other: TimeSpan) = DateTimeSpan(this, other) + operator fun plus(other: MonthSpan) = MonthSpan(totalMonths + other.totalMonths) + operator fun plus(other: DateTimeSpan) = DateTimeSpan(other.monthSpan + this, other.timeSpan) + + operator fun minus(other: TimeSpan) = this + -other + operator fun minus(other: MonthSpan) = this + -other + operator fun minus(other: DateTimeSpan) = this + -other + + operator fun times(times: Double) = MonthSpan((totalMonths * times).toInt()) + operator fun times(times: Int) = this * times.toDouble() + operator fun times(times: Float) = this * times.toDouble() + + operator fun div(times: Double) = MonthSpan((totalMonths / times).toInt()) + operator fun div(times: Int) = this / times.toDouble() + operator fun div(times: Float) = this / times.toDouble() + + override fun compareTo(other: MonthSpan): Int = this.totalMonths.compareTo(other.totalMonths) + + /** Converts this time to String formatting it like "20Y", "20Y 1M", "1M" or "0M". */ + override fun toString(): String { + val list = arrayListOf() + if (years != 0) list.add("${years}Y") + if (months != 0 || years == 0) list.add("${months}M") + return list.joinToString(" ") + } +} + +/** Total years of this [MonthSpan] as double (might contain decimals) */ +val MonthSpan.totalYears: Double get() = totalMonths.toDouble() / 12.0 + +/** Years part of this [MonthSpan] as integer */ +val MonthSpan.years: Int get() = totalMonths / 12 + +/** Months part of this [MonthSpan] as integer */ +val MonthSpan.months: Int get() = totalMonths % 12 diff --git a/klock/src/commonMain/kotlin/korlibs/time/NumberOfTimes.kt b/klock/src/commonMain/kotlin/korlibs/time/NumberOfTimes.kt new file mode 100644 index 00000000000..b94e6d9cebb --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/NumberOfTimes.kt @@ -0,0 +1,28 @@ +package korlibs.time + +import kotlin.jvm.JvmInline + +val infiniteTimes get() = NumberOfTimes.INFINITE +inline val Int.times get() = NumberOfTimes(this) + +@JvmInline +value class NumberOfTimes(val count: Int) { + companion object { + val ZERO = NumberOfTimes(0) + val ONE = NumberOfTimes(1) + val INFINITE = NumberOfTimes(Int.MIN_VALUE) + } + val isInfinite get() = this == INFINITE + val isFinite get() = !isInfinite + val hasMore get() = this != ZERO + val oneLess get() = if (this == INFINITE) INFINITE else NumberOfTimes(count - 1) + operator fun plus(other: NumberOfTimes) = if (this == INFINITE || other == INFINITE) INFINITE else NumberOfTimes(this.count + other.count) + operator fun minus(other: NumberOfTimes) = when { + this == other -> ZERO + this == INFINITE || other == INFINITE -> INFINITE + else -> NumberOfTimes(this.count - other.count) + } + operator fun times(other: Int) = if (this == INFINITE) INFINITE else NumberOfTimes(this.count * other) + operator fun div(other: Int) = if (this == INFINITE) INFINITE else NumberOfTimes(this.count / other) + override fun toString(): String = if (this == INFINITE) "$count times" else "Infinite times" +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/PatternDateFormat.kt b/klock/src/commonMain/kotlin/korlibs/time/PatternDateFormat.kt new file mode 100644 index 00000000000..73ffb8e585d --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/PatternDateFormat.kt @@ -0,0 +1,318 @@ +package korlibs.time + +import korlibs.time.internal.* +import korlibs.time.internal.MicroStrReader +import korlibs.time.internal.increment +import korlibs.time.internal.padded +import korlibs.time.internal.readTimeZoneOffset +import korlibs.time.internal.substr +import kotlin.jvm.JvmOverloads +import kotlin.math.absoluteValue +import kotlin.math.log10 +import kotlin.math.pow + +data class PatternDateFormat @JvmOverloads constructor( + val format: String, + val locale: KlockLocale? = null, + val tzNames: TimezoneNames = TimezoneNames.DEFAULT, + val options: Options = Options.DEFAULT +) : DateFormat, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + } + + val realLocale get() = locale ?: KlockLocale.default + + data class Options(val optionalSupport: Boolean = false) : Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + val DEFAULT = Options(optionalSupport = false) + val WITH_OPTIONAL = Options(optionalSupport = true) + } + } + + fun withLocale(locale: KlockLocale?) = this.copy(locale = locale) + fun withTimezoneNames(tzNames: TimezoneNames) = this.copy(tzNames = this.tzNames + tzNames) + fun withOptions(options: Options) = this.copy(options = options) + fun withOptional() = this.copy(options = options.copy(optionalSupport = true)) + fun withNonOptional() = this.copy(options = options.copy(optionalSupport = false)) + + private val openOffsets = LinkedHashMap() + private val closeOffsets = LinkedHashMap() + + internal val chunks = arrayListOf().also { chunks -> + val s = MicroStrReader(format) + while (s.hasMore) { + if (s.peekChar() == '\'') { + val escapedChunk = s.readChunk { + s.tryRead('\'') + while (s.hasMore && s.readChar() != '\'') Unit + } + chunks.add(escapedChunk) + continue + } + if (options.optionalSupport) { + val offset = chunks.size + if (s.tryRead('[')) { + openOffsets.increment(offset) + continue + } + if (s.tryRead(']')) { + closeOffsets.increment(offset - 1) + continue + } + } + chunks.add(s.tryReadOrNull("do") ?: s.readRepeatedChar()) + } + }.toList() + + internal val regexChunks: List = chunks.map { + when (it) { + "E", "EE", "EEE", "EEEE", "EEEEE", "EEEEEE" -> """(\w+)""" + "z", "zzz" -> """([\w\s\-\+:]+)""" + "do" -> """(\d{1,2}\w+)""" + "d" -> """(\d{1,2})""" + "dd" -> """(\d{2})""" + "M" -> """(\d{1,5})""" + "MM" -> """(\d{2})""" + "MMM", "MMMM", "MMMMM" -> """(\w+)""" + "y" -> """(\d{1,5})""" + "yy" -> """(\d{2})""" + "yyy" -> """(\d{3})""" + "yyyy" -> """(\d{4})""" + "YYYY" -> """(\d{4})""" + "H", "k" -> """(\d{1,2})""" + "HH", "kk" -> """(\d{2})""" + "h", "K" -> """(\d{1,2})""" + "hh", "KK" -> """(\d{2})""" + "m" -> """(\d{1,2})""" + "mm" -> """(\d{2})""" + "s" -> """(\d{1,2})""" + "ss" -> """(\d{2})""" + "S" -> """(\d{1,9})""" + "SS" -> """(\d{2})""" + "SSS" -> """(\d{3})""" + "SSSS" -> """(\d{4})""" + "SSSSS" -> """(\d{5})""" + "SSSSSS" -> """(\d{6})""" + "SSSSSSS" -> """(\d{7})""" + "SSSSSSSS" -> """(\d{8})""" + "SSSSSSSSS" -> """(\d{9})""" + "X", "XX", "XXX", "x", "xx", "xxx", "Z" -> """([\w:\+\-]+)""" + "a" -> """(\w+)""" + " " -> """(\s+)""" + else -> when { + it.startsWith('\'') -> "(" + Regex.escape(it.substr(1, it.length - 2)) + ")" + else -> "(" + Regex.escape(it) + ")" + } + } + } + + /** + * @return the regular expression string used for matching this format, able to be composed into another regex + */ + fun matchingRegexString(): String = regexChunks.mapIndexed { index, it -> + if (options.optionalSupport) { + val opens = openOffsets.getOrElse(index) { 0 } + val closes = closeOffsets.getOrElse(index) { 0 } + buildString { + repeat(opens) { append("(?:") } + append(it) + repeat(closes) { append(")?") } + } + } else { + it + } + }.joinToString("") + + //val escapedFormat = Regex.escape(format) + internal val rx2: Regex = Regex("^" + matchingRegexString() + "$") + + + // EEE, dd MMM yyyy HH:mm:ss z -- > Sun, 06 Nov 1994 08:49:37 GMT + // YYYY-MM-dd HH:mm:ss + + override fun format(dd: DateTimeTz): String { + val utc = dd.local + var out = "" + for (name in chunks) { + val nlen = name.length + out += when (name) { + "E", "EE", "EEE" -> DayOfWeek[utc.dayOfWeek.index0].localShortName(realLocale) + "EEEE", "EEEEE", "EEEEEE" -> DayOfWeek[utc.dayOfWeek.index0].localName(realLocale) + "z", "zzz" -> dd.offset.timeZone + "d", "dd" -> utc.dayOfMonth.padded(nlen) + "do" -> realLocale.getOrdinalByDay(utc.dayOfMonth) + "M", "MM" -> utc.month1.padded(nlen) + "MMM" -> Month[utc.month1].localName(realLocale).substr(0, 3) + "MMMM" -> Month[utc.month1].localName(realLocale) + "MMMMM" -> Month[utc.month1].localName(realLocale).substr(0, 1) + "y" -> utc.yearInt + "yy" -> (utc.yearInt % 100).padded(2) + "yyy" -> (utc.yearInt % 1000).padded(3) + "yyyy" -> utc.yearInt.padded(4) + "YYYY" -> utc.yearInt.padded(4) + + "H", "HH" -> mconvertRangeZero(utc.hours, 24).padded(nlen) + "k", "kk" -> mconvertRangeNonZero(utc.hours, 24).padded(nlen) + + "h", "hh" -> mconvertRangeNonZero(utc.hours, 12).padded(nlen) + "K", "KK" -> mconvertRangeZero(utc.hours, 12).padded(nlen) + + "m", "mm" -> utc.minutes.padded(nlen) + "s", "ss" -> utc.seconds.padded(nlen) + + "S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS" -> { + val milli = utc.milliseconds + val base10length = log10(utc.milliseconds.toDouble()).toInt() + 1 + if (base10length > name.length) { + (milli.toDouble() * 10.0.pow(-1 * (base10length - name.length))).toInt() + } else { + "${milli.padded(3)}000000".substr(0, name.length) + } + } + "X", "XX", "XXX", "x", "xx", "xxx" -> { + when { + name.startsWith("X") && dd.offset.totalMinutesInt == 0 -> "Z" + else -> { + val p = if (dd.offset.totalMinutesInt >= 0) "+" else "-" + val hours = (dd.offset.totalMinutesInt / 60).absoluteValue + val minutes = (dd.offset.totalMinutesInt % 60).absoluteValue + when (name) { + "X", "x" -> "$p${hours.padded(2)}" + "XX", "xx" -> "$p${hours.padded(2)}${minutes.padded(2)}" + "XXX", "xxx" -> "$p${hours.padded(2)}:${minutes.padded(2)}" + else -> name + } + } + } + } + "a" -> realLocale.h12Marker[if (utc.hours < 12) 0 else 1] + else -> when { + name.startsWith('\'') -> name.substring(1, name.length - 1) + else -> name + } + } + } + return out + } + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? { + var millisecond = 0 + var second = 0 + var minute = 0 + var hour = 0 + var day = 1 + var month = 1 + var fullYear = 1970 + var offset: TimeSpan? = null + var isPm = false + var is12HourFormat = false + val result = rx2.find(str) ?: return null //println("Parser error: Not match, $str, $rx2"); + for ((name, value) in chunks.zip(result.groupValues.drop(1))) { + if (value.isEmpty()) continue + + when (name) { + "E", "EE", "EEE", "EEEE", "EEEEE", "EEEEEE" -> Unit // day of week (Sun | Sunday) + "z", "zzz" -> { // timezone (GMT) + offset = MicroStrReader(value).readTimeZoneOffset(tzNames) + } + "d", "dd" -> day = value.toInt() + "do" -> day = realLocale.getDayByOrdinal(value) + "M", "MM" -> month = value.toInt() + "MMM" -> month = realLocale.monthsShort.indexOf(value) + 1 + "y", "yyyy", "YYYY" -> fullYear = value.toInt() + "yy" -> if (doThrow) throw RuntimeException("Not guessing years from two digits.") else return null + "yyy" -> fullYear = value.toInt() + if (value.toInt() < 800) 2000 else 1000 // guessing year... + "H", "HH", "k", "kk" -> hour = value.toInt() + "h", "hh", "K", "KK" -> { + hour = value.toInt() + is12HourFormat = true + } + "m", "mm" -> minute = value.toInt() + "s", "ss" -> second = value.toInt() + "S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS" -> { + val base10length = log10(value.toDouble()).toInt() + 1 + millisecond = if (base10length > 3) { + // only precision to millisecond supported, ignore the rest. ex: 9999999 => 999" + (value.toDouble() * 10.0.pow(-1 * (base10length - 3))).toInt() + } else { + value.toInt() + } + } + "X", "XX", "XXX", "x", "xx", "xxx" -> { + when { + name.startsWith("X") && value.first() == 'Z' -> offset = 0.hours + name.startsWith("x") && value.first() == 'Z' -> { + if (doThrow) throw RuntimeException("Zulu Time Zone is only accepted with X-XXX formats.") else return null + } + value.first() != 'Z' -> { + val valueUnsigned = value.replace(":", "").removePrefix("-").removePrefix("+") + val hours = when (name.length) { + 1 -> valueUnsigned.toInt() + else -> valueUnsigned.take(2).toInt() + } + val minutes = when (name.length) { + 1 -> 0 + else -> valueUnsigned.drop(2).toInt() + } + offset = hours.hours + minutes.minutes + if (value.first() == '-') { + offset = -offset + } + } + } + } + "MMMM" -> month = realLocale.months.indexOf(value) + 1 + "MMMMM" -> if (doThrow) throw RuntimeException("Not possible to get the month from one letter.") else return null + "a" -> isPm = value.equals("pm", ignoreCase = true) + else -> { + // ... + } + } + } + //return DateTime.createClamped(fullYear, month, day, hour, minute, second) + if (is12HourFormat) { + if (isPm) { + if (hour != 12) { + hour += 12 + } + } else { + if (hour == 12) { + hour = 0 + } + } + } + if (!doAdjust) { + if (month !in 1..12) if (doThrow) error("Invalid month $month") else return null + if (day !in 1..32) if (doThrow) error("Invalid day $day") else return null + if (hour !in 0..24) if (doThrow) error("Invalid hour $hour") else return null + if (minute !in 0..59) if (doThrow) error("Invalid minute $minute") else return null + if (second !in 0..59) if (doThrow) error("Invalid second $second") else return null + if (millisecond !in 0..999) if (doThrow) error("Invalid millisecond $millisecond") else return null + } + val dateTime = DateTime.createAdjusted(fullYear, month, day, hour umod 24, minute, second, millisecond) + return dateTime.toOffsetUnadjusted(offset ?: 0.hours) + } + + override fun toString(): String = format +} + +private fun mconvertRangeZero(value: Int, size: Int): Int { + return (value umod size) +} + +private fun mconvertRangeNonZero(value: Int, size: Int): Int { + val res = (value umod size) + return if (res == 0) size else res +} + +private fun MicroStrReader.readRepeatedChar(): String { + return readChunk { + val c = readChar() + while (hasMore && (tryRead(c))) Unit + } +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/PatternTimeFormat.kt b/klock/src/commonMain/kotlin/korlibs/time/PatternTimeFormat.kt new file mode 100644 index 00000000000..48bf42663f0 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/PatternTimeFormat.kt @@ -0,0 +1,183 @@ +package korlibs.time + +import korlibs.time.internal.* +import korlibs.time.internal.MicroStrReader +import korlibs.time.internal.increment +import korlibs.time.internal.padded +import korlibs.time.internal.substr +import kotlin.math.log10 +import kotlin.math.pow + +data class PatternTimeFormat( + val format: String, + val options: Options = Options.DEFAULT +) : TimeFormat, Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + } + + data class Options(val optionalSupport: Boolean = false) : Serializable { + companion object { + @Suppress("MayBeConstant", "unused") + private const val serialVersionUID = 1L + + val DEFAULT = Options(optionalSupport = false) + val WITH_OPTIONAL = Options(optionalSupport = true) + } + } + + fun withOptions(options: Options) = this.copy(options = options) + fun withOptional() = this.copy(options = options.copy(optionalSupport = true)) + fun withNonOptional() = this.copy(options = options.copy(optionalSupport = false)) + + private val openOffsets = LinkedHashMap() + private val closeOffsets = LinkedHashMap() + + internal val chunks = arrayListOf().also { chunks -> + val s = MicroStrReader(format) + while (s.hasMore) { + if (s.peekChar() == '\'') { + val escapedChunk = s.readChunk { + s.tryRead('\'') + while (s.hasMore && s.readChar() != '\'') Unit + } + chunks.add(escapedChunk) + continue + } + if (options.optionalSupport) { + val offset = chunks.size + if (s.tryRead('[')) { + openOffsets.increment(offset) + continue + } + if (s.tryRead(']')) { + closeOffsets.increment(offset - 1) + continue + } + } + val chunk = s.readChunk { + val c = s.readChar() + while (s.hasMore && s.tryRead(c)) Unit + } + chunks.add(chunk) + } + }.toList() + + private val regexChunks = chunks.map { + when (it) { + "H", "k" -> """(\d{1,})""" + "HH", "kk" -> """(\d{2,})""" + "h", "K" -> """(\d{1,2})""" + "hh", "KK" -> """(\d{2})""" + "m" -> """(\d{1,2})""" + "mm" -> """(\d{2})""" + "s" -> """(\d{1,2})""" + "ss" -> """(\d{2})""" + "S" -> """(\d{1,6})""" + "SS" -> """(\d{2})""" + "SSS" -> """(\d{3})""" + "SSSS" -> """(\d{4})""" + "SSSSS" -> """(\d{5})""" + "SSSSSS" -> """(\d{6})""" + "SSSSSSS" -> """(\d{7})""" + "SSSSSSSS" -> """(\d{8})""" + "a" -> """(\w+)""" + " " -> """(\s+)""" + else -> when { + it.startsWith('\'') -> "(" + Regex.escapeReplacement(it.substr(1, it.length - 2)) + ")" + else -> "(" + Regex.escapeReplacement(it) + ")" + } + } + } + + private val rx2: Regex = Regex("^" + regexChunks.mapIndexed { index, it -> + if (options.optionalSupport) { + val opens = openOffsets.getOrElse(index) { 0 } + val closes = closeOffsets.getOrElse(index) { 0 } + buildString { + repeat(opens) { append("(?:") } + append(it) + repeat(closes) { append(")?") } + } + } else { + it + } + }.joinToString("") + "$") + + private fun clampZero(value: Int, size: Int) = (value umod size) + + private fun clampNonZero(value: Int, size: Int) = (value umod size).let { if (it == 0) size else it } + + override fun format(dd: TimeSpan): String { + val time = Time(dd) + var out = "" + for (name in chunks) { + val nlen = name.length + out += when (name) { + "H", "HH" -> time.hour.padded(nlen) + "k", "kk" -> time.hour.padded(nlen) + + "h", "hh" -> clampNonZero(time.hour, 12).padded(nlen) + "K", "KK" -> clampZero(time.hour, 12).padded(nlen) + + "m", "mm" -> time.minute.padded(nlen) + "s", "ss" -> time.second.padded(nlen) + + "S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS" -> { + val milli = time.millisecond + val numberLength = log10(time.millisecond.toDouble()).toInt() + 1 + if (numberLength > name.length) { + (milli.toDouble() / 10.0.pow(numberLength - name.length)).toInt() + } else { + "${milli.padded(3)}00000".substr(0, name.length) + } + } + "a" -> if (time.hour < 12) "am" else if (time.hour < 24) "pm" else "" + else -> if (name.startsWith('\'')) name.substring(1, name.length - 1) else name + } + } + return out + } + + override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? { + var millisecond = 0 + var second = 0 + var minute = 0 + var hour = 0 + var isPm = false + var is12HourFormat = false + val result = rx2.find(str) ?: return null //println("Parser error: Not match, $str, $rx2"); + for ((name, value) in chunks.zip(result.groupValues.drop(1))) { + if (value.isEmpty()) continue + when (name) { + "H", "HH", "k", "kk" -> hour = value.toInt() + "h", "hh", "K", "KK" -> { + hour = value.toInt() umod 24 + is12HourFormat = true + } + "m", "mm" -> minute = value.toInt() + "s", "ss" -> second = value.toInt() + "S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS" -> { + val numberLength = log10(value.toDouble()).toInt() + 1 + millisecond = if (numberLength > 3) { + // only precision to millisecond supported, ignore the rest: 9999999 => 999 + (value.toDouble() * 10.0.pow(-1 * (numberLength - 3))).toInt() + } else { + value.toInt() + } + } + "a" -> isPm = value == "pm" + else -> { + // ... + } + } + } + if (is12HourFormat && isPm) { + hour += 12 + } + return hour.hours + minute.minutes + second.seconds + millisecond.milliseconds + } + + override fun toString(): String = format +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/PerformanceCounter.kt b/klock/src/commonMain/kotlin/korlibs/time/PerformanceCounter.kt new file mode 100644 index 00000000000..ccec5f32ec6 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/PerformanceCounter.kt @@ -0,0 +1,29 @@ +package korlibs.time + +import korlibs.time.internal.* +import kotlin.time.* + +/** + * Class for measuring relative times with as much precision as possible. + */ +object PerformanceCounter { + /** + * Returns a performance counter measure in nanoseconds. + */ + val nanoseconds: Double get() = KlockInternal.now.nanoseconds + + /** + * Returns a performance counter measure in microseconds. + */ + val microseconds: Double get() = KlockInternal.now.microseconds + + /** + * Returns a performance counter measure in milliseconds. + */ + val milliseconds: Double get() = KlockInternal.now.milliseconds + + /** + * Returns a performance counter as a [Duration]. + */ + val reference: Duration get() = KlockInternal.now +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/RangesExt.kt b/klock/src/commonMain/kotlin/korlibs/time/RangesExt.kt new file mode 100644 index 00000000000..1af9af9671a --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/RangesExt.kt @@ -0,0 +1,4 @@ +package korlibs.time + +public fun TimeSpan.convertRange(srcMin: TimeSpan, srcMax: TimeSpan, dstMin: TimeSpan, dstMax: TimeSpan): TimeSpan = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin))) +public fun DateTime.convertRange(srcMin: DateTime, srcMax: DateTime, dstMin: DateTime, dstMax: DateTime): DateTime = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin))) diff --git a/klock/src/commonMain/kotlin/korlibs/time/Sleep.kt b/klock/src/commonMain/kotlin/korlibs/time/Sleep.kt new file mode 100644 index 00000000000..088abc31b47 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Sleep.kt @@ -0,0 +1,7 @@ +package korlibs.time + +import korlibs.time.internal.* + +/** Sleeps the thread during the specified time. Spinlocks on JS */ +@ExperimentalStdlibApi +fun blockingSleep(time: TimeSpan) = KlockInternal.sleep(time) diff --git a/klock/src/commonMain/kotlin/korlibs/time/Stopwatch.kt b/klock/src/commonMain/kotlin/korlibs/time/Stopwatch.kt new file mode 100644 index 00000000000..d3449a06d5d --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Stopwatch.kt @@ -0,0 +1,25 @@ +package korlibs.time + +class Stopwatch(val nanosecondProvider: () -> Double = { PerformanceCounter.nanoseconds }) { + constructor(timeProvider: TimeProvider) : this({ timeProvider.now().unixMillis.milliseconds.nanoseconds }) + private var running = false + private var startNano = 0.0 + private val currentNano get() = nanosecondProvider() + private fun setStart() { startNano = currentNano } + init { + setStart() + } + fun start() = this.apply { + setStart() + running = true + } + fun restart() = start() + fun stop() = this.apply { + startNano = elapsedNanoseconds + running = false + } + val elapsedNanoseconds get() = if (running) currentNano - startNano else startNano + val elapsedMicroseconds get() = elapsedNanoseconds * 1000 + val elapsed: TimeSpan get() = elapsedNanoseconds.nanoseconds + fun getElapsedAndRestart(): TimeSpan = elapsed.also { restart() } +} diff --git a/klock/src/commonMain/kotlin/korlibs/time/Time.kt b/klock/src/commonMain/kotlin/korlibs/time/Time.kt new file mode 100644 index 00000000000..c8c08b0f168 --- /dev/null +++ b/klock/src/commonMain/kotlin/korlibs/time/Time.kt @@ -0,0 +1,50 @@ +package korlibs.time + +import korlibs.time.internal.Serializable +import kotlin.jvm.JvmInline +import kotlin.math.abs + +/** + * Represents a union of [millisecond], [second], [minute] and [hour]. + */ +@JvmInline +value class Time(val encoded: TimeSpan) : Comparable