add klock module

This commit is contained in:
InsanusMokrassar 2023-12-17 23:37:45 +06:00
parent 6230accb68
commit 65d01b1fb3
98 changed files with 7751 additions and 0 deletions

View File

@ -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`:

View File

@ -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
View 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
View File

@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"

View 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

View 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)

View 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))

View File

@ -0,0 +1,6 @@
package korlibs.time
/**
* An exception for Date operations.
*/
class DateException(msg: String) : RuntimeException(msg)

View 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)

View 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
}

View 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)

View 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())

View 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) }
}

View File

@ -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'")

View 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)"
}

View 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)
}
}

View 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
}

View 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

View 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)

View File

@ -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,
}

View 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)

View 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
}
}
}
}

View 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

View 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"
}

View 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
}
}

View 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
}

View File

@ -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
}

View 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)))

View 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)

View 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() }
}

View 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)

View 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)

View 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
}

View 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()
}
}
}

View 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
}

View 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()
}

View 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)
}

View 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,
)
}
}

View 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)

View 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
}

View 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)

View 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)
}
}

View 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
}

View File

@ -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()

View File

@ -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!!
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View File

@ -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

View 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)
}
}

View 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)
}

View 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")
}
}

View 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)
}
}

View File

@ -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))
}
}

View 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())
}
}

View File

@ -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))
}
}

View 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)
}
}

View File

@ -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))
}
}

View 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) }
)
}
}

View 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)
}
}

View 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)
}
}

View 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)))
}
}

View 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)
}
}

View File

@ -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()

View 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 }
}
}

View 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
}

View File

@ -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)
}
}

View File

@ -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)))
}
}
}

View 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++ }
}
}

View 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")
}
}

View 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())
}
}

View 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)
}
}

View 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)
}
}

View 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")
)
}
}

View 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
}
}

View 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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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))
}
}
}

View 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

View 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)

View 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

View 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)

View 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
}
}

View 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)
}

View File

@ -0,0 +1,5 @@
@file:Suppress("PackageDirectoryMismatch")
package korlibs.time.internal
actual interface Serializable

View File

@ -45,6 +45,8 @@ String[] includes = [
":resources", ":resources",
":klock",
":fsm:common", ":fsm:common",
":fsm:repos:common", ":fsm:repos:common",