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
2 changes: 2 additions & 0 deletions src/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub fn Date::next(Self, Weekday) -> Self
pub fn Date::next_or_same(Self, Weekday) -> Self
pub fn Date::nth_weekday_of_month(Int, Int, Weekday, Int) -> Self raise TempoError
pub fn Date::parse(String) -> Self raise TempoError
pub fn Date::parse_iso_week(String) -> Self raise TempoError
pub fn Date::parse_ordinal(String) -> Self raise TempoError
pub fn Date::previous(Self, Weekday) -> Self
pub fn Date::previous_or_same(Self, Weekday) -> Self
pub fn Date::quarter(Self) -> Int
Expand Down
41 changes: 41 additions & 0 deletions src/tempo.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,47 @@ pub fn Date::parse(s : String) -> Date raise TempoError {
Date::new(year, month, day)
}

///|
/// Parse an ISO 8601 ordinal date in 'YYYY-DDD' format.
///
/// This parser is strict: it accepts only an unsigned 4-digit year, a hyphen,
/// exactly 3 digits for the day-of-year, and end-of-input. Expanded-year output
/// from `Date::format_ordinal` is not accepted.
pub fn Date::parse_ordinal(s : String) -> Date raise TempoError {
Comment thread
brickfrog marked this conversation as resolved.
let v = s.view()
let (year, v) = parse_digits(v, 4)
let v = consume(v, '-')
let (day_of_year, v) = parse_digits(v, 3)
match v {
[] => ()
_ => raise TempoError("unexpected trailing characters")
}
Date::from_ordinal(year, day_of_year)
}

///|
/// Parse an ISO 8601 week date in 'YYYY-Www-D' format.
///
/// The leading year is the ISO week-numbering year, which can differ from the
/// resulting calendar year near New Year. This parser is strict: it accepts
/// only an unsigned 4-digit week-year, a literal `W`, a 2-digit week, a 1-digit
/// weekday, and end-of-input. Expanded-year output from `Date::format_iso_week`
/// is not accepted.
pub fn Date::parse_iso_week(s : String) -> Date raise TempoError {
Comment thread
brickfrog marked this conversation as resolved.
let v = s.view()
let (week_year, v) = parse_digits(v, 4)
let v = consume(v, '-')
let v = consume(v, 'W')
let (week, v) = parse_digits(v, 2)
let v = consume(v, '-')
let (weekday, v) = parse_digits(v, 1)
match v {
[] => ()
_ => raise TempoError("unexpected trailing characters")
}
Date::from_iso_week(week_year, week, weekday)
}

///|
/// Parse a calendar year-month in 'YYYY-MM' format.
pub fn YearMonth::parse(s : String) -> YearMonth raise TempoError {
Expand Down
62 changes: 62 additions & 0 deletions src/tempo_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,39 @@ test "Date::from_iso_week raises on residual Int year overflow" {
assert_true((try? @tempo.Date::from_iso_week(@int.MIN_VALUE, 1, 1)) is Err(_))
}

///|
test "Date::parse_iso_week valid and round-trips" {
assert_eq(
@tempo.Date::parse_iso_week("2020-W53-5"),
@tempo.Date::new(2021, 1, 1),
)
let new_year_monday = @tempo.Date::new(2024, 1, 1)
assert_eq(
@tempo.Date::parse_iso_week(new_year_monday.format_iso_week()),
new_year_monday,
)
let prior_week_year = @tempo.Date::new(2021, 1, 1)
assert_eq(
@tempo.Date::parse_iso_week(prior_week_year.format_iso_week()),
prior_week_year,
)
let week_53_end = @tempo.Date::new(2026, 12, 31)
assert_eq(
@tempo.Date::parse_iso_week(week_53_end.format_iso_week()),
week_53_end,
)
}

///|
test "Date::parse_iso_week rejects malformed and out-of-range input" {
assert_true((try? @tempo.Date::parse_iso_week("2024-W54-1")) is Err(_))
assert_true((try? @tempo.Date::parse_iso_week("2024-01-1")) is Err(_))
assert_true((try? @tempo.Date::parse_iso_week("2024-W1-1")) is Err(_))
assert_true((try? @tempo.Date::parse_iso_week("2024-W01")) is Err(_))
assert_true((try? @tempo.Date::parse_iso_week("2024-W01-8")) is Err(_))
assert_true((try? @tempo.Date::parse_iso_week("2024-W01-1Z")) is Err(_))
}

///|
test "Weekday conversion helpers" {
assert_true(@tempo.Weekday::from_int(1) == Some(@tempo.Monday))
Expand Down Expand Up @@ -1213,6 +1246,35 @@ test "Date::format_ordinal" {
)
}

///|
test "Date::parse_ordinal valid and round-trips" {
assert_eq(
@tempo.Date::parse_ordinal("2024-060"),
@tempo.Date::new(2024, 2, 29),
)
let jan1 = @tempo.Date::new(2024, 1, 1)
assert_eq(@tempo.Date::parse_ordinal(jan1.format_ordinal()), jan1)
let leap_day = @tempo.Date::new(2024, 2, 29)
assert_eq(@tempo.Date::parse_ordinal(leap_day.format_ordinal()), leap_day)
let leap_dec31 = @tempo.Date::new(2024, 12, 31)
assert_eq(@tempo.Date::parse_ordinal(leap_dec31.format_ordinal()), leap_dec31)
let common_dec31 = @tempo.Date::new(2023, 12, 31)
assert_eq(
@tempo.Date::parse_ordinal(common_dec31.format_ordinal()),
common_dec31,
)
}

///|
test "Date::parse_ordinal rejects malformed and out-of-range input" {
assert_true((try? @tempo.Date::parse_ordinal("2023-366")) is Err(_))
assert_true((try? @tempo.Date::parse_ordinal("2024-000")) is Err(_))
assert_true((try? @tempo.Date::parse_ordinal("2024-60")) is Err(_))
assert_true((try? @tempo.Date::parse_ordinal("2024-0060")) is Err(_))
assert_true((try? @tempo.Date::parse_ordinal("2024/060")) is Err(_))
assert_true((try? @tempo.Date::parse_ordinal("2024-060Z")) is Err(_))
}

///|
test "Date::parse roundtrip" {
let s = "2000-02-29"
Expand Down