///|
/// A recursive-descent parser for the RFC 9557 "Internet Extended Date/Time
/// Format" that Temporal accepts.
///
/// Every parse failure is reported as a `RangeError`, which is what the
/// Temporal specification requires of `from` on an invalid string.
///
/// The format is ISO 8601 extended with bracketed annotations: a time zone
/// (`[America/Chicago]`) and arbitrary key-value pairs, of which Temporal
/// understands `u-ca` (the calendar). An annotation may be flagged critical
/// with a leading `!`, which means an implementation must reject it rather
/// than ignore it if it does not understand it.
///
/// Spec:
///|
/// A cursor over the input.
priv struct Scanner {
source : String
mut pos : Int
}
///|
fn Scanner::new(source : String) -> Scanner {
{ source, pos: 0 }
}
///|
/// Returns whether the whole input has been consumed.
fn Scanner::is_done(self : Scanner) -> Bool {
self.pos >= self.source.length()
}
///|
/// Returns the character at the cursor, or `None` at the end of input.
fn Scanner::peek(self : Scanner) -> Char? {
self.peek_at(0)
}
///|
/// Returns the character `offset` positions ahead of the cursor.
fn Scanner::peek_at(self : Scanner, offset : Int) -> Char? {
self.source.get_char(self.pos + offset)
}
///|
/// Consumes `c` if it is next, reporting whether it was.
fn Scanner::eat(self : Scanner, c : Char) -> Bool {
if self.peek() is Some(actual) && actual == c {
self.pos = self.pos + 1
true
} else {
false
}
}
///|
/// Consumes either of two characters, reporting whether one matched.
///
/// Used for the case-insensitive designators (`T`/`t`, `Z`/`z`).
fn Scanner::eat_either(self : Scanner, a : Char, b : Char) -> Bool {
self.eat(a) || self.eat(b)
}
///|
/// Consumes `c`, raising a syntax error if it is not next.
fn Scanner::expect(self : Scanner, c : Char) -> Unit raise TemporalError {
if !self.eat(c) {
raise RangeError("expected '\{c}' at position \{self.pos}")
}
}
///|
/// Returns whether the next character is an ASCII digit.
fn Scanner::peek_is_digit(self : Scanner) -> Bool {
self.peek_at_is_digit(0)
}
///|
/// Returns whether the character `offset` ahead is an ASCII digit.
fn Scanner::peek_at_is_digit(self : Scanner, offset : Int) -> Bool {
self.peek_at(offset) is Some(c) && c >= '0' && c <= '9'
}
///|
/// Consumes exactly `count` digits and returns their value.
fn Scanner::digits(self : Scanner, count : Int) -> Int raise TemporalError {
let mut value = 0
for _ in 0..= '0' && c <= '9' => {
value = value * 10 + (c.to_int() - '0'.to_int())
self.pos = self.pos + 1
}
_ => raise RangeError("expected \{count} digits at position \{self.pos}")
}
}
value
}
///|
/// A parsed calendar date.
///
/// A bare month-day carries the reference year 1972, and a bare year-month
/// carries day 1; in both cases the caller knows which field is meaningful.
priv struct ParsedDate {
year : Int
month : Int
day : Int
}
///|
/// A parsed wall-clock time, with the fraction already scaled to nanoseconds.
priv struct ParsedTime {
hour : Int
minute : Int
second : Int
nanosecond : Int
}
///|
/// A parsed UTC offset: either the `Z` designator or a numeric offset.
priv enum ParsedOffset {
ZDesignator
NumericOffset(UtcOffset)
}
///|
/// Everything an RFC 9557 string can carry.
priv struct ParsedIxdtf {
date : ParsedDate?
time : ParsedTime?
offset : ParsedOffset?
time_zone : TimeZone?
calendar : String?
}
///|
/// Which production to parse the input against.
priv enum ParseVariant {
DateTimeVariant
YearMonthVariant
MonthDayVariant
TimeVariant
}
///|
/// Parses an annotated string against the given production.
fn parse_ixdtf(
source : String,
variant : ParseVariant,
) -> ParsedIxdtf raise TemporalError {
let scanner = Scanner::new(source)
let (date, time) = match variant {
TimeVariant => (None, Some(parse_time_spec_with_designator(scanner)))
YearMonthVariant => (Some(parse_year_month_date(scanner)), None)
MonthDayVariant => {
let date = parse_month_day_date(scanner)
(Some(date), parse_optional_time(scanner))
}
DateTimeVariant => {
let date = parse_full_date(scanner)
(Some(date), parse_optional_time(scanner))
}
}
let offset = parse_optional_offset(scanner)
let (time_zone, calendar) = parse_annotations(scanner)
if !scanner.is_done() {
raise RangeError("unexpected trailing input at position \{scanner.pos}")
}
// Year-month and month-day forms carry no year or day of their own, so a
// non-ISO calendar could not be applied to them meaningfully.
if variant is (YearMonthVariant | MonthDayVariant) &&
calendar is Some(name) &&
ascii_lowercase(name) != "iso8601" {
raise RangeError(
"the year-month and month-day formats are only defined for the ISO calendar",
)
}
// Temporal validates dates at parse time, before any calendar conversion.
if date is Some(d) {
let year = if variant is MonthDayVariant { 1972 } else { d.year }
let day = if variant is YearMonthVariant { 1 } else { d.day }
if !is_valid_iso_date(year, d.month, day) {
raise RangeError("the string does not contain a valid ISO date")
}
}
{ date, time, offset, time_zone, calendar }
}
///|
/// Parses `YYYY-MM-DD`, with either all separators or none.
fn parse_full_date(scanner : Scanner) -> ParsedDate raise TemporalError {
let year = parse_year(scanner)
let extended = scanner.eat('-')
let month = scanner.digits(2)
if extended {
scanner.expect('-')
} else if scanner.eat('-') {
raise RangeError("date separators must be used consistently")
}
let day = scanner.digits(2)
{ year, month, day }
}
///|
/// Parses `YYYY-MM`, the year-month production.
fn parse_year_month_date(scanner : Scanner) -> ParsedDate raise TemporalError {
let year = parse_year(scanner)
scanner.eat('-') |> ignore
let month = scanner.digits(2)
{ year, month, day: 1 }
}
///|
/// Parses `--MM-DD` or `MM-DD`, the month-day production.
fn parse_month_day_date(scanner : Scanner) -> ParsedDate raise TemporalError {
if scanner.eat('-') {
scanner.expect('-')
}
let month = scanner.digits(2)
scanner.eat('-') |> ignore
let day = scanner.digits(2)
// 1972 is the reference year the specification uses: a leap year, so that
// February 29 round-trips.
{ year: 1972, month, day }
}
///|
/// Parses a four-digit year, or a signed six-digit extended year.
fn parse_year(scanner : Scanner) -> Int raise TemporalError {
if scanner.peek() is Some('+') || scanner.peek() is Some('-') {
let negative = scanner.peek() is Some('-')
scanner.pos = scanner.pos + 1
let value = scanner.digits(6)
if negative && value == 0 {
// `-000000` would name a year zero with a negative sign, which the
// grammar forbids because it is not a distinct year.
raise RangeError("the extended year -000000 is not valid")
}
if negative {
-value
} else {
value
}
} else {
scanner.digits(4)
}
}
///|
/// Parses a time part introduced by a date-time separator, if one is present.
fn parse_optional_time(scanner : Scanner) -> ParsedTime? raise TemporalError {
if scanner.eat_either('T', 't') || scanner.eat(' ') {
Some(parse_time_spec(scanner))
} else {
None
}
}
///|
/// Parses a time part, allowing but not requiring a leading `T`.
fn parse_time_spec_with_designator(
scanner : Scanner,
) -> ParsedTime raise TemporalError {
scanner.eat_either('T', 't') |> ignore
parse_time_spec(scanner)
}
///|
/// Parses `HH`, `HH:MM` or `HH:MM:SS`, with an optional fraction.
fn parse_time_spec(scanner : Scanner) -> ParsedTime raise TemporalError {
let hour = scanner.digits(2)
if hour > 23 {
raise RangeError("hour must be in the range 0 to 23")
}
let extended = scanner.peek() is Some(':')
if !extended && !scanner.peek_is_digit() {
return { hour, minute: 0, second: 0, nanosecond: 0 }
}
if extended {
scanner.expect(':')
}
let minute = scanner.digits(2)
if minute > 59 {
raise RangeError("minute must be in the range 0 to 59")
}
let has_seconds = if extended {
scanner.eat(':')
} else {
scanner.peek_is_digit()
}
if !has_seconds {
return { hour, minute, second: 0, nanosecond: 0 }
}
let second = scanner.digits(2)
if second > 60 {
raise RangeError("second must be in the range 0 to 60")
}
// A leap second is accepted on input and clamped, since Temporal's model has
// no leap seconds.
let second = @cmp.minimum(second, 59)
let nanosecond = parse_optional_fraction(scanner)
{ hour, minute, second, nanosecond }
}
///|
/// Parses `.` or `,` followed by 1 to 9 digits, scaled to nanoseconds.
fn parse_optional_fraction(scanner : Scanner) -> Int raise TemporalError {
if !(scanner.peek() is Some('.') || scanner.peek() is Some(',')) {
return 0
}
scanner.pos = scanner.pos + 1
if !scanner.peek_is_digit() {
raise RangeError("a fraction must have at least one digit")
}
let mut value = 0
let mut digits = 0
while scanner.peek_is_digit() {
if digits >= 9 {
raise RangeError("fractional time cannot exceed nine digits")
}
value = value * 10 + (scanner.peek().unwrap().to_int() - '0'.to_int())
digits = digits + 1
scanner.pos = scanner.pos + 1
}
// Scale to nanoseconds: one digit means tenths of a second.
for _ in digits..<9 {
value = value * 10
}
value
}
///|
/// Parses `Z`, `z`, or a signed offset, if one is present.
fn parse_optional_offset(
scanner : Scanner,
) -> ParsedOffset? raise TemporalError {
if scanner.eat_either('Z', 'z') {
return Some(ZDesignator)
}
if !(scanner.peek() is Some('+') || scanner.peek() is Some('-')) {
return None
}
Some(NumericOffset(parse_offset_value(scanner)))
}
///|
/// Parses `±HH`, `±HH:MM`, `±HH:MM:SS` with an optional fraction.
fn parse_offset_value(scanner : Scanner) -> UtcOffset raise TemporalError {
let sign = if scanner.eat('-') {
-1L
} else {
scanner.expect('+')
1L
}
let hour = scanner.digits(2)
if hour > 23 {
raise RangeError("offset hour must be in the range 0 to 23")
}
let extended = scanner.peek() is Some(':')
let mut nanoseconds = hour.to_int64() * NS_PER_HOUR
if !extended && !scanner.peek_at_is_digit(0) {
return UtcOffset(sign * nanoseconds)
}
if extended {
scanner.expect(':')
}
let minute = scanner.digits(2)
if minute > 59 {
raise RangeError("offset minute must be in the range 0 to 59")
}
nanoseconds = nanoseconds + minute.to_int64() * NS_PER_MINUTE
let has_seconds = if extended {
scanner.eat(':')
} else {
scanner.peek_is_digit()
}
if !has_seconds {
return UtcOffset(sign * nanoseconds)
}
let second = scanner.digits(2)
if second > 59 {
raise RangeError("offset second must be in the range 0 to 59")
}
nanoseconds = nanoseconds + second.to_int64() * NS_PER_SECOND
nanoseconds = nanoseconds + parse_optional_fraction(scanner).to_int64()
UtcOffset(sign * nanoseconds)
}
///|
/// Parses the bracketed annotations that may follow a date-time.
///
/// Returns the time zone annotation, which must come first if present, and the
/// calendar named by a `u-ca` annotation.
fn parse_annotations(
scanner : Scanner,
) -> (TimeZone?, String?) raise TemporalError {
let mut time_zone = None
let mut calendar = None
let mut calendar_critical = false
let mut first = true
while scanner.peek() is Some('[') {
scanner.pos = scanner.pos + 1
let critical = scanner.eat('!')
let body = read_until_close_bracket(scanner)
match split_annotation(body) {
Some((key, value)) =>
if key == "u-ca" {
// A repeated calendar is only an error when either copy is marked
// critical; otherwise later ones are ignored.
if calendar is Some(_) {
if calendar_critical || critical {
raise RangeError("duplicate critical calendar annotation")
}
} else {
calendar = Some(value)
calendar_critical = critical
}
} else if critical {
raise RangeError("unrecognized critical annotation '\{key}'")
}
None => {
// No `=`, so this is the time zone annotation, which is only valid in
// the first position.
if !first || time_zone is Some(_) {
raise RangeError(
"the time zone annotation must come before any other annotation",
)
}
time_zone = Some(parse_time_zone_identifier(body))
}
}
first = false
}
(time_zone, calendar)
}
///|
/// Reads annotation text up to the closing bracket.
fn read_until_close_bracket(scanner : Scanner) -> String raise TemporalError {
let buf = StringBuilder::new()
while scanner.peek() is Some(c) {
if c == ']' {
scanner.pos = scanner.pos + 1
return buf.to_string()
}
buf.write_char(c)
scanner.pos = scanner.pos + 1
}
raise RangeError("unterminated annotation")
}
///|
/// Splits `key=value`, returning `None` when there is no `=`.
fn split_annotation(body : String) -> (String, String)? {
for i in 0.. TimeZone raise TemporalError {
if identifier == "" {
raise RangeError("the time zone annotation is empty")
}
let first = identifier.get_char(0).unwrap()
if first == '+' || first == '-' {
let scanner = Scanner::new(identifier)
let offset = parse_offset_value(scanner)
if !scanner.is_done() {
raise RangeError("trailing input in time zone offset")
}
// An offset used as a time zone identifier names a fixed zone, and zones
// are compared by whole minutes.
if offset.is_sub_minute() {
raise RangeError("a time zone offset must have minute precision")
}
return OffsetZone(offset)
}
if !is_valid_iana_identifier(identifier) {
raise RangeError("'\{identifier}' is not a valid time zone identifier")
}
IanaZone(identifier)
}
///|
/// Returns whether the string has the shape of an IANA time zone name.
fn is_valid_iana_identifier(identifier : String) -> Bool {
let mut component_length = 0
for i in 0.. Bool {
is_ascii_letter(c) ||
(c >= '0' && c <= '9') ||
c == '.' ||
c == '-' ||
c == '_' ||
c == '+'
}
///|
fn is_ascii_letter(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// ==== variant entry points ====
///|
/// Parses a date-time string, rejecting the `Z` designator.
///
/// `Z` asserts that the string names an exact instant, which a calendar date
/// or wall-clock time is not.
fn parse_date_time_string(source : String) -> ParsedIxdtf raise TemporalError {
let record = parse_ixdtf(source, DateTimeVariant)
reject_z_designator(record)
}
///|
fn reject_z_designator(record : ParsedIxdtf) -> ParsedIxdtf raise TemporalError {
if record.offset is Some(ZDesignator) {
raise RangeError(
"the UTC designator 'Z' is not valid when parsing a plain date or time",
)
}
record
}
///|
/// Parses a year-month string, falling back to the full date-time production.
///
/// `2020-01` is a year-month, but so is the year-month part of
/// `2020-01-01T00:00`, and the specification accepts both.
fn parse_year_month_string(source : String) -> ParsedIxdtf raise TemporalError {
reject_z_designator(parse_ixdtf(source, YearMonthVariant)) catch {
err =>
parse_date_time_string(source) catch {
// Report the year-month failure, which describes the intent better.
_ => raise err
}
}
}
///|
/// Parses a month-day string, falling back to the full date-time production.
fn parse_month_day_string(source : String) -> ParsedIxdtf raise TemporalError {
reject_z_designator(parse_ixdtf(source, MonthDayVariant)) catch {
err => parse_date_time_string(source) catch { _ => raise err }
}
}
///|
/// Parses a time string.
///
/// A bare time that could equally be read as a month-day or year-month is
/// ambiguous and rejected; prefixing it with `T` resolves the ambiguity.
fn parse_time_string(source : String) -> ParsedTime raise TemporalError {
let record = reject_z_designator(parse_ixdtf(source, TimeVariant)) catch {
err => parse_date_time_string(source) catch { _ => raise err }
}
if record.date is None && !starts_with_time_designator(source) {
let ambiguous = parses_as(source, MonthDayVariant) ||
parses_as(source, YearMonthVariant)
if ambiguous {
raise RangeError(
"'\{source}' is ambiguous: it reads as both a time and a date, so prefix it with 'T'",
)
}
}
match record.time {
Some(time) => time
None =>
raise RangeError(
"a PlainTime can only be parsed from a string with a time",
)
}
}
///|
/// Returns whether the input parses against the given production.
fn parses_as(source : String, variant : ParseVariant) -> Bool {
try {
parse_ixdtf(source, variant) |> ignore
true
} catch {
_ => false
}
}
///|
fn starts_with_time_designator(source : String) -> Bool {
source.get_char(0) is Some('T') || source.get_char(0) is Some('t')
}
///|
/// Parses an instant string, which must carry both a time and an offset.
fn parse_instant_string(
source : String,
) -> (ParsedDate, ParsedTime, ParsedOffset) raise TemporalError {
let record = parse_ixdtf(source, DateTimeVariant)
match (record.date, record.time, record.offset) {
(Some(date), Some(time), Some(offset)) => (date, time, offset)
_ =>
raise RangeError(
"an Instant string requires a date, a time and a UTC offset",
)
}
}
///|
/// Parses a zoned date-time string, which must carry a time zone annotation.
fn parse_zoned_date_time_string(
source : String,
) -> ParsedIxdtf raise TemporalError {
let record = parse_ixdtf(source, DateTimeVariant)
if record.time_zone is None {
raise RangeError(
"a ZonedDateTime string requires a time zone annotation, such as [UTC]",
)
}
record
}
///|
/// Resolves the calendar named by a parsed string.
fn calendar_of(record : ParsedIxdtf) -> Calendar raise TemporalError {
match record.calendar {
None => Calendar::ISO
Some(name) => Calendar::of_string(name)
}
}
///|
/// Returns the parsed time, or midnight when the string carried none.
fn time_or_midnight(record : ParsedIxdtf) -> IsoTime raise TemporalError {
match record.time {
None => iso_time_midnight
Some(time) => iso_time_of_parsed(time)
}
}
///|
/// Converts a parsed time into an `IsoTime`, splitting the nanosecond count.
fn iso_time_of_parsed(time : ParsedTime) -> IsoTime raise TemporalError {
IsoTime::new_with_overflow(
time.hour,
time.minute,
time.second,
time.nanosecond / 1_000_000,
time.nanosecond / 1_000 % 1_000,
time.nanosecond % 1_000,
Reject,
)
}
// ==== public parsing API ====
///|
/// Parses a `PlainDate` from an RFC 9557 string.
///
/// ```mbt check
/// test {
/// inspect(@temporal.PlainDate::of_string("2025-03-01"), content="2025-03-01")
/// inspect(
/// @temporal.PlainDate::of_string("2025-03-01T11:16:10[u-ca=iso8601]"),
/// content="2025-03-01",
/// )
/// }
/// ```
pub fn PlainDate::of_string(source : String) -> PlainDate raise TemporalError {
let record = parse_date_time_string(source)
let date = temporal_unwrap(record.date, "date component")
PlainDate::new_with_overflow(
date.year,
date.month,
date.day,
Reject,
calendar_of(record),
)
}
///|
/// Parses a `PlainTime` from an RFC 9557 string.
///
/// ```mbt check
/// test {
/// inspect(@temporal.PlainTime::of_string("12:30:45.5"), content="12:30:45.5")
/// }
/// ```
pub fn PlainTime::of_string(source : String) -> PlainTime raise TemporalError {
PlainTime::from_iso(iso_time_of_parsed(parse_time_string(source)))
}
///|
/// Parses a `PlainDateTime` from an RFC 9557 string.
///
/// ```mbt check
/// test {
/// inspect(
/// @temporal.PlainDateTime::of_string("2025-03-01T11:16:10"),
/// content="2025-03-01T11:16:10",
/// )
/// }
/// ```
pub fn PlainDateTime::of_string(
source : String,
) -> PlainDateTime raise TemporalError {
let record = parse_date_time_string(source)
let date = temporal_unwrap(record.date, "date component")
let iso_date = IsoDate::new_with_overflow(
date.year,
date.month,
date.day,
Reject,
)
PlainDateTime::from_iso(
IsoDateTime::new(iso_date, time_or_midnight(record)),
calendar_of(record),
)
}
///|
/// Parses a `Duration` from an ISO 8601 duration string.
///
/// ```mbt check
/// test {
/// let d = @temporal.Duration::of_string("P1Y2M3DT4H5M6.789S")
/// inspect(d, content="P1Y2M3DT4H5M6.789S")
/// inspect(@temporal.Duration::of_string("-P1D"), content="-P1D")
/// }
/// ```
pub fn Duration::of_string(source : String) -> Duration raise TemporalError {
let scanner = Scanner::new(source)
let sign = if scanner.eat('-') || scanner.eat('\u{2212}') {
-1L
} else {
scanner.eat('+') |> ignore
1L
}
if !scanner.eat_either('P', 'p') {
raise RangeError("a duration must start with the designator 'P'")
}
let mut years = 0L
let mut months = 0L
let mut weeks = 0L
let mut days = 0L
let mut saw_component = false
// The date designators must appear in descending order, each at most once.
for designator in [('Y', 0), ('M', 1), ('W', 2), ('D', 3)] {
let (letter, slot) = designator
if !scanner.peek_is_digit() {
break
}
let start = scanner.pos
let value = parse_duration_number(scanner)
if scanner.eat_either(letter, to_ascii_lower(letter)) {
saw_component = true
match slot {
0 => years = value
1 => months = value
2 => weeks = value
_ => days = value
}
} else {
// This number belongs to a later designator; rewind and let the next
// iteration claim it.
scanner.pos = start
}
}
let mut hours = 0L
let mut minutes = 0L
let mut seconds = 0L
let mut nanoseconds = 0L
if scanner.eat_either('T', 't') {
let (h, mi, s, ns) = parse_duration_time(scanner)
hours = h
minutes = mi
seconds = s
nanoseconds = ns
saw_component = true
}
if !saw_component {
raise RangeError("a duration must have at least one component")
}
if !scanner.is_done() {
raise RangeError("unexpected trailing input at position \{scanner.pos}")
}
Duration::new(
years * sign,
months * sign,
weeks * sign,
days * sign,
hours * sign,
minutes * sign,
seconds * sign,
nanoseconds / 1_000_000L * sign,
@int128.of_int64(nanoseconds / 1_000L % 1_000L * sign),
@int128.of_int64(nanoseconds % 1_000L * sign),
)
}
///|
/// Parses the time part of a duration, `T` already consumed.
///
/// A fraction may appear only on the last component present, and it carries
/// down into the smaller units.
fn parse_duration_time(
scanner : Scanner,
) -> (Int64, Int64, Int64, Int64) raise TemporalError {
if !scanner.peek_is_digit() {
raise RangeError("the time part of a duration must have a component")
}
let mut hours = 0L
let mut minutes = 0L
let mut seconds = 0L
let mut nanoseconds = 0L
let value = parse_duration_number(scanner)
let fraction = parse_optional_fraction(scanner).to_int64()
if scanner.eat_either('H', 'h') {
hours = value
if fraction != 0L {
// A fraction of an hour spills into minutes, seconds and below.
let total = fraction * 3600L
minutes = total / NS_PER_MINUTE
let rest = total % NS_PER_MINUTE
seconds = rest / NS_PER_SECOND
nanoseconds = rest % NS_PER_SECOND
return (hours, minutes, seconds, nanoseconds)
}
if !scanner.peek_is_digit() {
return (hours, 0L, 0L, 0L)
}
let value = parse_duration_number(scanner)
let fraction = parse_optional_fraction(scanner).to_int64()
if scanner.eat_either('M', 'm') {
minutes = value
if fraction != 0L {
let total = fraction * 60L
seconds = total / NS_PER_SECOND
nanoseconds = total % NS_PER_SECOND
return (hours, minutes, seconds, nanoseconds)
}
if !scanner.peek_is_digit() {
return (hours, minutes, 0L, 0L)
}
let value = parse_duration_number(scanner)
let fraction = parse_optional_fraction(scanner).to_int64()
if !scanner.eat_either('S', 's') {
raise RangeError("expected the seconds designator 'S'")
}
return (hours, minutes, value, fraction)
}
if !scanner.eat_either('S', 's') {
raise RangeError("expected a minutes or seconds designator")
}
return (hours, 0L, value, fraction)
}
if scanner.eat_either('M', 'm') {
minutes = value
if fraction != 0L {
let total = fraction * 60L
seconds = total / NS_PER_SECOND
nanoseconds = total % NS_PER_SECOND
return (0L, minutes, seconds, nanoseconds)
}
if !scanner.peek_is_digit() {
return (0L, minutes, 0L, 0L)
}
let value = parse_duration_number(scanner)
let fraction = parse_optional_fraction(scanner).to_int64()
if !scanner.eat_either('S', 's') {
raise RangeError("expected the seconds designator 'S'")
}
return (0L, minutes, value, fraction)
}
if !scanner.eat_either('S', 's') {
raise RangeError("expected a duration time designator")
}
(0L, 0L, value, fraction)
}
///|
/// Parses a run of digits as a duration component value.
fn parse_duration_number(scanner : Scanner) -> Int64 raise TemporalError {
if !scanner.peek_is_digit() {
raise RangeError("expected a number at position \{scanner.pos}")
}
let mut value = 0L
while scanner.peek_is_digit() {
let digit = (scanner.peek().unwrap().to_int() - '0'.to_int()).to_int64()
// Anything this large is already outside the valid duration range; stop
// here rather than wrapping.
if value > (9_223_372_036_854_775_807L - digit) / 10L {
raise RangeError("duration component exceeds its valid range")
}
value = value * 10L + digit
scanner.pos = scanner.pos + 1
}
value
}
///|
fn to_ascii_lower(c : Char) -> Char {
if c >= 'A' && c <= 'Z' {
Int::unsafe_to_char(c.to_int() + 32)
} else {
c
}
}