1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub struct PdfDate {
9 pub year: u16,
10 pub month: u8,
11 pub day: u8,
12 pub hour: u8,
13 pub minute: u8,
14 pub second: u8,
15 pub ut: char,
17 pub ut_hour: u8,
19 pub ut_minutes: u8,
21}
22
23impl PdfDate {
24 pub fn parse(s: &str) -> Option<Self> {
31 let s = s.strip_prefix("D:").unwrap_or(s);
32 if s.len() < 4 {
33 return None;
34 }
35
36 let year: u16 = s.get(0..4)?.parse().ok()?;
37 let month: u8 = s.get(4..6).and_then(|v| v.parse().ok()).unwrap_or(1);
38 let day: u8 = s.get(6..8).and_then(|v| v.parse().ok()).unwrap_or(1);
39 let hour: u8 = s.get(8..10).and_then(|v| v.parse().ok()).unwrap_or(0);
40 let minute: u8 = s.get(10..12).and_then(|v| v.parse().ok()).unwrap_or(0);
41 let second: u8 = s.get(12..14).and_then(|v| v.parse().ok()).unwrap_or(0);
42
43 let rest = s.get(14..).unwrap_or("");
44 let (ut, ut_hour, ut_minutes) = if rest.is_empty() {
45 ('Z', 0, 0)
46 } else {
47 let first = rest.chars().next()?;
48 match first {
49 'Z' => ('Z', 0, 0),
50 '+' | '-' => {
51 let tz = &rest[1..];
52 let tz = tz.replace('\'', "");
53 let uh: u8 = tz.get(0..2).and_then(|v| v.parse().ok()).unwrap_or(0);
54 let um: u8 = tz.get(2..4).and_then(|v| v.parse().ok()).unwrap_or(0);
55 (first, uh, um)
56 }
57 _ => ('Z', 0, 0),
58 }
59 };
60
61 Some(PdfDate {
62 year,
63 month,
64 day,
65 hour,
66 minute,
67 second,
68 ut,
69 ut_hour,
70 ut_minutes,
71 })
72 }
73
74 pub fn to_pdf_string(&self) -> String {
76 if self.ut == 'Z' {
77 format!(
78 "D:{:04}{:02}{:02}{:02}{:02}{:02}Z",
79 self.year, self.month, self.day, self.hour, self.minute, self.second
80 )
81 } else {
82 format!(
83 "D:{:04}{:02}{:02}{:02}{:02}{:02}{}{:02}'{:02}'",
84 self.year,
85 self.month,
86 self.day,
87 self.hour,
88 self.minute,
89 self.second,
90 self.ut,
91 self.ut_hour,
92 self.ut_minutes
93 )
94 }
95 }
96}
97
98impl std::fmt::Display for PdfDate {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(f, "{}", self.to_pdf_string())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_parse_full() {
110 let d = PdfDate::parse("D:20231015120000+05'30'").unwrap();
111 assert_eq!(d.year, 2023);
112 assert_eq!(d.month, 10);
113 assert_eq!(d.day, 15);
114 assert_eq!(d.hour, 12);
115 assert_eq!(d.minute, 0);
116 assert_eq!(d.second, 0);
117 assert_eq!(d.ut, '+');
118 assert_eq!(d.ut_hour, 5);
119 assert_eq!(d.ut_minutes, 30);
120 }
121
122 #[test]
123 fn test_parse_minimal() {
124 let d = PdfDate::parse("D:2023").unwrap();
125 assert_eq!(d.year, 2023);
126 assert_eq!(d.month, 1);
127 assert_eq!(d.day, 1);
128 }
129
130 #[test]
131 fn test_parse_zulu() {
132 let d = PdfDate::parse("D:20231015120000Z").unwrap();
133 assert_eq!(d.ut, 'Z');
134 assert_eq!(d.ut_hour, 0);
135 }
136
137 #[test]
138 fn test_roundtrip() {
139 let original = "D:20231015120000+05'30'";
140 let d = PdfDate::parse(original).unwrap();
141 assert_eq!(d.to_pdf_string(), original);
142 }
143
144 #[test]
145 fn test_roundtrip_zulu() {
146 let original = "D:20231015120000Z";
147 let d = PdfDate::parse(original).unwrap();
148 assert_eq!(d.to_pdf_string(), original);
149 }
150}