1use crate::api::error::UniError;
15use crate::core::id::{Eid, Vid};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::fmt;
19use std::hash::{Hash, Hasher};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum TemporalType {
28 Date,
29 LocalTime,
30 Time,
31 LocalDateTime,
32 DateTime,
33 Duration,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub enum TemporalValue {
43 Date { days_since_epoch: i32 },
45 LocalTime { nanos_since_midnight: i64 },
47 Time {
49 nanos_since_midnight: i64,
50 offset_seconds: i32,
51 },
52 LocalDateTime { nanos_since_epoch: i64 },
54 DateTime {
57 nanos_since_epoch: i64,
58 offset_seconds: i32,
59 timezone_name: Option<String>,
60 },
61 Duration { months: i64, days: i64, nanos: i64 },
64}
65
66impl Eq for TemporalValue {}
67
68impl Hash for TemporalValue {
69 fn hash<H: Hasher>(&self, state: &mut H) {
70 std::mem::discriminant(self).hash(state);
71 match self {
72 TemporalValue::Date { days_since_epoch } => days_since_epoch.hash(state),
73 TemporalValue::LocalTime {
74 nanos_since_midnight,
75 } => nanos_since_midnight.hash(state),
76 TemporalValue::Time {
77 nanos_since_midnight,
78 offset_seconds,
79 } => {
80 nanos_since_midnight.hash(state);
81 offset_seconds.hash(state);
82 }
83 TemporalValue::LocalDateTime { nanos_since_epoch } => nanos_since_epoch.hash(state),
84 TemporalValue::DateTime {
85 nanos_since_epoch,
86 offset_seconds,
87 timezone_name,
88 } => {
89 nanos_since_epoch.hash(state);
90 offset_seconds.hash(state);
91 timezone_name.hash(state);
92 }
93 TemporalValue::Duration {
94 months,
95 days,
96 nanos,
97 } => {
98 months.hash(state);
99 days.hash(state);
100 nanos.hash(state);
101 }
102 }
103 }
104}
105
106impl TemporalValue {
107 pub fn temporal_type(&self) -> TemporalType {
109 match self {
110 TemporalValue::Date { .. } => TemporalType::Date,
111 TemporalValue::LocalTime { .. } => TemporalType::LocalTime,
112 TemporalValue::Time { .. } => TemporalType::Time,
113 TemporalValue::LocalDateTime { .. } => TemporalType::LocalDateTime,
114 TemporalValue::DateTime { .. } => TemporalType::DateTime,
115 TemporalValue::Duration { .. } => TemporalType::Duration,
116 }
117 }
118
119 pub fn year(&self) -> Option<i64> {
125 self.to_date().map(|d| d.year() as i64)
126 }
127
128 pub fn month(&self) -> Option<i64> {
130 self.to_date().map(|d| d.month() as i64)
131 }
132
133 pub fn day(&self) -> Option<i64> {
135 self.to_date().map(|d| d.day() as i64)
136 }
137
138 pub fn hour(&self) -> Option<i64> {
140 self.to_time().map(|t| t.hour() as i64)
141 }
142
143 pub fn minute(&self) -> Option<i64> {
145 self.to_time().map(|t| t.minute() as i64)
146 }
147
148 pub fn second(&self) -> Option<i64> {
150 self.to_time().map(|t| t.second() as i64)
151 }
152
153 pub fn millisecond(&self) -> Option<i64> {
155 self.to_time().map(|t| (t.nanosecond() / 1_000_000) as i64)
156 }
157
158 pub fn microsecond(&self) -> Option<i64> {
160 self.to_time().map(|t| (t.nanosecond() / 1_000) as i64)
161 }
162
163 pub fn nanosecond(&self) -> Option<i64> {
165 self.to_time().map(|t| t.nanosecond() as i64)
166 }
167
168 pub fn quarter(&self) -> Option<i64> {
170 self.to_date().map(|d| ((d.month() - 1) / 3 + 1) as i64)
171 }
172
173 pub fn week(&self) -> Option<i64> {
175 self.to_date().map(|d| d.iso_week().week() as i64)
176 }
177
178 pub fn week_year(&self) -> Option<i64> {
180 self.to_date().map(|d| d.iso_week().year() as i64)
181 }
182
183 pub fn ordinal_day(&self) -> Option<i64> {
185 self.to_date().map(|d| d.ordinal() as i64)
186 }
187
188 pub fn day_of_week(&self) -> Option<i64> {
190 self.to_date()
191 .map(|d| (d.weekday().num_days_from_monday() + 1) as i64)
192 }
193
194 pub fn day_of_quarter(&self) -> Option<i64> {
196 self.to_date().map(|d| {
197 let quarter_start_month = ((d.month() - 1) / 3) * 3 + 1;
198 let quarter_start =
199 chrono::NaiveDate::from_ymd_opt(d.year(), quarter_start_month, 1).unwrap();
200 d.signed_duration_since(quarter_start).num_days() + 1
201 })
202 }
203
204 pub fn timezone(&self) -> Option<&str> {
206 match self {
207 TemporalValue::DateTime {
208 timezone_name: Some(name),
209 ..
210 } => Some(name.as_str()),
211 _ => None,
212 }
213 }
214
215 fn raw_offset_seconds(&self) -> Option<i32> {
217 match self {
218 TemporalValue::Time { offset_seconds, .. }
219 | TemporalValue::DateTime { offset_seconds, .. } => Some(*offset_seconds),
220 _ => None,
221 }
222 }
223
224 pub fn offset(&self) -> Option<String> {
226 self.raw_offset_seconds().map(format_offset)
227 }
228
229 pub fn offset_minutes(&self) -> Option<i64> {
231 self.raw_offset_seconds().map(|s| s as i64 / 60)
232 }
233
234 pub fn offset_seconds_value(&self) -> Option<i64> {
236 self.raw_offset_seconds().map(|s| s as i64)
237 }
238
239 fn raw_epoch_nanos(&self) -> Option<i64> {
241 match self {
242 TemporalValue::DateTime {
243 nanos_since_epoch, ..
244 }
245 | TemporalValue::LocalDateTime {
246 nanos_since_epoch, ..
247 } => Some(*nanos_since_epoch),
248 _ => None,
249 }
250 }
251
252 pub fn epoch_seconds(&self) -> Option<i64> {
254 self.raw_epoch_nanos().map(|n| n / 1_000_000_000)
255 }
256
257 pub fn epoch_millis(&self) -> Option<i64> {
259 self.raw_epoch_nanos().map(|n| n / 1_000_000)
260 }
261
262 pub fn to_date(&self) -> Option<chrono::NaiveDate> {
268 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?;
269 match self {
270 TemporalValue::Date { days_since_epoch } => {
271 epoch.checked_add_signed(chrono::Duration::days(*days_since_epoch as i64))
272 }
273 TemporalValue::LocalDateTime { nanos_since_epoch } => {
274 let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
275 Some(dt.date_naive())
276 }
277 TemporalValue::DateTime {
278 nanos_since_epoch,
279 offset_seconds,
280 ..
281 } => {
282 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
284 let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
285 Some(dt.date_naive())
286 }
287 _ => None,
288 }
289 }
290
291 pub fn to_time(&self) -> Option<chrono::NaiveTime> {
293 match self {
294 TemporalValue::LocalTime {
295 nanos_since_midnight,
296 }
297 | TemporalValue::Time {
298 nanos_since_midnight,
299 ..
300 } => nanos_to_time(*nanos_since_midnight),
301 TemporalValue::LocalDateTime { nanos_since_epoch } => {
302 let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
303 Some(dt.naive_utc().time())
304 }
305 TemporalValue::DateTime {
306 nanos_since_epoch,
307 offset_seconds,
308 ..
309 } => {
310 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
311 let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
312 Some(dt.naive_utc().time())
313 }
314 _ => None,
315 }
316 }
317}
318
319fn nanos_to_time(nanos: i64) -> Option<chrono::NaiveTime> {
321 let total_secs = nanos / 1_000_000_000;
322 let h = (total_secs / 3600) as u32;
323 let m = ((total_secs % 3600) / 60) as u32;
324 let s = (total_secs % 60) as u32;
325 let ns = (nanos % 1_000_000_000) as u32;
326 chrono::NaiveTime::from_hms_nano_opt(h, m, s, ns)
327}
328
329fn format_offset(offset_seconds: i32) -> String {
331 if offset_seconds == 0 {
332 return "Z".to_string();
333 }
334 format_offset_numeric(offset_seconds)
335}
336
337fn format_offset_numeric(offset_seconds: i32) -> String {
339 let sign = if offset_seconds >= 0 { '+' } else { '-' };
340 let abs = offset_seconds.unsigned_abs();
341 let h = abs / 3600;
342 let m = (abs % 3600) / 60;
343 let s = abs % 60;
344 if s != 0 {
345 format!("{}{:02}:{:02}:{:02}", sign, h, m, s)
346 } else {
347 format!("{}{:02}:{:02}", sign, h, m)
348 }
349}
350
351fn format_fractional(nanos: u32) -> String {
353 if nanos == 0 {
354 return String::new();
355 }
356 let s = format!("{:09}", nanos);
357 let trimmed = s.trim_end_matches('0');
358 format!(".{}", trimmed)
359}
360
361fn format_time_component(hour: u32, minute: u32, second: u32, nanos: u32) -> String {
363 if second == 0 && nanos == 0 {
364 format!("{:02}:{:02}", hour, minute)
365 } else {
366 let frac = format_fractional(nanos);
367 format!("{:02}:{:02}:{:02}{}", hour, minute, second, frac)
368 }
369}
370
371fn format_naive_time(t: &chrono::NaiveTime) -> String {
373 format_time_component(t.hour(), t.minute(), t.second(), t.nanosecond())
374}
375
376fn nanos_to_time_or_midnight(nanos: i64) -> chrono::NaiveTime {
378 nanos_to_time(nanos).unwrap_or_else(|| chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
379}
380
381impl fmt::Display for TemporalValue {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 match self {
384 TemporalValue::Date { days_since_epoch } => {
385 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
386 let date = epoch + chrono::Duration::days(*days_since_epoch as i64);
387 write!(f, "{}", date.format("%Y-%m-%d"))
388 }
389 TemporalValue::LocalTime {
390 nanos_since_midnight,
391 } => {
392 let time = nanos_to_time_or_midnight(*nanos_since_midnight);
393 write!(f, "{}", format_naive_time(&time))
394 }
395 TemporalValue::Time {
396 nanos_since_midnight,
397 offset_seconds,
398 } => {
399 let time = nanos_to_time_or_midnight(*nanos_since_midnight);
400 write!(
401 f,
402 "{}{}",
403 format_naive_time(&time),
404 format_offset(*offset_seconds)
405 )
406 }
407 TemporalValue::LocalDateTime { nanos_since_epoch } => {
408 let ndt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
409 write!(
410 f,
411 "{}T{}",
412 ndt.date().format("%Y-%m-%d"),
413 format_naive_time(&ndt.time())
414 )
415 }
416 TemporalValue::DateTime {
417 nanos_since_epoch,
418 offset_seconds,
419 timezone_name,
420 } => {
421 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
423 let ndt = chrono::DateTime::from_timestamp_nanos(local_nanos).naive_utc();
424 let tz = format_offset(*offset_seconds);
425 write!(
426 f,
427 "{}T{}{}",
428 ndt.date().format("%Y-%m-%d"),
429 format_naive_time(&ndt.time()),
430 tz
431 )?;
432 if let Some(name) = timezone_name {
433 write!(f, "[{}]", name)?;
434 }
435 Ok(())
436 }
437 TemporalValue::Duration {
438 months,
439 days,
440 nanos,
441 } => {
442 write!(f, "P")?;
443 let years = months / 12;
444 let rem_months = months % 12;
445 if years != 0 {
446 write!(f, "{}Y", years)?;
447 }
448 if rem_months != 0 {
449 write!(f, "{}M", rem_months)?;
450 }
451 if *days != 0 {
452 write!(f, "{}D", days)?;
453 }
454 let abs_nanos = nanos.unsigned_abs() as i128;
456 let nanos_sign = if *nanos < 0 { -1i64 } else { 1 };
457 let total_secs = (abs_nanos / 1_000_000_000) as i64;
458 let frac_nanos = (abs_nanos % 1_000_000_000) as u32;
459 let hours = total_secs / 3600;
460 let mins = (total_secs % 3600) / 60;
461 let secs = total_secs % 60;
462
463 if hours != 0 || mins != 0 || secs != 0 || frac_nanos != 0 {
464 write!(f, "T")?;
465 if hours != 0 {
466 write!(f, "{}H", hours * nanos_sign)?;
467 }
468 if mins != 0 {
469 write!(f, "{}M", mins * nanos_sign)?;
470 }
471 if secs != 0 || frac_nanos != 0 {
472 let frac = format_fractional(frac_nanos);
473 if nanos_sign < 0 && (secs != 0 || frac_nanos != 0) {
474 write!(f, "-{}{}", secs, frac)?;
475 } else {
476 write!(f, "{}{}", secs, frac)?;
477 }
478 write!(f, "S")?;
479 }
480 } else if years == 0 && rem_months == 0 && *days == 0 {
481 write!(f, "T0S")?;
483 }
484 Ok(())
485 }
486 }
487 }
488}
489
490use chrono::Datelike as _;
492use chrono::Timelike as _;
493
494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502#[serde(untagged)]
503#[non_exhaustive]
504pub enum Value {
505 Null,
507 Bool(bool),
509 Int(i64),
511 Float(f64),
513 String(String),
515 Bytes(Vec<u8>),
517 List(Vec<Value>),
519 Map(HashMap<String, Value>),
521
522 Node(Node),
525 Edge(Edge),
527 Path(Path),
529
530 Vector(Vec<f32>),
533
534 Temporal(TemporalValue),
537}
538
539impl Value {
544 pub fn is_null(&self) -> bool {
546 matches!(self, Value::Null)
547 }
548
549 pub fn as_bool(&self) -> Option<bool> {
551 match self {
552 Value::Bool(b) => Some(*b),
553 _ => None,
554 }
555 }
556
557 pub fn as_i64(&self) -> Option<i64> {
559 match self {
560 Value::Int(i) => Some(*i),
561 _ => None,
562 }
563 }
564
565 pub fn as_u64(&self) -> Option<u64> {
567 match self {
568 Value::Int(i) if *i >= 0 => Some(*i as u64),
569 _ => None,
570 }
571 }
572
573 pub fn as_f64(&self) -> Option<f64> {
577 match self {
578 Value::Float(f) => Some(*f),
579 Value::Int(i) => Some(*i as f64),
580 _ => None,
581 }
582 }
583
584 pub fn as_str(&self) -> Option<&str> {
586 match self {
587 Value::String(s) => Some(s),
588 _ => None,
589 }
590 }
591
592 pub fn is_i64(&self) -> bool {
594 matches!(self, Value::Int(_))
595 }
596
597 pub fn is_f64(&self) -> bool {
599 matches!(self, Value::Float(_))
600 }
601
602 pub fn is_string(&self) -> bool {
604 matches!(self, Value::String(_))
605 }
606
607 pub fn is_number(&self) -> bool {
609 matches!(self, Value::Int(_) | Value::Float(_))
610 }
611
612 pub fn as_array(&self) -> Option<&Vec<Value>> {
614 match self {
615 Value::List(l) => Some(l),
616 _ => None,
617 }
618 }
619
620 pub fn as_object(&self) -> Option<&HashMap<String, Value>> {
622 match self {
623 Value::Map(m) => Some(m),
624 _ => None,
625 }
626 }
627
628 pub fn is_bool(&self) -> bool {
630 matches!(self, Value::Bool(_))
631 }
632
633 pub fn is_list(&self) -> bool {
635 matches!(self, Value::List(_))
636 }
637
638 pub fn is_map(&self) -> bool {
640 matches!(self, Value::Map(_))
641 }
642
643 pub fn get(&self, key: &str) -> Option<&Value> {
647 match self {
648 Value::Map(m) => m.get(key),
649 _ => None,
650 }
651 }
652
653 pub fn is_temporal(&self) -> bool {
655 matches!(self, Value::Temporal(_))
656 }
657
658 pub fn as_temporal(&self) -> Option<&TemporalValue> {
660 match self {
661 Value::Temporal(t) => Some(t),
662 _ => None,
663 }
664 }
665}
666
667impl fmt::Display for Value {
668 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
669 match self {
670 Value::Null => write!(f, "null"),
671 Value::Bool(b) => write!(f, "{b}"),
672 Value::Int(i) => write!(f, "{i}"),
673 Value::Float(v) => {
674 if v.fract() == 0.0 && v.is_finite() {
675 write!(f, "{v:.1}")
676 } else {
677 write!(f, "{v}")
678 }
679 }
680 Value::String(s) => write!(f, "{s}"),
681 Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
682 Value::List(l) => {
683 write!(f, "[")?;
684 for (i, item) in l.iter().enumerate() {
685 if i > 0 {
686 write!(f, ", ")?;
687 }
688 write!(f, "{item}")?;
689 }
690 write!(f, "]")
691 }
692 Value::Map(m) => {
693 write!(f, "{{")?;
694 for (i, (k, v)) in m.iter().enumerate() {
695 if i > 0 {
696 write!(f, ", ")?;
697 }
698 write!(f, "{k}: {v}")?;
699 }
700 write!(f, "}}")
701 }
702 Value::Node(n) => write!(f, "(:{} {{vid: {}}})", n.labels.join(":"), n.vid),
703 Value::Edge(e) => write!(f, "-[:{}]-", e.edge_type),
704 Value::Path(p) => write!(
705 f,
706 "<path: {} nodes, {} edges>",
707 p.nodes.len(),
708 p.edges.len()
709 ),
710 Value::Vector(v) => write!(f, "<vector: {} dims>", v.len()),
711 Value::Temporal(t) => write!(f, "{t}"),
712 }
713 }
714}
715
716impl Eq for Value {}
721
722impl Hash for Value {
723 fn hash<H: Hasher>(&self, state: &mut H) {
724 std::mem::discriminant(self).hash(state);
726 match self {
727 Value::Null => {}
728 Value::Bool(b) => b.hash(state),
729 Value::Int(i) => i.hash(state),
730 Value::Float(f) => f.to_bits().hash(state),
731 Value::String(s) => s.hash(state),
732 Value::Bytes(b) => b.hash(state),
733 Value::List(l) => l.hash(state),
734 Value::Map(m) => hash_map(m, state),
735 Value::Node(n) => n.hash(state),
736 Value::Edge(e) => e.hash(state),
737 Value::Path(p) => p.hash(state),
738 Value::Vector(v) => {
739 v.len().hash(state);
740 for f in v {
741 f.to_bits().hash(state);
742 }
743 }
744 Value::Temporal(t) => t.hash(state),
745 }
746 }
747}
748
749fn hash_map<H: Hasher>(m: &HashMap<String, Value>, state: &mut H) {
755 let mut pairs: Vec<_> = m.iter().collect();
756 pairs.sort_by_key(|(k, _)| *k);
757 pairs.len().hash(state);
758 for (k, v) in pairs {
759 k.hash(state);
760 v.hash(state);
761 }
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
766pub struct Node {
767 pub vid: Vid,
769 pub labels: Vec<String>,
771 pub properties: HashMap<String, Value>,
773}
774
775impl Hash for Node {
776 fn hash<H: Hasher>(&self, state: &mut H) {
777 self.vid.hash(state);
778 let mut sorted_labels = self.labels.clone();
779 sorted_labels.sort();
780 sorted_labels.hash(state);
781 hash_map(&self.properties, state);
782 }
783}
784
785impl Node {
786 pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
793 let val = self
794 .properties
795 .get(property)
796 .ok_or_else(|| UniError::Query {
797 message: format!("Property '{}' not found on node {}", property, self.vid),
798 query: None,
799 })?;
800 T::from_value(val)
801 }
802
803 pub fn try_get<T: FromValue>(&self, property: &str) -> Option<T> {
805 self.properties
806 .get(property)
807 .and_then(|v| T::from_value(v).ok())
808 }
809}
810
811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
813pub struct Edge {
814 pub eid: Eid,
816 pub edge_type: String,
818 pub src: Vid,
820 pub dst: Vid,
822 pub properties: HashMap<String, Value>,
824}
825
826impl Hash for Edge {
827 fn hash<H: Hasher>(&self, state: &mut H) {
828 self.eid.hash(state);
829 self.edge_type.hash(state);
830 self.src.hash(state);
831 self.dst.hash(state);
832 hash_map(&self.properties, state);
833 }
834}
835
836impl Edge {
837 pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
844 let val = self
845 .properties
846 .get(property)
847 .ok_or_else(|| UniError::Query {
848 message: format!("Property '{}' not found on edge {}", property, self.eid),
849 query: None,
850 })?;
851 T::from_value(val)
852 }
853}
854
855#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
857pub struct Path {
858 pub nodes: Vec<Node>,
860 #[serde(rename = "relationships")]
862 pub edges: Vec<Edge>,
863}
864
865impl Path {
866 pub fn nodes(&self) -> &[Node] {
868 &self.nodes
869 }
870
871 pub fn edges(&self) -> &[Edge] {
873 &self.edges
874 }
875
876 pub fn len(&self) -> usize {
878 self.edges.len()
879 }
880
881 pub fn is_empty(&self) -> bool {
883 self.edges.is_empty()
884 }
885
886 pub fn start(&self) -> Option<&Node> {
888 self.nodes.first()
889 }
890
891 pub fn end(&self) -> Option<&Node> {
893 self.nodes.last()
894 }
895}
896
897pub trait FromValue: Sized {
903 fn from_value(value: &Value) -> crate::Result<Self>;
909}
910
911impl<T> FromValue for T
913where
914 T: for<'a> TryFrom<&'a Value, Error = UniError>,
915{
916 fn from_value(value: &Value) -> crate::Result<Self> {
917 Self::try_from(value)
918 }
919}
920
921macro_rules! impl_try_from_value_owned {
926 ($($t:ty),+ $(,)?) => {
927 $(
928 impl TryFrom<Value> for $t {
929 type Error = UniError;
930 fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
931 Self::try_from(&value)
932 }
933 }
934 )+
935 };
936}
937
938impl_try_from_value_owned!(
939 String,
940 i64,
941 i32,
942 f64,
943 bool,
944 Vid,
945 Eid,
946 Vec<f32>,
947 Path,
948 Node,
949 Edge
950);
951
952fn type_error(expected: &str, value: &Value) -> UniError {
958 UniError::Type {
959 expected: expected.to_string(),
960 actual: format!("{:?}", value),
961 }
962}
963
964impl TryFrom<&Value> for String {
965 type Error = UniError;
966
967 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
968 match value {
969 Value::String(s) => Ok(s.clone()),
970 Value::Int(i) => Ok(i.to_string()),
971 Value::Float(f) => Ok(f.to_string()),
972 Value::Bool(b) => Ok(b.to_string()),
973 Value::Temporal(t) => Ok(t.to_string()),
974 _ => Err(type_error("String", value)),
975 }
976 }
977}
978
979impl TryFrom<&Value> for i64 {
980 type Error = UniError;
981
982 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
983 match value {
984 Value::Int(i) => Ok(*i),
985 Value::Float(f) => Ok(*f as i64),
986 _ => Err(type_error("Int", value)),
987 }
988 }
989}
990
991impl TryFrom<&Value> for i32 {
992 type Error = UniError;
993
994 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
995 match value {
996 Value::Int(i) => i32::try_from(*i).map_err(|_| UniError::Type {
997 expected: "i32".to_string(),
998 actual: format!("Integer {} out of range", i),
999 }),
1000 Value::Float(f) => {
1001 if *f < i32::MIN as f64 || *f > i32::MAX as f64 {
1002 return Err(UniError::Type {
1003 expected: "i32".to_string(),
1004 actual: format!("Float {} out of range", f),
1005 });
1006 }
1007 if f.fract() != 0.0 {
1008 return Err(UniError::Type {
1009 expected: "i32".to_string(),
1010 actual: format!("Float {} has fractional part", f),
1011 });
1012 }
1013 Ok(*f as i32)
1014 }
1015 _ => Err(type_error("Int", value)),
1016 }
1017 }
1018}
1019
1020impl TryFrom<&Value> for f64 {
1021 type Error = UniError;
1022
1023 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1024 match value {
1025 Value::Float(f) => Ok(*f),
1026 Value::Int(i) => Ok(*i as f64),
1027 _ => Err(type_error("Float", value)),
1028 }
1029 }
1030}
1031
1032impl TryFrom<&Value> for bool {
1033 type Error = UniError;
1034
1035 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1036 match value {
1037 Value::Bool(b) => Ok(*b),
1038 _ => Err(type_error("Bool", value)),
1039 }
1040 }
1041}
1042
1043impl TryFrom<&Value> for Vid {
1044 type Error = UniError;
1045
1046 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1047 match value {
1048 Value::Node(n) => Ok(n.vid),
1049 Value::String(s) => {
1050 if let Ok(id) = s.parse::<u64>() {
1051 return Ok(Vid::new(id));
1052 }
1053 Err(UniError::Type {
1054 expected: "Vid".into(),
1055 actual: s.clone(),
1056 })
1057 }
1058 Value::Int(i) => Ok(Vid::new(*i as u64)),
1059 _ => Err(type_error("Vid", value)),
1060 }
1061 }
1062}
1063
1064impl TryFrom<&Value> for Eid {
1065 type Error = UniError;
1066
1067 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1068 match value {
1069 Value::Edge(e) => Ok(e.eid),
1070 Value::String(s) => {
1071 if let Ok(id) = s.parse::<u64>() {
1072 return Ok(Eid::new(id));
1073 }
1074 Err(UniError::Type {
1075 expected: "Eid".into(),
1076 actual: s.clone(),
1077 })
1078 }
1079 Value::Int(i) => Ok(Eid::new(*i as u64)),
1080 _ => Err(type_error("Eid", value)),
1081 }
1082 }
1083}
1084
1085impl TryFrom<&Value> for Vec<f32> {
1086 type Error = UniError;
1087
1088 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1089 match value {
1090 Value::Vector(v) => Ok(v.clone()),
1091 Value::List(l) => {
1092 let mut vec = Vec::with_capacity(l.len());
1093 for item in l {
1094 match item {
1095 Value::Float(f) => vec.push(*f as f32),
1096 Value::Int(i) => vec.push(*i as f32),
1097 _ => return Err(type_error("Float", item)),
1098 }
1099 }
1100 Ok(vec)
1101 }
1102 _ => Err(type_error("Vector", value)),
1103 }
1104 }
1105}
1106
1107impl<T> TryFrom<&Value> for Option<T>
1108where
1109 T: for<'a> TryFrom<&'a Value, Error = UniError>,
1110{
1111 type Error = UniError;
1112
1113 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1114 match value {
1115 Value::Null => Ok(None),
1116 _ => T::try_from(value).map(Some),
1117 }
1118 }
1119}
1120
1121impl<T> TryFrom<Value> for Option<T>
1122where
1123 T: TryFrom<Value, Error = UniError>,
1124{
1125 type Error = UniError;
1126 fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1127 match value {
1128 Value::Null => Ok(None),
1129 _ => T::try_from(value).map(Some),
1130 }
1131 }
1132}
1133
1134impl<T> TryFrom<&Value> for Vec<T>
1135where
1136 T: for<'a> TryFrom<&'a Value, Error = UniError>,
1137{
1138 type Error = UniError;
1139
1140 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1141 match value {
1142 Value::List(l) => {
1143 let mut vec = Vec::with_capacity(l.len());
1144 for item in l {
1145 vec.push(T::try_from(item)?);
1146 }
1147 Ok(vec)
1148 }
1149 _ => Err(type_error("List", value)),
1150 }
1151 }
1152}
1153
1154impl<T> TryFrom<Value> for Vec<T>
1155where
1156 T: TryFrom<Value, Error = UniError>,
1157{
1158 type Error = UniError;
1159 fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1160 match value {
1161 Value::List(l) => {
1162 let mut vec = Vec::with_capacity(l.len());
1163 for item in l {
1164 vec.push(T::try_from(item)?);
1165 }
1166 Ok(vec)
1167 }
1168 other => Err(type_error("List", &other)),
1169 }
1170 }
1171}
1172
1173fn get_with_fallback<'a>(map: &'a HashMap<String, Value>, keys: &[&str]) -> Option<&'a Value> {
1179 keys.iter().find_map(|k| map.get(*k))
1180}
1181
1182fn extract_properties(value: &Value) -> HashMap<String, Value> {
1184 match value {
1185 Value::Map(m) => m.clone(),
1186 _ => HashMap::new(),
1187 }
1188}
1189
1190impl TryFrom<&Value> for Node {
1191 type Error = UniError;
1192
1193 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1194 match value {
1195 Value::Node(n) => Ok(n.clone()),
1196 Value::Map(m) => {
1197 let vid_val = get_with_fallback(m, &["_vid", "_id", "vid"]);
1198 let props_val = m.get("properties");
1199
1200 let (Some(v), Some(p)) = (vid_val, props_val) else {
1201 return Err(type_error("Node Map", value));
1202 };
1203
1204 let labels = if let Some(Value::List(label_list)) = m.get("_labels") {
1206 label_list
1207 .iter()
1208 .filter_map(|v| {
1209 if let Value::String(s) = v {
1210 Some(s.clone())
1211 } else {
1212 None
1213 }
1214 })
1215 .collect()
1216 } else {
1217 Vec::new()
1218 };
1219
1220 Ok(Node {
1221 vid: Vid::try_from(v)?,
1222 labels,
1223 properties: extract_properties(p),
1224 })
1225 }
1226 _ => Err(type_error("Node", value)),
1227 }
1228 }
1229}
1230
1231impl TryFrom<&Value> for Edge {
1232 type Error = UniError;
1233
1234 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1235 match value {
1236 Value::Edge(e) => Ok(e.clone()),
1237 Value::Map(m) => {
1238 let eid_val = get_with_fallback(m, &["_eid", "_id", "eid"]);
1239 let type_val = get_with_fallback(m, &["_type_name", "_type", "edge_type"]);
1240 let src_val = get_with_fallback(m, &["_src", "src"]);
1241 let dst_val = get_with_fallback(m, &["_dst", "dst"]);
1242 let props_val = m.get("properties");
1243
1244 let (Some(id), Some(t), Some(s), Some(d), Some(p)) =
1245 (eid_val, type_val, src_val, dst_val, props_val)
1246 else {
1247 return Err(type_error("Edge Map", value));
1248 };
1249
1250 Ok(Edge {
1251 eid: Eid::try_from(id)?,
1252 edge_type: String::try_from(t)?,
1253 src: Vid::try_from(s)?,
1254 dst: Vid::try_from(d)?,
1255 properties: extract_properties(p),
1256 })
1257 }
1258 _ => Err(type_error("Edge", value)),
1259 }
1260 }
1261}
1262
1263impl TryFrom<&Value> for Path {
1264 type Error = UniError;
1265
1266 fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1267 match value {
1268 Value::Path(p) => Ok(p.clone()),
1269 Value::Map(m) => {
1270 let (Some(Value::List(nodes_list)), Some(Value::List(rels_list))) =
1271 (m.get("nodes"), m.get("relationships"))
1272 else {
1273 return Err(type_error("Path (Map with nodes/relationships)", value));
1274 };
1275
1276 let nodes = nodes_list
1277 .iter()
1278 .map(Node::try_from)
1279 .collect::<std::result::Result<Vec<_>, _>>()?;
1280
1281 let edges = rels_list
1282 .iter()
1283 .map(Edge::try_from)
1284 .collect::<std::result::Result<Vec<_>, _>>()?;
1285
1286 Ok(Path { nodes, edges })
1287 }
1288 _ => Err(type_error("Path", value)),
1289 }
1290 }
1291}
1292
1293impl From<String> for Value {
1298 fn from(v: String) -> Self {
1299 Value::String(v)
1300 }
1301}
1302
1303impl From<&str> for Value {
1304 fn from(v: &str) -> Self {
1305 Value::String(v.to_string())
1306 }
1307}
1308
1309impl From<i64> for Value {
1310 fn from(v: i64) -> Self {
1311 Value::Int(v)
1312 }
1313}
1314
1315impl From<i32> for Value {
1316 fn from(v: i32) -> Self {
1317 Value::Int(v as i64)
1318 }
1319}
1320
1321impl From<f64> for Value {
1322 fn from(v: f64) -> Self {
1323 Value::Float(v)
1324 }
1325}
1326
1327impl From<bool> for Value {
1328 fn from(v: bool) -> Self {
1329 Value::Bool(v)
1330 }
1331}
1332
1333impl From<Vec<f32>> for Value {
1334 fn from(v: Vec<f32>) -> Self {
1335 Value::Vector(v)
1336 }
1337}
1338
1339impl From<serde_json::Value> for Value {
1344 fn from(v: serde_json::Value) -> Self {
1345 match v {
1346 serde_json::Value::Null => Value::Null,
1347 serde_json::Value::Bool(b) => Value::Bool(b),
1348 serde_json::Value::Number(n) => {
1349 if let Some(i) = n.as_i64() {
1350 Value::Int(i)
1351 } else if let Some(f) = n.as_f64() {
1352 Value::Float(f)
1353 } else {
1354 Value::Null
1355 }
1356 }
1357 serde_json::Value::String(s) => Value::String(s),
1358 serde_json::Value::Array(arr) => {
1359 Value::List(arr.into_iter().map(Value::from).collect())
1360 }
1361 serde_json::Value::Object(obj) => {
1362 Value::Map(obj.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
1363 }
1364 }
1365 }
1366}
1367
1368impl From<Value> for serde_json::Value {
1369 fn from(v: Value) -> Self {
1370 match v {
1371 Value::Null => serde_json::Value::Null,
1372 Value::Bool(b) => serde_json::Value::Bool(b),
1373 Value::Int(i) => serde_json::Value::Number(serde_json::Number::from(i)),
1374 Value::Float(f) => serde_json::Number::from_f64(f)
1375 .map(serde_json::Value::Number)
1376 .unwrap_or(serde_json::Value::Null), Value::String(s) => serde_json::Value::String(s),
1378 Value::Bytes(b) => {
1379 use base64::Engine;
1380 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b))
1381 }
1382 Value::List(l) => {
1383 serde_json::Value::Array(l.into_iter().map(serde_json::Value::from).collect())
1384 }
1385 Value::Map(m) => {
1386 let mut map = serde_json::Map::new();
1387 for (k, v) in m {
1388 map.insert(k, v.into());
1389 }
1390 serde_json::Value::Object(map)
1391 }
1392 Value::Node(n) => {
1393 let mut map = serde_json::Map::new();
1394 map.insert(
1395 "_id".to_string(),
1396 serde_json::Value::String(n.vid.to_string()),
1397 );
1398 map.insert(
1399 "_labels".to_string(),
1400 serde_json::Value::Array(
1401 n.labels
1402 .into_iter()
1403 .map(serde_json::Value::String)
1404 .collect(),
1405 ),
1406 );
1407 let props: serde_json::Value = Value::Map(n.properties).into();
1408 map.insert("properties".to_string(), props);
1409 serde_json::Value::Object(map)
1410 }
1411 Value::Edge(e) => {
1412 let mut map = serde_json::Map::new();
1413 map.insert(
1414 "_id".to_string(),
1415 serde_json::Value::String(e.eid.to_string()),
1416 );
1417 map.insert("_type".to_string(), serde_json::Value::String(e.edge_type));
1418 map.insert(
1419 "_src".to_string(),
1420 serde_json::Value::String(e.src.to_string()),
1421 );
1422 map.insert(
1423 "_dst".to_string(),
1424 serde_json::Value::String(e.dst.to_string()),
1425 );
1426 let props: serde_json::Value = Value::Map(e.properties).into();
1427 map.insert("properties".to_string(), props);
1428 serde_json::Value::Object(map)
1429 }
1430 Value::Path(p) => {
1431 let mut map = serde_json::Map::new();
1432 map.insert(
1433 "nodes".to_string(),
1434 Value::List(p.nodes.into_iter().map(Value::Node).collect()).into(),
1435 );
1436 map.insert(
1437 "relationships".to_string(),
1438 Value::List(p.edges.into_iter().map(Value::Edge).collect()).into(),
1439 );
1440 serde_json::Value::Object(map)
1441 }
1442 Value::Vector(v) => serde_json::Value::Array(
1443 v.into_iter()
1444 .map(|f| {
1445 serde_json::Number::from_f64(f as f64)
1446 .map(serde_json::Value::Number)
1447 .unwrap_or(serde_json::Value::Null)
1448 })
1449 .collect(),
1450 ),
1451 Value::Temporal(t) => serde_json::Value::String(t.to_string()),
1452 }
1453 }
1454}
1455
1456#[macro_export]
1478macro_rules! unival {
1479 (null) => {
1481 $crate::Value::Null
1482 };
1483
1484 (true) => {
1486 $crate::Value::Bool(true)
1487 };
1488 (false) => {
1489 $crate::Value::Bool(false)
1490 };
1491
1492 ([ $($elem:tt),* $(,)? ]) => {
1494 $crate::Value::List(vec![ $( $crate::unival!($elem) ),* ])
1495 };
1496
1497 ({ $($key:tt : $val:tt),* $(,)? }) => {
1499 $crate::Value::Map({
1500 #[allow(unused_mut)]
1501 let mut map = ::std::collections::HashMap::new();
1502 $( map.insert(($key).to_string(), $crate::unival!($val)); )*
1503 map
1504 })
1505 };
1506
1507 ($e:expr) => {
1509 $crate::Value::from($e)
1510 };
1511}
1512
1513impl From<usize> for Value {
1518 fn from(v: usize) -> Self {
1519 Value::Int(v as i64)
1520 }
1521}
1522
1523impl From<u64> for Value {
1524 fn from(v: u64) -> Self {
1525 Value::Int(v as i64)
1526 }
1527}
1528
1529impl From<f32> for Value {
1530 fn from(v: f32) -> Self {
1531 Value::Float(v as f64)
1532 }
1533}
1534
1535#[cfg(test)]
1540mod tests {
1541 use super::*;
1542
1543 #[test]
1544 fn test_accessor_methods() {
1545 assert!(Value::Null.is_null());
1546 assert!(!Value::Int(1).is_null());
1547
1548 assert_eq!(Value::Bool(true).as_bool(), Some(true));
1549 assert_eq!(Value::Int(42).as_bool(), None);
1550
1551 assert_eq!(Value::Int(42).as_i64(), Some(42));
1552 assert_eq!(Value::Float(2.5).as_i64(), None);
1553
1554 assert_eq!(Value::Float(2.5).as_f64(), Some(2.5));
1556 assert_eq!(Value::Int(42).as_f64(), Some(42.0));
1557 assert_eq!(Value::String("x".into()).as_f64(), None);
1558
1559 assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
1560 assert_eq!(Value::Int(1).as_str(), None);
1561
1562 assert!(Value::Int(1).is_i64());
1563 assert!(!Value::Float(1.0).is_i64());
1564
1565 assert!(Value::Float(1.0).is_f64());
1566 assert!(!Value::Int(1).is_f64());
1567
1568 assert!(Value::Int(1).is_number());
1569 assert!(Value::Float(1.0).is_number());
1570 assert!(!Value::String("x".into()).is_number());
1571 }
1572
1573 #[test]
1574 fn test_serde_json_roundtrip() {
1575 let val = Value::Int(42);
1576 let json: serde_json::Value = val.clone().into();
1577 let back: Value = json.into();
1578 assert_eq!(val, back);
1579
1580 let val = Value::Float(2.5);
1581 let json: serde_json::Value = val.clone().into();
1582 let back: Value = json.into();
1583 assert_eq!(val, back);
1584
1585 let val = Value::String("hello".into());
1586 let json: serde_json::Value = val.clone().into();
1587 let back: Value = json.into();
1588 assert_eq!(val, back);
1589
1590 let val = Value::List(vec![Value::Int(1), Value::Int(2)]);
1591 let json: serde_json::Value = val.clone().into();
1592 let back: Value = json.into();
1593 assert_eq!(val, back);
1594 }
1595
1596 #[test]
1597 fn test_unival_macro() {
1598 assert_eq!(unival!(null), Value::Null);
1599 assert_eq!(unival!(true), Value::Bool(true));
1600 assert_eq!(unival!(false), Value::Bool(false));
1601 assert_eq!(unival!(42_i64), Value::Int(42));
1602 assert_eq!(unival!(2.5_f64), Value::Float(2.5));
1603 assert_eq!(unival!("hello"), Value::String("hello".into()));
1604
1605 let list = unival!([1_i64, 2_i64]);
1607 assert_eq!(list, Value::List(vec![Value::Int(1), Value::Int(2)]));
1608
1609 let map = unival!({"key": "val", "num": 42_i64});
1611 if let Value::Map(m) = &map {
1612 assert_eq!(m.get("key"), Some(&Value::String("val".into())));
1613 assert_eq!(m.get("num"), Some(&Value::Int(42)));
1614 } else {
1615 panic!("Expected Map");
1616 }
1617
1618 let x: i64 = 99;
1620 assert_eq!(unival!(x), Value::Int(99));
1621 }
1622
1623 #[test]
1624 fn test_int_float_distinction_preserved() {
1625 let int_val = Value::Int(42);
1627 let float_val = Value::Float(42.0);
1628
1629 assert!(int_val.is_i64());
1630 assert!(!int_val.is_f64());
1631
1632 assert!(float_val.is_f64());
1633 assert!(!float_val.is_i64());
1634
1635 assert_ne!(int_val, float_val);
1637 }
1638
1639 #[test]
1640 fn test_temporal_display_zero_seconds_omitted() {
1641 let lt = TemporalValue::LocalTime {
1643 nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1644 };
1645 assert_eq!(lt.to_string(), "12:00");
1646
1647 let lt2 = TemporalValue::LocalTime {
1649 nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1650 };
1651 assert_eq!(lt2.to_string(), "12:31:14");
1652
1653 let lt3 = TemporalValue::LocalTime {
1655 nanos_since_midnight: 500_000_000,
1656 };
1657 assert_eq!(lt3.to_string(), "00:00:00.5");
1658
1659 let t = TemporalValue::Time {
1661 nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1662 offset_seconds: 0,
1663 };
1664 assert_eq!(t.to_string(), "12:00Z");
1665
1666 let t2 = TemporalValue::Time {
1668 nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1669 offset_seconds: 3600,
1670 };
1671 assert_eq!(t2.to_string(), "12:31:14+01:00");
1672
1673 let epoch_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1675 .unwrap()
1676 .and_hms_opt(12, 31, 0)
1677 .unwrap()
1678 .and_utc()
1679 .timestamp_nanos_opt()
1680 .unwrap();
1681 let ldt = TemporalValue::LocalDateTime {
1682 nanos_since_epoch: epoch_nanos,
1683 };
1684 assert_eq!(ldt.to_string(), "1984-10-11T12:31");
1685
1686 let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1688 .unwrap()
1689 .and_hms_opt(11, 31, 0)
1690 .unwrap()
1691 .and_utc()
1692 .timestamp_nanos_opt()
1693 .unwrap();
1694 let dt = TemporalValue::DateTime {
1695 nanos_since_epoch: utc_nanos,
1696 offset_seconds: 3600,
1697 timezone_name: None,
1698 };
1699 assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00");
1700
1701 let utc_nanos2 = chrono::NaiveDate::from_ymd_opt(2015, 7, 21)
1703 .unwrap()
1704 .and_hms_nano_opt(20, 40, 32, 142_000_000)
1705 .unwrap()
1706 .and_utc()
1707 .timestamp_nanos_opt()
1708 .unwrap();
1709 let dt2 = TemporalValue::DateTime {
1710 nanos_since_epoch: utc_nanos2,
1711 offset_seconds: 3600,
1712 timezone_name: None,
1713 };
1714 assert_eq!(dt2.to_string(), "2015-07-21T21:40:32.142+01:00");
1715
1716 let utc_nanos3 = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1718 .unwrap()
1719 .and_hms_opt(12, 31, 0)
1720 .unwrap()
1721 .and_utc()
1722 .timestamp_nanos_opt()
1723 .unwrap();
1724 let dt3 = TemporalValue::DateTime {
1725 nanos_since_epoch: utc_nanos3,
1726 offset_seconds: 0,
1727 timezone_name: None,
1728 };
1729 assert_eq!(dt3.to_string(), "1984-10-11T12:31Z");
1730 }
1731
1732 #[test]
1733 fn test_temporal_display_fractional_trailing_zeros_stripped() {
1734 let d = TemporalValue::Duration {
1736 months: 0,
1737 days: 0,
1738 nanos: 900_000_000,
1739 };
1740 assert_eq!(d.to_string(), "PT0.9S");
1741
1742 let d2 = TemporalValue::Duration {
1744 months: 0,
1745 days: 0,
1746 nanos: 400_000_000,
1747 };
1748 assert_eq!(d2.to_string(), "PT0.4S");
1749
1750 let d3 = TemporalValue::Duration {
1752 months: 0,
1753 days: 0,
1754 nanos: 142_000_000,
1755 };
1756 assert_eq!(d3.to_string(), "PT0.142S");
1757
1758 let d4 = TemporalValue::Duration {
1760 months: 0,
1761 days: 0,
1762 nanos: 1,
1763 };
1764 assert_eq!(d4.to_string(), "PT0.000000001S");
1765 }
1766
1767 #[test]
1768 fn test_temporal_display_offset_second_precision() {
1769 let t = TemporalValue::Time {
1771 nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1772 offset_seconds: 2 * 3600 + 5 * 60 + 59,
1773 };
1774 assert_eq!(t.to_string(), "12:00+02:05:59");
1775
1776 let t2 = TemporalValue::Time {
1778 nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1779 offset_seconds: -(2 * 3600 + 5 * 60 + 7),
1780 };
1781 assert_eq!(t2.to_string(), "12:00-02:05:07");
1782 }
1783
1784 #[test]
1785 fn test_temporal_display_datetime_with_timezone_name() {
1786 let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1787 .unwrap()
1788 .and_hms_opt(11, 31, 0)
1789 .unwrap()
1790 .and_utc()
1791 .timestamp_nanos_opt()
1792 .unwrap();
1793 let dt = TemporalValue::DateTime {
1794 nanos_since_epoch: utc_nanos,
1795 offset_seconds: 3600,
1796 timezone_name: Some("Europe/Stockholm".to_string()),
1797 };
1798 assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00[Europe/Stockholm]");
1799 }
1800}