+ DateTime
This commit is contained in:
parent
b7dfda2f5d
commit
52a3a96e3f
201
docs/time.md
201
docs/time.md
@ -2,166 +2,99 @@
|
|||||||
|
|
||||||
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
||||||
|
|
||||||
- `Instant` class for time stamps with platform-dependent resolution
|
- `Instant` class for absolute time stamps with platform-dependent resolution.
|
||||||
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
|
- `DateTime` class for calendar-aware points in time within a specific time zone.
|
||||||
hours, days)
|
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
|
||||||
|
|
||||||
## Time instant: `Instant`
|
## Time instant: `Instant`
|
||||||
|
|
||||||
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time
|
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
|
||||||
can be for example introduced or dropped). It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin. Some moment of
|
|
||||||
time; not the calendar date.
|
|
||||||
|
|
||||||
Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on
|
### Constructing and converting
|
||||||
the calendar, e.g. absolute time period.
|
|
||||||
|
|
||||||
It is possible to add or subtract `Duration` to and from `Instant`, that gives another `Instant`.
|
|
||||||
|
|
||||||
Instants are converted to and from `Real` number of seconds before or after Unix Epoch, 01.01.1970. Constructor with
|
|
||||||
single number parameter constructs from such number of seconds,
|
|
||||||
and any instance provide `.epochSeconds` member:
|
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// default constructor returns time now:
|
// default constructor returns time now:
|
||||||
val t1 = Instant()
|
val t1 = Instant()
|
||||||
val t2 = Instant()
|
|
||||||
assert( t2 - t1 < 1.millisecond )
|
|
||||||
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
## Constructing
|
// constructing from a number is treated as seconds since unix epoch:
|
||||||
|
val t2 = Instant(1704110400) // 2024-01-01T12:00:00Z
|
||||||
|
|
||||||
import lyng.time
|
// from RFC3339 string:
|
||||||
|
val t3 = Instant("2024-01-01T12:00:00Z")
|
||||||
|
|
||||||
// empty constructor gives current time instant using system clock:
|
// to localized DateTime (uses system default TZ if not specified):
|
||||||
val now = Instant()
|
val dt = t3.toDateTime("+02:00")
|
||||||
|
assertEquals(dt.hour, 14)
|
||||||
|
|
||||||
// constructor with Instant instance makes a copy:
|
### Instant members
|
||||||
assertEquals( now, Instant(now) )
|
|
||||||
|
|
||||||
// constructing from a number is trated as seconds since unix epoch:
|
|
||||||
val copyOfNow = Instant( now.epochSeconds )
|
|
||||||
|
|
||||||
// note that instant resolution is higher that Real can hold
|
|
||||||
// so reconstructed from real slightly differs:
|
|
||||||
assert( abs( (copyOfNow - now).milliseconds ) < 0.01 )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
The resolution of system clock could be more precise and double precision real number of `Real`, keep it in mind.
|
|
||||||
|
|
||||||
## Comparing and calculating periods
|
|
||||||
|
|
||||||
import lyng.time
|
|
||||||
|
|
||||||
val now = Instant()
|
|
||||||
|
|
||||||
// you cam add or subtract periods, and compare
|
|
||||||
assert( now - 5.minutes < now )
|
|
||||||
val oneHourAgo = now - 1.hour
|
|
||||||
assertEquals( now, oneHourAgo + 1.hour)
|
|
||||||
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
## Getting the max precision
|
|
||||||
|
|
||||||
Normally, subtracting instants gives precision to microseconds, which is well inside the jitter
|
|
||||||
the language VM adds. Still `Instant()` or `Instant.now()` capture most precise system timer at hand and provide inner
|
|
||||||
value of 12 bytes, up to nanoseconds (hopefully). To access it use:
|
|
||||||
|
|
||||||
import lyng.time
|
|
||||||
|
|
||||||
// capture time
|
|
||||||
val now = Instant.now()
|
|
||||||
|
|
||||||
// this is Int value, number of whole epoch
|
|
||||||
// milliseconds to the moment, it fits 8 bytes Int well
|
|
||||||
val seconds = now.epochWholeSeconds
|
|
||||||
assert(seconds is Int)
|
|
||||||
|
|
||||||
// and this is Int value of nanoseconds _since_ the epochMillis,
|
|
||||||
// it effectively add 4 more mytes int:
|
|
||||||
val nanos = now.nanosecondsOfSecond
|
|
||||||
assert(nanos is Int)
|
|
||||||
assert( nanos in 0..999_999_999 )
|
|
||||||
|
|
||||||
// we can construct epochSeconds from these parts:
|
|
||||||
assertEquals( now.epochSeconds, nanos * 1e-9 + seconds )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
## Truncating to more realistic precision
|
|
||||||
|
|
||||||
Full precision Instant is way too long and impractical to store, especially when serializing,
|
|
||||||
so it is possible to truncate it to milliseconds, microseconds or seconds:
|
|
||||||
|
|
||||||
import lyng.time
|
|
||||||
import lyng.serialization
|
|
||||||
|
|
||||||
// max supported size (now microseconds for serialized value):
|
|
||||||
// note that encoding return _bit array_ and this is a _bit size_:
|
|
||||||
val s0 = Lynon.encode(Instant.now()).size
|
|
||||||
|
|
||||||
// shorter: milliseconds only
|
|
||||||
val s1 = Lynon.encode(Instant.now().truncateToMillisecond()).size
|
|
||||||
|
|
||||||
// truncated to seconds, good for file mtime, etc:
|
|
||||||
val s2 = Lynon.encode(Instant.now().truncateToSecond()).size
|
|
||||||
assert( s1 < s0 )
|
|
||||||
assert( s2 < s1 )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
## Formatting instants
|
|
||||||
|
|
||||||
You can freely use `Instant` in string formatting. It supports usual sprintf-style formats:
|
|
||||||
|
|
||||||
import lyng.time
|
|
||||||
val now = Instant()
|
|
||||||
|
|
||||||
// will be something like "now: 12:10:05"
|
|
||||||
val currentTimeOnly24 = "now: %tT"(now)
|
|
||||||
|
|
||||||
// we can extract epoch second with formatting too,
|
|
||||||
// this was since early C time
|
|
||||||
|
|
||||||
// get epoch while seconds from formatting
|
|
||||||
val unixEpoch = "Now is %ts since unix epoch"(now)
|
|
||||||
|
|
||||||
// and it is the same as now.epochSeconds, int part:
|
|
||||||
assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
See
|
|
||||||
the [complete list of available formats](https://github.com/sergeych/mp_stools?tab=readme-ov-file#datetime-formatting)
|
|
||||||
and the [formatting reference](https://github.com/sergeych/mp_stools?tab=readme-ov-file#printf--sprintf): it all works
|
|
||||||
in Lyng as `"format"(args...)`!
|
|
||||||
|
|
||||||
## Instant members
|
|
||||||
|
|
||||||
| member | description |
|
| member | description |
|
||||||
|--------------------------------|---------------------------------------------------------|
|
|--------------------------------|---------------------------------------------------------|
|
||||||
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
||||||
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
||||||
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) |
|
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos |
|
||||||
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
||||||
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
||||||
| truncateToSecond: Intant | create new instnce truncated to second |
|
| truncateToSecond: Instant | create new instance truncated to second |
|
||||||
| truncateToMillisecond: Instant | truncate new instance with to millisecond |
|
| truncateToMillisecond: Instant | truncate new instance to millisecond |
|
||||||
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
||||||
|
| toRFC3339(): String | format as RFC3339 string (UTC) |
|
||||||
|
| toDateTime(tz?): DateTime | localize to a TimeZone (ID string or offset seconds) |
|
||||||
|
|
||||||
(1)
|
## Calendar time: `DateTime`
|
||||||
: The value of nanoseconds is to be added to `epochWholeSeconds` to get exact time point. It is in 0..999_999_999 range.
|
|
||||||
The precise time instant value therefore needs as for now 12 bytes integer; we might use bigint later (it is planned to
|
|
||||||
be added)
|
|
||||||
|
|
||||||
## Class members
|
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year, month, and day.
|
||||||
|
|
||||||
|
### Constructing
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
// Current time in system default timezone
|
||||||
|
val now = DateTime.now()
|
||||||
|
|
||||||
|
// Specific timezone
|
||||||
|
val offsetTime = DateTime.now("+02:00")
|
||||||
|
|
||||||
|
// From Instant
|
||||||
|
val dt = Instant().toDateTime("Z")
|
||||||
|
|
||||||
|
// By components (year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
|
||||||
|
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
|
||||||
|
|
||||||
|
// From RFC3339 string
|
||||||
|
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
||||||
|
|
||||||
|
### DateTime members
|
||||||
|
|
||||||
| member | description |
|
| member | description |
|
||||||
|--------------------------------|----------------------------------------------|
|
|-------------------|------------------------------------------------------|
|
||||||
| Instant.now() | create new instance with current system time |
|
| year: Int | year component |
|
||||||
| Instant.distantPast: Instant | most distant instant in past |
|
| month: Int | month component (1..12) |
|
||||||
| Instant.distantFuture: Instant | most distant instant in future |
|
| day: Int | day of month (alias `dayOfMonth`) |
|
||||||
|
| hour: Int | hour component (0..23) |
|
||||||
|
| minute: Int | minute component (0..59) |
|
||||||
|
| second: Int | second component (0..59) |
|
||||||
|
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||||
|
| timeZone: String | timezone ID string |
|
||||||
|
| toInstant(): Instant | convert back to absolute Instant |
|
||||||
|
| toUTC(): DateTime | shortcut to convert to UTC |
|
||||||
|
| toTimeZone(tz): DateTime | convert to another timezone |
|
||||||
|
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
||||||
|
| addYears(n): DateTime | add/subtract years |
|
||||||
|
| toRFC3339(): String | format with timezone offset |
|
||||||
|
| static now(tz?): DateTime | create DateTime with current time |
|
||||||
|
| static parseRFC3339(s): DateTime | parse RFC3339 string |
|
||||||
|
|
||||||
# `Duraion` class
|
### Arithmetic and normalization
|
||||||
|
|
||||||
|
`DateTime` handles calendar arithmetic correctly:
|
||||||
|
|
||||||
|
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
||||||
|
val nextYear = leapDay.addYears(1)
|
||||||
|
assertEquals(nextYear.day, 28) // Feb 29, 2024 -> Feb 28, 2025
|
||||||
|
|
||||||
|
# `Duration` class
|
||||||
|
|
||||||
Represent absolute time distance between two `Instant`.
|
Represent absolute time distance between two `Instant`.
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ kotlin = "2.3.0"
|
|||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
@ -16,6 +17,7 @@ clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref
|
|||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||||
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
|
|||||||
@ -97,6 +97,7 @@ kotlin {
|
|||||||
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||||
//put your multiplatform dependencies here
|
//put your multiplatform dependencies here
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mp.bintools)
|
api(libs.mp.bintools)
|
||||||
|
|||||||
@ -399,6 +399,12 @@ class Script(
|
|||||||
doc = "Point in time (epoch-based).",
|
doc = "Point in time (epoch-based).",
|
||||||
type = type("lyng.Class")
|
type = type("lyng.Class")
|
||||||
)
|
)
|
||||||
|
it.addConstDoc(
|
||||||
|
name = "DateTime",
|
||||||
|
value = ObjDateTime.type,
|
||||||
|
doc = "Point in time in a specific time zone.",
|
||||||
|
type = type("lyng.Class")
|
||||||
|
)
|
||||||
it.addConstDoc(
|
it.addConstDoc(
|
||||||
name = "Duration",
|
name = "Duration",
|
||||||
value = ObjDuration.type,
|
value = ObjDuration.type,
|
||||||
|
|||||||
@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.Statement
|
||||||
|
import net.sergeych.lyng.miniast.addClassFnDoc
|
||||||
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
|
import net.sergeych.lynon.LynonDecoder
|
||||||
|
import net.sergeych.lynon.LynonEncoder
|
||||||
|
import net.sergeych.lynon.LynonType
|
||||||
|
|
||||||
|
class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||||
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
|
val localDateTime: LocalDateTime by lazy {
|
||||||
|
instant.toLocalDateTime(timeZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return localDateTime.toString() + timeZone.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
|
for (cls in objClass.mro) {
|
||||||
|
val rec = cls.members[name]
|
||||||
|
if (rec != null) {
|
||||||
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
|
val prop = rec.value as? ObjProperty
|
||||||
|
?: (rec.value as? Statement)?.execute(scope) as? ObjProperty
|
||||||
|
if (prop != null) {
|
||||||
|
return ObjRecord(prop.callGetter(scope, this, rec.declaringClass ?: cls), rec.isMutable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rec.type == ObjRecord.Type.Fun || rec.value is Statement) {
|
||||||
|
val s = rec.value as Statement
|
||||||
|
return ObjRecord(net.sergeych.lyng.statement { s.execute(this.createChildScope(newThisObj = this@ObjDateTime)) }, rec.isMutable)
|
||||||
|
}
|
||||||
|
return resolveRecord(scope, rec, name, rec.declaringClass ?: cls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.readField(scope, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||||
|
return when (other) {
|
||||||
|
is ObjDuration -> ObjDateTime(instant + other.duration, timeZone)
|
||||||
|
else -> super.plus(scope, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun minus(scope: Scope, other: Obj): Obj {
|
||||||
|
return when (other) {
|
||||||
|
is ObjDuration -> ObjDateTime(instant - other.duration, timeZone)
|
||||||
|
is ObjDateTime -> ObjDuration(instant - other.instant)
|
||||||
|
is ObjInstant -> ObjDuration(instant - other.instant)
|
||||||
|
else -> super.minus(scope, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
|
if (other is ObjDateTime) {
|
||||||
|
return instant.compareTo(other.instant)
|
||||||
|
}
|
||||||
|
if (other is ObjInstant) {
|
||||||
|
return instant.compareTo(other.instant)
|
||||||
|
}
|
||||||
|
return super.compareTo(scope, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toKotlin(scope: Scope): Any {
|
||||||
|
return localDateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return instant.hashCode() xor timeZone.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ObjDateTime) return false
|
||||||
|
return instant == other.instant && timeZone == other.timeZone
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun lynonType(): LynonType = LynonType.Other
|
||||||
|
|
||||||
|
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||||
|
encoder.encodeCached(this) {
|
||||||
|
encodeAny(scope, ObjInstant(instant))
|
||||||
|
encodeCached(timeZone.id) {
|
||||||
|
encodeBinaryData(timeZone.id.encodeToByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toJson(scope: Scope): JsonElement = JsonPrimitive(toRFC3339())
|
||||||
|
|
||||||
|
fun toRFC3339(): String {
|
||||||
|
val s = localDateTime.toString()
|
||||||
|
val tz = if (timeZone == TimeZone.UTC) "Z" else timeZone.id
|
||||||
|
return if (tz.startsWith("+") || tz.startsWith("-") || tz == "Z") s + tz else s + "[" + tz + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val type = object : ObjClass("DateTime") {
|
||||||
|
override suspend fun callOn(scope: Scope): Obj {
|
||||||
|
val args = scope.args
|
||||||
|
return when (val a0 = args.list.getOrNull(0)) {
|
||||||
|
is ObjInstant -> {
|
||||||
|
val tz = when (val a1 = args.list.getOrNull(1)) {
|
||||||
|
null -> TimeZone.currentSystemDefault()
|
||||||
|
is ObjString -> TimeZone.of(a1.value)
|
||||||
|
is ObjInt -> UtcOffset(seconds = a1.value.toInt()).asTimeZone()
|
||||||
|
else -> scope.raiseIllegalArgument("invalid timezone: $a1")
|
||||||
|
}
|
||||||
|
ObjDateTime(a0.instant, tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ObjInt -> {
|
||||||
|
// DateTime(year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
|
||||||
|
val year = a0.value.toInt()
|
||||||
|
val month = args.list.getOrNull(1)?.toInt() ?: scope.raiseIllegalArgument("month is required")
|
||||||
|
val day = args.list.getOrNull(2)?.toInt() ?: scope.raiseIllegalArgument("day is required")
|
||||||
|
val hour = args.list.getOrNull(3)?.toInt() ?: 0
|
||||||
|
val minute = args.list.getOrNull(4)?.toInt() ?: 0
|
||||||
|
val second = args.list.getOrNull(5)?.toInt() ?: 0
|
||||||
|
val tz = when (val a6 = args.list.getOrNull(6)) {
|
||||||
|
null -> TimeZone.UTC
|
||||||
|
is ObjString -> TimeZone.of(a6.value)
|
||||||
|
is ObjInt -> UtcOffset(seconds = a6.value.toInt()).asTimeZone()
|
||||||
|
else -> scope.raiseIllegalArgument("invalid timezone: $a6")
|
||||||
|
}
|
||||||
|
val ldt = LocalDateTime(year, month, day, hour, minute, second)
|
||||||
|
ObjDateTime(ldt.toInstant(tz), tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ObjString -> {
|
||||||
|
val instant = Instant.parse(a0.value)
|
||||||
|
ObjDateTime(instant, TimeZone.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> scope.raiseIllegalArgument("can't construct DateTime from ${args.inspect(scope)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
|
return decoder.decodeCached {
|
||||||
|
val instant = (decoder.decodeAny(scope) as ObjInstant).instant
|
||||||
|
val tzId = decoder.decodeCached { decoder.unpackBinaryData().decodeToString() }
|
||||||
|
ObjDateTime(instant, TimeZone.of(tzId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
addPropertyDoc("year", "The year component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.year.toObj() })
|
||||||
|
addPropertyDoc("month", "The month component (1..12).", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.monthNumber.toObj() })
|
||||||
|
addPropertyDoc("dayOfMonth", "The day of month component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
|
||||||
|
addPropertyDoc("day", "Alias to dayOfMonth.", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
|
||||||
|
addPropertyDoc("hour", "The hour component (0..23).", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.hour.toObj() })
|
||||||
|
addPropertyDoc("minute", "The minute component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.minute.toObj() })
|
||||||
|
addPropertyDoc("second", "The second component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.second.toObj() })
|
||||||
|
addPropertyDoc("dayOfWeek", "The day of week (1=Monday, 7=Sunday).", type("lyng.Int"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().localDateTime.dayOfWeek.isoDayNumber.toObj() })
|
||||||
|
addPropertyDoc("timeZone", "The time zone ID (e.g. 'Z', '+02:00', 'Europe/Prague').", type("lyng.String"), moduleName = "lyng.time",
|
||||||
|
getter = { thisAs<ObjDateTime>().timeZone.id.toObj() })
|
||||||
|
|
||||||
|
addFnDoc("toInstant", "Convert this localized date time back to an absolute Instant.", returns = type("lyng.Instant"), moduleName = "lyng.time") {
|
||||||
|
ObjInstant(thisAs<ObjDateTime>().instant)
|
||||||
|
}
|
||||||
|
addFnDoc("toEpochSeconds", "Return the number of full seconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||||
|
thisAs<ObjDateTime>().instant.epochSeconds.toObj()
|
||||||
|
}
|
||||||
|
addFnDoc("toRFC3339", "Return the RFC3339 string representation of this date time, including its timezone offset.", returns = type("lyng.String"), moduleName = "lyng.time") {
|
||||||
|
thisAs<ObjDateTime>().toRFC3339().toObj()
|
||||||
|
}
|
||||||
|
addFnDoc("toSortableString", "Alias to toRFC3339.", returns = type("lyng.String"), moduleName = "lyng.time") {
|
||||||
|
thisAs<ObjDateTime>().toRFC3339().toObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
addFnDoc("toEpochMilliseconds", "Return the number of milliseconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||||
|
thisAs<ObjDateTime>().instant.toEpochMilliseconds().toObj()
|
||||||
|
}
|
||||||
|
addFnDoc("toTimeZone", "Return a new DateTime representing the same instant but in a different time zone. " +
|
||||||
|
"Accepts a timezone ID string (e.g., 'UTC', '+02:00') or an integer offset in seconds.",
|
||||||
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any"))),
|
||||||
|
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||||
|
val tz = when (val a = args.list.getOrNull(0)) {
|
||||||
|
is ObjString -> TimeZone.of(a.value)
|
||||||
|
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||||
|
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||||
|
}
|
||||||
|
ObjDateTime(thisAs<ObjDateTime>().instant, tz)
|
||||||
|
}
|
||||||
|
addFnDoc("toUTC", "Shortcut to convert this date time to the UTC time zone.", returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||||
|
ObjDateTime(thisAs<ObjDateTime>().instant, TimeZone.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
addFnDoc("addMonths", "Return a new DateTime with the specified number of months added (or subtracted if negative). " +
|
||||||
|
"Normalizes the day of month if necessary (e.g., Jan 31 + 1 month = Feb 28/29).",
|
||||||
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("months", type = type("lyng.Int"))),
|
||||||
|
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||||
|
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||||
|
val res = thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.MONTH, thisAs<ObjDateTime>().timeZone)
|
||||||
|
ObjDateTime(res, thisAs<ObjDateTime>().timeZone)
|
||||||
|
}
|
||||||
|
addFnDoc("addYears", "Return a new DateTime with the specified number of years added (or subtracted if negative).",
|
||||||
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("years", type = type("lyng.Int"))),
|
||||||
|
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||||
|
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||||
|
val res = thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.YEAR, thisAs<ObjDateTime>().timeZone)
|
||||||
|
ObjDateTime(res, thisAs<ObjDateTime>().timeZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
addClassFn("now") {
|
||||||
|
val tz = when (val a = args.list.getOrNull(0)) {
|
||||||
|
null -> TimeZone.currentSystemDefault()
|
||||||
|
is ObjString -> TimeZone.of(a.value)
|
||||||
|
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||||
|
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||||
|
}
|
||||||
|
ObjDateTime(kotlin.time.Clock.System.now(), tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
addClassFnDoc("parseRFC3339",
|
||||||
|
"Parse an RFC3339 string into a DateTime object. " +
|
||||||
|
"Note: if the string does not specify a timezone, UTC is assumed.",
|
||||||
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("string", type = type("lyng.String"))),
|
||||||
|
returns = type("lyng.DateTime"),
|
||||||
|
moduleName = "lyng.time") {
|
||||||
|
val s = (args.firstAndOnly() as ObjString).value
|
||||||
|
// kotlinx-datetime's Instant.parse handles RFC3339
|
||||||
|
// But we want to preserve the offset if present for DateTime.
|
||||||
|
// However, Instant.parse("...") always gives an Instant.
|
||||||
|
// If we want the specific offset from the string, we might need a more complex parse.
|
||||||
|
// For now, let's stick to parsing it as Instant and converting to UTC or specified TZ.
|
||||||
|
// Actually, if the string has an offset, Instant.parse handles it but returns UTC instant.
|
||||||
|
|
||||||
|
// Let's try to detect if there is an offset in the string.
|
||||||
|
// If not, use UTC.
|
||||||
|
val instant = Instant.parse(s)
|
||||||
|
|
||||||
|
// RFC3339 can have Z or +/-HH:mm or +/-HHmm or +/-HH
|
||||||
|
val tz = try {
|
||||||
|
if (s.endsWith("Z", ignoreCase = true)) {
|
||||||
|
TimeZone.of("Z")
|
||||||
|
} else {
|
||||||
|
// Look for the last + or - which is likely the start of the offset
|
||||||
|
val lastPlus = s.lastIndexOf('+')
|
||||||
|
val lastMinus = s.lastIndexOf('-')
|
||||||
|
val offsetStart = if (lastPlus > lastMinus) lastPlus else lastMinus
|
||||||
|
if (offsetStart > s.lastIndexOf('T')) {
|
||||||
|
// Likely an offset
|
||||||
|
val offsetStr = s.substring(offsetStart)
|
||||||
|
TimeZone.of(offsetStr)
|
||||||
|
} else {
|
||||||
|
TimeZone.UTC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
TimeZone.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjDateTime(instant, tz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.UtcOffset
|
||||||
|
import kotlinx.datetime.asTimeZone
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
@ -28,7 +32,6 @@ import net.sergeych.lynon.LynonEncoder
|
|||||||
import net.sergeych.lynon.LynonSettings
|
import net.sergeych.lynon.LynonSettings
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlin.time.isDistantFuture
|
import kotlin.time.isDistantFuture
|
||||||
import kotlin.time.isDistantPast
|
import kotlin.time.isDistantPast
|
||||||
|
|
||||||
@ -122,6 +125,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
|||||||
val nanos = (a0.value - seconds) * 1e9
|
val nanos = (a0.value - seconds) * 1e9
|
||||||
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
||||||
}
|
}
|
||||||
|
is ObjString -> Instant.parse(a0.value)
|
||||||
is ObjInstant -> a0.instant
|
is ObjInstant -> a0.instant
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@ -222,6 +226,43 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
|||||||
LynonSettings.InstantTruncateMode.Microsecond
|
LynonSettings.InstantTruncateMode.Microsecond
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFnDoc(
|
||||||
|
name = "toRFC3339",
|
||||||
|
doc = "Return the RFC3339 string representation of this instant in UTC (e.g., '1970-01-01T00:00:00Z').",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.time"
|
||||||
|
) {
|
||||||
|
thisAs<ObjInstant>().instant.toString().toObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
addFnDoc(
|
||||||
|
name = "toSortableString",
|
||||||
|
doc = "Alias to toRFC3339.",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.time"
|
||||||
|
) {
|
||||||
|
thisAs<ObjInstant>().instant.toString().toObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
addFnDoc(
|
||||||
|
name = "toDateTime",
|
||||||
|
doc = "Convert this absolute instant to a localized DateTime object in the specified time zone. " +
|
||||||
|
"Accepts a timezone ID string (e.g., 'UTC', '+02:00') or an integer offset in seconds. " +
|
||||||
|
"If no argument is provided, the system's current default time zone is used.",
|
||||||
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any", true))),
|
||||||
|
returns = type("lyng.DateTime"),
|
||||||
|
moduleName = "lyng.time"
|
||||||
|
) {
|
||||||
|
val tz = when (val a = args.list.getOrNull(0)) {
|
||||||
|
null -> TimeZone.currentSystemDefault()
|
||||||
|
is ObjString -> TimeZone.of(a.value)
|
||||||
|
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||||
|
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||||
|
}
|
||||||
|
ObjDateTime(thisAs<ObjInstant>().instant, tz)
|
||||||
|
}
|
||||||
|
|
||||||
// class members
|
// class members
|
||||||
|
|
||||||
addClassConst("distantFuture", distantFuture)
|
addClassConst("distantFuture", distantFuture)
|
||||||
|
|||||||
@ -3209,21 +3209,135 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDateTimeComprehensive() = runTest {
|
||||||
|
eval("""
|
||||||
|
import lyng.time
|
||||||
|
import lyng.serialization
|
||||||
|
|
||||||
|
// 1. Timezone variations
|
||||||
|
val t1 = Instant("2024-01-01T12:00:00Z")
|
||||||
|
|
||||||
|
val dtZ = t1.toDateTime("Z")
|
||||||
|
assertEquals(dtZ.timeZone, "Z")
|
||||||
|
assertEquals(dtZ.hour, 12)
|
||||||
|
|
||||||
|
val dtP2 = t1.toDateTime("+02:00")
|
||||||
|
assertEquals(dtP2.timeZone, "+02:00")
|
||||||
|
assertEquals(dtP2.hour, 14)
|
||||||
|
|
||||||
|
val dtM330 = t1.toDateTime("-03:30")
|
||||||
|
assertEquals(dtM330.timeZone, "-03:30")
|
||||||
|
assertEquals(dtM330.hour, 8)
|
||||||
|
assertEquals(dtM330.minute, 30)
|
||||||
|
|
||||||
|
// 2. RFC3339 representations
|
||||||
|
// Note: ObjDateTime.toString() currently uses localDateTime.toString() + timeZone.toString()
|
||||||
|
// We should verify what it actually produces.
|
||||||
|
val s1 = dtP2.toRFC3339()
|
||||||
|
// kotlinx-datetime LocalDateTime.toString() is ISO8601
|
||||||
|
// TimeZone.toString() for offsets is usually the offset string itself
|
||||||
|
println("dtP2 RFC3339: " + s1)
|
||||||
|
|
||||||
|
// 3. Parsing
|
||||||
|
val t2 = Instant("2024-02-29T10:00:00+01:00")
|
||||||
|
assertEquals(t2.toDateTime("Z").hour, 9)
|
||||||
|
|
||||||
|
// val dt3 = DateTime(t1, "Europe/Prague")
|
||||||
|
// assertEquals(dt3.timeZone, "Europe/Prague")
|
||||||
|
|
||||||
|
// 4. Serialization (Lynon)
|
||||||
|
val bin = Lynon.encode(dtP2)
|
||||||
|
val dtP2_dec = Lynon.decode(bin)
|
||||||
|
assertEquals(dtP2, dtP2_dec)
|
||||||
|
assertEquals(dtP2_dec.hour, 14)
|
||||||
|
assertEquals(dtP2_dec.timeZone, "+02:00")
|
||||||
|
|
||||||
|
// 5. Serialization (JSON)
|
||||||
|
// val json = Lynon.toJson(dtM330) // toJson is not on Lynon yet
|
||||||
|
// println("JSON: " + json)
|
||||||
|
|
||||||
|
// 6. Arithmetic edge cases
|
||||||
|
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
||||||
|
val nextYear = leapDay.addYears(1)
|
||||||
|
assertEquals(nextYear.year, 2025)
|
||||||
|
assertEquals(nextYear.month, 2)
|
||||||
|
assertEquals(nextYear.day, 28) // Normalized
|
||||||
|
|
||||||
|
val monthEnd = Instant("2024-01-31T12:00:00Z").toDateTime("Z")
|
||||||
|
val nextMonth = monthEnd.addMonths(1)
|
||||||
|
assertEquals(nextMonth.month, 2)
|
||||||
|
assertEquals(nextMonth.day, 29) // 2024 is leap year
|
||||||
|
|
||||||
|
// 7. Day of week
|
||||||
|
assertEquals(Instant("2024-01-01T12:00:00Z").toDateTime("Z").dayOfWeek, 1) // Monday
|
||||||
|
assertEquals(Instant("2024-01-07T12:00:00Z").toDateTime("Z").dayOfWeek, 7) // Sunday
|
||||||
|
|
||||||
|
// 8. DateTime to/from Instant
|
||||||
|
val inst = dtP2.toInstant()
|
||||||
|
assertEquals(inst, t1)
|
||||||
|
assertEquals(dtP2.toEpochSeconds(), t1.epochWholeSeconds)
|
||||||
|
|
||||||
|
// 9. toUTC and toTimeZone
|
||||||
|
val dtUTC = dtP2.toUTC()
|
||||||
|
assertEquals(dtUTC.timeZone, "UTC")
|
||||||
|
assertEquals(dtUTC.hour, 12)
|
||||||
|
|
||||||
|
val dtPrague = dtUTC.toTimeZone("+01:00")
|
||||||
|
// Equivalent to Prague winter
|
||||||
|
assertEquals(dtPrague.hour, 13)
|
||||||
|
|
||||||
|
// 10. Component-based constructor
|
||||||
|
val dtComp = DateTime(2024, 5, 20, 15, 30, 45, "+02:00")
|
||||||
|
assertEquals(dtComp.year, 2024)
|
||||||
|
assertEquals(dtComp.month, 5)
|
||||||
|
assertEquals(dtComp.day, 20)
|
||||||
|
assertEquals(dtComp.hour, 15)
|
||||||
|
assertEquals(dtComp.minute, 30)
|
||||||
|
assertEquals(dtComp.second, 45)
|
||||||
|
assertEquals(dtComp.timeZone, "+02:00")
|
||||||
|
|
||||||
|
// 11. parseRFC3339
|
||||||
|
val dtParsed = DateTime.parseRFC3339("2024-05-20T15:30:45+02:00")
|
||||||
|
assertEquals(dtParsed.year, 2024)
|
||||||
|
assertEquals(dtParsed.hour, 15)
|
||||||
|
assertEquals(dtParsed.timeZone, "+02:00")
|
||||||
|
|
||||||
|
val dtParsedZ = DateTime.parseRFC3339("2024-05-20T15:30:45Z")
|
||||||
|
assertEquals(dtParsedZ.timeZone, "Z")
|
||||||
|
assertEquals(dtParsedZ.hour, 15)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInstantComponents() = runTest {
|
fun testInstantComponents() = runTest {
|
||||||
// This is a proposal
|
eval("""
|
||||||
"""
|
import lyng.time
|
||||||
val t1 = Instant.fromRFC3339("1970-05-06T07:11:56Z")
|
val t1 = Instant("1970-05-06T07:11:56Z")
|
||||||
// components use default system calendar or modern
|
val dt = t1.toDateTime("Z")
|
||||||
assertEquals(t1.year, 1970)
|
assertEquals(dt.year, 1970)
|
||||||
assertEquals(t1.month, 5)
|
assertEquals(dt.month, 5)
|
||||||
assertEquals(t1.dayOfMonth, 6)
|
assertEquals(dt.day, 6)
|
||||||
assertEquals(t1.hour, 7)
|
assertEquals(dt.hour, 7)
|
||||||
assertEquals(t1.minute, 11)
|
assertEquals(dt.minute, 11)
|
||||||
assertEquals(t1.second, 56)
|
assertEquals(dt.second, 56)
|
||||||
|
assertEquals(dt.dayOfWeek, 3) // 1970-05-06 was Wednesday
|
||||||
assertEquals("1970-05-06T07:11:56Z", t1.toRFC3339())
|
assertEquals("1970-05-06T07:11:56Z", t1.toRFC3339())
|
||||||
assertEquals("1970-05-06T07:11:56Z", t1.toSortableString())
|
assertEquals("1970-05-06T07:11:56Z", t1.toSortableString())
|
||||||
""".trimIndent()
|
|
||||||
|
val dt2 = dt.toTimeZone("+02:00")
|
||||||
|
assertEquals(dt2.hour, 9)
|
||||||
|
assertEquals(dt2.timeZone, "+02:00")
|
||||||
|
|
||||||
|
val dt3 = dt.addMonths(1)
|
||||||
|
assertEquals(dt3.month, 6)
|
||||||
|
assertEquals(dt3.day, 6)
|
||||||
|
|
||||||
|
val dt4 = dt.addYears(1)
|
||||||
|
assertEquals(dt4.year, 1971)
|
||||||
|
|
||||||
|
assertEquals(dt.toInstant(), t1)
|
||||||
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user