Add Date type and database release docs

This commit is contained in:
sergeych committed 2026-04-15 23:20:23 +03:00
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()