use chrono::{NaiveDate, NaiveTime, Timelike};
use std::collections::HashMap;
pub enum TemporalField {
Int(i64),
Float(f64),
Str(String),
Date(i64),
}
type Fields = HashMap<String, TemporalField>;
fn f_int(f: &Fields, k: &str) -> Option<i64> {
match f.get(k)? {
TemporalField::Int(n) => Some(*n),
_ => None,
}
}
fn f_num(f: &Fields, k: &str) -> Option<f64> {
#[allow(
clippy::cast_precision_loss,
reason = "duration counts are small; exactness beyond f64 is irrelevant"
)]
match f.get(k)? {
TemporalField::Int(n) => Some(*n as f64),
TemporalField::Float(x) => Some(*x),
_ => None,
}
}
fn f_str<'a>(f: &'a Fields, k: &str) -> Option<&'a str> {
match f.get(k)? {
TemporalField::Str(s) => Some(s),
_ => None,
}
}
fn f_date(f: &Fields, k: &str) -> Option<i64> {
match f.get(k)? {
TemporalField::Date(d) => Some(*d),
_ => None,
}
}
#[must_use]
pub fn render_temporal_map(name: &str, fields: &Fields) -> Option<String> {
match name {
"date" => resolve_date(fields).map(format_date),
"localtime" => {
if !fields.contains_key("hour") {
return None;
}
Some(format_time(&resolve_time(fields)?))
}
"time" => build_time_map(fields),
"localdatetime" => {
let date = resolve_date(fields)?;
let time = resolve_time(fields)?;
Some(format!("{}T{}", format_date(date), format_time(&time)))
}
"datetime" => build_date_time_map(fields),
"duration" => Some(format_duration(&build_duration_map(fields)?)),
_ => None,
}
}
#[must_use]
pub fn date_from_map(fields: &Fields) -> Option<i64> {
resolve_date(fields)
}
#[must_use]
pub fn date_to_epoch_days(d: NaiveDate) -> Option<i64> {
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
Some((d - epoch).num_days())
}
#[must_use]
pub fn epoch_days_to_date(days: i64) -> Option<NaiveDate> {
NaiveDate::from_ymd_opt(1970, 1, 1)?.checked_add_signed(chrono::Duration::try_days(days)?)
}
#[must_use]
pub fn parse_date_or_datetime_prefix(s: &str) -> Option<i64> {
let head = s.split_once('T').map_or(s, |(d, _)| d);
parse_date_string(head)
}
#[derive(Default)]
pub struct DateOverrides {
pub year: Option<i64>,
pub month: Option<i64>,
pub day: Option<i64>,
pub week: Option<i64>,
pub day_of_week: Option<i64>,
pub ordinal_day: Option<i64>,
pub quarter: Option<i64>,
pub day_of_quarter: Option<i64>,
}
#[must_use]
pub fn project_date(base_days: i64, o: &DateOverrides) -> Option<i64> {
use crate::calendar;
let (base_year, base_month, base_day) = calendar::civil_from_days(base_days);
let year = o.year.unwrap_or(base_year);
if o.week.is_some() || o.day_of_week.is_some() {
let (base_iso_year, base_week) = calendar::iso_week(base_days);
let iso_year = o.year.unwrap_or(base_iso_year);
let week = match o.week {
Some(w) => u32::try_from(w).ok()?,
None => base_week,
};
let dow = match o.day_of_week {
Some(d) => u32::try_from(d).ok()?,
None => calendar::iso_weekday(base_days),
};
return calendar::from_iso_ywd(iso_year, week, dow);
}
if let Some(ord) = o.ordinal_day {
return calendar::from_ordinal(year, u32::try_from(ord).ok()?);
}
if o.quarter.is_some() || o.day_of_quarter.is_some() {
let quarter = match o.quarter {
Some(q) => q,
None => i64::from((base_month - 1) / 3 + 1),
};
let day_of_quarter = o
.day_of_quarter
.unwrap_or_else(|| base_day_of_quarter(base_days));
let offset = u64::try_from(day_of_quarter - 1).ok()?;
let start_month = u32::try_from((quarter - 1) * 3 + 1).ok()?;
let start = calendar::ymd_to_days(year, start_month, 1)?;
let result = start + i64::try_from(offset).ok()?;
let (result_year, result_month, _) = calendar::civil_from_days(result);
let result_quarter = i64::from((result_month - 1) / 3 + 1);
if result_year != year || result_quarter != quarter {
return None;
}
return Some(result);
}
let month = match o.month {
Some(m) => u32::try_from(m).ok()?,
None => base_month,
};
let day = match o.day {
Some(d) => u32::try_from(d).ok()?,
None => base_day,
};
calendar::ymd_to_days(year, month, day)
}
#[must_use]
pub fn is_date_accessor(name: &str) -> bool {
matches!(
name,
"year"
| "quarter"
| "month"
| "week"
| "weekYear"
| "day"
| "ordinalDay"
| "weekDay"
| "dayOfQuarter"
)
}
#[must_use]
pub fn date_component(days: i64, name: &str) -> Option<i64> {
use crate::calendar;
let (year, month, day) = calendar::civil_from_days(days);
Some(match name {
"year" => year,
"quarter" => i64::from((month - 1) / 3 + 1),
"month" => i64::from(month),
"week" => i64::from(calendar::iso_week(days).1),
"weekYear" => calendar::iso_week(days).0,
"day" => i64::from(day),
"ordinalDay" => i64::from(calendar::ordinal(days)),
"weekDay" => i64::from(calendar::iso_weekday(days)),
"dayOfQuarter" => base_day_of_quarter(days),
_ => return None,
})
}
#[must_use]
pub fn is_time_accessor(name: &str) -> bool {
matches!(
name,
"hour" | "minute" | "second" | "millisecond" | "microsecond" | "nanosecond"
)
}
#[must_use]
pub fn is_zone_int_accessor(name: &str) -> bool {
matches!(name, "offsetMinutes" | "offsetSeconds")
}
#[must_use]
pub fn is_zone_str_accessor(name: &str) -> bool {
matches!(name, "timezone" | "offset")
}
#[must_use]
pub fn is_epoch_accessor(name: &str) -> bool {
matches!(name, "epochSeconds" | "epochMillis")
}
#[must_use]
pub fn time_component(nanos: i64, name: &str) -> Option<i64> {
let sub = nanos.rem_euclid(1_000_000_000);
Some(match name {
"hour" => nanos.div_euclid(3_600_000_000_000) % 24,
"minute" => nanos.div_euclid(60_000_000_000) % 60,
"second" => nanos.div_euclid(1_000_000_000) % 60,
"millisecond" => sub / 1_000_000,
"microsecond" => sub / 1_000,
"nanosecond" => sub,
_ => return None,
})
}
#[must_use]
pub fn zone_int_component(offset_seconds: i32, name: &str) -> Option<i64> {
Some(match name {
"offsetMinutes" => i64::from(offset_seconds) / 60,
"offsetSeconds" => i64::from(offset_seconds),
_ => return None,
})
}
#[must_use]
pub fn zone_str_component(offset_seconds: i32, zone: Option<&str>, name: &str) -> Option<String> {
let offset_str = || {
format_offset(&Offset {
seconds: offset_seconds,
has_seconds: offset_seconds % 60 != 0,
})
};
Some(match name {
"offset" => offset_str(),
"timezone" => zone.map_or_else(offset_str, str::to_string),
_ => return None,
})
}
#[must_use]
pub fn epoch_component(days: i64, nanos: i64, offset_seconds: i32, name: &str) -> Option<i64> {
let epoch_secs = days * 86_400 + nanos.div_euclid(1_000_000_000) - i64::from(offset_seconds);
Some(match name {
"epochSeconds" => epoch_secs,
"epochMillis" => epoch_secs * 1_000 + nanos.rem_euclid(1_000_000_000) / 1_000_000,
_ => return None,
})
}
#[must_use]
pub fn truncate_date(days: i64, unit: &str) -> Option<i64> {
use crate::calendar;
let (year, month, _) = calendar::civil_from_days(days);
let jan1 = |y: i64| calendar::ymd_to_days(y, 1, 1);
match unit {
"millennium" => jan1(year.div_euclid(1000) * 1000),
"century" => jan1(year.div_euclid(100) * 100),
"decade" => jan1(year.div_euclid(10) * 10),
"year" => jan1(year),
"weekYear" => calendar::from_iso_ywd(calendar::iso_week(days).0, 1, 1),
"quarter" => calendar::ymd_to_days(year, ((month - 1) / 3) * 3 + 1, 1),
"month" => calendar::ymd_to_days(year, month, 1),
"week" => Some(days - i64::from(calendar::num_days_from_monday(days))),
"day" => Some(days),
_ => None,
}
}
#[must_use]
pub fn truncate_time_nanos(nanos: i64, unit: &str) -> Option<i64> {
let floor = |step: i64| (nanos / step) * step;
match unit {
"hour" => Some(floor(3_600_000_000_000)),
"minute" => Some(floor(60_000_000_000)),
"second" => Some(floor(1_000_000_000)),
"millisecond" => Some(floor(1_000_000)),
"microsecond" => Some(floor(1_000)),
"millennium" | "century" | "decade" | "year" | "weekYear" | "quarter" | "month"
| "week" | "day" => Some(0),
_ => None,
}
}
fn base_day_of_quarter(days: i64) -> i64 {
let (year, month, _) = crate::calendar::civil_from_days(days);
let start_month = ((month - 1) / 3) * 3 + 1;
let start = crate::calendar::ymd_to_days(year, start_month, 1).unwrap_or(days);
days - start + 1
}
fn resolve_date(f: &Fields) -> Option<i64> {
use crate::calendar;
if let Some(anchor) = f_date(f, "date") {
if f.contains_key("week") {
let iso_year = match f_int(f, "year") {
Some(y) => y,
None => calendar::iso_week(anchor).0,
};
let week = u32::try_from(f_int(f, "week")?).ok()?;
let dow = match f_int(f, "dayOfWeek") {
Some(d) => u32::try_from(d).ok()?,
None => calendar::iso_weekday(anchor),
};
return calendar::from_iso_ywd(iso_year, week, dow);
}
return Some(anchor);
}
let year = f_int(f, "year")?;
if f.contains_key("month") || f.contains_key("day") {
let month = u32::try_from(f_int(f, "month").unwrap_or(1)).ok()?;
let day = u32::try_from(f_int(f, "day").unwrap_or(1)).ok()?;
calendar::ymd_to_days(year, month, day)
} else if f.contains_key("week") {
let week = u32::try_from(f_int(f, "week")?).ok()?;
let dow = u32::try_from(f_int(f, "dayOfWeek").unwrap_or(1)).ok()?;
calendar::from_iso_ywd(year, week, dow)
} else if f.contains_key("ordinalDay") {
calendar::from_ordinal(year, u32::try_from(f_int(f, "ordinalDay")?).ok()?)
} else if f.contains_key("quarter") {
let quarter = f_int(f, "quarter")?;
let day_of_quarter = f_int(f, "dayOfQuarter").unwrap_or(1);
let start_month = u32::try_from((quarter - 1) * 3 + 1).ok()?;
let start = calendar::ymd_to_days(year, start_month, 1)?;
let offset = i64::try_from(u64::try_from(day_of_quarter - 1).ok()?).ok()?;
Some(start + offset)
} else {
calendar::ymd_to_days(year, 1, 1)
}
}
fn resolve_time(f: &Fields) -> Option<TimeParts> {
let hour = u32::try_from(f_int(f, "hour").unwrap_or(0)).ok()?;
let minute = u32::try_from(f_int(f, "minute").unwrap_or(0)).ok()?;
let second = u32::try_from(f_int(f, "second").unwrap_or(0)).ok()?;
let milli = f_int(f, "millisecond").unwrap_or(0);
let micro = f_int(f, "microsecond").unwrap_or(0);
let nano = f_int(f, "nanosecond").unwrap_or(0);
let nanos = u32::try_from(milli * 1_000_000 + micro * 1_000 + nano).ok()?;
let precision = if f.contains_key("millisecond")
|| f.contains_key("microsecond")
|| f.contains_key("nanosecond")
{
TimePrecision::SubSecond
} else if f.contains_key("second") {
TimePrecision::Second
} else {
TimePrecision::Minute
};
NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?;
Some(TimeParts {
hour,
minute,
second,
nanos,
precision,
})
}
fn resolve_timezone(
f: &Fields,
date: Option<i64>,
time: &TimeParts,
) -> Option<(Offset, Option<String>)> {
match f_str(f, "timezone") {
None => Some((
Offset {
seconds: 0,
has_seconds: false,
},
None,
)),
Some(tz) if tz == "Z" || tz.starts_with('+') || tz.starts_with('-') => {
Some((parse_offset(tz)?, None))
}
Some(tz) => Some((resolve_zone_offset(date?, time, tz)?, Some(tz.to_string()))),
}
}
fn build_time_map(f: &Fields) -> Option<String> {
if !f.contains_key("hour") {
return None;
}
let time = resolve_time(f)?;
let (offset, _) = resolve_timezone(f, None, &time)?;
Some(format!("{}{}", format_time(&time), format_offset(&offset)))
}
fn build_date_time_map(f: &Fields) -> Option<String> {
use std::fmt::Write as _;
let date = resolve_date(f)?;
let time = resolve_time(f)?;
let (offset, zone) = resolve_timezone(f, Some(date), &time)?;
let mut out = format!(
"{}T{}{}",
format_date(date),
format_time(&time),
format_offset(&offset)
);
if let Some(z) = zone {
write!(out, "[{z}]").unwrap();
}
Some(out)
}
const AVG_DAYS_PER_MONTH: f64 = MONTH_SECS / DAY_SECS;
#[allow(
clippy::cast_possible_truncation,
reason = "duration component magnitudes stay within i64 for the corpus"
)]
fn approximate_duration(months_f: f64, days_f: f64, seconds_f: f64) -> DurationValue {
let months = months_f.trunc();
let days_f = days_f + (months_f - months) * AVG_DAYS_PER_MONTH;
let days = days_f.trunc();
let seconds_total = seconds_f + (days_f - days) * DAY_SECS;
let mut whole_secs = seconds_total.floor() as i64;
let mut sub_nanos = ((seconds_total - seconds_total.floor()) * 1e9).round() as i64;
if sub_nanos >= 1_000_000_000 {
whole_secs += 1;
sub_nanos -= 1_000_000_000;
}
DurationValue {
months: months as i64,
days: days as i64,
seconds: whole_secs,
nanos: sub_nanos,
}
}
fn build_duration_map(f: &Fields) -> Option<DurationValue> {
let (mut months_f, mut days_f, mut seconds_f) = (0.0, 0.0, 0.0);
let mut any = false;
for (key, factor_secs, into) in [
("years", YEAR_SECS, Unit::Month(12)),
("months", MONTH_SECS, Unit::Month(1)),
("weeks", DAY_SECS * 7.0, Unit::Day(7)),
("days", DAY_SECS, Unit::Day(1)),
("hours", 3600.0, Unit::Sec),
("minutes", 60.0, Unit::Sec),
("seconds", 1.0, Unit::Sec),
("milliseconds", 1e-3, Unit::Sec),
("microseconds", 1e-6, Unit::Sec),
("nanoseconds", 1e-9, Unit::Sec),
] {
let Some(val) = f_num(f, key) else { continue };
any = true;
match into {
#[allow(clippy::cast_precision_loss, reason = "mult is 1 or 12")]
Unit::Month(mult) => months_f += val * mult as f64,
Unit::Day(mult) => days_f += val * f64::from(mult),
Unit::Sec => seconds_f += val * factor_secs,
}
}
any.then(|| approximate_duration(months_f, days_f, seconds_f))
}
enum Unit {
Month(i64),
Day(i32),
Sec,
}
#[must_use]
pub fn render_from_epoch(seconds: i64, nanos: i64) -> Option<String> {
let dt = chrono::DateTime::from_timestamp(seconds, u32::try_from(nanos).ok()?)?;
Some(format_epoch_datetime(dt.naive_utc()))
}
#[must_use]
pub fn render_from_epoch_millis(millis: i64) -> Option<String> {
let dt = chrono::DateTime::from_timestamp_millis(millis)?;
Some(format_epoch_datetime(dt.naive_utc()))
}
fn format_epoch_datetime(naive: chrono::NaiveDateTime) -> String {
let nanos = naive.and_utc().timestamp_subsec_nanos();
let time = TimeParts {
hour: naive.hour(),
minute: naive.minute(),
second: naive.second(),
nanos,
precision: if nanos > 0 {
TimePrecision::SubSecond
} else {
TimePrecision::Second
},
};
let days = date_to_epoch_days(naive.date()).unwrap_or(0);
format!("{}T{}Z", format_date(days), format_time(&time))
}
#[must_use]
pub fn render_date(s: &str) -> Option<String> {
Some(format_date(parse_date_string(s.trim())?))
}
#[must_use]
pub fn render_local_time(s: &str) -> Option<String> {
let s = s.trim();
let (_, offset) = split_time_offset(s);
if offset.is_some() {
return None;
}
Some(format_time(&parse_time_of_day(s)?))
}
fn nanos_of_day(t: &TimeParts) -> i64 {
(i64::from(t.hour) * 3600 + i64::from(t.minute) * 60 + i64::from(t.second)) * 1_000_000_000
+ i64::from(t.nanos)
}
fn time_parts_from_nanos(nanos: i64) -> TimeParts {
let nanos = nanos.rem_euclid(86_400_000_000_000);
let secs = nanos / 1_000_000_000;
let sub = u32::try_from(nanos % 1_000_000_000).unwrap_or(0);
let precision = if sub != 0 {
TimePrecision::SubSecond
} else if secs % 60 != 0 {
TimePrecision::Second
} else {
TimePrecision::Minute
};
TimeParts {
hour: u32::try_from(secs / 3600).unwrap_or(0),
minute: u32::try_from((secs % 3600) / 60).unwrap_or(0),
second: u32::try_from(secs % 60).unwrap_or(0),
nanos: sub,
precision,
}
}
#[must_use]
pub fn localtime_nanos_from_map(fields: &Fields) -> Option<i64> {
resolve_time(fields).as_ref().map(nanos_of_day)
}
#[must_use]
pub fn localtime_nanos_from_str(s: &str) -> Option<i64> {
let s = s.trim();
let (_, offset) = split_time_offset(s);
if offset.is_some() {
return None;
}
Some(nanos_of_day(&parse_time_of_day(s)?))
}
#[must_use]
pub fn time_of_day_nanos_any(s: &str) -> Option<i64> {
let s = s.trim();
let s = s.split_once('[').map_or(s, |(head, _)| head); let s = s.split_once('T').map_or(s, |(_, time)| time); let (time, _offset) = split_time_offset(s); Some(nanos_of_day(&parse_time_of_day(time)?))
}
#[must_use]
pub fn render_localtime_nanos(nanos: i64) -> String {
format_time(&time_parts_from_nanos(nanos))
}
#[derive(Default)]
pub struct LocalTimeOverrides {
pub hour: Option<i64>,
pub minute: Option<i64>,
pub second: Option<i64>,
pub millisecond: Option<i64>,
pub microsecond: Option<i64>,
pub nanosecond: Option<i64>,
}
#[must_use]
pub fn project_localtime(base_nanos: i64, o: &LocalTimeOverrides) -> Option<i64> {
let base = time_parts_from_nanos(base_nanos);
let hour = match o.hour {
Some(h) => u32::try_from(h).ok()?,
None => base.hour,
};
let minute = match o.minute {
Some(m) => u32::try_from(m).ok()?,
None => base.minute,
};
let second = match o.second {
Some(s) => u32::try_from(s).ok()?,
None => base.second,
};
let nanos = if o.millisecond.is_some() || o.microsecond.is_some() || o.nanosecond.is_some() {
let base_milli = i64::from(base.nanos) / 1_000_000;
let base_micro = (i64::from(base.nanos) / 1_000) % 1_000;
let base_nano = i64::from(base.nanos) % 1_000;
let sub = o.millisecond.unwrap_or(base_milli) * 1_000_000
+ o.microsecond.unwrap_or(base_micro) * 1_000
+ o.nanosecond.unwrap_or(base_nano);
u32::try_from(sub).ok()?
} else {
base.nanos
};
let parts = TimeParts {
hour,
minute,
second,
nanos,
precision: TimePrecision::Minute, };
NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?; Some(nanos_of_day(&parts))
}
#[must_use]
pub fn localdatetime_parts_from_map(fields: &Fields) -> Option<(i64, i64)> {
let date = resolve_date(fields)?;
let time = resolve_time(fields)?;
Some((date, nanos_of_day(&time)))
}
#[must_use]
pub fn localdatetime_parts_from_str(s: &str) -> Option<(i64, i64)> {
let s = s.trim();
let Some((date_str, time_str)) = s.split_once('T') else {
return Some((parse_date_string(s)?, 0));
};
let date = parse_date_string(date_str)?;
let (_, offset) = split_time_offset(time_str);
if offset.is_some() {
return None;
}
Some((date, nanos_of_day(&parse_time_of_day(time_str)?)))
}
#[must_use]
pub fn render_localdatetime(date: i64, nanos_of_day: i64) -> String {
format!(
"{}T{}",
format_date(date),
render_localtime_nanos(nanos_of_day)
)
}
#[must_use]
pub fn render_time(s: &str) -> Option<String> {
let (time, offset) = split_time_offset(s.trim());
let time = parse_time_of_day(time)?;
let offset = parse_offset(offset?)?;
Some(format!("{}{}", format_time(&time), format_offset(&offset)))
}
#[must_use]
pub fn time_value_from_map(fields: &Fields) -> Option<(i64, i32)> {
if !fields.contains_key("hour") {
return None;
}
let time = resolve_time(fields)?;
let (offset, _) = resolve_timezone(fields, None, &time)?;
Some((nanos_of_day(&time), offset.seconds))
}
#[must_use]
pub fn time_value_from_str(s: &str) -> Option<(i64, i32)> {
let (time, offset) = split_time_offset(s.trim());
let offset_secs = match offset {
Some(o) => parse_offset(o)?.seconds,
None => 0,
};
Some((nanos_of_day(&parse_time_of_day(time)?), offset_secs))
}
#[must_use]
pub fn time_of_day_with_offset(s: &str) -> Option<(i64, Option<i32>)> {
let s = s.trim();
let s = s.split_once('[').map_or(s, |(head, _)| head); let s = s.split_once('T').map_or(s, |(_, t)| t); let (time, offset) = split_time_offset(s);
let nanos = nanos_of_day(&parse_time_of_day(time)?);
let offset = match offset {
Some(o) => Some(parse_offset(o)?.seconds),
None => None,
};
Some((nanos, offset))
}
#[must_use]
pub fn project_time(
base_nanos: i64,
base_offset: Option<i32>,
new_offset: Option<i32>,
) -> (i64, i32) {
match (new_offset, base_offset) {
(Some(new), Some(old)) => {
let shifted =
(base_nanos + i64::from(new - old) * 1_000_000_000).rem_euclid(86_400_000_000_000);
(shifted, new)
}
(Some(new), None) => (base_nanos, new),
(None, Some(old)) => (base_nanos, old),
(None, None) => (base_nanos, 0),
}
}
#[must_use]
pub fn render_time_value(nanos: i64, offset_seconds: i32) -> String {
let offset = Offset {
seconds: offset_seconds,
has_seconds: offset_seconds % 60 != 0,
};
format!(
"{}{}",
render_localtime_nanos(nanos),
format_offset(&offset)
)
}
type DateTimeParts = (i64, i64, i32, Option<String>);
#[must_use]
pub fn datetime_value_from_map(f: &Fields) -> Option<DateTimeParts> {
let date = resolve_date(f)?;
let time = resolve_time(f)?;
let (offset, zone) = resolve_timezone(f, Some(date), &time)?;
Some((date, nanos_of_day(&time), offset.seconds, zone))
}
#[must_use]
pub fn datetime_value_from_str(s: &str) -> Option<DateTimeParts> {
let s = s.trim();
if let Some(bracket) = s.find('[') {
let zone = s.get(bracket + 1..)?.strip_suffix(']')?;
let (date_str, time_offset) = s.get(..bracket)?.split_once('T')?;
let date = parse_date_string(date_str)?;
let (time_str, offset_str) = split_time_offset(time_offset);
let time = parse_time_of_day(time_str)?;
let offset = match offset_str {
Some(o) => parse_offset(o)?,
None => resolve_zone_offset(date, &time, zone)?,
};
return Some((
date,
nanos_of_day(&time),
offset.seconds,
Some(zone.to_string()),
));
}
let (date_str, time_offset) = s.split_once('T')?;
let date = parse_date_string(date_str)?;
let (time_str, offset_str) = split_time_offset(time_offset);
let time = parse_time_of_day(time_str)?;
Some((
date,
nanos_of_day(&time),
parse_offset(offset_str?)?.seconds,
None,
))
}
#[must_use]
pub fn time_offset_zone(s: &str) -> Option<(i64, Option<i32>, Option<String>)> {
let s = s.trim();
let (head, zone) = match s.split_once('[') {
Some((h, z)) => (h, z.strip_suffix(']').map(str::to_string)),
None => (s, None),
};
let body = head.split_once('T').map_or(head, |(_, t)| t);
let (time, offset) = split_time_offset(body);
let nanos = nanos_of_day(&parse_time_of_day(time)?);
let offset = match offset {
Some(o) => Some(parse_offset(o)?.seconds),
None => None,
};
Some((nanos, offset, zone))
}
#[must_use]
pub fn render_datetime_value(
date: i64,
nanos: i64,
offset_seconds: i32,
zone: Option<&str>,
) -> String {
use std::fmt::Write as _;
let offset = Offset {
seconds: offset_seconds,
has_seconds: offset_seconds % 60 != 0,
};
let mut out = format!(
"{}T{}{}",
format_date(date),
render_localtime_nanos(nanos),
format_offset(&offset)
);
if let Some(z) = zone {
write!(out, "[{z}]").unwrap();
}
out
}
#[must_use]
pub fn project_datetime(
date: i64,
nanos: i64,
src_offset: Option<i32>,
src_zone: Option<&str>,
new_tz: Option<&str>,
) -> Option<DateTimeParts> {
use chrono::offset::{Offset as _, TimeZone};
let base = epoch_days_to_date(date)?;
let local = base.and_time(nanos_to_naive_time(nanos)?);
let src_offset = match (src_offset, src_zone) {
(Some(o), Some(z)) => Some(
resolve_zone_offset(date, &time_parts_from_nanos(nanos), z)
.map_or(o, |off| off.seconds),
),
(o, _) => o,
};
let Some(tz) = new_tz else {
return Some(match src_offset {
Some(o) => (date, nanos, o, src_zone.map(str::to_string)),
None => (date, nanos, 0, None),
});
};
if let Some(new_off) = parse_offset_seconds(tz) {
return Some(match src_offset {
Some(src) => {
let shifted = local + chrono::Duration::seconds(i64::from(new_off - src));
(
date_to_epoch_days(shifted.date())?,
naive_time_nanos(shifted.time()),
new_off,
None,
)
}
None => (date, nanos, new_off, None),
});
}
let zone: chrono_tz::Tz = tz.parse().ok()?;
if let Some(src) = src_offset {
let utc = local - chrono::Duration::seconds(i64::from(src));
let zoned = zone.from_utc_datetime(&utc);
let off = zoned.offset().fix().local_minus_utc();
let l = zoned.naive_local();
Some((
date_to_epoch_days(l.date())?,
naive_time_nanos(l.time()),
off,
Some(tz.to_string()),
))
} else {
let off = resolve_zone_offset(date, &time_parts_from_nanos(nanos), tz)?;
Some((date, nanos, off.seconds, Some(tz.to_string())))
}
}
fn nanos_to_naive_time(nanos: i64) -> Option<NaiveTime> {
let tp = time_parts_from_nanos(nanos);
NaiveTime::from_hms_nano_opt(tp.hour, tp.minute, tp.second, tp.nanos)
}
fn naive_time_nanos(t: NaiveTime) -> i64 {
i64::from(t.num_seconds_from_midnight()) * 1_000_000_000 + i64::from(t.nanosecond())
}
#[must_use]
pub fn render_local_date_time(s: &str) -> Option<String> {
let (date, time) = s.trim().split_once('T')?;
let date = parse_date_string(date)?;
let (_, offset) = split_time_offset(time);
if offset.is_some() {
return None;
}
let time = parse_time_of_day(time)?;
Some(format!("{}T{}", format_date(date), format_time(&time)))
}
#[must_use]
pub fn render_date_time(s: &str) -> Option<String> {
let s = s.trim();
if let Some(bracket) = s.find('[') {
return render_date_time_named(s, bracket);
}
let (date, time_offset) = s.split_once('T')?;
let date = parse_date_string(date)?;
let (time, offset) = split_time_offset(time_offset);
let time = parse_time_of_day(time)?;
let offset = parse_offset(offset?)?;
Some(format!(
"{}T{}{}",
format_date(date),
format_time(&time),
format_offset(&offset)
))
}
fn render_date_time_named(s: &str, bracket: usize) -> Option<String> {
let zone = s.get(bracket + 1..)?.strip_suffix(']')?;
let head = s.get(..bracket)?;
let (date_str, time_offset) = head.split_once('T')?;
let date = parse_date_string(date_str)?;
let (time_str, offset_str) = split_time_offset(time_offset);
let time = parse_time_of_day(time_str)?;
let offset = match offset_str {
Some(o) => parse_offset(o)?,
None => resolve_zone_offset(date, &time, zone)?,
};
Some(format!(
"{}T{}{}[{}]",
format_date(date),
format_time(&time),
format_offset(&offset),
zone
))
}
fn resolve_zone_offset(days: i64, time: &TimeParts, zone: &str) -> Option<Offset> {
use chrono::offset::{LocalResult, Offset as _, TimeZone};
let tz: chrono_tz::Tz = zone.parse().ok()?;
let date = epoch_days_to_date(days)?;
let naive = date.and_time(NaiveTime::from_hms_nano_opt(
time.hour,
time.minute,
time.second,
time.nanos,
)?);
let resolved = match tz.from_local_datetime(&naive) {
LocalResult::Single(dt) | LocalResult::Ambiguous(dt, _) => dt,
LocalResult::None => return None,
};
Some(Offset {
seconds: resolved.offset().fix().local_minus_utc(),
has_seconds: true,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TimePrecision {
Minute,
Second,
SubSecond,
}
struct TimeParts {
hour: u32,
minute: u32,
second: u32,
nanos: u32,
precision: TimePrecision,
}
struct Offset {
seconds: i32,
has_seconds: bool,
}
fn parse_time_of_day(s: &str) -> Option<TimeParts> {
let (main, frac) = match s.split_once('.') {
Some((m, f)) => (m, Some(f)),
None => (s, None),
};
let (hour, minute, second, base) = if main.contains(':') {
let parts: Vec<&str> = main.split(':').collect();
match parts.as_slice() {
[h, m] => (h.parse().ok()?, m.parse().ok()?, 0, TimePrecision::Minute),
[h, m, sec] => (
h.parse().ok()?,
m.parse().ok()?,
sec.parse().ok()?,
TimePrecision::Second,
),
_ => return None,
}
} else {
if !main.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match main.len() {
2 => (main.parse().ok()?, 0, 0, TimePrecision::Minute),
4 => (
main[..2].parse().ok()?,
main[2..4].parse().ok()?,
0,
TimePrecision::Minute,
),
6 => (
main[..2].parse().ok()?,
main[2..4].parse().ok()?,
main[4..6].parse().ok()?,
TimePrecision::Second,
),
_ => return None,
}
};
let (nanos, precision) = match frac {
Some(f) if !f.is_empty() && f.len() <= 9 && f.bytes().all(|b| b.is_ascii_digit()) => {
let mut digits = f.to_string();
while digits.len() < 9 {
digits.push('0');
}
(digits.parse().ok()?, TimePrecision::SubSecond)
}
Some(_) => return None,
None => (0, base),
};
NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?;
Some(TimeParts {
hour,
minute,
second,
nanos,
precision,
})
}
fn format_time(t: &TimeParts) -> String {
match t.precision {
TimePrecision::Minute => format!("{:02}:{:02}", t.hour, t.minute),
TimePrecision::Second => format!("{:02}:{:02}:{:02}", t.hour, t.minute, t.second),
TimePrecision::SubSecond => {
let frac = format!("{:09}", t.nanos);
let frac = frac.trim_end_matches('0');
if frac.is_empty() {
format!("{:02}:{:02}:{:02}", t.hour, t.minute, t.second)
} else {
format!("{:02}:{:02}:{:02}.{frac}", t.hour, t.minute, t.second)
}
}
}
}
fn split_time_offset(s: &str) -> (&str, Option<&str>) {
for (i, c) in s.char_indices() {
if i > 0 && matches!(c, 'Z' | '+' | '-') {
return (&s[..i], Some(&s[i..]));
}
}
(s, None)
}
#[must_use]
pub fn parse_offset_seconds(s: &str) -> Option<i32> {
parse_offset(s).map(|o| o.seconds)
}
fn parse_offset(s: &str) -> Option<Offset> {
if s == "Z" {
return Some(Offset {
seconds: 0,
has_seconds: false,
});
}
let (sign, rest) = match s.strip_prefix('+') {
Some(rest) => (1, rest),
None => (-1, s.strip_prefix('-')?),
};
let (hours, minutes, secs, has_seconds): (i32, i32, i32, bool) = if rest.contains(':') {
let parts: Vec<&str> = rest.split(':').collect();
if !parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
{
return None;
}
match parts.as_slice() {
[h, m] => (h.parse().ok()?, m.parse().ok()?, 0, false),
[h, m, sec] => (h.parse().ok()?, m.parse().ok()?, sec.parse().ok()?, true),
_ => return None,
}
} else {
if !rest.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match rest.len() {
2 => (rest.parse().ok()?, 0, 0, false),
4 => (rest[..2].parse().ok()?, rest[2..4].parse().ok()?, 0, false),
6 => (
rest[..2].parse().ok()?,
rest[2..4].parse().ok()?,
rest[4..6].parse().ok()?,
true,
),
_ => return None,
}
};
if hours >= 24 || minutes >= 60 || secs >= 60 {
return None;
}
Some(Offset {
seconds: sign * (hours * 3600 + minutes * 60 + secs),
has_seconds,
})
}
fn format_offset(o: &Offset) -> String {
if o.seconds == 0 {
return "Z".to_string();
}
let sign = if o.seconds < 0 { '-' } else { '+' };
let abs = o.seconds.abs();
let (hours, minutes, secs) = (abs / 3600, (abs % 3600) / 60, abs % 60);
if o.has_seconds && secs != 0 {
format!("{sign}{hours:02}:{minutes:02}:{secs:02}")
} else {
format!("{sign}{hours:02}:{minutes:02}")
}
}
const DAY_SECS: f64 = 86_400.0;
const MONTH_SECS: f64 = 2_629_746.0;
const YEAR_SECS: f64 = 31_556_952.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DurationValue {
pub months: i64,
pub days: i64,
pub seconds: i64,
pub nanos: i64,
}
impl DurationValue {
#[must_use]
pub fn from_total_nanos(months: i64, days: i64, total_nanos: i64) -> Self {
Self {
months,
days,
seconds: total_nanos.div_euclid(1_000_000_000),
nanos: total_nanos.rem_euclid(1_000_000_000),
}
}
}
#[must_use]
pub fn render_duration(s: &str) -> Option<String> {
Some(format_duration(&parse_duration(s.trim())?))
}
#[must_use]
pub fn duration_value_from_str(s: &str) -> Option<DurationValue> {
parse_duration(s.trim())
}
#[must_use]
pub fn duration_value_from_map(fields: &Fields) -> Option<DurationValue> {
build_duration_map(fields)
}
#[must_use]
pub fn render_duration_value(dur: &DurationValue) -> String {
format_duration(dur)
}
#[must_use]
pub fn duration_component(dur: &DurationValue, name: &str) -> Option<i64> {
let (months, days, secs) = (dur.months, dur.days, dur.seconds);
let sub = dur.nanos; let total_nanos = || i128::from(secs) * 1_000_000_000 + i128::from(sub);
let v = match name {
"years" => months / 12,
"quarters" => months / 3,
"months" => months,
"monthsOfYear" => months % 12,
"monthsOfQuarter" => months % 3,
"quartersOfYear" => (months / 3) % 4,
"weeks" => days / 7,
"days" => days,
"daysOfWeek" => days % 7,
"hours" => secs / 3600,
"minutes" => secs / 60,
"seconds" => secs,
"minutesOfHour" => (secs / 60) % 60,
"secondsOfMinute" => secs % 60,
"milliseconds" => return i64::try_from(total_nanos() / 1_000_000).ok(),
"microseconds" => return i64::try_from(total_nanos() / 1_000).ok(),
"nanoseconds" => return i64::try_from(total_nanos()).ok(),
"millisecondsOfSecond" => sub / 1_000_000,
"microsecondsOfSecond" => sub / 1_000,
"nanosecondsOfSecond" => sub,
_ => return None,
};
Some(v)
}
#[must_use]
pub fn is_duration_accessor(name: &str) -> bool {
duration_component(
&DurationValue {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
},
name,
)
.is_some()
}
#[derive(Clone, Copy)]
pub enum BetweenMode {
Between,
Months,
Days,
Seconds,
}
pub type BetweenOperand = (Option<i64>, i64, Option<i32>, Option<String>);
const DAY_NANOS: i64 = 86_400_000_000_000;
type Instant = (i64, i64);
fn instant_from(days: i64, nanos: i64) -> Instant {
(
days + nanos.div_euclid(DAY_NANOS),
nanos.rem_euclid(DAY_NANOS),
)
}
fn add_signed_months(dt: Instant, m: i64) -> Instant {
(crate::calendar::add_months_to_days(dt.0, m), dt.1)
}
#[allow(
clippy::cast_possible_truncation,
reason = "the day quotient fits i64 across the full year range (±~7.3e11 days); \
i128 only guards the nanosecond intermediate"
)]
fn instant_num_days(dt1: Instant, dt2: Instant) -> i64 {
let total = i128::from(dt2.0 - dt1.0) * i128::from(DAY_NANOS) + i128::from(dt2.1 - dt1.1);
(total / i128::from(DAY_NANOS)) as i64
}
fn instant_sub_nanos(dt1: Instant, dt2: Instant) -> i64 {
(dt2.0 - dt1.0) * DAY_NANOS + (dt2.1 - dt1.1)
}
fn elapsed_seconds_between(dt1: Instant, dt2: Instant) -> DurationValue {
let day_secs = (dt2.0 - dt1.0) * 86_400;
let nanos_diff = dt2.1 - dt1.1; DurationValue {
months: 0,
days: 0,
seconds: day_secs + nanos_diff.div_euclid(1_000_000_000),
nanos: nanos_diff.rem_euclid(1_000_000_000),
}
}
fn between_instant(op: &BetweenOperand, partner: &BetweenOperand) -> Option<Instant> {
let (date, nanos, offset, _) = op;
let (p_date, _, p_offset, p_zone) = partner;
let date = date.or(*p_date)?;
let off = match offset {
Some(o) => *o,
None => match p_zone.as_deref() {
Some(z) => resolve_zone_offset(date, &time_parts_from_nanos(*nanos), z)?.seconds,
None => p_offset.unwrap_or(0),
},
};
Some(instant_from(date, *nanos - i64::from(off) * 1_000_000_000))
}
fn whole_months(dt1: Instant, dt2: Instant) -> i64 {
let (y1, m1, _) = crate::calendar::civil_from_days(dt1.0);
let (y2, m2, _) = crate::calendar::civil_from_days(dt2.0);
let mut m = (y2 - y1) * 12 + (i64::from(m2) - i64::from(m1));
let cand = add_signed_months(dt1, m);
if dt2 >= dt1 {
if cand > dt2 {
m -= 1;
}
} else if cand < dt2 {
m += 1;
}
m
}
#[allow(
clippy::single_match_else,
reason = "the both-dated arm is the substantive calendar path; the time-only \
else is the fallthrough — a match reads clearer than nested if-let"
)]
#[must_use]
pub fn duration_between(
a: &BetweenOperand,
b: &BetweenOperand,
mode: BetweenMode,
) -> Option<DurationValue> {
let (d1, n1, o1, _) = a;
let (d2, n2, o2, _) = b;
let (d1, n1, o1) = (*d1, *n1, *o1);
let (d2, n2, o2) = (*d2, *n2, *o2);
let both_off = o1.zip(o2);
let shift = |n: i64, o: i32| n - i64::from(o) * 1_000_000_000;
let zero = DurationValue {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
};
if matches!(mode, BetweenMode::Seconds) && (d1.is_some() || d2.is_some()) {
let dt1 = between_instant(a, b)?;
let dt2 = between_instant(b, a)?;
return Some(elapsed_seconds_between(dt1, dt2));
}
match (d1, d2) {
(Some(da), Some(db)) => {
let (dt1, dt2) = if let Some((oa, ob)) = both_off {
(
instant_from(da, shift(n1, oa)),
instant_from(db, shift(n2, ob)),
)
} else {
(instant_from(da, n1), instant_from(db, n2))
};
match mode {
BetweenMode::Months => Some(DurationValue {
months: whole_months(dt1, dt2),
..zero
}),
BetweenMode::Days => Some(DurationValue {
days: instant_num_days(dt1, dt2),
..zero
}),
BetweenMode::Seconds => Some(elapsed_seconds_between(dt1, dt2)),
BetweenMode::Between => {
let m = whole_months(dt1, dt2);
let rem = instant_sub_nanos(add_signed_months(dt1, m), dt2);
Some(DurationValue::from_total_nanos(
m,
rem / DAY_NANOS,
rem % DAY_NANOS,
))
}
}
}
_ => {
let diff = match (mode, both_off) {
(BetweenMode::Months | BetweenMode::Days, _) => 0,
(_, Some((oa, ob))) => shift(n2, ob) - shift(n1, oa),
(_, None) => n2 - n1,
};
Some(DurationValue::from_total_nanos(0, 0, diff))
}
}
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
reason = "scaling is inherently f64; the sub-day total is computed in f64 \
(not i64 seconds*1e9) to avoid an i64 overflow for large durations, \
and corpus durations are small whole counts"
)]
pub fn scale_duration(dur: &DurationValue, num: f64, divide: bool) -> DurationValue {
const DAY_NANOS_F: f64 = DAY_SECS * 1e9;
let op = |x: f64| if divide { x / num } else { x * num };
let m = op(dur.months as f64);
let d = op(dur.days as f64);
let n = op(dur.seconds as f64 * 1e9 + dur.nanos as f64);
let m_whole = m.trunc();
let d_total = d + (m - m_whole) * AVG_DAYS_PER_MONTH;
let d_whole = d_total.trunc();
let n_total = n + (d_total - d_whole) * DAY_NANOS_F;
#[allow(
clippy::cast_possible_truncation,
reason = "scaled component magnitudes stay within i64 for the corpus"
)]
DurationValue::from_total_nanos(m_whole as i64, d_whole as i64, n_total.trunc() as i64)
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
reason = "the whole-day quotient fits i64; i128 only guards the nanosecond product"
)]
pub fn date_plus_duration(date: i64, dur: &DurationValue) -> i64 {
let after_months = crate::calendar::add_months_to_days(date, dur.months);
let total_nanos = i128::from(dur.seconds) * 1_000_000_000 + i128::from(dur.nanos);
let extra_days = (total_nanos / i128::from(DAY_NANOS)) as i64;
after_months + dur.days + extra_days
}
#[must_use]
pub fn localtime_plus_duration(nanos_of_day: i64, dur_nanos: i64) -> i64 {
(nanos_of_day + dur_nanos).rem_euclid(DAY_NANOS)
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
reason = "the day carry and nanos-of-day both fit i64; i128 only guards the \
nanosecond product"
)]
pub fn datetime_plus_duration(date: i64, nanos_of_day: i64, dur: &DurationValue) -> (i64, i64) {
let days = crate::calendar::add_months_to_days(date, dur.months) + dur.days;
let total_nanos =
i128::from(nanos_of_day) + i128::from(dur.seconds) * 1_000_000_000 + i128::from(dur.nanos);
let day_ns = i128::from(DAY_NANOS);
let carry = (total_nanos.div_euclid(day_ns)) as i64;
let nod = (total_nanos.rem_euclid(day_ns)) as i64;
(days + carry, nod)
}
fn parse_duration(s: &str) -> Option<DurationValue> {
let rest = s.strip_prefix('P')?;
if rest.is_empty() {
return None; }
let date_seg = rest.split('T').next().unwrap_or(rest);
let alternative = rest.contains(':')
|| (date_seg.contains('-') && !date_seg.bytes().any(|b| b.is_ascii_alphabetic()));
if alternative {
return parse_duration_alternative(rest);
}
let (date_part, time_part) = match rest.split_once('T') {
Some((d, t)) => (d, Some(t)),
None => (rest, None),
};
let mut acc = (0.0_f64, 0.0_f64, 0.0_f64);
parse_designators(date_part, false, &mut acc)?;
if let Some(t) = time_part {
if t.is_empty() {
return None; }
parse_designators(t, true, &mut acc)?;
}
Some(approximate_duration(acc.0, acc.1, acc.2))
}
fn parse_duration_alternative(rest: &str) -> Option<DurationValue> {
let (date_str, time_str) = rest.split_once('T')?;
let [years, months, days] = date_str.split('-').collect::<Vec<_>>()[..] else {
return None;
};
let [hours, minutes, secs] = time_str.split(':').collect::<Vec<_>>()[..] else {
return None;
};
let years: i64 = years.parse().ok()?;
let months: i64 = months.parse().ok()?;
let days: i64 = days.parse().ok()?;
let hours: f64 = hours.parse().ok()?;
let minutes: f64 = minutes.parse().ok()?;
let secs: f64 = secs.parse().ok()?;
#[allow(
clippy::cast_possible_truncation,
reason = "alternative-form times are small, well within i64 nanos"
)]
Some(DurationValue::from_total_nanos(
years * 12 + months,
days,
((hours * 3600.0 + minutes * 60.0 + secs) * 1e9).round() as i64,
))
}
fn parse_designators(s: &str, in_time: bool, acc: &mut (f64, f64, f64)) -> Option<()> {
let mut chars = s.chars().peekable();
while chars.peek().is_some() {
let mut num = String::new();
if matches!(chars.peek(), Some('-' | '+')) {
num.push(chars.next()?);
}
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() || c == '.' {
num.push(c);
chars.next();
} else {
break;
}
}
if num.is_empty() || num == "-" || num == "+" {
return None;
}
let unit = chars.next()?;
let val: f64 = num.parse().ok()?;
match (in_time, unit) {
(false, 'Y') => acc.0 += val * 12.0,
(false, 'M') => acc.0 += val,
(false, 'W') => acc.1 += val * 7.0,
(false, 'D') => acc.1 += val,
(true, 'H') => acc.2 += val * 3600.0,
(true, 'M') => acc.2 += val * 60.0,
(true, 'S') => acc.2 += val,
_ => return None,
}
}
Some(())
}
fn format_duration(dur: &DurationValue) -> String {
use std::fmt::Write as _;
let years = dur.months / 12;
let months = dur.months % 12;
let days = dur.days;
let (mut secs, mut sub_ns) = (dur.seconds, dur.nanos);
if secs < 0 && sub_ns > 0 {
secs += 1;
sub_ns -= 1_000_000_000;
}
let hours = secs / 3600;
let rem = secs % 3600;
let minutes = rem / 60;
let whole_secs = rem % 60;
let mut out = String::from("P");
if years != 0 {
write!(out, "{years}Y").unwrap();
}
if months != 0 {
write!(out, "{months}M").unwrap();
}
if days != 0 {
write!(out, "{days}D").unwrap();
}
let mut time = String::new();
if hours != 0 {
write!(time, "{hours}H").unwrap();
}
if minutes != 0 {
write!(time, "{minutes}M").unwrap();
}
if whole_secs != 0 || sub_ns != 0 {
write!(time, "{}S", format_seconds_int(whole_secs, sub_ns)).unwrap();
}
if !time.is_empty() {
out.push('T');
out.push_str(&time);
}
if out == "P" {
out.push_str("T0S");
}
out
}
fn format_seconds_int(whole_secs: i64, sub_ns: i64) -> String {
if sub_ns == 0 {
return whole_secs.to_string();
}
let neg = whole_secs < 0 || sub_ns < 0;
let mut frac = format!("{:09}", sub_ns.unsigned_abs());
while frac.ends_with('0') {
frac.pop();
}
format!(
"{}{}.{}",
if neg { "-" } else { "" },
whole_secs.unsigned_abs(),
frac
)
}
#[must_use]
pub fn parse_date_string(input: &str) -> Option<i64> {
use crate::calendar;
let s = input.trim();
let (neg, had_sign, s) = if let Some(r) = s.strip_prefix('-') {
(true, true, r)
} else if let Some(r) = s.strip_prefix('+') {
(false, true, r)
} else {
(false, false, s)
};
let signed = |y: i64| if neg { -y } else { y };
if let Some(wpos) = s.find('W') {
let year: i64 = s[..wpos].trim_end_matches('-').parse().ok()?;
let rest: String = s[wpos + 1..].chars().filter(|c| *c != '-').collect();
if rest.len() < 2 || !rest.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let week: u32 = rest[..2].parse().ok()?;
let weekday: u32 = if rest.len() > 2 {
rest[2..].parse().ok()?
} else {
1
};
return calendar::from_iso_ywd(signed(year), week, weekday);
}
if let Some((head, tail)) = s.split_once('-') {
let year: i64 = head.parse().ok()?;
return match tail.split_once('-') {
Some((month, day)) => {
calendar::ymd_to_days(signed(year), month.parse().ok()?, day.parse().ok()?)
}
None if tail.len() == 3 => calendar::from_ordinal(signed(year), tail.parse().ok()?),
None => calendar::ymd_to_days(signed(year), tail.parse().ok()?, 1),
};
}
if !had_sign && !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
let year: i64 = s.get(..4)?.parse().ok()?;
return match s.len() {
8 => calendar::ymd_to_days(signed(year), s[4..6].parse().ok()?, s[6..8].parse().ok()?),
7 => calendar::from_ordinal(signed(year), s[4..].parse().ok()?),
6 => calendar::ymd_to_days(signed(year), s[4..6].parse().ok()?, 1),
4 => calendar::ymd_to_days(signed(year), 1, 1),
_ => None,
};
}
None
}
#[must_use]
pub fn format_date(days: i64) -> String {
crate::calendar::format_date(days)
}
#[cfg(test)]
mod tests {
use super::*;
fn d(s: &str) -> String {
format_date(parse_date_string(s).expect("should parse"))
}
#[test]
fn calendar_forms() {
assert_eq!(d("2015-07-21"), "2015-07-21");
assert_eq!(d("20150721"), "2015-07-21");
}
#[test]
fn truncate_date_units() {
let t = |s: &str, unit: &str| {
format_date(
truncate_date(parse_date_string(s).expect("parse"), unit).expect("truncate"),
)
};
assert_eq!(t("2017-10-11", "millennium"), "2000-01-01");
assert_eq!(t("1984-10-11", "century"), "1900-01-01");
assert_eq!(t("1984-10-11", "decade"), "1980-01-01");
assert_eq!(t("1984-10-11", "year"), "1984-01-01");
assert_eq!(t("1984-02-01", "weekYear"), "1984-01-02");
assert_eq!(t("1984-11-11", "quarter"), "1984-10-01");
assert_eq!(t("1984-10-11", "month"), "1984-10-01");
assert_eq!(t("1984-10-11", "week"), "1984-10-08");
assert_eq!(t("1984-10-11", "day"), "1984-10-11");
assert!(truncate_date(parse_date_string("1984-10-11").unwrap(), "bogus").is_none());
}
#[test]
fn extreme_year_date_operations_are_range_complete() {
let hi = parse_date_string("+999999999-06-15").expect("parse extreme +year");
assert_eq!(date_component(hi, "year"), Some(999_999_999));
assert_eq!(date_component(hi, "month"), Some(6));
assert_eq!(date_component(hi, "day"), Some(15));
assert_eq!(date_component(hi, "quarter"), Some(2));
assert!(date_component(hi, "weekDay").is_some());
assert!(date_component(hi, "ordinalDay").is_some());
assert!(date_component(hi, "dayOfQuarter").is_some());
assert_eq!(
format_date(truncate_date(hi, "year").unwrap()),
"+999999999-01-01"
);
assert_eq!(
format_date(truncate_date(hi, "month").unwrap()),
"+999999999-06-01"
);
let projected = project_date(
hi,
&DateOverrides {
day: Some(1),
..DateOverrides::default()
},
);
assert_eq!(format_date(projected.unwrap()), "+999999999-06-01");
let lo = parse_date_string("-999999999-01-01").expect("parse extreme -year");
assert_eq!(date_component(lo, "year"), Some(-999_999_999));
}
#[test]
fn duration_subsecond_accessors_dont_overflow_on_huge_spans() {
let huge = DurationValue::from_total_nanos(0, 0, 0);
let huge = DurationValue {
seconds: 31_556_951_999_913_600, ..huge
};
assert_eq!(duration_component(&huge, "nanoseconds"), None);
assert_eq!(duration_component(&huge, "milliseconds"), None);
assert_eq!(duration_component(&huge, "microseconds"), None);
assert_eq!(
duration_component(&huge, "seconds"),
Some(31_556_951_999_913_600)
);
let small = DurationValue::from_total_nanos(0, 0, 1_500_000_000);
assert_eq!(duration_component(&small, "milliseconds"), Some(1_500));
}
#[test]
fn signed_basic_dateless_form_is_rejected_not_misparsed() {
assert_eq!(parse_date_string("+10000202"), None);
assert_eq!(parse_date_string("-10000202"), None);
assert_eq!(
format_date(parse_date_string("+10000-202").expect("extended ordinal")),
"+10000-07-20"
);
}
#[test]
fn truncate_time_units() {
let n = 45_074_645_876_123_i64;
let r =
|unit: &str| render_localtime_nanos(truncate_time_nanos(n, unit).expect("truncate"));
assert_eq!(r("hour"), "12:00");
assert_eq!(r("minute"), "12:31");
assert_eq!(r("second"), "12:31:14");
assert_eq!(r("millisecond"), "12:31:14.645");
assert_eq!(r("microsecond"), "12:31:14.645876");
assert_eq!(r("day"), "00:00");
assert_eq!(r("year"), "00:00");
assert!(truncate_time_nanos(n, "bogus").is_none());
}
#[test]
fn project_localtime_keeps_unspecified_subsecond_groups() {
let base = 45_074_645_000_000_i64; let only_ns = LocalTimeOverrides {
hour: None,
minute: None,
second: None,
millisecond: None,
microsecond: None,
nanosecond: Some(2),
};
assert_eq!(
render_localtime_nanos(project_localtime(base, &only_ns).unwrap()),
"12:31:14.645000002"
);
let ms = LocalTimeOverrides {
millisecond: Some(123),
..only_ns
};
assert_eq!(
render_localtime_nanos(project_localtime(base, &ms).unwrap()),
"12:31:14.123000002"
);
}
#[test]
fn duration_str_parses_negative_components_round_trip() {
let secs = 16 * 3600; let v = DurationValue::from_total_nanos(149, -14, i64::from(secs) * 1_000_000_000);
assert_eq!(duration_value_from_str("P12Y5M-14DT16H"), Some(v));
assert_eq!(duration_value_from_str(&render_duration_value(&v)), Some(v));
}
#[test]
fn duration_construction_carries_rounded_up_subsecond() {
let fields: Fields =
[("seconds".to_string(), TemporalField::Float(0.999_999_999_6))].into();
let dv = build_duration_map(&fields).expect("duration");
assert!(dv.nanos >= 0 && dv.nanos < 1_000_000_000, "nanos canonical");
assert_eq!((dv.seconds, dv.nanos), (1, 0));
assert_eq!(render_duration_value(&dv), "PT1S");
}
#[test]
fn duration_render_preserves_subsecond_for_large_spans() {
let nanos = (278_565_i64 * 3600 + 45 * 60 + 22) * 1_000_000_000 + 142_000_000;
let dv = |n: i64| DurationValue::from_total_nanos(0, 0, n);
assert_eq!(render_duration_value(&dv(nanos)), "PT278565H45M22.142S");
assert_eq!(render_duration_value(&dv(-1_999_000_000)), "PT-1.999S");
assert_eq!(render_duration_value(&dv(-1_000_000)), "PT-0.001S");
}
#[test]
fn duration_scale_matches_opencypher() {
let base = DurationValue::from_total_nanos(149, 14, 58_390_000_000_001);
assert_eq!(
scale_duration(&base, 2.0, false),
DurationValue::from_total_nanos(298, 28, 116_780_000_000_002),
"P24Y10M28DT32H26M20.000000002S"
);
assert_eq!(
scale_duration(&base, 2.0, true),
DurationValue::from_total_nanos(74, 22, 48_068_000_000_000),
"P6Y2M22DT13H21M8S"
);
assert_eq!(
scale_duration(&base, 0.5, false),
DurationValue::from_total_nanos(74, 22, 48_068_000_000_000)
);
}
#[test]
fn duration_arithmetic() {
let d = |s: &str| parse_date_string(s).unwrap();
let nanos = (16 * 3600 + 12 * 60 + 70) * 1_000_000_000 + 2;
let dur = |m, dd, n| DurationValue::from_total_nanos(m, dd, n);
assert_eq!(
date_plus_duration(d("1984-10-11"), &dur(149, 14, nanos)),
d("1997-03-25")
);
assert_eq!(
date_plus_duration(d("1984-10-11"), &dur(-149, -14, -nanos)),
d("1972-04-27")
);
let big = 122_293_500_000_000_i64;
assert_eq!(
date_plus_duration(d("1984-10-11"), &dur(155, 29, big)),
d("1997-10-11")
);
assert_eq!(
date_plus_duration(d("1984-10-11"), &dur(-155, -29, -big)),
d("1971-10-12")
);
let day = 86_400_000_000_000_i64;
assert_eq!(
localtime_plus_duration(23 * 3_600_000_000_000, 2 * 3_600_000_000_000),
3_600_000_000_000
);
assert_eq!(localtime_plus_duration(0, -1), day - 1);
let (date, nod) = datetime_plus_duration(
d("1984-10-11"),
12 * 3_600_000_000_000,
&dur(0, 0, 13 * 3_600_000_000_000),
);
assert_eq!((date, nod), (d("1984-10-12"), 3_600_000_000_000)); }
#[test]
fn time_value_from_str_defaults_offset_to_utc() {
assert_eq!(
time_value_from_str("14:30"),
Some((14 * 3_600_000_000_000 + 30 * 60_000_000_000, 0))
);
assert_eq!(
time_value_from_str("14:30+01:00"),
Some((14 * 3_600_000_000_000 + 30 * 60_000_000_000, 3600))
);
}
#[test]
fn duration_between_units() {
let date = |s: &str| (Some(parse_date_string(s).unwrap()), 0_i64, None, None);
let lt = |h: i64, m: i64| (None, (h * 3600 + m * 60) * 1_000_000_000, None, None);
let dv = |m, dd, n| DurationValue::from_total_nanos(m, dd, n);
assert_eq!(
duration_between(
&date("1984-10-11"),
&date("2015-06-24"),
BetweenMode::Between
),
Some(dv(368, 13, 0)) );
assert_eq!(
duration_between(
&date("1984-10-11"),
&date("2015-06-24"),
BetweenMode::Months
),
Some(dv(368, 0, 0))
);
assert_eq!(
duration_between(&date("1984-10-11"), &date("2015-06-24"), BetweenMode::Days),
Some(dv(0, 11213, 0))
);
assert_eq!(
duration_between(<(14, 30), <(16, 30), BetweenMode::Between),
Some(dv(0, 0, 2 * 3_600_000_000_000))
);
let r = duration_between(
&date("2015-06-24"),
&date("1984-10-11"),
BetweenMode::Between,
)
.unwrap();
assert!(r.months <= 0 && r.days <= 0 && r.seconds <= 0 && r.nanos <= 0);
let ldt = |s: &str| {
let (d, t) = s.split_once('T').unwrap();
(
Some(parse_date_string(d).unwrap()),
nanos_of_day(&parse_time_of_day(t).unwrap()),
None,
None,
)
};
assert_eq!(
duration_between(
&ldt("2018-01-02T10:00:00"),
&ldt("2018-01-01T12:00:00"),
BetweenMode::Between,
),
Some(dv(0, 0, -22 * 3_600_000_000_000)),
"a 22h backward span is PT-22H, not P-1M30DT2H"
);
}
#[test]
fn duration_inseconds_dst_named_zone() {
let d = |y, m, day| crate::calendar::ymd_to_days(y, m, day).unwrap();
let h = |n: i64| n * 3_600_000_000_000;
let zoned = (
Some(d(2017, 10, 29)),
0_i64,
Some(7200_i32),
Some("Europe/Stockholm".to_string()),
);
let unzoned_0429_04 = (Some(d(2017, 10, 29)), h(4), None, None);
assert_eq!(
duration_between(&zoned, &unzoned_0429_04, BetweenMode::Seconds),
Some(DurationValue::from_total_nanos(0, 0, h(5))),
"00:00 (+02) → 04:00 across the fall-back is 5 real hours"
);
let next_date = (Some(d(2017, 10, 30)), 0_i64, None, None);
assert_eq!(
duration_between(&zoned, &next_date, BetweenMode::Seconds),
Some(DurationValue::from_total_nanos(0, 0, h(25)))
);
}
#[test]
fn localtime_str_round_trip() {
let r = |s: &str| render_localtime_nanos(localtime_nanos_from_str(s).expect("parse"));
assert_eq!(r("21:40:32.142"), "21:40:32.142");
assert_eq!(r("214032.142"), "21:40:32.142");
assert_eq!(r("21:40:32"), "21:40:32");
assert_eq!(r("21:40"), "21:40");
assert_eq!(r("21"), "21:00");
assert!(localtime_nanos_from_str("21:40:32+01:00").is_none());
}
#[test]
fn localtime_of_day_from_any_temporal_string() {
let r = |s: &str| render_localtime_nanos(time_of_day_nanos_any(s).expect("parse"));
assert_eq!(r("12:31:14.645876123"), "12:31:14.645876123"); assert_eq!(r("12:31:14.645876+01:00"), "12:31:14.645876"); assert_eq!(r("1984-10-11T12:31:14.645"), "12:31:14.645"); assert_eq!(r("1984-10-11T12:00+01:00[Europe/Stockholm]"), "12:00"); }
#[test]
fn localtime_projection_overrides() {
let base = localtime_nanos_from_str("12:31:14.645876123").unwrap();
let proj =
|o: &LocalTimeOverrides| render_localtime_nanos(project_localtime(base, o).unwrap());
assert_eq!(proj(&LocalTimeOverrides::default()), "12:31:14.645876123");
assert_eq!(
proj(&LocalTimeOverrides {
second: Some(42),
..Default::default()
}),
"12:31:42.645876123"
);
}
#[test]
fn localdatetime_str_round_trip() {
let r = |s: &str| {
let (d, n) = localdatetime_parts_from_str(s).expect("parse");
render_localdatetime(d, n)
};
assert_eq!(r("2015-07-21T21:40:32.142"), "2015-07-21T21:40:32.142");
assert_eq!(r("2015-W30-2T214032.142"), "2015-07-21T21:40:32.142");
assert_eq!(r("2015-202T21:40:32"), "2015-07-21T21:40:32");
assert_eq!(r("2015T214032"), "2015-01-01T21:40:32");
assert_eq!(r("20150721T21:40"), "2015-07-21T21:40");
assert_eq!(r("2015202T21"), "2015-07-21T21:00");
assert!(localdatetime_parts_from_str("2015-07-21T21:40:32+01:00").is_none());
}
#[test]
fn localdatetime_full_year_range() {
let r = |s: &str| {
let (d, n) = localdatetime_parts_from_str(s).expect("parse");
render_localdatetime(d, n)
};
assert_eq!(
r("0001-01-01T01:01:01.000000001"),
"0001-01-01T01:01:01.000000001"
);
assert_eq!(
r("9999-09-09T09:59:59.999999999"),
"9999-09-09T09:59:59.999999999"
);
}
#[test]
fn time_str_round_trip() {
let r = |s: &str| {
let (n, o) = time_value_from_str(s).expect("parse");
render_time_value(n, o)
};
assert_eq!(r("21:40:32.142+0100"), "21:40:32.142+01:00");
assert_eq!(r("214032.142Z"), "21:40:32.142Z");
assert_eq!(r("214032-0100"), "21:40:32-01:00");
assert_eq!(r("21:40-01:30"), "21:40-01:30");
assert_eq!(r("2140-00:00"), "21:40Z"); assert_eq!(r("2140-02"), "21:40-02:00");
assert_eq!(r("22+18:00"), "22:00+18:00");
assert_eq!(r("21:40:32"), "21:40:32Z");
}
#[test]
fn time_projection_zone_shift_vs_attach() {
let base = |s: &str| time_of_day_with_offset(s).expect("parse");
let ren = |(n, o): (i64, i32)| render_time_value(n, o);
let plus5 = Some(5 * 3600);
let (n, off) = base("12:31:14.645876123");
assert_eq!(ren(project_time(n, off, plus5)), "12:31:14.645876123+05:00");
let (n, off) = base("12:31:14.645876+01:00");
assert_eq!(ren(project_time(n, off, plus5)), "16:31:14.645876+05:00");
let (n, off) = base("12:31:14.645876+01:00");
assert_eq!(ren(project_time(n, off, None)), "12:31:14.645876+01:00");
let (n, off) = base("12:31:14.645876123");
assert_eq!(ren(project_time(n, off, None)), "12:31:14.645876123Z");
}
#[test]
fn datetime_str_round_trip() {
let r = |s: &str| {
let (d, n, o, z) = datetime_value_from_str(s).expect("parse");
render_datetime_value(d, n, o, z.as_deref())
};
assert_eq!(
r("2015-07-21T21:40:32.142+01:00"),
"2015-07-21T21:40:32.142+01:00"
);
assert_eq!(r("2015-07-21T21:40Z"), "2015-07-21T21:40Z");
assert_eq!(
r("2017-08-08T12:31:14.645876123[Europe/Stockholm]"),
"2017-08-08T12:31:14.645876123+02:00[Europe/Stockholm]"
);
}
#[test]
fn datetime_projection_shift_and_attach() {
let render = |p: Option<DateTimeParts>| {
let (d, n, o, z) = p.unwrap();
render_datetime_value(d, n, o, z.as_deref())
};
let date = parse_date_string("1984-10-11").unwrap();
let nanos = nanos_of_day(&parse_time_of_day("12:31:14.645876").unwrap());
assert_eq!(
render(project_datetime(
date,
nanos,
Some(3600),
None,
Some("+05:00")
)),
"1984-10-11T16:31:14.645876+05:00"
);
assert_eq!(
render(project_datetime(
date,
nanos,
Some(3600),
None,
Some("Pacific/Honolulu")
)),
"1984-10-11T01:31:14.645876-10:00[Pacific/Honolulu]"
);
assert_eq!(
render(project_datetime(
date,
nanos,
None,
None,
Some("Pacific/Honolulu")
)),
"1984-10-11T12:31:14.645876-10:00[Pacific/Honolulu]"
);
assert_eq!(
render(project_datetime(
date,
nanos,
Some(3600),
Some("Europe/Stockholm"),
None
)),
"1984-10-11T12:31:14.645876+01:00[Europe/Stockholm]"
);
}
#[test]
fn partial_forms() {
assert_eq!(d("2015-07"), "2015-07-01");
assert_eq!(d("201507"), "2015-07-01");
assert_eq!(d("2015"), "2015-01-01");
}
#[test]
fn week_forms() {
assert_eq!(d("2015-W30-2"), "2015-07-21");
assert_eq!(d("2015W302"), "2015-07-21");
assert_eq!(d("2015-W30"), "2015-07-20");
assert_eq!(d("2015W30"), "2015-07-20");
}
#[test]
fn ordinal_forms() {
assert_eq!(d("2015-202"), "2015-07-21");
assert_eq!(d("2015202"), "2015-07-21");
}
#[test]
fn rejects_garbage() {
assert!(parse_date_string("not-a-date").is_none());
assert!(parse_date_string("2015-13-01").is_none());
assert!(parse_date_string("").is_none());
}
#[test]
fn non_ascii_week_does_not_panic() {
assert!(parse_date_string("2015W€0").is_none());
assert!(parse_date_string("2015W3é").is_none());
}
#[test]
fn local_time_forms() {
let r = |s| render_local_time(s).unwrap();
assert_eq!(r("21:40:32.142"), "21:40:32.142");
assert_eq!(r("214032.142"), "21:40:32.142");
assert_eq!(r("21:40:32"), "21:40:32");
assert_eq!(r("214032"), "21:40:32");
assert_eq!(r("21:40"), "21:40");
assert_eq!(r("2140"), "21:40");
assert_eq!(r("21"), "21:00");
assert!(render_local_time("21:40Z").is_none());
}
#[test]
fn time_forms() {
let r = |s| render_time(s).unwrap();
assert_eq!(r("21:40:32.142+0100"), "21:40:32.142+01:00");
assert_eq!(r("214032.142Z"), "21:40:32.142Z");
assert_eq!(r("21:40:32+01:00"), "21:40:32+01:00");
assert_eq!(r("214032-0100"), "21:40:32-01:00");
assert_eq!(r("21:40-01:30"), "21:40-01:30");
assert_eq!(r("2140-00:00"), "21:40Z"); assert_eq!(r("2140-02"), "21:40-02:00");
assert_eq!(r("22+18:00"), "22:00+18:00");
assert!(render_time("21:40").is_none());
}
#[test]
fn local_date_time_forms() {
let r = |s| render_local_date_time(s).unwrap();
assert_eq!(r("2015-07-21T21:40:32.142"), "2015-07-21T21:40:32.142");
assert_eq!(r("2015-W30-2T214032.142"), "2015-07-21T21:40:32.142");
assert_eq!(r("2015-202T21:40:32"), "2015-07-21T21:40:32");
assert_eq!(r("2015T214032"), "2015-01-01T21:40:32");
assert_eq!(r("20150721T21:40"), "2015-07-21T21:40");
assert_eq!(r("2015-W30T2140"), "2015-07-20T21:40");
assert_eq!(r("2015202T21"), "2015-07-21T21:00");
}
#[test]
fn date_time_forms() {
let r = |s| render_date_time(s).unwrap();
assert_eq!(
r("2015-07-21T21:40:32.142+0100"),
"2015-07-21T21:40:32.142+01:00"
);
assert_eq!(r("2015-W30-2T214032.142Z"), "2015-07-21T21:40:32.142Z");
assert_eq!(r("2015-202T21:40:32+01:00"), "2015-07-21T21:40:32+01:00");
assert_eq!(r("2015T214032-0100"), "2015-01-01T21:40:32-01:00");
assert_eq!(r("20150721T21:40-01:30"), "2015-07-21T21:40-01:30");
assert_eq!(r("2015-W30T2140-00:00"), "2015-07-20T21:40Z");
assert_eq!(r("2015-W30T2140-02"), "2015-07-20T21:40-02:00");
assert_eq!(r("2015202T21+18:00"), "2015-07-21T21:00+18:00");
}
#[test]
fn date_time_named_zone_forms() {
let r = |s| render_date_time(s).unwrap();
assert_eq!(
r("2015-07-21T21:40:32.142+02:00[Europe/Stockholm]"),
"2015-07-21T21:40:32.142+02:00[Europe/Stockholm]"
);
assert_eq!(
r("2015-07-21T21:40:32.142+0845[Australia/Eucla]"),
"2015-07-21T21:40:32.142+08:45[Australia/Eucla]"
);
assert_eq!(
r("2015-07-21T21:40:32.142-04[America/New_York]"),
"2015-07-21T21:40:32.142-04:00[America/New_York]"
);
assert_eq!(
r("2015-07-21T21:40:32.142[Europe/London]"),
"2015-07-21T21:40:32.142+01:00[Europe/London]"
);
assert_eq!(
r("1818-07-21T21:40:32.142[Europe/Stockholm]"),
"1818-07-21T21:40:32.142+00:53:28[Europe/Stockholm]"
);
}
#[test]
fn duration_forms() {
let r = |s| render_duration(s).unwrap();
assert_eq!(r("P14DT16H12M"), "P14DT16H12M");
assert_eq!(r("P5M1.5D"), "P5M1DT12H");
assert_eq!(r("P0.75M"), "P22DT19H51M49.5S");
assert_eq!(r("PT0.75M"), "PT45S");
assert_eq!(r("P2.5W"), "P17DT12H");
assert_eq!(r("P12Y5M14DT16H12M70S"), "P12Y5M14DT16H13M10S");
assert_eq!(r("P2012-02-02T14:37:21.545"), "P2012Y2M2DT14H37M21.545S");
}
fn fields(pairs: &[(&str, TemporalField)]) -> Fields {
pairs
.iter()
.map(|(k, v)| {
(
(*k).to_string(),
match v {
TemporalField::Int(n) => TemporalField::Int(*n),
TemporalField::Float(x) => TemporalField::Float(*x),
TemporalField::Str(s) => TemporalField::Str(s.clone()),
TemporalField::Date(d) => TemporalField::Date(*d),
},
)
})
.collect()
}
fn int(n: i64) -> TemporalField {
TemporalField::Int(n)
}
#[test]
fn date_map_forms() {
let r = |p: &[(&str, TemporalField)]| render_temporal_map("date", &fields(p)).unwrap();
assert_eq!(
r(&[("year", int(1984)), ("month", int(10)), ("day", int(11))]),
"1984-10-11"
);
assert_eq!(r(&[("year", int(1984)), ("month", int(10))]), "1984-10-01");
assert_eq!(
r(&[
("year", int(1984)),
("week", int(10)),
("dayOfWeek", int(3))
]),
"1984-03-07"
);
assert_eq!(r(&[("year", int(1984)), ("week", int(10))]), "1984-03-05");
assert_eq!(r(&[("year", int(1984))]), "1984-01-01");
assert_eq!(
r(&[("year", int(1984)), ("ordinalDay", int(202))]),
"1984-07-20"
);
assert_eq!(
r(&[
("year", int(1984)),
("quarter", int(3)),
("dayOfQuarter", int(45))
]),
"1984-08-14"
);
assert_eq!(r(&[("year", int(1817)), ("week", int(1))]), "1816-12-30");
}
#[test]
fn date_map_anchored_week() {
let anchor = TemporalField::Date(parse_date_string("1816-12-31").unwrap());
let out =
render_temporal_map("date", &fields(&[("date", anchor), ("week", int(2))])).unwrap();
assert_eq!(out, "1817-01-07");
}
#[test]
fn time_and_subsecond_maps() {
let lt =
|p: &[(&str, TemporalField)]| render_temporal_map("localtime", &fields(p)).unwrap();
assert_eq!(
lt(&[
("hour", int(12)),
("minute", int(31)),
("second", int(14)),
("nanosecond", int(789)),
("millisecond", int(123)),
("microsecond", int(456)),
]),
"12:31:14.123456789"
);
assert_eq!(lt(&[("hour", int(12))]), "12:00");
let t = |p: &[(&str, TemporalField)]| render_temporal_map("time", &fields(p)).unwrap();
assert_eq!(t(&[("hour", int(12)), ("minute", int(31))]), "12:31Z");
assert_eq!(
t(&[
("hour", int(12)),
("minute", int(34)),
("second", int(56)),
("timezone", TemporalField::Str("+02:05:59".into())),
]),
"12:34:56+02:05:59"
);
}
#[test]
fn datetime_maps_with_zones() {
let dt = |p: &[(&str, TemporalField)]| render_temporal_map("datetime", &fields(p)).unwrap();
assert_eq!(
dt(&[("year", int(1984)), ("month", int(10)), ("day", int(11))]),
"1984-10-11T00:00Z"
);
assert_eq!(
dt(&[
("year", int(1984)),
("ordinalDay", int(202)),
("hour", int(12)),
("timezone", TemporalField::Str("+01:00".into())),
]),
"1984-07-20T12:00+01:00"
);
assert_eq!(
dt(&[
("year", int(1984)),
("ordinalDay", int(202)),
("hour", int(12)),
("timezone", TemporalField::Str("Europe/Stockholm".into())),
]),
"1984-07-20T12:00+02:00[Europe/Stockholm]"
);
assert_eq!(
dt(&[
("year", int(1984)),
("month", int(10)),
("day", int(11)),
("hour", int(12)),
("timezone", TemporalField::Str("Europe/Stockholm".into())),
]),
"1984-10-11T12:00+01:00[Europe/Stockholm]"
);
}
#[test]
fn duration_maps() {
let d = |p: &[(&str, TemporalField)]| render_temporal_map("duration", &fields(p)).unwrap();
assert_eq!(
d(&[("days", int(14)), ("hours", int(16)), ("minutes", int(12))]),
"P14DT16H12M"
);
assert_eq!(
d(&[("months", int(5)), ("days", TemporalField::Float(1.5))]),
"P5M1DT12H"
);
assert_eq!(
d(&[("months", TemporalField::Float(0.75))]),
"P22DT19H51M49.5S"
);
assert_eq!(d(&[("weeks", TemporalField::Float(2.5))]), "P17DT12H");
assert_eq!(
d(&[
("days", int(14)),
("seconds", int(70)),
("nanoseconds", int(1))
]),
"P14DT1M10.000000001S"
);
assert_eq!(
d(&[("minutes", TemporalField::Float(1.5)), ("seconds", int(1))]),
"PT1M31S"
);
}
#[test]
fn epoch_constructors() {
assert_eq!(
render_from_epoch(416_779, 999_999_999).unwrap(),
"1970-01-05T19:46:19.999999999Z"
);
assert_eq!(
render_from_epoch_millis(237_821_673_987).unwrap(),
"1977-07-15T13:34:33.987Z"
);
}
#[test]
fn rejects_malformed_temporals() {
assert!(render_local_time("21:40:32.1234567890").is_none());
assert!(render_time("21:40+01:-30").is_none());
assert!(render_time("21:40+24:00").is_none());
assert!(render_duration("P").is_none());
}
#[test]
fn date_components() {
let d = parse_date_string("1984-10-11").unwrap();
let c = |n| date_component(d, n).unwrap();
assert_eq!(c("year"), 1984);
assert_eq!(c("quarter"), 4);
assert_eq!(c("month"), 10);
assert_eq!(c("week"), 41);
assert_eq!(c("weekYear"), 1984);
assert_eq!(c("day"), 11);
assert_eq!(c("ordinalDay"), 285);
assert_eq!(c("weekDay"), 4);
assert_eq!(c("dayOfQuarter"), 11);
let d2 = parse_date_string("1984-01-01").unwrap();
assert_eq!(date_component(d2, "weekYear").unwrap(), 1983);
assert_eq!(date_component(d2, "week").unwrap(), 52);
assert_eq!(date_component(d2, "weekDay").unwrap(), 7);
assert!(date_component(d, "bogus").is_none());
}
}