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
1 change: 1 addition & 0 deletions src/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub fn DateTime::end_of_month(Self) -> Self
pub fn DateTime::end_of_year(Self) -> Self
pub fn DateTime::epoch() -> Self
pub fn DateTime::format(Self) -> String
pub fn DateTime::format_fixed(Self) -> String
pub fn DateTime::from_unix_micros(Int64) -> Self
pub fn DateTime::from_unix_millis(Int64) -> Self
pub fn DateTime::from_unix_nanos(Int64) -> Self
Expand Down
16 changes: 16 additions & 0 deletions src/tempo.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,22 @@ pub fn DateTime::format(self : DateTime) -> String {
format_datetime_without_zone(self) + "Z"
}

///|
/// Format this DateTime as a UTC timestamp with a fixed-width fractional
/// second; for years 0..9999, the whole output is fixed-width and
/// lexicographically sortable: `YYYY-MM-DDTHH:MM:SS.nnnnnnnnnZ`.
///
/// The fractional-second field is always present with exactly 9 nanosecond
/// digits. For years 0..9999, byte-for-byte string comparison of two
/// `format_fixed` outputs matches chronological order. Outside that year range,
/// expanded or negative year text (especially a leading `-`) does not preserve
/// that lexicographic ordering guarantee.
pub fn DateTime::format_fixed(self : DateTime) -> String {
let d = self.date
let t = self.time
"\{pad4_year(d.year)}-\{pad2(d.month)}-\{pad2(d.day)}T\{pad2(t.hour)}:\{pad2(t.minute)}:\{pad2(t.second)}.\{pad9(t.nanosecond)}Z"
}

///|
fn format_datetime_without_zone(dt : DateTime) -> String {
let d = dt.date
Expand Down
39 changes: 39 additions & 0 deletions src/tempo_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,45 @@ test "DateTime::format nanosecond precision" {
assert_eq(dt.format(), "2024-03-15T12:34:56.123456789Z")
}

///|
test "DateTime::format_fixed whole second keeps nanoseconds" {
let dt = @tempo.DateTime::from_unix_seconds(0L)
assert_eq(dt.format_fixed(), "1970-01-01T00:00:00.000000000Z")
}

///|
test "DateTime::format_fixed pads parsed fractional seconds" {
let dt = @tempo.DateTime::parse("2024-07-21T17:11:00.5Z")
assert_eq(dt.format_fixed(), "2024-07-21T17:11:00.500000000Z")
}

///|
test "DateTime::format_fixed sorts lexicographically for in-range years" {
let t1 = @tempo.DateTime::parse("2024-07-21T17:11:00.499999999Z")
let t2 = @tempo.DateTime::parse("2024-07-21T17:11:00.5Z")
let year_end = @tempo.DateTime::parse("2024-12-31T23:59:59.999999999Z")
let next_year = @tempo.DateTime::parse("2025-01-01T00:00:00.000000000Z")
assert_eq(t1 < t2, true)
assert_eq(t1.format_fixed() < t2.format_fixed(), true)
assert_eq(year_end < next_year, true)
assert_eq(year_end.format_fixed() < next_year.format_fixed(), true)
}

///|
test "DateTime::format_fixed in-range outputs share width" {
let first = @tempo.DateTime::new(
@tempo.Date::new(0, 1, 1),
@tempo.Time::new(0, 0, 0, 0),
).format_fixed()
let middle = @tempo.DateTime::parse("2024-07-21T17:11:00.5Z").format_fixed()
let last = @tempo.DateTime::new(
@tempo.Date::new(9999, 12, 31),
@tempo.Time::new(23, 59, 59, 999_999_999),
).format_fixed()
assert_eq(first.length(), middle.length())
assert_eq(middle.length(), last.length())
}

///|
test "DateTime::format negative year" {
let d = @tempo.Date::new(-500, 6, 15)
Expand Down