#![allow(clippy::many_single_char_names)]
#![allow(missing_docs)] #![allow(
clippy::manual_range_contains,
clippy::manual_clamp,
clippy::while_let_loop
)]
use alloc::string::String;
pub const MAX_EPOCH_DAYS: i64 = 100_000_000;
pub const MIN_EPOCH_DAYS: i64 = -100_000_000;
pub const NS_PER_DAY: i128 = 86_400_000_000_000;
pub const NS_PER_HOUR: i128 = 3_600_000_000_000;
pub const NS_PER_MINUTE: i128 = 60_000_000_000;
pub const NS_PER_SEC: i128 = 1_000_000_000;
pub const MAX_EPOCH_NS: i128 = MAX_EPOCH_DAYS as i128 * NS_PER_DAY;
pub const MIN_EPOCH_NS: i128 = MIN_EPOCH_DAYS as i128 * NS_PER_DAY;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IsoDate {
pub year: i32,
pub month: u8,
pub day: u8,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct IsoTime {
pub hour: u8,
pub minute: u8,
pub second: u8,
pub millisecond: u16,
pub microsecond: u16,
pub nanosecond: u16,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IsoDateTime {
pub date: IsoDate,
pub time: IsoTime,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct DurationFields {
pub years: i128,
pub months: i128,
pub weeks: i128,
pub days: i128,
pub hours: i128,
pub minutes: i128,
pub seconds: i128,
pub milliseconds: i128,
pub microseconds: i128,
pub nanoseconds: i128,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Unit {
Year,
Month,
Week,
Day,
Hour,
Minute,
Second,
Millisecond,
Microsecond,
Nanosecond,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RoundMode {
Ceil,
Floor,
Expand,
Trunc,
HalfCeil,
HalfFloor,
HalfExpand,
HalfTrunc,
HalfEven,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Overflow {
Constrain,
Reject,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TemporalKind {
PlainDate,
PlainTime,
PlainDateTime,
Duration,
Instant,
PlainYearMonth,
PlainMonthDay,
ZonedDateTime,
}
impl TemporalKind {
#[must_use]
pub fn type_name(self) -> &'static str {
match self {
TemporalKind::PlainDate => "PlainDate",
TemporalKind::PlainTime => "PlainTime",
TemporalKind::PlainDateTime => "PlainDateTime",
TemporalKind::Duration => "Duration",
TemporalKind::Instant => "Instant",
TemporalKind::PlainYearMonth => "PlainYearMonth",
TemporalKind::PlainMonthDay => "PlainMonthDay",
TemporalKind::ZonedDateTime => "ZonedDateTime",
}
}
}
#[derive(Clone, Debug)]
pub struct TemporalData {
pub kind: TemporalKind,
pub date: IsoDate,
pub time: IsoTime,
pub duration: DurationFields,
pub epoch_ns: i128,
pub calendar: String,
pub tz: Option<String>,
}
impl Default for TemporalData {
fn default() -> Self {
TemporalData {
kind: TemporalKind::PlainDate,
date: IsoDate {
year: 0,
month: 1,
day: 1,
},
time: IsoTime::default(),
duration: DurationFields::default(),
epoch_ns: 0,
calendar: String::from("iso8601"),
tz: None,
}
}
}
#[must_use]
pub fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
#[must_use]
pub fn iso_days_in_month(year: i32, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
#[must_use]
pub fn iso_days_in_year(year: i32) -> u16 {
if is_leap_year(year) { 366 } else { 365 }
}
#[must_use]
pub fn iso_to_epoch_days(date: IsoDate) -> i64 {
let y = i64::from(date.year);
let m = i64::from(date.month);
let d = i64::from(date.day);
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400; let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe - 719_468
}
#[must_use]
pub fn epoch_days_to_iso(z: i64) -> IsoDate {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; IsoDate {
year: (if m <= 2 { y + 1 } else { y }) as i32,
month: m as u8,
day: d as u8,
}
}
#[must_use]
pub fn iso_day_of_week(date: IsoDate) -> u8 {
let dow = (iso_to_epoch_days(date) + 3).rem_euclid(7); (dow + 1) as u8
}
#[must_use]
pub fn iso_day_of_year(date: IsoDate) -> u16 {
let jan1 = IsoDate {
year: date.year,
month: 1,
day: 1,
};
(iso_to_epoch_days(date) - iso_to_epoch_days(jan1) + 1) as u16
}
#[must_use]
pub fn iso_week_of_year(date: IsoDate) -> (u8, i32) {
let dow = i32::from(iso_day_of_week(date));
let doy = i32::from(iso_day_of_year(date));
let week = (doy - dow + 10) / 7;
if week < 1 {
let prev = date.year - 1;
let wk = if is_iso_long_year(prev) { 53 } else { 52 };
(wk as u8, prev)
} else if week > 52 && !is_iso_long_year(date.year) {
(1, date.year + 1)
} else {
(week as u8, date.year)
}
}
fn is_iso_long_year(year: i32) -> bool {
let p = |y: i32| (y + y / 4 - y / 100 + y / 400) % 7;
p(year) == 4 || p(year - 1) == 3
}
#[must_use]
pub fn iso_date_in_range(date: IsoDate) -> bool {
let d = iso_to_epoch_days(date);
(MIN_EPOCH_DAYS - 1..=MAX_EPOCH_DAYS + 1).contains(&d)
}
#[must_use]
pub fn regulate_iso_date(year: i32, month: i64, day: i64, overflow: Overflow) -> Option<IsoDate> {
match overflow {
Overflow::Constrain => {
let month = month.clamp(1, 12) as u8;
let dim = i64::from(iso_days_in_month(year, month));
let day = day.clamp(1, dim) as u8;
Some(IsoDate { year, month, day })
}
Overflow::Reject => {
if !(1..=12).contains(&month) {
return None;
}
let month = month as u8;
if day < 1 || day > i64::from(iso_days_in_month(year, month)) {
return None;
}
Some(IsoDate {
year,
month,
day: day as u8,
})
}
}
}
#[must_use]
pub fn regulate_iso_time(
hour: i64,
minute: i64,
second: i64,
ms: i64,
us: i64,
ns: i64,
overflow: Overflow,
) -> Option<IsoTime> {
let ck = |v: i64, max: i64| -> Option<i64> {
match overflow {
Overflow::Constrain => Some(v.clamp(0, max)),
Overflow::Reject => (0..=max).contains(&v).then_some(v),
}
};
Some(IsoTime {
hour: ck(hour, 23)? as u8,
minute: ck(minute, 59)? as u8,
second: ck(second, 59)? as u8,
millisecond: ck(ms, 999)? as u16,
microsecond: ck(us, 999)? as u16,
nanosecond: ck(ns, 999)? as u16,
})
}
#[must_use]
pub fn time_to_nanos(t: IsoTime) -> i128 {
i128::from(t.hour) * NS_PER_HOUR
+ i128::from(t.minute) * NS_PER_MINUTE
+ i128::from(t.second) * NS_PER_SEC
+ i128::from(t.millisecond) * 1_000_000
+ i128::from(t.microsecond) * 1_000
+ i128::from(t.nanosecond)
}
#[must_use]
pub fn balance_time_from_nanos(total_ns: i128) -> (i64, IsoTime) {
let day = total_ns.div_euclid(NS_PER_DAY);
let mut r = total_ns.rem_euclid(NS_PER_DAY);
let hour = (r / NS_PER_HOUR) as u8;
r %= NS_PER_HOUR;
let minute = (r / NS_PER_MINUTE) as u8;
r %= NS_PER_MINUTE;
let second = (r / NS_PER_SEC) as u8;
r %= NS_PER_SEC;
let millisecond = (r / 1_000_000) as u16;
r %= 1_000_000;
let microsecond = (r / 1_000) as u16;
let nanosecond = (r % 1_000) as u16;
(
day as i64,
IsoTime {
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
},
)
}
#[must_use]
pub fn add_time(t: IsoTime, delta_ns: i128) -> (i64, IsoTime) {
balance_time_from_nanos(time_to_nanos(t) + delta_ns)
}
#[must_use]
pub fn balance_iso_year_month(year: i64, month: i64) -> (i32, u8) {
let m0 = month - 1;
let y = year + m0.div_euclid(12);
let m = m0.rem_euclid(12) + 1;
(y as i32, m as u8)
}
#[must_use]
pub fn add_iso_date(
date: IsoDate,
years: i64,
months: i64,
weeks: i64,
days: i64,
overflow: Overflow,
) -> Option<IsoDate> {
let (y, m) =
balance_iso_year_month(i64::from(date.year) + years, i64::from(date.month) + months);
let intermediate = regulate_iso_date(y, i64::from(m), i64::from(date.day), overflow)?;
let total_days = days + weeks * 7;
let result = epoch_days_to_iso(iso_to_epoch_days(intermediate) + total_days);
iso_date_in_range(result).then_some(result)
}
#[must_use]
pub fn difference_iso_date(from: IsoDate, to: IsoDate, largest: Unit) -> (i64, i64, i64, i64) {
if largest == Unit::Day || largest == Unit::Week {
let mut days = iso_to_epoch_days(to) - iso_to_epoch_days(from);
let mut weeks = 0;
if largest == Unit::Week {
weeks = days / 7;
days %= 7;
}
return (0, 0, weeks, days);
}
let sign = match compare_iso_date(to, from) {
core::cmp::Ordering::Greater => 1_i64,
core::cmp::Ordering::Less => -1,
core::cmp::Ordering::Equal => return (0, 0, 0, 0),
};
let mut years = i64::from(to.year) - i64::from(from.year);
let mut mid = add_iso_date(from, years, 0, 0, 0, Overflow::Constrain).unwrap_or(from);
while sign > 0 && compare_iso_date(mid, to) == core::cmp::Ordering::Greater
|| sign < 0 && compare_iso_date(mid, to) == core::cmp::Ordering::Less
{
years -= sign;
mid = add_iso_date(from, years, 0, 0, 0, Overflow::Constrain).unwrap_or(from);
}
let mut months = 0_i64;
loop {
let Some(next) = add_iso_date(from, years, months + sign, 0, 0, Overflow::Constrain) else {
break;
};
if sign > 0 && compare_iso_date(next, to) == core::cmp::Ordering::Greater
|| sign < 0 && compare_iso_date(next, to) == core::cmp::Ordering::Less
{
break;
}
months += sign;
mid = next;
if months.abs() > 12 {
years += sign;
months -= 12 * sign;
}
}
if largest == Unit::Month {
months += years * 12;
years = 0;
}
let days = iso_to_epoch_days(to) - iso_to_epoch_days(mid);
(years, months, 0, days)
}
#[must_use]
pub fn compare_iso_date(a: IsoDate, b: IsoDate) -> core::cmp::Ordering {
(a.year, a.month, a.day).cmp(&(b.year, b.month, b.day))
}
#[must_use]
pub fn compare_iso_time(a: IsoTime, b: IsoTime) -> core::cmp::Ordering {
time_to_nanos(a).cmp(&time_to_nanos(b))
}
impl DurationFields {
#[must_use]
pub fn sign(&self) -> i64 {
for v in [
self.years,
self.months,
self.weeks,
self.days,
self.hours,
self.minutes,
self.seconds,
self.milliseconds,
self.microseconds,
self.nanoseconds,
] {
if v != 0 {
return v.signum() as i64;
}
}
0
}
#[must_use]
pub fn is_valid(&self) -> bool {
let mut sign = 0_i128;
for v in [
self.years,
self.months,
self.weeks,
self.days,
self.hours,
self.minutes,
self.seconds,
self.milliseconds,
self.microseconds,
self.nanoseconds,
] {
if v != 0 {
let s = v.signum();
if sign != 0 && s != sign {
return false;
}
sign = s;
}
}
const TWO_POW_32: i128 = 1 << 32;
const TWO_POW_53: i128 = 1 << 53;
if self.years.abs() >= TWO_POW_32
|| self.months.abs() >= TWO_POW_32
|| self.weeks.abs() >= TWO_POW_32
{
return false;
}
let mut total_ns: i128 = 0;
for (v, mult) in [
(self.days, NS_PER_DAY),
(self.hours, NS_PER_HOUR),
(self.minutes, NS_PER_MINUTE),
(self.seconds, NS_PER_SEC),
(self.milliseconds, 1_000_000),
(self.microseconds, 1_000),
(self.nanoseconds, 1),
] {
let Some(contrib) = v.checked_mul(mult) else {
return false;
};
let Some(sum) = total_ns.checked_add(contrib) else {
return false;
};
total_ns = sum;
}
(total_ns / NS_PER_SEC).abs() < TWO_POW_53
}
#[must_use]
pub fn time_nanos(&self) -> i128 {
self.hours * NS_PER_HOUR
+ self.minutes * NS_PER_MINUTE
+ self.seconds * NS_PER_SEC
+ self.milliseconds * 1_000_000
+ self.microseconds * 1_000
+ self.nanoseconds
}
}
#[must_use]
pub fn quantize_duration_fields(mut d: DurationFields) -> DurationFields {
fn q(v: i128) -> i128 {
(v as f64) as i128
}
d.years = q(d.years);
d.months = q(d.months);
d.weeks = q(d.weeks);
d.days = q(d.days);
d.hours = q(d.hours);
d.minutes = q(d.minutes);
d.seconds = q(d.seconds);
d.milliseconds = q(d.milliseconds);
d.microseconds = q(d.microseconds);
d.nanoseconds = q(d.nanoseconds);
d
}
#[must_use]
pub fn balance_time_duration(total_ns: i128, largest: Unit) -> DurationFields {
let sign = total_ns.signum();
let mut r = total_ns.abs();
let mut d = DurationFields::default();
let mut set = |field: &mut i128, per: i128, active: bool| {
if active {
*field = (r / per) * sign;
r %= per;
}
};
set(&mut d.hours, NS_PER_HOUR, largest <= Unit::Hour);
set(&mut d.minutes, NS_PER_MINUTE, largest <= Unit::Minute);
set(&mut d.seconds, NS_PER_SEC, largest <= Unit::Second);
set(&mut d.milliseconds, 1_000_000, largest <= Unit::Millisecond);
set(&mut d.microseconds, 1_000, largest <= Unit::Microsecond);
d.nanoseconds = r * sign;
d
}
#[must_use]
pub fn round_to_increment(x: i128, increment: i128, mode: RoundMode) -> i128 {
if increment <= 1 {
return x;
}
let q = x.div_euclid(increment);
let r = x.rem_euclid(increment);
if r == 0 {
return x;
}
let lower = q * increment;
let upper = lower + increment;
let pick_upper = match mode {
RoundMode::Ceil | RoundMode::Expand => true,
RoundMode::Floor | RoundMode::Trunc => false,
RoundMode::HalfCeil => 2 * r >= increment,
RoundMode::HalfFloor => 2 * r > increment,
RoundMode::HalfExpand => 2 * r > increment || (2 * r == increment && x >= 0),
RoundMode::HalfTrunc => 2 * r > increment || (2 * r == increment && x < 0),
RoundMode::HalfEven => {
if 2 * r == increment {
(q % 2) != 0
} else {
2 * r > increment
}
}
};
match mode {
RoundMode::Trunc => {
if x >= 0 {
lower
} else {
upper
}
}
RoundMode::Expand => {
if x >= 0 {
upper
} else {
lower
}
}
_ => {
if pick_upper {
upper
} else {
lower
}
}
}
}
#[must_use]
pub fn pad(v: u64, width: usize) -> String {
let s = alloc::format!("{v}");
if s.len() >= width {
s
} else {
let mut out = String::with_capacity(width);
for _ in 0..width - s.len() {
out.push('0');
}
out.push_str(&s);
out
}
}
#[must_use]
pub fn format_iso_year(year: i32) -> String {
if (0..=9999).contains(&year) {
pad(year as u64, 4)
} else {
let sign = if year < 0 { '-' } else { '+' };
alloc::format!("{sign}{}", pad(year.unsigned_abs() as u64, 6))
}
}
#[must_use]
pub fn format_fraction(sub_second_ns: u32, precision: Option<u8>) -> String {
match precision {
Some(0) => String::new(),
Some(p) => {
let full = pad(u64::from(sub_second_ns), 9);
alloc::format!(".{}", &full[..p as usize])
}
None => {
if sub_second_ns == 0 {
return String::new();
}
let full = pad(u64::from(sub_second_ns), 9);
let trimmed = full.trim_end_matches('0');
alloc::format!(".{trimmed}")
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ParsedIso {
pub date: Option<IsoDate>,
pub time: Option<IsoTime>,
pub offset_ns: Option<i128>,
pub z: bool,
pub tz_name: Option<String>,
pub calendar: Option<String>,
}
struct Cursor<'a> {
b: &'a [u8],
i: usize,
}
impl Cursor<'_> {
fn peek(&self) -> Option<u8> {
self.b.get(self.i).copied()
}
fn eat(&mut self, c: u8) -> bool {
if self.peek() == Some(c) {
self.i += 1;
true
} else {
false
}
}
fn eat_sign(&mut self) -> Option<bool> {
match self.peek() {
Some(b'+') => {
self.i += 1;
Some(false)
}
Some(b'-') => {
self.i += 1;
Some(true)
}
_ => None,
}
}
fn digits(&mut self, n: usize) -> Option<i64> {
let mut v = 0_i64;
for _ in 0..n {
let c = self.peek()?;
if !c.is_ascii_digit() {
return None;
}
v = v * 10 + i64::from(c - b'0');
self.i += 1;
}
Some(v)
}
}
fn time_core_is_calendar_ambiguous(core: &str) -> bool {
let b = core.as_bytes();
let md = {
let mut i = 0;
if b.len() >= 2 && b[0] == b'-' && b[1] == b'-' {
i = 2;
}
let digit = |x: u8| x.is_ascii_digit();
let two = |b: &[u8], i: usize| -> Option<i64> {
if i + 2 <= b.len() && digit(b[i]) && digit(b[i + 1]) {
Some(i64::from(b[i] - b'0') * 10 + i64::from(b[i + 1] - b'0'))
} else {
None
}
};
(|| {
let mm = two(b, i)?;
i += 2;
if i < b.len() && b[i] == b'-' {
i += 1;
}
let dd = two(b, i)?;
i += 2;
if i != b.len() || !(1..=12).contains(&mm) {
return None;
}
let max = iso_days_in_month(2000, mm as u8);
if dd >= 1 && dd <= i64::from(max) {
Some(())
} else {
None
}
})()
.is_some()
};
let ym = {
let two = |b: &[u8], i: usize| -> Option<i64> {
if i + 2 <= b.len() && b[i].is_ascii_digit() && b[i + 1].is_ascii_digit() {
Some(i64::from(b[i] - b'0') * 10 + i64::from(b[i + 1] - b'0'))
} else {
None
}
};
(|| {
if b.len() < 4 || !b[..4].iter().all(u8::is_ascii_digit) {
return None;
}
let mut i = 4;
if i < b.len() && b[i] == b'-' {
i += 1;
}
let mm = two(b, i)?;
i += 2;
if i == b.len() && (1..=12).contains(&mm) {
Some(())
} else {
None
}
})()
.is_some()
};
md || ym
}
#[must_use]
pub fn parse_iso_time_string(s: &str) -> Option<ParsedIso> {
let out = parse_iso_datetime(s)?;
if out.date.is_none()
&& !matches!(s.as_bytes().first(), Some(b'T' | b't'))
&& time_core_is_calendar_ambiguous(s.split('[').next().unwrap_or(s))
{
return None;
}
Some(out)
}
#[must_use]
pub fn parse_calendar_string(s: &str) -> Option<String> {
let p = parse_iso_datetime(s)?;
Some(p.calendar.unwrap_or_else(|| String::from("iso8601")))
}
#[must_use]
pub fn parse_iso_datetime(s: &str) -> Option<ParsedIso> {
let mut c = Cursor {
b: s.as_bytes(),
i: 0,
};
let mut out = ParsedIso::default();
let start = c.i;
if let Some(date) = parse_date(&mut c) {
out.date = Some(date);
if c.eat(b'T') || c.eat(b't') || c.eat(b' ') {
let (time, off, z) = parse_time_and_offset(&mut c)?;
out.time = Some(time);
out.offset_ns = off;
out.z = z;
}
} else {
c.i = start;
let _ = c.eat(b'T') || c.eat(b't');
let (time, off, z) = parse_time_and_offset(&mut c)?;
out.time = Some(time);
out.offset_ns = off;
out.z = z;
}
parse_annotations(&mut c, &mut out)?;
if c.i != c.b.len() {
return None;
}
Some(out)
}
fn parse_date(c: &mut Cursor) -> Option<IsoDate> {
let year = if let Some(neg) = c.eat_sign() {
let y = c.digits(6)?;
let y = if neg { -y } else { y };
if neg && y == 0 {
return None; }
y
} else {
c.digits(4)?
};
let sep = c.eat(b'-');
let month = c.digits(2)?;
if sep {
if !c.eat(b'-') {
return None;
}
} else if c.peek() == Some(b'-') {
return None;
}
let day = c.digits(2)?;
regulate_iso_date(year as i32, month, day, Overflow::Reject)
}
fn parse_time_and_offset(c: &mut Cursor) -> Option<(IsoTime, Option<i128>, bool)> {
let hour = c.digits(2)?;
let mut minute = 0;
let mut second = 0;
let mut frac_ns = 0_i64;
if c.eat(b':') {
minute = c.digits(2)?;
if c.eat(b':') {
second = c.digits(2)?;
frac_ns = parse_fraction(c)?;
}
} else if c.peek().is_some_and(|b| b.is_ascii_digit()) {
minute = c.digits(2)?;
if c.peek().is_some_and(|b| b.is_ascii_digit()) {
second = c.digits(2)?;
frac_ns = parse_fraction(c)?;
}
}
if second == 60 {
second = 59;
}
let time = regulate_iso_time(
hour,
minute,
second,
frac_ns / 1_000_000,
(frac_ns / 1_000) % 1_000,
frac_ns % 1_000,
Overflow::Reject,
)?;
let (off, z) = parse_offset(c)?;
Some((time, off, z))
}
fn parse_fraction(c: &mut Cursor) -> Option<i64> {
if c.eat(b'.') || c.eat(b',') {
let mut ns = 0_i64;
let mut count = 0;
while c.peek().is_some_and(|b| b.is_ascii_digit()) {
if count == 9 {
return None; }
ns = ns * 10 + i64::from(c.peek().unwrap() - b'0');
c.i += 1;
count += 1;
}
if count == 0 {
return None; }
for _ in count..9 {
ns *= 10;
}
Some(ns)
} else {
Some(0)
}
}
fn parse_offset(c: &mut Cursor) -> Option<(Option<i128>, bool)> {
if c.eat(b'Z') || c.eat(b'z') {
return Some((Some(0), true));
}
if let Some(neg) = c.eat_sign() {
let Some(h) = c.digits(2) else {
return Some((None, false));
};
let mut m = 0;
let mut s = 0;
let mut frac = 0_i64;
if c.eat(b':') {
m = c.digits(2)?;
if c.eat(b':') {
s = c.digits(2)?;
frac = parse_fraction(c)?;
}
} else if c.peek().is_some_and(|b| b.is_ascii_digit()) {
m = c.digits(2)?;
if c.peek().is_some_and(|b| b.is_ascii_digit()) {
s = c.digits(2)?;
frac = parse_fraction(c)?;
}
}
if h > 23 || m > 59 || s > 59 {
return None;
}
let ns = i128::from(h) * NS_PER_HOUR
+ i128::from(m) * NS_PER_MINUTE
+ i128::from(s) * NS_PER_SEC
+ i128::from(frac);
return Some((Some(if neg { -ns } else { ns }), false));
}
Some((None, false))
}
fn is_valid_annotation_key(key: &str) -> bool {
let mut chars = key.bytes();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_lowercase() || first == b'_') {
return false;
}
chars.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
}
fn parse_annotations(c: &mut Cursor, out: &mut ParsedIso) -> Option<()> {
let mut first = true;
let mut tz_seen = false;
let mut cal_count = 0_u32;
let mut cal_critical = false;
while c.eat(b'[') {
let critical = c.eat(b'!');
let start = c.i;
while c.peek().is_some_and(|b| b != b']') {
c.i += 1;
}
let inner = core::str::from_utf8(&c.b[start..c.i]).unwrap_or("");
if !c.eat(b']') {
return None; }
if let Some(eq) = inner.find('=') {
let key = &inner[..eq];
let value = &inner[eq + 1..];
if !is_valid_annotation_key(key) || value.is_empty() {
return None;
}
if key == "u-ca" {
cal_count += 1;
cal_critical |= critical;
if out.calendar.is_none() {
out.calendar = Some(String::from(value));
}
} else if critical {
return None;
}
} else {
if !first || tz_seen || inner.is_empty() {
return None;
}
tz_seen = true;
out.tz_name = Some(String::from(inner));
}
first = false;
}
if cal_count > 1 && cal_critical {
return None;
}
Some(())
}
#[must_use]
pub fn parse_iso_duration(s: &str) -> Option<DurationFields> {
let mut c = Cursor {
b: s.as_bytes(),
i: 0,
};
let sign = match c.eat_sign() {
Some(true) => -1_i128,
_ => 1,
};
if !(c.eat(b'P') || c.eat(b'p')) {
return None;
}
let mut d = DurationFields::default();
let mut any = false;
let mut last_date = 0;
while let Some((n, _frac)) = peek_number(&mut c) {
let desig = c.peek()?;
let ord = match desig.to_ascii_uppercase() {
b'Y' => 1,
b'M' => 2,
b'W' => 3,
b'D' => 4,
_ => break,
};
if ord <= last_date {
return None; }
last_date = ord;
c.i += 1;
match ord {
1 => d.years = i128::from(n) * sign,
2 => d.months = i128::from(n) * sign,
3 => d.weeks = i128::from(n) * sign,
_ => d.days = i128::from(n) * sign,
}
any = true;
}
if c.eat(b'T') || c.eat(b't') {
let mut last_time = 0;
let mut seen_time = false;
while let Some((n, frac)) = peek_number(&mut c) {
let desig = c.peek()?;
let ord = match desig.to_ascii_uppercase() {
b'H' => 1,
b'M' => 2,
b'S' => 3,
_ => return None,
};
if ord <= last_time {
return None;
}
last_time = ord;
c.i += 1;
seen_time = true;
any = true;
match ord {
1 => {
d.hours = i128::from(n) * sign;
if let Some(f) = frac {
distribute_fraction(&mut d, f, NS_PER_HOUR, sign);
break;
}
}
2 => {
d.minutes = i128::from(n) * sign;
if let Some(f) = frac {
distribute_fraction(&mut d, f, NS_PER_MINUTE, sign);
break;
}
}
_ => {
d.seconds = i128::from(n) * sign;
if let Some(f) = frac {
distribute_fraction(&mut d, f, NS_PER_SEC, sign);
}
}
}
}
if !seen_time {
return None; }
}
if !any || c.i != c.b.len() {
return None;
}
d.is_valid().then_some(d)
}
fn peek_number(c: &mut Cursor) -> Option<(i64, Option<i64>)> {
if !c.peek().is_some_and(|b| b.is_ascii_digit()) {
return None;
}
let mut n = 0_i64;
while c.peek().is_some_and(|b| b.is_ascii_digit()) {
n = n
.checked_mul(10)?
.checked_add(i64::from(c.peek().unwrap() - b'0'))?;
c.i += 1;
}
let frac = if c.peek() == Some(b'.') || c.peek() == Some(b',') {
c.i += 1;
let mut f = 0_i64;
let mut cnt = 0;
while c.peek().is_some_and(|b| b.is_ascii_digit()) {
if cnt == 9 {
return None; }
f = f * 10 + i64::from(c.peek().unwrap() - b'0');
c.i += 1;
cnt += 1;
}
if cnt == 0 {
return None; }
for _ in cnt..9 {
f *= 10;
}
Some(f)
} else {
None
};
Some((n, frac))
}
fn distribute_fraction(d: &mut DurationFields, frac_1e9: i64, per: i128, sign: i128) {
let total_ns = i128::from(frac_1e9) * per / NS_PER_SEC;
let mut r = total_ns;
d.seconds += (r / NS_PER_SEC) * sign;
r %= NS_PER_SEC;
d.milliseconds += (r / 1_000_000) * sign;
r %= 1_000_000;
d.microseconds += (r / 1_000) * sign;
r %= 1_000;
d.nanoseconds += r * sign;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn difference_iso_date_terminates_at_year_range_boundary() {
let from = IsoDate {
year: 1970,
month: 1,
day: 1,
};
let to = IsoDate {
year: 275760,
month: 9,
day: 13,
};
let (y, m, _w, d) = difference_iso_date(from, to, Unit::Year);
assert_eq!(y, 273790);
assert_eq!(m, 8);
assert_eq!(d, 12);
let min = IsoDate {
year: -271821,
month: 4,
day: 19,
};
let (ny, _nm, _nw, _nd) = difference_iso_date(to, min, Unit::Year);
assert!(ny < 0);
}
#[test]
fn balance_time_duration_holds_large_i128_fields() {
let seconds: i128 = 8_692_288_669_465_520;
let micro_total = seconds * 1_000_000 + 373_761;
let d = balance_time_duration(micro_total * 1_000, Unit::Microsecond);
assert_eq!(d.microseconds, 8_692_288_669_465_520_373_761);
assert_eq!(d.microseconds, micro_total);
assert_eq!(d.nanoseconds, 0);
let ns_total = seconds * 1_000_000_000 + 321_414_345;
let d = balance_time_duration(ns_total, Unit::Nanosecond);
assert_eq!(d.nanoseconds, 8_692_288_669_465_520_321_414_345);
assert_eq!(d.nanoseconds, ns_total);
let d = balance_time_duration(-ns_total, Unit::Nanosecond);
assert_eq!(d.nanoseconds, -ns_total);
}
#[test]
fn quantize_duration_rounds_fields_to_float64() {
let d = DurationFields {
microseconds: 18_446_744_073_709_551,
nanoseconds: -616,
..Default::default()
};
let q = quantize_duration_fields(d);
assert_eq!(q.microseconds, 18_446_744_073_709_552);
assert_eq!(q.nanoseconds, -616);
let small = DurationFields {
years: 1,
months: 11,
days: 24,
..Default::default()
};
assert_eq!(quantize_duration_fields(small), small);
}
#[test]
fn parse_calendar_string_extracts_annotation() {
assert_eq!(
parse_calendar_string("2020-01-01").as_deref(),
Some("iso8601")
);
assert_eq!(
parse_calendar_string("2020-01-01T00:00:00").as_deref(),
Some("iso8601")
);
assert_eq!(parse_calendar_string("15:23").as_deref(), Some("iso8601"));
assert_eq!(
parse_calendar_string("T15:23:30").as_deref(),
Some("iso8601")
);
assert_eq!(
parse_calendar_string("2020-01-01[u-ca=hebrew]").as_deref(),
Some("hebrew")
);
assert_eq!(parse_calendar_string(""), None);
assert_eq!(parse_calendar_string("not-a-date"), None);
}
#[test]
fn duration_is_valid_enforces_magnitude_limits() {
let mk = |years: i128, months: i128, weeks: i128, days: i128| DurationFields {
years,
months,
weeks,
days,
..Default::default()
};
assert!(mk(1, 2, 3, 4).is_valid());
let two_pow_32: i128 = 1 << 32;
assert!(mk(two_pow_32 - 1, 0, 0, 0).is_valid());
assert!(!mk(two_pow_32, 0, 0, 0).is_valid());
assert!(!mk(0, two_pow_32, 0, 0).is_valid());
assert!(!mk(0, 0, two_pow_32, 0).is_valid());
assert!(!mk(0, 0, -two_pow_32, 0).is_valid());
assert!(mk(0, 0, 0, 104_249_991_374).is_valid());
assert!(mk(0, 0, 0, -104_249_991_374).is_valid());
assert!(!mk(0, 0, 0, 104_249_991_375).is_valid());
assert!(!mk(0, 0, 0, -104_249_991_375).is_valid());
assert!(!mk(1, -1, 0, 0).is_valid());
}
#[test]
fn epoch_round_trip() {
for &(y, m, dd) in &[
(1970, 1, 1),
(2000, 2, 29),
(1, 1, 1),
(-1, 12, 31),
(275760, 9, 13),
] {
let date = IsoDate {
year: y,
month: m,
day: dd,
};
assert_eq!(epoch_days_to_iso(iso_to_epoch_days(date)), date);
}
assert_eq!(
iso_to_epoch_days(IsoDate {
year: 1970,
month: 1,
day: 1
}),
0
);
assert_eq!(
iso_to_epoch_days(IsoDate {
year: 1970,
month: 1,
day: 2
}),
1
);
assert_eq!(
iso_to_epoch_days(IsoDate {
year: 1969,
month: 12,
day: 31
}),
-1
);
}
#[test]
fn day_of_week_known() {
assert_eq!(
iso_day_of_week(IsoDate {
year: 2020,
month: 1,
day: 1
}),
3
);
assert_eq!(
iso_day_of_week(IsoDate {
year: 1970,
month: 1,
day: 1
}),
4
);
}
#[test]
fn leap_and_dim() {
assert!(is_leap_year(2000));
assert!(!is_leap_year(1900));
assert!(is_leap_year(2024));
assert_eq!(iso_days_in_month(2024, 2), 29);
assert_eq!(iso_days_in_month(2023, 2), 28);
}
#[test]
fn parse_basic() {
let p = parse_iso_datetime("2020-03-15T12:30:45.5").unwrap();
assert_eq!(
p.date.unwrap(),
IsoDate {
year: 2020,
month: 3,
day: 15
}
);
let t = p.time.unwrap();
assert_eq!(
(t.hour, t.minute, t.second, t.millisecond),
(12, 30, 45, 500)
);
let z = parse_iso_datetime("2020-03-15T12:30:45Z").unwrap();
assert!(z.z);
}
#[test]
fn parse_fraction_at_most_9_digits() {
assert!(parse_iso_datetime("1970-01-01T00:00:00.123456789").is_some());
assert!(parse_iso_datetime("1970-01-01T00:00:00.1234567891").is_none());
assert!(parse_iso_datetime("1970-01-01T00:00:00.1234567890").is_none());
assert!(parse_iso_datetime("00:00:00.123456789").is_some());
assert!(parse_iso_datetime("00:00:00.1234567891").is_none());
assert!(parse_iso_datetime("00+00:00:00.123456789").is_some());
assert!(parse_iso_datetime("00+00:00:00.1234567891").is_none());
let p = parse_iso_datetime("1970-01-01T00:00:00.5").unwrap();
assert_eq!(p.time.unwrap().millisecond, 500);
assert!(parse_iso_datetime("1970-01-01T00:00:00.").is_none());
assert!(parse_iso_duration("PT0.123456789S").is_some());
assert!(parse_iso_duration("PT0.1234567891S").is_none());
}
#[test]
fn parse_date_separator_consistency() {
assert!(parse_iso_datetime("2020-01-01").unwrap().date.is_some());
assert!(parse_iso_datetime("20200101").unwrap().date.is_some());
assert!(parse_iso_datetime("+002020-01-01").unwrap().date.is_some());
assert!(parse_iso_datetime("2020-0101").is_none_or(|p| p.date.is_none()));
assert!(parse_iso_datetime("202001-01").is_none_or(|p| p.date.is_none()));
assert!(parse_iso_datetime("+0002020-01-01").is_none());
}
#[test]
fn parse_offset_component_ranges() {
assert!(parse_iso_datetime("00:00:00+23:59").is_some());
assert!(parse_iso_datetime("00:00:00+00:00").is_some());
assert!(parse_iso_datetime("00:00-24:00").is_none());
assert!(parse_iso_datetime("00:00+24:00").is_none());
assert!(parse_iso_datetime("00:00:00-00:60").is_none());
}
#[test]
fn parse_duration_basic() {
let d = parse_iso_duration("P1Y2M3DT4H5M6.5S").unwrap();
assert_eq!(
(d.years, d.months, d.days, d.hours, d.minutes, d.seconds),
(1, 2, 3, 4, 5, 6)
);
assert_eq!(d.milliseconds, 500);
assert!(parse_iso_duration("P").is_none());
assert_eq!(parse_iso_duration("-P1D").unwrap().days, -1);
}
#[test]
fn add_date_overflow() {
let jan31 = IsoDate {
year: 2023,
month: 1,
day: 31,
};
assert_eq!(
add_iso_date(jan31, 0, 1, 0, 0, Overflow::Constrain).unwrap(),
IsoDate {
year: 2023,
month: 2,
day: 28
}
);
assert_eq!(add_iso_date(jan31, 0, 1, 0, 0, Overflow::Reject), None);
}
}