Skip to main content

reifydb_value/value/temporal/parse/
duration.rs

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