Skip to main content

reifydb_value/value/temporal/parse/
duration.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 ReifyDB
3
4use std::collections;
5
6use crate::{
7	error::{Error, TemporalKind, TypeError},
8	fragment::Fragment,
9	value::Duration,
10};
11
12fn validate_component_order(
13	component: char,
14	seen: &mut collections::HashSet<char>,
15	last_order: &mut u8,
16	current_order: u8,
17	fragment: Fragment,
18	position: usize,
19) -> Result<(), Error> {
20	let key = if component == 'M' {
21		'M'
22	} else {
23		component
24	};
25
26	if seen.contains(&key) {
27		let frag = fragment.sub_fragment(position, 1);
28		return Err(TypeError::Temporal {
29			kind: TemporalKind::DuplicateDurationComponent {
30				component,
31			},
32			message: format!("duplicate duration component '{}'", component),
33			fragment: frag,
34		}
35		.into());
36	}
37
38	if current_order <= *last_order {
39		let frag = fragment.sub_fragment(position, 1);
40		return Err(TypeError::Temporal {
41			kind: TemporalKind::OutOfOrderDurationComponent {
42				component,
43			},
44			message: format!("duration component '{}' is out of order", component),
45			fragment: frag,
46		}
47		.into());
48	}
49
50	seen.insert(key);
51	*last_order = current_order;
52	Ok(())
53}
54
55pub fn parse_duration(fragment: Fragment) -> Result<Duration, Error> {
56	let fragment_value = fragment.text();
57
58	if fragment_value.starts_with('P') {
59		return parse_iso_duration(fragment);
60	}
61
62	parse_human_duration(fragment)
63}
64
65fn parse_human_duration(fragment: Fragment) -> Result<Duration, Error> {
66	let input = fragment.text();
67	let bytes = input.as_bytes();
68	let len = bytes.len();
69
70	if len == 0 {
71		return Err(TypeError::Temporal {
72			kind: TemporalKind::InvalidDurationFormat,
73			message: "invalid duration format".into(),
74			fragment,
75		}
76		.into());
77	}
78
79	let mut months = 0i32;
80	let mut days = 0i32;
81	let mut nanos = 0i64;
82	let mut pos = 0;
83	let mut found_any = false;
84
85	let mut last_order = 0u8;
86
87	while pos < len {
88		let num_start = pos;
89		while pos < len && bytes[pos].is_ascii_digit() {
90			pos += 1;
91		}
92
93		if pos == num_start || pos >= len {
94			return Err(TypeError::Temporal {
95				kind: TemporalKind::InvalidDurationFormat,
96				message: "invalid duration format".into(),
97				fragment,
98			}
99			.into());
100		}
101
102		let num_str = &input[num_start..pos];
103		let value: i64 = num_str.parse().map_err(|_| {
104			let frag = fragment.sub_fragment(num_start, num_str.len());
105			let err: Error = TypeError::Temporal {
106				kind: TemporalKind::InvalidDurationFormat,
107				message: "invalid duration format".into(),
108				fragment: frag,
109			}
110			.into();
111			err
112		})?;
113
114		let (order, advance) = if pos + 1 < len && bytes[pos] == b'n' && bytes[pos + 1] == b's' {
115			nanos += value;
116			(9u8, 2)
117		} else if pos + 1 < len && bytes[pos] == b'u' && bytes[pos + 1] == b's' {
118			nanos += value * 1_000;
119			(8u8, 2)
120		} else if pos + 1 < len && bytes[pos] == b'm' && bytes[pos + 1] == b's' {
121			nanos += value * 1_000_000;
122			(7u8, 2)
123		} else if pos + 1 < len && bytes[pos] == b'm' && bytes[pos + 1] == b'o' {
124			months += value as i32;
125			(2u8, 2)
126		} else if bytes[pos] == b'y' {
127			months += value as i32 * 12;
128			(1u8, 1)
129		} else if bytes[pos] == b'd' {
130			days += value as i32;
131			(3u8, 1)
132		} else if bytes[pos] == b'h' {
133			nanos += value * 60 * 60 * 1_000_000_000;
134			(4u8, 1)
135		} else if bytes[pos] == b'm' {
136			nanos += value * 60 * 1_000_000_000;
137			(5u8, 1)
138		} else if bytes[pos] == b's' {
139			nanos += value * 1_000_000_000;
140			(6u8, 1)
141		} else {
142			let char_frag = fragment.sub_fragment(pos, 1);
143			return Err(TypeError::Temporal {
144				kind: TemporalKind::InvalidDurationCharacter,
145				message: format!("invalid character in duration '{}'", char_frag.text()),
146				fragment: char_frag,
147			}
148			.into());
149		};
150
151		if order <= last_order {
152			let frag = fragment.sub_fragment(pos, advance);
153			return Err(TypeError::Temporal {
154				kind: TemporalKind::OutOfOrderDurationComponent {
155					component: bytes[pos] as char,
156				},
157				message: format!("duration component '{}' is out of order", &input[pos..pos + advance]),
158				fragment: frag,
159			}
160			.into());
161		}
162
163		last_order = order;
164		pos += advance;
165		found_any = true;
166	}
167
168	if !found_any {
169		return Err(TypeError::Temporal {
170			kind: TemporalKind::InvalidDurationFormat,
171			message: "invalid duration format".into(),
172			fragment,
173		}
174		.into());
175	}
176
177	Ok(Duration::new(months, days, nanos)?)
178}
179
180fn parse_iso_duration(fragment: Fragment) -> Result<Duration, Error> {
181	let fragment_value = fragment.text();
182
183	if fragment_value.len() == 1 || fragment_value == "PT" {
184		return Err(TypeError::Temporal {
185			kind: TemporalKind::InvalidDurationFormat,
186			message: "invalid duration format".into(),
187			fragment,
188		}
189		.into());
190	}
191
192	let chars = fragment_value.chars().skip(1);
193	let mut months = 0i32;
194	let mut days = 0i32;
195	let mut nanos = 0i64;
196	let mut current_number = String::new();
197	let mut in_time_part = false;
198	let mut current_position = 1;
199
200	let mut seen_date_components = collections::HashSet::new();
201	let mut seen_time_components = collections::HashSet::new();
202	let mut last_date_component_order = 0u8;
203	let mut last_time_component_order = 0u8;
204
205	for c in chars {
206		match c {
207			'T' => {
208				in_time_part = true;
209				current_position += 1;
210			}
211			'0'..='9' | '.' => {
212				current_number.push(c);
213				current_position += 1;
214			}
215			'Y' => {
216				if in_time_part {
217					let unit_frag = fragment.sub_fragment(current_position, 1);
218					return Err(TypeError::Temporal {
219						kind: TemporalKind::InvalidUnitInContext {
220							unit: 'Y',
221							in_time_part: true,
222						},
223						message: format!("invalid unit '{}' in {}", 'Y', "time part (after T)"),
224						fragment: unit_frag,
225					}
226					.into());
227				}
228				if current_number.is_empty() {
229					let unit_frag = fragment.sub_fragment(current_position, 1);
230					return Err(TypeError::Temporal {
231						kind: TemporalKind::IncompleteDurationSpecification,
232						message: "incomplete duration specification".into(),
233						fragment: unit_frag,
234					}
235					.into());
236				}
237				if current_number.contains('.') {
238					let start = current_position - current_number.len();
239					let dot_pos = start + current_number.find('.').unwrap();
240					let char_frag = fragment.sub_fragment(dot_pos, 1);
241					return Err(TypeError::Temporal {
242						kind: TemporalKind::InvalidDurationCharacter,
243						message: format!(
244							"invalid character in duration '{}'",
245							char_frag.text()
246						),
247						fragment: char_frag,
248					}
249					.into());
250				}
251
252				validate_component_order(
253					'Y',
254					&mut seen_date_components,
255					&mut last_date_component_order,
256					1,
257					fragment.clone(),
258					current_position,
259				)?;
260
261				let years: i32 = current_number.parse().map_err(|_| {
262					let start = current_position - current_number.len();
263					let number_frag = fragment.sub_fragment(start, current_number.len());
264					let err: Error = TypeError::Temporal {
265						kind: TemporalKind::InvalidDurationComponentValue {
266							unit: 'Y',
267						},
268						message: format!("invalid year value '{}'", number_frag.text()),
269						fragment: number_frag,
270					}
271					.into();
272					err
273				})?;
274				months += years * 12;
275				current_number.clear();
276				current_position += 1;
277			}
278			'M' => {
279				if current_number.is_empty() {
280					let unit_frag = fragment.sub_fragment(current_position, 1);
281					return Err(TypeError::Temporal {
282						kind: TemporalKind::IncompleteDurationSpecification,
283						message: "incomplete duration specification".into(),
284						fragment: unit_frag,
285					}
286					.into());
287				}
288				if current_number.contains('.') {
289					let start = current_position - current_number.len();
290					let dot_pos = start + current_number.find('.').unwrap();
291					let char_frag = fragment.sub_fragment(dot_pos, 1);
292					return Err(TypeError::Temporal {
293						kind: TemporalKind::InvalidDurationCharacter,
294						message: format!(
295							"invalid character in duration '{}'",
296							char_frag.text()
297						),
298						fragment: char_frag,
299					}
300					.into());
301				}
302
303				if in_time_part {
304					validate_component_order(
305						'M',
306						&mut seen_time_components,
307						&mut last_time_component_order,
308						2,
309						fragment.clone(),
310						current_position,
311					)?;
312				} else {
313					validate_component_order(
314						'M',
315						&mut seen_date_components,
316						&mut last_date_component_order,
317						2,
318						fragment.clone(),
319						current_position,
320					)?;
321				}
322
323				let value: i64 = current_number.parse().map_err(|_| {
324					let start = current_position - current_number.len();
325					let number_frag = fragment.sub_fragment(start, current_number.len());
326					let err: Error = TypeError::Temporal {
327						kind: TemporalKind::InvalidDurationComponentValue {
328							unit: 'M',
329						},
330						message: format!("invalid month/minute value '{}'", number_frag.text()),
331						fragment: number_frag,
332					}
333					.into();
334					err
335				})?;
336				if in_time_part {
337					nanos += value * 60 * 1_000_000_000;
338				} else {
339					months += value as i32;
340				}
341				current_number.clear();
342				current_position += 1;
343			}
344			'W' => {
345				if in_time_part {
346					let unit_frag = fragment.sub_fragment(current_position, 1);
347					return Err(TypeError::Temporal {
348						kind: TemporalKind::InvalidUnitInContext {
349							unit: 'W',
350							in_time_part: true,
351						},
352						message: format!("invalid unit '{}' in {}", 'W', "time part (after T)"),
353						fragment: unit_frag,
354					}
355					.into());
356				}
357				if current_number.is_empty() {
358					let unit_frag = fragment.sub_fragment(current_position, 1);
359					return Err(TypeError::Temporal {
360						kind: TemporalKind::IncompleteDurationSpecification,
361						message: "incomplete duration specification".into(),
362						fragment: unit_frag,
363					}
364					.into());
365				}
366				if current_number.contains('.') {
367					let start = current_position - current_number.len();
368					let dot_pos = start + current_number.find('.').unwrap();
369					let char_frag = fragment.sub_fragment(dot_pos, 1);
370					return Err(TypeError::Temporal {
371						kind: TemporalKind::InvalidDurationCharacter,
372						message: format!(
373							"invalid character in duration '{}'",
374							char_frag.text()
375						),
376						fragment: char_frag,
377					}
378					.into());
379				}
380
381				validate_component_order(
382					'W',
383					&mut seen_date_components,
384					&mut last_date_component_order,
385					3,
386					fragment.clone(),
387					current_position,
388				)?;
389
390				let weeks: i32 = current_number.parse().map_err(|_| {
391					let start = current_position - current_number.len();
392					let number_frag = fragment.sub_fragment(start, current_number.len());
393					let err: Error = TypeError::Temporal {
394						kind: TemporalKind::InvalidDurationComponentValue {
395							unit: 'W',
396						},
397						message: format!("invalid week value '{}'", number_frag.text()),
398						fragment: number_frag,
399					}
400					.into();
401					err
402				})?;
403				days += weeks * 7;
404				current_number.clear();
405				current_position += 1;
406			}
407			'D' => {
408				if in_time_part {
409					let unit_frag = fragment.sub_fragment(current_position, 1);
410					return Err(TypeError::Temporal {
411						kind: TemporalKind::InvalidUnitInContext {
412							unit: 'D',
413							in_time_part: true,
414						},
415						message: format!("invalid unit '{}' in {}", 'D', "time part (after T)"),
416						fragment: unit_frag,
417					}
418					.into());
419				}
420				if current_number.is_empty() {
421					let unit_frag = fragment.sub_fragment(current_position, 1);
422					return Err(TypeError::Temporal {
423						kind: TemporalKind::IncompleteDurationSpecification,
424						message: "incomplete duration specification".into(),
425						fragment: unit_frag,
426					}
427					.into());
428				}
429				if current_number.contains('.') {
430					let start = current_position - current_number.len();
431					let dot_pos = start + current_number.find('.').unwrap();
432					let char_frag = fragment.sub_fragment(dot_pos, 1);
433					return Err(TypeError::Temporal {
434						kind: TemporalKind::InvalidDurationCharacter,
435						message: format!(
436							"invalid character in duration '{}'",
437							char_frag.text()
438						),
439						fragment: char_frag,
440					}
441					.into());
442				}
443
444				validate_component_order(
445					'D',
446					&mut seen_date_components,
447					&mut last_date_component_order,
448					4,
449					fragment.clone(),
450					current_position,
451				)?;
452
453				let day_value: i32 = current_number.parse().map_err(|_| {
454					let start = current_position - current_number.len();
455					let number_frag = fragment.sub_fragment(start, current_number.len());
456					let err: Error = TypeError::Temporal {
457						kind: TemporalKind::InvalidDurationComponentValue {
458							unit: 'D',
459						},
460						message: format!("invalid day value '{}'", number_frag.text()),
461						fragment: number_frag,
462					}
463					.into();
464					err
465				})?;
466				days += day_value;
467				current_number.clear();
468				current_position += 1;
469			}
470			'H' => {
471				if !in_time_part {
472					let unit_frag = fragment.sub_fragment(current_position, 1);
473					return Err(TypeError::Temporal {
474						kind: TemporalKind::InvalidUnitInContext {
475							unit: 'H',
476							in_time_part: false,
477						},
478						message: format!(
479							"invalid unit '{}' in {}",
480							'H', "date part (before T)"
481						),
482						fragment: unit_frag,
483					}
484					.into());
485				}
486				if current_number.is_empty() {
487					let unit_frag = fragment.sub_fragment(current_position, 1);
488					return Err(TypeError::Temporal {
489						kind: TemporalKind::IncompleteDurationSpecification,
490						message: "incomplete duration specification".into(),
491						fragment: unit_frag,
492					}
493					.into());
494				}
495				if current_number.contains('.') {
496					let start = current_position - current_number.len();
497					let dot_pos = start + current_number.find('.').unwrap();
498					let char_frag = fragment.sub_fragment(dot_pos, 1);
499					return Err(TypeError::Temporal {
500						kind: TemporalKind::InvalidDurationCharacter,
501						message: format!(
502							"invalid character in duration '{}'",
503							char_frag.text()
504						),
505						fragment: char_frag,
506					}
507					.into());
508				}
509
510				validate_component_order(
511					'H',
512					&mut seen_time_components,
513					&mut last_time_component_order,
514					1,
515					fragment.clone(),
516					current_position,
517				)?;
518
519				let hours: i64 = current_number.parse().map_err(|_| {
520					let start = current_position - current_number.len();
521					let number_frag = fragment.sub_fragment(start, current_number.len());
522					let err: Error = TypeError::Temporal {
523						kind: TemporalKind::InvalidDurationComponentValue {
524							unit: 'H',
525						},
526						message: format!("invalid hour value '{}'", number_frag.text()),
527						fragment: number_frag,
528					}
529					.into();
530					err
531				})?;
532				nanos += hours * 60 * 60 * 1_000_000_000;
533				current_number.clear();
534				current_position += 1;
535			}
536			'S' => {
537				if !in_time_part {
538					let unit_frag = fragment.sub_fragment(current_position, 1);
539					return Err(TypeError::Temporal {
540						kind: TemporalKind::InvalidUnitInContext {
541							unit: 'S',
542							in_time_part: false,
543						},
544						message: format!(
545							"invalid unit '{}' in {}",
546							'S', "date part (before T)"
547						),
548						fragment: unit_frag,
549					}
550					.into());
551				}
552				if current_number.is_empty() {
553					let unit_frag = fragment.sub_fragment(current_position, 1);
554					return Err(TypeError::Temporal {
555						kind: TemporalKind::IncompleteDurationSpecification,
556						message: "incomplete duration specification".into(),
557						fragment: unit_frag,
558					}
559					.into());
560				}
561
562				validate_component_order(
563					'S',
564					&mut seen_time_components,
565					&mut last_time_component_order,
566					3,
567					fragment.clone(),
568					current_position,
569				)?;
570
571				if current_number.contains('.') {
572					let seconds_float: f64 = current_number.parse().map_err(|_| {
573						let start = current_position - current_number.len();
574						let number_frag = fragment.sub_fragment(start, current_number.len());
575						let err: Error = TypeError::Temporal {
576							kind: TemporalKind::InvalidDurationComponentValue {
577								unit: 'S',
578							},
579							message: format!(
580								"invalid second value '{}'",
581								number_frag.text()
582							),
583							fragment: number_frag,
584						}
585						.into();
586						err
587					})?;
588					nanos += (seconds_float * 1_000_000_000.0) as i64;
589				} else {
590					let seconds: i64 = current_number.parse().map_err(|_| {
591						let start = current_position - current_number.len();
592						let number_frag = fragment.sub_fragment(start, current_number.len());
593						let err: Error = TypeError::Temporal {
594							kind: TemporalKind::InvalidDurationComponentValue {
595								unit: 'S',
596							},
597							message: format!(
598								"invalid second value '{}'",
599								number_frag.text()
600							),
601							fragment: number_frag,
602						}
603						.into();
604						err
605					})?;
606					nanos += seconds * 1_000_000_000;
607				}
608
609				current_number.clear();
610				current_position += 1;
611			}
612			_ => {
613				let char_frag = fragment.sub_fragment(current_position, 1);
614				return Err(TypeError::Temporal {
615					kind: TemporalKind::InvalidDurationCharacter,
616					message: format!("invalid character in duration '{}'", char_frag.text()),
617					fragment: char_frag,
618				}
619				.into());
620			}
621		}
622	}
623
624	if !current_number.is_empty() {
625		let start = current_position - current_number.len();
626		let number_frag = fragment.sub_fragment(start, current_number.len());
627		return Err(TypeError::Temporal {
628			kind: TemporalKind::IncompleteDurationSpecification,
629			message: "incomplete duration specification".into(),
630			fragment: number_frag,
631		}
632		.into());
633	}
634
635	Ok(Duration::new(months, days, nanos)?)
636}
637
638#[cfg(test)]
639pub mod tests {
640	use super::parse_duration;
641	use crate::fragment::Fragment;
642
643	#[test]
644	fn test_days() {
645		let fragment = Fragment::testing("P1D");
646		let duration = parse_duration(fragment).unwrap();
647		// 1 day = 1 day, 0 nanos
648		assert_eq!(duration.get_days(), 1);
649		assert_eq!(duration.get_nanos(), 0);
650	}
651
652	#[test]
653	fn test_time_hours_minutes() {
654		let fragment = Fragment::testing("PT2H30M");
655		let duration = parse_duration(fragment).unwrap();
656		// 2 hours 30 minutes = (2 * 60 * 60 + 30 * 60) * 1_000_000_000
657		// nanos
658		assert_eq!(duration.get_nanos(), (2 * 60 * 60 + 30 * 60) * 1_000_000_000);
659	}
660
661	#[test]
662	fn test_comptokenize() {
663		let fragment = Fragment::testing("P1DT2H30M");
664		let duration = parse_duration(fragment).unwrap();
665		// 1 day + 2 hours + 30 minutes
666		let expected_nanos = (2 * 60 * 60 + 30 * 60) * 1_000_000_000;
667		assert_eq!(duration.get_days(), 1);
668		assert_eq!(duration.get_nanos(), expected_nanos);
669	}
670
671	#[test]
672	fn test_seconds_only() {
673		let fragment = Fragment::testing("PT45S");
674		let duration = parse_duration(fragment).unwrap();
675		assert_eq!(duration.get_nanos(), 45 * 1_000_000_000);
676	}
677
678	#[test]
679	fn test_minutes_only() {
680		let fragment = Fragment::testing("PT5M");
681		let duration = parse_duration(fragment).unwrap();
682		assert_eq!(duration.get_nanos(), 5 * 60 * 1_000_000_000);
683	}
684
685	#[test]
686	fn test_hours_only() {
687		let fragment = Fragment::testing("PT1H");
688		let duration = parse_duration(fragment).unwrap();
689		assert_eq!(duration.get_nanos(), 60 * 60 * 1_000_000_000);
690	}
691
692	#[test]
693	fn test_weeks() {
694		let fragment = Fragment::testing("P1W");
695		let duration = parse_duration(fragment).unwrap();
696		assert_eq!(duration.get_days(), 7);
697		assert_eq!(duration.get_nanos(), 0);
698	}
699
700	#[test]
701	fn test_years() {
702		let fragment = Fragment::testing("P1Y");
703		let duration = parse_duration(fragment).unwrap();
704		assert_eq!(duration.get_months(), 12);
705		assert_eq!(duration.get_days(), 0);
706		assert_eq!(duration.get_nanos(), 0);
707	}
708
709	#[test]
710	fn test_months() {
711		let fragment = Fragment::testing("P1M");
712		let duration = parse_duration(fragment).unwrap();
713		assert_eq!(duration.get_months(), 1);
714		assert_eq!(duration.get_days(), 0);
715		assert_eq!(duration.get_nanos(), 0);
716	}
717
718	#[test]
719	fn test_full_format() {
720		let fragment = Fragment::testing("P1Y2M3DT4H5M6S");
721		let duration = parse_duration(fragment).unwrap();
722		let expected_months = 12 + 2; // 1 year + 2 months
723		let expected_days = 3;
724		let expected_nanos = 4 * 60 * 60 * 1_000_000_000 +    // 4 hours
725                            5 * 60 * 1_000_000_000 +          // 5 minutes
726                            6 * 1_000_000_000; // 6 seconds
727		assert_eq!(duration.get_months(), expected_months);
728		assert_eq!(duration.get_days(), expected_days);
729		assert_eq!(duration.get_nanos(), expected_nanos);
730	}
731
732	#[test]
733	fn test_invalid_format() {
734		let fragment = Fragment::testing("invalid");
735		let err = parse_duration(fragment).unwrap_err();
736		assert_eq!(err.0.code, "TEMPORAL_004");
737	}
738
739	#[test]
740	fn test_invalid_character() {
741		let fragment = Fragment::testing("P1X");
742		let err = parse_duration(fragment).unwrap_err();
743		assert_eq!(err.0.code, "TEMPORAL_014");
744	}
745
746	#[test]
747	fn test_years_in_time_part() {
748		let fragment = Fragment::testing("PTY");
749		let err = parse_duration(fragment).unwrap_err();
750		assert_eq!(err.0.code, "TEMPORAL_016");
751	}
752
753	#[test]
754	fn test_weeks_in_time_part() {
755		let fragment = Fragment::testing("PTW");
756		let err = parse_duration(fragment).unwrap_err();
757		assert_eq!(err.0.code, "TEMPORAL_016");
758	}
759
760	#[test]
761	fn test_days_in_time_part() {
762		let fragment = Fragment::testing("PTD");
763		let err = parse_duration(fragment).unwrap_err();
764		assert_eq!(err.0.code, "TEMPORAL_016");
765	}
766
767	#[test]
768	fn test_hours_in_date_part() {
769		let fragment = Fragment::testing("P1H");
770		let err = parse_duration(fragment).unwrap_err();
771		assert_eq!(err.0.code, "TEMPORAL_016");
772	}
773
774	#[test]
775	fn test_seconds_in_date_part() {
776		let fragment = Fragment::testing("P1S");
777		let err = parse_duration(fragment).unwrap_err();
778		assert_eq!(err.0.code, "TEMPORAL_016");
779	}
780
781	#[test]
782	fn test_incomplete_specification() {
783		let fragment = Fragment::testing("P1");
784		let err = parse_duration(fragment).unwrap_err();
785		assert_eq!(err.0.code, "TEMPORAL_015");
786	}
787
788	#[test]
789	fn test_human_seconds() {
790		let d = parse_duration(Fragment::testing("30s")).unwrap();
791		assert_eq!(d.get_nanos(), 30 * 1_000_000_000);
792	}
793
794	#[test]
795	fn test_human_minutes() {
796		let d = parse_duration(Fragment::testing("5m")).unwrap();
797		assert_eq!(d.get_nanos(), 5 * 60 * 1_000_000_000);
798	}
799
800	#[test]
801	fn test_human_hours() {
802		let d = parse_duration(Fragment::testing("1h")).unwrap();
803		assert_eq!(d.get_nanos(), 60 * 60 * 1_000_000_000);
804	}
805
806	#[test]
807	fn test_human_days() {
808		let d = parse_duration(Fragment::testing("1d")).unwrap();
809		assert_eq!(d.get_days(), 1);
810		assert_eq!(d.get_nanos(), 0);
811	}
812
813	#[test]
814	fn test_human_months() {
815		let d = parse_duration(Fragment::testing("3mo")).unwrap();
816		assert_eq!(d.get_months(), 3);
817	}
818
819	#[test]
820	fn test_human_years() {
821		let d = parse_duration(Fragment::testing("2y")).unwrap();
822		assert_eq!(d.get_months(), 24);
823	}
824
825	#[test]
826	fn test_human_hours_minutes() {
827		let d = parse_duration(Fragment::testing("2h30m")).unwrap();
828		assert_eq!(d.get_nanos(), (2 * 60 * 60 + 30 * 60) * 1_000_000_000);
829	}
830
831	#[test]
832	fn test_human_days_hours_minutes() {
833		let d = parse_duration(Fragment::testing("1d2h30m")).unwrap();
834		assert_eq!(d.get_days(), 1);
835		assert_eq!(d.get_nanos(), (2 * 60 * 60 + 30 * 60) * 1_000_000_000);
836	}
837
838	#[test]
839	fn test_human_full_format() {
840		let d = parse_duration(Fragment::testing("1y2mo3d4h5m6s")).unwrap();
841		assert_eq!(d.get_months(), 14); // 12 + 2
842		assert_eq!(d.get_days(), 3);
843		let expected_nanos = (4 * 60 * 60 + 5 * 60 + 6) * 1_000_000_000;
844		assert_eq!(d.get_nanos(), expected_nanos);
845	}
846
847	#[test]
848	fn test_human_milliseconds() {
849		let d = parse_duration(Fragment::testing("500ms")).unwrap();
850		assert_eq!(d.get_nanos(), 500 * 1_000_000);
851	}
852
853	#[test]
854	fn test_human_microseconds() {
855		let d = parse_duration(Fragment::testing("100us")).unwrap();
856		assert_eq!(d.get_nanos(), 100 * 1_000);
857	}
858
859	#[test]
860	fn test_human_nanoseconds() {
861		let d = parse_duration(Fragment::testing("50ns")).unwrap();
862		assert_eq!(d.get_nanos(), 50);
863	}
864
865	#[test]
866	fn test_human_seconds_with_milliseconds() {
867		let d = parse_duration(Fragment::testing("1s500ms")).unwrap();
868		assert_eq!(d.get_nanos(), 1 * 1_000_000_000 + 500 * 1_000_000);
869	}
870
871	#[test]
872	fn test_human_zero() {
873		let d = parse_duration(Fragment::testing("0s")).unwrap();
874		assert_eq!(d.get_months(), 0);
875		assert_eq!(d.get_days(), 0);
876		assert_eq!(d.get_nanos(), 0);
877	}
878
879	#[test]
880	fn test_human_all_sub_second() {
881		let d = parse_duration(Fragment::testing("123ms456us789ns")).unwrap();
882		assert_eq!(d.get_nanos(), 123 * 1_000_000 + 456 * 1_000 + 789);
883	}
884}