Add Date type and database release docs
This commit is contained in:
parent
14dc73db3e
commit
b3be908242
21
CHANGELOG.md
21
CHANGELOG.md
@ -7,6 +7,27 @@ History note:
|
|||||||
- Entries below are synchronized and curated for `1.5.x`.
|
- Entries below are synchronized and curated for `1.5.x`.
|
||||||
- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
|
- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Database access
|
||||||
|
- Added the portable `lyng.io.db` SQL contract and the first concrete provider, `lyng.io.db.sqlite`.
|
||||||
|
- Added SQLite support on JVM and Linux Native with:
|
||||||
|
- generic `openDatabase("sqlite:...")` dispatch
|
||||||
|
- typed `openSqlite(...)` helper
|
||||||
|
- real nested transactions via savepoints
|
||||||
|
- generated keys through `ExecutionResult.getGeneratedKeys()`
|
||||||
|
- strict schema-driven value conversion for `Bool`, `Decimal`, `Date`, `DateTime`, and `Instant`
|
||||||
|
- documented option handling for `readOnly`, `createIfMissing`, `foreignKeys`, and `busyTimeoutMillis`
|
||||||
|
- Added public docs for database usage and SQLite provider behavior.
|
||||||
|
|
||||||
|
### Time
|
||||||
|
- Added `Date` to `lyng.time` and the core library as a first-class calendar-date type.
|
||||||
|
- Added `Instant.toDate(...)`, `DateTime.date`, `DateTime.toDate()`, `Date.toDateTime(...)`, and related date arithmetic.
|
||||||
|
- Added docs, stdlib reference updates, serialization support, and comprehensive tests for `Date`.
|
||||||
|
|
||||||
|
### Release notes
|
||||||
|
- Full `:lyngio:jvmTest` and `:lyngio:linuxX64Test` pass on the release tree after SQLite hardening.
|
||||||
|
|
||||||
## 1.5.4 (2026-04-03)
|
## 1.5.4 (2026-04-03)
|
||||||
|
|
||||||
### Runtime and compiler stability
|
### Runtime and compiler stability
|
||||||
|
|||||||
@ -52,6 +52,8 @@ assertEquals(A.E.One, A.One)
|
|||||||
- [What's New in 1.5](docs/whats_new_1_5.md)
|
- [What's New in 1.5](docs/whats_new_1_5.md)
|
||||||
- [Testing and Assertions](docs/Testing.md)
|
- [Testing and Assertions](docs/Testing.md)
|
||||||
- [Filesystem and Processes (lyngio)](docs/lyngio.md)
|
- [Filesystem and Processes (lyngio)](docs/lyngio.md)
|
||||||
|
- [SQL Databases (lyng.io.db)](docs/lyng.io.db.md)
|
||||||
|
- [Time and Calendar Types](docs/time.md)
|
||||||
- [Return Statement](docs/return_statement.md)
|
- [Return Statement](docs/return_statement.md)
|
||||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||||
- [Samples directory](docs/samples)
|
- [Samples directory](docs/samples)
|
||||||
|
|||||||
@ -81,7 +81,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
- `import lyng.serialization`
|
- `import lyng.serialization`
|
||||||
- `Lynon` serialization utilities.
|
- `Lynon` serialization utilities.
|
||||||
- `import lyng.time`
|
- `import lyng.time`
|
||||||
- `Instant`, `DateTime`, `Duration`, and module `delay`.
|
- `Instant`, `Date`, `DateTime`, `Duration`, and module `delay`.
|
||||||
|
|
||||||
## 6. Optional (lyngio) Modules
|
## 6. Optional (lyngio) Modules
|
||||||
Requires installing `lyngio` into the import manager from host code.
|
Requires installing `lyngio` into the import manager from host code.
|
||||||
|
|||||||
240
docs/lyng.io.db.md
Normal file
240
docs/lyng.io.db.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
### lyng.io.db — SQL database access for Lyng scripts
|
||||||
|
|
||||||
|
This module provides the portable SQL database contract for Lyng. The first shipped provider is SQLite via `lyng.io.db.sqlite`.
|
||||||
|
|
||||||
|
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Install the module into a Lyng session
|
||||||
|
|
||||||
|
For SQLite-backed database access, install both the generic DB module and the SQLite provider:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.io.db.createDbModule
|
||||||
|
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||||
|
|
||||||
|
suspend fun bootstrapDb() {
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope: Scope = session.getScope()
|
||||||
|
createDbModule(scope)
|
||||||
|
createSqliteModule(scope)
|
||||||
|
session.eval("""
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`createSqliteModule(...)` also registers the `sqlite:` scheme for generic `openDatabase(...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Using from Lyng scripts
|
||||||
|
|
||||||
|
Typed SQLite open helper:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
|
||||||
|
val userCount = db.transaction { tx ->
|
||||||
|
tx.execute("create table user(id integer primary key autoincrement, name text not null)")
|
||||||
|
tx.execute("insert into user(name) values(?)", "Ada")
|
||||||
|
tx.execute("insert into user(name) values(?)", "Linus")
|
||||||
|
tx.select("select count(*) as count from user").toList()[0]["count"]
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(2, userCount)
|
||||||
|
```
|
||||||
|
|
||||||
|
Generic provider-based open:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
val db = openDatabase(
|
||||||
|
"sqlite:./app.db",
|
||||||
|
Map(
|
||||||
|
"foreignKeys" => true,
|
||||||
|
"busyTimeoutMillis" => 5000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested transactions use real savepoint semantics:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
|
||||||
|
tx.execute("insert into item(name) values(?)", "outer")
|
||||||
|
|
||||||
|
try {
|
||||||
|
tx.transaction { inner ->
|
||||||
|
inner.execute("insert into item(name) values(?)", "inner")
|
||||||
|
throw IllegalStateException("rollback nested")
|
||||||
|
}
|
||||||
|
} catch (_: IllegalStateException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, tx.select("select count(*) as count from item").toList()[0]["count"])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Intentional rollback without treating it as a backend failure:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
|
|
||||||
|
val db = openSqlite(":memory:")
|
||||||
|
|
||||||
|
assertThrows(RollbackException) {
|
||||||
|
db.transaction { tx ->
|
||||||
|
tx.execute("create table item(id integer primary key autoincrement, name text not null)")
|
||||||
|
tx.execute("insert into item(name) values(?)", "temporary")
|
||||||
|
throw RollbackException("stop here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Portable API
|
||||||
|
|
||||||
|
##### `Database`
|
||||||
|
|
||||||
|
- `transaction(block)` — opens a transaction, commits on normal exit, rolls back on uncaught failure.
|
||||||
|
|
||||||
|
##### `SqlTransaction`
|
||||||
|
|
||||||
|
- `select(clause, params...)` — execute a statement whose primary result is a row set.
|
||||||
|
- `execute(clause, params...)` — execute a side-effect statement and return `ExecutionResult`.
|
||||||
|
- `transaction(block)` — nested transaction with real savepoint semantics.
|
||||||
|
|
||||||
|
##### `ResultSet`
|
||||||
|
|
||||||
|
- `columns` — positional `SqlColumn` metadata, available before iteration.
|
||||||
|
- `size()` — result row count.
|
||||||
|
- `isEmpty()` — fast emptiness check where possible.
|
||||||
|
- `iterator()` / `toList()` — normal row iteration.
|
||||||
|
|
||||||
|
##### `SqlRow`
|
||||||
|
|
||||||
|
- `row[index]` — zero-based positional access.
|
||||||
|
- `row["columnName"]` — case-insensitive lookup by output column label.
|
||||||
|
|
||||||
|
Name-based access fails with `SqlUsageException` if the name is missing or ambiguous.
|
||||||
|
|
||||||
|
##### `ExecutionResult`
|
||||||
|
|
||||||
|
- `affectedRowsCount`
|
||||||
|
- `getGeneratedKeys()`
|
||||||
|
|
||||||
|
Statements that return rows directly, such as `... returning ...`, should use `select(...)`, not `execute(...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Value mapping
|
||||||
|
|
||||||
|
Portable bind values:
|
||||||
|
|
||||||
|
- `null`
|
||||||
|
- `Bool`
|
||||||
|
- `Int`, `Double`, `Decimal`
|
||||||
|
- `String`
|
||||||
|
- `Buffer`
|
||||||
|
- `Date`, `DateTime`, `Instant`
|
||||||
|
|
||||||
|
Unsupported parameter values fail with `SqlUsageException`.
|
||||||
|
|
||||||
|
Portable result metadata categories:
|
||||||
|
|
||||||
|
- `Binary`
|
||||||
|
- `String`
|
||||||
|
- `Int`
|
||||||
|
- `Double`
|
||||||
|
- `Decimal`
|
||||||
|
- `Bool`
|
||||||
|
- `Date`
|
||||||
|
- `DateTime`
|
||||||
|
- `Instant`
|
||||||
|
|
||||||
|
For temporal types, see [time functions](time.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SQLite provider
|
||||||
|
|
||||||
|
`lyng.io.db.sqlite` currently provides the first concrete backend.
|
||||||
|
|
||||||
|
Typed helper:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
openSqlite(
|
||||||
|
path: String,
|
||||||
|
readOnly: Bool = false,
|
||||||
|
createIfMissing: Bool = true,
|
||||||
|
foreignKeys: Bool = true,
|
||||||
|
busyTimeoutMillis: Int = 5000
|
||||||
|
): Database
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepted generic URL forms:
|
||||||
|
|
||||||
|
- `sqlite::memory:`
|
||||||
|
- `sqlite:relative/path.db`
|
||||||
|
- `sqlite:/absolute/path.db`
|
||||||
|
|
||||||
|
Supported `openDatabase(..., extraParams)` keys for SQLite:
|
||||||
|
|
||||||
|
- `readOnly: Bool`
|
||||||
|
- `createIfMissing: Bool`
|
||||||
|
- `foreignKeys: Bool`
|
||||||
|
- `busyTimeoutMillis: Int`
|
||||||
|
|
||||||
|
SQLite write/read policy in v1:
|
||||||
|
|
||||||
|
- `Bool` writes as `0` / `1`
|
||||||
|
- `Decimal` writes as canonical text
|
||||||
|
- `Date` writes as `YYYY-MM-DD`
|
||||||
|
- `DateTime` writes as ISO local timestamp text without timezone
|
||||||
|
- `Instant` writes as ISO UTC timestamp text with explicit timezone marker
|
||||||
|
- `TIME*` values stay `String`
|
||||||
|
- `TIMESTAMP` / `DATETIME` reject timezone-bearing stored text
|
||||||
|
|
||||||
|
Open-time validation failures:
|
||||||
|
|
||||||
|
- malformed URL or bad option shape -> `IllegalArgumentException`
|
||||||
|
- runtime open failure -> `DatabaseException`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Lifetime rules
|
||||||
|
|
||||||
|
Result sets and rows are valid only while their owning transaction is active.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- do not keep `ResultSet` or `SqlRow` objects after the transaction block returns
|
||||||
|
- copy the values you need into ordinary Lyng objects inside the transaction
|
||||||
|
|
||||||
|
The same lifetime rule applies to generated keys returned by `ExecutionResult.getGeneratedKeys()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Platform support
|
||||||
|
|
||||||
|
- `lyng.io.db` — generic contract, available when host code installs it
|
||||||
|
- `lyng.io.db.sqlite` — implemented on JVM and Linux Native in the current release tree
|
||||||
|
|
||||||
|
For the broader I/O overview, see [lyngio overview](lyngio.md).
|
||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#### Included Modules
|
#### Included Modules
|
||||||
|
|
||||||
|
- **[lyng.io.db](lyng.io.db.md):** Portable SQL database access. Provides `Database`, `SqlTransaction`, `ResultSet`, and SQLite support through `lyng.io.db.sqlite`.
|
||||||
- **[lyng.io.fs](lyng.io.fs.md):** Async filesystem access. Provides the `Path` class for file/directory operations, streaming, and globbing.
|
- **[lyng.io.fs](lyng.io.fs.md):** Async filesystem access. Provides the `Path` class for file/directory operations, streaming, and globbing.
|
||||||
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
||||||
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
||||||
@ -43,6 +44,8 @@ To use `lyngio` modules in your scripts, you must install them into your Lyng sc
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.EvalSession
|
import net.sergeych.lyng.EvalSession
|
||||||
|
import net.sergeych.lyng.io.db.createDbModule
|
||||||
|
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyng.io.process.createProcessModule
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
import net.sergeych.lyng.io.console.createConsoleModule
|
||||||
@ -61,6 +64,8 @@ suspend fun runMyScript() {
|
|||||||
val scope = session.getScope()
|
val scope = session.getScope()
|
||||||
|
|
||||||
// Install modules with policies
|
// Install modules with policies
|
||||||
|
createDbModule(scope)
|
||||||
|
createSqliteModule(scope)
|
||||||
createFs(PermitAllAccessPolicy, scope)
|
createFs(PermitAllAccessPolicy, scope)
|
||||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||||
@ -70,6 +75,8 @@ suspend fun runMyScript() {
|
|||||||
|
|
||||||
// Now scripts can import them
|
// Now scripts can import them
|
||||||
session.eval("""
|
session.eval("""
|
||||||
|
import lyng.io.db
|
||||||
|
import lyng.io.db.sqlite
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
import lyng.io.process
|
import lyng.io.process
|
||||||
import lyng.io.console
|
import lyng.io.console
|
||||||
@ -77,6 +84,7 @@ suspend fun runMyScript() {
|
|||||||
import lyng.io.net
|
import lyng.io.net
|
||||||
import lyng.io.ws
|
import lyng.io.ws
|
||||||
|
|
||||||
|
println("SQLite available: " + (openSqlite(":memory:") != null))
|
||||||
println("Working dir: " + Path(".").readUtf8())
|
println("Working dir: " + Path(".").readUtf8())
|
||||||
println("OS: " + Platform.details().name)
|
println("OS: " + Platform.details().name)
|
||||||
println("TTY: " + Console.isTty())
|
println("TTY: " + Console.isTty())
|
||||||
@ -94,6 +102,7 @@ suspend fun runMyScript() {
|
|||||||
`lyngio` is built with a "Secure by Default" philosophy. Every I/O or process operation is checked against a policy.
|
`lyngio` is built with a "Secure by Default" philosophy. Every I/O or process operation is checked against a policy.
|
||||||
|
|
||||||
- **Filesystem Security:** Implement `FsAccessPolicy` to restrict access to specific paths or operations (e.g., read-only access to a sandbox directory).
|
- **Filesystem Security:** Implement `FsAccessPolicy` to restrict access to specific paths or operations (e.g., read-only access to a sandbox directory).
|
||||||
|
- **Database Installation:** Database access is still explicit-capability style. The host must install `lyng.io.db` and at least one provider such as `lyng.io.db.sqlite`; otherwise scripts cannot open databases.
|
||||||
- **Process Security:** Implement `ProcessAccessPolicy` to restrict which executables can be run or to disable shell execution entirely.
|
- **Process Security:** Implement `ProcessAccessPolicy` to restrict which executables can be run or to disable shell execution entirely.
|
||||||
- **Console Security:** Implement `ConsoleAccessPolicy` to control output writes, event reads, and raw mode switching.
|
- **Console Security:** Implement `ConsoleAccessPolicy` to control output writes, event reads, and raw mode switching.
|
||||||
- **HTTP Security:** Implement `HttpAccessPolicy` to restrict which requests scripts may send.
|
- **HTTP Security:** Implement `HttpAccessPolicy` to restrict which requests scripts may send.
|
||||||
@ -101,6 +110,7 @@ suspend fun runMyScript() {
|
|||||||
- **WebSocket Security:** Implement `WsAccessPolicy` to restrict websocket connects and message flow.
|
- **WebSocket Security:** Implement `WsAccessPolicy` to restrict websocket connects and message flow.
|
||||||
|
|
||||||
For more details, see the specific module documentation:
|
For more details, see the specific module documentation:
|
||||||
|
- [Database Module Details](lyng.io.db.md)
|
||||||
- [Filesystem Security Details](lyng.io.fs.md#access-policy-security)
|
- [Filesystem Security Details](lyng.io.fs.md#access-policy-security)
|
||||||
- [Process Security Details](lyng.io.process.md#security-policy)
|
- [Process Security Details](lyng.io.process.md#security-policy)
|
||||||
- [Console Module Details](lyng.io.console.md)
|
- [Console Module Details](lyng.io.console.md)
|
||||||
@ -112,16 +122,16 @@ For more details, see the specific module documentation:
|
|||||||
|
|
||||||
#### Platform Support Overview
|
#### Platform Support Overview
|
||||||
|
|
||||||
| Platform | lyng.io.fs | lyng.io.process | lyng.io.console | lyng.io.http | lyng.io.ws | lyng.io.net |
|
| Platform | lyng.io.db/sqlite | lyng.io.fs | lyng.io.process | lyng.io.console | lyng.io.http | lyng.io.ws | lyng.io.net |
|
||||||
| :--- | :---: | :---: | :---: | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||||
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| **Linux Native** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| **Linux Native** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| **Apple Native** | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
| **Apple Native** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ |
|
||||||
| **Windows Native** | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
| **Windows Native** | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
||||||
| **Android** | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
| **Android** | ⚠️ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||||
| **JS / Node** | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
| **JS / Node** | ❌ | ✅ | ❌ | ⚠️ | ✅ | ✅ | ✅ |
|
||||||
| **JS / Browser** | ✅ (in-memory) | ❌ | ⚠️ | ✅ | ✅ | ❌ |
|
| **JS / Browser** | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ✅ | ✅ | ❌ |
|
||||||
| **Wasm** | ✅ (in-memory) | ❌ | ⚠️ | ❌ | ❌ | ❌ |
|
| **Wasm** | ❌ | ✅ (in-memory) | ❌ | ⚠️ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
Legend:
|
Legend:
|
||||||
- `✅` supported
|
- `✅` supported
|
||||||
|
|||||||
181
docs/time.md
181
docs/time.md
@ -1,74 +1,135 @@
|
|||||||
# Lyng time functions
|
# Lyng time functions
|
||||||
|
|
||||||
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
Lyng date and time support requires importing `lyng.time`. The module provides four related types:
|
||||||
|
|
||||||
- `Instant` class for absolute time stamps with platform-dependent resolution.
|
- `Instant` for absolute timestamps.
|
||||||
- `DateTime` class for calendar-aware points in time within a specific time zone.
|
- `Date` for calendar dates without time-of-day or timezone.
|
||||||
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
|
- `DateTime` for calendar-aware points in time in a specific timezone.
|
||||||
|
- `Duration` for absolute elapsed time.
|
||||||
|
|
||||||
## Time instant: `Instant`
|
## Time instant: `Instant`
|
||||||
|
|
||||||
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
|
`Instant` represents some moment of time independently of the calendar. It is similar to SQL `TIMESTAMP`
|
||||||
|
or Kotlin `Instant`.
|
||||||
|
|
||||||
### Constructing and converting
|
### Constructing and converting
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// default constructor returns time now:
|
|
||||||
val t1 = Instant()
|
val t1 = Instant()
|
||||||
|
val t2 = Instant(1704110400)
|
||||||
// constructing from a number is treated as seconds since unix epoch:
|
|
||||||
val t2 = Instant(1704110400) // 2024-01-01T12:00:00Z
|
|
||||||
|
|
||||||
// from RFC3339 string:
|
|
||||||
val t3 = Instant("2024-01-01T12:00:00.123456Z")
|
val t3 = Instant("2024-01-01T12:00:00.123456Z")
|
||||||
|
|
||||||
// truncation:
|
val t4 = t3.truncateToMinute()
|
||||||
val t4 = t3.truncateToMinute
|
assertEquals("2024-01-01T12:00:00Z", t4.toRFC3339())
|
||||||
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
|
|
||||||
|
|
||||||
// to localized DateTime (uses system default TZ if not specified):
|
|
||||||
val dt = t3.toDateTime("+02:00")
|
val dt = t3.toDateTime("+02:00")
|
||||||
assertEquals(dt.hour, 14)
|
assertEquals(14, dt.hour)
|
||||||
|
|
||||||
|
val d = t3.toDate("Z")
|
||||||
|
assertEquals(Date(2024, 1, 1), d)
|
||||||
|
|
||||||
### Instant members
|
### Instant members
|
||||||
|
|
||||||
| member | description |
|
| member | description |
|
||||||
|--------------------------------|---------------------------------------------------------|
|
|--------------------------------|------------------------------------------------------|
|
||||||
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
| epochSeconds: Real | offset in seconds since Unix epoch |
|
||||||
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
| epochWholeSeconds: Int | whole seconds since Unix epoch |
|
||||||
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos |
|
| nanosecondsOfSecond: Int | nanoseconds within the current second |
|
||||||
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
| isDistantFuture: Bool | true if it is `Instant.distantFuture` |
|
||||||
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
| isDistantPast: Bool | true if it is `Instant.distantPast` |
|
||||||
| truncateToMinute: Instant | create new instance truncated to minute |
|
| truncateToMinute(): Instant | truncate to minute precision |
|
||||||
| truncateToSecond: Instant | create new instance truncated to second |
|
| truncateToSecond(): Instant | truncate to second precision |
|
||||||
| truncateToMillisecond: Instant | truncate new instance to millisecond |
|
| truncateToMillisecond(): Instant | truncate to millisecond precision |
|
||||||
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
| truncateToMicrosecond(): Instant | truncate to microsecond precision |
|
||||||
| toRFC3339(): String | format as RFC3339 string (UTC) |
|
| toRFC3339(): String | format as RFC3339 string in UTC |
|
||||||
| toDateTime(tz?): DateTime | localize to a TimeZone (ID string or offset seconds) |
|
| toDateTime(tz?): DateTime | localize to a timezone |
|
||||||
|
| toDate(tz?): Date | convert to a calendar date in a timezone |
|
||||||
|
|
||||||
## Calendar time: `DateTime`
|
## Calendar date: `Date`
|
||||||
|
|
||||||
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year,
|
`Date` represents a pure calendar date. It has no time-of-day and no attached timezone. Use it for values
|
||||||
month, and day.
|
like birthdays, due dates, invoice dates, and SQL `DATE` columns.
|
||||||
|
|
||||||
|
### Constructing
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
val today = Date()
|
||||||
|
val d1 = Date(2026, 4, 15)
|
||||||
|
val d2 = Date("2024-02-29")
|
||||||
|
val d3 = Date.parseIso("2024-02-29")
|
||||||
|
val d4 = Date(DateTime(2024, 5, 20, 15, 30, 45, "+02:00"))
|
||||||
|
val d5 = Date(Instant("2024-01-01T23:30:00Z"), "+02:00")
|
||||||
|
|
||||||
|
### Date members
|
||||||
|
|
||||||
|
| member | description |
|
||||||
|
|--------------------------------|------------------------------------------------------------|
|
||||||
|
| year: Int | year component |
|
||||||
|
| month: Int | month component (1..12) |
|
||||||
|
| day: Int | day of month (alias `dayOfMonth`) |
|
||||||
|
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||||
|
| dayOfYear: Int | day of year (1..365/366) |
|
||||||
|
| isLeapYear: Bool | whether this date is in a leap year |
|
||||||
|
| lengthOfMonth: Int | number of days in this month |
|
||||||
|
| lengthOfYear: Int | 365 or 366 |
|
||||||
|
| toIsoString(): String | ISO `YYYY-MM-DD` string |
|
||||||
|
| toSortableString(): String | alias to `toIsoString()` |
|
||||||
|
| toDateTime(tz="Z"): DateTime | start-of-day `DateTime` in the specified timezone |
|
||||||
|
| atStartOfDay(tz="Z"): DateTime | alias to `toDateTime()` |
|
||||||
|
| addDays(n): Date | add or subtract calendar days |
|
||||||
|
| addMonths(n): Date | add or subtract months, normalizing end-of-month |
|
||||||
|
| addYears(n): Date | add or subtract years |
|
||||||
|
| daysUntil(other): Int | calendar days until `other` |
|
||||||
|
| daysSince(other): Int | calendar days since `other` |
|
||||||
|
| static today(tz?): Date | today in the specified timezone |
|
||||||
|
| static parseIso(s): Date | parse ISO `YYYY-MM-DD` |
|
||||||
|
|
||||||
|
### Date arithmetic
|
||||||
|
|
||||||
|
`Date` supports only whole-day arithmetic. This is deliberate: calendar dates should not silently accept
|
||||||
|
sub-day durations.
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
val d1 = Date(2026, 4, 15)
|
||||||
|
val d2 = d1.addDays(10)
|
||||||
|
|
||||||
|
assertEquals(Date(2026, 4, 25), d2)
|
||||||
|
assertEquals(Date(2026, 4, 18), d1 + 3.days)
|
||||||
|
assertEquals(Date(2026, 4, 12), d1 - 3.days)
|
||||||
|
assertEquals(10, d1.daysUntil(d2))
|
||||||
|
assertEquals(10, d2.daysSince(d1))
|
||||||
|
assertEquals(10, d2 - d1)
|
||||||
|
|
||||||
|
### Date conversions
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
## Calendar time: `DateTime`
|
||||||
|
|
||||||
|
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components
|
||||||
|
such as year, month, day, and hour.
|
||||||
|
|
||||||
### Constructing
|
### Constructing
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// Current time in system default timezone
|
|
||||||
val now = DateTime.now()
|
val now = DateTime.now()
|
||||||
|
|
||||||
// Specific timezone
|
|
||||||
val offsetTime = DateTime.now("+02:00")
|
val offsetTime = DateTime.now("+02:00")
|
||||||
|
|
||||||
// From Instant
|
|
||||||
val dt = Instant().toDateTime("Z")
|
val dt = Instant().toDateTime("Z")
|
||||||
|
|
||||||
// By components (year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
|
|
||||||
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
|
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
|
||||||
|
|
||||||
// From RFC3339 string
|
|
||||||
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
|
||||||
|
|
||||||
### DateTime members
|
### DateTime members
|
||||||
@ -83,7 +144,9 @@ month, and day.
|
|||||||
| second: Int | second component (0..59) |
|
| second: Int | second component (0..59) |
|
||||||
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
|
||||||
| timeZone: String | timezone ID string |
|
| timeZone: String | timezone ID string |
|
||||||
|
| date: Date | calendar date component |
|
||||||
| toInstant(): Instant | convert back to absolute Instant |
|
| toInstant(): Instant | convert back to absolute Instant |
|
||||||
|
| toDate(): Date | extract the calendar date in this timezone |
|
||||||
| toUTC(): DateTime | shortcut to convert to UTC |
|
| toUTC(): DateTime | shortcut to convert to UTC |
|
||||||
| toTimeZone(tz): DateTime | convert to another timezone |
|
| toTimeZone(tz): DateTime | convert to another timezone |
|
||||||
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
|
||||||
@ -96,28 +159,27 @@ month, and day.
|
|||||||
|
|
||||||
`DateTime` handles calendar arithmetic correctly:
|
`DateTime` handles calendar arithmetic correctly:
|
||||||
|
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
|
||||||
val nextYear = leapDay.addYears(1)
|
val nextYear = leapDay.addYears(1)
|
||||||
assertEquals(nextYear.day, 28) // Feb 29, 2024 -> Feb 28, 2025
|
assertEquals(28, nextYear.day)
|
||||||
|
|
||||||
# `Duration` class
|
# `Duration` class
|
||||||
|
|
||||||
Represent absolute time distance between two `Instant`.
|
`Duration` represents absolute elapsed time between two instants.
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
val t1 = Instant()
|
val t1 = Instant()
|
||||||
|
|
||||||
// yes we can delay to period, and it is not blocking. is suspends!
|
|
||||||
delay(1.millisecond)
|
delay(1.millisecond)
|
||||||
|
|
||||||
val t2 = Instant()
|
val t2 = Instant()
|
||||||
// be suspend, so actual time may vary:
|
|
||||||
assert( t2 - t1 >= 1.millisecond)
|
assert(t2 - t1 >= 1.millisecond)
|
||||||
assert( t2 - t1 < 100.millisecond)
|
assert(t2 - t1 < 100.millisecond)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Duration can be converted from numbers, like `5.minutes` and so on. Extensions are created for
|
Duration values can be created from numbers using extensions on `Int` and `Real`:
|
||||||
`Int` and `Real`, so for n as Real or Int it is possible to create durations::
|
|
||||||
|
|
||||||
- `n.millisecond`, `n.milliseconds`
|
- `n.millisecond`, `n.milliseconds`
|
||||||
- `n.second`, `n.seconds`
|
- `n.second`, `n.seconds`
|
||||||
@ -125,10 +187,9 @@ Duration can be converted from numbers, like `5.minutes` and so on. Extensions a
|
|||||||
- `n.hour`, `n.hours`
|
- `n.hour`, `n.hours`
|
||||||
- `n.day`, `n.days`
|
- `n.day`, `n.days`
|
||||||
|
|
||||||
The bigger time units like months or years are calendar-dependent and can't be used with `Duration`.
|
Larger units like months or years are calendar-dependent and are intentionally not part of `Duration`.
|
||||||
|
|
||||||
Each duration instance can be converted to number of any of these time units, as `Real` number, if `d` is a `Duration`
|
Each duration instance can be converted to numbers in these units:
|
||||||
instance:
|
|
||||||
|
|
||||||
- `d.microseconds`
|
- `d.microseconds`
|
||||||
- `d.milliseconds`
|
- `d.milliseconds`
|
||||||
@ -137,18 +198,16 @@ instance:
|
|||||||
- `d.hours`
|
- `d.hours`
|
||||||
- `d.days`
|
- `d.days`
|
||||||
|
|
||||||
for example
|
Example:
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
assertEquals( 60, 1.minute.seconds )
|
|
||||||
assertEquals( 10.milliseconds, 0.01.seconds )
|
|
||||||
|
|
||||||
|
assertEquals(60, 1.minute.seconds)
|
||||||
|
assertEquals(10.milliseconds, 0.01.seconds)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
# Utility functions
|
# Utility functions
|
||||||
|
|
||||||
## delay(duration: Duration)
|
## `delay(duration: Duration)`
|
||||||
|
|
||||||
Suspends current coroutine for at least the specified duration.
|
|
||||||
|
|
||||||
|
|
||||||
|
Suspends the current coroutine for at least the specified duration.
|
||||||
|
|||||||
@ -18,8 +18,9 @@ For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5
|
|||||||
- Descending ranges and loops with `downTo` / `downUntil`
|
- Descending ranges and loops with `downTo` / `downUntil`
|
||||||
- String interpolation with `$name` and `${expr}`
|
- String interpolation with `$name` and `${expr}`
|
||||||
- Decimal arithmetic, matrices/vectors, and complex numbers
|
- Decimal arithmetic, matrices/vectors, and complex numbers
|
||||||
|
- Calendar `Date` support in `lyng.time`
|
||||||
- Immutable collections and opt-in `ObservableList`
|
- Immutable collections and opt-in `ObservableList`
|
||||||
- Rich `lyngio` modules for console, HTTP, WebSocket, TCP, and UDP
|
- Rich `lyngio` modules for SQLite databases, console, HTTP, WebSocket, TCP, and UDP
|
||||||
- CLI improvements including the built-in formatter `lyng fmt`
|
- CLI improvements including the built-in formatter `lyng fmt`
|
||||||
- Better IDE support and stronger docs around the released feature set
|
- Better IDE support and stronger docs around the released feature set
|
||||||
|
|
||||||
@ -579,7 +580,7 @@ ws.close()
|
|||||||
|
|
||||||
These modules are capability-gated and host-installed, keeping Lyng safe by default while making networked scripts practical when enabled.
|
These modules are capability-gated and host-installed, keeping Lyng safe by default while making networked scripts practical when enabled.
|
||||||
|
|
||||||
See [lyngio overview](lyngio.md), [lyng.io.http](lyng.io.http.md), [lyng.io.ws](lyng.io.ws.md), and [lyng.io.net](lyng.io.net.md).
|
See [lyngio overview](lyngio.md), [lyng.io.db](lyng.io.db.md), [lyng.io.http](lyng.io.http.md), [lyng.io.ws](lyng.io.ws.md), and [lyng.io.net](lyng.io.net.md).
|
||||||
|
|
||||||
### CLI: Formatting Command
|
### CLI: Formatting Command
|
||||||
A new `fmt` subcommand has been added to the Lyng CLI.
|
A new `fmt` subcommand has been added to the Lyng CLI.
|
||||||
|
|||||||
@ -40,6 +40,8 @@ import net.sergeych.lyng.ScriptError
|
|||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
import net.sergeych.lyng.asFacade
|
import net.sergeych.lyng.asFacade
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
import net.sergeych.lyng.io.console.createConsoleModule
|
||||||
|
import net.sergeych.lyng.io.db.createDbModule
|
||||||
|
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyng.io.http.createHttpModule
|
import net.sergeych.lyng.io.http.createHttpModule
|
||||||
import net.sergeych.lyng.io.net.createNetModule
|
import net.sergeych.lyng.io.net.createNetModule
|
||||||
@ -138,6 +140,7 @@ private val baseCliImportManagerDefer = globalDefer {
|
|||||||
private fun ImportManager.invalidateCliModuleCaches() {
|
private fun ImportManager.invalidateCliModuleCaches() {
|
||||||
invalidatePackageCache("lyng.io.fs")
|
invalidatePackageCache("lyng.io.fs")
|
||||||
invalidatePackageCache("lyng.io.console")
|
invalidatePackageCache("lyng.io.console")
|
||||||
|
invalidatePackageCache("lyng.io.db.sqlite")
|
||||||
invalidatePackageCache("lyng.io.http")
|
invalidatePackageCache("lyng.io.http")
|
||||||
invalidatePackageCache("lyng.io.ws")
|
invalidatePackageCache("lyng.io.ws")
|
||||||
invalidatePackageCache("lyng.io.net")
|
invalidatePackageCache("lyng.io.net")
|
||||||
@ -225,6 +228,8 @@ private fun installCliModules(manager: ImportManager) {
|
|||||||
// Scripts still need to import the modules they use explicitly.
|
// Scripts still need to import the modules they use explicitly.
|
||||||
createFs(PermitAllAccessPolicy, manager)
|
createFs(PermitAllAccessPolicy, manager)
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, manager)
|
createConsoleModule(PermitAllConsoleAccessPolicy, manager)
|
||||||
|
createDbModule(manager)
|
||||||
|
createSqliteModule(manager)
|
||||||
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
||||||
createWsModule(PermitAllWsAccessPolicy, manager)
|
createWsModule(PermitAllWsAccessPolicy, manager)
|
||||||
createNetModule(PermitAllNetAccessPolicy, manager)
|
createNetModule(PermitAllNetAccessPolicy, manager)
|
||||||
|
|||||||
@ -5003,9 +5003,16 @@ class Compiler(
|
|||||||
is QualifiedThisRef -> resolveClassByName(ref.typeName)
|
is QualifiedThisRef -> resolveClassByName(ref.typeName)
|
||||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForMember(it.ref) }
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverClassForMember(it.ref) }
|
||||||
is MethodCallRef -> inferMethodCallReturnClass(ref)
|
is MethodCallRef -> inferMethodCallReturnClass(ref)
|
||||||
is ImplicitThisMethodCallRef -> inferMethodCallReturnClass(ref.methodName())
|
is ImplicitThisMethodCallRef -> {
|
||||||
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
|
||||||
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
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 CallRef -> inferCallReturnTypeDecl(ref)?.let { resolveTypeDeclObjClass(it) } ?: inferCallReturnClass(ref)
|
||||||
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||||
is FieldRef -> {
|
is FieldRef -> {
|
||||||
@ -5127,6 +5134,8 @@ class Compiler(
|
|||||||
leftClass == ObjInstant.type && rightClass == ObjDuration.type -> ObjInstant.type
|
leftClass == ObjInstant.type && rightClass == ObjDuration.type -> ObjInstant.type
|
||||||
leftClass == ObjDuration.type && rightClass == ObjInstant.type && ref.op == BinOp.PLUS -> ObjInstant.type
|
leftClass == ObjDuration.type && rightClass == ObjInstant.type && ref.op == BinOp.PLUS -> ObjInstant.type
|
||||||
leftClass == ObjDuration.type && rightClass == ObjDuration.type -> ObjDuration.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)) &&
|
(leftClass == ObjBuffer.type || leftClass.allParentsSet.contains(ObjBuffer.type)) &&
|
||||||
(rightClass == ObjBuffer.type || rightClass.allParentsSet.contains(ObjBuffer.type)) &&
|
(rightClass == ObjBuffer.type || rightClass.allParentsSet.contains(ObjBuffer.type)) &&
|
||||||
ref.op == BinOp.PLUS -> ObjBuffer.type
|
ref.op == BinOp.PLUS -> ObjBuffer.type
|
||||||
@ -5198,7 +5207,37 @@ class Compiler(
|
|||||||
if (receiverClass != null && isClassScopeCallableMember(receiverClass.className, ref.name)) {
|
if (receiverClass != null && isClassScopeCallableMember(receiverClass.className, ref.name)) {
|
||||||
resolveClassByName("${receiverClass.className}.${ref.name}")?.let { return it }
|
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? {
|
private fun inferMethodCallReturnTypeDecl(ref: MethodCallRef): TypeDecl? {
|
||||||
@ -5423,13 +5462,13 @@ class Compiler(
|
|||||||
"truncateToSecond",
|
"truncateToSecond",
|
||||||
"truncateToMinute",
|
"truncateToMinute",
|
||||||
"truncateToMillisecond" -> ObjInstant.type
|
"truncateToMillisecond" -> ObjInstant.type
|
||||||
"toDateTime",
|
"today",
|
||||||
|
"parseIso" -> ObjDate.type
|
||||||
|
"daysUntil",
|
||||||
|
"daysSince" -> ObjInt.type
|
||||||
"toTimeZone",
|
"toTimeZone",
|
||||||
"toUTC",
|
"toUTC",
|
||||||
"parseRFC3339",
|
"parseRFC3339",
|
||||||
"addYears",
|
|
||||||
"addMonths",
|
|
||||||
"addDays",
|
|
||||||
"addHours",
|
"addHours",
|
||||||
"addMinutes",
|
"addMinutes",
|
||||||
"addSeconds" -> ObjDateTime.type
|
"addSeconds" -> ObjDateTime.type
|
||||||
@ -5486,6 +5525,20 @@ class Compiler(
|
|||||||
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
||||||
return ObjInstant.type
|
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(
|
if (targetClass == ObjInstant.type && name in listOf(
|
||||||
"truncateToMinute",
|
"truncateToMinute",
|
||||||
"truncateToSecond",
|
"truncateToSecond",
|
||||||
@ -5538,6 +5591,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
if (targetClass == ObjDateTime.type) {
|
if (targetClass == ObjDateTime.type) {
|
||||||
return when (name) {
|
return when (name) {
|
||||||
|
"date" -> ObjDate.type
|
||||||
"year",
|
"year",
|
||||||
"month",
|
"month",
|
||||||
"day",
|
"day",
|
||||||
@ -9677,6 +9731,7 @@ class Compiler(
|
|||||||
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||||
"Exception" -> ObjException.Root
|
"Exception" -> ObjException.Root
|
||||||
"Instant" -> ObjInstant.type
|
"Instant" -> ObjInstant.type
|
||||||
|
"Date" -> ObjDate.type
|
||||||
"DateTime" -> ObjDateTime.type
|
"DateTime" -> ObjDateTime.type
|
||||||
"Duration" -> ObjDuration.type
|
"Duration" -> ObjDuration.type
|
||||||
"Buffer" -> ObjBuffer.type
|
"Buffer" -> ObjBuffer.type
|
||||||
|
|||||||
@ -952,6 +952,12 @@ class Script(
|
|||||||
doc = "Point in time (epoch-based).",
|
doc = "Point in time (epoch-based).",
|
||||||
type = type("lyng.Class")
|
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(
|
it.addConstDoc(
|
||||||
name = "DateTime",
|
name = "DateTime",
|
||||||
value = ObjDateTime.type,
|
value = ObjDateTime.type,
|
||||||
|
|||||||
@ -1171,14 +1171,16 @@ class BytecodeCompiler(
|
|||||||
ObjMap.type,
|
ObjMap.type,
|
||||||
ObjBuffer.type,
|
ObjBuffer.type,
|
||||||
ObjInstant.type,
|
ObjInstant.type,
|
||||||
ObjDateTime.type
|
ObjDateTime.type,
|
||||||
|
ObjDate.type
|
||||||
)
|
)
|
||||||
BinOp.MINUS -> receiverClass in setOf(
|
BinOp.MINUS -> receiverClass in setOf(
|
||||||
ObjInt.type,
|
ObjInt.type,
|
||||||
ObjReal.type,
|
ObjReal.type,
|
||||||
ObjSet.type,
|
ObjSet.type,
|
||||||
ObjInstant.type,
|
ObjInstant.type,
|
||||||
ObjDateTime.type
|
ObjDateTime.type,
|
||||||
|
ObjDate.type
|
||||||
)
|
)
|
||||||
BinOp.STAR -> receiverClass in setOf(ObjInt.type, ObjReal.type, ObjString.type)
|
BinOp.STAR -> receiverClass in setOf(ObjInt.type, ObjReal.type, ObjString.type)
|
||||||
BinOp.SLASH, BinOp.PERCENT -> receiverClass in setOf(ObjInt.type, ObjReal.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) {
|
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
||||||
ObjRegex.type
|
ObjRegex.type
|
||||||
} else {
|
} else {
|
||||||
inferMethodCallReturnClass(ref.name)
|
inferMethodCallReturnClass(targetClass, ref.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallRef -> inferCallReturnClass(ref)
|
is CallRef -> inferCallReturnClass(ref)
|
||||||
@ -7463,7 +7465,7 @@ class BytecodeCompiler(
|
|||||||
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
if (targetClass == ObjString.type && ref.name == "re" && ref.args.isEmpty() && !ref.isOptional) {
|
||||||
ObjRegex.type
|
ObjRegex.type
|
||||||
} else {
|
} else {
|
||||||
inferMethodCallReturnClass(ref.name)
|
inferMethodCallReturnClass(targetClass, ref.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallRef -> inferCallReturnClass(ref)
|
is CallRef -> inferCallReturnClass(ref)
|
||||||
@ -7546,6 +7548,7 @@ class BytecodeCompiler(
|
|||||||
"ObservableList" -> ObjObservableList.type
|
"ObservableList" -> ObjObservableList.type
|
||||||
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
"ChangeRejectionException" -> ObjChangeRejectionExceptionClass
|
||||||
"Instant" -> ObjInstant.type
|
"Instant" -> ObjInstant.type
|
||||||
|
"Date" -> ObjDate.type
|
||||||
"DateTime" -> ObjDateTime.type
|
"DateTime" -> ObjDateTime.type
|
||||||
"Duration" -> ObjDuration.type
|
"Duration" -> ObjDuration.type
|
||||||
"Exception" -> ObjException.Root
|
"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) {
|
private fun inferMethodCallReturnClass(name: String): ObjClass? = when (name) {
|
||||||
"map",
|
"map",
|
||||||
"mapNotNull",
|
"mapNotNull",
|
||||||
@ -7616,13 +7649,13 @@ class BytecodeCompiler(
|
|||||||
"truncateToSecond",
|
"truncateToSecond",
|
||||||
"truncateToMinute",
|
"truncateToMinute",
|
||||||
"truncateToMillisecond" -> ObjInstant.type
|
"truncateToMillisecond" -> ObjInstant.type
|
||||||
"toDateTime",
|
"today",
|
||||||
|
"parseIso" -> ObjDate.type
|
||||||
|
"daysUntil",
|
||||||
|
"daysSince" -> ObjInt.type
|
||||||
"toTimeZone",
|
"toTimeZone",
|
||||||
"toUTC",
|
"toUTC",
|
||||||
"parseRFC3339",
|
"parseRFC3339",
|
||||||
"addYears",
|
|
||||||
"addMonths",
|
|
||||||
"addDays",
|
|
||||||
"addHours",
|
"addHours",
|
||||||
"addMinutes",
|
"addMinutes",
|
||||||
"addSeconds" -> ObjDateTime.type
|
"addSeconds" -> ObjDateTime.type
|
||||||
@ -7667,6 +7700,20 @@ class BytecodeCompiler(
|
|||||||
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
if (targetClass == ObjInstant.type && (name == "distantFuture" || name == "distantPast")) {
|
||||||
return ObjInstant.type
|
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") {
|
if (targetClass == ObjString.type && name == "re") {
|
||||||
return ObjRegex.type
|
return ObjRegex.type
|
||||||
}
|
}
|
||||||
@ -7710,6 +7757,7 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
if (targetClass == ObjDateTime.type) {
|
if (targetClass == ObjDateTime.type) {
|
||||||
return when (name) {
|
return when (name) {
|
||||||
|
"date" -> ObjDate.type
|
||||||
"year",
|
"year",
|
||||||
"month",
|
"month",
|
||||||
"day",
|
"day",
|
||||||
|
|||||||
@ -64,11 +64,13 @@ object StdlibDocsBootstrap {
|
|||||||
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
|
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
|
||||||
|
|
||||||
// Also touch time module types so their docs (moduleName = "lyng.time") are registered
|
// 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 {
|
try {
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _instant = net.sergeych.lyng.obj.ObjInstant.type
|
val _instant = net.sergeych.lyng.obj.ObjInstant.type
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _date = net.sergeych.lyng.obj.ObjDate.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _datetime = net.sergeych.lyng.obj.ObjDateTime.type
|
val _datetime = net.sergeych.lyng.obj.ObjDateTime.type
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _duration = net.sergeych.lyng.obj.ObjDuration.type
|
val _duration = net.sergeych.lyng.obj.ObjDuration.type
|
||||||
|
|||||||
303
lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDate.kt
Normal file
303
lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDate.kt
Normal file
@ -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() })
|
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",
|
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() })
|
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") {
|
addFnDoc("toInstant", "Convert this localized date time back to an absolute Instant.", returns = type("lyng.Instant"), moduleName = "lyng.time") {
|
||||||
ObjInstant(thisAs<ObjDateTime>().instant)
|
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") {
|
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()
|
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.",
|
"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"))),
|
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any"))),
|
||||||
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||||
val tz = when (val a = args.list.getOrNull(0)) {
|
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||||
is ObjString -> TimeZone.of(a.value)
|
|
||||||
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
|
||||||
else -> raiseIllegalArgument("invalid timezone: $a")
|
|
||||||
}
|
|
||||||
ObjDateTime(thisAs<ObjDateTime>().instant, tz)
|
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") {
|
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"),
|
returns = type("lyng.DateTime"),
|
||||||
moduleName = "lyng.time"
|
moduleName = "lyng.time"
|
||||||
) {
|
) {
|
||||||
val tz = when (val a = args.list.getOrNull(0)) {
|
val tz = parseTimeZoneArg(this, args.list.getOrNull(0), TimeZone.currentSystemDefault())
|
||||||
null -> TimeZone.currentSystemDefault()
|
|
||||||
is ObjString -> TimeZone.of(a.value)
|
|
||||||
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
|
||||||
else -> raiseIllegalArgument("invalid timezone: $a")
|
|
||||||
}
|
|
||||||
ObjDateTime(thisAs<ObjInstant>().instant, tz)
|
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
|
// class members
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,8 @@ enum class LynonType(val objClass: ObjClass, val defaultFrequency: Int = 1) {
|
|||||||
Buffer(ObjBuffer.type, 50),
|
Buffer(ObjBuffer.type, 50),
|
||||||
Instant(ObjInstant.type, 30),
|
Instant(ObjInstant.type, 30),
|
||||||
Duration(ObjDuration.type),
|
Duration(ObjDuration.type),
|
||||||
Other(Obj.rootObjectType, 60);
|
Other(Obj.rootObjectType, 60),
|
||||||
|
Date(ObjDate.type, 20);
|
||||||
|
|
||||||
fun generalizeTo(other: LynonType): LynonType? {
|
fun generalizeTo(other: LynonType): LynonType? {
|
||||||
if (this == other) return this
|
if (this == other) return this
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.toList
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
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
|
@Test
|
||||||
fun testDoubleImports() = runTest {
|
fun testDoubleImports() = runTest {
|
||||||
val s = Scope.new()
|
val s = Scope.new()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user