diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dfd027..045931b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.0] - 2026-06-02 + +### Added + +- `Duration::as_seconds_f64` / `as_minutes_f64` / `as_hours_f64` — fractional + totals as `Double`. Magnitudes beyond ~2^53 ns (~104 days) lose sub-unit + precision. (#43) +- `Duration::humanize` — English elapsed formatting (e.g. `2 hours 30 minutes`): + days/hours/minutes/seconds, non-zero components only, negative durations + prefixed with `-`, sub-second magnitudes render as `0 seconds`. (#46) +- `Date::parse_ordinal` (`YYYY-DDD`) and `Date::parse_iso_week` (`YYYY-Www-D`), + the inverses of `format_ordinal` / `format_iso_week`. (#45) +- `DateTime::format_fixed` — fixed-width `YYYY-MM-DDTHH:MM:SS.nnnnnnnnnZ` with + always nine fractional digits; byte-for-byte string comparison matches + chronological order for years 0–9999. (#44) +- `DateTime::format_with(pattern)` — a brace-token format DSL (`{YYYY}` `{MM}` + `{DD}` `{HH}` `{mm}` `{ss}` `{fff}` `{nnnnnnnnn}`), with `{{` / `}}` for literal + braces; unknown tokens and unmatched braces raise `TempoError`. (#47) +- `Date::add_days_checked` / `add_months_checked` / `add_years_checked`, returning + `None` instead of wrapping when the result year leaves the `Int` range. (#42) + +### Changed + +- Hardened `Date` arithmetic against extreme-year `Int` overflow. `pad4_year` and + all formatting now handle the full `Int` year range (including `Int::min_value`) + instead of aborting; the epoch-day conversions use `Int64` so `add_days` and the + day↔date round-trip are correct across the whole `Int` year range (previously + wrong beyond ~±5.8M years). `Date::from_iso_week` now raises `TempoError` on a + residual out-of-range result year rather than silently wrapping. The infallible + `add_days` / `add_months` / `add_years` still wrap at the absolute `Int`-year + boundary; the new `*_checked` variants report it. (#42) + +### Fixed + +- `Date::format` and `Date::to_json` for negative (BCE) years now emit the proper + ISO 8601 expanded form (e.g. `-0001-01-01`), matching `DateTime::format`, + instead of the previous malformed `000-1-01-01`. (#42) + ## [0.7.0] - 2026-06-02 ### Added diff --git a/README.mbt.md b/README.mbt.md index 23f5b2b..fccff59 100644 --- a/README.mbt.md +++ b/README.mbt.md @@ -547,6 +547,13 @@ test { // Ordinal (day-of-year) dates, leap-aware. inspect(@tempo.Date::from_ordinal(2024, 60).format(), content="2024-02-29") inspect(@tempo.Date::new(2024, 12, 31).format_ordinal(), content="2024-366") + + // Parse the string forms (inverses of the format_* methods). + inspect( + @tempo.Date::parse_iso_week("2020-W53-5").format(), + content="2021-01-01", + ) + inspect(@tempo.Date::parse_ordinal("2024-060").format(), content="2024-02-29") } ``` @@ -564,6 +571,43 @@ test { } ``` +## Fractional and human-readable durations + +`as_seconds_f64` / `as_minutes_f64` / `as_hours_f64` give fractional totals as +`Double`. `humanize` renders an English elapsed string (days/hours/minutes/ +seconds, non-zero components only, `-` prefix for negatives). + +```moonbit nocheck +///| +test { + inspect(@tempo.Duration::minutes(90).as_hours_f64(), content="1.5") + inspect(@tempo.Duration::milliseconds(1500).as_seconds_f64(), content="1.5") + + let d = @tempo.Duration::hours(2) + @tempo.Duration::minutes(30) + inspect(d.humanize(), content="2 hours 30 minutes") + inspect(@tempo.Duration::seconds(1).humanize(), content="1 second") + inspect(@tempo.Duration::minutes(-90).humanize(), content="-1 hour 30 minutes") +} +``` + +## Custom formatting + +`format_fixed` produces a fixed-width timestamp (always nine fractional digits) +whose byte order matches chronological order for years 0–9999 — handy for log +lines and database keys. `format_with` is a brace-token DSL: the tokens +`{YYYY}`, `{MM}`, `{DD}`, `{HH}`, `{mm}`, `{ss}`, `{fff}`, and `{nnnnnnnnn}` +substitute, everything else is literal, `{{` and `}}` emit literal braces, and +an unknown token or unmatched brace raises. + +```moonbit nocheck +///| +test { + let dt = @tempo.DateTime::parse("2024-07-21T17:11:00.5Z") + inspect(dt.format_fixed(), content="2024-07-21T17:11:00.500000000Z") + inspect(dt.format_with("{YYYY}/{MM}/{DD} {HH}:{mm}"), content="2024/07/21 17:11") +} +``` + ## Not included - **Timezones / DST** — planned as a separate `tempo-tz` package diff --git a/moon.mod b/moon.mod index edff9ea..878e760 100644 --- a/moon.mod +++ b/moon.mod @@ -1,6 +1,6 @@ name = "brickfrog/tempo" -version = "0.7.0" +version = "0.8.0" readme = "README.mbt.md"