+ DateTime
This commit is contained in:
1 parent
b7dfda2f5d
commit
52a3a96e3f
7 files changed
+538
-148
No files matched your search
@@ -399,6 +399,12 @@ class Script(
|
||||
doc = "Point in time (epoch-based).",
|
||||
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(
|
||||
name = "Duration",
|
||||
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
|
||||
|
||||
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.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
@@ -28,7 +32,6 @@ import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonSettings
|
||||
import net.sergeych.lynon.LynonType
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.time.isDistantFuture
|
||||
import kotlin.time.isDistantPast
|
||||
|
||||
@@ -122,6 +125,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
val nanos = (a0.value - seconds) * 1e9
|
||||
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
||||
}
|
||||
is ObjString -> Instant.parse(a0.value)
|
||||
is ObjInstant -> a0.instant
|
||||
|
||||
else -> {
|
||||
@@ -222,6 +226,43 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
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
|
||||
|
||||
addClassConst("distantFuture", distantFuture)
|
||||
|
||||
Reference in new issue
Block a user