// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// The `%` directive language, the union of C23, POSIX, glibc, Python, Rust chrono
// and C++20 `std::format`. Both directions are here: [`Moment::strftime`] writes a
// moment the way each directive says, and [`strptime`] reads it back.
//
// The modifiers glibc and chrono share — `-` no padding, `_` space padding, `0`
// zero padding, `^` upper case, `#` swap case — work on the directives they make
// sense on, and an optional width sits between the flags and the letter. The two
// C99 locale selectors `E` and `O` are kept for compatibility and behave like the
// plain directive, because this package carries no locale.
///|
/// A moment written by a format string of `%` directives.
///
/// The directives are the ones every Unix knows plus the ones chrono and C++ add;
/// the table below is the whole set. Literal text between directives is copied
/// through untouched, and `%%` is a single per cent sign.
///
/// | | |
/// |:--:|:--|
/// | `%Y` `%C` `%y` | year of four digits, century, two-digit year |
/// | `%G` `%g` | ISO week year, two-digit |
/// | `%m` `%B` `%b` `%h` | month, long and short name |
/// | `%d` `%e` | day of month, zero- and space-padded |
/// | `%j` | day of year, `001` to `366` |
/// | `%U` `%W` `%V` | week of year, Sunday-, Monday-, and ISO-based |
/// | `%u` `%w` | weekday, `1` to `7` and `0` to `6` |
/// | `%a` `%A` | short and long weekday name |
/// | `%q` | quarter of the year, `1` to `4 (chrono) |
/// | `%H` `%k` `%I` `%l` | hour, `00` and ` 0`, twelve-hour `01` and ` 1` |
/// | `%M` `%S` | minute, second |
/// | `%p` `%P` | AM/PM and am/pm |
/// | `%f` `%.f` `%.Nf` `%Nf` `%N` | fractional second, dot or not, N digits or auto |
/// | `%s` | seconds since the epoch |
/// | `%z` `%:z` `%::z` `%:::z` `%#z` `%Ez` `%Oz` | offset, with and without colons |
/// | `%Z` | `UTC`, `UTC±HH:MM` for a fixed offset, or nothing when there is no zone |
/// | `%c` `%x` `%X` `%D` `%F` `%T` `%R` `%r` `%v` `%+` | the composite forms |
/// | `%n` `%t` `%%` | a newline, a tab, a per cent sign |
///
/// A moment with no date writes its date directives as 1 January 1900, as Python's
/// `time.strftime` and C's `struct tm` do, so a bare time still formats.
pub fn Moment::strftime(self : Moment, format : StringView) -> String {
let (d, t, z, epoch_secs) = decompose(self)
let f = expand(format.to_owned())
let out = StringBuilder()
let mut i = 0
while i < f.length() {
guard f[i].to_int() == 0x25 && directive(f, i) is Some((spec, next)) else {
out.write_char(f[i].unsafe_to_char())
i = i + 1
continue
}
let start = i
i = next
out.write_string(
write_spec(d, t, z, epoch_secs, spec, f[start:next].to_owned()),
)
}
out.to_string()
}
///|
/// The broken-down pieces a directive needs, and the epoch whether or not the
/// moment names one (a wall clock is read as if it sat at UTC, which is the only
/// deterministic choice a clock-less library can make).
fn decompose(m : Moment) -> (Date, Time, Zone?, Int64) {
match m {
Zoned(date~, time~, zone~) => (date, time, Some(zone), unwrap(m.epoch()))
Plain(date~, time~) => (date, time, None, wall_epoch(date, time))
Day(date~) => (date, midnight, None, wall_epoch(date, midnight))
Clock(time~) =>
(
{ year: 1900, month: 1, day: 1, },
time,
None,
wall_epoch({ year: 1900, month: 1, day: 1, }, time),
)
}
}
///|
fn unwrap(x : Int64?) -> Int64 {
match x {
Some(v) => v
None => 0L
}
}
///|
fn wall_epoch(d : Date, t : Time) -> Int64 {
d.days().to_int64() * 86400L + t.nanos() / 1_000_000_000L
}
///|
/// One directive's parts: the conversion letter, the flags, the width, and the
/// count of colons on a `%z` form.
priv struct Spec {
conv : Char
minus : Bool
under : Bool
zero : Bool
upper : Bool
swap : Bool
dot : Bool
alt : Bool
width : Int32
colon : Int32
}
///|
/// The directive that starts at the `%` at `i`, and the index after it; `None` when
/// the format ends before a conversion letter, which is then written as it stands.
fn directive(f : String, i : Int32) -> (Spec, Int32)? {
let mut k = i + 1
let mut minus = false
let mut under = false
let mut zero = false
let mut upper = false
let mut swap = false
let mut dot = false
let mut alt = false
while k < f.length() {
match f[k].to_int() {
0x2d => {
minus = true
k = k + 1
} // -
0x5f => {
under = true
k = k + 1
} // _
0x30 => {
zero = true
k = k + 1
} // 0
0x5e => {
upper = true
k = k + 1
} // ^
0x23 => {
swap = true
k = k + 1
} // #
0x2e => {
dot = true
k = k + 1
} // .
0x45 | 0x4f => {
alt = true
k = k + 1
} // E or O
_ => break
}
}
let start = k
let mut width = 0
while k < f.length() && is_digit(f[k].to_int()) {
width = width * 10 + (f[k].to_int() - 48)
k = k + 1
}
let width = if k == start { -1 } else { width }
let mut colon = 0
while k < f.length() && f[k].to_int() == 0x3a {
colon = colon + 1
k = k + 1
}
guard k < f.length() else { return None }
Some(
(
{
conv: f[k].unsafe_to_char(),
minus,
under,
zero,
upper,
swap,
dot,
alt,
width,
colon,
},
k + 1,
),
)
}
///|
/// `%F` and `%T` are shorthands, as are the composite forms the C locale and chrono
/// name, so they are spelt out once before either direction reads the format;
/// `%%F` stays a per cent sign and an F.
fn expand(f : String) -> String {
let out = StringBuilder()
let mut i = 0
while i < f.length() {
if f[i].to_int() == 0x25 && i + 1 < f.length() {
match f[i + 1].unsafe_to_char() {
'F' => out.write_string("%Y-%m-%d")
'T' => out.write_string("%H:%M:%S")
'D' => out.write_string("%m/%d/%y")
'R' => out.write_string("%H:%M")
'r' => out.write_string("%I:%M:%S %p")
'v' => out.write_string("%e-%b-%Y")
'c' => out.write_string("%a %b %e %H:%M:%S %Y")
'x' => out.write_string("%m/%d/%y")
'X' => out.write_string("%H:%M:%S")
'+' => out.write_string("%Y-%m-%dT%H:%M:%S%.f%:z")
c => {
out.write_char('%')
out.write_char(c)
}
}
i = i + 2
} else {
out.write_char(f[i].unsafe_to_char())
i = i + 1
}
}
out.to_string()
}
///|
/// A number written with the padding and case the directive asked for.
fn render_num(
spec : Spec,
value : Int64,
default_width : Int32,
default_pad : Char,
) -> String {
let w = if spec.width > 0 { spec.width } else { default_width }
let pad = if spec.minus {
if spec.width > 0 {
' '
} else {
'\u{0}'
}
} else if spec.under {
' '
} else if spec.zero {
'0'
} else {
default_pad
}
let s = value.to_string()
if pad != '\u{0}' {
let mut out = s
while out.length() < w {
out = pad.to_string() + out
}
out
} else {
s
}
}
///|
fn render_text(spec : Spec, s : String) -> String {
if spec.upper {
s.to_upper()
} else if spec.swap {
swap_case(s)
} else {
s
}
}
///|
fn swap_case(s : String) -> String {
let out = StringBuilder()
for i = 0; i < s.length(); i = i + 1 {
let c = s[i].unsafe_to_char()
let u = c.to_int()
if u >= 0x41 && u <= 0x5a {
out.write_char((u + 32).unsafe_to_char())
} else if u >= 0x61 && u <= 0x7a {
out.write_char((u - 32).unsafe_to_char())
} else {
out.write_char(c)
}
}
out.to_string()
}
///|
/// The fractional second as a directive asks for it: Python's six digits for a bare
/// `%f`; exactly N digits, truncated, for `%Nf`, with a leading dot for `%.Nf`; and
/// for `%.f` as many as the value needs, three, six or nine, or nothing at all on a
/// whole second, as chrono writes it. `%N` is GNU date's nine-digit nanoseconds.
fn fraction(nano : Int32, spec : Spec) -> String {
if spec.conv == 'N' {
let n = if spec.width > 0 && spec.width < 9 { spec.width } else { 9 }
return pad_nano(nano, n, false)
}
if spec.width > 0 {
let n = if spec.width > 9 { 9 } else { spec.width }
return pad_nano(nano, n, spec.dot)
}
if !spec.dot {
return pad_nano(nano, 6, false)
}
if nano == 0 {
""
} else if nano % 1_000_000 == 0 {
"." + pad_nano(nano, 3, false)
} else if nano % 1000 == 0 {
"." + pad_nano(nano, 6, false)
} else {
"." + pad_nano(nano, 9, false)
}
}
///|
fn pad_nano(nano : Int32, n : Int32, dot : Bool) -> String {
let scale = power10(9 - n)
let digits = pad(nano / scale, n)
if dot {
"." + digits
} else {
digits
}
}
///|
fn power10(n : Int32) -> Int32 {
let mut p = 1
for i = 0; i < n; i = i + 1 {
p = p * 10
}
p
}
///|
/// The offset text for a `%z` form, with the colon count the directive carried.
fn offset_text(minutes : Int32, spec : Spec) -> String {
let sign = if minutes < 0 { "-" } else { "+" }
let a = if minutes < 0 { -minutes } else { minutes }
let hh = a / 60
let mm = a % 60
let ss = 0
if spec.colon == 0 {
sign + pad(hh, 2) + pad(mm, 2)
} else if spec.colon == 1 {
sign + pad(hh, 2) + ":" + pad(mm, 2)
} else if spec.colon == 2 {
sign + pad(hh, 2) + ":" + pad(mm, 2) + ":" + pad(ss, 2)
} else {
// chrono's `%:::z`: the offset written only as whole hours.
sign + pad(hh, 2)
}
}
///|
/// One directive turned into text for the moment it describes. `raw` is the exact
/// text the directive occupied; an unknown directive is written back as it stood.
fn write_spec(
d : Date,
t : Time,
z : Zone?,
epoch_secs : Int64,
spec : Spec,
raw : String,
) -> String {
let (iso_year, iso_week, iso_day) = d.isocalendar()
match spec.conv {
'Y' => year_text(d.year, spec)
'C' => render_num(spec, (d.year / 100).to_int64(), 2, '0')
'y' => render_num(spec, (d.year % 100).to_int64(), 2, '0')
'G' => render_num(spec, iso_year.to_int64(), 4, '0')
'g' => render_num(spec, (iso_year % 100).to_int64(), 2, '0')
'm' => render_num(spec, d.month.to_int64(), 2, '0')
'B' => render_text(spec, long_months[d.month - 1])
'b' => render_text(spec, months[d.month - 1])
'h' => render_text(spec, months[d.month - 1])
'd' => render_num(spec, d.day.to_int64(), 2, '0')
'e' => render_num(spec, d.day.to_int64(), 2, ' ')
'j' => render_num(spec, d.yearday().to_int64(), 3, '0')
'U' => render_num(spec, week_sun(d).to_int64(), 2, '0')
'W' => render_num(spec, week_mon(d).to_int64(), 2, '0')
'V' => render_num(spec, iso_week.to_int64(), 2, '0')
'u' => render_num(spec, iso_day.to_int64(), 1, '0')
'w' => render_num(spec, d.weekday().to_int64(), 1, '0')
'a' => render_text(spec, weekdays[d.weekday()])
'A' => render_text(spec, long_days[d.weekday()])
'q' => render_num(spec, ((d.month - 1) / 3 + 1).to_int64(), 1, '0')
'H' => render_num(spec, t.hour.to_int64(), 2, '0')
'k' => render_num(spec, t.hour.to_int64(), 2, ' ')
'I' => render_num(spec, hour12(t).to_int64(), 2, '0')
'l' => render_num(spec, hour12(t).to_int64(), 2, ' ')
'M' => render_num(spec, t.minute.to_int64(), 2, '0')
'S' => render_num(spec, t.second.to_int64(), 2, '0')
'p' => render_text(spec, if t.hour < 12 { "AM" } else { "PM" })
'P' => render_text(spec, if t.hour < 12 { "am" } else { "pm" })
'f' => fraction(t.nano, spec)
'N' => fraction(t.nano, spec)
's' => epoch_secs.to_string()
'z' => zone_offset(z, spec)
'Z' =>
match z {
Some(Utc) => "UTC"
Some(Offset(m)) =>
if m == 0 {
"UTC"
} else {
let sign = if m < 0 { "-" } else { "+" }
let a = if m < 0 { -m } else { m }
"UTC" + sign + pad(a / 60, 2) + ":" + pad(a % 60, 2)
}
None => ""
}
'n' => "\n"
't' => "\t"
'%' => "%"
_ => raw
}
}
///|
fn year_text(year : Int32, spec : Spec) -> String {
if year < 0 {
"-" + render_num(spec, (-year).to_int64(), 4, '0')
} else {
render_num(spec, year.to_int64(), 4, '0')
}
}
///|
fn hour12(t : Time) -> Int32 {
if t.hour % 12 == 0 {
12
} else {
t.hour % 12
}
}
///|
fn zone_offset(z : Zone?, spec : Spec) -> String {
match z {
None => ""
Some(Utc) =>
if spec.conv == 'Z' {
"UTC"
} else if spec.colon == 0 {
"+0000"
} else if spec.colon == 1 {
"+00:00"
} else if spec.colon == 2 {
"+00:00:00"
} else {
"+00"
}
Some(Offset(m)) =>
if spec.conv == 'Z' {
"UTC"
} else if m == 0 && spec.alt {
// chrono's `%#z` writes `Z` for UTC.
"Z"
} else {
offset_text(m, spec)
}
}
}
///|
/// The week number with Sunday as the first day, as `%U` writes it.
fn week_sun(d : Date) -> Int32 {
(d.yearday() - 1 + 7 - d.weekday()) / 7
}
///|
/// The week number with Monday as the first day, as `%W` writes it.
fn week_mon(d : Date) -> Int32 {
(d.yearday() - 1 + 7 - (d.weekday() + 6) % 7) / 7
}
///|
/// A moment read by a format string of `%` directives.
///
/// The directives are the ones [`Moment::strftime`] writes. Literal characters in
/// the format must match the text exactly, except that whitespace in the format
/// matches any run of whitespace, which is what C's `strptime` does and what makes
/// a format survive a double space.
///
/// The result is `Zoned` when the text carried an offset or a raw `%s`, `Plain`
/// when it carried a date and a time, `Day` when only a date, and `Clock` when only
/// a time — the same four shapes [`parse`] answers with.
pub fn strptime(src : StringView, format : StringView) -> Moment raise Refused {
let s = src.to_owned()
let f = expand(format.to_owned())
let mut year = 1900
let mut month = 1
let mut mday = 1
let mut hour = 0
let mut minute = 0
let mut sec = 0
let mut nano = 0
let mut offset : Int32? = None
let mut pm : Bool? = None
let mut saw_date = false
let mut saw_time = false
let mut century : Int32? = None
let mut iso_year : Int32? = None
let mut iso_week : Int32? = None
let mut iso_wd : Int32? = None
let mut week_mode = 0
let mut week_num = 0
let mut week_wd : Int32? = None
let mut epoch_sec : Int64? = None
let mut full_year = false
let mut i = 0
let mut j = 0
while i < f.length() {
let fc = f[i].to_int()
if fc == 0x20 || fc == 0x09 {
while j < s.length() && (s[j].to_int() == 0x20 || s[j].to_int() == 0x09) {
j = j + 1
}
i = i + 1
continue
}
if fc != 0x25 {
if j >= s.length() || s[j].to_int() != fc {
raise Malformed("text does not match the format at \{j}")
}
i = i + 1
j = j + 1
continue
}
guard directive(f, i) is Some((spec, next)) else {
raise Malformed("a % at the end of the format")
}
i = next
match spec.conv {
'Y' => {
let (v, k) = take(s, j, if spec.width > 0 { spec.width } else { 4 })
year = v
full_year = true
j = k
saw_date = true
}
'y' => {
let (v, k) = take(s, j, 2)
year = if v >= 69 { 1900 + v } else { 2000 + v }
j = k
saw_date = true
}
'C' => {
let (v, k) = take(s, j, 2)
century = Some(v)
j = k
}
'g' => {
let (v, k) = take(s, j, 2)
iso_year = Some(if v >= 69 { 1900 + v } else { 2000 + v })
j = k
}
'G' => {
let (v, k) = take(s, j, 4)
iso_year = Some(v)
full_year = true
j = k
saw_date = true
}
'm' => {
let (v, k) = take(s, j, 2)
month = v
j = k
saw_date = true
}
'd' | 'e' => {
if (spec.conv == 'e' || spec.under || spec.zero == false) &&
j < s.length() &&
s[j].to_int() == 0x20 {
j = j + 1
}
let (v, k) = take(s, j, 2)
mday = v
j = k
saw_date = true
}
'H' | 'I' | 'k' | 'l' => {
if (spec.conv == 'k' || spec.conv == 'l' || spec.under) &&
j < s.length() &&
s[j].to_int() == 0x20 {
j = j + 1
}
let (v, k) = take(s, j, 2)
hour = v
j = k
saw_time = true
}
'M' => {
let (v, k) = take(s, j, 2)
minute = v
j = k
saw_time = true
}
'S' => {
let (v, k) = take(s, j, 2)
sec = v
j = k
saw_time = true
}
'f' | 'N' => {
saw_time = true
if spec.dot {
if j < s.length() && s[j].to_int() == 0x2e {
j = j + 1
} else if spec.width < 0 {
continue
} else {
raise Malformed("expected a dot before the fraction at \{j}")
}
}
let most = if spec.width > 0 && spec.width < 9 { spec.width } else { 9 }
let start = j
let mut v = 0
while j < s.length() && is_digit(s[j].to_int()) && j - start < most {
v = v * 10 + (s[j].to_int() - 48)
j = j + 1
}
if j == start {
raise Malformed("expected digits for %f at \{start}")
}
for k = j - start; k < 9; k = k + 1 {
v = v * 10
}
nano = v
}
'p' | 'P' => {
if j + 2 > s.length() {
raise Truncated("expected AM, PM, am or pm")
}
let word = s[j:j + 2].to_owned().to_upper()
pm = match word {
"AM" => Some(false)
"PM" => Some(true)
_ => raise Malformed("expected AM/PM at \{j}")
}
j = j + 2
}
'a' | 'A' => j = skip_name(s, j)
'b' | 'B' | 'h' => {
let (m, k) = read_month(s, j)
month = m
j = k
saw_date = true
}
'j' => {
let (v, k) = take(s, j, 3)
j = k
let base : Date = { year, month: 1, day: 1, }
let dd = base.plus_days(v - 1)
year = dd.year
month = dd.month
mday = dd.day
saw_date = true
}
'U' => {
let (v, k) = take(s, j, 2)
week_mode = 1
week_num = v
j = k
}
'W' => {
let (v, k) = take(s, j, 2)
week_mode = 2
week_num = v
j = k
}
'w' => {
let (v, k) = take(s, j, 1)
week_wd = Some(if v == 7 { 0 } else { v })
j = k
}
'u' => {
let (v, k) = take(s, j, 1)
week_wd = Some(if v == 7 { 0 } else { v })
iso_wd = Some(v)
j = k
}
'V' => {
let (v, k) = take(s, j, 2)
iso_week = Some(v)
j = k
}
's' => {
let (v, k) = take_int(s, j)
epoch_sec = Some(v)
j = k
}
'z' => {
let (o, k) = read_offset(s, j)
offset = Some(o)
j = k
}
'Z' => j = skip_name(s, j)
'n' => {
if j >= s.length() || s[j].to_int() != 0x0a {
raise Malformed("expected a newline at \{j}")
}
j = j + 1
}
't' => {
if j >= s.length() || s[j].to_int() != 0x09 {
raise Malformed("expected a tab at \{j}")
}
j = j + 1
}
'%' => {
if j >= s.length() || s[j].to_int() != 0x25 {
raise Malformed("expected a per cent sign at \{j}")
}
j = j + 1
}
other => raise Malformed("unknown directive %" + other.to_string())
}
}
match pm {
Some(true) => if hour < 12 { hour = hour + 12 }
Some(false) => if hour == 12 { hour = 0 }
None => ()
}
// A raw `%s` fixes the instant whatever else the format said.
match epoch_sec {
Some(secs) => return Moment::of_epoch(secs, zone=Utc)
None => ()
}
// An ISO week date: the week year, the week number and the weekday decide the
// date, as Python's `_strptime` does when all three are present.
match (iso_year, iso_week, iso_wd) {
(Some(y), Some(w), Some(wd)) => {
let dd = Date::of_isocalendar(y, w, wd)
year = dd.year
month = dd.month
mday = dd.day
saw_date = true
}
_ => ()
}
// A Sunday- or Monday-based week number with a weekday decides the date, whatever else was
// read, which is Python's rule.
if week_mode != 0 {
match week_wd {
Some(wd) => {
let dd = date_from_week(year, week_num, wd, week_mode)
year = dd.year
month = dd.month
mday = dd.day
saw_date = true
}
None => ()
}
}
// A century without a full year is joined to the two-digit year the format read.
match century {
Some(c) => if !full_year { year = c * 100 + year % 100 } else { () }
None => ()
}
let time = Time::new(hour, minute, second=sec, nano~)
if !saw_date {
return Clock(time~)
}
let date = Date::new(year, month, mday)
if !saw_time {
return Day(date~)
}
match offset {
Some(0) => Zoned(date~, time~, zone=Utc)
Some(m) => Zoned(date~, time~, zone=Offset(m))
None => Plain(date~, time~)
}
}
///|
/// The date a Sunday- or Monday-based week number names, when the weekday is known.
fn date_from_week(
year : Int32,
week : Int32,
wd_sun : Int32,
mode : Int32,
) -> Date {
let jan1 = { year, month: 1, day: 1, }.weekday()
// The days are counted from the week's first day, which is Sunday in mode 1 and Monday otherwise.
let (first, day) = if mode == 1 {
(jan1, wd_sun)
} else {
((jan1 + 6) % 7, (wd_sun + 6) % 7)
}
let doy = if week == 0 {
1 + day - first
} else {
1 + (7 - first) % 7 + (week - 1) * 7 + day
}
{ year, month: 1, day: 1, }.plus_days(doy - 1)
}
///|
/// Up to `n` digits at `j`, and where reading stopped.
///
/// Fewer than `n` is allowed when what follows is not a digit, because `%m` reads
/// `1-2-3` the way a person writes it.
fn take(s : String, j : Int32, n : Int32) -> (Int32, Int32) raise Refused {
let mut v = 0
let mut k = j
while k < s.length() && k - j < n && is_digit(s[k].to_int()) {
v = v * 10 + (s[k].to_int() - 48)
k = k + 1
}
if k == j {
raise Malformed("expected a digit at \{j}")
}
(v, k)
}
///|
/// An integer starting at `j`, and where reading stopped; used for `%s`.
fn take_int(s : String, j : Int32) -> (Int64, Int32) raise Refused {
let mut k = j
let mut sign = 1L
if k < s.length() && (s[k].to_int() == 0x2d || s[k].to_int() == 0x2b) {
sign = if s[k].to_int() == 0x2d { -1L } else { 1L }
k = k + 1
}
let start = k
let mut v = 0L
while k < s.length() && is_digit(s[k].to_int()) {
v = v * 10L + (s[k].to_int() - 48).to_int64()
k = k + 1
}
if k == start {
raise Malformed("expected digits for %s at \{j}")
}
(sign * v, k)
}
///|
fn is_digit(c : Int32) -> Bool {
c >= 0x30 && c <= 0x39
}
///|
/// Past a run of letters, for a name this parser does not need to keep.
fn skip_name(s : String, j : Int32) -> Int32 {
let mut k = j
while k < s.length() {
let c = s[k].to_int()
let letter = (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)
if !letter {
break
}
k = k + 1
}
k
}
///|
/// A month name at `j`, long form or short, as its number.
fn read_month(s : String, j : Int32) -> (Int32, Int32) raise Refused {
let end = skip_name(s, j)
if end == j {
raise Malformed("expected a month name at \{j}")
}
let word = s[j:end].to_owned()
for k = 0; k < 12; k = k + 1 {
if long_months[k] == word {
return (k + 1, end)
}
}
if word.length() >= 3 {
let three = word[0:3].to_owned()
for k = 0; k < 12; k = k + 1 {
if months[k] == three {
return (k + 1, j + 3)
}
}
}
raise Malformed("not a month name: " + word)
}
///|
/// An offset at `j`, as `Z`, `+HHMM`, `+HH:MM`, `+HH:MM:SS`, or the reduced form.
fn read_offset(s : String, j : Int32) -> (Int32, Int32) raise Refused {
if j < s.length() && (s[j].to_int() == 0x5a || s[j].to_int() == 0x7a) {
return (0, j + 1)
}
if j + 2 > s.length() {
raise Truncated("an offset")
}
let sign = s[j].to_int()
if sign != 0x2b && sign != 0x2d {
raise Malformed("an offset begins with a sign at \{j}")
}
let (h, a) = take(s, j + 1, 2)
let b = if a < s.length() && s[a].to_int() == 0x3a { a + 1 } else { a }
let (m, c) = take(s, b, 2)
let mut d = c
if d < s.length() && s[d].to_int() == 0x3a {
let (_sec, e) = take(s, d + 1, 2)
d = e
}
let total = h * 60 + m
(if sign == 0x2d { -total } else { total }, d)
}
///|
let long_days : Array[String] = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
]
///|
let long_months : Array[String] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December",
]