1use crate::error::{DecodeError, EncodeError};
2use crate::property::{PropertyValue, PropertyValueRef};
3use jiff::civil::{Date, DateTime, Time};
4use jiff::tz::{Offset, TimeZone};
5use jiff::{Span, Timestamp};
6use std::fmt;
7use std::str::FromStr;
8
9pub const NANOS_PER_SECOND: i64 = 1_000_000_000;
10pub const NANOS_PER_MINUTE: i64 = 60 * NANOS_PER_SECOND;
11pub const NANOS_PER_HOUR: i64 = 60 * NANOS_PER_MINUTE;
12pub const NANOS_PER_DAY: i64 = 86_400 * NANOS_PER_SECOND;
13const DAYS_PER_AVERAGE_YEAR: f64 = 365.2425;
14const DAYS_PER_AVERAGE_MONTH: f64 = DAYS_PER_AVERAGE_YEAR / 12.0;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum CurrentTemporalKind {
18 Date,
19 LocalTime,
20 Time,
21 LocalDateTime,
22 DateTime,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct DateValue {
27 inner: Date,
28}
29
30impl DateValue {
31 pub fn new(year: i32, month: u8, day: u8) -> Result<Self, EncodeError> {
32 let year = i16::try_from(year)
33 .map_err(|_| EncodeError::InvalidTemporal("year must be in -9999..=9999"))?;
34 let month = i8::try_from(month)
35 .map_err(|_| EncodeError::InvalidTemporal("month must be in 1..=12"))?;
36 let day =
37 i8::try_from(day).map_err(|_| EncodeError::InvalidTemporal("day must be in 1..=31"))?;
38 let inner = Date::new(year, month, day)
39 .map_err(|_| EncodeError::InvalidTemporal("invalid calendar date"))?;
40 Ok(Self { inner })
41 }
42
43 pub fn from_calendar(
44 year: i32,
45 month: Option<u8>,
46 day: Option<u8>,
47 ) -> Result<Self, EncodeError> {
48 Self::new(year, month.unwrap_or(1), day.unwrap_or(1))
49 }
50
51 pub fn from_iso_week(
52 year: i32,
53 week: u8,
54 day_of_week: Option<u8>,
55 ) -> Result<Self, EncodeError> {
56 if week == 0 || week > 53 {
57 return Err(EncodeError::InvalidTemporal("week must be in 1..=53"));
58 }
59 let day_of_week = day_of_week.unwrap_or(1);
60 if !(1..=7).contains(&day_of_week) {
61 return Err(EncodeError::InvalidTemporal("dayOfWeek must be in 1..=7"));
62 }
63
64 let start = iso_week_one_monday_epoch_day(year)?;
65 let day = start + (i64::from(week) - 1) * 7 + (i64::from(day_of_week) - 1);
66 let value = Self::from_epoch_day(day)?;
67 if value.iso_week_year()? != year {
68 return Err(EncodeError::InvalidTemporal("invalid ISO week date"));
69 }
70 Ok(value)
71 }
72
73 pub fn from_ordinal_day(year: i32, ordinal_day: u16) -> Result<Self, EncodeError> {
74 let jan1 = Self::new(year, 1, 1)?;
75 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
76 if ordinal_day == 0 || ordinal_day > days_in_year {
77 return Err(EncodeError::InvalidTemporal(
78 "ordinalDay must be valid for year",
79 ));
80 }
81 Self::from_epoch_day(jan1.epoch_day() + i64::from(ordinal_day) - 1)
82 }
83
84 pub fn from_quarter(
85 year: i32,
86 quarter: u8,
87 day_of_quarter: Option<u16>,
88 ) -> Result<Self, EncodeError> {
89 if !(1..=4).contains(&quarter) {
90 return Err(EncodeError::InvalidTemporal("quarter must be in 1..=4"));
91 }
92 let day_of_quarter = day_of_quarter.unwrap_or(1);
93 if day_of_quarter == 0 {
94 return Err(EncodeError::InvalidTemporal(
95 "dayOfQuarter must be positive",
96 ));
97 }
98
99 let month = (quarter - 1) * 3 + 1;
100 let start = Self::new(year, month, 1)?;
101 let value = Self::from_epoch_day(start.epoch_day() + i64::from(day_of_quarter) - 1)?;
102 if value.year() != year || ((value.month() - 1) / 3) + 1 != quarter {
103 return Err(EncodeError::InvalidTemporal(
104 "dayOfQuarter must be valid for quarter",
105 ));
106 }
107 Ok(value)
108 }
109
110 pub fn from_epoch_day(epoch_day: i64) -> Result<Self, EncodeError> {
111 let (year, month, day) = ymd_from_epoch_day(epoch_day)?;
112 Self::new(year, month, day)
113 }
114
115 pub fn year(&self) -> i32 {
116 i32::from(self.inner.year())
117 }
118
119 pub fn month(&self) -> u8 {
120 self.inner.month() as u8
121 }
122
123 pub fn day(&self) -> u8 {
124 self.inner.day() as u8
125 }
126
127 pub fn as_jiff(&self) -> Date {
128 self.inner
129 }
130
131 pub fn epoch_day(&self) -> i64 {
132 epoch_day_from_ymd(self.year(), self.month(), self.day())
133 }
134
135 pub fn iso_week_year(&self) -> Result<i32, EncodeError> {
136 let day = self.epoch_day();
137 let year = self.year();
138 if day < iso_week_one_monday_epoch_day(year)? {
139 Ok(year - 1)
140 } else if day >= iso_week_one_monday_epoch_day(year + 1)? {
141 Ok(year + 1)
142 } else {
143 Ok(year)
144 }
145 }
146
147 pub fn iso_day_of_week(&self) -> u8 {
148 day_of_week_from_epoch_day(self.epoch_day())
149 }
150
151 pub fn iso_week(&self) -> Result<u8, EncodeError> {
152 let week_year = self.iso_week_year()?;
153 let week_one = iso_week_one_monday_epoch_day(week_year)?;
154 Ok(((self.epoch_day() - week_one) / 7 + 1) as u8)
155 }
156
157 pub fn quarter(&self) -> u8 {
158 (self.month() - 1) / 3 + 1
159 }
160
161 pub fn ordinal_day(&self) -> Result<u16, EncodeError> {
162 let jan1 = Self::new(self.year(), 1, 1)?;
163 Ok((self.epoch_day() - jan1.epoch_day() + 1) as u16)
164 }
165
166 pub fn day_of_quarter(&self) -> Result<u16, EncodeError> {
167 let start = Self::new(self.year(), (self.quarter() - 1) * 3 + 1, 1)?;
168 Ok((self.epoch_day() - start.epoch_day() + 1) as u16)
169 }
170
171 pub fn checked_add_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
172 self.checked_add_duration_groups(
173 duration.total_months(),
174 duration.total_days(),
175 duration.total_time_nanos(),
176 )
177 }
178
179 pub fn checked_sub_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
180 self.checked_add_duration_groups(
181 checked_i64_from_i128(-i128::from(duration.total_months()))?,
182 checked_i64_from_i128(-i128::from(duration.total_days()))?,
183 duration
184 .total_time_nanos()
185 .checked_neg()
186 .ok_or(EncodeError::InvalidTemporal("duration arithmetic overflow"))?,
187 )
188 }
189
190 fn checked_add_duration_groups(
191 &self,
192 months: i64,
193 days: i64,
194 time_nanos: i128,
195 ) -> Result<Self, EncodeError> {
196 let date = self.checked_add_months(months)?;
197 let time_days = checked_i64_from_i128(time_nanos / i128::from(NANOS_PER_DAY))?;
198 date.checked_add_days(
199 days.checked_add(time_days)
200 .ok_or(EncodeError::InvalidTemporal(
201 "date duration arithmetic overflow",
202 ))?,
203 )
204 }
205
206 fn checked_add_months(&self, months: i64) -> Result<Self, EncodeError> {
207 let month_index = i128::from(self.year()) * 12 + i128::from(self.month()) - 1;
208 let target =
209 month_index
210 .checked_add(i128::from(months))
211 .ok_or(EncodeError::InvalidTemporal(
212 "date duration arithmetic overflow",
213 ))?;
214 let year = checked_i32_from_i128(target.div_euclid(12))?;
215 let month = u8::try_from(target.rem_euclid(12) + 1)
216 .map_err(|_| EncodeError::InvalidTemporal("month must be in 1..=12"))?;
217 let day = self.day().min(days_in_month(year, month));
218 Self::new(year, month, day)
219 }
220
221 fn checked_add_days(&self, days: i64) -> Result<Self, EncodeError> {
222 let epoch_day = i128::from(self.epoch_day())
223 .checked_add(i128::from(days))
224 .ok_or(EncodeError::InvalidTemporal(
225 "date duration arithmetic overflow",
226 ))?;
227 Self::from_epoch_day(checked_i64_from_i128(epoch_day)?)
228 }
229}
230
231impl fmt::Display for DateValue {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 self.inner.fmt(f)
234 }
235}
236
237impl FromStr for DateValue {
238 type Err = DecodeError;
239
240 fn from_str(s: &str) -> Result<Self, Self::Err> {
241 parse_open_cypher_date(s)
242 }
243}
244
245#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
246pub struct LocalTimeValue {
247 inner: Time,
248}
249
250impl LocalTimeValue {
251 pub fn new(hour: u8, minute: u8, second: u8, nanos: u32) -> Result<Self, EncodeError> {
252 let hour = i8::try_from(hour)
253 .map_err(|_| EncodeError::InvalidTemporal("hour must be in 0..=23"))?;
254 let minute = i8::try_from(minute)
255 .map_err(|_| EncodeError::InvalidTemporal("minute must be in 0..=59"))?;
256 let second = i8::try_from(second)
257 .map_err(|_| EncodeError::InvalidTemporal("second must be in 0..=59"))?;
258 let nanos = i32::try_from(nanos)
259 .map_err(|_| EncodeError::InvalidTemporal("nanos must be < 1_000_000_000"))?;
260 let inner = Time::new(hour, minute, second, nanos)
261 .map_err(|_| EncodeError::InvalidTemporal("invalid wall clock time"))?;
262 Ok(Self { inner })
263 }
264
265 pub fn hour(&self) -> u8 {
266 self.inner.hour() as u8
267 }
268
269 pub fn minute(&self) -> u8 {
270 self.inner.minute() as u8
271 }
272
273 pub fn second(&self) -> u8 {
274 self.inner.second() as u8
275 }
276
277 pub fn nanos(&self) -> u32 {
278 self.inner.subsec_nanosecond() as u32
279 }
280
281 pub fn as_jiff(&self) -> Time {
282 self.inner
283 }
284
285 pub fn nanos_since_midnight(&self) -> i64 {
286 (((self.hour() as i64 * 60 + self.minute() as i64) * 60 + self.second() as i64)
287 * NANOS_PER_SECOND)
288 + self.nanos() as i64
289 }
290
291 pub fn from_nanos_since_midnight(nanos: i64) -> Result<Self, EncodeError> {
292 if !(0..NANOS_PER_DAY).contains(&nanos) {
293 return Err(EncodeError::InvalidTemporal(
294 "nanos since midnight must be in 0..<24h",
295 ));
296 }
297 let hour = nanos / NANOS_PER_HOUR;
298 let nanos = nanos % NANOS_PER_HOUR;
299 let minute = nanos / NANOS_PER_MINUTE;
300 let nanos = nanos % NANOS_PER_MINUTE;
301 let second = nanos / NANOS_PER_SECOND;
302 let nanos = nanos % NANOS_PER_SECOND;
303 Self::new(hour as u8, minute as u8, second as u8, nanos as u32)
304 }
305
306 pub fn checked_add_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
307 self.checked_add_time_nanos(duration.total_time_nanos())
308 }
309
310 pub fn checked_sub_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
311 self.checked_add_time_nanos(
312 duration
313 .total_time_nanos()
314 .checked_neg()
315 .ok_or(EncodeError::InvalidTemporal("duration arithmetic overflow"))?,
316 )
317 }
318
319 fn checked_add_time_nanos(&self, nanos: i128) -> Result<Self, EncodeError> {
320 let shifted = i128::from(self.nanos_since_midnight())
321 .checked_add(nanos)
322 .ok_or(EncodeError::InvalidTemporal(
323 "time duration arithmetic overflow",
324 ))?;
325 let wrapped = shifted.rem_euclid(i128::from(NANOS_PER_DAY));
326 Self::from_nanos_since_midnight(checked_i64_from_i128(wrapped)?)
327 }
328}
329
330impl fmt::Display for LocalTimeValue {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 self.inner.fmt(f)
333 }
334}
335
336impl FromStr for LocalTimeValue {
337 type Err = DecodeError;
338
339 fn from_str(s: &str) -> Result<Self, Self::Err> {
340 let inner = s
341 .parse::<Time>()
342 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))?;
343 Ok(Self { inner })
344 }
345}
346
347#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
348pub struct UtcOffsetValue {
349 seconds_east: i32,
350}
351
352impl UtcOffsetValue {
353 pub fn utc() -> Self {
354 Self { seconds_east: 0 }
355 }
356
357 pub fn new(seconds_east: i32) -> Result<Self, EncodeError> {
358 Offset::from_seconds(seconds_east)
359 .map_err(|_| EncodeError::InvalidTemporal("offset out of range"))?;
360 Ok(Self { seconds_east })
361 }
362
363 pub fn seconds_east(&self) -> i32 {
364 self.seconds_east
365 }
366
367 pub fn is_utc(&self) -> bool {
368 self.seconds_east == 0
369 }
370
371 pub fn as_jiff(&self) -> Offset {
372 Offset::from_seconds(self.seconds_east)
373 .expect("UtcOffsetValue should always store a validated offset")
374 }
375}
376
377impl fmt::Display for UtcOffsetValue {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 if self.seconds_east == 0 {
380 return f.write_str("Z");
381 }
382 let sign = if self.seconds_east < 0 { '-' } else { '+' };
383 let abs = self.seconds_east.abs();
384 let hours = abs / 3600;
385 let minutes = (abs % 3600) / 60;
386 let seconds = abs % 60;
387 if seconds == 0 {
388 write!(f, "{sign}{hours:02}:{minutes:02}")
389 } else {
390 write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
391 }
392 }
393}
394
395impl FromStr for UtcOffsetValue {
396 type Err = DecodeError;
397
398 fn from_str(s: &str) -> Result<Self, Self::Err> {
399 parse_offset(s)
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
404pub struct ZonedTimeValue {
405 pub time: LocalTimeValue,
406 pub offset: UtcOffsetValue,
407}
408
409impl ZonedTimeValue {
410 pub fn new(time: LocalTimeValue, offset: UtcOffsetValue) -> Self {
411 Self { time, offset }
412 }
413
414 pub fn utc_nanos_since_midnight(&self) -> i64 {
415 let local = self.time.nanos_since_midnight();
416 let offset = self.offset.seconds_east() as i64 * NANOS_PER_SECOND;
417 (local - offset).rem_euclid(NANOS_PER_DAY)
418 }
419
420 pub fn with_offset_same_instant(&self, offset: UtcOffsetValue) -> Result<Self, EncodeError> {
421 let local =
422 self.utc_nanos_since_midnight() + offset.seconds_east() as i64 * NANOS_PER_SECOND;
423 Ok(Self {
424 time: LocalTimeValue::from_nanos_since_midnight(local.rem_euclid(NANOS_PER_DAY))?,
425 offset,
426 })
427 }
428
429 pub fn checked_add_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
430 Ok(Self {
431 time: self.time.checked_add_duration(duration)?,
432 offset: self.offset,
433 })
434 }
435
436 pub fn checked_sub_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
437 Ok(Self {
438 time: self.time.checked_sub_duration(duration)?,
439 offset: self.offset,
440 })
441 }
442}
443
444impl fmt::Display for ZonedTimeValue {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 write!(f, "{}{}", self.time, self.offset)
447 }
448}
449
450impl FromStr for ZonedTimeValue {
451 type Err = DecodeError;
452
453 fn from_str(s: &str) -> Result<Self, Self::Err> {
454 let idx = find_offset_start(s, false)
455 .ok_or_else(|| DecodeError::InvalidTemporal("missing zone offset".into()))?;
456 Ok(Self {
457 time: s[..idx].parse()?,
458 offset: s[idx..].parse()?,
459 })
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
464pub struct LocalDateTimeValue {
465 inner: DateTime,
466}
467
468impl LocalDateTimeValue {
469 pub fn new(
470 year: i32,
471 month: u8,
472 day: u8,
473 hour: u8,
474 minute: u8,
475 second: u8,
476 nanos: u32,
477 ) -> Result<Self, EncodeError> {
478 let year = i16::try_from(year)
479 .map_err(|_| EncodeError::InvalidTemporal("year must be in -9999..=9999"))?;
480 let month = i8::try_from(month)
481 .map_err(|_| EncodeError::InvalidTemporal("month must be in 1..=12"))?;
482 let day =
483 i8::try_from(day).map_err(|_| EncodeError::InvalidTemporal("day must be in 1..=31"))?;
484 let hour = i8::try_from(hour)
485 .map_err(|_| EncodeError::InvalidTemporal("hour must be in 0..=23"))?;
486 let minute = i8::try_from(minute)
487 .map_err(|_| EncodeError::InvalidTemporal("minute must be in 0..=59"))?;
488 let second = i8::try_from(second)
489 .map_err(|_| EncodeError::InvalidTemporal("second must be in 0..=59"))?;
490 let nanos = i32::try_from(nanos)
491 .map_err(|_| EncodeError::InvalidTemporal("nanos must be < 1_000_000_000"))?;
492 let inner = DateTime::new(year, month, day, hour, minute, second, nanos)
493 .map_err(|_| EncodeError::InvalidTemporal("invalid local datetime"))?;
494 Ok(Self { inner })
495 }
496
497 pub fn from_date_and_time(date: DateValue, time: LocalTimeValue) -> Result<Self, EncodeError> {
498 Self::new(
499 date.year(),
500 date.month(),
501 date.day(),
502 time.hour(),
503 time.minute(),
504 time.second(),
505 time.nanos(),
506 )
507 }
508
509 pub fn date(&self) -> DateValue {
510 DateValue {
511 inner: self.inner.date(),
512 }
513 }
514
515 pub fn time(&self) -> LocalTimeValue {
516 LocalTimeValue {
517 inner: self.inner.time(),
518 }
519 }
520
521 pub fn as_jiff(&self) -> DateTime {
522 self.inner
523 }
524
525 pub fn local_epoch_day_and_nanos(&self) -> (i64, i64) {
526 let date = self.date();
527 let time = self.time();
528 (date.epoch_day(), time.nanos_since_midnight())
529 }
530
531 pub fn checked_add_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
532 self.checked_add_duration_groups(
533 duration.total_months(),
534 duration.total_days(),
535 duration.total_time_nanos(),
536 )
537 }
538
539 pub fn checked_sub_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
540 self.checked_add_duration_groups(
541 checked_i64_from_i128(-i128::from(duration.total_months()))?,
542 checked_i64_from_i128(-i128::from(duration.total_days()))?,
543 duration
544 .total_time_nanos()
545 .checked_neg()
546 .ok_or(EncodeError::InvalidTemporal("duration arithmetic overflow"))?,
547 )
548 }
549
550 fn checked_add_duration_groups(
551 &self,
552 months: i64,
553 days: i64,
554 time_nanos: i128,
555 ) -> Result<Self, EncodeError> {
556 let date = self.date().checked_add_months(months)?;
557 let shifted_time = i128::from(self.time().nanos_since_midnight())
558 .checked_add(time_nanos)
559 .ok_or(EncodeError::InvalidTemporal(
560 "datetime duration arithmetic overflow",
561 ))?;
562 let time_days = shifted_time.div_euclid(i128::from(NANOS_PER_DAY));
563 let time_nanos = shifted_time.rem_euclid(i128::from(NANOS_PER_DAY));
564 let epoch_day = i128::from(date.epoch_day())
565 .checked_add(i128::from(days))
566 .and_then(|value| value.checked_add(time_days))
567 .ok_or(EncodeError::InvalidTemporal(
568 "datetime duration arithmetic overflow",
569 ))?;
570 let date = DateValue::from_epoch_day(checked_i64_from_i128(epoch_day)?)?;
571 let time = LocalTimeValue::from_nanos_since_midnight(checked_i64_from_i128(time_nanos)?)?;
572 Self::from_date_and_time(date, time)
573 }
574}
575
576impl fmt::Display for LocalDateTimeValue {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 self.inner.fmt(f)
579 }
580}
581
582impl FromStr for LocalDateTimeValue {
583 type Err = DecodeError;
584
585 fn from_str(s: &str) -> Result<Self, Self::Err> {
586 match s.parse::<DateTime>() {
587 Ok(inner) => Ok(Self { inner }),
588 Err(parse_err) => {
589 let (date, time) = s.split_once('T').ok_or_else(|| {
590 DecodeError::InvalidTemporal(format!("invalid local datetime: {parse_err}"))
591 })?;
592 Self::from_date_and_time(date.parse()?, time.parse()?)
593 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))
594 }
595 }
596 }
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
600pub struct ZonedDateTimeValue {
601 pub datetime: LocalDateTimeValue,
602 pub offset: UtcOffsetValue,
603 pub zone_id: Option<Box<str>>,
604}
605
606impl ZonedDateTimeValue {
607 pub fn new(
608 datetime: LocalDateTimeValue,
609 offset: UtcOffsetValue,
610 zone_id: Option<impl Into<Box<str>>>,
611 ) -> Result<Self, EncodeError> {
612 let zone_id = zone_id.map(Into::into);
613 if let Some(zone_id_str) = zone_id.as_deref() {
614 validate_zoned_components(&datetime, &offset, zone_id_str)
615 .map_err(|_| EncodeError::InvalidTemporal("invalid zone id or offset mismatch"))?;
616 }
617 Ok(Self {
618 datetime,
619 offset,
620 zone_id,
621 })
622 }
623
624 pub fn in_named_zone(
625 datetime: LocalDateTimeValue,
626 zone_id: impl Into<Box<str>>,
627 ) -> Result<Self, EncodeError> {
628 let zone_id = zone_id.into();
629 let zoned = datetime
630 .as_jiff()
631 .in_tz(zone_id.as_ref())
632 .map_err(|_| EncodeError::InvalidTemporal("invalid zone id or local datetime"))?;
633 Ok(Self {
634 datetime,
635 offset: UtcOffsetValue::new(zoned.offset().seconds())?,
636 zone_id: Some(zone_id),
637 })
638 }
639
640 pub fn from_epoch(seconds: i64, nanoseconds: u32) -> Result<Self, EncodeError> {
641 let nanoseconds = i32::try_from(nanoseconds)
642 .map_err(|_| EncodeError::InvalidTemporal("nanoseconds out of range"))?;
643 let timestamp = Timestamp::new(seconds, nanoseconds)
644 .map_err(|_| EncodeError::InvalidTemporal("epoch datetime out of range"))?;
645 Ok(Self::from_utc_timestamp(timestamp))
646 }
647
648 pub fn from_epoch_millis(milliseconds: i64) -> Result<Self, EncodeError> {
649 let timestamp = Timestamp::from_millisecond(milliseconds)
650 .map_err(|_| EncodeError::InvalidTemporal("epoch datetime out of range"))?;
651 Ok(Self::from_utc_timestamp(timestamp))
652 }
653
654 fn from_utc_timestamp(timestamp: Timestamp) -> Self {
655 let zoned = timestamp.to_zoned(TimeZone::UTC);
656 Self {
657 datetime: LocalDateTimeValue {
658 inner: zoned.datetime(),
659 },
660 offset: UtcOffsetValue::utc(),
661 zone_id: None,
662 }
663 }
664
665 pub fn utc_epoch_day_and_nanos(&self) -> (i64, i64) {
666 let (day, nanos) = self.datetime.local_epoch_day_and_nanos();
667 let offset = self.offset.seconds_east() as i64 * NANOS_PER_SECOND;
668 let shifted = nanos - offset;
669 (
670 day + shifted.div_euclid(NANOS_PER_DAY),
671 shifted.rem_euclid(NANOS_PER_DAY),
672 )
673 }
674
675 pub fn with_offset_same_instant(&self, offset: UtcOffsetValue) -> Result<Self, EncodeError> {
676 let (day, nanos) = self.utc_epoch_day_and_nanos();
677 Self::from_utc_epoch_day_and_nanos(day, nanos, offset, None::<Box<str>>)
678 }
679
680 #[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
681 pub fn with_named_zone_same_instant(
682 &self,
683 zone_id: impl Into<Box<str>>,
684 ) -> Result<Self, EncodeError> {
685 let zone_id = zone_id.into();
686 let (day, nanos) = self.utc_epoch_day_and_nanos();
687 let timestamp = timestamp_from_epoch_day_and_nanos(day, nanos)?;
688 let zoned = timestamp
689 .in_tz(zone_id.as_ref())
690 .map_err(|_| EncodeError::InvalidTemporal("invalid zone id"))?;
691 Ok(Self {
692 datetime: LocalDateTimeValue {
693 inner: zoned.datetime(),
694 },
695 offset: UtcOffsetValue::new(zoned.offset().seconds())?,
696 zone_id: Some(zone_id),
697 })
698 }
699
700 #[cfg(not(any(feature = "tzdb-system", feature = "tzdb-bundled")))]
701 pub fn with_named_zone_same_instant(
702 &self,
703 _zone_id: impl Into<Box<str>>,
704 ) -> Result<Self, EncodeError> {
705 Err(EncodeError::InvalidTemporal("named zones are not enabled"))
706 }
707
708 fn from_utc_epoch_day_and_nanos(
709 day: i64,
710 nanos: i64,
711 offset: UtcOffsetValue,
712 zone_id: Option<impl Into<Box<str>>>,
713 ) -> Result<Self, EncodeError> {
714 let shifted = i128::from(day) * i128::from(NANOS_PER_DAY)
715 + i128::from(nanos)
716 + i128::from(offset.seconds_east()) * i128::from(NANOS_PER_SECOND);
717 let local_day = shifted.div_euclid(i128::from(NANOS_PER_DAY));
718 let local_nanos = shifted.rem_euclid(i128::from(NANOS_PER_DAY));
719 let date = DateValue::from_epoch_day(checked_i64_from_i128(local_day)?)?;
720 let time = LocalTimeValue::from_nanos_since_midnight(checked_i64_from_i128(local_nanos)?)?;
721 Ok(Self {
722 datetime: LocalDateTimeValue::from_date_and_time(date, time)?,
723 offset,
724 zone_id: zone_id.map(Into::into),
725 })
726 }
727
728 pub fn checked_add_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
729 if self.zone_id.is_some() {
730 return Err(EncodeError::InvalidTemporal(
731 "named-zone datetime duration arithmetic is not implemented",
732 ));
733 }
734 Ok(Self {
735 datetime: self.datetime.checked_add_duration(duration)?,
736 offset: self.offset,
737 zone_id: None,
738 })
739 }
740
741 pub fn checked_sub_duration(&self, duration: &DurationValue) -> Result<Self, EncodeError> {
742 if self.zone_id.is_some() {
743 return Err(EncodeError::InvalidTemporal(
744 "named-zone datetime duration arithmetic is not implemented",
745 ));
746 }
747 Ok(Self {
748 datetime: self.datetime.checked_sub_duration(duration)?,
749 offset: self.offset,
750 zone_id: None,
751 })
752 }
753}
754
755impl fmt::Display for ZonedDateTimeValue {
756 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757 write!(f, "{}{}", self.datetime, self.offset)?;
758 if let Some(zone_id) = &self.zone_id {
759 write!(f, "[{zone_id}]")?;
760 }
761 Ok(())
762 }
763}
764
765impl FromStr for ZonedDateTimeValue {
766 type Err = DecodeError;
767
768 fn from_str(s: &str) -> Result<Self, Self::Err> {
769 let (main, zone_id) = split_zone_suffix(s)?;
770 let value = match find_offset_start(main, true) {
771 Some(idx) => {
772 #[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
773 if zone_id.is_some() {
774 return parse_zoned_datetime_with_jiff(s);
775 }
776
777 let value = Self {
778 datetime: main[..idx].parse()?,
779 offset: main[idx..].parse()?,
780 zone_id: zone_id.map(Into::into),
781 };
782 if let Some(zone_id) = value.zone_id.as_deref() {
783 validate_zoned_components(&value.datetime, &value.offset, zone_id)
784 .map_err(DecodeError::InvalidTemporal)?;
785 }
786 value
787 }
788 None => {
789 let Some(zone_id) = zone_id else {
790 return Err(DecodeError::InvalidTemporal(
791 "missing datetime offset".into(),
792 ));
793 };
794 Self::in_named_zone(main.parse()?, zone_id)
795 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))?
796 }
797 };
798 Ok(value)
799 }
800}
801
802pub fn current_temporal_value(
803 kind: CurrentTemporalKind,
804 epoch_millis: i64,
805 timezone: Option<&str>,
806) -> Result<PropertyValue, EncodeError> {
807 let timestamp = Timestamp::from_millisecond(epoch_millis)
808 .map_err(|_| EncodeError::InvalidTemporal("epoch datetime out of range"))?;
809 let (timezone, zone_id) = current_time_zone(timezone)?;
810 let zoned = timestamp.to_zoned(timezone);
811 let date = DateValue {
812 inner: zoned.date(),
813 };
814 let time = LocalTimeValue {
815 inner: zoned.time(),
816 };
817 let offset = UtcOffsetValue::new(zoned.offset().seconds())?;
818 Ok(match kind {
819 CurrentTemporalKind::Date => PropertyValue::Date(date),
820 CurrentTemporalKind::LocalTime => PropertyValue::LocalTime(time),
821 CurrentTemporalKind::Time => PropertyValue::ZonedTime(ZonedTimeValue::new(time, offset)),
822 CurrentTemporalKind::LocalDateTime => PropertyValue::LocalDateTime(LocalDateTimeValue {
823 inner: zoned.datetime(),
824 }),
825 CurrentTemporalKind::DateTime => PropertyValue::ZonedDateTime(ZonedDateTimeValue {
826 datetime: LocalDateTimeValue {
827 inner: zoned.datetime(),
828 },
829 offset,
830 zone_id,
831 }),
832 })
833}
834
835fn current_time_zone(timezone: Option<&str>) -> Result<(TimeZone, Option<Box<str>>), EncodeError> {
836 let Some(timezone) = timezone else {
837 let timezone = TimeZone::system();
838 let zone_id = timezone.iana_name().map(Box::<str>::from);
839 return Ok((timezone, zone_id));
840 };
841
842 if let Ok(offset) = timezone.parse::<UtcOffsetValue>() {
843 return Ok((TimeZone::fixed(offset.as_jiff()), None));
844 }
845
846 let timezone =
847 TimeZone::get(timezone).map_err(|_| EncodeError::InvalidTemporal("invalid zone id"))?;
848 let zone_id = timezone.iana_name().map(Box::<str>::from);
849 Ok((timezone, zone_id))
850}
851
852#[derive(Debug, Clone, Copy, Default)]
853pub struct DurationMapParts {
854 pub years: f64,
855 pub months: f64,
856 pub weeks: f64,
857 pub days: f64,
858 pub hours: f64,
859 pub minutes: f64,
860 pub seconds: f64,
861 pub milliseconds: f64,
862 pub microseconds: f64,
863 pub nanoseconds: f64,
864}
865
866#[derive(Debug, Clone, Copy, Default)]
867struct DurationIntegerParts {
868 years: i64,
869 months: i64,
870 weeks: i64,
871 days: i64,
872 hours: i64,
873 minutes: i64,
874 seconds: i64,
875}
876
877#[derive(Debug, Clone)]
878pub struct DurationValue {
879 inner: Span,
880 months: i64,
881 days: i64,
882 time_nanos: i128,
883}
884
885impl DurationValue {
886 pub fn zero() -> Self {
887 Self {
888 inner: Span::default(),
889 months: 0,
890 days: 0,
891 time_nanos: 0,
892 }
893 }
894
895 #[allow(clippy::too_many_arguments)]
896 pub fn from_parts(
897 years: i64,
898 months: i64,
899 weeks: i64,
900 days: i64,
901 hours: i64,
902 minutes: i64,
903 seconds: i64,
904 milliseconds: i64,
905 microseconds: i64,
906 nanoseconds: i64,
907 ) -> Result<Self, EncodeError> {
908 let months_total = checked_i64_from_i128(i128::from(years) * 12_i128 + i128::from(months))?;
909 let days_total = checked_i64_from_i128(i128::from(weeks) * 7_i128 + i128::from(days))?;
910 let time_nanos = i128::from(hours) * i128::from(NANOS_PER_HOUR)
911 + i128::from(minutes) * i128::from(NANOS_PER_MINUTE)
912 + i128::from(seconds) * i128::from(NANOS_PER_SECOND)
913 + i128::from(milliseconds) * 1_000_000
914 + i128::from(microseconds) * 1_000
915 + i128::from(nanoseconds);
916 let inner = Span::new()
920 .try_years(years)
921 .and_then(|s| s.try_months(months))
922 .and_then(|s| s.try_weeks(weeks))
923 .and_then(|s| s.try_days(days))
924 .and_then(|s| s.try_hours(hours))
925 .and_then(|s| s.try_minutes(minutes))
926 .and_then(|s| s.try_seconds(seconds))
927 .and_then(|s| s.try_milliseconds(milliseconds))
928 .and_then(|s| s.try_microseconds(microseconds))
929 .and_then(|s| s.try_nanoseconds(nanoseconds))
930 .unwrap_or_else(|_| Span::default());
931 Ok(Self {
932 inner,
933 months: months_total,
934 days: days_total,
935 time_nanos,
936 })
937 }
938
939 pub fn from_open_cypher_parts(parts: DurationMapParts) -> Result<Self, EncodeError> {
940 let (years, years_fraction) = split_whole_fraction(parts.years)?;
941 let months_value = parts.months + years_fraction * 12.0;
942 let (months, months_fraction) = split_whole_fraction(months_value)?;
943
944 let days_value = months_fraction * DAYS_PER_AVERAGE_MONTH + parts.weeks * 7.0 + parts.days;
945 let (whole_days, day_fraction) = split_whole_fraction(days_value)?;
946
947 let nanos_value = day_fraction * NANOS_PER_DAY as f64
948 + parts.hours * NANOS_PER_HOUR as f64
949 + parts.minutes * NANOS_PER_MINUTE as f64
950 + parts.seconds * NANOS_PER_SECOND as f64
951 + parts.milliseconds * 1_000_000.0
952 + parts.microseconds * 1_000.0
953 + parts.nanoseconds;
954 if !nanos_value.is_finite() {
955 return Err(EncodeError::InvalidTemporal(
956 "duration field must be finite",
957 ));
958 }
959 let total_nanos = checked_i128_from_rounded_f64(nanos_value)?;
960 let days = whole_days;
961 let (hours, minutes, seconds, nanoseconds) = split_time_nanos(total_nanos)?;
962
963 Self::from_parts(
964 years,
965 months,
966 0,
967 days,
968 hours,
969 minutes,
970 seconds,
971 0,
972 0,
973 nanoseconds,
974 )
975 }
976
977 pub fn years(&self) -> i64 {
978 self.months / 12
979 }
980
981 pub fn months(&self) -> i64 {
982 self.months % 12
983 }
984
985 pub fn weeks(&self) -> i64 {
986 0
987 }
988
989 pub fn days(&self) -> i64 {
990 self.days
991 }
992
993 pub fn hours(&self) -> i64 {
994 split_time_nanos(self.time_nanos)
995 .expect("DurationValue should store checked time")
996 .0
997 }
998
999 pub fn minutes(&self) -> i64 {
1000 split_time_nanos(self.time_nanos)
1001 .expect("DurationValue should store checked time")
1002 .1
1003 }
1004
1005 pub fn seconds(&self) -> i64 {
1006 split_time_nanos(self.time_nanos)
1007 .expect("DurationValue should store checked time")
1008 .2
1009 }
1010
1011 pub fn milliseconds(&self) -> i64 {
1012 self.nanoseconds() / 1_000_000
1013 }
1014
1015 pub fn microseconds(&self) -> i64 {
1016 self.nanoseconds() / 1_000
1017 }
1018
1019 pub fn nanoseconds(&self) -> i64 {
1020 split_time_nanos(self.time_nanos)
1021 .expect("DurationValue should store checked time")
1022 .3
1023 }
1024
1025 pub fn total_months(&self) -> i64 {
1026 self.months
1027 }
1028
1029 pub fn total_quarters(&self) -> i64 {
1030 self.total_months() / 3
1031 }
1032
1033 pub fn total_weeks(&self) -> i64 {
1034 self.total_days() / 7
1035 }
1036
1037 pub fn total_days(&self) -> i64 {
1038 self.days
1039 }
1040
1041 pub fn total_time_nanos(&self) -> i128 {
1042 self.time_nanos
1043 }
1044
1045 pub fn as_jiff(&self) -> Option<&Span> {
1046 Self::from_jiff_span(self.inner.clone())
1047 .ok()
1048 .filter(|value| value == self)
1049 .map(|_| &self.inner)
1050 }
1051
1052 pub fn checked_add_duration(&self, other: &DurationValue) -> Result<Self, EncodeError> {
1053 let months = checked_i64_from_i128(i128::from(self.months) + i128::from(other.months))?;
1054 let days = checked_i64_from_i128(i128::from(self.days) + i128::from(other.days))?;
1055 let time_nanos = self
1056 .time_nanos
1057 .checked_add(other.time_nanos)
1058 .ok_or(EncodeError::InvalidTemporal("duration arithmetic overflow"))?;
1059 Self::from_groups(months, days, time_nanos)
1060 }
1061
1062 pub fn checked_sub_duration(&self, other: &DurationValue) -> Result<Self, EncodeError> {
1063 let months = checked_i64_from_i128(i128::from(self.months) - i128::from(other.months))?;
1064 let days = checked_i64_from_i128(i128::from(self.days) - i128::from(other.days))?;
1065 let time_nanos = self
1066 .time_nanos
1067 .checked_sub(other.time_nanos)
1068 .ok_or(EncodeError::InvalidTemporal("duration arithmetic overflow"))?;
1069 Self::from_groups(months, days, time_nanos)
1070 }
1071
1072 pub fn checked_mul_number(&self, factor: f64) -> Result<Self, EncodeError> {
1073 self.checked_scale(factor)
1074 }
1075
1076 pub fn checked_div_number(&self, divisor: f64) -> Result<Self, EncodeError> {
1077 if divisor == 0.0 {
1078 return Err(EncodeError::InvalidTemporal("duration division by zero"));
1079 }
1080 self.checked_scale(1.0 / divisor)
1081 }
1082
1083 fn checked_scale(&self, factor: f64) -> Result<Self, EncodeError> {
1084 if !factor.is_finite() {
1085 return Err(EncodeError::InvalidTemporal(
1086 "duration arithmetic factor must be finite",
1087 ));
1088 }
1089
1090 let months_value = self.months as f64 * factor;
1091 let (months, months_fraction) = split_whole_fraction(months_value)?;
1092 let days_value = self.days as f64 * factor + months_fraction * DAYS_PER_AVERAGE_MONTH;
1093 let (days, day_fraction) = split_whole_fraction(days_value)?;
1094 let nanos_value = self.time_nanos as f64 * factor + day_fraction * NANOS_PER_DAY as f64;
1095 if !nanos_value.is_finite() {
1096 return Err(EncodeError::InvalidTemporal(
1097 "duration field must be finite",
1098 ));
1099 }
1100 let time_nanos = checked_i128_from_rounded_f64(nanos_value)?;
1101 Self::from_groups(months, days, time_nanos)
1102 }
1103
1104 fn from_groups(months: i64, days: i64, time_nanos: i128) -> Result<Self, EncodeError> {
1105 let years = months / 12;
1106 let months = months % 12;
1107 let (hours, minutes, seconds, nanoseconds) = split_time_nanos(time_nanos)?;
1108 Self::from_parts(
1109 years,
1110 months,
1111 0,
1112 days,
1113 hours,
1114 minutes,
1115 seconds,
1116 0,
1117 0,
1118 nanoseconds,
1119 )
1120 }
1121
1122 fn from_jiff_span(inner: Span) -> Result<Self, EncodeError> {
1123 let months = i64::from(inner.get_years()) * 12 + i64::from(inner.get_months());
1124 let days = i64::from(inner.get_weeks()) * 7 + i64::from(inner.get_days());
1125 let time_nanos = i128::from(inner.get_hours()) * i128::from(NANOS_PER_HOUR)
1126 + i128::from(inner.get_minutes()) * i128::from(NANOS_PER_MINUTE)
1127 + i128::from(inner.get_seconds()) * i128::from(NANOS_PER_SECOND)
1128 + i128::from(inner.get_milliseconds()) * 1_000_000
1129 + i128::from(inner.get_microseconds()) * 1_000
1130 + i128::from(inner.get_nanoseconds());
1131 Ok(Self {
1132 inner,
1133 months,
1134 days,
1135 time_nanos,
1136 })
1137 }
1138}
1139
1140impl fmt::Display for DurationValue {
1141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1142 let months = self.total_months();
1143 let days = self.total_days();
1144 let nanos = self.total_time_nanos();
1145
1146 if months == 0 && days == 0 && nanos == 0 {
1147 return f.write_str("PT0S");
1148 }
1149
1150 f.write_str("P")?;
1151
1152 let years = months / 12;
1153 let months_of_year = months % 12;
1154 if years != 0 {
1155 write!(f, "{years}Y")?;
1156 }
1157 if months_of_year != 0 {
1158 write!(f, "{months_of_year}M")?;
1159 }
1160 if days != 0 {
1161 write!(f, "{days}D")?;
1162 }
1163
1164 let (hours, minutes, seconds, nanoseconds) =
1165 split_time_nanos(nanos).map_err(|_| fmt::Error)?;
1166 if hours != 0 || minutes != 0 || seconds != 0 || nanoseconds != 0 {
1167 f.write_str("T")?;
1168 if hours != 0 {
1169 write!(f, "{hours}H")?;
1170 }
1171 if minutes != 0 {
1172 write!(f, "{minutes}M")?;
1173 }
1174 if seconds != 0 || nanoseconds != 0 {
1175 write_duration_seconds(f, seconds, nanoseconds)?;
1176 }
1177 }
1178
1179 Ok(())
1180 }
1181}
1182
1183impl FromStr for DurationValue {
1184 type Err = DecodeError;
1185
1186 fn from_str(s: &str) -> Result<Self, Self::Err> {
1187 parse_open_cypher_duration(s).or_else(|_| {
1188 let inner = s
1189 .parse::<Span>()
1190 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))?;
1191 Self::from_jiff_span(inner).map_err(|err| DecodeError::InvalidTemporal(err.to_string()))
1192 })
1193 }
1194}
1195
1196impl PartialEq for DurationValue {
1197 fn eq(&self, other: &Self) -> bool {
1198 self.total_months() == other.total_months()
1199 && self.total_days() == other.total_days()
1200 && self.total_time_nanos() == other.total_time_nanos()
1201 }
1202}
1203
1204impl Eq for DurationValue {}
1205
1206pub fn temporal_component(
1207 value: PropertyValueRef<'_>,
1208 key: &str,
1209) -> Result<Option<PropertyValue>, EncodeError> {
1210 match value {
1211 PropertyValueRef::Null => Ok(Some(PropertyValue::Null)),
1212 PropertyValueRef::Date(value) => date_component(value, key),
1213 PropertyValueRef::LocalTime(value) => local_time_component(value, key),
1214 PropertyValueRef::ZonedTime(value) => zoned_time_component(value, key),
1215 PropertyValueRef::LocalDateTime(value) => local_datetime_component(value, key),
1216 PropertyValueRef::ZonedDateTime(value) => zoned_datetime_component(value, key),
1217 PropertyValueRef::Duration(value) => duration_component(value, key),
1218 _ => Ok(None),
1219 }
1220}
1221
1222fn integer(value: impl Into<i64>) -> PropertyValue {
1223 PropertyValue::Integer(value.into())
1224}
1225
1226fn string(value: impl Into<String>) -> PropertyValue {
1227 PropertyValue::String(value.into())
1228}
1229
1230fn date_component(value: &DateValue, key: &str) -> Result<Option<PropertyValue>, EncodeError> {
1231 let component = match key {
1232 "year" => integer(value.year() as i64),
1233 "quarter" => integer(value.quarter() as i64),
1234 "month" => integer(value.month() as i64),
1235 "week" => integer(value.iso_week()? as i64),
1236 "weekYear" => integer(value.iso_week_year()? as i64),
1237 "day" => integer(value.day() as i64),
1238 "ordinalDay" => integer(value.ordinal_day()? as i64),
1239 "weekDay" => integer(value.iso_day_of_week() as i64),
1240 "dayOfQuarter" => integer(value.day_of_quarter()? as i64),
1241 _ => return Ok(None),
1242 };
1243 Ok(Some(component))
1244}
1245
1246fn local_time_component(
1247 value: &LocalTimeValue,
1248 key: &str,
1249) -> Result<Option<PropertyValue>, EncodeError> {
1250 let nanos = value.nanos();
1251 let component = match key {
1252 "hour" => integer(value.hour() as i64),
1253 "minute" => integer(value.minute() as i64),
1254 "second" => integer(value.second() as i64),
1255 "millisecond" => integer((nanos / 1_000_000) as i64),
1256 "microsecond" => integer((nanos / 1_000) as i64),
1257 "nanosecond" => integer(nanos as i64),
1258 _ => return Ok(None),
1259 };
1260 Ok(Some(component))
1261}
1262
1263fn zoned_time_component(
1264 value: &ZonedTimeValue,
1265 key: &str,
1266) -> Result<Option<PropertyValue>, EncodeError> {
1267 if let Some(component) = local_time_component(&value.time, key)? {
1268 return Ok(Some(component));
1269 }
1270 let component = match key {
1271 "timezone" | "offset" => string(value.offset.to_string()),
1272 "offsetMinutes" => integer(i64::from(value.offset.seconds_east() / 60)),
1273 "offsetSeconds" => integer(i64::from(value.offset.seconds_east())),
1274 _ => return Ok(None),
1275 };
1276 Ok(Some(component))
1277}
1278
1279fn local_datetime_component(
1280 value: &LocalDateTimeValue,
1281 key: &str,
1282) -> Result<Option<PropertyValue>, EncodeError> {
1283 if let Some(component) = date_component(&value.date(), key)? {
1284 return Ok(Some(component));
1285 }
1286 local_time_component(&value.time(), key)
1287}
1288
1289fn zoned_datetime_component(
1290 value: &ZonedDateTimeValue,
1291 key: &str,
1292) -> Result<Option<PropertyValue>, EncodeError> {
1293 if let Some(component) = local_datetime_component(&value.datetime, key)? {
1294 return Ok(Some(component));
1295 }
1296 let component = match key {
1297 "timezone" => string(
1298 value
1299 .zone_id
1300 .as_deref()
1301 .map(str::to_owned)
1302 .unwrap_or_else(|| value.offset.to_string()),
1303 ),
1304 "offset" => string(value.offset.to_string()),
1305 "offsetMinutes" => integer(i64::from(value.offset.seconds_east() / 60)),
1306 "offsetSeconds" => integer(i64::from(value.offset.seconds_east())),
1307 "epochSeconds" => integer(zoned_datetime_epoch_seconds(value)?),
1308 "epochMillis" => integer(zoned_datetime_epoch_millis(value)?),
1309 _ => return Ok(None),
1310 };
1311 Ok(Some(component))
1312}
1313
1314fn zoned_datetime_epoch_seconds(value: &ZonedDateTimeValue) -> Result<i64, EncodeError> {
1315 let (day, nanos) = value.utc_epoch_day_and_nanos();
1316 checked_i64_from_i128(i128::from(day) * 86_400 + i128::from(nanos / NANOS_PER_SECOND))
1317}
1318
1319fn zoned_datetime_epoch_millis(value: &ZonedDateTimeValue) -> Result<i64, EncodeError> {
1320 let (day, nanos) = value.utc_epoch_day_and_nanos();
1321 checked_i64_from_i128(i128::from(day) * 86_400_000 + i128::from(nanos / 1_000_000))
1322}
1323
1324fn duration_component(
1325 value: &DurationValue,
1326 key: &str,
1327) -> Result<Option<PropertyValue>, EncodeError> {
1328 let months = value.total_months();
1329 let days = value.total_days();
1330 let nanos = value.total_time_nanos();
1331 let nanos_per_hour = i128::from(NANOS_PER_HOUR);
1332 let nanos_per_minute = i128::from(NANOS_PER_MINUTE);
1333 let nanos_per_second = i128::from(NANOS_PER_SECOND);
1334 let component = match key {
1335 "years" => integer(months / 12),
1336 "quarters" => integer(value.total_quarters()),
1337 "months" => integer(months),
1338 "weeks" => integer(value.total_weeks()),
1339 "days" => integer(days),
1340 "hours" => integer(checked_i64_from_i128(nanos.div_euclid(nanos_per_hour))?),
1341 "minutes" => integer(checked_i64_from_i128(nanos.div_euclid(nanos_per_minute))?),
1342 "seconds" => integer(checked_i64_from_i128(nanos.div_euclid(nanos_per_second))?),
1343 "milliseconds" => integer(checked_i64_from_i128(nanos.div_euclid(1_000_000))?),
1344 "microseconds" => integer(checked_i64_from_i128(nanos.div_euclid(1_000))?),
1345 "nanoseconds" => integer(checked_i64_from_i128(nanos)?),
1346 "quartersOfYear" => integer((months % 12) / 3),
1347 "monthsOfQuarter" => integer(months % 3),
1348 "monthsOfYear" => integer(months % 12),
1349 "daysOfWeek" => integer(days % 7),
1350 "minutesOfHour" => integer(checked_i64_from_i128(
1351 nanos.div_euclid(nanos_per_minute).rem_euclid(60),
1352 )?),
1353 "secondsOfMinute" => integer(checked_i64_from_i128(
1354 nanos.div_euclid(nanos_per_second).rem_euclid(60),
1355 )?),
1356 "millisecondsOfSecond" => integer(checked_i64_from_i128(
1357 nanos.div_euclid(1_000_000).rem_euclid(1000),
1358 )?),
1359 "microsecondsOfSecond" => integer(checked_i64_from_i128(
1360 nanos.div_euclid(1_000).rem_euclid(1_000_000),
1361 )?),
1362 "nanosecondsOfSecond" => {
1363 integer(checked_i64_from_i128(nanos.rem_euclid(nanos_per_second))?)
1364 }
1365 _ => return Ok(None),
1366 };
1367 Ok(Some(component))
1368}
1369
1370fn epoch_day_from_ymd(year: i32, month: u8, day: u8) -> i64 {
1371 let month = month as i64;
1372 let day = day as i64;
1373 let year = year as i64 - i64::from(month <= 2);
1374 let era = if year >= 0 { year } else { year - 399 } / 400;
1375 let year_of_era = year - era * 400;
1376 let month_prime = month + if month > 2 { -3 } else { 9 };
1377 let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
1378 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
1379 era * 146_097 + day_of_era - 719_468
1380}
1381
1382fn ymd_from_epoch_day(epoch_day: i64) -> Result<(i32, u8, u8), EncodeError> {
1383 let z = epoch_day + 719_468;
1384 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
1385 let day_of_era = z - era * 146_097;
1386 let year_of_era =
1387 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1388 let mut year = year_of_era + era * 400;
1389 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1390 let month_prime = (5 * day_of_year + 2) / 153;
1391 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1392 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1393 year += i64::from(month <= 2);
1394
1395 let year = i32::try_from(year)
1396 .map_err(|_| EncodeError::InvalidTemporal("year must be in -9999..=9999"))?;
1397 let month =
1398 u8::try_from(month).map_err(|_| EncodeError::InvalidTemporal("month must be in 1..=12"))?;
1399 let day =
1400 u8::try_from(day).map_err(|_| EncodeError::InvalidTemporal("day must be in 1..=31"))?;
1401 Ok((year, month, day))
1402}
1403
1404fn is_leap_year(year: i32) -> bool {
1405 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
1406}
1407
1408fn days_in_month(year: i32, month: u8) -> u8 {
1409 match month {
1410 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1411 4 | 6 | 9 | 11 => 30,
1412 2 if is_leap_year(year) => 29,
1413 2 => 28,
1414 _ => 31,
1415 }
1416}
1417
1418fn day_of_week_from_epoch_day(epoch_day: i64) -> u8 {
1419 (epoch_day + 3).rem_euclid(7) as u8 + 1
1420}
1421
1422fn iso_week_one_monday_epoch_day(year: i32) -> Result<i64, EncodeError> {
1423 let jan4 = DateValue::new(year, 1, 4)?.epoch_day();
1424 Ok(jan4 - (i64::from(day_of_week_from_epoch_day(jan4)) - 1))
1425}
1426
1427fn split_whole_fraction(value: f64) -> Result<(i64, f64), EncodeError> {
1428 if !value.is_finite() {
1429 return Err(EncodeError::InvalidTemporal(
1430 "duration field must be finite",
1431 ));
1432 }
1433 let whole = value.trunc();
1434 if whole < i64::MIN as f64 || whole > i64::MAX as f64 {
1435 return Err(EncodeError::InvalidTemporal(
1436 "duration field outside supported range",
1437 ));
1438 }
1439 Ok((whole as i64, value - whole))
1440}
1441
1442fn split_time_nanos(value: i128) -> Result<(i64, i64, i64, i64), EncodeError> {
1443 let nanos_per_hour = i128::from(NANOS_PER_HOUR);
1444 let nanos_per_minute = i128::from(NANOS_PER_MINUTE);
1445 let nanos_per_second = i128::from(NANOS_PER_SECOND);
1446
1447 let hours = value / nanos_per_hour;
1448 let rem = value % nanos_per_hour;
1449 let minutes = rem / nanos_per_minute;
1450 let rem = rem % nanos_per_minute;
1451 let seconds = rem / nanos_per_second;
1452 let nanoseconds = rem % nanos_per_second;
1453
1454 Ok((
1455 checked_i64_from_i128(hours)?,
1456 checked_i64_from_i128(minutes)?,
1457 checked_i64_from_i128(seconds)?,
1458 checked_i64_from_i128(nanoseconds)?,
1459 ))
1460}
1461
1462fn write_duration_seconds(
1463 f: &mut fmt::Formatter<'_>,
1464 seconds: i64,
1465 nanoseconds: i64,
1466) -> fmt::Result {
1467 let total = i128::from(seconds) * i128::from(NANOS_PER_SECOND) + i128::from(nanoseconds);
1468 let sign = if total < 0 { "-" } else { "" };
1469 let abs = total.abs();
1470 let whole = abs / i128::from(NANOS_PER_SECOND);
1471 let frac = abs % i128::from(NANOS_PER_SECOND);
1472 if frac == 0 {
1473 write!(f, "{sign}{whole}S")
1474 } else {
1475 let mut frac = format!("{frac:09}");
1476 while frac.ends_with('0') {
1477 frac.pop();
1478 }
1479 write!(f, "{sign}{whole}.{frac}S")
1480 }
1481}
1482
1483fn checked_i64_from_i128(value: i128) -> Result<i64, EncodeError> {
1484 i64::try_from(value)
1485 .map_err(|_| EncodeError::InvalidTemporal("duration field outside supported range"))
1486}
1487
1488fn round_ties_even_with_float_tolerance(value: f64) -> f64 {
1489 let whole = value.trunc();
1490 let fraction = value - whole;
1491 let half_distance = (fraction.abs() - 0.5).abs();
1492 let tolerance = f64::EPSILON * value.abs().max(1.0) * 8.0;
1493 if half_distance <= tolerance {
1494 let whole_i128 = whole as i128;
1495 if whole_i128 % 2 == 0 {
1496 whole
1497 } else {
1498 whole + fraction.signum()
1499 }
1500 } else {
1501 value.round()
1502 }
1503}
1504
1505fn checked_i128_from_rounded_f64(value: f64) -> Result<i128, EncodeError> {
1506 if !value.is_finite() {
1507 return Err(EncodeError::InvalidTemporal(
1508 "duration field must be finite",
1509 ));
1510 }
1511 let rounded = round_ties_even_with_float_tolerance(value);
1512 if rounded < i128::MIN as f64 || rounded > i128::MAX as f64 {
1513 return Err(EncodeError::InvalidTemporal(
1514 "duration field outside supported range",
1515 ));
1516 }
1517 Ok(rounded as i128)
1518}
1519
1520fn checked_i32_from_i128(value: i128) -> Result<i32, EncodeError> {
1521 i32::try_from(value)
1522 .map_err(|_| EncodeError::InvalidTemporal("duration field outside supported range"))
1523}
1524
1525fn timestamp_from_epoch_day_and_nanos(day: i64, nanos: i64) -> Result<Timestamp, EncodeError> {
1526 if !(0..NANOS_PER_DAY).contains(&nanos) {
1527 return Err(EncodeError::InvalidTemporal(
1528 "nanos since midnight must be in 0..<24h",
1529 ));
1530 }
1531 let total_seconds = i128::from(day) * 86_400 + i128::from(nanos.div_euclid(NANOS_PER_SECOND));
1532 let seconds = checked_i64_from_i128(total_seconds)?;
1533 let subsec_nanos = i32::try_from(nanos.rem_euclid(NANOS_PER_SECOND))
1534 .map_err(|_| EncodeError::InvalidTemporal("nanoseconds out of range"))?;
1535 Timestamp::new(seconds, subsec_nanos)
1536 .map_err(|_| EncodeError::InvalidTemporal("epoch datetime out of range"))
1537}
1538
1539fn parse_open_cypher_date(s: &str) -> Result<DateValue, DecodeError> {
1540 if let Ok(inner) = s.parse::<Date>() {
1541 return Ok(DateValue { inner });
1542 }
1543
1544 if let Some(w_pos) = s.find('W') {
1545 let year_part = s[..w_pos].strip_suffix('-').unwrap_or(&s[..w_pos]);
1546 let year = parse_i32_digits(year_part, "invalid ISO week year")?;
1547 let tail = &s[w_pos + 1..];
1548 let (week, day) = match tail.split_once('-') {
1549 Some((week, day)) => (
1550 parse_u8_digits(week, "invalid ISO week")?,
1551 Some(parse_u8_digits(day, "invalid ISO day")?),
1552 ),
1553 None if tail.len() == 2 => (parse_u8_digits(tail, "invalid ISO week")?, None),
1554 None if tail.len() == 3 => (
1555 parse_u8_digits(&tail[..2], "invalid ISO week")?,
1556 Some(parse_u8_digits(&tail[2..], "invalid ISO day")?),
1557 ),
1558 _ => return Err(DecodeError::InvalidTemporal("invalid ISO week date".into())),
1559 };
1560 return DateValue::from_iso_week(year, week, day)
1561 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1562 }
1563
1564 if s.len() == 4 && is_ascii_digits(s) {
1565 return DateValue::from_calendar(parse_i32_digits(s, "invalid year")?, None, None)
1566 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1567 }
1568
1569 if s.len() == 6 && is_ascii_digits(s) {
1570 return DateValue::from_calendar(
1571 parse_i32_digits(&s[..4], "invalid year")?,
1572 Some(parse_u8_digits(&s[4..], "invalid month")?),
1573 None,
1574 )
1575 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1576 }
1577
1578 if s.len() == 7 && is_ascii_digits(s) {
1579 return DateValue::from_ordinal_day(
1580 parse_i32_digits(&s[..4], "invalid year")?,
1581 parse_u16_digits(&s[4..], "invalid ordinal day")?,
1582 )
1583 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1584 }
1585
1586 if s.len() == 7 && &s[4..5] == "-" {
1587 return DateValue::from_calendar(
1588 parse_i32_digits(&s[..4], "invalid year")?,
1589 Some(parse_u8_digits(&s[5..], "invalid month")?),
1590 None,
1591 )
1592 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1593 }
1594
1595 if s.len() == 8 && &s[4..5] == "-" {
1596 return DateValue::from_ordinal_day(
1597 parse_i32_digits(&s[..4], "invalid year")?,
1598 parse_u16_digits(&s[5..], "invalid ordinal day")?,
1599 )
1600 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1601 }
1602
1603 Err(DecodeError::InvalidTemporal("invalid date".into()))
1604}
1605
1606fn parse_open_cypher_duration(s: &str) -> Result<DurationValue, DecodeError> {
1607 let rest = s
1608 .strip_prefix('P')
1609 .ok_or_else(|| DecodeError::InvalidTemporal("duration must start with P".into()))?;
1610 if rest.contains('-') {
1611 if let Ok(value) = parse_alternative_duration(rest) {
1612 return Ok(value);
1613 }
1614 }
1615
1616 let mut parts = DurationMapParts::default();
1617 let mut integer_parts = DurationIntegerParts::default();
1618 let mut integer_only = true;
1619 let mut in_time = false;
1620 let mut number_start: Option<usize> = None;
1621
1622 for (idx, ch) in rest.char_indices() {
1623 if ch == 'T' {
1624 if number_start.is_some() {
1625 return Err(DecodeError::InvalidTemporal(
1626 "missing duration designator".into(),
1627 ));
1628 }
1629 in_time = true;
1630 continue;
1631 }
1632
1633 if ch.is_ascii_digit() || ch == '.' || ((ch == '+' || ch == '-') && number_start.is_none())
1634 {
1635 number_start.get_or_insert(idx);
1636 continue;
1637 }
1638
1639 let start = number_start
1640 .take()
1641 .ok_or_else(|| DecodeError::InvalidTemporal("missing duration field value".into()))?;
1642 let field = &rest[start..idx];
1643 if field.contains('.') {
1644 integer_only = false;
1645 } else {
1646 let value = parse_i64_duration_field(field, "invalid duration field")?;
1647 match (ch, in_time) {
1648 ('Y', false) => integer_parts.years = value,
1649 ('M', false) => integer_parts.months = value,
1650 ('W', false) => integer_parts.weeks = value,
1651 ('D', false) => integer_parts.days = value,
1652 ('H', true) => integer_parts.hours = value,
1653 ('M', true) => integer_parts.minutes = value,
1654 ('S', true) => integer_parts.seconds = value,
1655 _ => {}
1656 }
1657 }
1658
1659 let value = parse_f64_digits(field, "invalid duration field")?;
1660 match (ch, in_time) {
1661 ('Y', false) => parts.years = value,
1662 ('M', false) => parts.months = value,
1663 ('W', false) => parts.weeks = value,
1664 ('D', false) => parts.days = value,
1665 ('H', true) => parts.hours = value,
1666 ('M', true) => parts.minutes = value,
1667 ('S', true) => parts.seconds = value,
1668 _ => {
1669 return Err(DecodeError::InvalidTemporal(
1670 "invalid duration designator".into(),
1671 ))
1672 }
1673 }
1674 }
1675
1676 if number_start.is_some() {
1677 return Err(DecodeError::InvalidTemporal(
1678 "missing duration designator".into(),
1679 ));
1680 }
1681
1682 if integer_only {
1683 return DurationValue::from_parts(
1684 integer_parts.years,
1685 integer_parts.months,
1686 integer_parts.weeks,
1687 integer_parts.days,
1688 integer_parts.hours,
1689 integer_parts.minutes,
1690 integer_parts.seconds,
1691 0,
1692 0,
1693 0,
1694 )
1695 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()));
1696 }
1697
1698 DurationValue::from_open_cypher_parts(parts)
1699 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))
1700}
1701
1702fn parse_alternative_duration(rest: &str) -> Result<DurationValue, DecodeError> {
1703 let (date, time) = rest
1704 .split_once('T')
1705 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration".into()))?;
1706 let mut date_parts = date.split('-');
1707 let years = parse_f64_digits(
1708 date_parts
1709 .next()
1710 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration year".into()))?,
1711 "invalid duration year",
1712 )?;
1713 let months = parse_f64_digits(
1714 date_parts
1715 .next()
1716 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration month".into()))?,
1717 "invalid duration month",
1718 )?;
1719 let days = parse_f64_digits(
1720 date_parts
1721 .next()
1722 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration day".into()))?,
1723 "invalid duration day",
1724 )?;
1725 if date_parts.next().is_some() {
1726 return Err(DecodeError::InvalidTemporal("invalid duration date".into()));
1727 }
1728
1729 let mut time_parts = time.split(':');
1730 let hours = parse_f64_digits(
1731 time_parts
1732 .next()
1733 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration hour".into()))?,
1734 "invalid duration hour",
1735 )?;
1736 let minutes = parse_f64_digits(
1737 time_parts
1738 .next()
1739 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration minute".into()))?,
1740 "invalid duration minute",
1741 )?;
1742 let seconds = parse_f64_digits(
1743 time_parts
1744 .next()
1745 .ok_or_else(|| DecodeError::InvalidTemporal("invalid duration second".into()))?,
1746 "invalid duration second",
1747 )?;
1748 if time_parts.next().is_some() {
1749 return Err(DecodeError::InvalidTemporal("invalid duration time".into()));
1750 }
1751
1752 DurationValue::from_open_cypher_parts(DurationMapParts {
1753 years,
1754 months,
1755 days,
1756 hours,
1757 minutes,
1758 seconds,
1759 ..DurationMapParts::default()
1760 })
1761 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))
1762}
1763
1764fn is_ascii_digits(value: &str) -> bool {
1765 !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
1766}
1767
1768fn parse_i32_digits(value: &str, msg: &str) -> Result<i32, DecodeError> {
1769 if !is_ascii_digits(value) {
1770 return Err(DecodeError::InvalidTemporal(msg.into()));
1771 }
1772 value
1773 .parse::<i32>()
1774 .map_err(|_| DecodeError::InvalidTemporal(msg.into()))
1775}
1776
1777fn parse_u8_digits(value: &str, msg: &str) -> Result<u8, DecodeError> {
1778 if !is_ascii_digits(value) {
1779 return Err(DecodeError::InvalidTemporal(msg.into()));
1780 }
1781 value
1782 .parse::<u8>()
1783 .map_err(|_| DecodeError::InvalidTemporal(msg.into()))
1784}
1785
1786fn parse_u16_digits(value: &str, msg: &str) -> Result<u16, DecodeError> {
1787 if !is_ascii_digits(value) {
1788 return Err(DecodeError::InvalidTemporal(msg.into()));
1789 }
1790 value
1791 .parse::<u16>()
1792 .map_err(|_| DecodeError::InvalidTemporal(msg.into()))
1793}
1794
1795fn parse_i64_duration_field(value: &str, msg: &str) -> Result<i64, DecodeError> {
1796 if value.is_empty()
1797 || matches!(value, "+" | "-")
1798 || !value
1799 .trim_start_matches(['+', '-'])
1800 .bytes()
1801 .all(|b| b.is_ascii_digit())
1802 {
1803 return Err(DecodeError::InvalidTemporal(msg.into()));
1804 }
1805 value
1806 .parse::<i64>()
1807 .map_err(|_| DecodeError::InvalidTemporal(msg.into()))
1808}
1809
1810fn parse_f64_digits(value: &str, msg: &str) -> Result<f64, DecodeError> {
1811 if value.is_empty() {
1812 return Err(DecodeError::InvalidTemporal(msg.into()));
1813 }
1814 let parsed = value
1815 .parse::<f64>()
1816 .map_err(|_| DecodeError::InvalidTemporal(msg.into()))?;
1817 if !parsed.is_finite() {
1818 return Err(DecodeError::InvalidTemporal(msg.into()));
1819 }
1820 Ok(parsed)
1821}
1822
1823fn parse_offset(s: &str) -> Result<UtcOffsetValue, DecodeError> {
1824 if s == "Z" {
1825 return Ok(UtcOffsetValue::utc());
1826 }
1827 if !matches!(s.len(), 3 | 5 | 6 | 7 | 9) {
1828 return Err(DecodeError::InvalidTemporal(
1829 "offset must be Z, ±HH, ±HHMM, ±HH:MM, ±HHMMSS or ±HH:MM:SS".into(),
1830 ));
1831 }
1832 let sign = match &s[0..1] {
1833 "+" => 1,
1834 "-" => -1,
1835 _ => {
1836 return Err(DecodeError::InvalidTemporal(
1837 "offset must start with '+' or '-'".into(),
1838 ))
1839 }
1840 };
1841 let hours = s[1..3]
1842 .parse::<i32>()
1843 .map_err(|_| DecodeError::InvalidTemporal("invalid offset hour".into()))?;
1844 let mut minutes = 0;
1845 let mut seconds = 0;
1846 if s.len() == 5 || s.len() == 7 {
1847 minutes = s[3..5]
1848 .parse::<i32>()
1849 .map_err(|_| DecodeError::InvalidTemporal("invalid offset minute".into()))?;
1850 if s.len() == 7 {
1851 seconds = s[5..7]
1852 .parse::<i32>()
1853 .map_err(|_| DecodeError::InvalidTemporal("invalid offset second".into()))?;
1854 }
1855 } else if s.len() >= 6 {
1856 if &s[3..4] != ":" {
1857 return Err(DecodeError::InvalidTemporal(
1858 "offset separator must be ':'".into(),
1859 ));
1860 }
1861 minutes = s[4..6]
1862 .parse::<i32>()
1863 .map_err(|_| DecodeError::InvalidTemporal("invalid offset minute".into()))?;
1864 }
1865 if s.len() == 9 {
1866 if &s[6..7] != ":" {
1867 return Err(DecodeError::InvalidTemporal(
1868 "offset separator must be ':'".into(),
1869 ));
1870 }
1871 seconds = s[7..9]
1872 .parse::<i32>()
1873 .map_err(|_| DecodeError::InvalidTemporal("invalid offset second".into()))?;
1874 }
1875 if minutes > 59 || seconds > 59 {
1876 return Err(DecodeError::InvalidTemporal(
1877 "offset minute and second must be in 0..=59".into(),
1878 ));
1879 }
1880 let seconds_east = sign * (hours * 3600 + minutes * 60 + seconds);
1881 UtcOffsetValue::new(seconds_east).map_err(|err| DecodeError::InvalidTemporal(err.to_string()))
1882}
1883
1884fn split_zone_suffix(s: &str) -> Result<(&str, Option<&str>), DecodeError> {
1885 match s.rsplit_once('[') {
1886 Some((prefix, zone)) if zone.ends_with(']') => Ok((prefix, Some(&zone[..zone.len() - 1]))),
1887 Some(_) => Err(DecodeError::InvalidTemporal(
1888 "zone id suffix must end with ']'".into(),
1889 )),
1890 None => Ok((s, None)),
1891 }
1892}
1893
1894fn find_offset_start(s: &str, has_date: bool) -> Option<usize> {
1895 if s.ends_with('Z') {
1896 return Some(s.len() - 1);
1897 }
1898 let start = if has_date {
1899 s.find('T').map(|idx| idx + 1)?
1900 } else {
1901 1
1902 };
1903 let bytes = s.as_bytes();
1904 for idx in start..bytes.len() {
1905 if bytes[idx] == b'+' || bytes[idx] == b'-' {
1906 return Some(idx);
1907 }
1908 }
1909 None
1910}
1911
1912#[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
1913fn parse_zoned_datetime_with_jiff(s: &str) -> Result<ZonedDateTimeValue, DecodeError> {
1914 let zoned = s
1915 .parse::<jiff::Zoned>()
1916 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))?;
1917 let zone_id = zoned
1918 .time_zone()
1919 .iana_name()
1920 .map(|name| Box::<str>::from(name.to_owned()));
1921 let offset = zoned
1922 .offset()
1923 .to_string()
1924 .parse::<UtcOffsetValue>()
1925 .map_err(|err| DecodeError::InvalidTemporal(err.to_string()))?;
1926 Ok(ZonedDateTimeValue {
1927 datetime: LocalDateTimeValue {
1928 inner: zoned.datetime(),
1929 },
1930 offset,
1931 zone_id,
1932 })
1933}
1934
1935#[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
1936fn validate_zoned_components(
1937 datetime: &LocalDateTimeValue,
1938 offset: &UtcOffsetValue,
1939 zone_id: &str,
1940) -> Result<(), String> {
1941 let candidate = format!("{}{}[{zone_id}]", datetime, offset);
1942 candidate
1943 .parse::<jiff::Zoned>()
1944 .map(|_| ())
1945 .map_err(|err| err.to_string())
1946}
1947
1948#[cfg(not(any(feature = "tzdb-system", feature = "tzdb-bundled")))]
1949fn validate_zoned_components(
1950 _datetime: &LocalDateTimeValue,
1951 _offset: &UtcOffsetValue,
1952 _zone_id: &str,
1953) -> Result<(), String> {
1954 Ok(())
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959 use super::*;
1960
1961 fn storage_display(value: crate::property::PropertyValue) -> String {
1962 let storage = crate::codec::encode_property_value(&value).unwrap();
1963 crate::render::storage_value_to_display_text(storage.as_ref()).unwrap()
1964 }
1965
1966 fn storage_roundtrip(value: crate::property::PropertyValue) -> crate::property::PropertyValue {
1967 let storage = crate::codec::encode_property_value(&value).unwrap();
1968 crate::codec::decode_property_value(storage.as_ref()).unwrap()
1969 }
1970
1971 #[test]
1972 fn parses_and_formats_date() {
1973 let value = DateValue::new(2026, 4, 6).unwrap();
1974 assert_eq!(value.to_string(), "2026-04-06");
1975 assert_eq!("2026-04-06".parse::<DateValue>().unwrap(), value);
1976 }
1977
1978 #[test]
1979 fn parses_and_formats_local_time() {
1980 let value = LocalTimeValue::new(12, 0, 1, 120_000_000).unwrap();
1981 assert_eq!(value.to_string(), "12:00:01.12");
1982 assert_eq!("12:00:01.12".parse::<LocalTimeValue>().unwrap(), value);
1983 }
1984
1985 #[test]
1986 fn parses_and_formats_duration() {
1987 let value = "P2M10DT2H30M".parse::<DurationValue>().unwrap();
1988 assert_eq!(value.to_string(), "P2M10DT2H30M");
1989 assert!(value.as_jiff().is_some());
1990 }
1991
1992 #[test]
1993 fn parses_offset() {
1994 let value = "+01:00".parse::<UtcOffsetValue>().unwrap();
1995 assert_eq!(value.seconds_east(), 3600);
1996 assert_eq!(value.to_string(), "+01:00");
1997 }
1998
1999 #[test]
2000 fn parses_compact_hour_offset_and_canonicalizes() {
2001 let value = "+01".parse::<UtcOffsetValue>().unwrap();
2002 assert_eq!(value.seconds_east(), 3600);
2003 assert_eq!(value.to_string(), "+01:00");
2004 }
2005
2006 #[test]
2007 fn parses_negative_half_hour_offset() {
2008 let value = "-05:30".parse::<UtcOffsetValue>().unwrap();
2009 assert_eq!(value.seconds_east(), -(5 * 3600 + 30 * 60));
2010 assert_eq!(value.to_string(), "-05:30");
2011 }
2012
2013 #[test]
2014 fn parses_utc_and_second_precision_offsets() {
2015 let utc = "Z".parse::<UtcOffsetValue>().unwrap();
2016 assert_eq!(utc.seconds_east(), 0);
2017 assert_eq!(utc.to_string(), "Z");
2018
2019 let value = "+02:05:59".parse::<UtcOffsetValue>().unwrap();
2020 assert_eq!(value.seconds_east(), 2 * 3600 + 5 * 60 + 59);
2021 assert_eq!(value.to_string(), "+02:05:59");
2022 }
2023
2024 #[test]
2025 fn parses_zoned_time() {
2026 let value = "12:00:00+01:00".parse::<ZonedTimeValue>().unwrap();
2027 assert_eq!(value.to_string(), "12:00:00+01:00");
2028 }
2029
2030 #[test]
2031 fn parses_compact_zoned_time_and_canonicalizes() {
2032 let value = "12:00:00+01".parse::<ZonedTimeValue>().unwrap();
2033 assert_eq!(value.to_string(), "12:00:00+01:00");
2034 }
2035
2036 #[test]
2037 fn parses_offset_only_zoned_datetime() {
2038 let value = "2026-04-06T12:00:00+01:00"
2039 .parse::<ZonedDateTimeValue>()
2040 .unwrap();
2041 assert_eq!(value.to_string(), "2026-04-06T12:00:00+01:00");
2042 }
2043
2044 #[test]
2045 fn constructs_temporal1_date_forms() {
2046 assert_eq!(
2047 DateValue::from_iso_week(1817, 2, Some(2))
2048 .unwrap()
2049 .to_string(),
2050 "1817-01-07"
2051 );
2052 assert_eq!(
2053 DateValue::from_ordinal_day(1984, 202).unwrap().to_string(),
2054 "1984-07-20"
2055 );
2056 assert_eq!(
2057 DateValue::from_quarter(1984, 3, Some(45))
2058 .unwrap()
2059 .to_string(),
2060 "1984-08-14"
2061 );
2062 }
2063
2064 #[test]
2065 fn constructs_epoch_datetime_and_open_cypher_duration() {
2066 assert_eq!(
2067 ZonedDateTimeValue::from_epoch(416_779, 999_999_999)
2068 .unwrap()
2069 .to_string(),
2070 "1970-01-05T19:46:19.999999999Z"
2071 );
2072 assert_eq!(
2073 ZonedDateTimeValue::from_epoch_millis(237_821_673_987)
2074 .unwrap()
2075 .to_string(),
2076 "1977-07-15T13:34:33.987Z"
2077 );
2078
2079 let duration = DurationValue::from_open_cypher_parts(DurationMapParts {
2080 months: 0.75,
2081 ..DurationMapParts::default()
2082 })
2083 .unwrap();
2084 assert_eq!(duration.to_string(), "P22DT19H51M49.5S");
2085 }
2086
2087 #[test]
2088 fn parses_temporal2_date_string_forms() {
2089 assert_eq!(
2090 "2015-07-01",
2091 "2015-07".parse::<DateValue>().unwrap().to_string()
2092 );
2093 assert_eq!(
2094 "2015-07-01",
2095 "201507".parse::<DateValue>().unwrap().to_string()
2096 );
2097 assert_eq!(
2098 "2015-07-21",
2099 "2015-W30-2".parse::<DateValue>().unwrap().to_string()
2100 );
2101 assert_eq!(
2102 "2015-07-20",
2103 "2015W30".parse::<DateValue>().unwrap().to_string()
2104 );
2105 assert_eq!(
2106 "2015-07-21",
2107 "2015-202".parse::<DateValue>().unwrap().to_string()
2108 );
2109 assert_eq!(
2110 "2015-01-01",
2111 "2015".parse::<DateValue>().unwrap().to_string()
2112 );
2113 }
2114
2115 #[test]
2116 fn parses_temporal2_datetime_string_forms() {
2117 assert_eq!(
2118 "2015-07-21T21:40:32.142",
2119 storage_display(crate::property::PropertyValue::LocalDateTime(
2120 "2015-W30-2T214032.142"
2121 .parse::<LocalDateTimeValue>()
2122 .unwrap()
2123 ))
2124 );
2125 assert_eq!(
2126 "2015-07-21T21:40:32+01:00",
2127 storage_display(crate::property::PropertyValue::ZonedDateTime(
2128 "2015-202T21:40:32+0100"
2129 .parse::<ZonedDateTimeValue>()
2130 .unwrap()
2131 ))
2132 );
2133 assert_eq!(
2134 "2015-07-21T21:40",
2135 storage_display(crate::property::PropertyValue::LocalDateTime(
2136 "20150721T21:40".parse::<LocalDateTimeValue>().unwrap()
2137 ))
2138 );
2139 assert_eq!(
2140 "2015-07-20T21:40-02:00",
2141 storage_display(crate::property::PropertyValue::ZonedDateTime(
2142 "2015-W30T2140-02".parse::<ZonedDateTimeValue>().unwrap()
2143 ))
2144 );
2145 }
2146
2147 #[test]
2148 fn parses_temporal2_duration_string_forms() {
2149 assert_eq!(
2150 "P5M1DT12H",
2151 "P5M1.5D".parse::<DurationValue>().unwrap().to_string()
2152 );
2153 assert_eq!(
2154 "P22DT19H51M49.5S",
2155 "P0.75M".parse::<DurationValue>().unwrap().to_string()
2156 );
2157 assert_eq!(
2158 "PT45S",
2159 "PT0.75M".parse::<DurationValue>().unwrap().to_string()
2160 );
2161 assert_eq!(
2162 "P17DT12H",
2163 "P2.5W".parse::<DurationValue>().unwrap().to_string()
2164 );
2165 assert_eq!(
2166 "P12Y5M14DT16H13M10S",
2167 "P12Y5M14DT16H12M70S"
2168 .parse::<DurationValue>()
2169 .unwrap()
2170 .to_string()
2171 );
2172 assert_eq!(
2173 "P13DT40H13M10S",
2174 "P13DT40H13M10S"
2175 .parse::<DurationValue>()
2176 .unwrap()
2177 .to_string()
2178 );
2179 assert_eq!(
2180 "P2012Y2M2DT14H37M21.545S",
2181 "P2012-02-02T14:37:21.545"
2182 .parse::<DurationValue>()
2183 .unwrap()
2184 .to_string()
2185 );
2186 }
2187
2188 #[test]
2189 fn duration_preserves_open_cypher_component_groups() {
2190 let base = DurationValue::from_open_cypher_parts(DurationMapParts {
2191 years: 12.0,
2192 months: 5.0,
2193 days: 14.0,
2194 hours: 16.0,
2195 minutes: 12.0,
2196 seconds: 70.0,
2197 ..DurationMapParts::default()
2198 })
2199 .unwrap();
2200 let same_time_group = DurationValue::from_open_cypher_parts(DurationMapParts {
2201 years: 12.0,
2202 months: 5.0,
2203 days: 14.0,
2204 hours: 16.0,
2205 minutes: 13.0,
2206 seconds: 10.0,
2207 ..DurationMapParts::default()
2208 })
2209 .unwrap();
2210 let different_day_group = DurationValue::from_open_cypher_parts(DurationMapParts {
2211 years: 12.0,
2212 months: 5.0,
2213 days: 13.0,
2214 hours: 40.0,
2215 minutes: 13.0,
2216 seconds: 10.0,
2217 ..DurationMapParts::default()
2218 })
2219 .unwrap();
2220
2221 assert_eq!(base, same_time_group);
2222 assert_ne!(base, different_day_group);
2223 assert_eq!(base.to_string(), "P12Y5M14DT16H13M10S");
2224 assert_eq!(different_day_group.to_string(), "P12Y5M13DT40H13M10S");
2225 }
2226
2227 #[test]
2228 fn negative_duration_components_use_floor_seconds() {
2229 let duration =
2230 DurationValue::from_parts(0, 0, 0, 0, -23, -59, -59, 0, 0, -900_000_000).unwrap();
2231 let value = crate::property::PropertyValue::Duration(duration);
2232
2233 assert_eq!(storage_display(value.clone()), "PT-23H-59M-59.9S");
2234 assert_eq!(
2235 temporal_component(value.as_ref(), "seconds").unwrap(),
2236 Some(crate::property::PropertyValue::Integer(-86_400))
2237 );
2238 assert_eq!(
2239 temporal_component(value.as_ref(), "nanosecondsOfSecond").unwrap(),
2240 Some(crate::property::PropertyValue::Integer(100_000_000))
2241 );
2242 assert_eq!(
2243 temporal_component(value.as_ref(), "secondsOfMinute").unwrap(),
2244 Some(crate::property::PropertyValue::Integer(0))
2245 );
2246 assert_eq!(
2247 temporal_component(value.as_ref(), "millisecondsOfSecond").unwrap(),
2248 Some(crate::property::PropertyValue::Integer(100))
2249 );
2250 }
2251
2252 #[test]
2253 fn duration_supports_large_open_cypher_groups() {
2254 let calendar =
2255 DurationValue::from_parts(1_999_999_998, 11, 0, 30, 0, 0, 0, 0, 0, 0).unwrap();
2256 assert_eq!(calendar.years(), 1_999_999_998);
2257 assert_eq!(calendar.days(), 30);
2258 assert!(calendar.as_jiff().is_none());
2259 let calendar_value = crate::property::PropertyValue::Duration(calendar);
2260 assert_eq!(
2261 storage_display(calendar_value.clone()),
2262 "P1999999998Y11M30D"
2263 );
2264 assert_eq!(storage_roundtrip(calendar_value.clone()), calendar_value);
2265
2266 let seconds =
2267 DurationValue::from_parts(0, 0, 0, 0, 17_531_639_991_215, 59, 59, 0, 0, 0).unwrap();
2268 assert_eq!(seconds.hours(), 17_531_639_991_215);
2269 assert!(seconds.as_jiff().is_none());
2270 let seconds_value = crate::property::PropertyValue::Duration(seconds);
2271 assert_eq!(
2272 storage_display(seconds_value.clone()),
2273 "PT17531639991215H59M59S"
2274 );
2275 assert_eq!(storage_roundtrip(seconds_value.clone()), seconds_value);
2276 }
2277
2278 #[test]
2279 fn duration_fractional_nanoseconds_use_ties_even_rounding() {
2280 for (nanoseconds, expected) in [
2281 (0.5, "PT0S"),
2282 (0.6, "PT0.000000001S"),
2283 (1.5, "PT0.000000002S"),
2284 (-0.5, "PT0S"),
2285 (-0.6, "PT-0.000000001S"),
2286 (-1.5, "PT-0.000000002S"),
2287 ] {
2288 let duration = DurationValue::from_open_cypher_parts(DurationMapParts {
2289 nanoseconds,
2290 ..DurationMapParts::default()
2291 })
2292 .unwrap();
2293 assert_eq!(duration.to_string(), expected);
2294 }
2295 }
2296
2297 #[test]
2298 fn duration_fractional_years_cascade_through_months() {
2299 let duration = DurationValue::from_open_cypher_parts(DurationMapParts {
2300 years: 12.5,
2301 months: 5.5,
2302 days: 14.5,
2303 hours: 16.5,
2304 minutes: 12.5,
2305 seconds: 70.5,
2306 nanoseconds: 3.0,
2307 ..DurationMapParts::default()
2308 })
2309 .unwrap();
2310
2311 assert_eq!(duration.to_string(), "P12Y11M29DT33H58M13.500000003S");
2312 }
2313
2314 #[test]
2315 fn temporal8_date_time_and_datetime_arithmetic() {
2316 let duration = DurationValue::from_open_cypher_parts(DurationMapParts {
2317 years: 12.5,
2318 months: 5.5,
2319 days: 14.5,
2320 hours: 16.5,
2321 minutes: 12.5,
2322 seconds: 70.5,
2323 nanoseconds: 3.0,
2324 ..DurationMapParts::default()
2325 })
2326 .unwrap();
2327
2328 let date = DateValue::new(1984, 10, 11).unwrap();
2329 assert_eq!(
2330 date.checked_add_duration(&duration).unwrap().to_string(),
2331 "1997-10-11"
2332 );
2333 assert_eq!(
2334 date.checked_sub_duration(&duration).unwrap().to_string(),
2335 "1971-10-12"
2336 );
2337
2338 let time = LocalTimeValue::new(12, 31, 14, 1).unwrap();
2339 assert_eq!(
2340 time.checked_add_duration(&duration).unwrap().to_string(),
2341 "22:29:27.500000004"
2342 );
2343 assert_eq!(
2344 time.checked_sub_duration(&duration).unwrap().to_string(),
2345 "02:33:00.499999998"
2346 );
2347
2348 let datetime = LocalDateTimeValue::new(1984, 10, 11, 12, 31, 14, 1).unwrap();
2349 assert_eq!(
2350 datetime
2351 .checked_add_duration(&duration)
2352 .unwrap()
2353 .to_string(),
2354 "1997-10-11T22:29:27.500000004"
2355 );
2356 assert_eq!(
2357 datetime
2358 .checked_sub_duration(&duration)
2359 .unwrap()
2360 .to_string(),
2361 "1971-10-12T02:33:00.499999998"
2362 );
2363 }
2364
2365 #[test]
2366 fn temporal8_duration_subtract_multiply_and_divide() {
2367 let base = DurationValue::from_open_cypher_parts(DurationMapParts {
2368 years: 12.0,
2369 months: 5.0,
2370 days: 14.0,
2371 hours: 16.0,
2372 minutes: 12.0,
2373 seconds: 70.0,
2374 nanoseconds: 1.0,
2375 ..DurationMapParts::default()
2376 })
2377 .unwrap();
2378 let fractional = DurationValue::from_open_cypher_parts(DurationMapParts {
2379 years: 12.5,
2380 months: 5.5,
2381 days: 14.5,
2382 hours: 16.5,
2383 minutes: 12.5,
2384 seconds: 70.5,
2385 nanoseconds: 3.0,
2386 ..DurationMapParts::default()
2387 })
2388 .unwrap();
2389
2390 assert_eq!(
2391 base.checked_sub_duration(&fractional).unwrap().to_string(),
2392 "P-6M-15DT-17H-45M-3.500000002S"
2393 );
2394 assert_eq!(
2395 base.checked_mul_number(0.5).unwrap().to_string(),
2396 "P6Y2M22DT13H21M8S"
2397 );
2398 assert_eq!(
2399 base.checked_div_number(2.0).unwrap().to_string(),
2400 "P6Y2M22DT13H21M8S"
2401 );
2402 }
2403
2404 #[test]
2405 fn duration_formats_negative_time_components_without_day_carry() {
2406 let cases = [
2407 (
2408 DurationMapParts {
2409 seconds: -2.0,
2410 milliseconds: 1.0,
2411 ..DurationMapParts::default()
2412 },
2413 "PT-1.999S",
2414 ),
2415 (
2416 DurationMapParts {
2417 days: 1.0,
2418 milliseconds: -1.0,
2419 ..DurationMapParts::default()
2420 },
2421 "P1DT-0.001S",
2422 ),
2423 (
2424 DurationMapParts {
2425 seconds: -60.0,
2426 milliseconds: -1.0,
2427 ..DurationMapParts::default()
2428 },
2429 "PT-1M-0.001S",
2430 ),
2431 ];
2432
2433 for (parts, expected) in cases {
2434 assert_eq!(
2435 DurationValue::from_open_cypher_parts(parts)
2436 .unwrap()
2437 .to_string(),
2438 expected
2439 );
2440 }
2441 }
2442
2443 #[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
2444 #[test]
2445 fn parses_named_zone_zoned_datetime() {
2446 let value = "2026-04-06T12:00:00+01:00[Europe/Lisbon]"
2447 .parse::<ZonedDateTimeValue>()
2448 .unwrap();
2449 assert_eq!(
2450 value.to_string(),
2451 "2026-04-06T12:00:00+01:00[Europe/Lisbon]"
2452 );
2453 assert_eq!(value.zone_id.as_deref(), Some("Europe/Lisbon"));
2454 }
2455
2456 #[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
2457 #[test]
2458 fn rejects_named_zone_offset_mismatch() {
2459 let err = "2026-04-06T12:00:00+00:00[Europe/Lisbon]"
2460 .parse::<ZonedDateTimeValue>()
2461 .unwrap_err();
2462 assert!(matches!(err, DecodeError::InvalidTemporal(_)));
2463 }
2464}