Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
```

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion moon.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "brickfrog/tempo"

version = "0.7.0"
version = "0.8.0"

readme = "README.mbt.md"

Expand Down