mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2024-11-26 03:58:45 +00:00
add klock module
This commit is contained in:
parent
6230accb68
commit
65d01b1fb3
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## 0.20.23
|
## 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
|
## 0.20.22
|
||||||
|
|
||||||
* `Common`:
|
* `Common`:
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
# MicroUtils
|
# 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:
|
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)
|
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)
|
||||||
|
47
klock/LICENSE
Normal file
47
klock/LICENSE
Normal file
@ -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.
|
7
klock/build.gradle
Normal file
7
klock/build.gradle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
@ -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 <T> 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
|
8
klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt
Normal file
8
klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt
Normal file
@ -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)
|
72
klock/src/commonMain/kotlin/korlibs/time/Date.kt
Normal file
72
klock/src/commonMain/kotlin/korlibs/time/Date.kt
Normal file
@ -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<Date>, 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))
|
@ -0,0 +1,6 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception for Date operations.
|
||||||
|
*/
|
||||||
|
class DateException(msg: String) : RuntimeException(msg)
|
43
klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt
Normal file
43
klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt
Normal file
@ -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)
|
458
klock/src/commonMain/kotlin/korlibs/time/DateTime.kt
Normal file
458
klock/src/commonMain/kotlin/korlibs/time/DateTime.kt
Normal file
@ -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<DateTime>, 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
|
||||||
|
}
|
154
klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt
Normal file
154
klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt
Normal file
@ -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<DateTime>, 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 <T> _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<DateTimeRange> = 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<DateTimeRange>.toStringLongs() = this.map { it.toStringLongs() }.toString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a right-opened range between two [DateTime]s
|
||||||
|
*/
|
||||||
|
infix fun DateTime.until(other: DateTime) = DateTimeRange(this, other)
|
270
klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt
Normal file
270
klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt
Normal file
@ -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<DateTimeRange>) : 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<DateTimeRange>) : 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<DateTimeRange>): String = "${ranges.map { it.toStringLongs() }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Fast {
|
||||||
|
internal fun combine(ranges: List<DateTimeRange>): List<DateTimeRange> {
|
||||||
|
if (ranges.isEmpty()) return ranges
|
||||||
|
|
||||||
|
val sorted = ranges.sortedBy { it.from.unixMillis }
|
||||||
|
val out = arrayListOf<DateTimeRange>()
|
||||||
|
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<DateTimeRange>()
|
||||||
|
//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<DateTimeRange>()
|
||||||
|
//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<DateTimeRange>): List<DateTimeRange> {
|
||||||
|
// @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<DateTimeRange>()
|
||||||
|
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<DateTimeRange>.toRangeSet() = DateTimeRangeSet(this.toList())
|
143
klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt
Normal file
143
klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt
Normal file
@ -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<DateTimeSpan>, 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<String>().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) }
|
||||||
|
}
|
@ -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'")
|
125
klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt
Normal file
125
klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt
Normal file
@ -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<DateTimeTz>, 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)"
|
||||||
|
}
|
95
klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt
Normal file
95
klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt
Normal file
@ -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<DayOfWeekWithLocale> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
42
klock/src/commonMain/kotlin/korlibs/time/Frequency.kt
Normal file
42
klock/src/commonMain/kotlin/korlibs/time/Frequency.kt
Normal file
@ -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<Frequency>, 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
|
||||||
|
}
|
446
klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt
Normal file
446
klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt
Normal file
@ -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
|
122
klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt
Normal file
122
klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt
Normal file
@ -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<String>
|
||||||
|
abstract val months: List<String>
|
||||||
|
abstract val firstDayOfWeek: DayOfWeek
|
||||||
|
open val monthsShort: List<String> get() = months.map { it.substr(0, 3) }
|
||||||
|
open val daysOfWeekShort: List<String> get() = daysOfWeek.map { it.substr(0, 3) }
|
||||||
|
|
||||||
|
//private val daysOfWeekWithLocaleList: Array<DayOfWeekWithLocale> = 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<DayOfWeek> { 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<String> by klockAtomicLazy { months.map { it.substr(0, 3) } }
|
||||||
|
//open val daysOfWeekShort: List<String> by klockAtomicLazy { daysOfWeek.map { it.substr(0, 3) } }
|
||||||
|
/*
|
||||||
|
private val _lock = KlockLock()
|
||||||
|
private val _monthsShort = KlockAtomicRef<List<String>?>(null)
|
||||||
|
private val _daysOfWeekShort = KlockAtomicRef<List<String>?>(null)
|
||||||
|
//open val monthsShort by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { months.map { it.substr(0, 3) } }
|
||||||
|
open val monthsShort: List<String> get() = _lock {
|
||||||
|
if (_monthsShort.value == null) {
|
||||||
|
_monthsShort.value = months.map { it.substr(0, 3) }
|
||||||
|
}
|
||||||
|
_monthsShort.value!!
|
||||||
|
}
|
||||||
|
open val daysOfWeekShort: List<String> get() = _lock {
|
||||||
|
if (_daysOfWeekShort.value == null) {
|
||||||
|
_daysOfWeekShort.value = daysOfWeek.map { it.substr(0, 3) }
|
||||||
|
}
|
||||||
|
_daysOfWeekShort.value!!
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
open val h12Marker: List<String> 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 <R> 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<String> get() = listOf(
|
||||||
|
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
||||||
|
)
|
||||||
|
override val months: List<String> 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)
|
@ -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,
|
||||||
|
}
|
37
klock/src/commonMain/kotlin/korlibs/time/Measure.kt
Normal file
37
klock/src/commonMain/kotlin/korlibs/time/Measure.kt
Normal file
@ -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 <T> 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 <T> measureTimeWithResult(callback: () -> T): TimedResult<T> {
|
||||||
|
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<T>(val result: T, val time: TimeSpan)
|
131
klock/src/commonMain/kotlin/korlibs/time/Month.kt
Normal file
131
klock/src/commonMain/kotlin/korlibs/time/Month.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt
Normal file
66
klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt
Normal file
@ -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<MonthSpan>, 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<String>()
|
||||||
|
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
|
28
klock/src/commonMain/kotlin/korlibs/time/NumberOfTimes.kt
Normal file
28
klock/src/commonMain/kotlin/korlibs/time/NumberOfTimes.kt
Normal file
@ -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"
|
||||||
|
}
|
318
klock/src/commonMain/kotlin/korlibs/time/PatternDateFormat.kt
Normal file
318
klock/src/commonMain/kotlin/korlibs/time/PatternDateFormat.kt
Normal file
@ -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<Int, Int>()
|
||||||
|
private val closeOffsets = LinkedHashMap<Int, Int>()
|
||||||
|
|
||||||
|
internal val chunks = arrayListOf<String>().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<String> = 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
|
||||||
|
}
|
||||||
|
}
|
183
klock/src/commonMain/kotlin/korlibs/time/PatternTimeFormat.kt
Normal file
183
klock/src/commonMain/kotlin/korlibs/time/PatternTimeFormat.kt
Normal file
@ -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<Int, Int>()
|
||||||
|
private val closeOffsets = LinkedHashMap<Int, Int>()
|
||||||
|
|
||||||
|
internal val chunks = arrayListOf<String>().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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
4
klock/src/commonMain/kotlin/korlibs/time/RangesExt.kt
Normal file
4
klock/src/commonMain/kotlin/korlibs/time/RangesExt.kt
Normal file
@ -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)))
|
7
klock/src/commonMain/kotlin/korlibs/time/Sleep.kt
Normal file
7
klock/src/commonMain/kotlin/korlibs/time/Sleep.kt
Normal file
@ -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)
|
25
klock/src/commonMain/kotlin/korlibs/time/Stopwatch.kt
Normal file
25
klock/src/commonMain/kotlin/korlibs/time/Stopwatch.kt
Normal file
@ -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() }
|
||||||
|
}
|
50
klock/src/commonMain/kotlin/korlibs/time/Time.kt
Normal file
50
klock/src/commonMain/kotlin/korlibs/time/Time.kt
Normal file
@ -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<Time>, Serializable {
|
||||||
|
companion object {
|
||||||
|
@Suppress("MayBeConstant", "unused")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
|
||||||
|
/** Constructs a new [Time] from the [hour], [minute], [second] and [millisecond] components. */
|
||||||
|
operator fun invoke(hour: Int, minute: Int = 0, second: Int = 0, millisecond: Int = 0): Time =
|
||||||
|
Time(hour.hours + minute.minutes + second.seconds + millisecond.milliseconds)
|
||||||
|
|
||||||
|
private const val DIV_MILLISECONDS = 1
|
||||||
|
private const val DIV_SECONDS = DIV_MILLISECONDS * 1000
|
||||||
|
private const val DIV_MINUTES = DIV_SECONDS * 60
|
||||||
|
private const val DIV_HOURS = DIV_MINUTES * 60
|
||||||
|
}
|
||||||
|
/** The [millisecond] part. */
|
||||||
|
val millisecond: Int get() = abs((encoded.millisecondsInt / DIV_MILLISECONDS) % 1000)
|
||||||
|
/** The [second] part. */
|
||||||
|
val second: Int get() = abs((encoded.millisecondsInt / DIV_SECONDS) % 60)
|
||||||
|
/** The [minute] part. */
|
||||||
|
val minute: Int get() = abs((encoded.millisecondsInt / DIV_MINUTES) % 60)
|
||||||
|
/** The [hour] part. */
|
||||||
|
val hour: Int get() = (encoded.millisecondsInt / DIV_HOURS)
|
||||||
|
/** The [hour] part adjusted to 24-hour format. */
|
||||||
|
val hourAdjusted: Int get() = (encoded.millisecondsInt / DIV_HOURS % 24)
|
||||||
|
|
||||||
|
/** Returns new [Time] instance adjusted to 24-hour format. */
|
||||||
|
fun adjust(): Time = Time(hourAdjusted, minute, second, millisecond)
|
||||||
|
|
||||||
|
/** Converts this date to String using [format] for representing it. */
|
||||||
|
fun format(format: String) = TimeFormat(format).format(this)
|
||||||
|
/** Converts this date to String using [format] for representing it. */
|
||||||
|
fun format(format: TimeFormat) = format.format(this)
|
||||||
|
|
||||||
|
/** Converts this time to String formatting it like "00:00:00.000", "23:59:59.999" or "-23:59:59.999" if the [hour] is negative */
|
||||||
|
override fun toString(): String = "${if (hour < 0) "-" else ""}${abs(hour).toString().padStart(2, '0')}:${abs(minute).toString().padStart(2, '0')}:${abs(second).toString().padStart(2, '0')}.${abs(millisecond).toString().padStart(3, '0')}"
|
||||||
|
|
||||||
|
override fun compareTo(other: Time): Int = encoded.compareTo(other.encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun Time.plus(span: TimeSpan) = Time(this.encoded + span)
|
35
klock/src/commonMain/kotlin/korlibs/time/TimeFormat.kt
Normal file
35
klock/src/commonMain/kotlin/korlibs/time/TimeFormat.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
interface TimeFormat {
|
||||||
|
fun format(dd: TimeSpan): String
|
||||||
|
fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean = true): TimeSpan?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_FORMAT = TimeFormat("HH:mm:ss.SSS")
|
||||||
|
val FORMAT_TIME = TimeFormat("HH:mm:ss")
|
||||||
|
|
||||||
|
val FORMATS = listOf(DEFAULT_FORMAT, FORMAT_TIME)
|
||||||
|
|
||||||
|
fun parse(time: String): TimeSpan {
|
||||||
|
var lastError: Throwable? = null
|
||||||
|
for (format in FORMATS) {
|
||||||
|
try {
|
||||||
|
return format.parse(time)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
lastError = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError!!
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke(pattern: String) = PatternTimeFormat(pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TimeFormat.parse(str: String, doAdjust: Boolean = true): TimeSpan =
|
||||||
|
tryParse(str, doThrow = true, doAdjust = doAdjust) ?: throw DateException("Not a valid format: '$str' for '$this'")
|
||||||
|
fun TimeFormat.parseTime(str: String): Time = Time(parse(str))
|
||||||
|
|
||||||
|
fun TimeFormat.format(time: Double): String = format(time.milliseconds)
|
||||||
|
fun TimeFormat.format(time: Long): String = format(time.milliseconds)
|
||||||
|
fun TimeFormat.format(time: Time): String = format(time.encoded)
|
23
klock/src/commonMain/kotlin/korlibs/time/TimeProvider.kt
Normal file
23
klock/src/commonMain/kotlin/korlibs/time/TimeProvider.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
/** Class to provide time that can be overridden to mock or change its behaviour. */
|
||||||
|
interface TimeProvider {
|
||||||
|
/** Returns a [DateTime] for this provider. */
|
||||||
|
fun now(): DateTime
|
||||||
|
|
||||||
|
companion object : TimeProvider {
|
||||||
|
override fun now(): DateTime = DateTime.now()
|
||||||
|
|
||||||
|
/** Constructs a [TimeProvider] from a [callback] producing a [DateTime]. */
|
||||||
|
operator fun invoke(callback: () -> DateTime) = object : TimeProvider {
|
||||||
|
override fun now(): DateTime = callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun TimeProvider.measure(block: () -> Unit): TimeSpan {
|
||||||
|
val start = now()
|
||||||
|
block()
|
||||||
|
val end = now()
|
||||||
|
return end - start
|
||||||
|
}
|
18
klock/src/commonMain/kotlin/korlibs/time/TimeSampler.kt
Normal file
18
klock/src/commonMain/kotlin/korlibs/time/TimeSampler.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to count the number of times a sample was added in a timeWindow
|
||||||
|
*/
|
||||||
|
class TimeSampler(var timeWindow: TimeSpan = 1.seconds) {
|
||||||
|
private val events = ArrayDeque<Double>()
|
||||||
|
|
||||||
|
val count: Int get() = events.size
|
||||||
|
|
||||||
|
fun add() {
|
||||||
|
val now = DateTime.now()
|
||||||
|
events.add(now.unixMillisDouble)
|
||||||
|
while (events.first() < (now - timeWindow).unixMillisDouble) {
|
||||||
|
events.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
208
klock/src/commonMain/kotlin/korlibs/time/TimeSpan.kt
Normal file
208
klock/src/commonMain/kotlin/korlibs/time/TimeSpan.kt
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.internal.*
|
||||||
|
import kotlin.jvm.*
|
||||||
|
import kotlin.math.*
|
||||||
|
import kotlin.time.*
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
private val DURATION_NIL: Duration = (-0x001FFFFFFFFFFFF3L).toDuration(DurationUnit.NANOSECONDS)
|
||||||
|
|
||||||
|
val Duration.Companion.NIL: Duration get() = DURATION_NIL
|
||||||
|
//val Duration.Companion.ZERO get() = Duration.ZERO
|
||||||
|
|
||||||
|
/** [TimeSpan] representing this number as [nanoseconds] or 1 / 1_000_000_000 [seconds]. */
|
||||||
|
inline val Long.nanoseconds: Duration get() = toDuration(DurationUnit.NANOSECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [microseconds] or 1 / 1_000_000 [seconds]. */
|
||||||
|
inline val Long.microseconds: Duration get() = toDuration(DurationUnit.MICROSECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [milliseconds] or 1 / 1_000 [seconds]. */
|
||||||
|
inline val Long.milliseconds: Duration get() = toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [seconds]. */
|
||||||
|
inline val Long.seconds: Duration get() = toDuration(DurationUnit.SECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [minutes] or 60 [seconds]. */
|
||||||
|
inline val Long.minutes: Duration get() = toDuration(DurationUnit.MINUTES)
|
||||||
|
/** [TimeSpan] representing this number as [hours] or 3_600 [seconds]. */
|
||||||
|
inline val Long.hours: Duration get() = toDuration(DurationUnit.HOURS)
|
||||||
|
/** [TimeSpan] representing this number as [days] or 86_400 [seconds]. */
|
||||||
|
inline val Long.days: Duration get() = toDuration(DurationUnit.DAYS)
|
||||||
|
/** [TimeSpan] representing this number as [weeks] or 604_800 [seconds]. */
|
||||||
|
inline val Long.weeks: Duration get() = (this * 7).days
|
||||||
|
|
||||||
|
/** [TimeSpan] representing this number as [nanoseconds] or 1 / 1_000_000_000 [seconds]. */
|
||||||
|
inline val Float.nanoseconds get() = this.toDouble().nanoseconds
|
||||||
|
/** [TimeSpan] representing this number as [microseconds] or 1 / 1_000_000 [seconds]. */
|
||||||
|
inline val Float.microseconds get() = this.toDouble().microseconds
|
||||||
|
/** [TimeSpan] representing this number as [milliseconds] or 1 / 1_000 [seconds]. */
|
||||||
|
inline val Float.milliseconds get() = this.toDouble().milliseconds
|
||||||
|
/** [TimeSpan] representing this number as [seconds]. */
|
||||||
|
inline val Float.seconds get() = this.toDouble().seconds
|
||||||
|
/** [TimeSpan] representing this number as [minutes] or 60 [seconds]. */
|
||||||
|
inline val Float.minutes get() = this.toDouble().minutes
|
||||||
|
/** [TimeSpan] representing this number as [hours] or 3_600 [seconds]. */
|
||||||
|
inline val Float.hours get() = this.toDouble().hours
|
||||||
|
/** [TimeSpan] representing this number as [days] or 86_400 [seconds]. */
|
||||||
|
inline val Float.days get() = this.toDouble().days
|
||||||
|
/** [TimeSpan] representing this number as [weeks] or 604_800 [seconds]. */
|
||||||
|
inline val Float.weeks get() = this.toDouble().weeks
|
||||||
|
|
||||||
|
/** [TimeSpan] representing this number as [nanoseconds] or 1 / 1_000_000_000 [seconds]. */
|
||||||
|
inline val Int.nanoseconds: Duration get() = this.toDouble().nanoseconds
|
||||||
|
/** [TimeSpan] representing this number as [microseconds] or 1 / 1_000_000 [seconds]. */
|
||||||
|
inline val Int.microseconds: Duration get() = this.toDouble().microseconds
|
||||||
|
/** [TimeSpan] representing this number as [milliseconds] or 1 / 1_000 [seconds]. */
|
||||||
|
inline val Int.milliseconds: Duration get() = this.toDouble().milliseconds
|
||||||
|
/** [TimeSpan] representing this number as [seconds]. */
|
||||||
|
inline val Int.seconds: Duration get() = this.toDouble().seconds
|
||||||
|
/** [TimeSpan] representing this number as [minutes] or 60 [seconds]. */
|
||||||
|
inline val Int.minutes: Duration get() = this.toDouble().minutes
|
||||||
|
/** [TimeSpan] representing this number as [hours] or 3_600 [seconds]. */
|
||||||
|
inline val Int.hours: Duration get() = this.toDouble().hours
|
||||||
|
/** [TimeSpan] representing this number as [days] or 86_400 [seconds]. */
|
||||||
|
inline val Int.days: Duration get() = this.toDouble().days
|
||||||
|
/** [TimeSpan] representing this number as [weeks] or 604_800 [seconds]. */
|
||||||
|
inline val Int.weeks: Duration get() = this.toDouble().weeks
|
||||||
|
|
||||||
|
/** [TimeSpan] representing this number as [nanoseconds] or 1 / 1_000_000_000 [seconds]. */
|
||||||
|
inline val Double.nanoseconds: Duration get() = toDuration(DurationUnit.NANOSECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [microseconds] or 1 / 1_000_000 [seconds]. */
|
||||||
|
inline val Double.microseconds: Duration get() = toDuration(DurationUnit.MICROSECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [milliseconds] or 1 / 1_000 [seconds]. */
|
||||||
|
inline val Double.milliseconds: Duration get() = toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [seconds]. */
|
||||||
|
inline val Double.seconds: Duration get() = toDuration(DurationUnit.SECONDS)
|
||||||
|
/** [TimeSpan] representing this number as [minutes] or 60 [seconds]. */
|
||||||
|
inline val Double.minutes: Duration get() = toDuration(DurationUnit.MINUTES)
|
||||||
|
/** [TimeSpan] representing this number as [hours] or 3_600 [seconds]. */
|
||||||
|
inline val Double.hours: Duration get() = toDuration(DurationUnit.HOURS)
|
||||||
|
/** [TimeSpan] representing this number as [days] or 86_400 [seconds]. */
|
||||||
|
inline val Double.days: Duration get() = toDuration(DurationUnit.DAYS)
|
||||||
|
/** [TimeSpan] representing this number as [weeks] or 604_800 [seconds]. */
|
||||||
|
inline val Double.weeks: Duration get() = (this * 7).days
|
||||||
|
|
||||||
|
fun Duration.Companion.fromNanoseconds(value: Double): Duration = value.nanoseconds
|
||||||
|
fun Duration.Companion.fromMicroseconds(value: Double): Duration = value.microseconds
|
||||||
|
fun Duration.Companion.fromMilliseconds(value: Double): Duration = value.milliseconds
|
||||||
|
fun Duration.Companion.fromSeconds(value: Double): Duration = value.seconds
|
||||||
|
fun Duration.Companion.fromMinutes(value: Double): Duration = value.minutes
|
||||||
|
fun Duration.Companion.fromHours(value: Double): Duration = value.hours
|
||||||
|
fun Duration.Companion.fromDays(value: Double): Duration = value.days
|
||||||
|
fun Duration.Companion.fromWeeks(value: Double): Duration = value.weeks
|
||||||
|
|
||||||
|
inline fun Duration.Companion.fromNanoseconds(value: Number): Duration = fromNanoseconds(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromMicroseconds(value: Number): Duration = fromMicroseconds(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromMilliseconds(value: Number): Duration = fromMilliseconds(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromSeconds(value: Number): Duration = fromSeconds(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromMinutes(value: Number): Duration = fromMinutes(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromHours(value: Number): Duration = fromHours(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromDays(value: Number): Duration = fromDays(value.toDouble())
|
||||||
|
inline fun Duration.Companion.fromWeeks(value: Number): Duration = fromWeeks(value.toDouble())
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a span of time, with [milliseconds] precision.
|
||||||
|
*
|
||||||
|
* It is a value class wrapping [Double] instead of [Long] to work on JavaScript without allocations.
|
||||||
|
*/
|
||||||
|
typealias TimeSpan = Duration
|
||||||
|
|
||||||
|
operator fun Duration.unaryPlus(): Duration = this
|
||||||
|
|
||||||
|
val Duration.milliseconds: Double get() = this.inWholeNanoseconds.toDouble() / 1_000_000.0
|
||||||
|
|
||||||
|
/** Returns the total number of [nanoseconds] for this [TimeSpan] (1 / 1_000_000_000 [seconds]) */
|
||||||
|
val Duration.nanoseconds: Double get() = this.inWholeNanoseconds.toDouble()
|
||||||
|
/** Returns the total number of [nanoseconds] for this [TimeSpan] (1 / 1_000_000_000 [seconds]) as Integer */
|
||||||
|
val Duration.nanosecondsInt: Int get() = this.inWholeNanoseconds.toInt()
|
||||||
|
|
||||||
|
/** Returns the total number of [microseconds] for this [TimeSpan] (1 / 1_000_000 [seconds]) */
|
||||||
|
val Duration.microseconds: Double get() = this.inWholeMicroseconds.toDouble()
|
||||||
|
/** Returns the total number of [microseconds] for this [TimeSpan] (1 / 1_000_000 [seconds]) as Integer */
|
||||||
|
val Duration.microsecondsInt: Int get() = this.inWholeMicroseconds.toInt()
|
||||||
|
|
||||||
|
/** Returns the total number of [seconds] for this [TimeSpan] */
|
||||||
|
val Duration.seconds: Double get() = this.milliseconds / MILLIS_PER_SECOND
|
||||||
|
/** Returns the total number of [minutes] for this [TimeSpan] (60 [seconds]) */
|
||||||
|
val Duration.minutes: Double get() = this.milliseconds / MILLIS_PER_MINUTE
|
||||||
|
/** Returns the total number of [hours] for this [TimeSpan] (3_600 [seconds]) */
|
||||||
|
val Duration.hours: Double get() = this.milliseconds / MILLIS_PER_HOUR
|
||||||
|
/** Returns the total number of [days] for this [TimeSpan] (86_400 [seconds]) */
|
||||||
|
val Duration.days: Double get() = this.milliseconds / MILLIS_PER_DAY
|
||||||
|
/** Returns the total number of [weeks] for this [TimeSpan] (604_800 [seconds]) */
|
||||||
|
val Duration.weeks: Double get() = this.milliseconds / MILLIS_PER_WEEK
|
||||||
|
|
||||||
|
/** Returns the total number of [milliseconds] as a [Long] */
|
||||||
|
val Duration.millisecondsLong: Long get() = milliseconds.toLong()
|
||||||
|
/** Returns the total number of [milliseconds] as an [Int] */
|
||||||
|
val Duration.millisecondsInt: Int get() = milliseconds.toInt()
|
||||||
|
|
||||||
|
fun TimeSpan(milliseconds: Double): Duration = milliseconds.milliseconds
|
||||||
|
|
||||||
|
operator fun Duration.plus(other: MonthSpan): DateTimeSpan = DateTimeSpan(other, this)
|
||||||
|
operator fun Duration.plus(other: DateTimeSpan): DateTimeSpan = DateTimeSpan(other.monthSpan, other.timeSpan + this)
|
||||||
|
|
||||||
|
operator fun Duration.minus(other: MonthSpan): DateTimeSpan = this + (-other)
|
||||||
|
operator fun Duration.minus(other: DateTimeSpan): DateTimeSpan = this + (-other)
|
||||||
|
|
||||||
|
operator fun Duration.times(scale: Float): Duration = TimeSpan((this.milliseconds * scale))
|
||||||
|
operator fun Duration.div(scale: Float): Duration = TimeSpan(this.milliseconds / scale)
|
||||||
|
|
||||||
|
infix fun Duration.divFloat(other: Duration): Float = (this.milliseconds / other.milliseconds).toFloat()
|
||||||
|
operator fun Duration.rem(other: Duration): Duration = (this.milliseconds % other.milliseconds).milliseconds
|
||||||
|
infix fun Duration.umod(other: Duration): Duration = (this.milliseconds umod other.milliseconds).milliseconds
|
||||||
|
|
||||||
|
/** Return true if [Duration.NIL] */
|
||||||
|
val Duration.isNil: Boolean get() = this == DURATION_NIL
|
||||||
|
|
||||||
|
fun Duration.Companion.now(): Duration = KlockInternal.now
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats this [TimeSpan] into something like `12:30:40.100`.
|
||||||
|
*
|
||||||
|
* For 3 hour, 20 minutes and 15 seconds
|
||||||
|
*
|
||||||
|
* 1 [components] (seconds): 12015
|
||||||
|
* 2 [components] (minutes): 200:15
|
||||||
|
* 3 [components] (hours) : 03:20:15
|
||||||
|
* 4 [components] (days) : 00:03:20:15
|
||||||
|
*
|
||||||
|
* With milliseconds would add decimals to the seconds part.
|
||||||
|
*/
|
||||||
|
fun Duration.toTimeString(components: Int = 3, addMilliseconds: Boolean = false): String =
|
||||||
|
toTimeString(milliseconds, components, addMilliseconds)
|
||||||
|
|
||||||
|
fun Duration.roundMilliseconds(): Duration = kotlin.math.round(milliseconds).milliseconds
|
||||||
|
fun max(a: Duration, b: Duration): Duration = max(a.milliseconds, b.milliseconds).milliseconds
|
||||||
|
fun min(a: Duration, b: Duration): Duration = min(a.milliseconds, b.milliseconds).milliseconds
|
||||||
|
fun Duration.clamp(min: Duration, max: Duration): Duration = when {
|
||||||
|
this < min -> min
|
||||||
|
this > max -> max
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
inline fun Duration.coalesce(block: () -> Duration): Duration = if (this != Duration.NIL) this else block()
|
||||||
|
|
||||||
|
private val timeSteps = listOf(60, 60, 24)
|
||||||
|
private fun toTimeStringRaw(totalMilliseconds: Double, components: Int = 3): String {
|
||||||
|
var timeUnit = floor(totalMilliseconds / 1000.0).toInt()
|
||||||
|
|
||||||
|
val out = arrayListOf<String>()
|
||||||
|
|
||||||
|
for (n in 0 until components) {
|
||||||
|
if (n == components - 1) {
|
||||||
|
out += timeUnit.padded(2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val step = timeSteps.getOrNull(n) ?: throw RuntimeException("Just supported ${timeSteps.size} steps")
|
||||||
|
val cunit = timeUnit % step
|
||||||
|
timeUnit /= step
|
||||||
|
out += cunit.padded(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.reversed().joinToString(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
@PublishedApi
|
||||||
|
internal fun toTimeString(totalMilliseconds: Double, components: Int = 3, addMilliseconds: Boolean = false): String {
|
||||||
|
val milliseconds = (totalMilliseconds % 1000).toInt()
|
||||||
|
val out = toTimeStringRaw(totalMilliseconds, components)
|
||||||
|
return if (addMilliseconds) "$out.$milliseconds" else out
|
||||||
|
}
|
45
klock/src/commonMain/kotlin/korlibs/time/TimedCache.kt
Normal file
45
klock/src/commonMain/kotlin/korlibs/time/TimedCache.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.reflect.*
|
||||||
|
|
||||||
|
class TimedCache<T : Any>(var ttl: TimeSpan, val timeProvider: TimeProvider = TimeProvider, val gen: () -> T) {
|
||||||
|
private var cachedTime: DateTime = DateTime.EPOCH
|
||||||
|
private lateinit var _value: T
|
||||||
|
|
||||||
|
var value: T
|
||||||
|
get() = getValue(Unit, null)
|
||||||
|
set(value) { setValue(Unit, null, value) }
|
||||||
|
|
||||||
|
operator fun getValue(obj: Any, prop: KProperty<*>?): T {
|
||||||
|
val now = timeProvider.now()
|
||||||
|
if (cachedTime == DateTime.EPOCH || (now - cachedTime) >= ttl) {
|
||||||
|
cachedTime = now
|
||||||
|
this._value = gen()
|
||||||
|
}
|
||||||
|
return _value
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(obj: Any, prop: KProperty<*>?, value: T) {
|
||||||
|
this._value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntTimedCache(val ttl: TimeSpan, val timeProvider: TimeProvider = TimeProvider, val gen: () -> Int) {
|
||||||
|
@PublishedApi internal var cachedTime = DateTime.EPOCH
|
||||||
|
@PublishedApi internal var _value: Int = 0
|
||||||
|
|
||||||
|
var value: Int
|
||||||
|
get() = get()
|
||||||
|
set(value) { _value = value }
|
||||||
|
|
||||||
|
inline fun get(): Int {
|
||||||
|
val now = timeProvider.now()
|
||||||
|
if (cachedTime == DateTime.EPOCH || (now - cachedTime >= ttl)) {
|
||||||
|
cachedTime = now
|
||||||
|
_value = gen()
|
||||||
|
}
|
||||||
|
return _value
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun getValue(obj: Any?, property: KProperty<*>): Int = get()
|
||||||
|
}
|
208
klock/src/commonMain/kotlin/korlibs/time/Timezone.kt
Normal file
208
klock/src/commonMain/kotlin/korlibs/time/Timezone.kt
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
enum class Timezone(val abbr: String, val offset: TimezoneOffset, val long: String = abbr) {
|
||||||
|
PDT("PDT", -7),
|
||||||
|
PST("PST", -8),
|
||||||
|
GMT("GMT", 0),
|
||||||
|
UTC("UTC", 0),
|
||||||
|
CET("CET", +1),
|
||||||
|
CEST("CEST", +2),
|
||||||
|
ACDT("ACDT", +10, 30),
|
||||||
|
ACST("ACST", +9, 30),
|
||||||
|
ACT("ACT", -5),
|
||||||
|
ACWST("ACWST", +8, 45),
|
||||||
|
ADT("ADT", -3),
|
||||||
|
AEDT("AEDT", +11),
|
||||||
|
AEST("AEST", +10),
|
||||||
|
AFT("AFT", +4, 30),
|
||||||
|
AKDT("AKDT", -8),
|
||||||
|
AKST("AKST", -9),
|
||||||
|
ALMT("ALMT", +6),
|
||||||
|
AMST("AMST", -3),
|
||||||
|
AMT_BRAZIL("AMT", -4, long = "AMT_BRAZIL"),
|
||||||
|
AMT_ARMENIA("AMT", +4, long = "AMT_ARMENIA"),
|
||||||
|
ANAT("ANAT", +12),
|
||||||
|
AQTT("AQTT", +5),
|
||||||
|
ART("ART", -3),
|
||||||
|
AST_ARABIA("AST", +3, long = "AST_ARABIA"),
|
||||||
|
AST_ATLANTIC("AST", -4, long = "AST_ATLANTIC"),
|
||||||
|
AWST("AWST", +8),
|
||||||
|
AZOST("AZOST", 0),
|
||||||
|
AZOT("AZOT", -1),
|
||||||
|
AZT("AZT", +4),
|
||||||
|
BDT("BDT", +8),
|
||||||
|
BIOT("BIOT", +6),
|
||||||
|
BIT("BIT", -12),
|
||||||
|
BOT("BOT", -4),
|
||||||
|
BRST("BRST", -2),
|
||||||
|
BRT("BRT", -3),
|
||||||
|
BST_BANGLADESH("BST", +6, long = "BST_BANGLADESH"),
|
||||||
|
BST_BOUGAINVILLE("BST", +11, long = "BOUGAINVILLE"),
|
||||||
|
BST_BRITISH("BST", +1, long = "BRITISH"),
|
||||||
|
BTT("BTT", +6),
|
||||||
|
CAT("CAT", +2),
|
||||||
|
CCT("CCT", +6, 30),
|
||||||
|
CDT_AMERICA("CDT", -5, long = "CDT_AMERICA"),
|
||||||
|
CDT_CUBA("CDT", -4, long = "CDT_CUBA"),
|
||||||
|
CHADT("CHADT", +13, 45),
|
||||||
|
CHAST("CHAST", +12, 45),
|
||||||
|
CHOT("CHOT", +8),
|
||||||
|
CHOST("CHOST", +9),
|
||||||
|
CHST("CHST", +10),
|
||||||
|
CHUT("CHUT", +10),
|
||||||
|
CIST("CIST", -8),
|
||||||
|
CIT("CIT", +8),
|
||||||
|
CKT("CKT", -10),
|
||||||
|
CLST("CLST", -3),
|
||||||
|
CLT("CLT", -4),
|
||||||
|
COST("COST", -4),
|
||||||
|
COT("COT", -5),
|
||||||
|
CST_AMERICA("CST", -6, long = "CST_AMERICA"),
|
||||||
|
CST_CHINA("CST", +8, long = "CST_CHINA"),
|
||||||
|
CST_CUBA("CST", -5, long = "CST_CUBA"),
|
||||||
|
CT("CT", +8),
|
||||||
|
CVT("CVT", -1),
|
||||||
|
CWST("CWST", +8, 45),
|
||||||
|
CXT("CXT", +7),
|
||||||
|
DAVT("DAVT", +7),
|
||||||
|
DDUT("DDUT", +10),
|
||||||
|
DFT("DFT", +1),
|
||||||
|
EASST("EASST", -5),
|
||||||
|
EAST("EAST", -6),
|
||||||
|
EAT("EAT", +3),
|
||||||
|
ECT_CARIBBEAN("ECT", -4, long = "ECT_CARIBBEAN"),
|
||||||
|
ECT_ECUADOR("ECT", -5, long = "ECT_ECUADOR"),
|
||||||
|
EDT("EDT", -4),
|
||||||
|
EEST("EEST", +3),
|
||||||
|
EET("EET", +2),
|
||||||
|
EGST("EGST", 0),
|
||||||
|
EGT("EGT", -1),
|
||||||
|
EIT("EIT", +9),
|
||||||
|
EST("EST", -5),
|
||||||
|
FET("FET", +3),
|
||||||
|
FJT("FJT", +12),
|
||||||
|
FKST("FKST", -3),
|
||||||
|
FKT("FKT", -4),
|
||||||
|
FNT("FNT", -2),
|
||||||
|
GALT("GALT", -6),
|
||||||
|
GAMT("GAMT", -9),
|
||||||
|
GET("GET", +4),
|
||||||
|
GFT("GFT", -3),
|
||||||
|
GILT("GILT", +12),
|
||||||
|
GIT("GIT", -9),
|
||||||
|
GST_GEORGIA("GST", -2, long = "GST_GEORGIA"),
|
||||||
|
GST_GULF("GST", +4, long = "GST_GULF"),
|
||||||
|
GYT("GYT", -4),
|
||||||
|
HDT("HDT", -9),
|
||||||
|
HAEC("HAEC", +2),
|
||||||
|
HST("HST", -10),
|
||||||
|
HKT("HKT", +8),
|
||||||
|
HMT("HMT", +5),
|
||||||
|
HOVST("HOVST", +8),
|
||||||
|
HOVT("HOVT", +7),
|
||||||
|
ICT("ICT", +7),
|
||||||
|
IDLW("IDLW", -12),
|
||||||
|
IDT("IDT", +3),
|
||||||
|
IOT("IOT", +3),
|
||||||
|
IRDT("IRDT", +4, 30),
|
||||||
|
IRKT("IRKT", +8),
|
||||||
|
IRST("IRST", +3, 30),
|
||||||
|
IST_INDIA("IST", +5, 30, long = "IST_INDIA"),
|
||||||
|
IST_IRISH("IST", +1, long = "IST_IRISH"),
|
||||||
|
IST_ISRAEL("IST", +2, long = "IST_ISRAEL"),
|
||||||
|
JST("JST", +9),
|
||||||
|
KALT("KALT", +2),
|
||||||
|
KGT("KGT", +6),
|
||||||
|
KOST("KOST", +11),
|
||||||
|
KRAT("KRAT", +7),
|
||||||
|
KST("KST", +9),
|
||||||
|
LHST_STD("LHST", +10, 30, long = "LHST_STD"),
|
||||||
|
LHST_SUMMER("LHST", +11, long = "LHST_SUMMER"),
|
||||||
|
LINT("LINT", +14),
|
||||||
|
MAGT("MAGT", +12),
|
||||||
|
MART("MART", -9, 30),
|
||||||
|
MAWT("MAWT", +5),
|
||||||
|
MDT("MDT", -6),
|
||||||
|
MET("MET", +1),
|
||||||
|
MEST("MEST", +2),
|
||||||
|
MHT("MHT", +12),
|
||||||
|
MIST("MIST", +11),
|
||||||
|
MIT("MIT", -9, 30),
|
||||||
|
MMT("MMT", +6, 30),
|
||||||
|
MSK("MSK", +3),
|
||||||
|
MST_MALAYSIA("MST_", +8, long = "MST_MALAYSIA"),
|
||||||
|
MST_AMERICA("MST", -7, long = "MST_AMERICA"),
|
||||||
|
MUT("MUT", +4),
|
||||||
|
MVT("MVT", +5),
|
||||||
|
MYT("MYT", +8),
|
||||||
|
NCT("NCT", +11),
|
||||||
|
NDT("NDT", -2, 30),
|
||||||
|
NFT("NFT", +11),
|
||||||
|
NOVT("NOVT", +7),
|
||||||
|
NPT("NPT", +5, 45),
|
||||||
|
NST("NST", -3, 30),
|
||||||
|
NT("NT", -3, 30),
|
||||||
|
NUT("NUT", -11),
|
||||||
|
NZDT("NZDT", +13),
|
||||||
|
NZST("NZST", +12),
|
||||||
|
OMST("OMST", +6),
|
||||||
|
ORAT("ORAT", +5),
|
||||||
|
PET("PET", -5),
|
||||||
|
PETT("PETT", +12),
|
||||||
|
PGT("PGT", +10),
|
||||||
|
PHOT("PHOT", +13),
|
||||||
|
PHT("PHT", +8),
|
||||||
|
PKT("PKT", +5),
|
||||||
|
PMDT("PMDT", -2),
|
||||||
|
PMST("PMST", -3),
|
||||||
|
PONT("PONT", +11),
|
||||||
|
PYST("PYST", -3),
|
||||||
|
PYT("PYT", -4),
|
||||||
|
RET("RET", +4),
|
||||||
|
ROTT("ROTT", -3),
|
||||||
|
SAKT("SAKT", +11),
|
||||||
|
SAMT("SAMT", +4),
|
||||||
|
SAST("SAST", +2),
|
||||||
|
SBT("SBT", +11),
|
||||||
|
SCT("SCT", +4),
|
||||||
|
SDT("SDT", -10),
|
||||||
|
SGT("SGT", +8),
|
||||||
|
SLST("SLST", +5, 30),
|
||||||
|
SRET("SRET", +11),
|
||||||
|
SRT("SRT", -3),
|
||||||
|
SST_SAMOA("SST", -11, long = "SST_SAMOA"),
|
||||||
|
SST_SINGAPORE("SST", +8, long = "SST_SINGAPORE"),
|
||||||
|
SYOT("SYOT", +3),
|
||||||
|
TAHT("TAHT", -10),
|
||||||
|
THA("THA", +7),
|
||||||
|
TFT("TFT", +5),
|
||||||
|
TJT("TJT", +5),
|
||||||
|
TKT("TKT", +13),
|
||||||
|
TLT("TLT", +9),
|
||||||
|
TMT("TMT", +5),
|
||||||
|
TRT("TRT", +3),
|
||||||
|
TOT("TOT", +13),
|
||||||
|
TVT("TVT", +12),
|
||||||
|
ULAST("ULAST", +9),
|
||||||
|
ULAT("ULAT", +8),
|
||||||
|
UYST("UYST", -2),
|
||||||
|
UYT("UYT", -3),
|
||||||
|
UZT("UZT", +5),
|
||||||
|
VET("VET", -4),
|
||||||
|
VLAT("VLAT", +10),
|
||||||
|
VOLT("VOLT", +4),
|
||||||
|
VOST("VOST", +6),
|
||||||
|
VUT("VUT", +11),
|
||||||
|
WAKT("WAKT", +12),
|
||||||
|
WAST("WAST", +2),
|
||||||
|
WAT("WAT", +1),
|
||||||
|
WEST("WEST", +1),
|
||||||
|
WET("WET", 0),
|
||||||
|
WIT("WIT", +7),
|
||||||
|
WST("WST", +8),
|
||||||
|
YAKT("YAKT", +9),
|
||||||
|
YEKT("YEKT", +5),
|
||||||
|
;
|
||||||
|
|
||||||
|
constructor(abbr: String, hours: Int, minutes: Int = 0, long: String = abbr) : this(abbr, (hours.hours + minutes.minutes).offset, long)
|
||||||
|
}
|
29
klock/src/commonMain/kotlin/korlibs/time/TimezoneNames.kt
Normal file
29
klock/src/commonMain/kotlin/korlibs/time/TimezoneNames.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.internal.Serializable
|
||||||
|
|
||||||
|
data class TimezoneNames(val timeZones: List<Timezone>) : Serializable {
|
||||||
|
val namesToOffsetsList by lazy { timeZones.groupBy { it.abbr } }
|
||||||
|
val namesToOffsets by lazy { namesToOffsetsList.map { it.key to it.value.first().offset.time }.toMap() }
|
||||||
|
constructor(vararg tz: Timezone) : this(tz.toList())
|
||||||
|
|
||||||
|
operator fun plus(other: TimezoneNames): TimezoneNames = TimezoneNames(this.timeZones + other.timeZones)
|
||||||
|
operator fun get(name: String): Timezone? = namesToOffsetsList[name.uppercase().trim()]?.first()
|
||||||
|
|
||||||
|
/** Some abbreviations collides, so we can get a list of Timezones based on abbreviation */
|
||||||
|
fun getAll(name: String): List<Timezone> = namesToOffsetsList[name.uppercase().trim()] ?: emptyList()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Suppress("MayBeConstant", "unused")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
|
||||||
|
val DEFAULT = TimezoneNames(
|
||||||
|
Timezone.PDT,
|
||||||
|
Timezone.PST,
|
||||||
|
Timezone.GMT,
|
||||||
|
Timezone.UTC,
|
||||||
|
Timezone.CET,
|
||||||
|
Timezone.CEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
69
klock/src/commonMain/kotlin/korlibs/time/TimezoneOffset.kt
Normal file
69
klock/src/commonMain/kotlin/korlibs/time/TimezoneOffset.kt
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.internal.KlockInternal
|
||||||
|
import korlibs.time.internal.MILLIS_PER_MINUTE
|
||||||
|
import korlibs.time.internal.Serializable
|
||||||
|
import korlibs.time.internal.padded
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a time zone offset with millisecond precision. Usually minute is enough.
|
||||||
|
* Can be used along [DateTimeTz] to construct non universal, local times.
|
||||||
|
*
|
||||||
|
* This class is inlined so no boxing should be required.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class TimezoneOffset(
|
||||||
|
/** [TimezoneOffset] in [totalMilliseconds] */
|
||||||
|
val totalMilliseconds: Double
|
||||||
|
) : Comparable<TimezoneOffset>, Serializable {
|
||||||
|
/** Returns whether this [TimezoneOffset] has a positive component */
|
||||||
|
val positive: Boolean get() = totalMilliseconds >= 0.0
|
||||||
|
|
||||||
|
/** [TimeSpan] time for this [TimezoneOffset] */
|
||||||
|
val time get() = totalMilliseconds.milliseconds
|
||||||
|
|
||||||
|
/** [TimezoneOffset] in [totalMinutes] */
|
||||||
|
val totalMinutes: Double get() = totalMilliseconds / MILLIS_PER_MINUTE
|
||||||
|
|
||||||
|
/** [TimezoneOffset] in [totalMinutes] as integer */
|
||||||
|
val totalMinutesInt get() = totalMinutes.toInt()
|
||||||
|
|
||||||
|
/** Returns a string representation of this [TimezoneOffset] */
|
||||||
|
val timeZone: String get() {
|
||||||
|
val sign = if (positive) "+" else "-"
|
||||||
|
val hour = deltaHoursAbs.padded(2)
|
||||||
|
val minute = deltaMinutesAbs.padded(2)
|
||||||
|
return if (time == 0.minutes) "UTC" else "GMT$sign$hour$minute"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val deltaTotalMinutesAbs: Int get() = abs(totalMinutes.toInt())
|
||||||
|
internal val deltaHoursAbs: Int get() = deltaTotalMinutesAbs / 60
|
||||||
|
internal val deltaMinutesAbs: Int get() = deltaTotalMinutesAbs % 60
|
||||||
|
|
||||||
|
override fun toString(): String = timeZone
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val UTC = TimezoneOffset(0.0)
|
||||||
|
|
||||||
|
@Suppress("MayBeConstant", "unused")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
|
||||||
|
/** Constructs a new [TimezoneOffset] from a [TimeSpan]. */
|
||||||
|
operator fun invoke(time: TimeSpan) = TimezoneOffset(time.milliseconds)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns timezone offset as a [TimeSpan], for a specified [time].
|
||||||
|
* For example, GMT+01 would return 60.minutes.
|
||||||
|
* This uses the Operating System to compute daylight offsets when required.
|
||||||
|
*/
|
||||||
|
fun local(time: DateTime): TimezoneOffset = KlockInternal.localTimezoneOffsetMinutes(time).offset
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: TimezoneOffset): Int = totalMilliseconds.compareTo(other.totalMilliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A [TimeSpan] as a [TimezoneOffset]. */
|
||||||
|
val TimeSpan.offset get() = TimezoneOffset(this)
|
144
klock/src/commonMain/kotlin/korlibs/time/Year.kt
Normal file
144
klock/src/commonMain/kotlin/korlibs/time/Year.kt
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.internal.Serializable
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Year in a typed way.
|
||||||
|
*
|
||||||
|
* A year is a set of 365 days or 366 for leap years.
|
||||||
|
* It is the time it takes the earth to fully orbit the sun.
|
||||||
|
*
|
||||||
|
* The integrated model is capable of determine if a year is leap for years 1 until 9999 inclusive.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class Year(val year: Int) : Comparable<Year>, Serializable {
|
||||||
|
companion object {
|
||||||
|
@Suppress("MayBeConstant", "unused")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Year instance checking that the year is between 1 and 9999 inclusive.
|
||||||
|
*
|
||||||
|
* It throws a [DateException] if the year is not in the 1..9999 range.
|
||||||
|
*/
|
||||||
|
fun checked(year: Int) = year.apply { if (year !in 1..9999) throw DateException("Year $year not in 1..9999") }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a year is leap checking that the year is between 1..9999 or throwing a [DateException] when outside that range.
|
||||||
|
*/
|
||||||
|
fun isLeapChecked(year: Int): Boolean = isLeap(checked(year))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a year is leap. The model works for years between 1..9999. Outside this range, the result might be invalid.
|
||||||
|
*/
|
||||||
|
fun isLeap(year: Int): Boolean = (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the year from the number of days since 0001-01-01.
|
||||||
|
*/
|
||||||
|
fun fromDays(days: Int): Year {
|
||||||
|
// https://en.wikipedia.org/wiki/Leap_year#Algorithm
|
||||||
|
// Each 400 years the modular cycle is completed
|
||||||
|
|
||||||
|
val v400 = days / DAYS_PER_400_YEARS
|
||||||
|
val r400 = days - (v400 * DAYS_PER_400_YEARS)
|
||||||
|
|
||||||
|
val v100 = min(r400 / DAYS_PER_100_YEARS, 3)
|
||||||
|
val r100 = r400 - (v100 * DAYS_PER_100_YEARS)
|
||||||
|
|
||||||
|
val v4 = r100 / DAYS_PER_4_YEARS
|
||||||
|
val r4 = r100 - (v4 * DAYS_PER_4_YEARS)
|
||||||
|
|
||||||
|
val v1 = min(r4 / DAYS_COMMON, 3)
|
||||||
|
|
||||||
|
val extra = if (days < 0) 0 else 1
|
||||||
|
//val extra = 1
|
||||||
|
|
||||||
|
return Year(extra + v1 + (v4 * 4) + (v100 * 100) + (v400 * 400))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of days of a year depending on being leap or not.
|
||||||
|
* Normal, non leap years contain 365 days, while leap ones 366.
|
||||||
|
*/
|
||||||
|
fun days(isLeap: Boolean) = if (isLeap) DAYS_LEAP else DAYS_COMMON
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of leap years that happened between 1 and the specified [year].
|
||||||
|
*/
|
||||||
|
fun leapCountSinceOne(year: Int): Int {
|
||||||
|
if (year < 1) {
|
||||||
|
// @TODO: Hack for negative numbers. Maybe we can do some kind of offset and subtract.
|
||||||
|
var leapCount = 0
|
||||||
|
var y = 1
|
||||||
|
while (y >= year) {
|
||||||
|
if (Year(y).isLeap) leapCount--
|
||||||
|
y--
|
||||||
|
}
|
||||||
|
return leapCount
|
||||||
|
}
|
||||||
|
val y1 = (year - 1)
|
||||||
|
val res = (y1 / 4) - (y1 / 100) + (y1 / 400)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of days since 1 and the beginning of the specified [year].
|
||||||
|
*/
|
||||||
|
fun daysSinceOne(year: Int): Int = DAYS_COMMON * (year - 1) + leapCountSinceOne(year)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of days in a normal year.
|
||||||
|
*/
|
||||||
|
const val DAYS_COMMON = 365
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of days in a leap year.
|
||||||
|
*/
|
||||||
|
const val DAYS_LEAP = 366
|
||||||
|
|
||||||
|
private const val LEAP_PER_4_YEARS = 1
|
||||||
|
private const val LEAP_PER_100_YEARS = 24 // 24 or 25 (25 the first chunk)
|
||||||
|
private const val LEAP_PER_400_YEARS = 97
|
||||||
|
|
||||||
|
private const val DAYS_PER_4_YEARS = 4 * DAYS_COMMON + LEAP_PER_4_YEARS
|
||||||
|
private const val DAYS_PER_100_YEARS = 100 * DAYS_COMMON + LEAP_PER_100_YEARS
|
||||||
|
private const val DAYS_PER_400_YEARS = 400 * DAYS_COMMON + LEAP_PER_400_YEARS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this year is leap checking that the year is between 1..9999 or throwing a [DateException] when outside that range.
|
||||||
|
*/
|
||||||
|
val isLeapChecked get() = Year.isLeapChecked(year)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this year is leap. The model works for years between 1..9999. Outside this range, the result might be invalid.
|
||||||
|
*/
|
||||||
|
val isLeap get() = Year.isLeap(year)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total days of this year, 365 (non leap) [DAYS_COMMON] or 366 (leap) [DAYS_LEAP].
|
||||||
|
*/
|
||||||
|
val days: Int get() = Year.days(isLeap)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of leap years since the year 1 (without including this one)
|
||||||
|
*/
|
||||||
|
val leapCountSinceOne: Int get() = leapCountSinceOne(year)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of days since year 1 to reach this year
|
||||||
|
*/
|
||||||
|
val daysSinceOne: Int get() = daysSinceOne(year)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two years.
|
||||||
|
*/
|
||||||
|
override fun compareTo(other: Year): Int = this.year.compareTo(other.year)
|
||||||
|
|
||||||
|
operator fun plus(delta: Int): Year = Year(year + delta)
|
||||||
|
operator fun minus(delta: Int): Year = Year(year - delta)
|
||||||
|
operator fun minus(other: Year): Int = this.year - other.year
|
||||||
|
}
|
65
klock/src/commonMain/kotlin/korlibs/time/YearMonth.kt
Normal file
65
klock/src/commonMain/kotlin/korlibs/time/YearMonth.kt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.internal.Serializable
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a couple of [year] and [month].
|
||||||
|
*
|
||||||
|
* It is packed in a value class wrapping an Int to prevent allocations.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class YearMonth(internal val internalPackedInfo: Int) : Serializable {
|
||||||
|
companion object {
|
||||||
|
@Suppress("MayBeConstant", "unused")
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
|
||||||
|
/** Constructs a new [YearMonth] from the [year] and [month] components. */
|
||||||
|
operator fun invoke(year: Year, month: Month) = YearMonth(year.year, month.index1)
|
||||||
|
/** Constructs a new [YearMonth] from the [year] and [month] components. */
|
||||||
|
operator fun invoke(year: Int, month: Month) = YearMonth(year, month.index1)
|
||||||
|
/** Constructs a new [YearMonth] from the [year] and [month] components. */
|
||||||
|
operator fun invoke(year: Int, month1: Int) = YearMonth((year shl 4) or (month1 and 15))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The [year] part. */
|
||||||
|
val year: Year get() = Year(yearInt)
|
||||||
|
/** The [year] part as [Int]. */
|
||||||
|
val yearInt: Int get() = internalPackedInfo ushr 4
|
||||||
|
|
||||||
|
/** The [month] part. */
|
||||||
|
val month: Month get() = Month[month1]
|
||||||
|
/** The [month] part as [Int] where [Month.January] is 1. */
|
||||||
|
val month1: Int get() = internalPackedInfo and 15
|
||||||
|
|
||||||
|
/** Days in this [month] of this [year]. */
|
||||||
|
val days: Int get() = month.days(year)
|
||||||
|
/** Number of days since the start of the [year] to reach this [month]. */
|
||||||
|
val daysToStart: Int get() = month.daysToStart(year)
|
||||||
|
/** Number of days since the start of the [year] to reach next [month]. */
|
||||||
|
val daysToEnd: Int get() = month.daysToEnd(year)
|
||||||
|
|
||||||
|
operator fun plus(span: MonthSpan): YearMonth {
|
||||||
|
val newMonth = this.month1 + span.months
|
||||||
|
val yearAdjust = when {
|
||||||
|
newMonth > 12 -> +1
|
||||||
|
newMonth < 1 -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
return YearMonth(Year(this.yearInt + span.years + yearAdjust), Month[newMonth])
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minus(span: MonthSpan): YearMonth = this + (-span)
|
||||||
|
|
||||||
|
override fun toString(): String = "$month $yearInt"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [YearMonth] representing [this] year and this [month].
|
||||||
|
*/
|
||||||
|
fun Year.withMonth(month: Month) = YearMonth(this, month)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [YearMonth] representing this [year] and [this] month.
|
||||||
|
*/
|
||||||
|
fun Month.withYear(year: Year) = YearMonth(year, this)
|
107
klock/src/commonMain/kotlin/korlibs/time/_TImeBenchmark.kt
Normal file
107
klock/src/commonMain/kotlin/korlibs/time/_TImeBenchmark.kt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package korlibs.time.benchmark
|
||||||
|
|
||||||
|
import korlibs.time.Stopwatch
|
||||||
|
import korlibs.time.measureTime
|
||||||
|
import korlibs.time.milliseconds
|
||||||
|
import korlibs.time.nanoseconds
|
||||||
|
import kotlin.jvm.JvmName
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class BenchmarkResult(
|
||||||
|
val timePerCallNanoseconds: Long,
|
||||||
|
val maxDeviationNanoseconds: Long,
|
||||||
|
val partialResults: List<PartialResult>,
|
||||||
|
val dummyResult: Double
|
||||||
|
) {
|
||||||
|
data class PartialResult(val nanos: Long, val iters: Long) {
|
||||||
|
val nanosPerIter get() = nanos.toDouble() / iters.toDouble()
|
||||||
|
|
||||||
|
override fun toString(): String = "PartialResult(nanosPerIter=$nanosPerIter, nanos=$nanos, iters=$iters)"
|
||||||
|
}
|
||||||
|
|
||||||
|
val timePerCallMicroseconds: Double get() = timePerCallNanoseconds / 1000.0
|
||||||
|
val maxDeviationMicroseconds: Double get() = maxDeviationNanoseconds / 1000.0
|
||||||
|
|
||||||
|
val timePerCall get() = timePerCallNanoseconds.nanoseconds
|
||||||
|
val maxDeviation get() = maxDeviationNanoseconds.nanoseconds
|
||||||
|
|
||||||
|
private fun rounded(value: Double) = (value * 10000).toLong() / 10000.0
|
||||||
|
|
||||||
|
override fun toString(): String = "${rounded(timePerCallMicroseconds)} µs ± ${rounded(maxDeviationMicroseconds)} µs"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("BenchmarkGeneric")
|
||||||
|
inline fun <reified T> benchmark(noinline block: () -> T): BenchmarkResult {
|
||||||
|
return when (T::class) {
|
||||||
|
Unit::class -> benchmark { 1.0.also { block() } }
|
||||||
|
Int::class -> benchmark { (block() as Int).toDouble() }
|
||||||
|
Long::class -> benchmark { (block() as Long).toDouble() }
|
||||||
|
Double::class -> benchmark { block() as Double }
|
||||||
|
else -> benchmark { 1.0.also { block() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun measureScale(block: () -> Double): Int {
|
||||||
|
var sum = 0.0
|
||||||
|
sum += block()
|
||||||
|
for (scale in 0..9) {
|
||||||
|
val iters = 10.0.pow(scale).toInt()
|
||||||
|
val time = measureTime {
|
||||||
|
for (n in 0 until iters) {
|
||||||
|
sum += block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum *= 0.1
|
||||||
|
if (time >= 10.milliseconds) return iters + (sum * 0.00000000000001 * 0.00000000000001 * 0.00000000000001 * 0.00000000000001).toInt()
|
||||||
|
}
|
||||||
|
return Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun benchmark(block: () -> Double): BenchmarkResult {
|
||||||
|
val stopwatch = Stopwatch()
|
||||||
|
var dummySum = 0.0
|
||||||
|
dummySum += measureScale(block)
|
||||||
|
val itersToGetAtLeast10Ms = measureScale(block)
|
||||||
|
//println("SCALE: $itersToGetAtLeast10Ms")
|
||||||
|
|
||||||
|
val allResults = ArrayList<BenchmarkResult.PartialResult>()
|
||||||
|
|
||||||
|
for (n in 0 until 200) {
|
||||||
|
stopwatch.start()
|
||||||
|
for (m in 0 until itersToGetAtLeast10Ms) dummySum += block()
|
||||||
|
val time = stopwatch.elapsedNanoseconds
|
||||||
|
allResults += BenchmarkResult.PartialResult(time.toLong(), (itersToGetAtLeast10Ms).toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = allResults.drop(allResults.size / 2)
|
||||||
|
|
||||||
|
val ftotalNanoseconds = results.map { it.nanos }.sum().toDouble()
|
||||||
|
val ftotalIters = results.map { it.iters }.sum().toDouble()
|
||||||
|
val fminNanoseconds = results.map { it.nanosPerIter }.minOrNull()!!
|
||||||
|
val fmaxNanoseconds = results.map { it.nanosPerIter }.maxOrNull()!!
|
||||||
|
//for (res in results) println("res: $res")
|
||||||
|
|
||||||
|
return BenchmarkResult(
|
||||||
|
(ftotalNanoseconds / ftotalIters).toLong(),
|
||||||
|
(fmaxNanoseconds - fminNanoseconds).toLong(),
|
||||||
|
results,
|
||||||
|
dummySum
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> printBenchmark(name: String, full: Boolean = false, noinline block: () -> T) {
|
||||||
|
val result = benchmark(block)
|
||||||
|
println("Benchmark '$name' : $result")
|
||||||
|
if (full) {
|
||||||
|
for (r in result.partialResults) println(" - $r")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Show ratios
|
||||||
|
fun printBenchmarks(vararg benchmarks: Pair<String, () -> Double>, full: Boolean = false) {
|
||||||
|
for ((name, block) in benchmarks) {
|
||||||
|
printBenchmark(name, full, block)
|
||||||
|
}
|
||||||
|
}
|
219
klock/src/commonMain/kotlin/korlibs/time/_Time.internal.kt
Normal file
219
klock/src/commonMain/kotlin/korlibs/time/_Time.internal.kt
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
import kotlin.jvm.*
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
internal inline fun Int.chainComparison(comparer: () -> Int): Int = if (this == 0) comparer() else this
|
||||||
|
|
||||||
|
internal inline fun <T> List<T>.fastForEach(callback: (T) -> Unit) {
|
||||||
|
var n = 0
|
||||||
|
while (n < size) callback(this[n++])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original implementation grabbed from Kds to prevent additional dependencies:
|
||||||
|
// - https://github.com/korlibs/kds/blob/965f6017d7ad82e4bad714acf26cd7189186bdb3/kds/src/commonMain/kotlin/korlibs/datastructure/_Extensions.kt#L48
|
||||||
|
internal inline fun genericBinarySearch(
|
||||||
|
fromIndex: Int,
|
||||||
|
toIndex: Int,
|
||||||
|
invalid: (from: Int, to: Int, low: Int, high: Int) -> Int = { from, to, low, high -> -low - 1 },
|
||||||
|
check: (index: Int) -> Int
|
||||||
|
): Int {
|
||||||
|
var low = fromIndex
|
||||||
|
var high = toIndex - 1
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
val mid = (low + high) / 2
|
||||||
|
val mval = check(mid)
|
||||||
|
|
||||||
|
when {
|
||||||
|
mval < 0 -> low = mid + 1
|
||||||
|
mval > 0 -> high = mid - 1
|
||||||
|
else -> return mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return invalid(fromIndex, toIndex, low, high)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
internal value class BSearchResult(val raw: Int) {
|
||||||
|
val found: Boolean get() = raw >= 0
|
||||||
|
val index: Int get() = if (found) raw else -1
|
||||||
|
val nearIndex: Int get() = if (found) raw else -raw - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal const val MILLIS_PER_SECOND = 1000
|
||||||
|
internal const val MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60 // 60_000
|
||||||
|
internal const val MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60 // 3600_000
|
||||||
|
internal const val MILLIS_PER_DAY = MILLIS_PER_HOUR * 24 // 86400_000
|
||||||
|
internal const val MILLIS_PER_WEEK = MILLIS_PER_DAY * 7 // 604800_000
|
||||||
|
internal const val MILLIS_PER_MICROSECOND = 1.0 / 1000.0
|
||||||
|
internal const val MILLIS_PER_NANOSECOND = MILLIS_PER_MICROSECOND / 1000.0
|
||||||
|
|
||||||
|
internal fun Int.padded(count: Int): String {
|
||||||
|
// @TODO: Handle edge case Int.MIN_VALUE that could not be represented as abs
|
||||||
|
val res = this.absoluteValue.toString().padStart(count, '0')
|
||||||
|
return if (this < 0) return "-$res" else res
|
||||||
|
}
|
||||||
|
internal fun Double.padded(intCount: Int, decCount: Int): String {
|
||||||
|
val intPart = floor(this).toInt()
|
||||||
|
val decPart = round((this - intPart) * 10.0.pow(decCount)).toInt()
|
||||||
|
return "${intPart.padded(intCount).substr(-intCount, intCount)}.${decPart.toString().padStart(decCount, '0').substr(-decCount)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun String.substr(start: Int, length: Int = this.length): String {
|
||||||
|
val low = (if (start >= 0) start else this.length + start).coerceIn(0, this.length)
|
||||||
|
val high = (if (length >= 0) low + length else this.length + length).coerceIn(0, this.length)
|
||||||
|
return if (high < low) "" else this.substring(low, high)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun String.splitKeep(regex: Regex): List<String> {
|
||||||
|
val str = this
|
||||||
|
val out = arrayListOf<String>()
|
||||||
|
var lastPos = 0
|
||||||
|
for (part in regex.findAll(this)) {
|
||||||
|
val prange = part.range
|
||||||
|
if (lastPos != prange.start) {
|
||||||
|
out += str.substring(lastPos, prange.start)
|
||||||
|
}
|
||||||
|
out += str.substring(prange)
|
||||||
|
lastPos = prange.endInclusive + 1
|
||||||
|
}
|
||||||
|
if (lastPos != str.length) {
|
||||||
|
out += str.substring(lastPos)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
internal class Moduler(val value: Double) {
|
||||||
|
private var avalue = abs(value)
|
||||||
|
private val sign = sign(value)
|
||||||
|
|
||||||
|
fun double(count: Double): Double {
|
||||||
|
val ret = (avalue / count)
|
||||||
|
avalue %= count
|
||||||
|
return floor(ret) * sign
|
||||||
|
}
|
||||||
|
fun double(count: Int): Double = double(count.toDouble())
|
||||||
|
fun double(count: Float): Double = double(count.toDouble())
|
||||||
|
|
||||||
|
fun int(count: Double): Int = double(count).toInt()
|
||||||
|
fun int(count: Int): Int = int(count.toDouble())
|
||||||
|
fun int(count: Float): Int = int(count.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal expect object KlockInternal {
|
||||||
|
val currentTime: Double
|
||||||
|
val now: TimeSpan
|
||||||
|
fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan
|
||||||
|
fun sleep(time: TimeSpan)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect interface Serializable
|
||||||
|
|
||||||
|
internal fun <K> MutableMap<K, Int>.increment(key: K) {
|
||||||
|
this.getOrPut(key) { 0 }
|
||||||
|
this[key] = this[key]!! + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MicroStrReader(val str: String, var offset: Int = 0) {
|
||||||
|
val length get() = str.length
|
||||||
|
val available get() = str.length - offset
|
||||||
|
val hasMore get() = offset < str.length
|
||||||
|
val eof get() = !hasMore
|
||||||
|
inline fun readChunk(callback: () -> Unit): String {
|
||||||
|
val start = this.offset
|
||||||
|
callback()
|
||||||
|
val end = this.offset
|
||||||
|
return this.str.substring(start, end)
|
||||||
|
}
|
||||||
|
fun peekCharOrZero(): Char = if (hasMore) str[offset] else '\u0000'
|
||||||
|
fun peekChar(): Char = str[offset]
|
||||||
|
fun readChar(): Char = str[offset++]
|
||||||
|
fun tryRead(expected: Char): Boolean {
|
||||||
|
if (eof || peekChar() != expected) return false
|
||||||
|
readChar()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fun tryReadOrNull(expected: String): String? {
|
||||||
|
return if (tryRead(expected)) expected else null
|
||||||
|
}
|
||||||
|
fun tryRead(expected: String): Boolean {
|
||||||
|
if (expected.length > available) return false
|
||||||
|
for (n in expected.indices) if (this.str[offset + n] != expected[n]) return false
|
||||||
|
offset += expected.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fun read(count: Int): String = this.str.substring(offset, (offset + count).coerceAtMost(length)).also { this.offset += it.length }
|
||||||
|
fun readRemaining(): String = read(available)
|
||||||
|
fun readInt(count: Int): Int = read(count).toInt()
|
||||||
|
fun tryReadInt(count: Int): Int? = read(count).toIntOrNull()
|
||||||
|
fun tryReadDouble(count: Int): Double? = read(count).replace(',', '.').toDoubleOrNull()
|
||||||
|
|
||||||
|
fun tryReadDouble(): Double? {
|
||||||
|
var numCount = 0
|
||||||
|
var num = 0
|
||||||
|
var denCount = 0
|
||||||
|
var den = 0
|
||||||
|
var decimals = false
|
||||||
|
loop@while (hasMore) {
|
||||||
|
when (val pc = peekChar()) {
|
||||||
|
',' -> {
|
||||||
|
if (numCount == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
decimals = true
|
||||||
|
readChar()
|
||||||
|
}
|
||||||
|
in '0'..'9' -> {
|
||||||
|
val c = readChar()
|
||||||
|
if (decimals) {
|
||||||
|
denCount++
|
||||||
|
den *= 10
|
||||||
|
den += (c - '0')
|
||||||
|
} else {
|
||||||
|
numCount++
|
||||||
|
num *= 10
|
||||||
|
num += (c - '0')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
break@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (numCount == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return num.toDouble() + (den.toDouble() * 10.0.pow(-denCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun spinlock(time: TimeSpan) {
|
||||||
|
val start = TimeSpan.now()
|
||||||
|
while (TimeSpan.now() - start < time) Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun MicroStrReader.readTimeZoneOffset(tzNames: TimezoneNames = TimezoneNames.DEFAULT): TimeSpan? {
|
||||||
|
val reader = this
|
||||||
|
for ((name, offset) in tzNames.namesToOffsets) {
|
||||||
|
if (name == "GMT" || name == "UTC") continue
|
||||||
|
if (reader.tryRead(name)) return offset
|
||||||
|
}
|
||||||
|
if (reader.tryRead('Z')) return 0.minutes
|
||||||
|
var sign = +1
|
||||||
|
reader.tryRead("GMT")
|
||||||
|
reader.tryRead("UTC")
|
||||||
|
if (reader.tryRead("+")) sign = +1
|
||||||
|
if (reader.tryRead("-")) sign = -1
|
||||||
|
val part = reader.readRemaining().replace(":", "")
|
||||||
|
val hours = part.substr(0, 2).padStart(2, '0').toIntOrNull() ?: return null
|
||||||
|
val minutes = part.substr(2, 2).padStart(2, '0').toIntOrNull() ?: return null
|
||||||
|
val roffset = hours.hours + minutes.minutes
|
||||||
|
return if (sign > 0) +roffset else -roffset
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
internal infix fun Int.umod(other: Int): Int {
|
||||||
|
val rm = this % other
|
||||||
|
val remainder = if (rm == -0) 0 else rm
|
||||||
|
return when {
|
||||||
|
remainder < 0 -> remainder + other
|
||||||
|
else -> remainder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal infix fun Double.umod(other: Double): Double {
|
||||||
|
val rm = this % other
|
||||||
|
val remainder = if (rm == -0.0) 0.0 else rm
|
||||||
|
return when {
|
||||||
|
remainder < 0.0 -> remainder + other
|
||||||
|
else -> remainder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal infix fun Int.div2(other: Int): Int = when {
|
||||||
|
this < 0 || this % other == 0 -> this / other
|
||||||
|
else -> (this / other) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Int.cycle(min: Int, max: Int): Int = ((this - min) umod (max - min + 1)) + min
|
||||||
|
internal fun Int.cycleSteps(min: Int, max: Int): Int = (this - min) / (max - min + 1)
|
||||||
|
internal fun Double.toInt2(): Int = if (this < 0.0) floor(this).toInt() else this.toInt()
|
||||||
|
internal fun Double.toIntMod(mod: Int): Int = (this umod mod.toDouble()).toInt2()
|
@ -0,0 +1,218 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
|
||||||
|
// @TODO: Should we include the most popular timezones increasing the artifact size? Maybe include a plugin mechanism and a registration in klock-locale?
|
||||||
|
// @TODO: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
|
||||||
|
|
||||||
|
private var ExtendedTimezoneNamesOrNull: TimezoneNames? = null
|
||||||
|
|
||||||
|
val ExtendedTimezoneNames: TimezoneNames get() {
|
||||||
|
if (ExtendedTimezoneNamesOrNull == null) {
|
||||||
|
ExtendedTimezoneNamesOrNull = TimezoneNames(
|
||||||
|
Timezone.ACDT,
|
||||||
|
Timezone.ACST,
|
||||||
|
Timezone.ACT,
|
||||||
|
Timezone.ACWST,
|
||||||
|
Timezone.ADT,
|
||||||
|
Timezone.AEDT,
|
||||||
|
Timezone.AEST,
|
||||||
|
Timezone.AFT,
|
||||||
|
Timezone.AKDT,
|
||||||
|
Timezone.AKST,
|
||||||
|
Timezone.ALMT,
|
||||||
|
Timezone.AMST,
|
||||||
|
Timezone.AMT_ARMENIA,
|
||||||
|
Timezone.AMT_BRAZIL,
|
||||||
|
Timezone.ANAT,
|
||||||
|
Timezone.AQTT,
|
||||||
|
Timezone.ART,
|
||||||
|
Timezone.AST_ARABIA,
|
||||||
|
Timezone.AST_ATLANTIC,
|
||||||
|
Timezone.AWST,
|
||||||
|
Timezone.AZOST,
|
||||||
|
Timezone.AZOT,
|
||||||
|
Timezone.AZT,
|
||||||
|
Timezone.BDT,
|
||||||
|
Timezone.BIOT,
|
||||||
|
Timezone.BIT,
|
||||||
|
Timezone.BOT,
|
||||||
|
Timezone.BRST,
|
||||||
|
Timezone.BRT,
|
||||||
|
Timezone.BST_BOUGAINVILLE,
|
||||||
|
Timezone.BST_BANGLADESH,
|
||||||
|
Timezone.BST_BRITISH,
|
||||||
|
Timezone.BTT,
|
||||||
|
Timezone.CAT,
|
||||||
|
Timezone.CCT,
|
||||||
|
Timezone.CDT_AMERICA,
|
||||||
|
Timezone.CDT_CUBA,
|
||||||
|
Timezone.CEST,
|
||||||
|
Timezone.CET,
|
||||||
|
Timezone.CHADT,
|
||||||
|
Timezone.CHAST,
|
||||||
|
Timezone.CHOT,
|
||||||
|
Timezone.CHOST,
|
||||||
|
Timezone.CHST,
|
||||||
|
Timezone.CHUT,
|
||||||
|
Timezone.CIST,
|
||||||
|
Timezone.CIT,
|
||||||
|
Timezone.CKT,
|
||||||
|
Timezone.CLST,
|
||||||
|
Timezone.CLT,
|
||||||
|
Timezone.COST,
|
||||||
|
Timezone.COT,
|
||||||
|
Timezone.CST_AMERICA,
|
||||||
|
Timezone.CST_CHINA,
|
||||||
|
Timezone.CST_CUBA,
|
||||||
|
Timezone.CT,
|
||||||
|
Timezone.CVT,
|
||||||
|
Timezone.CWST,
|
||||||
|
Timezone.CXT,
|
||||||
|
Timezone.DAVT,
|
||||||
|
Timezone.DDUT,
|
||||||
|
Timezone.DFT,
|
||||||
|
Timezone.EASST,
|
||||||
|
Timezone.EAST,
|
||||||
|
Timezone.EAT,
|
||||||
|
Timezone.ECT_CARIBBEAN,
|
||||||
|
Timezone.ECT_ECUADOR,
|
||||||
|
Timezone.EDT,
|
||||||
|
Timezone.EEST,
|
||||||
|
Timezone.EET,
|
||||||
|
Timezone.EGST,
|
||||||
|
Timezone.EGT,
|
||||||
|
Timezone.EIT,
|
||||||
|
Timezone.EST,
|
||||||
|
Timezone.FET,
|
||||||
|
Timezone.FJT,
|
||||||
|
Timezone.FKST,
|
||||||
|
Timezone.FKT,
|
||||||
|
Timezone.FNT,
|
||||||
|
Timezone.GALT,
|
||||||
|
Timezone.GAMT,
|
||||||
|
Timezone.GET,
|
||||||
|
Timezone.GFT,
|
||||||
|
Timezone.GILT,
|
||||||
|
Timezone.GIT,
|
||||||
|
Timezone.GMT,
|
||||||
|
Timezone.GST_GEORGIA,
|
||||||
|
Timezone.GST_GULF,
|
||||||
|
Timezone.GYT,
|
||||||
|
Timezone.HDT,
|
||||||
|
Timezone.HAEC,
|
||||||
|
Timezone.HST,
|
||||||
|
Timezone.HKT,
|
||||||
|
Timezone.HMT,
|
||||||
|
Timezone.HOVST,
|
||||||
|
Timezone.HOVT,
|
||||||
|
Timezone.ICT,
|
||||||
|
Timezone.IDLW,
|
||||||
|
Timezone.IDT,
|
||||||
|
Timezone.IOT,
|
||||||
|
Timezone.IRDT,
|
||||||
|
Timezone.IRKT,
|
||||||
|
Timezone.IRST,
|
||||||
|
Timezone.IST_INDIA,
|
||||||
|
Timezone.IST_IRISH,
|
||||||
|
Timezone.IST_ISRAEL,
|
||||||
|
Timezone.JST,
|
||||||
|
Timezone.KALT,
|
||||||
|
Timezone.KGT,
|
||||||
|
Timezone.KOST,
|
||||||
|
Timezone.KRAT,
|
||||||
|
Timezone.KST,
|
||||||
|
Timezone.LHST_STD,
|
||||||
|
Timezone.LHST_SUMMER,
|
||||||
|
Timezone.LINT,
|
||||||
|
Timezone.MAGT,
|
||||||
|
Timezone.MART,
|
||||||
|
Timezone.MAWT,
|
||||||
|
Timezone.MDT,
|
||||||
|
Timezone.MET,
|
||||||
|
Timezone.MEST,
|
||||||
|
Timezone.MHT,
|
||||||
|
Timezone.MIST,
|
||||||
|
Timezone.MIT,
|
||||||
|
Timezone.MMT,
|
||||||
|
Timezone.MSK,
|
||||||
|
Timezone.MST_AMERICA,
|
||||||
|
Timezone.MST_MALAYSIA,
|
||||||
|
Timezone.MUT,
|
||||||
|
Timezone.MVT,
|
||||||
|
Timezone.MYT,
|
||||||
|
Timezone.NCT,
|
||||||
|
Timezone.NDT,
|
||||||
|
Timezone.NFT,
|
||||||
|
Timezone.NOVT,
|
||||||
|
Timezone.NPT,
|
||||||
|
Timezone.NST,
|
||||||
|
Timezone.NT,
|
||||||
|
Timezone.NUT,
|
||||||
|
Timezone.NZDT,
|
||||||
|
Timezone.NZST,
|
||||||
|
Timezone.OMST,
|
||||||
|
Timezone.ORAT,
|
||||||
|
Timezone.PDT,
|
||||||
|
Timezone.PET,
|
||||||
|
Timezone.PETT,
|
||||||
|
Timezone.PGT,
|
||||||
|
Timezone.PHOT,
|
||||||
|
Timezone.PHT,
|
||||||
|
Timezone.PKT,
|
||||||
|
Timezone.PMDT,
|
||||||
|
Timezone.PMST,
|
||||||
|
Timezone.PONT,
|
||||||
|
Timezone.PST,
|
||||||
|
Timezone.PST,
|
||||||
|
Timezone.PYST,
|
||||||
|
Timezone.PYT,
|
||||||
|
Timezone.RET,
|
||||||
|
Timezone.ROTT,
|
||||||
|
Timezone.SAKT,
|
||||||
|
Timezone.SAMT,
|
||||||
|
Timezone.SAST,
|
||||||
|
Timezone.SBT,
|
||||||
|
Timezone.SCT,
|
||||||
|
Timezone.SDT,
|
||||||
|
Timezone.SGT,
|
||||||
|
Timezone.SLST,
|
||||||
|
Timezone.SRET,
|
||||||
|
Timezone.SRT,
|
||||||
|
Timezone.SST_SAMOA,
|
||||||
|
Timezone.SST_SINGAPORE,
|
||||||
|
Timezone.SYOT,
|
||||||
|
Timezone.TAHT,
|
||||||
|
Timezone.THA,
|
||||||
|
Timezone.TFT,
|
||||||
|
Timezone.TJT,
|
||||||
|
Timezone.TKT,
|
||||||
|
Timezone.TLT,
|
||||||
|
Timezone.TMT,
|
||||||
|
Timezone.TRT,
|
||||||
|
Timezone.TOT,
|
||||||
|
Timezone.TVT,
|
||||||
|
Timezone.ULAST,
|
||||||
|
Timezone.ULAT,
|
||||||
|
Timezone.UTC,
|
||||||
|
Timezone.UYST,
|
||||||
|
Timezone.UYT,
|
||||||
|
Timezone.UZT,
|
||||||
|
Timezone.VET,
|
||||||
|
Timezone.VLAT,
|
||||||
|
Timezone.VOLT,
|
||||||
|
Timezone.VOST,
|
||||||
|
Timezone.VUT,
|
||||||
|
Timezone.WAKT,
|
||||||
|
Timezone.WAST,
|
||||||
|
Timezone.WAT,
|
||||||
|
Timezone.WEST,
|
||||||
|
Timezone.WET,
|
||||||
|
Timezone.WIT,
|
||||||
|
Timezone.WST,
|
||||||
|
Timezone.YAKT,
|
||||||
|
Timezone.YEKT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ExtendedTimezoneNamesOrNull!!
|
||||||
|
}
|
81
klock/src/commonMain/kotlin/korlibs/time/locale/de.kt
Normal file
81
klock/src/commonMain/kotlin/korlibs/time/locale/de.kt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
import korlibs.time.KlockLocaleContext
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
|
||||||
|
val KlockLocale.Companion.german get() = GermanKlockLocale
|
||||||
|
|
||||||
|
open class GermanKlockLocale : KlockLocale() {
|
||||||
|
|
||||||
|
companion object : GermanKlockLocale() {
|
||||||
|
private val NUM_TO_ORDINAL: Array<String> = arrayOf(
|
||||||
|
"nullte",
|
||||||
|
"erste",
|
||||||
|
"zweite",
|
||||||
|
"dritte",
|
||||||
|
"vierte",
|
||||||
|
"fünfte",
|
||||||
|
"sechste",
|
||||||
|
"siebte",
|
||||||
|
"achte",
|
||||||
|
"neunte",
|
||||||
|
"zehnte",
|
||||||
|
"elfte",
|
||||||
|
"zwölfte",
|
||||||
|
"dreizehnte",
|
||||||
|
"vierzehnte",
|
||||||
|
"fünfzehnte",
|
||||||
|
"sechzehnte",
|
||||||
|
"siebzehnte",
|
||||||
|
"achtzehnte",
|
||||||
|
"neunzehnte",
|
||||||
|
"zwanzigste",
|
||||||
|
"einundzwanzigste",
|
||||||
|
"zweiundzwanzigste",
|
||||||
|
"dreiundzwanzigste",
|
||||||
|
"vierundzwanzigste",
|
||||||
|
"fünfundzwanzigste",
|
||||||
|
"sechsundzwanzigste",
|
||||||
|
"siebenundzwanzigste",
|
||||||
|
"achtundzwanzigste",
|
||||||
|
"neunundzwanzigste",
|
||||||
|
"dreißigste",
|
||||||
|
"einunddreißigste"
|
||||||
|
)
|
||||||
|
private val ORDINAL_TO_NUM: Map<String, Int> = NUM_TO_ORDINAL.mapIndexed { index, s -> s to index }.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrdinalByDay(day: Int, context: KlockLocaleContext): String {
|
||||||
|
return NUM_TO_ORDINAL.getOrNull(day) ?: error("Invalid numeral")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDayByOrdinal(ordinal: String): Int {
|
||||||
|
return ORDINAL_TO_NUM[ordinal.lowercase()] ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override val ISO639_1 = "de"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("vorm.", "nachm.")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||||
|
"Juli", "August", "September", "Oktober", "November", "Dezember"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd.MM.y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d. MMMM y")
|
||||||
|
override val formatDateLong = format("d. MMMM y")
|
||||||
|
override val formatDateMedium = format("dd.MM.y")
|
||||||
|
override val formatDateShort = format("dd.MM.yy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
35
klock/src/commonMain/kotlin/korlibs/time/locale/es.kt
Normal file
35
klock/src/commonMain/kotlin/korlibs/time/locale/es.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.spanish get() = SpanishKlockLocale
|
||||||
|
|
||||||
|
open class SpanishKlockLocale : KlockLocale() {
|
||||||
|
companion object : SpanishKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "es"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("a.m.", "p.m.")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"enero", "febrero", "marzo", "abril", "mayo", "junio",
|
||||||
|
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd/MM/yyyy HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd/MM/yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d 'de' MMMM 'de' y")
|
||||||
|
override val formatDateLong = format("d 'de' MMMM 'de' y")
|
||||||
|
override val formatDateMedium = format("dd/MM/yyyy")
|
||||||
|
override val formatDateShort = format("dd/MM/yy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
45
klock/src/commonMain/kotlin/korlibs/time/locale/fr.kt
Normal file
45
klock/src/commonMain/kotlin/korlibs/time/locale/fr.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.french get() = FrenchKlockLocale
|
||||||
|
|
||||||
|
open class FrenchKlockLocale : KlockLocale() {
|
||||||
|
companion object : FrenchKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "fr"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("AM", "PM")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"janvier",
|
||||||
|
"février",
|
||||||
|
"mars",
|
||||||
|
"avril",
|
||||||
|
"mai",
|
||||||
|
"juin",
|
||||||
|
"juillet",
|
||||||
|
"août",
|
||||||
|
"septembre",
|
||||||
|
"octobre",
|
||||||
|
"novembre",
|
||||||
|
"décembre"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("d MMM y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd/MM/y HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE d MMMM y")
|
||||||
|
override val formatDateLong = format("d MMMM y")
|
||||||
|
override val formatDateMedium = format("d MMM y")
|
||||||
|
override val formatDateShort = format("dd/MM/y")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
35
klock/src/commonMain/kotlin/korlibs/time/locale/it.kt
Normal file
35
klock/src/commonMain/kotlin/korlibs/time/locale/it.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.italian get() = ItalianKlockLocale
|
||||||
|
|
||||||
|
open class ItalianKlockLocale : KlockLocale() {
|
||||||
|
companion object : ItalianKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "it"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("AM", "PM")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno",
|
||||||
|
"luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd MMM y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd/MM/yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE d MMMM y")
|
||||||
|
override val formatDateLong = format("d MMMM y")
|
||||||
|
override val formatDateMedium = format("dd MMM y")
|
||||||
|
override val formatDateShort = format("dd/MM/yy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
37
klock/src/commonMain/kotlin/korlibs/time/locale/ja.kt
Normal file
37
klock/src/commonMain/kotlin/korlibs/time/locale/ja.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.japanese get() = JapaneseKlockLocale
|
||||||
|
|
||||||
|
open class JapaneseKlockLocale : KlockLocale() {
|
||||||
|
companion object : JapaneseKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "ja"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("午前", "午後")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Sunday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val monthsShort: List<String> get() = months
|
||||||
|
override val daysOfWeekShort: List<String> = listOf("日", "月", "火", "水", "木", "金", "土")
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("y/MM/dd H:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("y/MM/dd H:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("y'年'M'月'd'日'EEEE")
|
||||||
|
override val formatDateLong = format("y'年'M'月'd'日'")
|
||||||
|
override val formatDateMedium = format("y/MM/dd")
|
||||||
|
override val formatDateShort = format("y/MM/dd")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("H:mm:ss")
|
||||||
|
override val formatTimeShort = format("H:mm")
|
||||||
|
}
|
37
klock/src/commonMain/kotlin/korlibs/time/locale/ko.kt
Normal file
37
klock/src/commonMain/kotlin/korlibs/time/locale/ko.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.korean get() = KoreanKlockLocale
|
||||||
|
|
||||||
|
open class KoreanKlockLocale : KlockLocale() {
|
||||||
|
companion object : KoreanKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "ko"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("오전", "오후")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val daysOfWeekShort: List<String> = listOf("일", "월", "화", "수", "목", "금", "토")
|
||||||
|
override val monthsShort: List<String> get() = months
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("y. M. d. a h:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("yy. M. d. a h:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("y년 M월 d일 EEEE")
|
||||||
|
override val formatDateLong = format("y년 M월 d일")
|
||||||
|
override val formatDateMedium = format("y. M. d.")
|
||||||
|
override val formatDateShort = format("yy. M. d.")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("a h:mm:ss")
|
||||||
|
override val formatTimeShort = format("a h:mm")
|
||||||
|
}
|
46
klock/src/commonMain/kotlin/korlibs/time/locale/nb.kt
Normal file
46
klock/src/commonMain/kotlin/korlibs/time/locale/nb.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.norwegian get() = NorwegianKlockLocale
|
||||||
|
|
||||||
|
open class NorwegianKlockLocale : KlockLocale() {
|
||||||
|
|
||||||
|
companion object : NorwegianKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "nb"
|
||||||
|
|
||||||
|
override val firstDayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"søndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "lørdag"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val months = listOf(
|
||||||
|
"januar",
|
||||||
|
"februar",
|
||||||
|
"mars",
|
||||||
|
"april",
|
||||||
|
"mai",
|
||||||
|
"juni",
|
||||||
|
"juli",
|
||||||
|
"august",
|
||||||
|
"september",
|
||||||
|
"oktober",
|
||||||
|
"november",
|
||||||
|
"desember"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd.MM.y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d. MMMM y")
|
||||||
|
override val formatDateLong = format("d. MMMM y")
|
||||||
|
override val formatDateMedium = format("dd.MM.y")
|
||||||
|
override val formatDateShort = format("dd.MM.yy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
|
||||||
|
}
|
35
klock/src/commonMain/kotlin/korlibs/time/locale/nl.kt
Normal file
35
klock/src/commonMain/kotlin/korlibs/time/locale/nl.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.dutch get() = DutchKlockLocale
|
||||||
|
|
||||||
|
open class DutchKlockLocale : KlockLocale() {
|
||||||
|
companion object : DutchKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "nl"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("a.m.", "p.m.")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"januari", "februari", "maart", "april", "mei", "juni",
|
||||||
|
"juli", "augustus", "september", "oktober", "november", "december"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("d MMM y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd-MM-yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE d MMMM y")
|
||||||
|
override val formatDateLong = format("d MMMM y")
|
||||||
|
override val formatDateMedium = format("d MMM y")
|
||||||
|
override val formatDateShort = format("dd-MM-y")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
48
klock/src/commonMain/kotlin/korlibs/time/locale/pt.kt
Normal file
48
klock/src/commonMain/kotlin/korlibs/time/locale/pt.kt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.portuguese get() = PortugueseKlockLocale
|
||||||
|
|
||||||
|
open class PortugueseKlockLocale : KlockLocale() {
|
||||||
|
companion object : PortugueseKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "pt"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("AM", "PM")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"domingo",
|
||||||
|
"segunda-feira",
|
||||||
|
"terça-feira",
|
||||||
|
"quarta-feira",
|
||||||
|
"quinta-feira",
|
||||||
|
"sexta-feira",
|
||||||
|
"sábado"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"janeiro",
|
||||||
|
"fevereiro",
|
||||||
|
"março",
|
||||||
|
"abril",
|
||||||
|
"maio",
|
||||||
|
"junho",
|
||||||
|
"julho",
|
||||||
|
"agosto",
|
||||||
|
"setembro",
|
||||||
|
"outubro",
|
||||||
|
"novembro",
|
||||||
|
"dezembro"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("d 'de' MMM 'de' y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd/MM/y HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d 'de' MMMM 'de' y")
|
||||||
|
override val formatDateLong = format("d 'de' MMMM 'de' y")
|
||||||
|
override val formatDateMedium = format("d 'de' MMM 'de' y")
|
||||||
|
override val formatDateShort = format("dd/MM/y")
|
||||||
|
}
|
50
klock/src/commonMain/kotlin/korlibs/time/locale/ru.kt
Normal file
50
klock/src/commonMain/kotlin/korlibs/time/locale/ru.kt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
import korlibs.time.KlockLocaleContext
|
||||||
|
import korlibs.time.KlockLocaleGender
|
||||||
|
|
||||||
|
val KlockLocale.Companion.russian get() = RussianKlockLocale
|
||||||
|
|
||||||
|
open class RussianKlockLocale : KlockLocale() {
|
||||||
|
companion object : RussianKlockLocale()
|
||||||
|
|
||||||
|
override fun getOrdinalByDay(day: Int, context: KlockLocaleContext): String = when (context.gender) {
|
||||||
|
KlockLocaleGender.Masculine -> "$day-й"
|
||||||
|
// if feminine is ever added to KlockLocaleGender, don't forget to add implementation here: "$day-я"
|
||||||
|
else -> "$day-е"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDayByOrdinal(ordinal: String): Int = ordinal.substringBeforeLast('-').toInt()
|
||||||
|
|
||||||
|
override val ISO639_1 = "ru"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("ДП", "ПП")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val daysOfWeekShort = listOf(
|
||||||
|
"вс", "пн", "вт", "ср", "чт", "пт", "сб"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val months = listOf(
|
||||||
|
"января", "февраля", "марта", "апреля", "мая", "июня",
|
||||||
|
"июля", "августа", "сентября", "октября", "ноября", "декабря"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("d MMM y г. H:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.y H:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d MMMM y г.")
|
||||||
|
override val formatDateLong = format("d MMMM y г.")
|
||||||
|
override val formatDateMedium = format("d MMM y г.")
|
||||||
|
override val formatDateShort = format("dd.MM.y")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("H:mm:ss")
|
||||||
|
override val formatTimeShort = format("H:mm")
|
||||||
|
}
|
45
klock/src/commonMain/kotlin/korlibs/time/locale/sv.kt
Normal file
45
klock/src/commonMain/kotlin/korlibs/time/locale/sv.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.swedish get() = SwedishKlockLocale
|
||||||
|
|
||||||
|
open class SwedishKlockLocale : KlockLocale() {
|
||||||
|
|
||||||
|
companion object : SwedishKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1: String = "sv"
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek: List<String> = listOf(
|
||||||
|
"söndag", "måndag", "tisdag", "onsdag", "torsdag", "fredag", "lördag"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val months: List<String> = listOf(
|
||||||
|
"januari",
|
||||||
|
"februari",
|
||||||
|
"mars",
|
||||||
|
"april",
|
||||||
|
"maj",
|
||||||
|
"juni",
|
||||||
|
"juli",
|
||||||
|
"augusti",
|
||||||
|
"september",
|
||||||
|
"oktober",
|
||||||
|
"november",
|
||||||
|
"december"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd.MM.y HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.yy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d. MMMM y")
|
||||||
|
override val formatDateLong = format("d. MMMM y")
|
||||||
|
override val formatDateMedium = format("dd.MM.y")
|
||||||
|
override val formatDateShort = format("dd.MM.yy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
51
klock/src/commonMain/kotlin/korlibs/time/locale/tr.kt
Normal file
51
klock/src/commonMain/kotlin/korlibs/time/locale/tr.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
|
||||||
|
val KlockLocale.Companion.turkish get() = TurkishKlockLocale
|
||||||
|
|
||||||
|
open class TurkishKlockLocale : KlockLocale() {
|
||||||
|
companion object : TurkishKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "tr"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("ÖÖ", "ÖS")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val daysOfWeekShort = listOf("Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt")
|
||||||
|
|
||||||
|
override val months = listOf(
|
||||||
|
"Ocak",
|
||||||
|
"Şubat",
|
||||||
|
"Mart",
|
||||||
|
"Nisan",
|
||||||
|
"Mayıs",
|
||||||
|
"Haziran",
|
||||||
|
"Temmuz",
|
||||||
|
"Ağustos",
|
||||||
|
"Eylül",
|
||||||
|
"Ekim",
|
||||||
|
"Kasım",
|
||||||
|
"Aralık"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val monthsShort = listOf(
|
||||||
|
"Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("dd MMM yyyy HH:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.yyyy HH:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("dd MMMM yyyy EEEE")
|
||||||
|
override val formatDateLong = format("d MMMM yyyy")
|
||||||
|
override val formatDateMedium = format("dd MMM yyyy")
|
||||||
|
override val formatDateShort = format("dd.MM.yyyy")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("HH:mm:ss")
|
||||||
|
override val formatTimeShort = format("HH:mm")
|
||||||
|
}
|
39
klock/src/commonMain/kotlin/korlibs/time/locale/uk.kt
Normal file
39
klock/src/commonMain/kotlin/korlibs/time/locale/uk.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.ukrainian get() = UkrainianKlockLocale
|
||||||
|
|
||||||
|
open class UkrainianKlockLocale : KlockLocale() {
|
||||||
|
companion object : UkrainianKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "uk"
|
||||||
|
override val h12Marker = listOf("ДП", "ПП")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Monday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"неділя", "понеділок", "вівторок", "середа", "четвер", "п'ятниця", "субота"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val daysOfWeekShort = listOf(
|
||||||
|
"нд", "пн", "вт", "ср", "чт", "пт", "сб"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val months = listOf(
|
||||||
|
"січня", "лютого", "березня", "квітня", "травня", "червня",
|
||||||
|
"липня", "серпня", "вересня", "жовтня", "листопада", "грудня"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("d MMM y р. H:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("dd.MM.y H:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("EEEE, d MMMM y р.")
|
||||||
|
override val formatDateLong = format("d MMMM y р.")
|
||||||
|
override val formatDateMedium = format("d MMM y р.")
|
||||||
|
override val formatDateShort = format("dd.MM.y")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("H:mm:ss")
|
||||||
|
override val formatTimeShort = format("H:mm")
|
||||||
|
}
|
39
klock/src/commonMain/kotlin/korlibs/time/locale/zh.kt
Normal file
39
klock/src/commonMain/kotlin/korlibs/time/locale/zh.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
|
||||||
|
val KlockLocale.Companion.chinese get() = ChineseKlockLocale
|
||||||
|
|
||||||
|
open class ChineseKlockLocale : KlockLocale() {
|
||||||
|
companion object : ChineseKlockLocale()
|
||||||
|
|
||||||
|
override val ISO639_1 = "zh"
|
||||||
|
|
||||||
|
override val h12Marker = listOf("上午", "下午")
|
||||||
|
|
||||||
|
override val firstDayOfWeek: DayOfWeek = DayOfWeek.Sunday
|
||||||
|
|
||||||
|
override val daysOfWeek = listOf(
|
||||||
|
"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"
|
||||||
|
)
|
||||||
|
override val months = listOf(
|
||||||
|
"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val daysOfWeekShort: List<String> = listOf("周日", "周一", "周二", "周三", "周四", "周五", "周六")
|
||||||
|
override val monthsShort: List<String> = listOf(
|
||||||
|
"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"
|
||||||
|
)
|
||||||
|
|
||||||
|
override val formatDateTimeMedium = format("y年M月d日 ah:mm:ss")
|
||||||
|
override val formatDateTimeShort = format("y/M/d ah:mm")
|
||||||
|
|
||||||
|
override val formatDateFull = format("y年M月d日EEEE")
|
||||||
|
override val formatDateLong = format("y年M月d日")
|
||||||
|
override val formatDateMedium = format("y年M月d日")
|
||||||
|
override val formatDateShort = format("y/M/d")
|
||||||
|
|
||||||
|
override val formatTimeMedium = format("h:mm:ss")
|
||||||
|
override val formatTimeShort = format("h:mm")
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
fun DateFormat.parseLong(str: String) = parse(str).local.unixMillisLong
|
||||||
|
fun DateFormat.parseDouble(str: String) = parse(str).local.unixMillisDouble
|
||||||
|
fun DateFormat.parseDoubleOrNull(str: String) = tryParse(str)?.local?.unixMillisDouble
|
52
klock/src/commonTest/kotlin/korlibs/time/DateFormatTest.kt
Normal file
52
klock/src/commonTest/kotlin/korlibs/time/DateFormatTest.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
class DateFormatTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
assertEquals("Sat, 03 Dec 2011 10:15:30 GMT+0100", DateFormat.FORMAT1.parse("2011-12-03T10:15:30+01:00").toStringDefault())
|
||||||
|
assertEquals("Sat, 03 Dec 2011 10:15:30 GMT+0100", DateFormat.FORMAT1.parse("2011-12-03T10:15:30+0100").toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseIsoFormat() {
|
||||||
|
val iso8601JsonFormat = PatternDateFormat("yyyy-MM-ddTHH:mm:ss.SZ")
|
||||||
|
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.1Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.01Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.001Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.0001Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.00001Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.000001Z").toString(DateFormat.FORMAT2))
|
||||||
|
|
||||||
|
// Fails with "S" -> """(\d{1,6})""", now succeeds with (\d{1,9})
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.0000001Z").toString(DateFormat.FORMAT2)) //7S
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.00000001Z").toString(DateFormat.FORMAT2)) //8S
|
||||||
|
assertEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.000000001Z").toString(DateFormat.FORMAT2)) //9S
|
||||||
|
|
||||||
|
//assertFailsWith<DateException> { iso8601JsonFormat.parse("2020-01-01T13:12:30.0000000001Z").toString(DateFormat.FORMAT2) } //10S
|
||||||
|
|
||||||
|
// Negative tests
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.10Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.100Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.1000Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.10000Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.100000Z").toString(DateFormat.FORMAT2))
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.1000000Z").toString(DateFormat.FORMAT2)) //7S
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.10000000Z").toString(DateFormat.FORMAT2)) //8S
|
||||||
|
assertNotEquals("2020-01-01T13:12:30.001Z", iso8601JsonFormat.parse("2020-01-01T13:12:30.100000000Z").toString(DateFormat.FORMAT2)) //9S
|
||||||
|
|
||||||
|
//assertFailsWith<DateException> { iso8601JsonFormat.parse("2020-01-01T13:12:30.1000000000Z").toString(DateFormat.FORMAT2) } //10S
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimezoneBug670() {
|
||||||
|
val expected = TimezoneOffset(9.hours)
|
||||||
|
val actual = DateFormat("z").parse("+09:00").offset
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
}
|
93
klock/src/commonTest/kotlin/korlibs/time/DateRangeTest.kt
Normal file
93
klock/src/commonTest/kotlin/korlibs/time/DateRangeTest.kt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class DateRangeTest {
|
||||||
|
val date1 = DateTime(2018, Month.December, 24)
|
||||||
|
val date2 = DateTime(2018, Month.November, 26)
|
||||||
|
val date3 = DateTime(2017, Month.November, 26)
|
||||||
|
val date4 = DateTime(1, Month.November, 26)
|
||||||
|
|
||||||
|
val christmas = DateTime(2018, Month.December, 25)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val src = DateTime(2000, Month.January, 10)
|
||||||
|
val dst = DateTime(2000, Month.February, 20)
|
||||||
|
val other = DateTime(2000, Month.February, 1)
|
||||||
|
|
||||||
|
val outside1 = DateTime(2000, Month.January, 9)
|
||||||
|
val outside2 = DateTime(2000, Month.February, 21)
|
||||||
|
|
||||||
|
//assertTrue(other in (src .. dst))
|
||||||
|
|
||||||
|
//assertTrue(src in (src .. dst))
|
||||||
|
//assertTrue(dst in (src .. dst))
|
||||||
|
|
||||||
|
assertTrue(other in (src until dst))
|
||||||
|
|
||||||
|
assertTrue(src in (src until dst))
|
||||||
|
assertTrue(dst !in (src until dst))
|
||||||
|
|
||||||
|
//assertTrue(outside1 !in (src .. dst))
|
||||||
|
//assertTrue(outside2 !in (src .. dst))
|
||||||
|
assertTrue(outside1 !in (src until dst))
|
||||||
|
assertTrue(outside2 !in (src until dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDaysUntilChristmas() {
|
||||||
|
assertEquals("1D", (date1 until christmas).span.toString())
|
||||||
|
assertEquals("29D", (date2 until christmas).span.toString(includeWeeks = false))
|
||||||
|
assertEquals("1Y 29D", (date3 until christmas).span.toString(includeWeeks = false))
|
||||||
|
assertEquals("2017Y 29D", (date4 until christmas).span.toString(includeWeeks = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDaysUntilChristmasRev() {
|
||||||
|
assertEquals("-1D", (christmas until date1).span.toString())
|
||||||
|
assertEquals("-29D", (christmas until date2).span.toString(includeWeeks = false))
|
||||||
|
assertEquals("-1Y -29D", (christmas until date3).span.toString(includeWeeks = false))
|
||||||
|
assertEquals("-2017Y -29D", (christmas until date4).span.toString(includeWeeks = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompareDate() {
|
||||||
|
val range = range(0, 100)
|
||||||
|
assertEquals(-1, range.compareTo(date(110)))
|
||||||
|
assertEquals(-1, range.compareTo(date(100)))
|
||||||
|
assertEquals(0, range.compareTo(date(99)))
|
||||||
|
assertEquals(0, range.compareTo(date(50)))
|
||||||
|
assertEquals(0, range.compareTo(date(0)))
|
||||||
|
assertEquals(+1, range.compareTo(date(-1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWithout() {
|
||||||
|
assertEquals("[0..100]", range(0, 100).without(range(-5, 0)).toStringLongs())
|
||||||
|
assertEquals("[5..100]", range(0, 100).without(range(-5, 5)).toStringLongs())
|
||||||
|
assertEquals("[0..5, 95..100]", range(0, 100).without(range(5, 95)).toStringLongs())
|
||||||
|
assertEquals("[0..95]", range(0, 100).without(range(95, 100)).toStringLongs())
|
||||||
|
assertEquals("[0..100]", range(0, 100).without(range(100, 105)).toStringLongs())
|
||||||
|
assertEquals("[0..100]", range(0, 100).without(range(105, 100)).toStringLongs())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mergeOnIntersectionOrNull() {
|
||||||
|
val baseRange = range(0, 100)
|
||||||
|
assertEquals("-50..100", baseRange.mergeOnContactOrNull(range(-50, 50))?.toStringLongs())
|
||||||
|
assertEquals("-50..100", baseRange.mergeOnContactOrNull(range(-50, 0))?.toStringLongs())
|
||||||
|
assertEquals("0..100", baseRange.mergeOnContactOrNull(range(0, 100))?.toStringLongs())
|
||||||
|
assertEquals("-50..150", baseRange.mergeOnContactOrNull(range(-50, 150))?.toStringLongs())
|
||||||
|
assertEquals("0..100", baseRange.mergeOnContactOrNull(range(25, 75))?.toStringLongs())
|
||||||
|
assertEquals("0..150", baseRange.mergeOnContactOrNull(range(25, 150))?.toStringLongs())
|
||||||
|
assertEquals("0..200", baseRange.mergeOnContactOrNull(range(100, 200))?.toStringLongs())
|
||||||
|
assertEquals(null, baseRange.mergeOnContactOrNull(range(101, 200))?.toStringLongs())
|
||||||
|
}
|
||||||
|
|
||||||
|
val date = DateTime.EPOCH
|
||||||
|
fun date(time: Int) = (date + time.milliseconds)
|
||||||
|
fun range(from: Int, to: Int) = date(from) until date(to)
|
||||||
|
}
|
78
klock/src/commonTest/kotlin/korlibs/time/DateTest.kt
Normal file
78
klock/src/commonTest/kotlin/korlibs/time/DateTest.kt
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.locale.spanish
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val date = Date(2019, Month.September, 18)
|
||||||
|
val date2 = Date(2019, Month.September, 19)
|
||||||
|
val time = Time(13, 9, 37, 150)
|
||||||
|
|
||||||
|
assertEquals(DayOfWeek.Wednesday, date.dayOfWeek)
|
||||||
|
assertEquals(DayOfWeek.Thursday, date2.dayOfWeek)
|
||||||
|
assertEquals(4, date2.dayOfWeekInt)
|
||||||
|
assertEquals(1, Date(2019, Month.January, 1).dayOfYear)
|
||||||
|
assertEquals(262, date2.dayOfYear)
|
||||||
|
assertEquals(2019, date2.year)
|
||||||
|
assertEquals(Year(2019), date2.yearYear)
|
||||||
|
|
||||||
|
assertEquals("2019-09-18", date.toString())
|
||||||
|
assertEquals("13:09:37.150", time.toString())
|
||||||
|
assertEquals("Wed, 18 Sep 2019 00:00:00 UTC", (date.dateTimeDayStart).toStringDefault())
|
||||||
|
assertEquals("Wed, 18 Sep 2019 13:09:37 UTC", (date + time).toStringDefault())
|
||||||
|
assertEquals("2019-10-01", (Date(2019, Month.September, 30) + 1.days).toString())
|
||||||
|
assertEquals("2019-10-30", (Date(2019, Month.September, 30) + 1.months).toString())
|
||||||
|
assertEquals("2019-09-30", (Date(2019, Month.October, 1) - 1.days).toString())
|
||||||
|
assertEquals("2019-09-01", (Date(2019, Month.October, 1) - 1.months).toString())
|
||||||
|
|
||||||
|
assertEquals("2019-10-31", (Date(2019, Month.September, 30) + DateTimeSpan(1.months, 1.days)).toString())
|
||||||
|
assertEquals("2019-08-31", (Date(2019, Month.October, 1) - DateTimeSpan(1.months, 1.days)).toString())
|
||||||
|
|
||||||
|
(Time(9, 0) .. Time(19, 30)).let { range ->
|
||||||
|
assertEquals(true, range.contains(Time(11, 0)))
|
||||||
|
assertEquals(false, range.contains(Time(8, 59, 59, 999)))
|
||||||
|
assertEquals(false, range.contains(Time(19, 31)))
|
||||||
|
}
|
||||||
|
|
||||||
|
(Date(2019, 9, 18) .. Date(2019, 10, 2)).let { range ->
|
||||||
|
assertEquals(true, range.contains(Date(2019, 9, 18)))
|
||||||
|
assertEquals(true, range.contains(Date(2019, 10, 1)))
|
||||||
|
assertEquals(false, range.contains(Date(2019, 9, 17)))
|
||||||
|
assertEquals(false, range.contains(Date(2019, 10, 3)))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("Wed, 18 Sep 2019 00:00:00 UTC", (Date(2019, Month.September, 17) + Time(24)).toStringDefault())
|
||||||
|
assertEquals("Tue, 17 Sep 2019 00:00:00 UTC", (Date(2019, Month.September, 18) - Time(24)).toStringDefault())
|
||||||
|
|
||||||
|
assertEquals("Wed, 18 Sep 2019 01:02:20 UTC", (Date(2019, Month.September, 18) + Time(hour = 1, minute = 1, second = 80)).toStringDefault())
|
||||||
|
val format = "EEE, dd MMM YYYY HH:mm:ss.SSS"
|
||||||
|
assertEquals("Wed, 18 Sep 2019 01:02:20.300", (Date(2019, Month.September, 18) + Time(hour = 1, minute = 1, second = 80, millisecond = 300)).format(format))
|
||||||
|
assertEquals("Wed, 18 Sep 2019 01:02:21.300", (Date(2019, Month.September, 18) + Time(hour = 1, minute = 1, second = 80, millisecond = 1300)).format(format))
|
||||||
|
assertEquals("Wed, 18 Sep 2019 23:58:39.700", (Date(2019, Month.September, 18) - Time(hour = 1, minute = 1, second = 80, millisecond = 300)).format(format))
|
||||||
|
assertEquals("Wed, 18 Sep 2019 23:58:38.700", (Date(2019, Month.September, 18) - Time(hour = 1, minute = 1, second = 80, millisecond = 1300)).format(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
fun date(dayOfMonth: Int, dayOfWeek: DayOfWeek, locale: KlockLocale) = Date(2019, Month.September, dayOfMonth).inThisWeek(dayOfWeek, locale).toString()
|
||||||
|
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-16", date(dayOfMonth, DayOfWeek.Monday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-17", date(dayOfMonth, DayOfWeek.Tuesday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-18", date(dayOfMonth, DayOfWeek.Wednesday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-19", date(dayOfMonth, DayOfWeek.Thursday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-20", date(dayOfMonth, DayOfWeek.Friday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-21", date(dayOfMonth, DayOfWeek.Saturday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 16..22) assertEquals("2019-09-22", date(dayOfMonth, DayOfWeek.Sunday, KlockLocale.spanish), "failed for day = $dayOfMonth")
|
||||||
|
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-15", date(dayOfMonth, DayOfWeek.Sunday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-16", date(dayOfMonth, DayOfWeek.Monday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-17", date(dayOfMonth, DayOfWeek.Tuesday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-18", date(dayOfMonth, DayOfWeek.Wednesday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-19", date(dayOfMonth, DayOfWeek.Thursday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-20", date(dayOfMonth, DayOfWeek.Friday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
for (dayOfMonth in 15..21) assertEquals("2019-09-21", date(dayOfMonth, DayOfWeek.Saturday, KlockLocale.english), "failed for day = $dayOfMonth")
|
||||||
|
}
|
||||||
|
}
|
155
klock/src/commonTest/kotlin/korlibs/time/DateTimeRangeSetTest.kt
Normal file
155
klock/src/commonTest/kotlin/korlibs/time/DateTimeRangeSetTest.kt
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateTimeRangeSetTest {
|
||||||
|
val date = DateTime.EPOCH
|
||||||
|
fun date(time: Int) = (date + time.milliseconds)
|
||||||
|
fun range(from: Int, to: Int) = date(from) until date(to)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val range1 = range(0, 10)
|
||||||
|
val range2 = range(20, 30)
|
||||||
|
val range3 = range(30, 40)
|
||||||
|
|
||||||
|
val rangesList = listOf(range1, range2, range3)
|
||||||
|
val ranges = DateTimeRangeSet(rangesList)
|
||||||
|
assertEquals("[0..10, 20..30, 30..40]", DateTimeRangeSet.toStringLongs(rangesList))
|
||||||
|
assertEquals("[0..10, 20..40]", ranges.toStringLongs())
|
||||||
|
assertEquals("[0..10, 30..40]", (ranges - range(10, 30)).toStringLongs())
|
||||||
|
assertEquals("[0..5, 30..40]", (ranges - range(5, 30)).toStringLongs())
|
||||||
|
assertEquals("[5..10, 20..35]", (ranges.intersection(range(5, 35))).toStringLongs())
|
||||||
|
assertEquals("[5..10, 20..35]", (ranges.intersection(range(5, 15), range(16, 35))).toStringLongs())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomRanges(count: Int, min: Int = 0, max: Int = 1000000, seed: Long = 0L): List<DateTimeRange> {
|
||||||
|
val random = Random(seed)
|
||||||
|
fun randomRange(): DateTimeRange {
|
||||||
|
val a = random.nextInt(min, max)
|
||||||
|
val b = random.nextInt(min, max)
|
||||||
|
return range(min(a, b), max(a, b))
|
||||||
|
}
|
||||||
|
return (0 until count).map { randomRange() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomRangesSeparated(count: Int = 30, seed: Long = 0L): DateTimeRangeSet {
|
||||||
|
val random = Random(seed)
|
||||||
|
val out = arrayListOf<DateTimeRange>()
|
||||||
|
var pos = random.nextLong(1000)
|
||||||
|
for (n in 0 until count) {
|
||||||
|
val dist = random.nextLong(1000)
|
||||||
|
out += DateTimeRange(date + (pos).milliseconds, date + (pos + dist).milliseconds)
|
||||||
|
val separation = random.nextLong(1000)
|
||||||
|
pos += dist + separation
|
||||||
|
}
|
||||||
|
return DateTimeRangeSet(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCombine() {
|
||||||
|
val ranges = randomRanges(10000, seed = 0L)
|
||||||
|
assertEquals(DateTimeRangeSet.Fast.combine(ranges), DateTimeRangeSet.Slow.combine(ranges))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMinus() {
|
||||||
|
val r1 = randomRangesSeparated(40, seed = 10L)
|
||||||
|
val r2 = randomRangesSeparated(10, seed = 1L)
|
||||||
|
|
||||||
|
val fast = DateTimeRangeSet.Fast.minus(r1, r2).toStringLongs()
|
||||||
|
val slow = DateTimeRangeSet.Slow.minus(r1, r2).toStringLongs()
|
||||||
|
//println(r1)
|
||||||
|
//println(r2)
|
||||||
|
//println("fast: $fast")
|
||||||
|
//println("slow: $slow")
|
||||||
|
assertEquals(fast, slow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIntersect() {
|
||||||
|
//val r1 = randomRangesSeparated(40, seed = 10L)
|
||||||
|
//val r2 = randomRangesSeparated(10, seed = 1L)
|
||||||
|
|
||||||
|
for (seed in listOf(1L, 3L, 10L, 100L, 1000L)) {
|
||||||
|
|
||||||
|
//val r1 = randomRangesSeparated(1000, seed = seed)
|
||||||
|
//val r2 = randomRangesSeparated(1010, seed = seed + 1)
|
||||||
|
val r1 = randomRangesSeparated(100, seed = seed)
|
||||||
|
val r2 = randomRangesSeparated(115, seed = seed + 1)
|
||||||
|
|
||||||
|
//val r1 = randomRangesSeparated(3, seed = 10L)
|
||||||
|
//val r2 = randomRangesSeparated(3, seed = 1L)
|
||||||
|
|
||||||
|
val fast = DateTimeRangeSet.Fast.intersection(r1, r2).toStringLongs()
|
||||||
|
val slow = DateTimeRangeSet.Slow.intersection(r1, r2).toStringLongs()
|
||||||
|
//val slow = DateTimeRangeSet.Fast.intersection(r1, r2).toStringLongs()
|
||||||
|
//println(r1)
|
||||||
|
//println(r2)
|
||||||
|
//println("fast: $fast")
|
||||||
|
//println("slow: $slow")
|
||||||
|
assertEquals(fast, slow)
|
||||||
|
|
||||||
|
val fast2 = DateTimeRangeSet.Fast.intersection(r2, r1).toStringLongs()
|
||||||
|
val slow2 = DateTimeRangeSet.Slow.intersection(r2, r1).toStringLongs()
|
||||||
|
|
||||||
|
assertEquals(fast2, slow2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContains() {
|
||||||
|
DateTimeRangeSet(range(0, 100), range(150, 200)).let { ranges ->
|
||||||
|
assertEquals(false, date(-50) in ranges)
|
||||||
|
assertEquals(true, date(0) in ranges)
|
||||||
|
assertEquals(true, date(50) in ranges)
|
||||||
|
assertEquals(false, date(100) in ranges)
|
||||||
|
assertEquals(false, date(120) in ranges)
|
||||||
|
assertEquals(true, date(150) in ranges)
|
||||||
|
assertEquals(true, date(170) in ranges)
|
||||||
|
assertEquals(false, date(200) in ranges)
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeRangeSet(range(0, 100), range(150, 200), range(250, 300)).let { ranges ->
|
||||||
|
assertEquals(false, date(-50) in ranges)
|
||||||
|
assertEquals(true, date(0) in ranges)
|
||||||
|
assertEquals(true, date(50) in ranges)
|
||||||
|
assertEquals(false, date(100) in ranges)
|
||||||
|
assertEquals(false, date(120) in ranges)
|
||||||
|
assertEquals(true, date(150) in ranges)
|
||||||
|
assertEquals(true, date(170) in ranges)
|
||||||
|
assertEquals(false, date(200) in ranges)
|
||||||
|
assertEquals(true, date(250) in ranges)
|
||||||
|
assertEquals(true, date(270) in ranges)
|
||||||
|
assertEquals(false, date(300) in ranges)
|
||||||
|
assertEquals(false, date(320) in ranges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContainsRange() {
|
||||||
|
val range = DateTimeRangeSet(range(0, 100), range(200, 300), range(400, 500))
|
||||||
|
assertEquals(true, range(50, 70) in range)
|
||||||
|
assertEquals(true, range(250, 270) in range)
|
||||||
|
assertEquals(true, range(400, 500) in range)
|
||||||
|
|
||||||
|
assertEquals(true, range(0, 100) in range)
|
||||||
|
assertEquals(true, range(200, 300) in range)
|
||||||
|
assertEquals(true, range(400, 500) in range)
|
||||||
|
|
||||||
|
assertEquals(true, range(200, 300) in range)
|
||||||
|
assertEquals(false, range(100, 110) in range)
|
||||||
|
assertEquals(false, range(90, 110) in range)
|
||||||
|
assertEquals(false, range(500, 510) in range)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSize() {
|
||||||
|
assertEquals(100.milliseconds, range(100, 200).size)
|
||||||
|
assertEquals(200.milliseconds, DateTimeRangeSet(range(100, 200), range(150, 300)).size)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateTimeRangeTest {
|
||||||
|
val format = ISO8601.DATETIME_COMPLETE
|
||||||
|
fun String.date() = format.parseUtc(this)
|
||||||
|
fun range(from: Int, to: Int) = (DateTime.EPOCH + from.milliseconds) until (DateTime.EPOCH + to.milliseconds)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val range1 = "2019-09-17T13:53:31".date() until "2019-10-17T07:00:00".date()
|
||||||
|
val range2 = "2019-09-17T14:53:31".date() until "2019-10-17T08:00:00".date()
|
||||||
|
val range3 = "2019-10-19T00:00:00".date() until "2019-10-20T00:00:00".date()
|
||||||
|
assertEquals("2019-09-17T14:53:31..2019-10-17T07:00:00", range1.intersectionWith(range2)?.toString(format))
|
||||||
|
assertEquals(null, range1.intersectionWith(range3)?.toString(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
assertEquals("[]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(0, 100))))
|
||||||
|
assertEquals("[50..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(-50, 50))))
|
||||||
|
assertEquals("[0..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(-50, -10))))
|
||||||
|
assertEquals("[0..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(-50, 0))))
|
||||||
|
assertEquals("[0..20, 70..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(20, 70))))
|
||||||
|
assertEquals("[0..50]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(50, 100))))
|
||||||
|
assertEquals("[0..50]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(50, 120))))
|
||||||
|
assertEquals("[0..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(100, 120))))
|
||||||
|
assertEquals("[0..100]", DateTimeRangeSet.toStringLongs(range(0, 100).without(range(120, 200))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContainsRange() {
|
||||||
|
assertEquals(true, range(0, 100) in range(0, 100))
|
||||||
|
assertEquals(true, range(20, 80) in range(0, 100))
|
||||||
|
assertEquals(true, range(80, 100) in range(0, 100))
|
||||||
|
|
||||||
|
assertEquals(false, range(-50, -20) in range(0, 100))
|
||||||
|
assertEquals(false, range(-10, 110) in range(0, 100))
|
||||||
|
assertEquals(false, range(80, 101) in range(0, 100))
|
||||||
|
assertEquals(false, range(-50, 0) in range(0, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTest() {
|
||||||
|
val range = DateTimeRange(Date(2019, Month.September, 18), Time(8), Time(13))
|
||||||
|
assertEquals("2019-09-18T08:00:00..2019-09-18T13:00:00", range.toString(ISO8601.DATETIME_COMPLETE.extended))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOptionalPatterns() {
|
||||||
|
val format = PatternDateFormat("YYYY[-MM[-dd]]").withOptional()
|
||||||
|
assertEquals("2019-01-01", format.parse("2019").toString(format))
|
||||||
|
assertEquals("2019-09-01", format.parse("2019-09").toString(format))
|
||||||
|
assertEquals("2019-09-03", format.parse("2019-09-03").toString(format))
|
||||||
|
}
|
||||||
|
}
|
39
klock/src/commonTest/kotlin/korlibs/time/DateTimeSpanTest.kt
Normal file
39
klock/src/commonTest/kotlin/korlibs/time/DateTimeSpanTest.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateTimeSpanTest {
|
||||||
|
@Test
|
||||||
|
fun testBasic() {
|
||||||
|
assertEquals("Thu, 13 Dec 2018 00:00:00 UTC", DateTime(2018, 12, 13).toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAddDistance() {
|
||||||
|
assertEquals(DateTime(2018, 12, 13), DateTime(2017, 12, 13) + 1.years)
|
||||||
|
assertEquals(DateTime(2018, 1, 13), DateTime(2017, 12, 13) + 1.months)
|
||||||
|
assertEquals(DateTime(2018, 1, 3), DateTime(2017, 12, 13) + 3.weeks)
|
||||||
|
assertEquals(DateTime(2018, 1, 2), DateTime(2017, 12, 13) + 20.days)
|
||||||
|
assertEquals(DateTime(2017, 12, 13, 3, 0, 0, 0), DateTime(2017, 12, 13) + 3.hours)
|
||||||
|
assertEquals(DateTime(2017, 12, 13, 0, 3, 0, 0), DateTime(2017, 12, 13) + 3.minutes)
|
||||||
|
assertEquals(DateTime(2017, 12, 13, 0, 0, 3, 0), DateTime(2017, 12, 13) + 3.seconds)
|
||||||
|
assertEquals(DateTime(2017, 12, 13, 0, 0, 0, 3), DateTime(2017, 12, 13) + 3.milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testToString() {
|
||||||
|
assertEquals("1Y 1M", (1.years + 1.months).toString())
|
||||||
|
assertEquals("1M", (1.months).toString())
|
||||||
|
assertEquals("1M 10W 10H", (1.months + 10.hours + 10.weeks).toString())
|
||||||
|
assertEquals("1M 11W 1D 10H", (1.months + 10.hours + 10.weeks + 8.days).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug63() {
|
||||||
|
val startDate = DateTime.fromString("Mon, 01 Jan 2019 00:00:00 UTC").utc
|
||||||
|
val endDate = DateTime.fromString("Sat, 01 Sep 2019 00:00:00 UTC").utc
|
||||||
|
val span = (startDate until endDate).span
|
||||||
|
assertEquals("8M", span.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateTimeStartOfEndOfTest {
|
||||||
|
val date = DateTime(2019, Month.October, 27, 21, 9, 33, 500)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testQuarter() {
|
||||||
|
assertEquals(1, DateTime(2019, Month.January, 1).quarter)
|
||||||
|
assertEquals(1, DateTime(2019, Month.February, 1).quarter)
|
||||||
|
assertEquals(1, DateTime(2019, Month.March, 1).quarter)
|
||||||
|
|
||||||
|
assertEquals(2, DateTime(2019, Month.April, 1).quarter)
|
||||||
|
assertEquals(2, DateTime(2019, Month.May, 1).quarter)
|
||||||
|
assertEquals(2, DateTime(2019, Month.June, 1).quarter)
|
||||||
|
|
||||||
|
assertEquals(3, DateTime(2019, Month.July, 1).quarter)
|
||||||
|
assertEquals(3, DateTime(2019, Month.August, 1).quarter)
|
||||||
|
assertEquals(3, DateTime(2019, Month.September, 1).quarter)
|
||||||
|
|
||||||
|
assertEquals(4, DateTime(2019, Month.October, 1).quarter)
|
||||||
|
assertEquals(4, DateTime(2019, Month.November, 1).quarter)
|
||||||
|
assertEquals(4, DateTime(2019, Month.December, 1).quarter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStartOf() {
|
||||||
|
assertEquals("2019-01-01T00:00:00", date.startOfYear.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-01T00:00:00", date.startOfMonth.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-01T00:00:00", date.startOfQuarter.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-21T00:00:00", date.startOfIsoWeek.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T00:00:00", date.startOfWeek.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T00:00:00", date.startOfDay.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:00:00", date.startOfHour.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:09:00", date.startOfMinute.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:09:33", date.startOfSecond.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEndOf() {
|
||||||
|
assertEquals("2019-12-31T23:59:59", date.endOfYear.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-31T23:59:59", date.endOfMonth.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-12-31T23:59:59", date.endOfQuarter.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T23:59:59", date.endOfIsoWeek.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-28T23:59:59", date.endOfWeek.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T23:59:59", date.endOfDay.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:59:59", date.endOfHour.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:09:59", date.endOfMinute.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
assertEquals("2019-10-27T21:09:33", date.endOfSecond.toString(ISO8601.DATETIME_COMPLETE))
|
||||||
|
}
|
||||||
|
}
|
531
klock/src/commonTest/kotlin/korlibs/time/DateTimeTest.kt
Normal file
531
klock/src/commonTest/kotlin/korlibs/time/DateTimeTest.kt
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class DateTimeTest {
|
||||||
|
val HttpDate by lazy { DateFormat("EEE, dd MMM yyyy HH:mm:ss z") }
|
||||||
|
val HttpDate2 by lazy { DateFormat("EEE, dd MMM yyyy H:mm:ss z") }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromString() {
|
||||||
|
assertEquals("Mon, 04 Dec 2017 04:35:37 UTC", DateTime.fromString("2017-12-04T04:35:37Z").toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormattingToCustomDateTimeFormats() {
|
||||||
|
val dt = DateTime(2018, 9, 8, 4, 8, 9)
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:09 UTC", dt.format("EEE, dd MMM yyyy HH:mm:ss z"))
|
||||||
|
assertEquals("Saturday, 08 Sep 2018 04:08:09 UTC", dt.format("EEEE, dd MMM yyyy HH:mm:ss z"))
|
||||||
|
// This doesn't follow the Java's rules
|
||||||
|
//assertEquals("S, 08 Sep 2018 04:08:09 UTC", dt.format("EEEEE, dd MMM yyyy HH:mm:ss z"))
|
||||||
|
//assertEquals("Sa, 08 Sep 2018 04:08:09 UTC", dt.format("EEEEEE, dd MMM yyyy HH:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 8 Sep 2018 04:08:09 UTC", dt.format("EEE, d MMM yyyy HH:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 9 2018 04:08:09 UTC", dt.format("EEE, dd M yyyy HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 09 2018 04:08:09 UTC", dt.format("EEE, dd MM yyyy HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 September 2018 04:08:09 UTC", dt.format("EEE, dd MMMM yyyy HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 S 2018 04:08:09 UTC", dt.format("EEE, dd MMMMM yyyy HH:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:09 UTC", dt.format("EEE, dd MMM y HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 Sep 18 04:08:09 UTC", dt.format("EEE, dd MMM yy HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 Sep 018 04:08:09 UTC", dt.format("EEE, dd MMM yyy HH:mm:ss z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:09 UTC", dt.format("EEE, dd MMM yyyy HH:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:09 UTC", dt.format("EEE, dd MMM YYYY HH:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 4:08:09 UTC", dt.format("EEE, dd MMM yyyy H:mm:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 4:08:09 am UTC", dt.format("EEE, dd MMM yyyy h:mm:ss a z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:09 am UTC", dt.format("EEE, dd MMM yyyy hh:mm:ss a z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:8:09 UTC", dt.format("EEE, dd MMM yyyy HH:m:ss z"))
|
||||||
|
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s z"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormattingToCustomDateTimeFormatsWithMilliseconds999() {
|
||||||
|
val dt = DateTime(2018, 9, 8, 4, 8, 9, 999)
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.9 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.S z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.99 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.999 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.9990 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.99900 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.999000 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSSSS z"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormattingToCustomDateTimeFormatsWithMilliseconds009() {
|
||||||
|
val dt = DateTime(2018, 9, 8, 4, 8, 9, 9)
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.0 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.S z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.00 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.009 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.0090 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.00900 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSSS z"))
|
||||||
|
assertEquals("Sat, 08 Sep 2018 04:08:9.009000 UTC", dt.format("EEE, dd MMM yyyy HH:mm:s.SSSSSS z"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesInCustomStringFormats() {
|
||||||
|
val dtmilli = 1536379689000L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9).unixMillisLong)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss z").parseLong("Sat, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Saturday, 08 Sep 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEEE, dd MMM yyyy HH:mm:ss z").parseLong("Saturday, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "S, 08 Sep 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEEEE, dd MMM yyyy HH:mm:ss z").parseLong("S, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sa, 08 Sep 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEEEEE, dd MMM yyyy HH:mm:ss z").parseLong("Sa, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 8 Sep 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, d MMM yyyy HH:mm:ss z").parseLong("Sat, 8 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 9 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd M yyyy HH:mm:ss z").parseLong("Sat, 08 9 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 09 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MM yyyy HH:mm:ss z").parseLong("Sat, 08 09 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 September 2018 04:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMMM yyyy HH:mm:ss z").parseLong("Sat, 08 September 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 S 2018 04:08:09 UTC",
|
||||||
|
expected = null,
|
||||||
|
actual = DateFormat("EEE, dd MMMMM yyyy HH:mm:ss z").parseDoubleOrNull("Sat, 08 S 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 UTC - y",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM y HH:mm:ss z").parseLong("Sat, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 18 04:08:09 UTC - yy",
|
||||||
|
expected = null,
|
||||||
|
actual = DateFormat("EEE, dd MMM yy HH:mm:ss z").parseDoubleOrNull("Sat, 08 Sep 18 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 018 04:08:09 UTC - yyy",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyy HH:mm:ss z").parseLong("Sat, 08 Sep 018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 UTC - YYYY",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM YYYY HH:mm:ss z").parseLong("Sat, 08 Sep 2018 04:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 4:08:09 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy H:mm:ss z").parseLong("Sat, 08 Sep 2018 4:08:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 am UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:m:ss z").parseLong("Sat, 08 Sep 2018 04:8:09 UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:9 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:s z").parseLong("Sat, 08 Sep 2018 04:08:9 UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNewParserBug1() {
|
||||||
|
DateFormat("EEE, dd MMMM yyyy HH:mm:ss z").parseLong("Sat, 08 September 2018 04:08:09 UTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesInCustomStringFormatsWithAmPm() {
|
||||||
|
val amDtmilli = 1536379689000L
|
||||||
|
assertEquals(amDtmilli, DateTime(2018, 9, 8, 4, 8, 9).unixMillisLong)
|
||||||
|
|
||||||
|
val pmDtmilli = 1536422889000L
|
||||||
|
assertEquals(pmDtmilli, DateTime(2018, 9, 8, 16, 8, 9).unixMillisLong)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 4:08:09 am UTC",
|
||||||
|
expected = amDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy h:mm:ss a z").parseLong("Sat, 08 Sep 2018 4:08:09 am UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 am UTC",
|
||||||
|
expected = amDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy hh:mm:ss a z").parseLong("Sat, 08 Sep 2018 04:08:09 am UTC")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 4:08:09 pm UTC",
|
||||||
|
expected = pmDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy h:mm:ss a z").parseLong("Sat, 08 Sep 2018 4:08:09 pm UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 pm UTC",
|
||||||
|
expected = pmDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy hh:mm:ss a z").parseLong("Sat, 08 Sep 2018 04:08:09 pm UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesWithPmMixedWith24Hourformat() {
|
||||||
|
val pmDtmilli = 1536422889000L
|
||||||
|
assertEquals(pmDtmilli, DateTime(2018, 9, 8, 16, 8, 9).unixMillisLong)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 4:08:09 pm UTC",
|
||||||
|
expected = pmDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy H:mm:ss a z").parseLong("Sat, 08 Sep 2018 16:08:09 pm UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09 pm UTC",
|
||||||
|
expected = pmDtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss a z").parseLong("Sat, 08 Sep 2018 16:08:09 pm UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesWithDeciSeconds() {
|
||||||
|
var dtmilli = 1536379689009L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9, 9).unixMillisLong)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.9 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.S z").parseLong("Sat, 08 Sep 2018 04:08:09.9 UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesWithCentiSeconds() {
|
||||||
|
var dtmilli = 1536379689099L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9, 99).unixMillisLong)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.99 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.SS z").parseLong("Sat, 08 Sep 2018 04:08:09.99 UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesWithMilliseconds() {
|
||||||
|
val dtmilli = 1536379689999L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9, 999).unixMillisLong)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.999 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.SSS z").parseLong("Sat, 08 Sep 2018 04:08:09.999 UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParsingDateTimesWithGreaterPrecisionThanMillisecond() {
|
||||||
|
val dtmilli = 1536379689999L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9, 999).unixMillisLong)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.9999 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.SSSS z").parseLong("Sat, 08 Sep 2018 04:08:09.9999 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.99999 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.SSSSS z").parseLong("Sat, 08 Sep 2018 04:08:09.99999 UTC")
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
message = "Sat, 08 Sep 2018 04:08:09.999999 UTC",
|
||||||
|
expected = dtmilli,
|
||||||
|
actual = DateFormat("EEE, dd MMM yyyy HH:mm:ss.SSSSSS z").parseLong("Sat, 08 Sep 2018 04:08:09.999999 UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParse() {
|
||||||
|
assertEquals("Mon, 18 Sep 2017 04:58:45 UTC", HttpDate.format(1505710725916L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReverseParse() {
|
||||||
|
val STR = "Tue, 19 Sep 2017 00:58:45 UTC"
|
||||||
|
assertEquals(STR, HttpDate.format(HttpDate.parse(STR)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseAmPmFormat() {
|
||||||
|
val format = DateFormat("MMM d, yyyy h:mm:ss a")
|
||||||
|
|
||||||
|
assertEquals("Tue, 07 Sep 2021 12:50:08 UTC", HttpDate.format(format.parse("Sep 7, 2021 12:50:08 pm")))
|
||||||
|
assertEquals("Tue, 07 Sep 2021 13:50:08 UTC", HttpDate.format(format.parse("Sep 7, 2021 1:50:08 PM")))
|
||||||
|
assertEquals("Tue, 07 Sep 2021 00:50:08 UTC", HttpDate.format(format.parse("Sep 7, 2021 12:50:08 am")))
|
||||||
|
assertEquals("Tue, 07 Sep 2021 00:00:00 UTC", HttpDate.format(format.parse("Sep 7, 2021 12:00:00 am")))
|
||||||
|
assertEquals("Tue, 07 Sep 2021 12:00:00 UTC", HttpDate.format(format.parse("Sep 7, 2021 12:00:00 pm")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCheckedCreation() {
|
||||||
|
assertEquals("Mon, 18 Sep 2017 23:58:45 UTC", HttpDate.format(DateTime(2017, 9, 18, 23, 58, 45)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreatedAdjusted() {
|
||||||
|
assertEquals(
|
||||||
|
"Thu, 18 Jan 2018 23:58:45 UTC",
|
||||||
|
HttpDate.format(DateTime.createAdjusted(2017, 13, 18, 23, 58, 45))
|
||||||
|
)
|
||||||
|
assertEquals("Mon, 18 Sep 2017 23:58:45 UTC", HttpDate.format(DateTime.createAdjusted(2017, 9, 18, 23, 58, 45)))
|
||||||
|
assertEquals(
|
||||||
|
"Mon, 01 Jan 2018 00:00:01 UTC",
|
||||||
|
HttpDate.format(DateTime.createAdjusted(2017, 12, 31, 23, 59, 61))
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Thu, 21 Mar 2024 19:32:20 UTC",
|
||||||
|
HttpDate.format(DateTime.createAdjusted(2017, 12, 31, 23, 59, 200_000_000))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreatedClamped() {
|
||||||
|
assertEquals("Mon, 18 Sep 2017 23:58:45 UTC", HttpDate.format(DateTime.createClamped(2017, 9, 18, 23, 58, 45)))
|
||||||
|
assertEquals("Mon, 18 Dec 2017 23:58:45 UTC", HttpDate.format(DateTime.createClamped(2017, 13, 18, 23, 58, 45)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSpecial() {
|
||||||
|
assertEquals("Mon, 01 Jan 0001 00:00:00 UTC", HttpDate.format(DateTime.createClamped(1, 1, 1, 0, 0, 0, 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBaseAdjust() {
|
||||||
|
val date = DateTime(Year(2018), Month.November, 4, 5, 54, 30)
|
||||||
|
|
||||||
|
assertEquals("Sun, 04 Nov 2018 05:54:30 GMT+0100", date.toOffsetUnadjusted((+60).minutes).toStringDefault())
|
||||||
|
assertEquals("Sun, 04 Nov 2018 06:54:30 GMT+0100", date.toOffset((+60).minutes).toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMinMaxClamp() {
|
||||||
|
val a = DateTime(Year(2018), Month.November, 4, 5, 54, 30)
|
||||||
|
val b = DateTime(Year(2018), Month.November, 4, 6, 54, 30)
|
||||||
|
val c = DateTime(Year(2018), Month.November, 4, 7, 54, 30)
|
||||||
|
assertEquals(a, min(a, b))
|
||||||
|
assertEquals(b, max(a, b))
|
||||||
|
assertEquals(b, a.clamp(b, c))
|
||||||
|
assertEquals(b, b.clamp(a, c))
|
||||||
|
assertEquals(a, a.clamp(a, c))
|
||||||
|
assertEquals(c, c.clamp(a, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStartEndDay() {
|
||||||
|
val date = DateTime(1568803601377)
|
||||||
|
val start = date.dateDayStart
|
||||||
|
val end = date.dateDayEnd
|
||||||
|
assertEquals("2019-09-18T00:00:00", ISO8601.DATETIME_COMPLETE.extended.format(start))
|
||||||
|
assertEquals("2019-09-18T23:59:59", ISO8601.DATETIME_COMPLETE.extended.format(end))
|
||||||
|
assertEquals(1568764800000L, start.unixMillisLong)
|
||||||
|
assertEquals(1568851199999L, end.unixMillisLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimeZones() {
|
||||||
|
"Tue, 19 Sep 2017 00:58:45 GMT-0800".let { STR -> assertEquals(STR, HttpDate.parse(STR).toStringDefault()) }
|
||||||
|
"Tue, 19 Sep 2017 00:58:45 GMT+0800".let { STR -> assertEquals(STR, HttpDate.parse(STR).toStringDefault()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug37() {
|
||||||
|
val format = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||||
|
format.parse("2019-04-15T17:28:46.862+0900")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug33() {
|
||||||
|
assertEquals("20190412", DateTime(2019, 4, 12).localUnadjusted.format("yyyyMMdd"))
|
||||||
|
assertEquals("2019年04月12日", Date(2019, 4, 12).format("yyyy年MM月dd日"))
|
||||||
|
assertEquals("2019年04月12日", Date(2019, 4, 12).format("yyyy'年'MM'月'dd'日'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug93() {
|
||||||
|
// 2020-03-20 11:13:31.317 +05:00
|
||||||
|
val dateTime = DateTime(
|
||||||
|
year = 2020, month = 3, day = 20,
|
||||||
|
hour = 11, minute = 13, second = 31, milliseconds = 317
|
||||||
|
)
|
||||||
|
val original = DateTimeTz.local(dateTime, TimezoneOffset(5.hours))
|
||||||
|
|
||||||
|
val formatter1 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS X")
|
||||||
|
val string1 = formatter1.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +05", string1)
|
||||||
|
assertEquals(original, formatter1.parse(string1))
|
||||||
|
|
||||||
|
val formatter2 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS XX")
|
||||||
|
val string2 = formatter2.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +0500", string2)
|
||||||
|
assertEquals(original, formatter2.parse(string2))
|
||||||
|
|
||||||
|
val formatter3 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS XXX")
|
||||||
|
val string3 = formatter3.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +05:00", string3)
|
||||||
|
assertEquals(original, formatter3.parse(string3))
|
||||||
|
|
||||||
|
val formatter4 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS x")
|
||||||
|
val string4 = formatter4.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +05", string4)
|
||||||
|
assertEquals(original, formatter4.parse(string4))
|
||||||
|
|
||||||
|
val formatter5 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS xx")
|
||||||
|
val string5 = formatter5.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +0500", string5)
|
||||||
|
assertEquals(original, formatter5.parse(string5))
|
||||||
|
|
||||||
|
val formatter6 = DateFormat("yyyy-MM-dd HH:mm:ss.SSS xxx")
|
||||||
|
val string6 = formatter6.format(original)
|
||||||
|
assertEquals("2020-03-20 11:13:31.317 +05:00", string6)
|
||||||
|
assertEquals(original, formatter6.parse(string6))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug103() {
|
||||||
|
assertEquals(
|
||||||
|
"Fri, 15 Oct -0249 19:33:20 UTC",
|
||||||
|
DateTime.fromUnixMillis(-70000000000000L).toStringDefault()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Mon, 01 Jan 0001 00:00:00 UTC",
|
||||||
|
DateTime.fromUnixMillis(-62135596800000L).toStringDefault()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Sat, 11 Aug -0027 08:00:00 UTC",
|
||||||
|
DateTime.fromUnixMillis(-63000000000000L).toStringDefault()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Sun, 31 Dec 0000 23:59:59 UTC",
|
||||||
|
DateTime.fromUnixMillis(-62135596800000L - 1L).toStringDefault()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug123() {
|
||||||
|
val str1 = "1989-01-01T10:00:00Z"
|
||||||
|
val str2 = "1989-01-01T10:00:00.000Z"
|
||||||
|
assertEquals(str1, DateTime.parse(str1).format(DateFormat.FORMAT1))
|
||||||
|
assertEquals(str2, DateTime.parse(str2).format(DateFormat.FORMAT2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIssue131() {
|
||||||
|
assertEquals(
|
||||||
|
"2020-07-23T12:30:52.999000000Z",
|
||||||
|
DateTime(1595507452999L).format("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDaySuffix() {
|
||||||
|
// 2020-03-21 11:13:31.317 +05:00
|
||||||
|
val dateTime = DateTime(
|
||||||
|
year = 2020, month = 3, day = 21,
|
||||||
|
hour = 11, minute = 13, second = 31, milliseconds = 317
|
||||||
|
)
|
||||||
|
val original = DateTimeTz.local(dateTime, TimezoneOffset(5.hours))
|
||||||
|
|
||||||
|
val formatter = DateFormat("yyyy-MM-do HH:mm:ss.SSS xxx")
|
||||||
|
val string = formatter.format(original)
|
||||||
|
assertEquals("2020-03-21st 11:13:31.317 +05:00", string)
|
||||||
|
assertEquals(original, formatter.parse(string))
|
||||||
|
|
||||||
|
// 2020-03-22 11:13:31.317 +05:00
|
||||||
|
val dateTime2 = DateTime(
|
||||||
|
year = 2020, month = 3, day = 22,
|
||||||
|
hour = 11, minute = 13, second = 31, milliseconds = 317
|
||||||
|
)
|
||||||
|
val original2 = DateTimeTz.local(dateTime2, TimezoneOffset(5.hours))
|
||||||
|
|
||||||
|
val formatter2 = DateFormat("yyyy-MM-do HH:mm:ss.SSS xxx")
|
||||||
|
val string2 = formatter2.format(original2)
|
||||||
|
assertEquals("2020-03-22nd 11:13:31.317 +05:00", string2)
|
||||||
|
assertEquals(original2, formatter2.parse(string2))
|
||||||
|
|
||||||
|
|
||||||
|
// 2020-03-20 11:13:31.317 +05:00
|
||||||
|
val dateTime3 = DateTime(
|
||||||
|
year = 2020, month = 3, day = 20,
|
||||||
|
hour = 11, minute = 13, second = 31, milliseconds = 317
|
||||||
|
)
|
||||||
|
val original3 = DateTimeTz.local(dateTime3, TimezoneOffset(5.hours))
|
||||||
|
|
||||||
|
val formatter3 = DateFormat("yyyy-MM-do HH:mm:ss.SSS xxx")
|
||||||
|
val string3 = formatter3.format(original3)
|
||||||
|
assertEquals("2020-03-20th 11:13:31.317 +05:00", string3)
|
||||||
|
assertEquals(original3, formatter3.parse(string3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIssue154() {
|
||||||
|
assertEquals(TimezoneOffset(9.hours), DateFormat("z").parse("+09:00").offset)
|
||||||
|
assertEquals(TimezoneOffset((-9).hours), DateFormat("z").parse("-09:00").offset)
|
||||||
|
assertEquals(TimezoneOffset(15.hours), DateFormat("z").parse("+15:00").offset)
|
||||||
|
assertEquals(TimezoneOffset((-15).hours), DateFormat("z").parse("-15:00").offset)
|
||||||
|
assertEquals(TimezoneOffset(0.hours), DateFormat("z").parse("+00:00").offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPatternFormatRegex() {
|
||||||
|
val dtmilli = 1536379689000L
|
||||||
|
assertEquals(dtmilli, DateTime(2018, 9, 8, 4, 8, 9).unixMillisLong)
|
||||||
|
|
||||||
|
val fmt = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
|
||||||
|
val msg = "[Sat, 08 Sep 2018 04:08:09 UTC] Example log message"
|
||||||
|
val nomsg = "[Sat, 08 Sep 20G8 04:08:09 UTC] Example log message" // 20G8
|
||||||
|
|
||||||
|
val logPattern = Regex("""^\[(""" + fmt.matchingRegexString() + """)\] """)
|
||||||
|
|
||||||
|
assertTrue(logPattern.containsMatchIn(msg), message = "correct datestamp should match")
|
||||||
|
assertFalse(logPattern.containsMatchIn(nomsg), message = "incorrect datestamp shouldn't match")
|
||||||
|
|
||||||
|
val match = logPattern.find(msg)
|
||||||
|
assertNotNull(match)
|
||||||
|
|
||||||
|
assertEquals(dtmilli, fmt.parseLong(match.groups[1]!!.value), message = "datestamp parsed from log line has correct value")
|
||||||
|
assertEquals("Example log message", msg.drop(match.value.length), message = "total match length for composed regex")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testThrowOnInvalid() {
|
||||||
|
assertEquals("Invalid hour 25", assertFailsWith<RuntimeException> {
|
||||||
|
DateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-02-01 25:00:00", doAdjust = false)
|
||||||
|
}.message)
|
||||||
|
|
||||||
|
DateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-02-01 25:00:00", doAdjust = true)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class DateTimeWithOffsetTest {
|
||||||
|
val date1 = (DateTime(2018, 3, 2, 1) + 100.seconds)
|
||||||
|
val date2 = (DateTime(2018, 3, 2, 1) + 100.seconds + 60.minutes)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test1() {
|
||||||
|
assertEquals(date1.toOffsetUnadjusted((+60).minutes), date1.toOffsetUnadjusted((+60).minutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
assertEquals(date1.toOffsetUnadjusted((-60).minutes), date2.toOffsetUnadjusted(0.minutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test4() {
|
||||||
|
assertNotEquals(date1, date2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test5() {
|
||||||
|
assertTrue(date1.toOffsetUnadjusted(0.minutes) > date1.toOffsetUnadjusted((+60).minutes))
|
||||||
|
assertTrue(date1.toOffsetUnadjusted(0.minutes) < date1.toOffsetUnadjusted((-60).minutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test6() {
|
||||||
|
assertEquals(date1.toOffsetUnadjusted(0.minutes), date1.toOffsetUnadjusted((+0).minutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test7() {
|
||||||
|
assertEquals(date1.toOffset((+60).minutes), date1.toOffset((+0).minutes))
|
||||||
|
assertTrue(date1.toOffsetUnadjusted((-60).minutes) > date1.toOffsetUnadjusted((+0).minutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test8() {
|
||||||
|
assertEquals(
|
||||||
|
DateTime(Year(2018), Month.March, 2).toOffsetUnadjusted((+60).minutes),
|
||||||
|
DateTime(Year(2018), Month.February, 2).toOffsetUnadjusted((+60).minutes) + 1.months
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test9() {
|
||||||
|
val format = "yyyy-MM-dd HH:mm:ss z"
|
||||||
|
val date = DateTimeTz.local(DateTime(Year(2018), Month.March, 2), offset = 60.minutes.offset)
|
||||||
|
assertEquals("2018-03-02 00:00:00 GMT+0100", date.toString(format))
|
||||||
|
assertEquals("2018-03-02 00:00:00 GMT+0300", date.addOffsetUnadjusted((+120).minutes).toString(format))
|
||||||
|
}
|
||||||
|
}
|
73
klock/src/commonTest/kotlin/korlibs/time/DayOfWeekTest.kt
Normal file
73
klock/src/commonTest/kotlin/korlibs/time/DayOfWeekTest.kt
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.locale.spanish
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DayOfWeekTest {
|
||||||
|
@Test
|
||||||
|
fun testFirstDayOfWeek() {
|
||||||
|
assertEquals(DayOfWeek.Sunday, DayOfWeek.firstDayOfWeek(KlockLocale.english))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsWeekend() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(true, false, false, false, false, false, true),
|
||||||
|
DayOfWeek.values().map { it.isWeekend(KlockLocale.english) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNext() {
|
||||||
|
assertEquals(DayOfWeek.Monday, DayOfWeek.Sunday.next)
|
||||||
|
assertEquals(DayOfWeek.Tuesday, DayOfWeek.Monday.next)
|
||||||
|
assertEquals(DayOfWeek.Wednesday, DayOfWeek.Tuesday.next)
|
||||||
|
assertEquals(DayOfWeek.Thursday, DayOfWeek.Wednesday.next)
|
||||||
|
assertEquals(DayOfWeek.Friday, DayOfWeek.Thursday.next)
|
||||||
|
assertEquals(DayOfWeek.Saturday, DayOfWeek.Friday.next)
|
||||||
|
assertEquals(DayOfWeek.Sunday, DayOfWeek.Saturday.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrev() {
|
||||||
|
assertEquals(DayOfWeek.Saturday, DayOfWeek.Sunday.prev)
|
||||||
|
assertEquals(DayOfWeek.Sunday, DayOfWeek.Monday.prev)
|
||||||
|
assertEquals(DayOfWeek.Monday, DayOfWeek.Tuesday.prev)
|
||||||
|
assertEquals(DayOfWeek.Tuesday, DayOfWeek.Wednesday.prev)
|
||||||
|
assertEquals(DayOfWeek.Wednesday, DayOfWeek.Thursday.prev)
|
||||||
|
assertEquals(DayOfWeek.Thursday, DayOfWeek.Friday.prev)
|
||||||
|
assertEquals(DayOfWeek.Friday, DayOfWeek.Saturday.prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompareLocale() {
|
||||||
|
assertEquals(true, DayOfWeek.Monday.withLocale(KlockLocale.spanish) < DayOfWeek.Sunday.withLocale(KlockLocale.spanish))
|
||||||
|
assertEquals(true, DayOfWeek.Monday.withLocale(KlockLocale.english) > DayOfWeek.Sunday.withLocale(KlockLocale.english))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(0, 1, 2, 3, 4, 5, 6),
|
||||||
|
(0 until 7).map { DayOfWeek[it].index0Locale(KlockLocale.english) }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(6, 0, 1, 2, 3, 4, 5),
|
||||||
|
(0 until 7).map { DayOfWeek[it].index0Locale(KlockLocale.spanish) }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday,
|
||||||
|
DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday
|
||||||
|
),
|
||||||
|
(0 until 7).map { DayOfWeek.get0(it, KlockLocale.english) }
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday,
|
||||||
|
DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday
|
||||||
|
),
|
||||||
|
(0 until 7).map { DayOfWeek.get0(it, KlockLocale.spanish) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
24
klock/src/commonTest/kotlin/korlibs/time/FrequencyTest.kt
Normal file
24
klock/src/commonTest/kotlin/korlibs/time/FrequencyTest.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class FrequencyTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
assertEquals(100.milliseconds, 10.timesPerSecond.timeSpan)
|
||||||
|
assertEquals(10.hz, 100.milliseconds.hz)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFrequencyOperatorsWorkAsExpected() {
|
||||||
|
assertEquals((-60).hz, -(60.hz))
|
||||||
|
assertEquals((+60).hz, +(60.hz))
|
||||||
|
assertEquals(80.hz, 60.hz + 20.hz)
|
||||||
|
assertEquals(40.hz, 60.hz - 20.hz)
|
||||||
|
assertEquals(120.hz, 60.hz * 2)
|
||||||
|
assertEquals(90.hz, 60.hz * 1.5)
|
||||||
|
assertEquals(90.hz, 60.hz * 1.5f)
|
||||||
|
assertEquals(5.hz, 65.hz % 30.hz)
|
||||||
|
}
|
||||||
|
}
|
181
klock/src/commonTest/kotlin/korlibs/time/ISO8601Test.kt
Normal file
181
klock/src/commonTest/kotlin/korlibs/time/ISO8601Test.kt
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class ISO8601Test {
|
||||||
|
@Test
|
||||||
|
fun testDate() {
|
||||||
|
val date = DateTime(2019, Month.April, 14)
|
||||||
|
assertEquals("2019-04-14", date.format(ISO8601.DATE_CALENDAR_COMPLETE))
|
||||||
|
assertEquals("2019-04-14", date.format(ISO8601.DATE_CALENDAR_COMPLETE.extended))
|
||||||
|
assertEquals("20190414", date.format(ISO8601.DATE_CALENDAR_COMPLETE.basic))
|
||||||
|
|
||||||
|
assertEquals("2019-04", date.format(ISO8601.DATE_CALENDAR_REDUCED0))
|
||||||
|
assertEquals("2019-04", date.format(ISO8601.DATE_CALENDAR_REDUCED0.extended))
|
||||||
|
assertEquals("2019-04", date.format(ISO8601.DATE_CALENDAR_REDUCED0.basic))
|
||||||
|
|
||||||
|
assertEquals("2019", date.format(ISO8601.DATE_CALENDAR_REDUCED1))
|
||||||
|
assertEquals("2019", date.format(ISO8601.DATE_CALENDAR_REDUCED1.extended))
|
||||||
|
assertEquals("2019", date.format(ISO8601.DATE_CALENDAR_REDUCED1.basic))
|
||||||
|
|
||||||
|
assertEquals("19", date.format(ISO8601.DATE_CALENDAR_REDUCED2))
|
||||||
|
assertEquals("19", date.format(ISO8601.DATE_CALENDAR_REDUCED2.extended))
|
||||||
|
assertEquals("19", date.format(ISO8601.DATE_CALENDAR_REDUCED2.basic))
|
||||||
|
|
||||||
|
assertEquals("+002019-04-14", date.format(ISO8601.DATE_CALENDAR_EXPANDED0))
|
||||||
|
|
||||||
|
assertEquals("2019-W15-7", date.format(ISO8601.DATE_WEEK_COMPLETE))
|
||||||
|
assertEquals("+002019-W15-7", date.format(ISO8601.DATE_WEEK_EXPANDED0))
|
||||||
|
|
||||||
|
assertEquals(date, ISO8601.DATE.parse("2019-04-14").utc)
|
||||||
|
assertEquals(date, ISO8601.DATE.parse("2019-W15-7").utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTime() {
|
||||||
|
val time = 15.hours + 30.minutes + 12.seconds
|
||||||
|
assertEquals("15:30:12", ISO8601.TIME_LOCAL_COMPLETE.format(time))
|
||||||
|
assertEquals("153012", ISO8601.TIME_LOCAL_COMPLETE.basic.format(time))
|
||||||
|
|
||||||
|
assertEquals("15:30:12,00", ISO8601.TIME_LOCAL_FRACTION0.format(time))
|
||||||
|
assertEquals("15:30:12,63", ISO8601.TIME_LOCAL_FRACTION0.format(time + 630.milliseconds))
|
||||||
|
|
||||||
|
assertEquals("15:30,20", ISO8601.TIME_LOCAL_FRACTION1.format(time))
|
||||||
|
assertEquals("15,50", ISO8601.TIME_LOCAL_FRACTION2.format(time))
|
||||||
|
|
||||||
|
assertEquals("15,50Z", ISO8601.TIME_UTC_FRACTION2.format(time))
|
||||||
|
|
||||||
|
assertEquals(time + 630.milliseconds, ISO8601.TIME_LOCAL_FRACTION0.parse("15:30:12,63"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInterval() {
|
||||||
|
if (KotlinVersion.CURRENT < KotlinVersion(1, 9, 20)) return
|
||||||
|
|
||||||
|
assertEquals(24.hours, 1.days)
|
||||||
|
assertEquals((27 * 24).hours, 27.days)
|
||||||
|
val time = 1.years + 0.months + 27.days + 11.hours + 9.minutes + 11.seconds
|
||||||
|
assertEquals("P1Y0M27DT11H9M11S", ISO8601.INTERVAL_COMPLETE0.format(time))
|
||||||
|
|
||||||
|
assertEquals(time, ISO8601.INTERVAL_COMPLETE0.parse("P1Y0M27DT11H9M11S"))
|
||||||
|
assertEquals(time, ISO8601.INTERVAL.parse("P1Y0M27DT11H9M11S"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWeekOfYear() {
|
||||||
|
assertEquals(1, DateTime(2019, Month.January, 1).dayOfYear)
|
||||||
|
|
||||||
|
assertEquals(1, DateTime(2019, Month.January, 1).weekOfYear1)
|
||||||
|
assertEquals(1, DateTime(2019, Month.January, 6).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2019, Month.January, 7).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2019, Month.January, 13).weekOfYear1)
|
||||||
|
assertEquals(3, DateTime(2019, Month.January, 14).weekOfYear1)
|
||||||
|
|
||||||
|
assertEquals(1, DateTime(2018, Month.January, 1).weekOfYear1)
|
||||||
|
assertEquals(1, DateTime(2018, Month.January, 7).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2018, Month.January, 8).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2018, Month.January, 14).weekOfYear1)
|
||||||
|
assertEquals(3, DateTime(2018, Month.January, 15).weekOfYear1)
|
||||||
|
|
||||||
|
assertEquals(1, DateTime(2018, Month.January, 1).weekOfYear1)
|
||||||
|
assertEquals(1, DateTime(2018, Month.January, 7).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2018, Month.January, 8).weekOfYear1)
|
||||||
|
assertEquals(2, DateTime(2018, Month.January, 14).weekOfYear1)
|
||||||
|
assertEquals(3, DateTime(2018, Month.January, 15).weekOfYear1)
|
||||||
|
|
||||||
|
assertEquals(44, DateTime(2007, Month.November, 3).weekOfYear1)
|
||||||
|
assertEquals(6, DateTime(2007, Month.November, 3).dayOfWeek.index1Monday)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDateTimeComplete() {
|
||||||
|
assertEquals("20190917T114805", ISO8601.DATETIME_COMPLETE.basic.format(1568720885000))
|
||||||
|
assertEquals("2019-09-17T11:48:05", ISO8601.DATETIME_COMPLETE.extended.format(1568720885000))
|
||||||
|
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_COMPLETE.parse("20190917T114805").utc.toStringDefault())
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_COMPLETE.parse("2019-09-17T11:48:05").utc.toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDateTimeUtcComplete() {
|
||||||
|
assertEquals("20190917T114805Z", ISO8601.DATETIME_UTC_COMPLETE.basic.format(1568720885000))
|
||||||
|
assertEquals("2019-09-17T11:48:05Z", ISO8601.DATETIME_UTC_COMPLETE.extended.format(1568720885000))
|
||||||
|
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_UTC_COMPLETE.parse("20190917T114805Z").utc.toStringDefault())
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_UTC_COMPLETE.parse("2019-09-17T11:48:05Z").utc.toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDateTimeUtcCompleteFraction() {
|
||||||
|
assertEquals("20190917T114805.000Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.basic.format(1568720885000))
|
||||||
|
assertEquals("2019-09-17T11:48:05.000Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.extended.format(1568720885000))
|
||||||
|
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.parse("20190917T114805.000Z").utc.toStringDefault())
|
||||||
|
assertEquals("Tue, 17 Sep 2019 11:48:05 UTC", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.parse("2019-09-17T11:48:05.000Z").utc.toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDateTimeUtcFraction() {
|
||||||
|
assertEquals("2019-09-17T11:48:05.123Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.extended.format(1568720885123))
|
||||||
|
assertEquals("2019-09-17T11:48:05.023Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.extended.format(1568720885023))
|
||||||
|
assertEquals("2022-02-05T15:32:18.096Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.extended.format(1644075138096))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIssue84() {
|
||||||
|
if (KotlinVersion.CURRENT < KotlinVersion(1, 9, 20)) return // @TODO: This should be only for WASM
|
||||||
|
|
||||||
|
val badUtc = DateTime(
|
||||||
|
date = Date(2020, 1, 4),
|
||||||
|
time = Time(2, 42, 55, millisecond = 500)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"2020-01-04T02:42:55,50",
|
||||||
|
badUtc.format(ISO8601.IsoDateTimeFormat("YYYYMMDDThhmmss,ss", "YYYY-MM-DDThh:mm:ss,ss"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIssue102() {
|
||||||
|
val time = 15.hours + 30.minutes + 12.seconds + 160.milliseconds
|
||||||
|
|
||||||
|
assertEquals("15:30:12", ISO8601.IsoTimeFormat("hhmmss", "hh:mm:ss").format(time))
|
||||||
|
assertEquals("153012", ISO8601.IsoTimeFormat("hhmmss", "hh:mm:ss").basic.format(time))
|
||||||
|
|
||||||
|
assertEquals("15:30:12.2", ISO8601.IsoTimeFormat("hhmmss.s", "hh:mm:ss.s").format(time))
|
||||||
|
assertEquals("15:30:12,2", ISO8601.IsoTimeFormat("hhmmss,s", "hh:mm:ss,s").format(time))
|
||||||
|
assertEquals("15:30:12.16", ISO8601.IsoTimeFormat("hhmmss.ss", "hh:mm:ss.ss").format(time))
|
||||||
|
assertEquals("15:30:12,16", ISO8601.IsoTimeFormat("hhmmss,ss", "hh:mm:ss,ss").format(time))
|
||||||
|
assertEquals("15:30:12.160", ISO8601.IsoTimeFormat("hhmmss.sss", "hh:mm:ss.sss").format(time))
|
||||||
|
assertEquals("15:30:12,160", ISO8601.IsoTimeFormat("hhmmss,sss", "hh:mm:ss,sss").format(time))
|
||||||
|
|
||||||
|
assertEquals("15:30.2", ISO8601.IsoTimeFormat("hhmm.m", "hh:mm.m").format(time))
|
||||||
|
assertEquals("15:30,2", ISO8601.IsoTimeFormat("hhmm,m", "hh:mm,m").format(time))
|
||||||
|
assertEquals("15:30.20", ISO8601.IsoTimeFormat("hhmm.mm", "hh:mm.mm").format(time))
|
||||||
|
assertEquals("15:30,20", ISO8601.IsoTimeFormat("hhmm,mm", "hh:mm,mm").format(time))
|
||||||
|
|
||||||
|
assertEquals("15.5", ISO8601.IsoTimeFormat("hh.h", "hh.h").format(time))
|
||||||
|
assertEquals("15,5", ISO8601.IsoTimeFormat("hh,h", "hh,h").format(time))
|
||||||
|
assertEquals("15.50", ISO8601.IsoTimeFormat("hh.hh", "hh.hh").format(time))
|
||||||
|
assertEquals("15,50", ISO8601.IsoTimeFormat("hh,hh", "hh,hh").format(time))
|
||||||
|
|
||||||
|
assertEquals("15,5Z", ISO8601.IsoTimeFormat("hh,hZ", null).format(time))
|
||||||
|
|
||||||
|
assertEquals(time, ISO8601.IsoTimeFormat("hhmmss,ss", "hh:mm:ss,ss").parse("15:30:12,16"))
|
||||||
|
assertEquals(time, ISO8601.IsoTimeFormat("hhmmss.sss", "hh:mm:ss.sss").parse("15:30:12.160"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimeZoneParsing() {
|
||||||
|
val string1 = "2022-10-06T19:57:29.285+02:00"
|
||||||
|
val string2 = "2022-10-06T17:57:29.285+00:00"
|
||||||
|
|
||||||
|
val parsed1 = ISO8601.DATETIME_UTC_COMPLETE_FRACTION.parse(string1)
|
||||||
|
val parsed2 = ISO8601.DATETIME_UTC_COMPLETE_FRACTION.parse(string2)
|
||||||
|
|
||||||
|
assertEquals(string1, ISO8601.DATETIME_UTC_COMPLETE_FRACTION.format(parsed1))
|
||||||
|
assertEquals(string2.removeSuffix("+00:00") + "Z", ISO8601.DATETIME_UTC_COMPLETE_FRACTION.format(parsed2))
|
||||||
|
assertEquals(parsed1.utc, parsed2.utc)
|
||||||
|
}
|
||||||
|
}
|
24
klock/src/commonTest/kotlin/korlibs/time/IssuesTest.kt
Normal file
24
klock/src/commonTest/kotlin/korlibs/time/IssuesTest.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class IssuesTest {
|
||||||
|
@Test
|
||||||
|
fun testIssue73() {
|
||||||
|
val dateFormat = DateFormat("yyyy-mm-dd HH:mm z")
|
||||||
|
|
||||||
|
fun tparse(str: String) = dateFormat.parse(str).local.toString("hh:mm a")
|
||||||
|
|
||||||
|
assertEquals("11:10 am", tparse("2019-10-17 11:10 +12")) // Gives 11:10 am
|
||||||
|
assertEquals("12:10 pm", tparse("2019-10-17 12:10 +12")) // Gives 00:10 pm, I believe it should be 12:10 pm
|
||||||
|
assertEquals("01:10 pm", tparse("2019-10-17 13:10 +12")) // Gives 01:10 pm
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIssue81() {
|
||||||
|
val format = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS")
|
||||||
|
val dateStr = "2019-04-26T19:00:00.0000000"
|
||||||
|
assertEquals(dateStr, format.format(format.parse(dateStr)))
|
||||||
|
}
|
||||||
|
}
|
23
klock/src/commonTest/kotlin/korlibs/time/KlockTest.kt
Normal file
23
klock/src/commonTest/kotlin/korlibs/time/KlockTest.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class KlockTest {
|
||||||
|
//@Test
|
||||||
|
@Test
|
||||||
|
fun testTimeAdvances() {
|
||||||
|
val time1 = DateTime.nowUnixMillis()
|
||||||
|
assertTrue("Time is provided in milliseconds since EPOCH. Expected ($time1 >= 1508887000000)") { time1 >= 1508887000000 }
|
||||||
|
while (true) {
|
||||||
|
val time2 = DateTime.nowUnixMillis()
|
||||||
|
assertTrue("Time advances") { time2 >= time1 }
|
||||||
|
if (time2 > time1) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testThatLocalTimezoneOffsetRuns() {
|
||||||
|
assertTrue(TimezoneOffset.local(DateTime(0L)).time.seconds != -1.0)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class KotlinNativeImmutabilityTest {
|
||||||
|
@Test
|
||||||
|
fun testImmutabilityDateFormat() {
|
||||||
|
assertEquals("2019-09-01", myDateFormat1a.let { format -> format.parse("2019-09-01").toString(format) })
|
||||||
|
assertEquals("2019-09-01", myDateFormat2a.let { format -> format.parse("2019-09-01").toString(format) })
|
||||||
|
assertEquals("2019-09-01", myDateFormat1b.let { format -> format.parse("2019-09-01").toString(format) })
|
||||||
|
assertEquals("2019-09-01", myDateFormat2b.let { format -> format.parse("2019-09-01").toString(format) })
|
||||||
|
assertEquals("2019-09-01", DateFormat.FORMAT_DATE.let { format -> format.parse("2019-09-01").toString(format) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testImmutabilityDateTimeRange() {
|
||||||
|
assertEquals("0.1s", myDateTimeRange1a.span.toString())
|
||||||
|
assertEquals("0.1s", myDateTimeRange1b.span.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testImmutabilityKlockLocale() {
|
||||||
|
assertEquals(12, myKlockLocale1a.monthsShort.size)
|
||||||
|
assertEquals(7, myKlockLocale1a.daysOfWeekShort.size)
|
||||||
|
assertEquals(12, myKlockLocale1b.monthsShort.size)
|
||||||
|
assertEquals(7, myKlockLocale1b.daysOfWeekShort.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val myDateFormat1b = DateFormat("YYYY-MM-dd")
|
||||||
|
private val myDateFormat2b = DateFormat("YYYY[-MM[-dd]]").withOptional()
|
||||||
|
private val myDateTimeRange1b = DateTimeRange(DateTime(0L), DateTime(100L))
|
||||||
|
private val myKlockLocale1b = KlockLocale.English()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val myDateFormat1a = DateFormat("YYYY-MM-dd")
|
||||||
|
private val myDateFormat2a = DateFormat("YYYY[-MM[-dd]]").withOptional()
|
||||||
|
|
||||||
|
private val myDateTimeRange1a = DateTimeRange(DateTime(0L), DateTime(100L))
|
||||||
|
private val myKlockLocale1a = KlockLocale.English()
|
20
klock/src/commonTest/kotlin/korlibs/time/MeasureTest.kt
Normal file
20
klock/src/commonTest/kotlin/korlibs/time/MeasureTest.kt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class MeasureTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val result = measureTimeWithResult {
|
||||||
|
val start = DateTime.now()
|
||||||
|
do {
|
||||||
|
val current = DateTime.now()
|
||||||
|
} while (current - start < 40.milliseconds)
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
assertEquals("hello", result.result)
|
||||||
|
assertTrue("Near 40.milliseconds != ${result.time}") { result.time >= 20.milliseconds && result.time <= 1.seconds }
|
||||||
|
}
|
||||||
|
}
|
109
klock/src/commonTest/kotlin/korlibs/time/MonthTest.kt
Normal file
109
klock/src/commonTest/kotlin/korlibs/time/MonthTest.kt
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.Month.April
|
||||||
|
import korlibs.time.Month.August
|
||||||
|
import korlibs.time.Month.December
|
||||||
|
import korlibs.time.Month.February
|
||||||
|
import korlibs.time.Month.January
|
||||||
|
import korlibs.time.Month.July
|
||||||
|
import korlibs.time.Month.June
|
||||||
|
import korlibs.time.Month.March
|
||||||
|
import korlibs.time.Month.May
|
||||||
|
import korlibs.time.Month.November
|
||||||
|
import korlibs.time.Month.October
|
||||||
|
import korlibs.time.Month.September
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class MonthTest {
|
||||||
|
@Test
|
||||||
|
fun testBasicMonthMetrics() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
|
||||||
|
0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335
|
||||||
|
),
|
||||||
|
listOf(false, true).map { leap -> (1..12).map { Month(it).daysToStart(leap = leap) } }.flatMap { it }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
365, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365,
|
||||||
|
366, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366
|
||||||
|
),
|
||||||
|
listOf(false, true).map { leap -> (0..12).map { Month(it).daysToEnd(leap = leap) } }.flatMap { it }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
|
||||||
|
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||||
|
),
|
||||||
|
listOf(false, true).map { leap -> (1..12).map { Month(it).days(leap = leap) } }.flatMap { it }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(January, February, March, April, May, June, July, August, September, October, November, December),
|
||||||
|
(1..12).map { Month(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromDayOfYear() {
|
||||||
|
fun Month.Companion.fromDayOfYearSlow(day: Int, isLeap: Boolean): Month? {
|
||||||
|
val table = MONTH_TABLE(isLeap)
|
||||||
|
for ((month, range) in table) if (day in range) return month
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (leap in listOf(false, true)) {
|
||||||
|
for (day in -100..Year.days(leap) + 100) {
|
||||||
|
val month = Month.fromDayOfYear(day, leap = leap)
|
||||||
|
val monthSure = Month.fromDayOfYearSlow(day, isLeap = leap)
|
||||||
|
assertEquals(monthSure, month, "day=$day, monthSure=$monthSure, month=$month, leap=$leap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDaysInMonth() {
|
||||||
|
for (leap in listOf(false, true)) {
|
||||||
|
val table = MONTH_TABLE(leap)
|
||||||
|
for ((month, range) in table) {
|
||||||
|
assertEquals((range.endInclusive - range.start + 1), month.days(leap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MONTH_TABLE_COMMON = mapOf(
|
||||||
|
Month.January to 1..31,
|
||||||
|
Month.February to 32..59,
|
||||||
|
Month.March to 60..90,
|
||||||
|
Month.April to 91..120,
|
||||||
|
Month.May to 121..151,
|
||||||
|
Month.June to 152..181,
|
||||||
|
Month.July to 182..212,
|
||||||
|
Month.August to 213..243,
|
||||||
|
Month.September to 244..273,
|
||||||
|
Month.October to 274..304,
|
||||||
|
Month.November to 305..334,
|
||||||
|
Month.December to 335..365
|
||||||
|
)
|
||||||
|
|
||||||
|
val MONTH_TABLE_LEAP = mapOf(
|
||||||
|
Month.January to 1..31,
|
||||||
|
Month.February to 32..60,
|
||||||
|
Month.March to 61..91,
|
||||||
|
Month.April to 92..121,
|
||||||
|
Month.May to 122..152,
|
||||||
|
Month.June to 153..182,
|
||||||
|
Month.July to 183..213,
|
||||||
|
Month.August to 214..244,
|
||||||
|
Month.September to 245..274,
|
||||||
|
Month.October to 275..305,
|
||||||
|
Month.November to 306..335,
|
||||||
|
Month.December to 336..366
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MONTH_TABLE(leap: Boolean) = if (leap) MONTH_TABLE_LEAP else MONTH_TABLE_COMMON
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class NumberOfTimesTest {
|
||||||
|
@Test
|
||||||
|
fun testInfinite() {
|
||||||
|
assertEquals(infiniteTimes, infiniteTimes.oneLess)
|
||||||
|
assertEquals(infiniteTimes, infiniteTimes + 1.times)
|
||||||
|
assertEquals(infiniteTimes, infiniteTimes - 1.times)
|
||||||
|
assertEquals(0.times, infiniteTimes - infiniteTimes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOps() {
|
||||||
|
assertEquals(0.times, 1.times.oneLess)
|
||||||
|
assertEquals((-1).times, 0.times.oneLess)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimes() {
|
||||||
|
assertEquals(true, infiniteTimes.hasMore)
|
||||||
|
assertEquals(true, 1.times.hasMore)
|
||||||
|
assertEquals(false, 0.times.hasMore)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class SimplerDateFormatTest {
|
||||||
|
// Sun, 06 Nov 1994 08:49:37 GMT
|
||||||
|
val format = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParse() {
|
||||||
|
assertEquals(784111777000, format.parseLong("Sun, 06 Nov 1994 08:49:37 UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormat() {
|
||||||
|
assertEquals("Sun, 06 Nov 1994 08:49:37 UTC", format.format(784111777000))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseFormat() {
|
||||||
|
val dateStr = "Sun, 06 Nov 1994 08:49:37 UTC"
|
||||||
|
assertEquals(dateStr, format.format(format.parseDouble(dateStr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBug67() {
|
||||||
|
assertEquals(31, DateTime(2000, 12, 31).dayOfMonth)
|
||||||
|
//assertEquals(31, DateFormat("yyyy-MM-dd'T'HH:mm:ss").parse("2000-12-31T00:00:00").dayOfMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
class StrictOffset {
|
||||||
|
val format = DateFormat("yyyy-MM-dd'T'HH:mm:ssxxx")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseUtc() {
|
||||||
|
assertEquals(1462390174000, format.parseLong("2016-05-04T19:29:34+00:00"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormatUtc() {
|
||||||
|
assertEquals("2016-05-04T19:29:34+00:00", format.format(1462390174000))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormatWithNegativeOffset() {
|
||||||
|
val now = DateTime.fromUnixMillis(1462390174000)
|
||||||
|
.toOffsetUnadjusted((-3.5).hours)
|
||||||
|
val formatted = now.format(format)
|
||||||
|
assertEquals("2016-05-04T19:29:34-03:30", formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormatWithPositiveOffset() {
|
||||||
|
val now = DateTime.fromUnixMillis(1462390174000)
|
||||||
|
.toOffsetUnadjusted(4.5.hours)
|
||||||
|
val formatted = now.format(format)
|
||||||
|
assertEquals("2016-05-04T19:29:34+04:30", formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseFormatUtc() {
|
||||||
|
val dateStr = "2016-05-04T19:29:34+00:00"
|
||||||
|
assertEquals(dateStr, format.format(format.parseDouble(dateStr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseWithOffsetAsUtc() {
|
||||||
|
val offsetDateStr = "2016-05-04T19:29:34+05:00"
|
||||||
|
val utcDateStr = "2016-05-04T14:29:34+00:00"
|
||||||
|
assertEquals(format.parse(offsetDateStr).utc, format.parse(utcDateStr).utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseWithOffset() {
|
||||||
|
assertEquals(1462390174000, format.parseLong("2016-05-04T19:29:34-07:00"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseFormatOffset() {
|
||||||
|
val dateStr = "2016-05-04T19:29:34+05:00"
|
||||||
|
val date = format.parse(dateStr)
|
||||||
|
//println(date.base)
|
||||||
|
//println(date.offset)
|
||||||
|
assertEquals(dateStr, format.format(date))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseWithZuluFails() {
|
||||||
|
val dateStr = "2016-05-04T19:29:34Z"
|
||||||
|
assertFailsWith(RuntimeException::class, "Zulu Time Zone is only accepted with X-XXX formats.") {
|
||||||
|
format.parseDouble(dateStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZuluCapableOffset {
|
||||||
|
val format = DateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseUtc() {
|
||||||
|
assertEquals(1462390174000, format.parseLong("2016-05-04T19:29:34+00:00"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseZulu() {
|
||||||
|
assertEquals(1462390174000, format.parseLong("2016-05-04T19:29:34Z"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormatUtc() {
|
||||||
|
assertEquals("2016-05-04T19:29:34Z", format.format(1462390174000))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseWithUtcOffsetFormatsWithZulu() {
|
||||||
|
val dateStr = "2016-05-04T19:29:34+00:00"
|
||||||
|
val expectedStr = "2016-05-04T19:29:34Z"
|
||||||
|
assertEquals(expectedStr, format.format(format.parseDouble(dateStr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseWithZuluFormatsWithZulu() {
|
||||||
|
val dateStr = "2016-05-04T19:29:34Z"
|
||||||
|
assertEquals(dateStr, format.format(format.parseDouble(dateStr)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
klock/src/commonTest/kotlin/korlibs/time/StopwatchTest.kt
Normal file
26
klock/src/commonTest/kotlin/korlibs/time/StopwatchTest.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class StopwatchTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
var nanos = 0.0
|
||||||
|
val stopwatch = Stopwatch { nanos }
|
||||||
|
fun check() = stopwatch.elapsedNanoseconds
|
||||||
|
assertEquals(0.0, check()).also { nanos++ }
|
||||||
|
assertEquals(0.0, check()).also { nanos++ }
|
||||||
|
assertEquals(0.0, check())
|
||||||
|
stopwatch.start()
|
||||||
|
assertEquals(0.0, check()).also { nanos++ }
|
||||||
|
assertEquals(1.0, check()).also { nanos++ }
|
||||||
|
assertEquals(2.0, check())
|
||||||
|
stopwatch.stop()
|
||||||
|
assertEquals(2.0, check()).also { nanos++ }
|
||||||
|
assertEquals(2.0, check())
|
||||||
|
stopwatch.start()
|
||||||
|
assertEquals(0.0, check()).also { nanos++ }
|
||||||
|
assertEquals(1.0, check()).also { nanos++ }
|
||||||
|
}
|
||||||
|
}
|
67
klock/src/commonTest/kotlin/korlibs/time/TimeFormatTest.kt
Normal file
67
klock/src/commonTest/kotlin/korlibs/time/TimeFormatTest.kt
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TimeFormatTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormat() {
|
||||||
|
val time1 = Time(hour = 48, minute = 23, second = 12, millisecond = 450)
|
||||||
|
assertEquals("48:23:12.450", TimeFormat.DEFAULT_FORMAT.format(time1))
|
||||||
|
assertEquals("48:23:12", TimeFormat.FORMAT_TIME.format(time1))
|
||||||
|
assertEquals("48.23.12", TimeFormat("HH.mm.ss").format(time1))
|
||||||
|
assertEquals("12:23:12", TimeFormat("hh:mm:ss").format(time1))
|
||||||
|
assertEquals("48:23:12", TimeFormat("kk:mm:ss").format(time1))
|
||||||
|
assertEquals("00:23:12", TimeFormat("KK:mm:ss").format(time1))
|
||||||
|
|
||||||
|
val time2 = Time(hour = 50, minute = 2, second = 0, millisecond = 109)
|
||||||
|
assertEquals("2:2:0.10", TimeFormat("h:m:s.SS").format(time2))
|
||||||
|
assertEquals("2:2:0.1", TimeFormat("h:m:s.S").format(time2))
|
||||||
|
assertEquals("2:2:0", TimeFormat("K:m:s").format(time2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParse() {
|
||||||
|
val time1 = TimeFormat.FORMAT_TIME.parseTime("23:59:59")
|
||||||
|
assertEquals(23, time1.hour)
|
||||||
|
assertEquals(59, time1.minute)
|
||||||
|
assertEquals(59, time1.second)
|
||||||
|
assertEquals(0, time1.millisecond)
|
||||||
|
|
||||||
|
val time2 = TimeFormat.FORMAT_TIME.parseTime("48:00:00")
|
||||||
|
assertEquals(48, time2.hour)
|
||||||
|
assertEquals(0, time2.minute)
|
||||||
|
assertEquals(0, time2.second)
|
||||||
|
assertEquals(0, time2.millisecond)
|
||||||
|
|
||||||
|
val time3 = TimeFormat.DEFAULT_FORMAT.parseTime("23:59:59.999")
|
||||||
|
assertEquals(23, time3.hour)
|
||||||
|
assertEquals(59, time3.minute)
|
||||||
|
assertEquals(59, time3.second)
|
||||||
|
assertEquals(999, time3.millisecond)
|
||||||
|
|
||||||
|
val time4 = TimeFormat.DEFAULT_FORMAT.parseTime("48:00:00.000")
|
||||||
|
assertEquals(48, time4.hour)
|
||||||
|
assertEquals(0, time4.minute)
|
||||||
|
assertEquals(0, time4.second)
|
||||||
|
assertEquals(0, time4.millisecond)
|
||||||
|
|
||||||
|
val time = TimeFormat("ss.mm.HH").parseTime("59.59.48")
|
||||||
|
assertEquals(48, time.hour)
|
||||||
|
assertEquals(59, time.minute)
|
||||||
|
assertEquals(59, time.second)
|
||||||
|
assertEquals(0, time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFromUnix() {
|
||||||
|
val now = 1611658981L * 1000
|
||||||
|
val diff = TimezoneOffset.local(DateTime(now))
|
||||||
|
|
||||||
|
val dateUnix = DateTimeTz.fromUnix(now)
|
||||||
|
val dateUnixLocal = DateTimeTz.fromUnixLocal(now).addOffset(diff)
|
||||||
|
|
||||||
|
assertEquals(dateUnix.hours, dateUnixLocal.hours, "testFromUnix. now=$now, diff=$diff, dataUnix=$dateUnix, dateUnixLocal=$dateUnixLocal")
|
||||||
|
}
|
||||||
|
}
|
108
klock/src/commonTest/kotlin/korlibs/time/TimeSpanTest.kt
Normal file
108
klock/src/commonTest/kotlin/korlibs/time/TimeSpanTest.kt
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class TimeSpanTest {
|
||||||
|
@Test
|
||||||
|
fun testToTimeString() {
|
||||||
|
assertEquals("00:00:01", 1.seconds.toTimeString(components = 3))
|
||||||
|
assertEquals("00:01:02", 62.seconds.toTimeString(components = 3))
|
||||||
|
assertEquals("01:01:00", 3660.seconds.toTimeString(components = 3))
|
||||||
|
|
||||||
|
assertEquals("00:01", 1.seconds.toTimeString(components = 2))
|
||||||
|
assertEquals("01:02", 62.seconds.toTimeString(components = 2))
|
||||||
|
assertEquals("61:00", 3660.seconds.toTimeString(components = 2))
|
||||||
|
|
||||||
|
assertEquals("01", 1.seconds.toTimeString(components = 1))
|
||||||
|
assertEquals("62", 62.seconds.toTimeString(components = 1))
|
||||||
|
assertEquals("3660", 3660.seconds.toTimeString(components = 1))
|
||||||
|
|
||||||
|
assertEquals("01:01:02.500", 3662.5.seconds.toTimeString(components = 3, addMilliseconds = true))
|
||||||
|
assertEquals("61:02.500", 3662.5.seconds.toTimeString(components = 2, addMilliseconds = true))
|
||||||
|
assertEquals("3662.500", 3662.5.seconds.toTimeString(components = 1, addMilliseconds = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOperators() {
|
||||||
|
assertEquals((-1).seconds, -(1.seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimes() {
|
||||||
|
assertEquals(1000.nanoseconds, 1.microseconds)
|
||||||
|
assertEquals(1000.microseconds, 1.milliseconds)
|
||||||
|
assertEquals(1000.milliseconds, 1.seconds)
|
||||||
|
assertEquals(60.seconds, 1.minutes)
|
||||||
|
assertEquals(60.minutes, 1.hours)
|
||||||
|
assertEquals(24.hours, 1.days)
|
||||||
|
assertEquals(7.days, 1.weeks)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimes2() {
|
||||||
|
val second = 1.seconds
|
||||||
|
val hour = 1.hours
|
||||||
|
val week = 1.weeks
|
||||||
|
|
||||||
|
assertEquals(second.nanoseconds, 1_000_000_000.0)
|
||||||
|
assertEquals(second.microseconds, 1_000_000.0)
|
||||||
|
assertEquals(second.milliseconds, 1_000.0)
|
||||||
|
assertEquals(second.seconds, 1.0)
|
||||||
|
|
||||||
|
assertEquals(hour.minutes, 60.0)
|
||||||
|
assertEquals(hour.hours, 1.0)
|
||||||
|
|
||||||
|
assertEquals(week.days, 7.0)
|
||||||
|
assertEquals(week.weeks, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNull() {
|
||||||
|
assertTrue { TimeSpan.ZERO != TimeSpan.NIL }
|
||||||
|
assertTrue { TimeSpan.NIL == TimeSpan.NIL }
|
||||||
|
assertTrue { TimeSpan.NIL.isNil }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ratio() {
|
||||||
|
assertEquals(0.5, 0.5.seconds / 1.seconds)
|
||||||
|
assertEquals(5.0, 10.seconds / 2000.milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun remaining() {
|
||||||
|
if (KotlinVersion.CURRENT < KotlinVersion(1, 9, 20)) return // @TODO: This should be only for WASM
|
||||||
|
|
||||||
|
assertEquals(0.5.seconds, 0.5.seconds % 1.seconds)
|
||||||
|
assertEquals(0.seconds, 1.seconds % 1.seconds)
|
||||||
|
assertEquals(0.5.seconds, 1.5.seconds % 1.seconds)
|
||||||
|
assertEquals((-0.5).seconds, ((-1.5).seconds) % 1.seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun umod() {
|
||||||
|
assertEquals(0.5.seconds, 0.5.seconds umod 1.seconds)
|
||||||
|
assertEquals(0.seconds, 1.seconds umod 1.seconds)
|
||||||
|
assertEquals(0.5.seconds, 1.5.seconds umod 1.seconds)
|
||||||
|
assertEquals(0.5.seconds, (-1.5).seconds umod 1.seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clamp() {
|
||||||
|
assertEquals(1.seconds, min(1.seconds, 2.seconds))
|
||||||
|
assertEquals(2.seconds, max(1.seconds, 2.seconds))
|
||||||
|
|
||||||
|
assertEquals(1.seconds, min(2.seconds, 1.seconds))
|
||||||
|
assertEquals(2.seconds, max(2.seconds, 1.seconds))
|
||||||
|
|
||||||
|
assertEquals(1.seconds, 1.seconds.clamp(0.seconds, 2.seconds))
|
||||||
|
assertEquals(0.seconds, (-1).seconds.clamp(0.seconds, 2.seconds))
|
||||||
|
assertEquals(2.seconds, 2.1.seconds.clamp(0.seconds, 2.seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testToString() {
|
||||||
|
assertEquals("1s", 1.seconds.toString())
|
||||||
|
assertEquals("500us", 0.5.milliseconds.toString())
|
||||||
|
}
|
||||||
|
}
|
45
klock/src/commonTest/kotlin/korlibs/time/TimeTest.kt
Normal file
45
klock/src/commonTest/kotlin/korlibs/time/TimeTest.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TimeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
assertEquals("09:30:00.000", (Time(9) + 30.minutes).toString())
|
||||||
|
assertEquals("26:00:00.000", (Time(26)).toString())
|
||||||
|
assertEquals("26:00:30.256", (Time(26) + 30.seconds + 256.milliseconds).toString())
|
||||||
|
assertEquals("16:00:30.256", (Time(16) + 30.seconds + 256.milliseconds).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProperties() {
|
||||||
|
val time = Time(hour = 12, minute = 36, second = 28, millisecond = 128)
|
||||||
|
assertEquals(12, time.hour)
|
||||||
|
assertEquals(36, time.minute)
|
||||||
|
assertEquals(28, time.second)
|
||||||
|
assertEquals(128, time.millisecond)
|
||||||
|
|
||||||
|
assertEquals(25, Time(25).hour)
|
||||||
|
assertEquals(36, Time(36).hour)
|
||||||
|
assertEquals(12, Time(36).hourAdjusted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAdjusting() {
|
||||||
|
val time = Time(hour = 25, minute = 62, second = 70, millisecond = 2000)
|
||||||
|
assertEquals(0, time.millisecond)
|
||||||
|
assertEquals(12, time.second)
|
||||||
|
assertEquals(3, time.minute)
|
||||||
|
assertEquals(26, time.hour)
|
||||||
|
assertEquals(2, time.hourAdjusted)
|
||||||
|
|
||||||
|
val timeAdjusted = time.adjust()
|
||||||
|
assertEquals(0, timeAdjusted.millisecond)
|
||||||
|
assertEquals(12, timeAdjusted.second)
|
||||||
|
assertEquals(3, timeAdjusted.minute)
|
||||||
|
assertEquals(2, timeAdjusted.hour)
|
||||||
|
assertEquals(2, timeAdjusted.hourAdjusted)
|
||||||
|
}
|
||||||
|
}
|
37
klock/src/commonTest/kotlin/korlibs/time/TimedCacheTest.kt
Normal file
37
klock/src/commonTest/kotlin/korlibs/time/TimedCacheTest.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class TimedCacheTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
var now = DateTime.fromUnixMillis(1000L)
|
||||||
|
val provider = TimeProvider { now }
|
||||||
|
var value = 0
|
||||||
|
val cache = TimedCache<Int>(1.seconds, provider) { value++ }
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
now = DateTime.fromUnixMillis(2000L)
|
||||||
|
assertEquals(1, cache.value)
|
||||||
|
assertEquals(1, cache.value)
|
||||||
|
cache.value = 100
|
||||||
|
assertEquals(100, cache.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInt() {
|
||||||
|
var now = DateTime.fromUnixMillis(1000L)
|
||||||
|
val provider = TimeProvider { now }
|
||||||
|
var value = 0
|
||||||
|
val cache = IntTimedCache(1.seconds, provider) { value++ }
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
assertEquals(0, cache.value)
|
||||||
|
now = DateTime.fromUnixMillis(2000L)
|
||||||
|
assertEquals(1, cache.value)
|
||||||
|
assertEquals(1, cache.value)
|
||||||
|
cache.value = 100
|
||||||
|
assertEquals(100, cache.value)
|
||||||
|
}
|
||||||
|
}
|
26
klock/src/commonTest/kotlin/korlibs/time/TimezoneTest.kt
Normal file
26
klock/src/commonTest/kotlin/korlibs/time/TimezoneTest.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import korlibs.time.locale.*
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class TimezoneTest {
|
||||||
|
@Test
|
||||||
|
fun testToTimezone() {
|
||||||
|
assertEquals(
|
||||||
|
"Fri, 24 Nov 2023 21:11:17 GMT+0530",
|
||||||
|
DateTime.fromUnixMillis(1700840477693).toTimezone(Timezone.IST_INDIA).toStringDefault()
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Fri, 24 Nov 2023 16:41:17 GMT+0100",
|
||||||
|
DateTime.fromUnixMillis(1700840477693).toTimezone(Timezone.CET).toStringDefault()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimezoneAbbrCollision() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(Timezone.IST_INDIA, Timezone.IST_IRISH, Timezone.IST_ISRAEL),
|
||||||
|
ExtendedTimezoneNames.getAll("IST")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
40
klock/src/commonTest/kotlin/korlibs/time/YearMonthTest.kt
Normal file
40
klock/src/commonTest/kotlin/korlibs/time/YearMonthTest.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class YearMonthTest {
|
||||||
|
@Test
|
||||||
|
fun testPacking() {
|
||||||
|
val y1 = Year(1)
|
||||||
|
val y2 = Year(2018)
|
||||||
|
val y3 = Year(9999)
|
||||||
|
|
||||||
|
val m1 = Month.January
|
||||||
|
val m2 = Month.June
|
||||||
|
val m3 = Month.December
|
||||||
|
|
||||||
|
val ym1 = YearMonth(y1, m1)
|
||||||
|
val ym2 = YearMonth(y2, m2)
|
||||||
|
val ym3 = YearMonth(y3, m3)
|
||||||
|
|
||||||
|
assertEquals(y1, ym1.year)
|
||||||
|
assertEquals(y2, ym2.year)
|
||||||
|
assertEquals(y3, ym3.year)
|
||||||
|
|
||||||
|
assertEquals(m1, ym1.month)
|
||||||
|
assertEquals(m2, ym2.month)
|
||||||
|
assertEquals(m3, ym3.month)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
assertEquals(YearMonth(2019, Month.January), (YearMonth(2018, Month.December) + 1.months))
|
||||||
|
assertEquals(YearMonth(2019, Month.December), (YearMonth(2018, Month.December) + 1.years))
|
||||||
|
|
||||||
|
assertEquals(YearMonth(2018, Month.December), (YearMonth(2019, Month.January) - 1.months))
|
||||||
|
assertEquals(YearMonth(2018, Month.December), (YearMonth(2018, Month.June) + 6.months))
|
||||||
|
|
||||||
|
assertEquals(YearMonth(2021, Month.February), (YearMonth(2018, Month.June) + 1.years + 20.months)) // 2 years + 8 months
|
||||||
|
}
|
||||||
|
}
|
18
klock/src/commonTest/kotlin/korlibs/time/YearTest.kt
Normal file
18
klock/src/commonTest/kotlin/korlibs/time/YearTest.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package korlibs.time
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class YearTest {
|
||||||
|
@Test
|
||||||
|
fun testLeap() {
|
||||||
|
assertEquals(true, Year.isLeap(2000))
|
||||||
|
assertEquals(false, Year.isLeap(2006))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLeapInstance() {
|
||||||
|
assertEquals(true, Year(2000).isLeap)
|
||||||
|
assertEquals(false, Year(2006).isLeap)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class PaddedTest {
|
||||||
|
@Test
|
||||||
|
fun testPadded() {
|
||||||
|
assertEquals("15.013", 15.013.padded(2, 3))
|
||||||
|
assertEquals("01.013", 1.013.padded(2, 3))
|
||||||
|
assertEquals("1.013", 1.013333.padded(1, 3))
|
||||||
|
assertEquals("1.100", 1.1.padded(1, 3))
|
||||||
|
assertEquals("1.010", 1.010.padded(1, 3))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import korlibs.time.hours
|
||||||
|
import korlibs.time.minutes
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TimeZoneParserTest {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
assertEquals(null, "TEST".tz)
|
||||||
|
assertEquals(0.hours, "Z".tz)
|
||||||
|
assertEquals(0.hours, "UTC".tz)
|
||||||
|
assertEquals(0.hours, "GMT".tz)
|
||||||
|
assertEquals((-7).hours, "PDT".tz)
|
||||||
|
assertEquals((-8).hours, "PST".tz)
|
||||||
|
assertEquals(2.hours + 30.minutes, "UTC+0230".tz)
|
||||||
|
assertEquals(-(2.hours + 30.minutes), "UTC-0230".tz)
|
||||||
|
}
|
||||||
|
private val String.tz get() = reader.readTimeZoneOffset()
|
||||||
|
private val String.reader get() = MicroStrReader(this)
|
||||||
|
}
|
@ -0,0 +1,340 @@
|
|||||||
|
package korlibs.time.locale
|
||||||
|
|
||||||
|
import korlibs.time.DateFormat
|
||||||
|
import korlibs.time.DateTime
|
||||||
|
import korlibs.time.DayOfWeek
|
||||||
|
import korlibs.time.KlockLocale
|
||||||
|
import korlibs.time.KlockLocaleContext
|
||||||
|
import korlibs.time.KlockLocaleGender
|
||||||
|
import korlibs.time.Month
|
||||||
|
import korlibs.time.format
|
||||||
|
import korlibs.time.parse
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class KlockLocaleTest {
|
||||||
|
val date = DateTime(year = 2019, month = Month.March, day = 13, hour = 21, minute = 36, second = 45, milliseconds = 512)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSpanishLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
mié, 13 mar 2019 21:36:45 UTC
|
||||||
|
13/03/2019 21:36:45
|
||||||
|
13/03/19 21:36
|
||||||
|
miércoles, 13 de marzo de 2019
|
||||||
|
13 de marzo de 2019
|
||||||
|
13/03/2019
|
||||||
|
13/03/19
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(SpanishKlockLocale, KlockLocale.spanish)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFrenchLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
mer, 13 mar 2019 21:36:45 UTC
|
||||||
|
13 mar 2019 21:36:45
|
||||||
|
13/03/2019 21:36
|
||||||
|
mercredi 13 mars 2019
|
||||||
|
13 mars 2019
|
||||||
|
13 mar 2019
|
||||||
|
13/03/2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(FrenchKlockLocale, KlockLocale.french)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGermanLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
Mit, 13 Mär 2019 21:36:45 UTC
|
||||||
|
13.03.2019 21:36:45
|
||||||
|
13.03.19 21:36
|
||||||
|
Mittwoch, 13. März 2019
|
||||||
|
13. März 2019
|
||||||
|
13.03.2019
|
||||||
|
13.03.19
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(GermanKlockLocale, KlockLocale.german)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNorwegianLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
ons, 13 mar 2019 21:36:45 UTC
|
||||||
|
13.03.2019 21:36:45
|
||||||
|
13.03.19 21:36
|
||||||
|
onsdag, 13. mars 2019
|
||||||
|
13. mars 2019
|
||||||
|
13.03.2019
|
||||||
|
13.03.19
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(NorwegianKlockLocale, KlockLocale.norwegian)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSwedishLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
ons, 13 mar 2019 21:36:45 UTC
|
||||||
|
13.03.2019 21:36:45
|
||||||
|
13.03.19 21:36
|
||||||
|
onsdag, 13. mars 2019
|
||||||
|
13. mars 2019
|
||||||
|
13.03.2019
|
||||||
|
13.03.19
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(SwedishKlockLocale, KlockLocale.swedish)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testJapaneseLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
水, 13 3月 2019 21:36:45 UTC
|
||||||
|
2019/03/13 21:36:45
|
||||||
|
2019/03/13 21:36
|
||||||
|
2019年3月13日水曜日
|
||||||
|
2019年3月13日
|
||||||
|
2019/03/13
|
||||||
|
2019/03/13
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(JapaneseKlockLocale, KlockLocale.japanese)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDutchLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
woe, 13 maa 2019 21:36:45 UTC
|
||||||
|
13 maa 2019 21:36:45
|
||||||
|
13-03-19 21:36
|
||||||
|
woensdag 13 maart 2019
|
||||||
|
13 maart 2019
|
||||||
|
13 maa 2019
|
||||||
|
13-03-2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(DutchKlockLocale, KlockLocale.dutch)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPortugueseLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
qua, 13 mar 2019 21:36:45 UTC
|
||||||
|
13 de mar de 2019 21:36:45
|
||||||
|
13/03/2019 21:36
|
||||||
|
quarta-feira, 13 de março de 2019
|
||||||
|
13 de março de 2019
|
||||||
|
13 de mar de 2019
|
||||||
|
13/03/2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(PortugueseKlockLocale, KlockLocale.portuguese)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRussianLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
ср, 13 мар 2019 21:36:45 UTC
|
||||||
|
13 мар 2019 г. 21:36:45
|
||||||
|
13.03.2019 21:36
|
||||||
|
среда, 13 марта 2019 г.
|
||||||
|
13 марта 2019 г.
|
||||||
|
13 мар 2019 г.
|
||||||
|
13.03.2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(RussianKlockLocale, KlockLocale.russian)
|
||||||
|
)
|
||||||
|
|
||||||
|
val neuter = KlockLocaleContext(gender = KlockLocaleGender.Neuter)
|
||||||
|
assertEquals("1-е", RussianKlockLocale.getOrdinalByDay(1, neuter))
|
||||||
|
assertEquals("20-е", RussianKlockLocale.getOrdinalByDay(20, neuter))
|
||||||
|
val masculine = KlockLocaleContext(gender = KlockLocaleGender.Masculine)
|
||||||
|
assertEquals("1-й", RussianKlockLocale.getOrdinalByDay(1, masculine))
|
||||||
|
assertEquals("20-й", RussianKlockLocale.getOrdinalByDay(20, masculine))
|
||||||
|
|
||||||
|
assertEquals(1, RussianKlockLocale.getDayByOrdinal("1-й"))
|
||||||
|
assertEquals(5, RussianKlockLocale.getDayByOrdinal("5-е"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testKoreanLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
수, 13 3월 2019 21:36:45 UTC
|
||||||
|
2019. 3. 13. 오후 9:36:45
|
||||||
|
19. 3. 13. 오후 9:36
|
||||||
|
2019년 3월 13일 수요일
|
||||||
|
2019년 3월 13일
|
||||||
|
2019. 3. 13.
|
||||||
|
19. 3. 13.
|
||||||
|
오후 9:36:45
|
||||||
|
오후 9:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(KoreanKlockLocale, KlockLocale.korean)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testChineseLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
周三, 13 三月 2019 21:36:45 UTC
|
||||||
|
2019年3月13日 下午9:36:45
|
||||||
|
2019/3/13 下午9:36
|
||||||
|
2019年3月13日星期三
|
||||||
|
2019年3月13日
|
||||||
|
2019年3月13日
|
||||||
|
2019/3/13
|
||||||
|
9:36:45
|
||||||
|
9:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(ChineseKlockLocale, KlockLocale.chinese)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUkrainianLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
ср, 13 бер 2019 21:36:45 UTC
|
||||||
|
13 бер 2019 р. 21:36:45
|
||||||
|
13.03.2019 21:36
|
||||||
|
середа, 13 березня 2019 р.
|
||||||
|
13 березня 2019 р.
|
||||||
|
13 бер 2019 р.
|
||||||
|
13.03.2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(UkrainianKlockLocale, KlockLocale.ukrainian)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTurkishLocale() {
|
||||||
|
assertEquals(
|
||||||
|
"""
|
||||||
|
Çar, 13 Mar 2019 21:36:45 UTC
|
||||||
|
13 Mar 2019 21:36:45
|
||||||
|
13.03.2019 21:36
|
||||||
|
13 Mart 2019 Çarşamba
|
||||||
|
13 Mart 2019
|
||||||
|
13 Mar 2019
|
||||||
|
13.03.2019
|
||||||
|
21:36:45
|
||||||
|
21:36
|
||||||
|
""".trimIndent(),
|
||||||
|
multiFormat(TurkishKlockLocale, KlockLocale.turkish)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun multiFormat(locale1: KlockLocale, locale2: KlockLocale, date: DateTime = this@KlockLocaleTest.date): String {
|
||||||
|
return listOf(
|
||||||
|
date.toString(korlibs.time.DateFormat.DEFAULT_FORMAT.withLocale(locale1)),
|
||||||
|
locale2.formatDateTimeMedium.format(date),
|
||||||
|
locale2.formatDateTimeShort.format(date),
|
||||||
|
locale2.formatDateFull.format(date),
|
||||||
|
locale2.formatDateLong.format(date),
|
||||||
|
locale2.formatDateMedium.format(date),
|
||||||
|
locale2.formatDateShort.format(date),
|
||||||
|
locale2.formatTimeMedium.format(date),
|
||||||
|
locale2.formatTimeShort.format(date)
|
||||||
|
).joinToString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMonthLocaleName() {
|
||||||
|
assertEquals("febrero", Month.February.localName(KlockLocale.spanish))
|
||||||
|
assertEquals("feb", Month.February.localShortName(KlockLocale.spanish))
|
||||||
|
|
||||||
|
assertEquals("2月", Month.February.localName(KlockLocale.japanese))
|
||||||
|
assertEquals("2月", Month.February.localShortName(KlockLocale.japanese))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDayOfWeekLocalName() {
|
||||||
|
assertEquals("月曜日", DayOfWeek.Monday.localName(KlockLocale.japanese))
|
||||||
|
assertEquals("月", DayOfWeek.Monday.localShortName(KlockLocale.japanese))
|
||||||
|
|
||||||
|
assertEquals("середа", DayOfWeek.Wednesday.localName(KlockLocale.ukrainian))
|
||||||
|
assertEquals("ср", DayOfWeek.Wednesday.localShortName(KlockLocale.ukrainian))
|
||||||
|
|
||||||
|
assertEquals("воскресенье", DayOfWeek.Sunday.localName(KlockLocale.russian))
|
||||||
|
assertEquals("вс", DayOfWeek.Sunday.localShortName(KlockLocale.russian))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTemporalSetDefault() {
|
||||||
|
assertEquals("Wed, 13 Mar 2019 21:36:45 UTC", date.toStringDefault())
|
||||||
|
KlockLocale.setTemporarily(KlockLocale.spanish) {
|
||||||
|
assertEquals("mié, 13 mar 2019 21:36:45 UTC", date.toStringDefault())
|
||||||
|
}
|
||||||
|
assertEquals("Wed, 13 Mar 2019 21:36:45 UTC", date.toStringDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
val HttpDate = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExtendedTimezoneNames() {
|
||||||
|
assertEquals(
|
||||||
|
"Tue, 19 Sep 2017 00:58:45 GMT+0300",
|
||||||
|
HttpDate.withTimezoneNames(ExtendedTimezoneNames).parse("Tue, 19 Sep 2017 00:58:45 MSK").toStringDefault()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOrdinalsEnglish() {
|
||||||
|
val englishOrdinalsMap = mapOf(0 to "0th", 1 to "1st", 2 to "2nd", 3 to "3rd", 4 to "4th", 5 to "5th", 6 to "6th", 7 to "7th", 8 to "8th", 9 to "9th",
|
||||||
|
10 to "10th", 11 to "11th", 12 to "12th", 13 to "13th", 14 to "14th", 15 to "15th", 16 to "16th", 17 to "17th", 18 to "18th", 19 to "19th",
|
||||||
|
20 to "20th", 21 to "21st", 22 to "22nd", 23 to "23rd", 24 to "24th", 25 to "25th", 26 to "26th", 27 to "27th", 28 to "28th", 29 to "29th",
|
||||||
|
30 to "30th", 31 to "31st")
|
||||||
|
englishOrdinalsMap.forEach {
|
||||||
|
assertEquals(it.value, KlockLocale.english.getOrdinalByDay(it.key))
|
||||||
|
assertEquals(it.key, KlockLocale.english.getDayByOrdinal(it.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOrdinalsGerman() {
|
||||||
|
val germanOrdinalsMap = mapOf(0 to "nullte", 1 to "erste", 2 to "zweite", 3 to "dritte", 4 to "vierte", 5 to "fünfte", 6 to "sechste", 7 to "siebte",
|
||||||
|
8 to "achte", 9 to "neunte", 10 to "zehnte", 11 to "elfte", 12 to "zwölfte", 13 to "dreizehnte", 14 to "vierzehnte", 15 to "fünfzehnte",
|
||||||
|
16 to "sechzehnte", 17 to "siebzehnte", 18 to "achtzehnte", 19 to "neunzehnte", 20 to "zwanzigste", 21 to "einundzwanzigste",
|
||||||
|
22 to "zweiundzwanzigste", 23 to "dreiundzwanzigste", 24 to "vierundzwanzigste", 25 to "fünfundzwanzigste", 26 to "sechsundzwanzigste",
|
||||||
|
27 to "siebenundzwanzigste", 28 to "achtundzwanzigste", 29 to "neunundzwanzigste", 30 to "dreißigste", 31 to "einunddreißigste")
|
||||||
|
germanOrdinalsMap.forEach {
|
||||||
|
assertEquals(it.value, KlockLocale.german.getOrdinalByDay(it.key))
|
||||||
|
assertEquals(it.key, KlockLocale.german.getDayByOrdinal(it.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
klock/src/jsMain/kotlin/korlibs/time/Klock.internal.js.kt
Normal file
26
klock/src/jsMain/kotlin/korlibs/time/Klock.internal.js.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
|
||||||
|
@JsName("globalThis")
|
||||||
|
private external val globalThis: dynamic
|
||||||
|
|
||||||
|
internal actual object KlockInternal {
|
||||||
|
actual val currentTime: Double get() = (js("Date.now()").unsafeCast<Double>())
|
||||||
|
|
||||||
|
actual val now: TimeSpan get() = TimeSpan.fromMilliseconds(globalThis.performance.now())
|
||||||
|
|
||||||
|
actual fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan {
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val rtime = time.unixMillisDouble
|
||||||
|
return js("-(new Date(rtime)).getTimezoneOffset()").unsafeCast<Int>().minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun sleep(time: TimeSpan) {
|
||||||
|
spinlock(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual interface Serializable
|
9
klock/src/jsMain/kotlin/korlibs/time/Klock.js.kt
Normal file
9
klock/src/jsMain/kotlin/korlibs/time/Klock.js.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package korlibs.time.js
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
import kotlin.js.Date
|
||||||
|
|
||||||
|
fun Date.toDateTime(): DateTime = DateTime(this.getTime())
|
||||||
|
fun DateTime.toDate(): Date = Date(this.unixMillisDouble)
|
37
klock/src/jvmMain/kotlin/Time.internal.jvm.kt
Normal file
37
klock/src/jvmMain/kotlin/Time.internal.jvm.kt
Normal file
@ -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 <T> 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
|
8
klock/src/jvmMain/kotlin/Time.jvm.kt
Normal file
8
klock/src/jvmMain/kotlin/Time.jvm.kt
Normal file
@ -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)
|
49
klock/src/linuxMain/kotlin/Klock.internal.linux.kt
Normal file
49
klock/src/linuxMain/kotlin/Klock.internal.linux.kt
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.gettimeofday
|
||||||
|
import platform.posix.localtime_r
|
||||||
|
import platform.posix.time_tVar
|
||||||
|
import platform.posix.timeval
|
||||||
|
import platform.posix.tm
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal actual object KlockInternal {
|
||||||
|
actual val currentTime: Double get() = memScoped {
|
||||||
|
val timeVal = alloc<timeval>()
|
||||||
|
gettimeofday(timeVal.ptr, null)
|
||||||
|
val sec = timeVal.tv_sec
|
||||||
|
val usec = timeVal.tv_usec
|
||||||
|
((sec * 1_000L) + (usec / 1_000L)).toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val now: TimeSpan get() = memScoped {
|
||||||
|
val timeVal = alloc<timeval>()
|
||||||
|
gettimeofday(timeVal.ptr, null)
|
||||||
|
val sec = timeVal.tv_sec
|
||||||
|
val usec = timeVal.tv_usec
|
||||||
|
TimeSpan.fromSeconds(sec.toInt()) + TimeSpan.fromMicroseconds(usec.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun sleep(time: TimeSpan) {
|
||||||
|
val micros = time.microseconds.toLong()
|
||||||
|
val s = micros / 1_000_000
|
||||||
|
val u = micros % 1_000_000
|
||||||
|
if (s > 0) platform.posix.sleep(s.convert())
|
||||||
|
if (u > 0) platform.posix.usleep(u.convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: kotlin-native bug: https://github.com/JetBrains/kotlin-native/pull/1901
|
||||||
|
//private val microStart = kotlin.system.getTimeMicros()
|
||||||
|
//actual fun currentTimeMillis(): Long = kotlin.system.getTimeMillis()
|
||||||
|
//actual fun microClock(): Double = (kotlin.system.getTimeMicros() - microStart).toDouble()
|
||||||
|
|
||||||
|
actual fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = memScoped {
|
||||||
|
val t = alloc<time_tVar>()
|
||||||
|
val tm = alloc<tm>()
|
||||||
|
t.value = (time.unixMillisLong / 1000L).convert()
|
||||||
|
localtime_r(t.ptr, tm.ptr)
|
||||||
|
tm.tm_gmtoff.toInt().seconds
|
||||||
|
}
|
||||||
|
}
|
68
klock/src/mingwMain/kotlin/Klockinternal.mingw.kt
Normal file
68
klock/src/mingwMain/kotlin/Klockinternal.mingw.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
import korlibs.time.*
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.mingw_gettimeofday
|
||||||
|
import platform.posix.timeval
|
||||||
|
import platform.windows.FILETIME
|
||||||
|
import platform.windows.FileTimeToSystemTime
|
||||||
|
import platform.windows.GetTimeZoneInformation
|
||||||
|
import platform.windows.SYSTEMTIME
|
||||||
|
import platform.windows.SystemTimeToFileTime
|
||||||
|
import platform.windows.SystemTimeToTzSpecificLocalTime
|
||||||
|
import platform.windows.TIME_ZONE_INFORMATION
|
||||||
|
import platform.windows.TzSpecificLocalTimeToSystemTime
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal actual object KlockInternal {
|
||||||
|
actual val currentTime: Double
|
||||||
|
get() = memScoped {
|
||||||
|
val timeVal = alloc<timeval>()
|
||||||
|
mingw_gettimeofday(timeVal.ptr, null) // mingw: doesn't expose gettimeofday, but mingw_gettimeofday
|
||||||
|
val sec = timeVal.tv_sec
|
||||||
|
val usec = timeVal.tv_usec
|
||||||
|
((sec * 1_000L) + (usec / 1_000L)).toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val now: TimeSpan
|
||||||
|
get() = memScoped {
|
||||||
|
val timeVal = alloc<timeval>()
|
||||||
|
mingw_gettimeofday(timeVal.ptr, null)
|
||||||
|
val sec = timeVal.tv_sec
|
||||||
|
val usec = timeVal.tv_usec
|
||||||
|
TimeSpan.fromSeconds(sec) + TimeSpan.fromMicroseconds(usec)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = memScoped {
|
||||||
|
val timeAsFileTime = UnixMillisecondsToWindowsTicks(time.unixMillisLong)
|
||||||
|
val utcFtime = FILETIME_fromWindowsTicks(this, timeAsFileTime)
|
||||||
|
val timezone = getTimeZoneInformation(this)
|
||||||
|
val utcStime = utcFtime.toSystemTime(this)
|
||||||
|
val localStime = utcStime.toTimezone(this, timezone)
|
||||||
|
val localUnix = localStime.toFiletime(this).toUnix()
|
||||||
|
val utcUnix = utcStime.toFiletime(this).toUnix()
|
||||||
|
return (localUnix - utcUnix).milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun sleep(time: TimeSpan) {
|
||||||
|
val micros = time.microseconds.toLong()
|
||||||
|
val s = micros / 1_000_000
|
||||||
|
val u = micros % 1_000_000
|
||||||
|
if (s > 0) platform.posix.sleep(s.convert())
|
||||||
|
if (u > 0) platform.posix.usleep(u.convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FILETIME_fromWindowsTicks(scope: NativePlacement, ticks: Long): FILETIME = scope.run { alloc<FILETIME>().apply { dwHighDateTime = (ticks ushr 32).toUInt(); dwLowDateTime = ticks.toUInt() } }
|
||||||
|
fun getTimeZoneInformation(scope: NativePlacement) = scope.run { alloc<TIME_ZONE_INFORMATION>().apply { GetTimeZoneInformation(this.ptr) } }
|
||||||
|
fun FILETIME.toSystemTime(scope: NativePlacement): SYSTEMTIME = scope.run { alloc<SYSTEMTIME>().apply { FileTimeToSystemTime(this@toSystemTime.ptr, this.ptr) } }
|
||||||
|
fun SYSTEMTIME.toTimezone(scope: NativePlacement, tzi: TIME_ZONE_INFORMATION): SYSTEMTIME = scope.run { alloc<SYSTEMTIME>().apply { SystemTimeToTzSpecificLocalTime(tzi.ptr, this@toTimezone.ptr, this.ptr) } }
|
||||||
|
fun SYSTEMTIME.toUtc(scope: NativePlacement, tzi: TIME_ZONE_INFORMATION): SYSTEMTIME = scope.run { alloc<SYSTEMTIME>().apply { TzSpecificLocalTimeToSystemTime(tzi.ptr, this@toUtc.ptr, this.ptr) } }
|
||||||
|
fun SYSTEMTIME.toFiletime(scope: NativePlacement): FILETIME = scope.run { alloc<FILETIME>().apply { SystemTimeToFileTime(this@toFiletime.ptr, this.ptr) } }
|
||||||
|
fun FILETIME.toWindowsTicks() = ((dwHighDateTime.toULong() shl 32) or (dwLowDateTime.toULong())).toLong()
|
||||||
|
fun FILETIME.toUnix() = WindowsTickToUnixMilliseconds(toWindowsTicks())
|
||||||
|
fun FILETIME_fromUnix(scope: NativePlacement, unix: Long): FILETIME = FILETIME_fromWindowsTicks(scope, UnixMillisecondsToWindowsTicks(unix))
|
||||||
|
const val WINDOWS_TICK = 10_000L
|
||||||
|
const val MS_TO_UNIX_EPOCH = 11644473600_000L
|
||||||
|
fun WindowsTickToUnixMilliseconds(windowsTicks: Long) = (windowsTicks / WINDOWS_TICK - MS_TO_UNIX_EPOCH)
|
||||||
|
fun UnixMillisecondsToWindowsTicks(unix: Long) = ((unix + MS_TO_UNIX_EPOCH) * WINDOWS_TICK)
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package korlibs.time.internal
|
||||||
|
|
||||||
|
actual interface Serializable
|
@ -45,6 +45,8 @@ String[] includes = [
|
|||||||
|
|
||||||
":resources",
|
":resources",
|
||||||
|
|
||||||
|
":klock",
|
||||||
|
|
||||||
":fsm:common",
|
":fsm:common",
|
||||||
":fsm:repos:common",
|
":fsm:repos:common",
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user