use super::IsoSpec;
use crate::core::TzSetting;
use crate::error::{Result, TemplateError};
use crate::value::{DateType, DateValue};
use chrono::{DateTime, FixedOffset, NaiveDate, TimeZone, Utc};
pub fn parse_iso_like(
s: &str,
kind: DateType,
spec: &IsoSpec,
env_tz: &TzSetting,
xs_mode: bool,
) -> Result<DateValue> {
let tz = match spec.force_utc {
Some(true) | None => TzSetting::Fixed(FixedOffset::east_opt(0).unwrap()),
Some(false) => *env_tz,
};
let mut parsed = match kind {
DateType::Date => parse_date(s, &tz, xs_mode)?,
DateType::Time => parse_time(s, &tz, xs_mode)?,
DateType::DateTime | DateType::Unknown => parse_date_time(s, &tz, xs_mode)?,
};
if parsed.kind != kind && kind != DateType::Unknown {
parsed.kind = kind;
}
Ok(parsed)
}
fn parse_offset(s: &str, xs_mode: bool) -> Result<FixedOffset> {
if s == "Z" {
return Ok(FixedOffset::east_opt(0).unwrap());
}
let b = s.as_bytes();
if b.is_empty() || (b[0] != b'+' && b[0] != b'-') {
return Err(TemplateError::misc("The time zone offset is malformed."));
}
let sign: i32 = if b[0] == b'-' { -1 } else { 1 };
if xs_mode {
if b.len() != 6 || b[3] != b':' {
return Err(TemplateError::misc(
"The time zone offset didn't match the expected pattern: Z|(?:[-+][0-9]{2}:[0-9]{2})",
));
}
let h: i32 = s[1..3]
.parse()
.map_err(|_| TemplateError::misc("The offset-hours part is a malformed integer."))?;
let m: i32 = s[4..6]
.parse()
.map_err(|_| TemplateError::misc("The offset-minutes part is a malformed integer."))?;
check_offset(h, m)?;
Ok(FixedOffset::east_opt(sign * (h * 3600 + m * 60)).unwrap())
} else {
let (h, m) = match b.len() {
3 => (s[1..3].parse::<i32>().map_err(|_| {
TemplateError::misc("The offset-hours part is a malformed integer.")
})?, 0),
5 => (s[1..3].parse::<i32>().map_err(|_| {
TemplateError::misc("The offset-hours part is a malformed integer.")
})?, s[3..5].parse::<i32>().map_err(|_| {
TemplateError::misc("The offset-minutes part is a malformed integer.")
})?),
6 if b[3] == b':' => (s[1..3].parse::<i32>().map_err(|_| {
TemplateError::misc("The offset-hours part is a malformed integer.")
})?, s[4..6].parse::<i32>().map_err(|_| {
TemplateError::misc("The offset-minutes part is a malformed integer.")
})?),
_ => {
return Err(TemplateError::misc(
"The time zone offset didn't match the expected pattern: Z|(?:[-+][0-9]{2}(?::?[0-9]{2})?)",
))
}
};
check_offset(h, m)?;
Ok(FixedOffset::east_opt(sign * (h * 3600 + m * 60)).unwrap())
}
}
fn check_offset(h: i32, m: i32) -> Result<()> {
if !(0..=23).contains(&h) {
return Err(TemplateError::misc(
"The offset-hours part must be at least 0 and can't be more than 23.",
));
}
if !(0..=59).contains(&m) {
return Err(TemplateError::misc(
"The offset-minutes part must be at least 0 and can't be more than 59.",
));
}
Ok(())
}
fn frac_to_ms(g: &str) -> Result<u32> {
let digits = if g.len() > 3 { &g[..3] } else { g };
let v: u32 = digits
.parse()
.map_err(|_| TemplateError::misc("The partial-seconds part is a malformed integer."))?;
Ok(match g.len() {
1 => v * 100,
2 => v * 10,
_ => v,
})
}
fn parse_int(s: &str, name: &str, min: i64, max: i64) -> Result<i64> {
let (neg, start) = if let Some(r) = s.strip_prefix('-') {
(true, r)
} else {
(false, s)
};
let trimmed = start.trim_start_matches('0');
let t = if trimmed.is_empty() { "0" } else { trimmed };
let mut v: i64 = t
.parse()
.map_err(|_| TemplateError::misc(format!("The {name} part is a malformed integer.")))?;
if neg {
v = -v;
}
if v < min {
return Err(TemplateError::misc(format!(
"The {name} part must be at least {min}."
)));
}
if v > max {
return Err(TemplateError::misc(format!(
"The {name} part can't be more than {max}."
)));
}
Ok(v)
}
pub fn fields_to_utc(
year: i64,
month: u32,
day: u32,
h: u32,
mi: u32,
s: u32,
ms: u32,
) -> Result<DateTime<Utc>> {
if year >= 1582 {
let naive = NaiveDate::from_ymd_opt(year as i32, month, day)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?
.and_hms_milli_opt(h, mi, s, ms)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?;
Ok(Utc.from_utc_datetime(&naive))
} else {
let jdn = julian_day_number(year, month as i64, day as i64);
let epoch_days = jdn - 2440588;
let secs = epoch_days * 86400 + h as i64 * 3600 + mi as i64 * 60 + s as i64;
DateTime::<Utc>::from_timestamp(secs, ms * 1_000_000)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fields_to_date(
year: i64,
month: u32,
day: u32,
h: u32,
mi: u32,
s: u32,
ms: u32,
tz: FixedOffset,
) -> Result<DateValue> {
let epoch = fields_to_utc(year, month, day, h, mi, s, ms)? - tz;
Ok(DateValue::new(epoch.with_timezone(&tz), DateType::DateTime))
}
fn julian_day_number(y: i64, m: i64, d: i64) -> i64 {
367 * y - (7 * (y + 5001 + (m - 9) / 7)) / 4 + (275 * m) / 9 + d + 1729777
}
fn parse_date(s: &str, tz: &TzSetting, xs_mode: bool) -> Result<DateValue> {
if xs_mode {
let (date_part, zone) = split_xs_zone(s)?;
let parts: Vec<&str> = date_part.split('-').collect();
if parts.len() != 3 {
return Err(TemplateError::misc(
"The value didn't match the expected pattern: (-?[0-9]+)-([0-9]{2})-([0-9]{2})(Z|(?:[-+][0-9]{2}:[0-9]{2}))?",
));
}
let year = parse_int(parts[0], "year", i64::MIN, i64::MAX)?;
let month = parse_int(parts[1], "month", 1, 12)? as u32;
let day = parse_int(parts[2], "day-of-month", 1, 31)? as u32;
let year = xs_year_to_era(year, xs_mode)?;
let off = match zone {
Some(z) => parse_offset(z, true)?,
None => tz.offset_at(&Utc::now().naive_utc()),
};
fields_to_date(year, month, day, 0, 0, 0, 0, off)
} else {
let m = match split_iso_date(s) {
Some(m) => m,
None => {
return Err(TemplateError::misc(
"The value didn't match the expected pattern: (-?[0-9]{4,})-([0-9]{2})-([0-9]{2}) or (-?[0-9]{4,}?)([0-9]{2})([0-9]{2})",
))
}
};
let off = tz.offset_at(&Utc::now().naive_utc());
let year = xs_year_to_era(m.year, xs_mode)?;
fields_to_date(year, m.month as u32, m.day as u32, 0, 0, 0, 0, off)
}
}
fn xs_year_to_era(year: i64, xs_mode: bool) -> Result<i64> {
if year <= 0 {
let adj = -year + if xs_mode { 0 } else { 1 };
if adj == 0 {
return Err(TemplateError::misc(
"Year 0 is not allowed in XML schema dates. BC 1 is -1, AD 1 is 1.",
));
}
Ok(adj)
} else {
Ok(year)
}
}
struct IsoDateMatch {
year: i64,
month: i64,
day: i64,
}
fn split_iso_date(s: &str) -> Option<IsoDateMatch> {
let b = s.as_bytes();
let neg = b.first() == Some(&b'-');
let start = if neg { 1 } else { 0 };
let body = &s[start..];
let year_s;
let rest;
if let Some(d1) = body.find('-') {
if d1 == 0 {
return None;
}
year_s = &body[..d1];
rest = &body[d1 + 1..];
if year_s.len() < 4 || !year_s.bytes().all(|c| c.is_ascii_digit()) {
return None;
}
let (month_s, day_s) = rest.split_once('-')?;
if month_s.len() != 2 || day_s.len() != 2 {
return None;
}
let year = signed_parse(neg, year_s)?;
let month: i64 = month_s.parse().ok()?;
let day: i64 = day_s.parse().ok()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
Some(IsoDateMatch { year, month, day })
} else {
let b2 = body.as_bytes();
let n = b2.len();
if n < 8 || !b2.iter().all(|c| c.is_ascii_digit()) {
return None;
}
year_s = &body[..n - 4];
if year_s.len() < 4 {
return None;
}
let year = signed_parse(neg, year_s)?;
let month = ((b2[n - 4] - b'0') * 10 + (b2[n - 3] - b'0')) as i64;
let day = ((b2[n - 2] - b'0') * 10 + (b2[n - 1] - b'0')) as i64;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
Some(IsoDateMatch { year, month, day })
}
}
fn signed_parse(neg: bool, s: &str) -> Option<i64> {
let v: i64 = s.parse().ok()?;
Some(if neg { -v } else { v })
}
fn split_xs_zone(s: &str) -> Result<(&str, Option<&str>)> {
if let Some(z) = s.strip_suffix('Z') {
return Ok((z, Some("Z")));
}
if s.len() > 6 {
let b = s.as_bytes();
let c = b[b.len() - 6];
if (c == b'+' || c == b'-') && b[b.len() - 3] == b':' {
return Ok((&s[..s.len() - 6], Some(&s[s.len() - 6..])));
}
}
Ok((s, None))
}
fn parse_time(s: &str, tz: &TzSetting, xs_mode: bool) -> Result<DateValue> {
if xs_mode {
let (body, zone) = split_xs_zone(s)?;
let parts: Vec<&str> = body.split(':').collect();
if parts.len() != 3 {
return Err(TemplateError::misc(
"The value didn't match the expected pattern: ([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?(Z|(?:[-+][0-9]{2}:[0-9]{2}))?",
));
}
let (hms, frac) = match parts[2].split_once('.') {
Some((h, f)) => (h, Some(f)),
None => (parts[2], None),
};
let h = parse_int(parts[0], "hour-of-day", 0, 24)? as u32;
let mi = parse_int(parts[1], "minute", 0, 59)? as u32;
let sec = parse_int(hms, "second", 0, 60)? as u32;
let ms = match frac {
Some(f) => frac_to_ms(f)?,
None => 0,
};
let off = match zone {
Some(z) => parse_offset(z, true)?,
None => tz.offset_at(&Utc::now().naive_utc()),
};
time_fields_to_value(h, mi, sec, ms, off)
} else {
parse_iso_time(s, tz)
}
}
fn parse_iso_time(s: &str, tz: &TzSetting) -> Result<DateValue> {
let (body, zone) = split_iso_zone(s);
let (h, mi, sec, ms) = parse_iso_time_fields(body)?;
let off = match zone {
Some(z) => parse_offset(z, false)?,
None => tz.offset_at(&Utc::now().naive_utc()),
};
time_fields_to_value(h, mi, sec, ms, off)
}
fn check_hms(h: u32, mi: u32, sec: u32) -> Result<()> {
if h > 24 {
return Err(TemplateError::misc(
"The hour-of-day part can't be more than 24.",
));
}
if mi > 59 {
return Err(TemplateError::misc(
"The minute part can't be more than 59.",
));
}
if sec > 60 {
return Err(TemplateError::misc(
"The second part can't be more than 60.",
));
}
if h == 24 && (mi != 0 || sec != 0) {
return Err(TemplateError::misc(
"Hour 24 is only allowed in the case of midnight.",
));
}
Ok(())
}
fn time_fields_to_value(h: u32, mi: u32, sec: u32, ms: u32, off: FixedOffset) -> Result<DateValue> {
let day = if h == 24 { 2 } else { 1 };
let h = h % 24;
let naive = NaiveDate::from_ymd_opt(1970, 1, day)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?
.and_hms_milli_opt(h, mi, sec, ms)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?;
let utc = Utc.from_utc_datetime(&naive) - off;
Ok(DateValue::new(utc.with_timezone(&off), DateType::Time))
}
fn split_iso_zone(s: &str) -> (&str, Option<&str>) {
if let Some(z) = s.strip_suffix('Z') {
return (z, Some("Z"));
}
let b = s.as_bytes();
if b.len() >= 3 {
let c = b[b.len() - 3];
if c == b'+' || c == b'-' {
return (&s[..s.len() - 3], Some(&s[s.len() - 3..]));
}
}
if b.len() >= 5 {
let c = b[b.len() - 5];
if c == b'+' || c == b'-' {
return (&s[..s.len() - 5], Some(&s[s.len() - 5..]));
}
}
if b.len() >= 6 {
let c = b[b.len() - 6];
if (c == b'+' || c == b'-') && b[b.len() - 3] == b':' {
return (&s[..s.len() - 6], Some(&s[s.len() - 6..]));
}
}
(s, None)
}
fn parse_date_time(s: &str, tz: &TzSetting, xs_mode: bool) -> Result<DateValue> {
let Some((date_part, time_part)) = s.split_once('T') else {
return Err(TemplateError::misc(
"The value didn't match the expected date-time pattern (missing \"T\" separator).",
));
};
if xs_mode {
let (time_body, zone) = split_xs_zone(time_part)?;
let parts: Vec<&str> = time_body.split(':').collect();
if parts.len() != 3 {
return Err(TemplateError::misc(
"The value didn't match the expected pattern: (-?[0-9]+)-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?(Z|(?:[-+][0-9]{2}:[0-9]{2}))?",
));
}
let (hms, frac) = match parts[2].split_once('.') {
Some((h, f)) => (h, Some(f)),
None => (parts[2], None),
};
let h = parse_int(parts[0], "hour-of-day", 0, 24)? as u32;
let mi = parse_int(parts[1], "minute", 0, 59)? as u32;
let sec = parse_int(hms, "second", 0, 60)? as u32;
let ms = match frac {
Some(f) => frac_to_ms(f)?,
None => 0,
};
check_hms(h, mi, sec)?;
let off = match zone {
Some(z) => parse_offset(z, true)?,
None => tz.offset_at(&Utc::now().naive_utc()),
};
date_time_fields(date_part, h, mi, sec, ms, off)
} else {
let (time_body, zone) = split_iso_zone(time_part);
let hms = parse_iso_time_fields(time_body)?;
let off = match zone {
Some(z) => parse_offset(z, false)?,
None => tz.offset_at(&Utc::now().naive_utc()),
};
date_time_fields(date_part, hms.0, hms.1, hms.2, hms.3, off)
}
}
fn parse_iso_time_fields(s: &str) -> Result<(u32, u32, u32, u32)> {
let b = s.as_bytes();
if b.len() < 2 || !b[..2].iter().all(|c| c.is_ascii_digit()) {
return Err(TemplateError::misc(
"The value didn't match the expected ISO 8601 time pattern.",
));
}
let h = ((b[0] - b'0') * 10 + (b[1] - b'0')) as u32;
let mut rest = &s[2..];
if let Some(r) = rest.strip_prefix(':') {
rest = r;
}
let (mi, sec, ms) = if rest.is_empty() {
(0, 0, 0)
} else {
if rest.len() < 2 || !rest[..2].bytes().all(|c| c.is_ascii_digit()) {
return Err(TemplateError::misc(
"The value didn't match the expected ISO 8601 time pattern.",
));
}
let mi = ((rest.as_bytes()[0] - b'0') * 10 + (rest.as_bytes()[1] - b'0')) as u32;
rest = &rest[2..];
if let Some(r) = rest.strip_prefix(':') {
rest = r;
}
if rest.is_empty() {
(mi, 0, 0)
} else {
let (sec_part, frac) = match rest.split_once([',', '.']) {
Some((a, f)) => (a, Some(f)),
None => (rest, None),
};
if sec_part.is_empty() {
let ms = match frac {
Some(f) => frac_to_ms(f)?,
None => 0,
};
(mi, 0, ms)
} else {
if sec_part.len() != 2 || !sec_part.bytes().all(|c| c.is_ascii_digit()) {
return Err(TemplateError::misc(
"The value didn't match the expected ISO 8601 time pattern.",
));
}
let sec =
((sec_part.as_bytes()[0] - b'0') * 10 + (sec_part.as_bytes()[1] - b'0')) as u32;
let ms = match frac {
Some(f) => frac_to_ms(f)?,
None => 0,
};
(mi, sec, ms)
}
}
};
check_hms(h, mi, sec)?;
Ok((h, mi, sec, ms))
}
fn date_time_fields(
date_part: &str,
h: u32,
mi: u32,
sec: u32,
ms: u32,
off: FixedOffset,
) -> Result<DateValue> {
let m = split_iso_date(date_part)
.ok_or_else(|| TemplateError::misc("The date part didn't match the expected pattern."))?;
let year = xs_year_to_era(m.year, false)?;
let instant = if year >= 1582 {
let naive = NaiveDate::from_ymd_opt(year as i32, m.month as u32, m.day as u32)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?
.and_hms_milli_opt(h, mi, sec, ms)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?;
Utc.from_utc_datetime(&naive)
} else {
let jdn = julian_day_number(year, m.month, m.day);
let epoch_days = jdn - 2440588;
let secs = epoch_days * 86400 + h as i64 * 3600 + mi as i64 * 60 + sec as i64;
DateTime::<Utc>::from_timestamp(secs, ms * 1_000_000)
.ok_or_else(|| TemplateError::misc("Date calculation faliure."))?
};
Ok(DateValue::new(
(instant - off).with_timezone(&off),
DateType::DateTime,
))
}