use std::collections::BTreeMap;
use std::fmt;
use std::hash::{Hash, Hasher};
use chrono::{
Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike,
};
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::types::{GraphError, Result, Value};
fn get_i64(map: &BTreeMap<String, Value>, key: &str) -> Option<i64> {
match map.get(key) {
Some(Value::I64(n)) => Some(*n),
_ => None,
}
}
fn get_f64(map: &BTreeMap<String, Value>, key: &str) -> Option<f64> {
match map.get(key) {
Some(Value::I64(n)) => Some(*n as f64),
Some(Value::F64(n)) => Some(*n),
_ => None,
}
}
fn parse_offset(s: &str) -> Result<FixedOffset> {
if s == "Z" || s == "z" {
return Ok(FixedOffset::east_opt(0).unwrap());
}
let sign: i32 = if s.starts_with('+') {
1
} else if s.starts_with('-') {
-1
} else {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid offset: {s}"),
hint: None,
});
};
let body = &s[1..];
let (h, m) = if body.len() == 2 {
(
body.parse::<i32>().map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?,
0,
)
} else if body.len() == 4 {
let hh = body[..2]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let mm = body[2..]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(hh, mm)
} else if body.len() == 5 && body.as_bytes()[2] == b':' {
let hh = body[..2]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let mm = body[3..]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(hh, mm)
} else if body.len() == 8 && body.as_bytes()[2] == b':' && body.as_bytes()[5] == b':' {
let hh = body[..2]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let mm = body[3..5]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let ss = body[6..8]
.parse::<i32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let total = sign * (hh * 3600 + mm * 60 + ss);
return FixedOffset::east_opt(total).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("offset out of range: {s}"),
hint: None,
});
} else {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid offset: {s}"),
hint: None,
});
};
let secs = sign * (h * 3600 + m * 60);
FixedOffset::east_opt(secs).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("offset out of range: {s}"),
hint: None,
})
}
fn split_time_offset(s: &str) -> (&str, Option<&str>) {
if s.ends_with('Z') || s.ends_with('z') {
return (&s[..s.len() - 1], Some(&s[s.len() - 1..]));
}
if let Some(pos) = s.rfind('+') {
if pos > 0 {
return (&s[..pos], Some(&s[pos..]));
}
}
if let Some(pos) = s.rfind('-') {
if pos > 0 {
return (&s[..pos], Some(&s[pos..]));
}
}
(s, None)
}
fn parse_time_str(s: &str) -> Result<NaiveTime> {
if s.contains(':') {
let parts: Vec<&str> = s.splitn(3, ':').collect();
let h: u32 =
parts[0]
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let m: u32 =
parts
.get(1)
.unwrap_or(&"0")
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
if parts.len() < 3 {
return NaiveTime::from_hms_opt(h, m, 0).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid time: {s}"),
hint: None,
});
}
let sec_part = parts[2];
if let Some(dot_pos) = sec_part.find('.') {
let sec: u32 = sec_part[..dot_pos]
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let frac = &sec_part[dot_pos + 1..];
let nano = parse_frac_nanos(frac)?;
NaiveTime::from_hms_nano_opt(h, m, sec, nano).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid time: {s}"),
hint: None,
})
} else {
let sec: u32 = sec_part.parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
NaiveTime::from_hms_opt(h, m, sec).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid time: {s}"),
hint: None,
})
}
} else {
let (digits, frac) = if let Some(dot_pos) = s.find('.') {
(&s[..dot_pos], Some(&s[dot_pos + 1..]))
} else {
(s, None)
};
let (h, m, sec) = match digits.len() {
2 => (
digits
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?,
0u32,
0u32,
),
4 => {
let hh = digits[..2]
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let mm = digits[2..]
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(hh, mm, 0)
}
6 => {
let hh = digits[..2]
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let mm = digits[2..4]
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let ss = digits[4..]
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(hh, mm, ss)
}
_ => {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid compact time: {s}"),
hint: None,
})
}
};
let nano = if let Some(f) = frac {
parse_frac_nanos(f)?
} else {
0
};
NaiveTime::from_hms_nano_opt(h, m, sec, nano).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid time: {s}"),
hint: None,
})
}
}
fn parse_num<T: std::str::FromStr>(s: &str) -> Result<T>
where
T::Err: fmt::Display,
{
s.parse::<T>().map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})
}
fn parse_frac_nanos(frac: &str) -> Result<u32> {
let mut padded = String::from(frac);
while padded.len() < 9 {
padded.push('0');
}
padded.truncate(9);
padded
.parse::<u32>()
.map_err(|e| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})
}
fn parse_date_str(s: &str) -> Result<NaiveDate> {
if s.contains('W') {
return parse_week_date(s);
}
if s.contains('-') {
let parts: Vec<&str> = s.split('-').collect();
match parts.len() {
3 => {
let y: i32 = parts[0].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let m: u32 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let d: u32 = parts[2].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
NaiveDate::from_ymd_opt(y, m, d).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
})
}
2 => {
let y: i32 = parts[0].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let part2: u32 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
if parts[1].len() == 3 {
NaiveDate::from_yo_opt(y, part2).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid ordinal date: {s}"),
hint: None,
})
} else {
NaiveDate::from_ymd_opt(y, part2, 1).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
})
}
}
_ => Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
}),
}
} else {
match s.len() {
8 => {
let y: i32 = parse_num(&s[..4])?;
let m: u32 = parse_num(&s[4..6])?;
let d: u32 = parse_num(&s[6..])?;
NaiveDate::from_ymd_opt(y, m, d).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
})
}
7 => {
let y: i32 = parse_num(&s[..4])?;
let d: u32 = parse_num(&s[4..])?;
NaiveDate::from_yo_opt(y, d).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid ordinal date: {s}"),
hint: None,
})
}
6 => {
let y: i32 = parse_num(&s[..4])?;
let m: u32 = parse_num(&s[4..])?;
NaiveDate::from_ymd_opt(y, m, 1).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
})
}
4 => {
let y: i32 = parse_num(s)?;
NaiveDate::from_ymd_opt(y, 1, 1).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
})
}
_ => Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid date: {s}"),
hint: None,
}),
}
}
}
fn parse_week_date(s: &str) -> Result<NaiveDate> {
let compact = s.replace('-', "");
let w_pos = compact.find('W').ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid week date: {s}"),
hint: None,
})?;
let year: i32 = compact[..w_pos]
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let after_w = &compact[w_pos + 1..];
let (week, day) = if after_w.len() >= 3 {
let wk: u32 = after_w[..2].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let d: u32 = after_w[2..3]
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(wk, d)
} else if after_w.len() == 2 {
let wk: u32 =
after_w
.parse()
.map_err(|e: std::num::ParseIntError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
(wk, 1) } else {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid week date: {s}"),
hint: None,
});
};
let weekday = match day {
1 => chrono::Weekday::Mon,
2 => chrono::Weekday::Tue,
3 => chrono::Weekday::Wed,
4 => chrono::Weekday::Thu,
5 => chrono::Weekday::Fri,
6 => chrono::Weekday::Sat,
7 => chrono::Weekday::Sun,
_ => {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid day of week {day} in: {s}"),
hint: None,
})
}
};
NaiveDate::from_isoywd_opt(year, week, weekday).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid week date: {s}"),
hint: None,
})
}
fn fmt_local_time(t: &NaiveTime, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}:{:02}", t.hour(), t.minute())?;
let sec = t.second();
let nano = t.nanosecond();
if sec != 0 || nano != 0 {
write!(f, ":{:02}", sec)?;
if nano != 0 {
let s = format!("{:09}", nano);
let trimmed = s.trim_end_matches('0');
write!(f, ".{trimmed}")?;
}
}
Ok(())
}
pub fn fmt_offset_public(off: &FixedOffset) -> String {
let secs = off.local_minus_utc();
if secs == 0 {
"Z".to_string()
} else {
let sign = if secs < 0 { '-' } else { '+' };
let abs = secs.unsigned_abs();
let h = abs / 3600;
let m = (abs % 3600) / 60;
let s = abs % 60;
if s != 0 {
format!("{sign}{h:02}:{m:02}:{s:02}")
} else {
format!("{sign}{h:02}:{m:02}")
}
}
}
fn fmt_offset(off: &FixedOffset, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let secs = off.local_minus_utc();
if secs == 0 {
write!(f, "Z")
} else {
let sign = if secs < 0 { '-' } else { '+' };
let abs = secs.unsigned_abs();
let h = abs / 3600;
let m = (abs % 3600) / 60;
let s = abs % 60;
if s != 0 {
write!(f, "{sign}{h:02}:{m:02}:{s:02}")
} else {
write!(f, "{sign}{h:02}:{m:02}")
}
}
}
fn base_source_offset(map: &BTreeMap<String, Value>) -> Option<FixedOffset> {
match map.get("time").or_else(|| map.get("datetime")) {
Some(Value::Time(t)) => Some(t.1),
Some(Value::DateTime(dt)) => Some(dt.1),
_ => None,
}
}
fn base_source_offset_at(
map: &BTreeMap<String, Value>,
ndt: &NaiveDateTime,
) -> Option<FixedOffset> {
match map.get("time").or_else(|| map.get("datetime")) {
Some(Value::Time(t)) => Some(t.1),
Some(Value::DateTime(dt)) => {
if let Some(ref tz_name) = dt.2 {
if let Ok((off, _)) = resolve_tz_name_at(tz_name, ndt) {
Some(off)
} else {
Some(dt.1)
}
} else {
Some(dt.1)
}
}
_ => None,
}
}
fn time_from_map(map: &BTreeMap<String, Value>) -> Result<NaiveTime> {
let base_time = match map.get("time").or_else(|| map.get("datetime")) {
Some(Value::LocalTime(t)) => Some(t.0),
Some(Value::Time(t)) => Some(t.0),
Some(Value::LocalDateTime(dt)) => Some(dt.0.time()),
Some(Value::DateTime(dt)) => Some(dt.0.time()),
_ => None,
};
let h = get_i64(map, "hour")
.or_else(|| base_time.map(|t| t.hour() as i64))
.unwrap_or(0) as u32;
let m = get_i64(map, "minute")
.or_else(|| base_time.map(|t| t.minute() as i64))
.unwrap_or(0) as u32;
let s = get_i64(map, "second")
.or_else(|| base_time.map(|t| t.second() as i64))
.unwrap_or(0) as u32;
let mut nano = get_i64(map, "nanosecond")
.or_else(|| base_time.map(|t| (t.nanosecond() % 1_000_000_000) as i64))
.unwrap_or(0) as u32;
nano += get_i64(map, "millisecond").unwrap_or(0) as u32 * 1_000_000;
nano += get_i64(map, "microsecond").unwrap_or(0) as u32 * 1_000;
NaiveTime::from_hms_nano_opt(h, m, s, nano).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "invalid time components".to_string(),
hint: None,
})
}
fn date_from_map(map: &BTreeMap<String, Value>) -> Result<NaiveDate> {
let base_date = match map.get("date").or_else(|| map.get("datetime")) {
Some(Value::Date(d)) => Some(d.0),
Some(Value::LocalDateTime(dt)) => Some(dt.0.date()),
Some(Value::DateTime(dt)) => Some(dt.0.date()),
_ => None,
};
if let Some(week) = get_i64(map, "week") {
let year = get_i64(map, "year")
.or_else(|| base_date.map(|d| d.iso_week().year() as i64))
.unwrap_or(0) as i32;
let dow = get_i64(map, "dayOfWeek").unwrap_or(
base_date
.map(|d| d.weekday().num_days_from_monday() as i64 + 1)
.unwrap_or(1),
) as u32;
let weekday = match dow {
1 => chrono::Weekday::Mon,
2 => chrono::Weekday::Tue,
3 => chrono::Weekday::Wed,
4 => chrono::Weekday::Thu,
5 => chrono::Weekday::Fri,
6 => chrono::Weekday::Sat,
7 => chrono::Weekday::Sun,
_ => {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid dayOfWeek: {dow}"),
hint: None,
})
}
};
NaiveDate::from_isoywd_opt(year, week as u32, weekday).ok_or_else(|| {
GraphError::Serialization {
context: String::new(),
source: "invalid week date components".to_string(),
hint: None,
}
})
} else if let Some(ord) = get_i64(map, "ordinalDay") {
let year = get_i64(map, "year")
.or_else(|| base_date.map(|d| d.year() as i64))
.unwrap_or(0) as i32;
NaiveDate::from_yo_opt(year, ord as u32).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "invalid ordinal date components".to_string(),
hint: None,
})
} else if let Some(quarter) = get_i64(map, "quarter") {
let year = get_i64(map, "year")
.or_else(|| base_date.map(|d| d.year() as i64))
.unwrap_or(0) as i32;
let quarter_start_month = ((quarter - 1) * 3 + 1) as u32;
if let Some(doq) = get_i64(map, "dayOfQuarter") {
let start = NaiveDate::from_ymd_opt(year, quarter_start_month, 1).ok_or_else(|| {
GraphError::Serialization {
context: String::new(),
source: "invalid quarter".to_string(),
hint: None,
}
})?;
start
.checked_add_signed(chrono::Duration::days(doq - 1))
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "dayOfQuarter out of range".to_string(),
hint: None,
})
} else if let Some(bd) = base_date {
let base_month_in_quarter = (bd.month() - 1) % 3; let month = quarter_start_month + base_month_in_quarter;
let day = get_i64(map, "day").unwrap_or(bd.day() as i64) as u32;
NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "invalid quarter date".to_string(),
hint: None,
})
} else {
NaiveDate::from_ymd_opt(year, quarter_start_month, 1).ok_or_else(|| {
GraphError::Serialization {
context: String::new(),
source: "invalid quarter".to_string(),
hint: None,
}
})
}
} else {
let year = get_i64(map, "year")
.or_else(|| base_date.map(|d| d.year() as i64))
.unwrap_or(0) as i32;
let month = get_i64(map, "month")
.or_else(|| base_date.map(|d| d.month() as i64))
.unwrap_or(1) as u32;
let day = get_i64(map, "day")
.or_else(|| base_date.map(|d| d.day() as i64))
.unwrap_or(1) as u32;
NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "invalid date components".to_string(),
hint: None,
})
}
}
fn offset_from_map(map: &BTreeMap<String, Value>) -> Result<(FixedOffset, Option<String>)> {
match map.get("timezone") {
Some(Value::String(s)) => {
if s.starts_with('+') || s.starts_with('-') || s == "Z" || s == "z" {
Ok((parse_offset(s)?, None))
} else {
resolve_tz_name_now(s)
}
}
_ => {
match map.get("time").or_else(|| map.get("datetime")) {
Some(Value::Time(t)) => Ok((t.1, None)),
Some(Value::DateTime(dt)) => Ok((dt.1, dt.2.clone())),
Some(Value::LocalTime(_) | Value::LocalDateTime(_)) => {
Ok((FixedOffset::east_opt(0).unwrap(), None))
}
_ => {
Ok((FixedOffset::east_opt(0).unwrap(), None))
}
}
}
}
}
fn resolve_tz_name_now(name: &str) -> Result<(FixedOffset, Option<String>)> {
let tz: chrono_tz::Tz = name.parse().map_err(|_| GraphError::Serialization {
context: String::new(),
source: format!("unknown timezone: {name}"),
hint: None,
})?;
let now = chrono::Utc::now().with_timezone(&tz);
let off = now.offset().fix();
Ok((off, Some(name.to_string())))
}
fn resolve_tz_name_at(name: &str, dt: &NaiveDateTime) -> Result<(FixedOffset, Option<String>)> {
let tz: chrono_tz::Tz = name.parse().map_err(|_| GraphError::Serialization {
context: String::new(),
source: format!("unknown timezone: {name}"),
hint: None,
})?;
let aware = tz
.from_local_datetime(dt)
.earliest()
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("ambiguous or invalid datetime in timezone: {name}"),
hint: None,
})?;
let off = aware.offset().fix();
Ok((off, Some(name.to_string())))
}
#[derive(Debug, Clone)]
pub struct CypherDate(pub NaiveDate);
impl PartialEq for CypherDate {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for CypherDate {}
impl Hash for CypherDate {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for CypherDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02}",
self.0.year(),
self.0.month(),
self.0.day()
)
}
}
impl Serialize for CypherDate {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("CypherDate", 3)?;
s.serialize_field("year", &self.0.year())?;
s.serialize_field("month", &self.0.month())?;
s.serialize_field("day", &self.0.day())?;
s.end()
}
}
impl<'de> Deserialize<'de> for CypherDate {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Year,
Month,
Day,
}
struct CypherDateVisitor;
impl<'de> Visitor<'de> for CypherDateVisitor {
type Value = CypherDate;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a CypherDate struct with year, month, day")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherDate, A::Error> {
let year: i32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let month: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let day: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| de::Error::custom("invalid date"))?;
Ok(CypherDate(date))
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherDate, A::Error> {
let (mut year, mut month, mut day) = (None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Year => year = Some(map.next_value()?),
Field::Month => month = Some(map.next_value()?),
Field::Day => day = Some(map.next_value()?),
}
}
let year: i32 = year.ok_or_else(|| de::Error::missing_field("year"))?;
let month: u32 = month.ok_or_else(|| de::Error::missing_field("month"))?;
let day: u32 = day.ok_or_else(|| de::Error::missing_field("day"))?;
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| de::Error::custom("invalid date"))?;
Ok(CypherDate(date))
}
}
deserializer.deserialize_any(CypherDateVisitor)
}
}
impl CypherDate {
pub fn from_iso_string(s: &str) -> Result<Self> {
parse_date_str(s).map(CypherDate)
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
date_from_map(map).map(CypherDate)
}
}
#[derive(Debug, Clone)]
pub struct CypherLocalTime(pub NaiveTime);
impl PartialEq for CypherLocalTime {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for CypherLocalTime {}
impl Hash for CypherLocalTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for CypherLocalTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_local_time(&self.0, f)
}
}
impl Serialize for CypherLocalTime {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("CypherLocalTime", 4)?;
s.serialize_field("hour", &self.0.hour())?;
s.serialize_field("minute", &self.0.minute())?;
s.serialize_field("second", &self.0.second())?;
s.serialize_field("nanosecond", &self.0.nanosecond())?;
s.end()
}
}
impl<'de> Deserialize<'de> for CypherLocalTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Hour,
Minute,
Second,
Nanosecond,
}
struct V;
impl<'de> Visitor<'de> for V {
type Value = CypherLocalTime;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("CypherLocalTime")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherLocalTime, A::Error> {
let h: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let m: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let s: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let n: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
let t = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
Ok(CypherLocalTime(t))
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherLocalTime, A::Error> {
let (mut hour, mut minute, mut second, mut nano) = (None, None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Hour => hour = Some(map.next_value()?),
Field::Minute => minute = Some(map.next_value()?),
Field::Second => second = Some(map.next_value()?),
Field::Nanosecond => nano = Some(map.next_value()?),
}
}
let h: u32 = hour.ok_or_else(|| de::Error::missing_field("hour"))?;
let m: u32 = minute.ok_or_else(|| de::Error::missing_field("minute"))?;
let s: u32 = second.ok_or_else(|| de::Error::missing_field("second"))?;
let n: u32 = nano.ok_or_else(|| de::Error::missing_field("nanosecond"))?;
let t = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
Ok(CypherLocalTime(t))
}
}
deserializer.deserialize_any(V)
}
}
impl CypherLocalTime {
pub fn from_iso_string(s: &str) -> Result<Self> {
parse_time_str(s).map(CypherLocalTime)
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
time_from_map(map).map(CypherLocalTime)
}
}
#[derive(Debug, Clone)]
pub struct CypherTime(pub NaiveTime, pub FixedOffset);
impl PartialEq for CypherTime {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 && self.1 == other.1
}
}
impl Eq for CypherTime {}
impl Hash for CypherTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
self.1.local_minus_utc().hash(state);
}
}
impl fmt::Display for CypherTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_local_time(&self.0, f)?;
fmt_offset(&self.1, f)
}
}
impl Serialize for CypherTime {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("CypherTime", 5)?;
s.serialize_field("hour", &self.0.hour())?;
s.serialize_field("minute", &self.0.minute())?;
s.serialize_field("second", &self.0.second())?;
s.serialize_field("nanosecond", &self.0.nanosecond())?;
s.serialize_field("offset_seconds", &self.1.local_minus_utc())?;
s.end()
}
}
impl<'de> Deserialize<'de> for CypherTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Hour,
Minute,
Second,
Nanosecond,
#[serde(rename = "offset_seconds")]
OffsetSeconds,
}
struct V;
impl<'de> Visitor<'de> for V {
type Value = CypherTime;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("CypherTime")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherTime, A::Error> {
let h: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let m: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let s: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let n: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
let o: i32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(4, &self))?;
let t = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
let offset =
FixedOffset::east_opt(o).ok_or_else(|| de::Error::custom("invalid offset"))?;
Ok(CypherTime(t, offset))
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherTime, A::Error> {
let (mut hour, mut minute, mut second, mut nano, mut off) =
(None, None, None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Hour => hour = Some(map.next_value()?),
Field::Minute => minute = Some(map.next_value()?),
Field::Second => second = Some(map.next_value()?),
Field::Nanosecond => nano = Some(map.next_value()?),
Field::OffsetSeconds => off = Some(map.next_value()?),
}
}
let h: u32 = hour.ok_or_else(|| de::Error::missing_field("hour"))?;
let m: u32 = minute.ok_or_else(|| de::Error::missing_field("minute"))?;
let s: u32 = second.ok_or_else(|| de::Error::missing_field("second"))?;
let n: u32 = nano.ok_or_else(|| de::Error::missing_field("nanosecond"))?;
let o: i32 = off.ok_or_else(|| de::Error::missing_field("offset_seconds"))?;
let t = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
let offset =
FixedOffset::east_opt(o).ok_or_else(|| de::Error::custom("invalid offset"))?;
Ok(CypherTime(t, offset))
}
}
deserializer.deserialize_any(V)
}
}
impl CypherTime {
pub fn from_iso_string(s: &str) -> Result<Self> {
let (time_part, off_part) = split_time_offset(s);
let t = parse_time_str(time_part)?;
let off = match off_part {
Some(o) => parse_offset(o)?,
None => FixedOffset::east_opt(0).unwrap(),
};
Ok(CypherTime(t, off))
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
let t = time_from_map(map)?;
let (off, _tz_name) = offset_from_map(map)?;
let t = if map.contains_key("timezone") {
if let Some(src_off) = base_source_offset(map) {
if src_off != off {
let delta = off.local_minus_utc() - src_off.local_minus_utc();
let secs = t.num_seconds_from_midnight() as i64 + delta as i64;
let secs = secs.rem_euclid(86400) as u32;
NaiveTime::from_num_seconds_from_midnight_opt(
secs,
t.nanosecond() % 1_000_000_000,
)
.unwrap_or(t)
} else {
t
}
} else {
t
}
} else {
t
};
Ok(CypherTime(t, off))
}
}
#[derive(Debug, Clone)]
pub struct CypherLocalDateTime(pub NaiveDateTime);
impl PartialEq for CypherLocalDateTime {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for CypherLocalDateTime {}
impl Hash for CypherLocalDateTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for CypherLocalDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let d = self.0.date();
write!(f, "{:04}-{:02}-{:02}T", d.year(), d.month(), d.day())?;
fmt_local_time(&self.0.time(), f)
}
}
impl Serialize for CypherLocalDateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let d = self.0.date();
let t = self.0.time();
let mut s = serializer.serialize_struct("CypherLocalDateTime", 7)?;
s.serialize_field("year", &d.year())?;
s.serialize_field("month", &d.month())?;
s.serialize_field("day", &d.day())?;
s.serialize_field("hour", &t.hour())?;
s.serialize_field("minute", &t.minute())?;
s.serialize_field("second", &t.second())?;
s.serialize_field("nanosecond", &t.nanosecond())?;
s.end()
}
}
impl<'de> Deserialize<'de> for CypherLocalDateTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Year,
Month,
Day,
Hour,
Minute,
Second,
Nanosecond,
}
struct V;
impl<'de> Visitor<'de> for V {
type Value = CypherLocalDateTime;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("CypherLocalDateTime")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherLocalDateTime, A::Error> {
let year: i32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let month: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let day: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let h: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
let m: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(4, &self))?;
let s: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(5, &self))?;
let n: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(6, &self))?;
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| de::Error::custom("invalid date"))?;
let time = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
Ok(CypherLocalDateTime(NaiveDateTime::new(date, time)))
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherLocalDateTime, A::Error> {
let (mut year, mut month, mut day) = (None, None, None);
let (mut hour, mut minute, mut second, mut nano) = (None, None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Year => year = Some(map.next_value()?),
Field::Month => month = Some(map.next_value()?),
Field::Day => day = Some(map.next_value()?),
Field::Hour => hour = Some(map.next_value()?),
Field::Minute => minute = Some(map.next_value()?),
Field::Second => second = Some(map.next_value()?),
Field::Nanosecond => nano = Some(map.next_value()?),
}
}
let y: i32 = year.ok_or_else(|| de::Error::missing_field("year"))?;
let mo: u32 = month.ok_or_else(|| de::Error::missing_field("month"))?;
let dy: u32 = day.ok_or_else(|| de::Error::missing_field("day"))?;
let h: u32 = hour.ok_or_else(|| de::Error::missing_field("hour"))?;
let mi: u32 = minute.ok_or_else(|| de::Error::missing_field("minute"))?;
let s: u32 = second.ok_or_else(|| de::Error::missing_field("second"))?;
let n: u32 = nano.ok_or_else(|| de::Error::missing_field("nanosecond"))?;
let d = NaiveDate::from_ymd_opt(y, mo, dy)
.ok_or_else(|| de::Error::custom("invalid date"))?;
let t = NaiveTime::from_hms_nano_opt(h, mi, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
Ok(CypherLocalDateTime(NaiveDateTime::new(d, t)))
}
}
deserializer.deserialize_any(V)
}
}
impl CypherLocalDateTime {
pub fn from_iso_string(s: &str) -> Result<Self> {
let t_pos =
s.find('T')
.or_else(|| s.find('t'))
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("expected 'T' separator in: {s}"),
hint: None,
})?;
let date = parse_date_str(&s[..t_pos])?;
let time = parse_time_str(&s[t_pos + 1..])?;
Ok(CypherLocalDateTime(NaiveDateTime::new(date, time)))
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
let d = date_from_map(map)?;
let t = time_from_map(map)?;
Ok(CypherLocalDateTime(NaiveDateTime::new(d, t)))
}
}
#[derive(Debug, Clone)]
pub struct CypherDateTime(pub NaiveDateTime, pub FixedOffset, pub Option<String>);
impl PartialEq for CypherDateTime {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 && self.1 == other.1 && self.2 == other.2
}
}
impl Eq for CypherDateTime {}
impl Hash for CypherDateTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
self.1.local_minus_utc().hash(state);
self.2.hash(state);
}
}
impl fmt::Display for CypherDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let d = self.0.date();
write!(f, "{:04}-{:02}-{:02}T", d.year(), d.month(), d.day())?;
fmt_local_time(&self.0.time(), f)?;
fmt_offset(&self.1, f)?;
if let Some(ref tz) = self.2 {
write!(f, "[{tz}]")?;
}
Ok(())
}
}
impl Serialize for CypherDateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let d = self.0.date();
let t = self.0.time();
let field_count = if self.2.is_some() { 9 } else { 8 };
let mut s = serializer.serialize_struct("CypherDateTime", field_count)?;
s.serialize_field("year", &d.year())?;
s.serialize_field("month", &d.month())?;
s.serialize_field("day", &d.day())?;
s.serialize_field("hour", &t.hour())?;
s.serialize_field("minute", &t.minute())?;
s.serialize_field("second", &t.second())?;
s.serialize_field("nanosecond", &t.nanosecond())?;
s.serialize_field("offset_seconds", &self.1.local_minus_utc())?;
if let Some(ref tz) = self.2 {
s.serialize_field("tz_name", tz)?;
}
s.end()
}
}
impl<'de> Deserialize<'de> for CypherDateTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Year,
Month,
Day,
Hour,
Minute,
Second,
Nanosecond,
#[serde(rename = "offset_seconds")]
OffsetSeconds,
#[serde(rename = "tz_name")]
TzName,
}
struct V;
impl<'de> Visitor<'de> for V {
type Value = CypherDateTime;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("CypherDateTime")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherDateTime, A::Error> {
let year: i32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let month: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let day: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let h: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
let m: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(4, &self))?;
let s: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(5, &self))?;
let n: u32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(6, &self))?;
let o: i32 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(7, &self))?;
let tz_name: Option<String> = seq.next_element().ok().flatten();
let date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| de::Error::custom("invalid date"))?;
let time = NaiveTime::from_hms_nano_opt(h, m, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
let offset =
FixedOffset::east_opt(o).ok_or_else(|| de::Error::custom("invalid offset"))?;
let dt = NaiveDateTime::new(date, time);
Ok(CypherDateTime(dt, offset, tz_name))
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherDateTime, A::Error> {
let (mut year, mut month, mut day) = (None, None, None);
let (mut hour, mut minute, mut second, mut nano, mut off) =
(None, None, None, None, None);
let mut tz_name: Option<String> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Year => year = Some(map.next_value()?),
Field::Month => month = Some(map.next_value()?),
Field::Day => day = Some(map.next_value()?),
Field::Hour => hour = Some(map.next_value()?),
Field::Minute => minute = Some(map.next_value()?),
Field::Second => second = Some(map.next_value()?),
Field::Nanosecond => nano = Some(map.next_value()?),
Field::OffsetSeconds => off = Some(map.next_value()?),
Field::TzName => tz_name = Some(map.next_value()?),
}
}
let y: i32 = year.ok_or_else(|| de::Error::missing_field("year"))?;
let mo: u32 = month.ok_or_else(|| de::Error::missing_field("month"))?;
let dy: u32 = day.ok_or_else(|| de::Error::missing_field("day"))?;
let h: u32 = hour.ok_or_else(|| de::Error::missing_field("hour"))?;
let mi: u32 = minute.ok_or_else(|| de::Error::missing_field("minute"))?;
let s: u32 = second.ok_or_else(|| de::Error::missing_field("second"))?;
let n: u32 = nano.ok_or_else(|| de::Error::missing_field("nanosecond"))?;
let o: i32 = off.ok_or_else(|| de::Error::missing_field("offset_seconds"))?;
let d = NaiveDate::from_ymd_opt(y, mo, dy)
.ok_or_else(|| de::Error::custom("invalid date"))?;
let t = NaiveTime::from_hms_nano_opt(h, mi, s, n)
.ok_or_else(|| de::Error::custom("invalid time"))?;
let offset =
FixedOffset::east_opt(o).ok_or_else(|| de::Error::custom("invalid offset"))?;
Ok(CypherDateTime(NaiveDateTime::new(d, t), offset, tz_name))
}
}
deserializer.deserialize_any(V)
}
}
impl CypherDateTime {
pub fn from_iso_string(s: &str) -> Result<Self> {
let t_pos =
s.find('T')
.or_else(|| s.find('t'))
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("expected 'T' separator in: {s}"),
hint: None,
})?;
let date = parse_date_str(&s[..t_pos])?;
let time_and_off = &s[t_pos + 1..];
let (time_off_part, tz_name) = if let Some(bracket_pos) = time_and_off.find('[') {
let name = time_and_off[bracket_pos + 1..]
.trim_end_matches(']')
.to_string();
(&time_and_off[..bracket_pos], Some(name))
} else {
(time_and_off, None)
};
let (time_part, off_part) = split_time_offset(time_off_part);
let time = parse_time_str(time_part)?;
let off = if let Some(off_str) = off_part {
parse_offset(off_str)?
} else if let Some(ref tz) = tz_name {
let ndt = NaiveDateTime::new(date, time);
let (resolved, _) = resolve_tz_name_at(tz, &ndt)?;
resolved
} else {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("CypherDateTime requires an offset or timezone: {s}"),
hint: None,
});
};
Ok(CypherDateTime(NaiveDateTime::new(date, time), off, tz_name))
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
let d = date_from_map(map)?;
let t = time_from_map(map)?;
let ndt = NaiveDateTime::new(d, t);
match map.get("timezone") {
Some(Value::String(s)) => {
let target_off = if s.starts_with('+') || s.starts_with('-') || s == "Z" || s == "z"
{
parse_offset(s)?
} else {
FixedOffset::east_opt(0).unwrap()
};
let is_named = !(s.starts_with('+') || s.starts_with('-') || s == "Z" || s == "z");
let ndt = if let Some(src_off) = base_source_offset_at(map, &ndt) {
if is_named {
let utc_ndt =
ndt - chrono::Duration::seconds(src_off.local_minus_utc() as i64);
let tz: chrono_tz::Tz =
s.parse().map_err(|_| GraphError::Serialization {
context: String::new(),
source: format!("unknown timezone: {s}"),
hint: None,
})?;
let aware = tz.from_utc_datetime(&utc_ndt);
let off = aware.offset().fix();
let converted = aware.naive_local();
return Ok(CypherDateTime(converted, off, Some(s.to_string())));
} else if src_off != target_off {
let delta = target_off.local_minus_utc() - src_off.local_minus_utc();
ndt + chrono::Duration::seconds(delta as i64)
} else {
ndt
}
} else {
ndt
};
if is_named {
let (off, tz_name) = resolve_tz_name_at(s, &ndt)?;
Ok(CypherDateTime(ndt, off, tz_name))
} else {
Ok(CypherDateTime(ndt, target_off, None))
}
}
_ => {
match map.get("time").or_else(|| map.get("datetime")) {
Some(Value::Time(t)) => Ok(CypherDateTime(ndt, t.1, None)),
Some(Value::DateTime(dt)) => {
if let Some(ref tz_name) = dt.2 {
let (off, tz) = resolve_tz_name_at(tz_name, &ndt)?;
Ok(CypherDateTime(ndt, off, tz))
} else {
Ok(CypherDateTime(ndt, dt.1, None))
}
}
_ => {
Ok(CypherDateTime(ndt, FixedOffset::east_opt(0).unwrap(), None))
}
}
}
}
}
pub fn from_epoch(seconds: i64, nanos: i64) -> Self {
let total_nanos = seconds * 1_000_000_000 + nanos;
let secs = total_nanos.div_euclid(1_000_000_000);
let ns = total_nanos.rem_euclid(1_000_000_000) as u32;
let dt = chrono::DateTime::from_timestamp(secs, ns)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
CypherDateTime(dt.naive_utc(), FixedOffset::east_opt(0).unwrap(), None)
}
pub fn from_epoch_millis(millis: i64) -> Self {
let secs = millis.div_euclid(1000);
let ms = millis.rem_euclid(1000) as u32;
let dt = chrono::DateTime::from_timestamp(secs, ms * 1_000_000)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
CypherDateTime(dt.naive_utc(), FixedOffset::east_opt(0).unwrap(), None)
}
}
#[derive(Debug, Clone)]
pub struct CypherDuration {
pub months: i64,
pub days: i64,
pub seconds: i64,
pub nanos: i64,
}
impl PartialEq for CypherDuration {
fn eq(&self, other: &Self) -> bool {
self.months == other.months
&& self.days == other.days
&& self.seconds == other.seconds
&& self.nanos == other.nanos
}
}
impl Eq for CypherDuration {}
impl Hash for CypherDuration {
fn hash<H: Hasher>(&self, state: &mut H) {
self.months.hash(state);
self.days.hash(state);
self.seconds.hash(state);
self.nanos.hash(state);
}
}
impl fmt::Display for CypherDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let years = self.months / 12;
let months = self.months % 12;
let (total_seconds, nanos) = if self.seconds < 0 && self.nanos > 0 {
(self.seconds + 1, self.nanos - 1_000_000_000)
} else if self.seconds > 0 && self.nanos < 0 {
(self.seconds - 1, self.nanos + 1_000_000_000)
} else {
(self.seconds, self.nanos)
};
let hours = total_seconds / 3600;
let rem = total_seconds % 3600;
let minutes = rem / 60;
let secs = rem % 60;
let has_date_part = years != 0 || months != 0 || self.days != 0;
let has_time_part = hours != 0 || minutes != 0 || secs != 0 || nanos != 0;
if !has_date_part && !has_time_part {
return write!(f, "PT0S");
}
write!(f, "P")?;
if years != 0 {
write!(f, "{years}Y")?;
}
if months != 0 {
write!(f, "{months}M")?;
}
if self.days != 0 {
write!(f, "{}D", self.days)?;
}
if has_time_part {
write!(f, "T")?;
if hours != 0 {
write!(f, "{hours}H")?;
}
if minutes != 0 {
write!(f, "{minutes}M")?;
}
if secs != 0 || nanos != 0 {
if nanos != 0 {
let frac = format!("{:09}", nanos.unsigned_abs());
let trimmed = frac.trim_end_matches('0');
let neg = secs < 0 || (secs == 0 && nanos < 0);
if neg {
let abs_secs = secs.unsigned_abs();
write!(f, "-{abs_secs}.{trimmed}S")?;
} else {
write!(f, "{secs}.{trimmed}S")?;
}
} else {
write!(f, "{secs}S")?;
}
}
}
Ok(())
}
}
impl Serialize for CypherDuration {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("CypherDuration", 4)?;
s.serialize_field("months", &self.months)?;
s.serialize_field("days", &self.days)?;
s.serialize_field("seconds", &self.seconds)?;
s.serialize_field("nanos", &self.nanos)?;
s.end()
}
}
impl<'de> Deserialize<'de> for CypherDuration {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Months,
Days,
Seconds,
Nanos,
}
struct V;
impl<'de> Visitor<'de> for V {
type Value = CypherDuration;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("CypherDuration")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<CypherDuration, A::Error> {
let months: i64 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let days: i64 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let seconds: i64 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let nanos: i64 = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
Ok(CypherDuration {
months,
days,
seconds,
nanos,
})
}
fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> std::result::Result<CypherDuration, A::Error> {
let (mut months, mut days, mut seconds, mut nanos) = (None, None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Months => months = Some(map.next_value()?),
Field::Days => days = Some(map.next_value()?),
Field::Seconds => seconds = Some(map.next_value()?),
Field::Nanos => nanos = Some(map.next_value()?),
}
}
Ok(CypherDuration {
months: months.ok_or_else(|| de::Error::missing_field("months"))?,
days: days.ok_or_else(|| de::Error::missing_field("days"))?,
seconds: seconds.ok_or_else(|| de::Error::missing_field("seconds"))?,
nanos: nanos.ok_or_else(|| de::Error::missing_field("nanos"))?,
})
}
}
deserializer.deserialize_any(V)
}
}
impl CypherDuration {
pub fn from_iso_string(s: &str) -> Result<Self> {
if !s.starts_with('P') && !s.starts_with('p') {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("duration must start with 'P': {s}"),
hint: None,
});
}
let body = &s[1..];
let (date_part, time_part) = if let Some(t_pos) = body.find('T').or_else(|| body.find('t'))
{
(&body[..t_pos], Some(&body[t_pos + 1..]))
} else {
(body, None)
};
let mut months: i64 = 0;
let mut days: i64 = 0;
let mut seconds: i64 = 0;
let mut nanos: i64 = 0;
if !date_part.is_empty() {
if date_part.contains('-') && !date_part.bytes().any(|b| b.is_ascii_alphabetic()) {
let parts: Vec<&str> = date_part.split('-').collect();
if parts.len() == 3 {
let y: i64 = parts[0].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let m: i64 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let d: i64 = parts[2].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
months += y * 12 + m;
days += d;
} else {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("invalid date-format duration: {date_part}"),
hint: None,
});
}
} else {
parse_duration_date_part(
date_part,
&mut months,
&mut days,
&mut seconds,
&mut nanos,
)?;
}
}
if let Some(tp) = time_part {
if !tp.is_empty() {
if tp.contains(':') {
let parts: Vec<&str> = tp.split(':').collect();
if parts.len() >= 2 {
let h: i64 = parts[0].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let m: i64 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
seconds += h * 3600 + m * 60;
if parts.len() == 3 {
let s_str = parts[2];
if let Some(dot_pos) = s_str.find('.') {
let int_part = &s_str[..dot_pos];
let frac_part = &s_str[dot_pos + 1..];
let int_val: i64 = if int_part.is_empty() {
0
} else {
int_part.parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?
};
seconds += int_val;
nanos += parse_frac_nanos(frac_part)? as i64;
} else {
let s_val: i64 =
s_str.parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
seconds += s_val;
}
}
}
} else {
parse_duration_time_part(tp, &mut seconds, &mut nanos)?;
}
}
}
Ok(CypherDuration {
months,
days,
seconds,
nanos,
})
}
pub fn from_map(map: &BTreeMap<String, Value>) -> Result<Self> {
let years = get_f64(map, "years").unwrap_or(0.0);
let mons = get_f64(map, "months").unwrap_or(0.0);
let weeks = get_f64(map, "weeks").unwrap_or(0.0);
let days = get_f64(map, "days").unwrap_or(0.0);
let hours = get_f64(map, "hours").unwrap_or(0.0);
let minutes = get_f64(map, "minutes").unwrap_or(0.0);
let secs = get_f64(map, "seconds").unwrap_or(0.0);
let millis = get_f64(map, "milliseconds").unwrap_or(0.0);
let micros = get_f64(map, "microseconds").unwrap_or(0.0);
let ns = get_f64(map, "nanoseconds").unwrap_or(0.0);
let months_f = years * 12.0 + mons;
let total_months = months_f.trunc() as i64;
let frac_months = months_f - months_f.trunc();
let days_f = weeks * 7.0 + days + frac_months * 30.436875;
let total_days = days_f.trunc() as i64;
let frac_days = days_f - days_f.trunc();
let nanos_from_time = (hours + frac_days * 24.0) * 3_600_000_000_000.0
+ minutes * 60_000_000_000.0
+ secs * 1_000_000_000.0
+ millis * 1_000_000.0
+ micros * 1_000.0
+ ns;
let total_nanos_i = nanos_from_time.round() as i64;
let mut total_seconds = total_nanos_i / 1_000_000_000;
let mut total_nanos = total_nanos_i % 1_000_000_000;
if total_seconds > 0 && total_nanos < 0 {
total_seconds -= 1;
total_nanos += 1_000_000_000;
} else if total_seconds < 0 && total_nanos > 0 {
total_seconds += 1;
total_nanos -= 1_000_000_000;
}
Ok(CypherDuration {
months: total_months,
days: total_days,
seconds: total_seconds,
nanos: total_nanos,
})
}
}
fn cascade_frac_months(frac: f64, days: &mut i64, seconds: &mut i64, nanos: &mut i64) {
if frac == 0.0 {
return;
}
let days_f = frac * 30.436875;
let int_days = days_f.trunc() as i64;
*days += int_days;
let frac_days = days_f - days_f.trunc();
cascade_frac_days(frac_days, seconds, nanos);
}
fn cascade_frac_days(frac: f64, seconds: &mut i64, nanos: &mut i64) {
if frac == 0.0 {
return;
}
let total_nanos_f = frac * 86400.0 * 1_000_000_000.0;
let total_nanos_i = total_nanos_f.round() as i64;
*seconds += total_nanos_i / 1_000_000_000;
*nanos += total_nanos_i % 1_000_000_000;
}
fn parse_duration_date_part(
s: &str,
months: &mut i64,
days: &mut i64,
seconds: &mut i64,
nanos: &mut i64,
) -> Result<()> {
let mut num_start = 0;
let bytes = s.as_bytes();
let mut i = 0;
if i < bytes.len() && bytes[i] == b'-' {
i += 1;
}
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() || b == b'.' {
i += 1;
continue;
}
let num_str = &s[num_start..i];
let n: f64 =
num_str
.parse()
.map_err(|e: std::num::ParseFloatError| GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
})?;
let int_part = n.trunc() as i64;
let frac = n - n.trunc();
match b {
b'Y' | b'y' => {
*months += int_part * 12;
let frac_months = frac * 12.0;
let int_frac_months = frac_months.trunc() as i64;
*months += int_frac_months;
cascade_frac_months(frac_months - frac_months.trunc(), days, seconds, nanos);
}
b'M' | b'm' => {
*months += int_part;
cascade_frac_months(frac, days, seconds, nanos);
}
b'W' | b'w' => {
let days_f = n * 7.0;
let int_days = days_f.trunc() as i64;
*days += int_days;
cascade_frac_days(days_f - days_f.trunc(), seconds, nanos);
}
b'D' | b'd' => {
*days += int_part;
cascade_frac_days(frac, seconds, nanos);
}
_ => {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("unexpected char '{b}' in duration date part"),
hint: None,
})
}
}
i += 1;
num_start = i;
if i < bytes.len() && bytes[i] == b'-' {
i += 1;
}
}
Ok(())
}
fn parse_duration_time_part(s: &str, seconds: &mut i64, nanos: &mut i64) -> Result<()> {
let mut num_start = 0;
let bytes = s.as_bytes();
let mut i = 0;
if i < bytes.len() && bytes[i] == b'-' {
i += 1;
}
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() || b == b'.' {
i += 1;
continue;
}
let num_str = &s[num_start..i];
match b {
b'H' | b'h' => {
let n: f64 = num_str.parse().map_err(|e: std::num::ParseFloatError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let int_part = n.trunc() as i64;
let frac = n - n.trunc();
*seconds += int_part * 3600;
if frac != 0.0 {
let frac_nanos = (frac * 3_600_000_000_000.0).round() as i64;
*seconds += frac_nanos / 1_000_000_000;
*nanos += frac_nanos % 1_000_000_000;
}
}
b'M' | b'm' => {
let n: f64 = num_str.parse().map_err(|e: std::num::ParseFloatError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
let int_part = n.trunc() as i64;
let frac = n - n.trunc();
*seconds += int_part * 60;
if frac != 0.0 {
let frac_nanos = (frac * 60_000_000_000.0).round() as i64;
*seconds += frac_nanos / 1_000_000_000;
*nanos += frac_nanos % 1_000_000_000;
}
}
b'S' | b's' => {
if let Some(dot_pos) = num_str.find('.') {
let int_part = &num_str[..dot_pos];
let frac_part = &num_str[dot_pos + 1..];
let negative = int_part.starts_with('-');
let int_val: i64 = if int_part.is_empty() || int_part == "-" {
0
} else {
int_part.parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?
};
let frac_nanos = parse_frac_nanos(frac_part)? as i64;
*seconds += int_val;
*nanos += if negative { -frac_nanos } else { frac_nanos };
} else {
let n: i64 = num_str.parse().map_err(|e: std::num::ParseIntError| {
GraphError::Serialization {
context: String::new(),
source: e.to_string(),
hint: None,
}
})?;
*seconds += n;
}
}
_ => {
return Err(GraphError::Serialization {
context: String::new(),
source: format!("unexpected char '{b}' in duration time part"),
hint: None,
})
}
}
i += 1;
num_start = i;
if i < bytes.len() && bytes[i] == b'-' {
i += 1;
}
}
Ok(())
}
pub fn extract_date(val: &Value) -> Option<NaiveDate> {
match val {
Value::Date(d) => Some(d.0),
Value::LocalDateTime(dt) => Some(dt.0.date()),
Value::DateTime(dt) => Some(dt.0.date()),
_ => None,
}
}
pub fn extract_time(val: &Value) -> Option<NaiveTime> {
match val {
Value::Date(_) => Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
Value::LocalTime(t) => Some(t.0),
Value::Time(t) => Some(t.0),
Value::LocalDateTime(dt) => Some(dt.0.time()),
Value::DateTime(dt) => Some(dt.0.time()),
_ => None,
}
}
pub fn extract_offset_secs(val: &Value) -> i32 {
match val {
Value::Time(t) => t.1.local_minus_utc(),
Value::DateTime(dt) => dt.1.local_minus_utc(),
_ => 0,
}
}
fn is_time_only(val: &Value) -> bool {
matches!(val, Value::LocalTime(_) | Value::Time(_))
}
fn has_date(val: &Value) -> bool {
matches!(
val,
Value::Date(_) | Value::LocalDateTime(_) | Value::DateTime(_)
)
}
fn is_offset_aware(val: &Value) -> bool {
matches!(val, Value::Time(_) | Value::DateTime(_))
}
fn extract_tz_name(val: &Value) -> Option<&str> {
if let Value::DateTime(dt) = val {
dt.2.as_deref()
} else {
None
}
}
fn resolve_offset_in_tz(date: NaiveDate, time: NaiveTime, tz_name: &str) -> i64 {
if let Ok(tz) = tz_name.parse::<chrono_tz::Tz>() {
let ndt = NaiveDateTime::new(date, time);
if let Some(aware) = tz.from_local_datetime(&ndt).earliest() {
return aware.offset().fix().local_minus_utc() as i64;
}
}
0
}
fn effective_offset_dst(val: &Value, other: &Value) -> i64 {
if is_offset_aware(val) && is_offset_aware(other) {
return extract_offset_secs(val) as i64;
}
if is_offset_aware(val) {
if extract_tz_name(val).is_some() && !is_offset_aware(other) {
return extract_offset_secs(val) as i64;
}
return 0;
}
if let Some(tz_name) = extract_tz_name(other) {
if !is_offset_aware(val) {
let date = extract_date(val)
.or_else(|| extract_date(other))
.unwrap_or(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
let time = extract_time(val).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
return resolve_offset_in_tz(date, time, tz_name);
}
}
0
}
fn days_in_month(year: i32, month: u32) -> u32 {
if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1)
}
.unwrap()
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
.num_days() as u32
}
fn add_months_to_date(d: NaiveDate, months: i64) -> NaiveDate {
let total_months = d.year() as i64 * 12 + (d.month() as i64 - 1) + months;
let y = total_months.div_euclid(12) as i32;
let m = (total_months.rem_euclid(12) + 1) as u32;
let max_day = days_in_month(y, m);
let day = (d.day()).min(max_day);
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
fn time_to_nanos(t: NaiveTime) -> i64 {
t.num_seconds_from_midnight() as i64 * 1_000_000_000 + t.nanosecond() as i64
}
fn month_diff(d1: NaiveDate, d2: NaiveDate) -> i64 {
let mut months =
(d2.year() as i64 - d1.year() as i64) * 12 + d2.month() as i64 - d1.month() as i64;
let adjusted = add_months_to_date(d1, months);
if months > 0 && adjusted > d2 {
months -= 1;
} else if months < 0 && adjusted < d2 {
months += 1;
}
months
}
pub fn duration_between(lhs: &Value, rhs: &Value) -> CypherDuration {
let d1 = extract_date(lhs);
let d2 = extract_date(rhs);
let t1 = extract_time(lhs);
let t2 = extract_time(rhs);
let both_have_dates = d1.is_some() && d2.is_some() && has_date(lhs) && has_date(rhs);
let either_time_only = is_time_only(lhs) || is_time_only(rhs);
if either_time_only && !both_have_dates {
let lhs_nanos =
t1.map(time_to_nanos).unwrap_or(0) - effective_offset_dst(lhs, rhs) * 1_000_000_000;
let rhs_nanos =
t2.map(time_to_nanos).unwrap_or(0) - effective_offset_dst(rhs, lhs) * 1_000_000_000;
let diff_nanos = rhs_nanos - lhs_nanos;
let total_secs = diff_nanos.div_euclid(1_000_000_000);
let rem_nanos = diff_nanos.rem_euclid(1_000_000_000);
return CypherDuration {
months: 0,
days: 0,
seconds: total_secs,
nanos: rem_nanos,
};
}
if let (Some(date1), Some(date2)) = (d1, d2) {
let months = month_diff(date1, date2);
let d1_advanced = add_months_to_date(date1, months);
let mut day_diff = date2.signed_duration_since(d1_advanced).num_days();
let t1_nanos =
t1.map(time_to_nanos).unwrap_or(0) - effective_offset_dst(lhs, rhs) * 1_000_000_000;
let t2_nanos =
t2.map(time_to_nanos).unwrap_or(0) - effective_offset_dst(rhs, lhs) * 1_000_000_000;
let mut time_diff_nanos = t2_nanos - t1_nanos;
let nanos_per_day: i64 = 86_400_000_000_000;
if day_diff > 0 && time_diff_nanos < 0 {
day_diff -= 1;
time_diff_nanos += nanos_per_day;
} else if day_diff < 0 && time_diff_nanos > 0 {
day_diff += 1;
time_diff_nanos -= nanos_per_day;
}
let secs = time_diff_nanos.div_euclid(1_000_000_000);
let ns = time_diff_nanos.rem_euclid(1_000_000_000);
CypherDuration {
months,
days: day_diff,
seconds: secs,
nanos: ns,
}
} else {
CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
}
}
}
pub fn duration_in_months(lhs: &Value, rhs: &Value) -> CypherDuration {
let d1 = extract_date(lhs);
let d2 = extract_date(rhs);
if is_time_only(lhs) || is_time_only(rhs) || d1.is_none() || d2.is_none() {
return CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
};
}
let date1 = d1.unwrap();
let date2 = d2.unwrap();
let mut months = month_diff(date1, date2);
let d1_advanced = add_months_to_date(date1, months);
if d1_advanced == date2 {
let t1_nanos = extract_time(lhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(lhs, rhs) * 1_000_000_000;
let t2_nanos = extract_time(rhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(rhs, lhs) * 1_000_000_000;
if months > 0 && t2_nanos < t1_nanos {
months -= 1;
} else if months < 0 && t2_nanos > t1_nanos {
months += 1;
}
}
CypherDuration {
months,
days: 0,
seconds: 0,
nanos: 0,
}
}
pub fn duration_in_days(lhs: &Value, rhs: &Value) -> CypherDuration {
let d1 = extract_date(lhs);
let d2 = extract_date(rhs);
if is_time_only(lhs) || is_time_only(rhs) {
return CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
};
}
if let (Some(date1), Some(date2)) = (d1, d2) {
let day_nanos = date2.signed_duration_since(date1).num_days() * 86_400_000_000_000i64;
let t1_nanos = extract_time(lhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(lhs, rhs) * 1_000_000_000;
let t2_nanos = extract_time(rhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(rhs, lhs) * 1_000_000_000;
let total_nanos = day_nanos + (t2_nanos - t1_nanos);
let nanos_per_day: i64 = 86_400_000_000_000;
let days = total_nanos / nanos_per_day;
CypherDuration {
months: 0,
days,
seconds: 0,
nanos: 0,
}
} else {
CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
}
}
}
pub fn duration_in_seconds(lhs: &Value, rhs: &Value) -> CypherDuration {
let d1 = extract_date(lhs);
let d2 = extract_date(rhs);
let either_time_only = is_time_only(lhs) || is_time_only(rhs);
let t1_nanos = extract_time(lhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(lhs, rhs) * 1_000_000_000;
let t2_nanos = extract_time(rhs).map(time_to_nanos).unwrap_or(0)
- effective_offset_dst(rhs, lhs) * 1_000_000_000;
let time_diff = t2_nanos - t1_nanos;
if either_time_only && !(has_date(lhs) && has_date(rhs)) {
let secs = time_diff.div_euclid(1_000_000_000);
let ns = time_diff.rem_euclid(1_000_000_000);
return CypherDuration {
months: 0,
days: 0,
seconds: secs,
nanos: ns,
};
}
if let (Some(date1), Some(date2)) = (d1, d2) {
let total_day_secs = date2.signed_duration_since(date1).num_days() * 86400;
let total_nanos = total_day_secs * 1_000_000_000 + time_diff;
let secs = total_nanos.div_euclid(1_000_000_000);
let ns = total_nanos.rem_euclid(1_000_000_000);
CypherDuration {
months: 0,
days: 0,
seconds: secs,
nanos: ns,
}
} else {
CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
}
}
}
pub fn parse_offset_public(s: &str) -> Result<i32> {
parse_offset(s).map(|off| off.local_minus_utc())
}
pub fn truncate_date(unit: &str, val: &Value, map: &BTreeMap<String, Value>) -> Result<NaiveDate> {
let date = extract_date(val).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "truncate_date requires a temporal with a date component".into(),
hint: None,
})?;
let mut d = truncate_date_core(unit, date)?;
if let Some(Value::I64(day)) = map.get("day") {
d = d
.with_day(*day as u32)
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid day: {day}"),
hint: None,
})?;
}
if let Some(Value::I64(dow)) = map.get("dayOfWeek") {
let current_dow = d.weekday().num_days_from_monday() as i64 + 1;
let delta = *dow - current_dow;
d += chrono::Duration::days(delta);
}
if let Some(Value::I64(m)) = map.get("month") {
d = d
.with_month(*m as u32)
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid month: {m}"),
hint: None,
})?;
}
Ok(d)
}
fn truncate_date_core(unit: &str, date: NaiveDate) -> Result<NaiveDate> {
let y = date.year();
match unit {
"millennium" => Ok(NaiveDate::from_ymd_opt((y / 1000) * 1000, 1, 1).unwrap()),
"century" => Ok(NaiveDate::from_ymd_opt((y / 100) * 100, 1, 1).unwrap()),
"decade" => Ok(NaiveDate::from_ymd_opt((y / 10) * 10, 1, 1).unwrap()),
"year" => Ok(NaiveDate::from_ymd_opt(y, 1, 1).unwrap()),
"weekYear" => {
let iso_year = date.iso_week().year();
Ok(NaiveDate::from_isoywd_opt(iso_year, 1, chrono::Weekday::Mon).unwrap())
}
"quarter" => {
let q = (date.month() - 1) / 3;
let first_month = q * 3 + 1;
Ok(NaiveDate::from_ymd_opt(y, first_month, 1).unwrap())
}
"month" => Ok(NaiveDate::from_ymd_opt(y, date.month(), 1).unwrap()),
"week" => {
let dow = date.weekday().num_days_from_monday() as i64;
Ok(date - chrono::Duration::days(dow))
}
"day" | "hour" | "minute" | "second" | "millisecond" | "microsecond" => Ok(date),
_ => Err(GraphError::Serialization {
context: String::new(),
source: format!("unsupported truncation unit for date: {unit}"),
hint: None,
}),
}
}
pub fn truncate_time(unit: &str, val: &Value, map: &BTreeMap<String, Value>) -> Result<NaiveTime> {
let time = extract_time(val).ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: "truncate_time requires a temporal with a time component".into(),
hint: None,
})?;
let mut t = truncate_time_core(unit, time)?;
if let Some(Value::I64(ns)) = map.get("nanosecond") {
let truncated_nanos = t.nanosecond();
let new_nanos = truncated_nanos + *ns as u32;
t = t
.with_nanosecond(new_nanos)
.ok_or_else(|| GraphError::Serialization {
context: String::new(),
source: format!("invalid nanosecond: {ns}"),
hint: None,
})?;
}
Ok(t)
}
fn truncate_time_core(unit: &str, time: NaiveTime) -> Result<NaiveTime> {
match unit {
"millennium" | "century" | "decade" | "year" | "weekYear" | "quarter" | "month"
| "week" | "day" => Ok(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
"hour" => Ok(NaiveTime::from_hms_opt(time.hour(), 0, 0).unwrap()),
"minute" => Ok(NaiveTime::from_hms_opt(time.hour(), time.minute(), 0).unwrap()),
"second" => Ok(NaiveTime::from_hms_opt(time.hour(), time.minute(), time.second()).unwrap()),
"millisecond" => {
let ms = time.nanosecond() / 1_000_000;
Ok(NaiveTime::from_hms_nano_opt(
time.hour(),
time.minute(),
time.second(),
ms * 1_000_000,
)
.unwrap())
}
"microsecond" => {
let us = time.nanosecond() / 1_000;
Ok(
NaiveTime::from_hms_nano_opt(time.hour(), time.minute(), time.second(), us * 1_000)
.unwrap(),
)
}
_ => Err(GraphError::Serialization {
context: String::new(),
source: format!("unsupported truncation unit for time: {unit}"),
hint: None,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_display() {
let d = CypherDate(NaiveDate::from_ymd_opt(1984, 10, 11).unwrap());
assert_eq!(d.to_string(), "1984-10-11");
}
#[test]
fn test_date_parse() {
let d = CypherDate::from_iso_string("1984-10-11").unwrap();
assert_eq!(d.0, NaiveDate::from_ymd_opt(1984, 10, 11).unwrap());
let d2 = CypherDate::from_iso_string("19841011").unwrap();
assert_eq!(d2.0, d.0);
let d3 = CypherDate::from_iso_string("1984").unwrap();
assert_eq!(d3.0, NaiveDate::from_ymd_opt(1984, 1, 1).unwrap());
let d4 = CypherDate::from_iso_string("1984-10").unwrap();
assert_eq!(d4.0, NaiveDate::from_ymd_opt(1984, 10, 1).unwrap());
}
#[test]
fn test_date_week_parse() {
let d = CypherDate::from_iso_string("2015-W30-2").unwrap();
assert_eq!(
d.0,
NaiveDate::from_isoywd_opt(2015, 30, chrono::Weekday::Tue).unwrap()
);
let d2 = CypherDate::from_iso_string("2015W302").unwrap();
assert_eq!(d2.0, d.0);
let d3 = CypherDate::from_iso_string("2015-W30").unwrap();
assert_eq!(
d3.0,
NaiveDate::from_isoywd_opt(2015, 30, chrono::Weekday::Mon).unwrap()
);
}
#[test]
fn test_date_ordinal_parse() {
let d = CypherDate::from_iso_string("1984-202").unwrap();
assert_eq!(d.0, NaiveDate::from_yo_opt(1984, 202).unwrap());
let d2 = CypherDate::from_iso_string("1984202").unwrap();
assert_eq!(d2.0, d.0);
}
#[test]
fn test_local_time_display() {
let t = CypherLocalTime(NaiveTime::from_hms_nano_opt(12, 31, 14, 645_876_123).unwrap());
assert_eq!(t.to_string(), "12:31:14.645876123");
let t2 = CypherLocalTime(NaiveTime::from_hms_opt(21, 40, 0).unwrap());
assert_eq!(t2.to_string(), "21:40");
let t3 = CypherLocalTime(NaiveTime::from_hms_opt(21, 40, 32).unwrap());
assert_eq!(t3.to_string(), "21:40:32");
let t4 = CypherLocalTime(NaiveTime::from_hms_nano_opt(21, 40, 32, 142_000_000).unwrap());
assert_eq!(t4.to_string(), "21:40:32.142");
}
#[test]
fn test_time_display() {
let utc = FixedOffset::east_opt(0).unwrap();
let plus1 = FixedOffset::east_opt(3600).unwrap();
let minus130 = FixedOffset::west_opt(5400).unwrap();
let t1 = CypherTime(
NaiveTime::from_hms_nano_opt(21, 40, 32, 142_000_000).unwrap(),
utc,
);
assert_eq!(t1.to_string(), "21:40:32.142Z");
let t2 = CypherTime(NaiveTime::from_hms_opt(21, 40, 32).unwrap(), plus1);
assert_eq!(t2.to_string(), "21:40:32+01:00");
let t3 = CypherTime(NaiveTime::from_hms_opt(21, 40, 0).unwrap(), minus130);
assert_eq!(t3.to_string(), "21:40-01:30");
}
#[test]
fn test_datetime_display() {
let utc = FixedOffset::east_opt(0).unwrap();
let plus1 = FixedOffset::east_opt(3600).unwrap();
let dt1 = CypherDateTime(
NaiveDate::from_ymd_opt(1984, 10, 11)
.unwrap()
.and_hms_opt(12, 31, 14)
.unwrap(),
utc,
None,
);
assert_eq!(dt1.to_string(), "1984-10-11T12:31:14Z");
let dt2 = CypherDateTime(
NaiveDate::from_ymd_opt(1984, 10, 11)
.unwrap()
.and_hms_opt(12, 31, 14)
.unwrap(),
plus1,
None,
);
assert_eq!(dt2.to_string(), "1984-10-11T12:31:14+01:00");
}
#[test]
fn test_duration_display() {
let d1 = CypherDuration {
months: 0,
days: 14,
seconds: 58320,
nanos: 0,
};
assert_eq!(d1.to_string(), "P14DT16H12M");
let d2 = CypherDuration {
months: 14,
days: 3,
seconds: 0,
nanos: 0,
};
assert_eq!(d2.to_string(), "P1Y2M3D");
let d3 = CypherDuration {
months: 0,
days: 0,
seconds: 0,
nanos: 0,
};
assert_eq!(d3.to_string(), "PT0S");
let d4 = CypherDuration {
months: 0,
days: 0,
seconds: -79200,
nanos: 0,
};
assert_eq!(d4.to_string(), "PT-22H");
}
#[test]
fn test_duration_parse() {
let d = CypherDuration::from_iso_string("P14DT16H12M").unwrap();
assert_eq!(d.days, 14);
assert_eq!(d.seconds, 16 * 3600 + 12 * 60);
let d2 = CypherDuration::from_iso_string("P1Y2M3D").unwrap();
assert_eq!(d2.months, 14);
assert_eq!(d2.days, 3);
let d3 = CypherDuration::from_iso_string("PT0S").unwrap();
assert_eq!(d3.months, 0);
assert_eq!(d3.days, 0);
assert_eq!(d3.seconds, 0);
}
#[test]
fn test_time_parse() {
let t = CypherLocalTime::from_iso_string("12:31:14.645876123").unwrap();
assert_eq!(
t.0,
NaiveTime::from_hms_nano_opt(12, 31, 14, 645_876_123).unwrap()
);
let t2 = CypherLocalTime::from_iso_string("21:40").unwrap();
assert_eq!(t2.0, NaiveTime::from_hms_opt(21, 40, 0).unwrap());
let t3 = CypherLocalTime::from_iso_string("14").unwrap();
assert_eq!(t3.0, NaiveTime::from_hms_opt(14, 0, 0).unwrap());
}
#[test]
fn test_offset_parse() {
let t = CypherTime::from_iso_string("21:40:32.142Z").unwrap();
assert_eq!(t.1, FixedOffset::east_opt(0).unwrap());
let t2 = CypherTime::from_iso_string("21:40:32+01:00").unwrap();
assert_eq!(t2.1, FixedOffset::east_opt(3600).unwrap());
let t3 = CypherTime::from_iso_string("21:40-01:30").unwrap();
assert_eq!(t3.1, FixedOffset::west_opt(5400).unwrap());
}
#[test]
fn test_serde_roundtrip_date() {
let d = CypherDate(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap());
let bytes = rmp_serde::to_vec_named(&d).unwrap();
let d2: CypherDate = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(d, d2);
}
#[test]
fn test_serde_roundtrip_duration() {
let d = CypherDuration {
months: 14,
days: 3,
seconds: 7200,
nanos: 500,
};
let bytes = rmp_serde::to_vec_named(&d).unwrap();
let d2: CypherDuration = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(d, d2);
}
#[test]
fn test_date_from_map() {
let mut map = BTreeMap::new();
map.insert("year".to_string(), Value::I64(1984));
map.insert("month".to_string(), Value::I64(10));
map.insert("day".to_string(), Value::I64(11));
let d = CypherDate::from_map(&map).unwrap();
assert_eq!(d.0, NaiveDate::from_ymd_opt(1984, 10, 11).unwrap());
}
#[test]
fn test_duration_from_map() {
let mut map = BTreeMap::new();
map.insert("years".to_string(), Value::I64(1));
map.insert("months".to_string(), Value::I64(2));
map.insert("days".to_string(), Value::I64(3));
map.insert("hours".to_string(), Value::I64(4));
let d = CypherDuration::from_map(&map).unwrap();
assert_eq!(d.months, 14);
assert_eq!(d.days, 3);
assert_eq!(d.seconds, 14400);
}
}