From 732d8f38774465b05bfc11f1324d76f61cec6bba Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 9 Jul 2025 23:59:01 +0300 Subject: [PATCH] fix #34 minimal time manipulation --- docs/time.md | 123 ++++++++++++++++ docs/tutorial.md | 14 +- lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/Arguments.kt | 3 + .../kotlin/net/sergeych/lyng/Compiler.kt | 2 +- .../kotlin/net/sergeych/lyng/Obj.kt | 2 + .../kotlin/net/sergeych/lyng/ObjBuffer.kt | 15 +- .../kotlin/net/sergeych/lyng/ObjClass.kt | 31 +++- .../kotlin/net/sergeych/lyng/ObjDuration.kt | 137 ++++++++++++++++++ .../kotlin/net/sergeych/lyng/ObjInstant.kt | 91 ++++++++++++ .../kotlin/net/sergeych/lyng/Parser.kt | 2 +- .../kotlin/net/sergeych/lyng/Script.kt | 14 ++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 47 ++++++ lynglib/src/jvmTest/kotlin/BookTest.kt | 11 ++ 14 files changed, 478 insertions(+), 16 deletions(-) create mode 100644 docs/time.md create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjDuration.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInstant.kt diff --git a/docs/time.md b/docs/time.md new file mode 100644 index 0000000..212b114 --- /dev/null +++ b/docs/time.md @@ -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. + + diff --git a/docs/tutorial.md b/docs/tutorial.md index 6a938da..ecdf482 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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 diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 10ff290..b243445 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 91f8097..ae909e8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -42,6 +42,9 @@ data class Arguments(val list: List,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) = Arguments(values.toList()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 1171874..d06f304 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 009babf..9377136 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -320,6 +320,8 @@ open class Obj { } } +fun Double.toObj(): Obj = ObjReal(this) + @Suppress("unused") inline fun T.toObj(): Obj = Obj.from(this) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBuffer.kt index 080e4de..9054481 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBuffer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjBuffer.kt @@ -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.. { 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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index cfd3bab..23d0c81 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -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() + private val classMembers = mutableMapOf() 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) + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjDuration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjDuration.kt new file mode 100644 index 0000000..6ccb202 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjDuration.kt @@ -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().duration.toDouble(DurationUnit.DAYS).toObj() + } + addFn("hours") { + thisAs().duration.toDouble(DurationUnit.HOURS).toObj() + } + addFn("minutes") { + thisAs().duration.toDouble(DurationUnit.MINUTES).toObj() + } + addFn("seconds") { + thisAs().duration.toDouble(DurationUnit.SECONDS).toObj() + } + addFn("milliseconds") { + thisAs().duration.toDouble(DurationUnit.MILLISECONDS).toObj() + } + ObjInt.type.addFn("seconds") { + ObjDuration(thisAs().value.seconds) + } + + ObjInt.type.addFn("second") { + ObjDuration(thisAs().value.seconds) + } + + ObjInt.type.addFn("milliseconds") { + ObjDuration(thisAs().value.milliseconds) + } + + ObjInt.type.addFn("millisecond") { + ObjDuration(thisAs().value.milliseconds) + } + + ObjReal.type.addFn("seconds") { + ObjDuration(thisAs().value.seconds) + } + + ObjReal.type.addFn("second") { + ObjDuration(thisAs().value.seconds) + } + + ObjReal.type.addFn("milliseconds") { + ObjDuration(thisAs().value.milliseconds) + } + ObjReal.type.addFn("millisecond") { + ObjDuration(thisAs().value.milliseconds) + } + + ObjInt.type.addFn("minutes") { + ObjDuration(thisAs().value.minutes) + } + ObjReal.type.addFn("minutes") { + ObjDuration(thisAs().value.minutes) + } + ObjInt.type.addFn("minute") { + ObjDuration(thisAs().value.minutes) + } + ObjReal.type.addFn("minute") { + ObjDuration(thisAs().value.minutes) + } + ObjInt.type.addFn("hours") { + ObjDuration(thisAs().value.hours) + } + ObjReal.type.addFn("hours") { + ObjDuration(thisAs().value.hours) + } + ObjInt.type.addFn("hour") { + ObjDuration(thisAs().value.hours) + } + ObjReal.type.addFn("hour") { + ObjDuration(thisAs().value.hours) + } + ObjInt.type.addFn("days") { + ObjDuration(thisAs().value.days) + } + ObjReal.type.addFn("days") { + ObjDuration(thisAs().value.days) + } + ObjInt.type.addFn("day") { + ObjDuration(thisAs().value.days) + } + ObjReal.type.addFn("day") { + ObjDuration(thisAs().value.days) + } + + +// addFn("epochSeconds") { +// val instant = thisAs().instant +// ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9) +// } +// addFn("epochMilliseconds") { +// ObjInt(instant.toEpochMilliseconds()) +// } + } + } +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInstant.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInstant.kt new file mode 100644 index 0000000..7030648 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjInstant.kt @@ -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().instant + ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9) + } + addFn("isDistantFuture") { + thisAs().instant.isDistantFuture.toObj() + } + addFn("isDistantPast") { + thisAs().instant.isDistantPast.toObj() + } + addClassConst("distantFuture", distantFuture) + addClassConst("distantPast", distantPast) +// addFn("epochMilliseconds") { +// ObjInt(instant.toEpochMilliseconds()) +// } + } + + } +} + + diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 87fa4a5..cb7cc00 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -250,7 +250,7 @@ private class Parser(fromPos: Pos) { in digitsSet -> { pos.back() - decodeNumber(loadChars(digits), from) + decodeNumber(loadChars { it in digitsSet || it == '_'}, from) } '\'' -> { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 5d530f1..a6b118d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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 + } + } } } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 56032f0..ba8368c 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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() + ) + } + } \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index c6c4659..23fd2ec 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.kt @@ -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 = } else { var isValid = true val result = mutableListOf() + + // 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") + } + } \ No newline at end of file