fix #34 minimal time manipulation

This commit is contained in:
Sergey Chernov 2025-07-09 23:59:01 +03:00
parent 23006b5caa
commit 732d8f3877
14 changed files with 478 additions and 16 deletions

123
docs/time.md Normal file
View File

@ -0,0 +1,123 @@
# Lyng time functions
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
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds, hours, days)
## Time instant: `Instant`
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time 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 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
// default constructor returns time now:
val t1 = Instant()
val t2 = Instant()
assert( t2 - t1 < 1.millisecond )
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
>>> void
## Constructing
import lyng.time
// empty constructor gives current time instant using system clock:
val now = Instant()
// constructor with Instant instance makes a copy:
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
## Instant members
| member | description |
|-----------------------|---------------------------------------------------------|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
| isDistantPast: Bool | true if it `Instant.distantPast` |
## Class members
| member | description |
|--------------------------------|---------------------------------------------------------|
| Instant.distantPast: Instant | most distant instant in past |
| Instant.distantFuture: Instant | most distant instant in future |
# `Duraion` class
Represent absolute time distance between two `Instant`.
import lyng.time
val t1 = Instant()
// yes we can delay to period, and it is not blocking. is suspends!
delay(1.millisecond)
val t2 = Instant()
// be suspend, so actual time may vary:
assert( t2 - t1 >= 1.millisecond)
assert( t2 - t1 < 100.millisecond)
>>> void
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
- `n.millisecond`, `n.milliseconds`
- `n.second`, `n.seconds`
- `n.minute`, `n.minutes`
- `n.hour`, `n.hours`
- `n.day`, `n.days`
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration` instance:
- `d.milliseconds`
- `d.seconds`
- `d.minutes`
- `d.hours`
- `d.days`
for example
import lyng.time
assertEquals( 60, 1.minute.seconds )
>>> void
# Utility functions
## delay(duration: Duration)
Suspends current coroutine for at least the specified duration.

View File

@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one:
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
- [math in Lyng](math.md)
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator]
- Some class references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md)
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
# Expressions
@ -668,7 +668,6 @@ are [Iterable]:
Please see [Map] reference for detailed description on using Maps.
# Flow control operators
## if-then-else
@ -1101,6 +1100,17 @@ and you can use ranges in for-loops:
See [Ranges](Range.md) for detailed documentation on it.
# Time routines
These should be imported from [lyng.time](time.md). For example:
import lyng.time
val now = Instant()
val hourAgo = now - 1.hour
See [more docs on time manipulation](time.md)
# Comments
// single line comment

View File

@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.7.3-SNAPSHOT"
version = "0.7.4-SNAPSHOT"
buildscript {
repositories {

View File

@ -42,6 +42,9 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
return list.map { it.toKotlin(scope) }
}
fun inspect(): String = list.joinToString(", ") { it.inspect() }
companion object {
val EMPTY = Arguments(emptyList())
fun from(values: Collection<Obj>) = Arguments(values.toList())

View File

@ -751,7 +751,7 @@ class Compiler(
val t = cc.next()
return when (t.type) {
Token.Type.INT, Token.Type.HEX -> {
val n = t.value.toLong(if (t.type == Token.Type.HEX) 16 else 10)
val n = t.value.replace("_", "").toLong(if (t.type == Token.Type.HEX) 16 else 10)
if (isPlus) ObjInt(n) else ObjInt(-n)
}

View File

@ -320,6 +320,8 @@ open class Obj {
}
}
fun Double.toObj(): Obj = ObjReal(this)
@Suppress("unused")
inline fun <reified T> T.toObj(): Obj = Obj.from(this)

View File

@ -25,8 +25,7 @@ class ObjBuffer(val byteArray: UByteArray) : Obj() {
val start: Int = index.startInt(scope)
val end: Int = index.exclusiveIntEnd(scope) ?: size
ObjBuffer(byteArray.sliceArray(start..<end))
}
else ObjInt(byteArray[checkIndex(scope, index)].toLong(), true)
} else ObjInt(byteArray[checkIndex(scope, index)].toLong(), true)
}
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
@ -42,7 +41,7 @@ class ObjBuffer(val byteArray: UByteArray) : Obj() {
val size by byteArray::size
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjBuffer) return -1
if (other !is ObjBuffer) return super.compareTo(scope, other)
val limit = min(size, other.size)
for (i in 0..<limit) {
val own = byteArray[i]
@ -60,7 +59,8 @@ class ObjBuffer(val byteArray: UByteArray) : Obj() {
ObjBuffer(byteArray + other.byteArray)
else if (other.isInstanceOf(ObjIterable)) {
ObjBuffer(
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray().toUByteArray()
byteArray + other.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray()
)
} else scope.raiseIllegalArgument("can't concatenate buffer with ${other.inspect()}")
}
@ -84,7 +84,8 @@ class ObjBuffer(val byteArray: UByteArray) : Obj() {
else -> {
if (obj.isInstanceOf(ObjIterable)) {
ObjBuffer(
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray().toUByteArray()
obj.toFlow(scope).map { it.toLong().toUByte() }.toList().toTypedArray()
.toUByteArray()
)
} else
scope.raiseIllegalArgument(

View File

@ -4,7 +4,7 @@ val ObjClassType by lazy { ObjClass("Class") }
open class ObjClass(
val className: String,
vararg val parents: ObjClass,
vararg parents: ObjClass,
) : Obj() {
var instanceConstructor: Statement? = null
@ -18,6 +18,7 @@ open class ObjClass(
// members: fields most often
private val members = mutableMapOf<String, ObjRecord>()
private val classMembers = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className
@ -32,9 +33,9 @@ open class ObjClass(
return instance
}
fun defaultInstance(): Obj = object : Obj() {
override val objClass: ObjClass = this@ObjClass
}
// fun defaultInstance(): Obj = object : Obj() {
// override val objClass: ObjClass = this@ObjClass
// }
fun createField(
name: String,
@ -49,11 +50,25 @@ open class ObjClass(
members[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun createClassField(
name: String,
initialValue: Obj,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = classMembers[name]
if( existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
classMembers[name] = ObjRecord(initialValue, isMutable, visibility)
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
}
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
/**
@ -68,6 +83,14 @@ open class ObjClass(
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
getInstanceMemberOrNull(name)
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
override suspend fun readField(scope: Scope, name: String): ObjRecord {
classMembers[name]?.let {
println("class field $it")
return it
}
return super.readField(scope, name)
}
}

View File

@ -0,0 +1,137 @@
package net.sergeych.lyng
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
class ObjDuration(val duration: Duration) : Obj() {
override val objClass: ObjClass = type
override fun toString(): String {
return duration.toString()
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if( other is ObjDuration)
duration.compareTo(other.duration)
else -1
}
companion object {
val type = object : ObjClass("Duration") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
if( args.list.size > 1 )
scope.raiseIllegalArgument("can't construct Duration(${args.inspect()})")
val a0 = args.list.getOrNull(0)
return ObjDuration(
when (a0) {
null -> Duration.ZERO
is ObjInt -> a0.value.seconds
is ObjReal -> a0.value.seconds
else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
}
}
)
}
}.apply {
addFn("days") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj()
}
addFn("hours") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj()
}
addFn("minutes") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj()
}
addFn("seconds") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj()
}
addFn("milliseconds") {
thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj()
}
ObjInt.type.addFn("seconds") {
ObjDuration(thisAs<ObjInt>().value.seconds)
}
ObjInt.type.addFn("second") {
ObjDuration(thisAs<ObjInt>().value.seconds)
}
ObjInt.type.addFn("milliseconds") {
ObjDuration(thisAs<ObjInt>().value.milliseconds)
}
ObjInt.type.addFn("millisecond") {
ObjDuration(thisAs<ObjInt>().value.milliseconds)
}
ObjReal.type.addFn("seconds") {
ObjDuration(thisAs<ObjReal>().value.seconds)
}
ObjReal.type.addFn("second") {
ObjDuration(thisAs<ObjReal>().value.seconds)
}
ObjReal.type.addFn("milliseconds") {
ObjDuration(thisAs<ObjReal>().value.milliseconds)
}
ObjReal.type.addFn("millisecond") {
ObjDuration(thisAs<ObjReal>().value.milliseconds)
}
ObjInt.type.addFn("minutes") {
ObjDuration(thisAs<ObjInt>().value.minutes)
}
ObjReal.type.addFn("minutes") {
ObjDuration(thisAs<ObjReal>().value.minutes)
}
ObjInt.type.addFn("minute") {
ObjDuration(thisAs<ObjInt>().value.minutes)
}
ObjReal.type.addFn("minute") {
ObjDuration(thisAs<ObjReal>().value.minutes)
}
ObjInt.type.addFn("hours") {
ObjDuration(thisAs<ObjInt>().value.hours)
}
ObjReal.type.addFn("hours") {
ObjDuration(thisAs<ObjReal>().value.hours)
}
ObjInt.type.addFn("hour") {
ObjDuration(thisAs<ObjInt>().value.hours)
}
ObjReal.type.addFn("hour") {
ObjDuration(thisAs<ObjReal>().value.hours)
}
ObjInt.type.addFn("days") {
ObjDuration(thisAs<ObjInt>().value.days)
}
ObjReal.type.addFn("days") {
ObjDuration(thisAs<ObjReal>().value.days)
}
ObjInt.type.addFn("day") {
ObjDuration(thisAs<ObjInt>().value.days)
}
ObjReal.type.addFn("day") {
ObjDuration(thisAs<ObjReal>().value.days)
}
// addFn("epochSeconds") {
// val instant = thisAs<ObjInstant>().instant
// ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
// }
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
}
}
}

View File

@ -0,0 +1,91 @@
package net.sergeych.lyng
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.isDistantFuture
import kotlinx.datetime.isDistantPast
class ObjInstant(val instant: Instant) : Obj() {
override val objClass: ObjClass get() = type
override fun toString(): String {
return instant.toString()
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant + other.duration)
else -> super.plus(scope, other)
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjInstant(instant - other.duration)
is ObjInstant -> ObjDuration(instant - other.instant)
else -> super.plus(scope, other)
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjInstant) {
return instant.compareTo(other.instant)
}
return super.compareTo(scope, other)
}
companion object {
val distantFuture by lazy {
ObjInstant(Instant.DISTANT_FUTURE)
}
val distantPast by lazy {
ObjInstant(Instant.DISTANT_PAST)
}
val type = object : ObjClass("Instant") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
val a0 = args.list.getOrNull(0)
return ObjInstant(
when (a0) {
null -> {
val t = Clock.System.now()
Instant.fromEpochSeconds(t.epochSeconds, (t.nanosecondsOfSecond / 1_000_000).toLong()*1_000_000)
}
is ObjInt -> Instant.fromEpochSeconds(a0.value)
is ObjReal -> {
val seconds = a0.value.toLong()
val nanos = (a0.value - seconds) * 1e9
Instant.fromEpochSeconds(seconds, nanos.toLong())
}
is ObjInstant -> a0.instant
else -> {
scope.raiseIllegalArgument("can't construct Instant(${args.inspect()})")
}
}
)
}
}.apply {
addFn("epochSeconds") {
val instant = thisAs<ObjInstant>().instant
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
}
addFn("isDistantFuture") {
thisAs<ObjInstant>().instant.isDistantFuture.toObj()
}
addFn("isDistantPast") {
thisAs<ObjInstant>().instant.isDistantPast.toObj()
}
addClassConst("distantFuture", distantFuture)
addClassConst("distantPast", distantPast)
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
}
}
}

View File

@ -250,7 +250,7 @@ private class Parser(fromPos: Pos) {
in digitsSet -> {
pos.back()
decodeNumber(loadChars(digits), from)
decodeNumber(loadChars { it in digitsSet || it == '_'}, from)
}
'\'' -> {

View File

@ -179,6 +179,20 @@ class Script(
addPackage("lyng.buffer") {
it.addConst("Buffer", ObjBuffer.type)
}
addPackage("lyng.time") {
it.addConst("Instant", ObjInstant.type)
it.addConst("Duration", ObjDuration.type)
it.addFn("delay") {
val a = args.firstAndOnly()
when(a) {
is ObjInt -> delay(a.value * 1000)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect()}")
}
ObjVoid
}
}
}
}

View File

@ -1,3 +1,4 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
@ -2513,4 +2514,50 @@ class ScriptTest {
""".trimIndent())
}
@Test
fun testInstant() = runTest {
eval(
"""
import lyng.time
val now = Instant()
// assertEquals( now.epochSeconds, Instant(now.epochSeconds).epochSeconds )
assert( 10.seconds is Duration )
assertEquals( 10.seconds, Duration(10) )
assertEquals( 10.milliseconds, Duration(0.01) )
assertEquals( 10.milliseconds, 0.01.seconds )
assertEquals( 1001.5.milliseconds, 1.0015.seconds )
val n1 = now + 7.seconds
assert( n1 is Instant )
assertEquals( n1 - now, 7.seconds )
assertEquals( now - n1, -7.seconds )
""".trimIndent()
)
delay(1000)
}
@Test
fun testTimeStatics() = runTest {
eval(
"""
import lyng.time
assert( 100.minutes is Duration )
assert( 100.days is Duration )
assert( 1.day == 24.hours )
assert( 1.day.hours == 24 )
assert( 1.hour.seconds == 3600 )
assert( 1.minute.milliseconds == 60_000 )
assert(Instant.distantFuture is Instant)
assert(Instant.distantPast is Instant)
assert( Instant.distantFuture - Instant.distantPast > 70_000_000.days)
val maxRange = Instant.distantFuture - Instant.distantPast
println("всего лет %g"(maxRange.days/365.2425))
""".trimIndent()
)
}
}

View File

@ -2,6 +2,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.ObjVoid
import net.sergeych.lyng.Scope
@ -105,6 +106,10 @@ fun parseDocTests(fileName: String, bookMode: Boolean = false): Flow<DocTest> =
} else {
var isValid = true
val result = mutableListOf<String>()
// remove empty trails:
while( block.last().isEmpty() ) block.removeLast()
while (block.size > outStart) {
val line = block.removeAt(outStart)
if (!line.startsWith(">>> ")) {
@ -288,4 +293,10 @@ class BookTest {
fun testExceptionsBooks() = runTest {
runDocTests("../docs/exceptions_handling.md")
}
@Test
fun testTimeBooks() = runBlocking {
runDocTests("../docs/time.md")
}
}