Add Date type and database release docs
This commit is contained in:
1 parent
14dc73db3e
commit
b3be908242
19 files changed
+928
-106
No files matched your search
@@ -5003,9 +5003,16 @@ class Compiler(
|
||||
is QualifiedThisRef -> resolveClassByName(ref.typeName)
|
||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForMember(it.ref) }
|
||||
is MethodCallRef -> inferMethodCallReturnClass(ref)
|
||||
is ImplicitThisMethodCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||
is ImplicitThisMethodCallRef -> {
|
||||
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
|
||||
inferMethodCallReturnClass(typeName?.let { resolveClassByName(it) }, ref.methodName())
|
||||
}
|
||||
is ThisMethodSlotCallRef -> {
|
||||
val typeName = currentImplicitThisTypeName()
|
||||
inferMethodCallReturnClass(typeName?.let { resolveClassByName(it) }, ref.methodName())
|
||||
}
|
||||
is QualifiedThisMethodSlotCallRef ->
|
||||
inferMethodCallReturnClass(resolveClassByName(ref.receiverTypeName()), ref.methodName())
|
||||
is CallRef -> inferCallReturnTypeDecl(ref)?.let { resolveTypeDeclObjClass(it) } ?: inferCallReturnClass(ref)
|
||||
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||
is FieldRef -> {
|
||||
@@ -5127,6 +5134,8 @@ class Compiler(
|
||||
leftClass == ObjInstant.type && rightClass == ObjDuration.type -> ObjInstant.type
|
||||
leftClass == ObjDuration.type && rightClass == ObjInstant.type && ref.op == BinOp.PLUS -> ObjInstant.type
|
||||
leftClass == ObjDuration.type && rightClass == ObjDuration.type -> ObjDuration.type
|
||||
leftClass == ObjDate.type && rightClass == ObjDate.type && ref.op == BinOp.MINUS -> ObjInt.type
|
||||
leftClass == ObjDate.type && rightClass == ObjDuration.type -> ObjDate.type
|
||||
(leftClass == ObjBuffer.type || leftClass.allParentsSet.contains(ObjBuffer.type)) &&
|
||||
(rightClass == ObjBuffer.type || rightClass.allParentsSet.contains(ObjBuffer.type)) &&
|
||||
ref.op == BinOp.PLUS -> ObjBuffer.type
|
||||
@@ -5198,7 +5207,37 @@ class Compiler(
|
||||
if (receiverClass != null && isClassScopeCallableMember(receiverClass.className, ref.name)) {
|
||||
resolveClassByName("${receiverClass.className}.${ref.name}")?.let { return it }
|
||||
}
|
||||
return inferMethodCallReturnClass(ref.name)
|
||||
return inferMethodCallReturnClass(receiverClass, ref.name)
|
||||
}
|
||||
|
||||
private fun inferMethodCallReturnClass(targetClass: ObjClass?, name: String): ObjClass? {
|
||||
when (targetClass) {
|
||||
ObjDate.type -> {
|
||||
return when (name) {
|
||||
"toIsoString", "toSortableString", "toString" -> ObjString.type
|
||||
"toDateTime", "atStartOfDay" -> ObjDateTime.type
|
||||
"addDays", "addMonths", "addYears", "today", "parseIso" -> ObjDate.type
|
||||
"daysUntil", "daysSince" -> ObjInt.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
ObjInstant.type -> {
|
||||
return when (name) {
|
||||
"toDateTime" -> ObjDateTime.type
|
||||
"toDate" -> ObjDate.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
ObjDateTime.type -> {
|
||||
return when (name) {
|
||||
"toDate" -> ObjDate.type
|
||||
"toInstant" -> ObjInstant.type
|
||||
"toUTC", "toTimeZone", "parseRFC3339", "addYears", "addMonths" -> ObjDateTime.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
else -> return inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferMethodCallReturnTypeDecl(ref: MethodCallRef): TypeDecl? {
|
||||
@@ -5423,13 +5462,13 @@ class Compiler(
|
||||
"truncateToSecond",
|
||||
"truncateToMinute",
|
||||
"truncateToMillisecond" -> ObjInstant.type
|
||||
"toDateTime",
|
||||
"today",
|
||||
"parseIso" -> ObjDate.type
|
||||
"daysUntil",
|
||||
"daysSince" -> ObjInt.type
|
||||
"toTimeZone",
|
||||
"toUTC",
|
||||
"parseRFC3339",
|
||||
"addYears",
|
||||
"addMonths",
|
||||
"addDays",
|
||||
"addHours",
|
||||
"addMinutes",
|
||||
"addSeconds" -> ObjDateTime.type
|
||||
@@ -5486,6 +5525,20 @@ class Compiler(
|
||||
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
||||
return ObjInstant.type
|
||||
}
|
||||
if (targetClass == ObjDate.type) {
|
||||
return when (name) {
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"dayOfMonth",
|
||||
"dayOfWeek",
|
||||
"dayOfYear",
|
||||
"lengthOfMonth",
|
||||
"lengthOfYear" -> ObjInt.type
|
||||
"isLeapYear" -> ObjBool.type
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (targetClass == ObjInstant.type && name in listOf(
|
||||
"truncateToMinute",
|
||||
"truncateToSecond",
|
||||
@@ -5538,6 +5591,7 @@ class Compiler(
|
||||
}
|
||||
if (targetClass == ObjDateTime.type) {
|
||||
return when (name) {
|
||||
"date" -> ObjDate.type
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
@@ -9677,6 +9731,7 @@ class Compiler(
|
||||
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||
"Exception" -> ObjException.Root
|
||||
"Instant" -> ObjInstant.type
|
||||
"Date" -> ObjDate.type
|
||||
"DateTime" -> ObjDateTime.type
|
||||
"Duration" -> ObjDuration.type
|
||||
"Buffer" -> ObjBuffer.type
|
||||
|
||||
@@ -952,6 +952,12 @@ class Script(
|
||||
doc = "Point in time (epoch-based).",
|
||||
type = type("lyng.Class")
|
||||
)
|
||||
it.addConstDoc(
|
||||
name = "Date",
|
||||
value = ObjDate.type,
|
||||
doc = "Calendar date without time-of-day or time zone.",
|
||||
type = type("lyng.Class")
|
||||
)
|
||||
it.addConstDoc(
|
||||
name = "DateTime",
|
||||
value = ObjDateTime.type,
|
||||
|
||||
@@ -1171,14 +1171,16 @@ class BytecodeCompiler(
|
||||
ObjMap.type,
|
||||
ObjBuffer.type,
|
||||
ObjInstant.type,
|
||||
ObjDateTime.type
|
||||
ObjDateTime.type,
|
||||
ObjDate.type
|
||||
)
|
||||
BinOp.MINUS -> receiverClass in setOf(
|
||||
ObjInt.type,
|
||||
ObjReal.type,
|
||||
ObjSet.type,
|
||||
ObjInstant.type,
|
||||
ObjDateTime.type
|
||||
ObjDateTime.type,
|
||||
ObjDate.type
|
||||
)
|
||||
BinOp.STAR -> receiverClass in setOf(ObjInt.type, ObjReal.type, ObjString.type)
|
||||
BinOp.SLASH, BinOp.PERCENT -> receiverClass in setOf(ObjInt.type, ObjReal.type)
|
||||
@@ -7290,7 +7292,7 @@ class BytecodeCompiler(
|
||||
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
||||
ObjRegex.type
|
||||
} else {
|
||||
inferMethodCallReturnClass(ref.name)
|
||||
inferMethodCallReturnClass(targetClass, ref.name)
|
||||
}
|
||||
}
|
||||
is CallRef -> inferCallReturnClass(ref)
|
||||
@@ -7463,7 +7465,7 @@ class BytecodeCompiler(
|
||||
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
||||
ObjRegex.type
|
||||
} else {
|
||||
inferMethodCallReturnClass(ref.name)
|
||||
inferMethodCallReturnClass(targetClass, ref.name)
|
||||
}
|
||||
}
|
||||
is CallRef -> inferCallReturnClass(ref)
|
||||
@@ -7546,6 +7548,7 @@ class BytecodeCompiler(
|
||||
"ObservableList" -> ObjObservableList.type
|
||||
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||
"Instant" -> ObjInstant.type
|
||||
"Date" -> ObjDate.type
|
||||
"DateTime" -> ObjDateTime.type
|
||||
"Duration" -> ObjDuration.type
|
||||
"Exception" -> ObjException.Root
|
||||
@@ -7584,6 +7587,36 @@ class BytecodeCompiler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferMethodCallReturnClass(targetClass: ObjClass?, name: String): ObjClass? {
|
||||
when (targetClass) {
|
||||
ObjDate.type -> {
|
||||
return when (name) {
|
||||
"toIsoString", "toSortableString", "toString" -> ObjString.type
|
||||
"toDateTime", "atStartOfDay" -> ObjDateTime.type
|
||||
"addDays", "addMonths", "addYears", "today", "parseIso" -> ObjDate.type
|
||||
"daysUntil", "daysSince" -> ObjInt.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
ObjInstant.type -> {
|
||||
return when (name) {
|
||||
"toDateTime" -> ObjDateTime.type
|
||||
"toDate" -> ObjDate.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
ObjDateTime.type -> {
|
||||
return when (name) {
|
||||
"toDate" -> ObjDate.type
|
||||
"toInstant" -> ObjInstant.type
|
||||
"toUTC", "toTimeZone", "parseRFC3339", "addYears", "addMonths" -> ObjDateTime.type
|
||||
else -> inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
else -> return inferMethodCallReturnClass(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferMethodCallReturnClass(name: String): ObjClass? = when (name) {
|
||||
"map",
|
||||
"mapNotNull",
|
||||
@@ -7616,13 +7649,13 @@ class BytecodeCompiler(
|
||||
"truncateToSecond",
|
||||
"truncateToMinute",
|
||||
"truncateToMillisecond" -> ObjInstant.type
|
||||
"toDateTime",
|
||||
"today",
|
||||
"parseIso" -> ObjDate.type
|
||||
"daysUntil",
|
||||
"daysSince" -> ObjInt.type
|
||||
"toTimeZone",
|
||||
"toUTC",
|
||||
"parseRFC3339",
|
||||
"addYears",
|
||||
"addMonths",
|
||||
"addDays",
|
||||
"addHours",
|
||||
"addMinutes",
|
||||
"addSeconds" -> ObjDateTime.type
|
||||
@@ -7667,6 +7700,20 @@ class BytecodeCompiler(
|
||||
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
||||
return ObjInstant.type
|
||||
}
|
||||
if (targetClass == ObjDate.type) {
|
||||
return when (name) {
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"dayOfMonth",
|
||||
"dayOfWeek",
|
||||
"dayOfYear",
|
||||
"lengthOfMonth",
|
||||
"lengthOfYear" -> ObjInt.type
|
||||
"isLeapYear" -> ObjBool.type
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (targetClass == ObjString.type && name == "re") {
|
||||
return ObjRegex.type
|
||||
}
|
||||
@@ -7710,6 +7757,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
if (targetClass == ObjDateTime.type) {
|
||||
return when (name) {
|
||||
"date" -> ObjDate.type
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
|
||||
@@ -64,11 +64,13 @@ object StdlibDocsBootstrap {
|
||||
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
|
||||
|
||||
// Also touch time module types so their docs (moduleName = "lyng.time") are registered
|
||||
// This enables completion/quick docs for symbols imported via `import lyng.time` (e.g., Instant, DateTime, Duration)
|
||||
// This enables completion/quick docs for symbols imported via `import lyng.time` (e.g., Instant, Date, DateTime, Duration)
|
||||
try {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val _instant = net.sergeych.lyng.obj.ObjInstant.type
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val _date = net.sergeych.lyng.obj.ObjDate.type
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val _datetime = net.sergeych.lyng.obj.ObjDateTime.type
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val _duration = net.sergeych.lyng.obj.ObjDuration.type
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* 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.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.UtcOffset
|
||||
import kotlinx.datetime.asTimeZone
|
||||
import kotlinx.datetime.isoDayNumber
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.miniast.ParamDoc
|
||||
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
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class ObjDate(val date: LocalDate) : Obj() {
|
||||
override val objClass: ObjClass get() = type
|
||||
|
||||
override fun toString(): String = date.toString()
|
||||
|
||||
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||
return when (other) {
|
||||
is ObjDuration -> ObjDate(addDays(date, requireWholeDays(scope, other.duration)))
|
||||
else -> super.plus(scope, other)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj {
|
||||
return when (other) {
|
||||
is ObjDuration -> ObjDate(addDays(date, -requireWholeDays(scope, other.duration)))
|
||||
is ObjDate -> ObjInt.of(daysBetween(other.date, date).toLong())
|
||||
else -> super.minus(scope, other)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
return if (other is ObjDate) {
|
||||
date.compareTo(other.date)
|
||||
} else super.compareTo(scope, other)
|
||||
}
|
||||
|
||||
override suspend fun toKotlin(scope: Scope): Any = date
|
||||
|
||||
override fun hashCode(): Int = date.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is ObjDate && date == other.date
|
||||
|
||||
override suspend fun lynonType(): LynonType = LynonType.Date
|
||||
|
||||
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
|
||||
encoder.encodeSigned(date.year.toLong())
|
||||
encoder.encodeUnsigned(date.month.number.toULong())
|
||||
encoder.encodeUnsigned(date.day.toULong())
|
||||
}
|
||||
|
||||
override suspend fun toJson(scope: Scope): JsonElement = JsonPrimitive(date.toString())
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("Date") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val args = scope.args
|
||||
return when (val a0 = args.list.getOrNull(0)) {
|
||||
null -> ObjDate(today(TimeZone.currentSystemDefault()))
|
||||
is ObjDate -> a0
|
||||
is ObjInt -> {
|
||||
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")
|
||||
ObjDate(createDate(scope, year, month, day))
|
||||
}
|
||||
is ObjString -> ObjDate(parseIso(scope, a0.value))
|
||||
is ObjDateTime -> ObjDate(toDate(a0.instant, a0.timeZone))
|
||||
is ObjInstant -> {
|
||||
val tz = parseTimeZoneArg(scope, args.list.getOrNull(1), TimeZone.currentSystemDefault())
|
||||
ObjDate(toDate(a0.instant, tz))
|
||||
}
|
||||
else -> scope.raiseIllegalArgument("can't construct Date from ${args.inspect(scope)}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
val year = decoder.unpackSigned().toInt()
|
||||
val month = decoder.unpackUnsigned().toInt()
|
||||
val day = decoder.unpackUnsigned().toInt()
|
||||
return ObjDate(createDate(scope, year, month, day))
|
||||
}
|
||||
}.apply {
|
||||
addPropertyDoc("year", "The year component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.year.toObj() })
|
||||
addPropertyDoc("month", "The month component (1..12).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.month.number.toObj() })
|
||||
addPropertyDoc("dayOfMonth", "The day of month component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.day.toObj() })
|
||||
addPropertyDoc("day", "Alias to dayOfMonth.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.day.toObj() })
|
||||
addPropertyDoc("dayOfWeek", "The day of week (1=Monday, 7=Sunday).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.dayOfWeek.isoDayNumber.toObj() })
|
||||
addPropertyDoc("dayOfYear", "The day of year (1..365/366).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDate>().date.dayOfYear.toObj() })
|
||||
addPropertyDoc("isLeapYear", "Whether this date is in a leap year.", type("lyng.Bool"), moduleName = "lyng.time",
|
||||
getter = { isLeapYear(thisAs<ObjDate>().date.year).toObj() })
|
||||
addPropertyDoc("lengthOfMonth", "Number of days in this date's month.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { monthLength(thisAs<ObjDate>().date.year, thisAs<ObjDate>().date.month.number).toObj() })
|
||||
addPropertyDoc("lengthOfYear", "Number of days in this date's year.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { (if (isLeapYear(thisAs<ObjDate>().date.year)) 366 else 365).toObj() })
|
||||
|
||||
addFnDoc("toIsoString", "Return the canonical ISO date string representation (`YYYY-MM-DD`).",
|
||||
returns = type("lyng.String"), moduleName = "lyng.time") {
|
||||
thisAs<ObjDate>().date.toString().toObj()
|
||||
}
|
||||
addFnDoc("toSortableString", "Alias to toIsoString.", returns = type("lyng.String"), moduleName = "lyng.time") {
|
||||
thisAs<ObjDate>().date.toString().toObj()
|
||||
}
|
||||
addFnDoc("toDateTime", "Convert this date to a DateTime at the start of day in the specified timezone.",
|
||||
params = listOf(ParamDoc("tz", type = type("lyng.Any", true))),
|
||||
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.UTC)
|
||||
toDateTime(thisAs<ObjDate>().date, tz)
|
||||
}
|
||||
addFnDoc("atStartOfDay", "Alias to toDateTime.",
|
||||
params = listOf(ParamDoc("tz", type = type("lyng.Any", true))),
|
||||
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.UTC)
|
||||
toDateTime(thisAs<ObjDate>().date, tz)
|
||||
}
|
||||
addFnDoc("addDays", "Return a new Date with the specified number of days added (or subtracted if negative).",
|
||||
params = listOf(ParamDoc("days", type = type("lyng.Int"))),
|
||||
returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||
ObjDate(addDays(thisAs<ObjDate>().date, n))
|
||||
}
|
||||
addFnDoc("addMonths", "Return a new Date with the specified number of months added (or subtracted if negative). End-of-month values are normalized.",
|
||||
params = listOf(ParamDoc("months", type = type("lyng.Int"))),
|
||||
returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||
ObjDate(addMonths(thisAs<ObjDate>().date, n))
|
||||
}
|
||||
addFnDoc("addYears", "Return a new Date with the specified number of years added (or subtracted if negative).",
|
||||
params = listOf(ParamDoc("years", type = type("lyng.Int"))),
|
||||
returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||
ObjDate(addYears(thisAs<ObjDate>().date, n))
|
||||
}
|
||||
addFnDoc("daysUntil", "Return the number of whole calendar days until the other date.",
|
||||
params = listOf(ParamDoc("other", type = type("lyng.Date"))),
|
||||
returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||
val other = requiredArg<ObjDate>(0)
|
||||
daysBetween(thisAs<ObjDate>().date, other.date).toObj()
|
||||
}
|
||||
addFnDoc("daysSince", "Return the number of whole calendar days since the other date.",
|
||||
params = listOf(ParamDoc("other", type = type("lyng.Date"))),
|
||||
returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||
val other = requiredArg<ObjDate>(0)
|
||||
daysBetween(other.date, thisAs<ObjDate>().date).toObj()
|
||||
}
|
||||
|
||||
addClassFnDoc("today", "Return today's date in the specified timezone, or in the current system timezone if omitted.",
|
||||
params = listOf(ParamDoc("tz", type = type("lyng.Any", true))),
|
||||
returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||
ObjDate(today(tz))
|
||||
}
|
||||
addClassFnDoc("parseIso", "Parse an ISO date string (`YYYY-MM-DD`) into a Date.",
|
||||
params = listOf(ParamDoc("string", type = type("lyng.String"))),
|
||||
returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
ObjDate(parseIso(this, requiredArg<ObjString>(0).value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseTimeZoneArg(scope: ScopeFacade, value: Obj?, default: TimeZone): TimeZone {
|
||||
return when (value) {
|
||||
null -> default
|
||||
is ObjString -> TimeZone.of(value.value)
|
||||
is ObjInt -> UtcOffset(seconds = value.value.toInt()).asTimeZone()
|
||||
else -> scope.raiseIllegalArgument("invalid timezone: $value")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseTimeZoneArg(scope: Scope, value: Obj?, default: TimeZone): TimeZone =
|
||||
parseTimeZoneArg(scope.asFacade(), value, default)
|
||||
|
||||
internal fun toDate(instant: kotlin.time.Instant, tz: TimeZone): LocalDate {
|
||||
val ldt = instant.toLocalDateTime(tz)
|
||||
return LocalDate(ldt.year, ldt.month.number, ldt.day)
|
||||
}
|
||||
|
||||
internal fun toDateTime(date: LocalDate, tz: TimeZone): ObjDateTime {
|
||||
val ldt = LocalDateTime(date.year, date.month.number, date.day, 0, 0, 0)
|
||||
return ObjDateTime(ldt.toInstant(tz), tz)
|
||||
}
|
||||
|
||||
private fun parseIso(scope: ScopeFacade, value: String): LocalDate {
|
||||
val match = DATE_REGEX.matchEntire(value.trim()) ?: scope.raiseIllegalArgument("invalid ISO date string: $value")
|
||||
val year = match.groupValues[1].toInt()
|
||||
val month = match.groupValues[2].toInt()
|
||||
val day = match.groupValues[3].toInt()
|
||||
return createDate(scope, year, month, day)
|
||||
}
|
||||
|
||||
private fun parseIso(scope: Scope, value: String): LocalDate = parseIso(scope.asFacade(), value)
|
||||
|
||||
private fun createDate(scope: ScopeFacade, year: Int, month: Int, day: Int): LocalDate {
|
||||
if (month !in 1..12) scope.raiseIllegalArgument("month must be in 1..12")
|
||||
val maxDay = monthLength(year, month)
|
||||
if (day !in 1..maxDay) scope.raiseIllegalArgument("day must be in 1..$maxDay")
|
||||
return LocalDate(year, month, day)
|
||||
}
|
||||
|
||||
private fun createDate(scope: Scope, year: Int, month: Int, day: Int): LocalDate =
|
||||
createDate(scope.asFacade(), year, month, day)
|
||||
|
||||
private fun today(tz: TimeZone): LocalDate = toDate(Clock.System.now(), tz)
|
||||
|
||||
private fun addDays(date: LocalDate, days: Int): LocalDate {
|
||||
val start = LocalDateTime(date.year, date.month.number, date.day, 0, 0, 0).toInstant(TimeZone.UTC)
|
||||
return toDate(start + days.days, TimeZone.UTC)
|
||||
}
|
||||
|
||||
private fun daysBetween(start: LocalDate, end: LocalDate): Int {
|
||||
val startInstant = LocalDateTime(start.year, start.month.number, start.day, 0, 0, 0).toInstant(TimeZone.UTC)
|
||||
val endInstant = LocalDateTime(end.year, end.month.number, end.day, 0, 0, 0).toInstant(TimeZone.UTC)
|
||||
return ((endInstant.epochSeconds - startInstant.epochSeconds) / 86_400L).toInt()
|
||||
}
|
||||
|
||||
private fun addMonths(date: LocalDate, months: Int): LocalDate {
|
||||
if (months == 0) return date
|
||||
val totalMonths = date.year.toLong() * 12L + (date.month.number - 1).toLong() + months.toLong()
|
||||
val newYear = floorDiv(totalMonths, 12L).toInt()
|
||||
val newMonth = floorMod(totalMonths, 12L).toInt() + 1
|
||||
val newDay = minOf(date.day, monthLength(newYear, newMonth))
|
||||
return LocalDate(newYear, newMonth, newDay)
|
||||
}
|
||||
|
||||
private fun addYears(date: LocalDate, years: Int): LocalDate {
|
||||
if (years == 0) return date
|
||||
val newYear = date.year + years
|
||||
val newDay = minOf(date.day, monthLength(newYear, date.month.number))
|
||||
return LocalDate(newYear, date.month.number, newDay)
|
||||
}
|
||||
|
||||
private fun requireWholeDays(scope: Scope, duration: Duration): Int {
|
||||
val days = duration.inWholeDays
|
||||
if (days.absoluteValue > Int.MAX_VALUE.toLong()) {
|
||||
scope.raiseIllegalArgument("date arithmetic day count is too large")
|
||||
}
|
||||
if (duration != days.days) {
|
||||
scope.raiseIllegalArgument("Date arithmetic supports only whole-day durations")
|
||||
}
|
||||
return days.toInt()
|
||||
}
|
||||
|
||||
private fun isLeapYear(year: Int): Boolean =
|
||||
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
|
||||
|
||||
private fun monthLength(year: Int, month: Int): Int = when (month) {
|
||||
1, 3, 5, 7, 8, 10, 12 -> 31
|
||||
4, 6, 9, 11 -> 30
|
||||
2 -> if (isLeapYear(year)) 29 else 28
|
||||
else -> error("invalid month: $month")
|
||||
}
|
||||
|
||||
private fun floorDiv(a: Long, b: Long): Long {
|
||||
var q = a / b
|
||||
if ((a xor b) < 0 && q * b != a) q -= 1
|
||||
return q
|
||||
}
|
||||
|
||||
private fun floorMod(a: Long, b: Long): Long = a - floorDiv(a, b) * b
|
||||
|
||||
private val DATE_REGEX = Regex("""([+-]?\d{4,})-(\d{2})-(\d{2})""")
|
||||
@@ -202,10 +202,15 @@ class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||
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() })
|
||||
addPropertyDoc("date", "The calendar date component of this DateTime.", type("lyng.Date"), moduleName = "lyng.time",
|
||||
getter = { ObjDate(toDate(thisAs<ObjDateTime>().instant, thisAs<ObjDateTime>().timeZone)) })
|
||||
|
||||
addFnDoc("toInstant", "Convert this localized date time back to an absolute Instant.", returns = type("lyng.Instant"), moduleName = "lyng.time") {
|
||||
ObjInstant(thisAs<ObjDateTime>().instant)
|
||||
}
|
||||
addFnDoc("toDate", "Return the calendar date component of this DateTime.", returns = type("lyng.Date"), moduleName = "lyng.time") {
|
||||
ObjDate(toDate(thisAs<ObjDateTime>().instant, thisAs<ObjDateTime>().timeZone))
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -223,11 +228,7 @@ class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||
"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")
|
||||
}
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||
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") {
|
||||
|
||||
@@ -268,14 +268,19 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
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")
|
||||
}
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||
ObjDateTime(thisAs<ObjInstant>().instant, tz)
|
||||
}
|
||||
addFnDoc(
|
||||
name = "toDate",
|
||||
doc = "Convert this instant to a calendar Date in the specified time zone. If omitted, the system default time zone is used.",
|
||||
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any", true))),
|
||||
returns = type("lyng.Date"),
|
||||
moduleName = "lyng.time"
|
||||
) {
|
||||
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||
ObjDate(toDate(thisAs<ObjInstant>().instant, tz))
|
||||
}
|
||||
|
||||
// class members
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
|
||||
Buffer(ObjBuffer.type, 50),
|
||||
Instant(ObjInstant.type, 30),
|
||||
Duration(ObjDuration.type),
|
||||
Other(Obj.rootObjectType, 60);
|
||||
Other(Obj.rootObjectType, 60),
|
||||
Date(ObjDate.type, 20);
|
||||
|
||||
fun generalizeTo(other: LynonType): LynonType? {
|
||||
if (this == other) return this
|
||||
@@ -211,4 +212,4 @@ open class LynonEncoder(val bout: BitOutput, val settings: LynonSettings = Lynon
|
||||
bout.putBit(bit)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -3479,6 +3480,68 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDateComprehensive() = runTest {
|
||||
eval(
|
||||
"""
|
||||
import lyng.time
|
||||
import lyng.serialization
|
||||
|
||||
val d1 = Date(2026, 4, 15)
|
||||
assertEquals(2026, d1.year)
|
||||
assertEquals(4, d1.month)
|
||||
assertEquals(15, d1.day)
|
||||
assertEquals(15, d1.dayOfMonth)
|
||||
assertEquals("2026-04-15", d1.toIsoString())
|
||||
assertEquals("2026-04-15", d1.toSortableString())
|
||||
|
||||
val d2 = Date.parseIso("2024-02-29")
|
||||
assertEquals(true, d2.isLeapYear)
|
||||
assertEquals(29, d2.lengthOfMonth)
|
||||
assertEquals(366, d2.lengthOfYear)
|
||||
assertEquals(60, d2.dayOfYear)
|
||||
assertEquals(4, d2.dayOfWeek) // Thursday
|
||||
|
||||
val d3 = d2.addYears(1)
|
||||
assertEquals(Date(2025, 2, 28), d3)
|
||||
|
||||
val d4 = Date(2024, 1, 31).addMonths(1)
|
||||
assertEquals(Date(2024, 2, 29), d4)
|
||||
|
||||
val d5 = d1.addDays(10)
|
||||
assertEquals(Date(2026, 4, 25), d5)
|
||||
assertEquals(Date(2026, 4, 18), d1 + 3.days)
|
||||
assertEquals(Date(2026, 4, 12), d1 - 3.days)
|
||||
assertEquals(10, d1.daysUntil(d5))
|
||||
assertEquals(10, d5.daysSince(d1))
|
||||
assertEquals(10, d5 - d1)
|
||||
|
||||
val i = Instant("2024-01-01T23:30:00Z")
|
||||
assertEquals(Date(2024, 1, 1), i.toDate("Z"))
|
||||
assertEquals(Date(2024, 1, 2), i.toDate("+02:00"))
|
||||
|
||||
val dt = DateTime(2024, 5, 20, 15, 30, 45, "+02:00")
|
||||
assertEquals(Date(2024, 5, 20), dt.date)
|
||||
assertEquals(Date(2024, 5, 20), dt.toDate())
|
||||
assertEquals(DateTime(2024, 5, 20, 0, 0, 0, "Z"), Date(2024, 5, 20).toDateTime("Z"))
|
||||
assertEquals(DateTime(2024, 5, 20, 0, 0, 0, "+02:00"), Date(2024, 5, 20).atStartOfDay("+02:00"))
|
||||
|
||||
val roundTrip = Lynon.decode(Lynon.encode(d1))
|
||||
assertEquals(d1, roundTrip)
|
||||
assertEquals("\"2026-04-15\"", d1.toJsonString())
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val decoded = eval(
|
||||
"""
|
||||
import lyng.time
|
||||
Date(2026, 4, 15)
|
||||
""".trimIndent()
|
||||
).decodeSerializable<LocalDate>()
|
||||
|
||||
assertEquals(LocalDate(2026, 4, 15), decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoubleImports() = runTest {
|
||||
val s = Scope.new()
|
||||
|
||||
Reference in new issue
Block a user