pdfluent_lopdf/
datetime.rs1use super::Object;
2
3#[cfg(feature = "chrono")]
4mod chrono_impl {
5 use crate::{Object, datetime::convert_utc_offset};
6 use chrono::prelude::*;
7
8 impl From<DateTime<Local>> for Object {
9 fn from(date: DateTime<Local>) -> Self {
10 let mut timezone_str = date.format("D:%Y%m%d%H%M%S%:z'").to_string().into_bytes();
11 convert_utc_offset(&mut timezone_str);
12 Object::string_literal(timezone_str)
13 }
14 }
15
16 impl From<DateTime<Utc>> for Object {
17 fn from(date: DateTime<Utc>) -> Self {
18 Object::string_literal(date.format("D:%Y%m%d%H%M%SZ").to_string())
19 }
20 }
21
22 impl TryFrom<super::DateTime> for DateTime<Local> {
23 type Error = chrono::format::ParseError;
24
25 fn try_from(value: super::DateTime) -> Result<DateTime<Local>, Self::Error> {
26 let from_date = |date: NaiveDate| {
27 FixedOffset::east_opt(0)
28 .unwrap()
29 .from_utc_datetime(&date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()))
30 };
31
32 DateTime::parse_from_str(&value.0, "%Y%m%d%H%M%S%#z")
33 .or_else(|_| DateTime::parse_from_str(&value.0, "%Y%m%d%H%M%#z"))
34 .or_else(|_| NaiveDate::parse_from_str(&value.0, "%Y%m%d").map(from_date))
35 .map(|date| date.with_timezone(&Local))
36 }
37 }
38}
39
40#[cfg(feature = "jiff")]
41mod jiff_impl {
42 use crate::{Object, datetime::convert_utc_offset};
43 use jiff::{Timestamp, Zoned};
44
45 impl From<Zoned> for Object {
46 fn from(date: Zoned) -> Self {
47 let mut timezone_str = date.strftime("D:%Y%m%d%H%M%S%:z'").to_string().into_bytes();
48 convert_utc_offset(&mut timezone_str);
49 Object::string_literal(timezone_str)
50 }
51 }
52
53 impl From<Timestamp> for Object {
54 fn from(date: Timestamp) -> Self {
55 Object::string_literal(date.strftime("D:%Y%m%d%H%M%SZ").to_string())
56 }
57 }
58
59 impl TryFrom<super::DateTime> for Zoned {
60 type Error = jiff::Error;
61
62 fn try_from(value: super::DateTime) -> Result<Self, Self::Error> {
63 use jiff::civil::{Date, DateTime};
64
65 Zoned::strptime("%Y%m%d%H%M%S%#z", &value.0)
86 .or_else(|_| {
87 DateTime::strptime("%Y%m%d%H%M%SZ", &value.0).and_then(|dt| dt.in_tz("UTC"))
88 })
89 .or_else(|_| Zoned::strptime("%Y%m%d%H%M%#z", &value.0))
90 .or_else(|_| {
91 DateTime::strptime("%Y%m%d%H%MZ", &value.0).and_then(|dt| dt.in_tz("UTC"))
92 })
93 .or_else(|_| {
94 Date::strptime("%Y%m%d", &value.0).and_then(|dt| dt.at(0, 0, 0, 0).in_tz("GMT"))
95 })
96 }
97 }
98}
99
100#[cfg(feature = "time")]
101mod time_impl {
102 use crate::Object;
103 use time::{OffsetDateTime, Time, format_description::FormatItem};
104
105 impl From<Time> for Object {
106 fn from(date: Time) -> Self {
107 Object::string_literal(
109 format!(
110 "D:{}",
111 date.format(&FormatItem::Literal("%Y%m%d%H%M%SZ".as_bytes()))
112 .unwrap()
113 )
114 .into_bytes(),
115 )
116 }
117 }
118
119 impl From<OffsetDateTime> for Object {
120 fn from(date: OffsetDateTime) -> Self {
121 Object::string_literal({
122 let format = time::format_description::parse(
124 "D:[year][month][day][hour][minute][second][offset_hour sign:mandatory]'[offset_minute]'",
125 )
126 .unwrap();
127 date.format(&format).unwrap()
128 })
129 }
130 }
131
132 impl TryFrom<super::DateTime> for OffsetDateTime {
137 type Error = time::Error;
138
139 fn try_from(value: super::DateTime) -> Result<OffsetDateTime, Self::Error> {
140 let format = time::format_description::parse(
141 "[year][month][day][hour][minute][second][offset_hour sign:mandatory][offset_minute]",
142 )
143 .unwrap();
144
145 Ok(OffsetDateTime::parse(&value.0, &format)?)
146 }
147 }
148}
149
150#[allow(dead_code)]
152fn convert_utc_offset(bytes: &mut [u8]) {
153 let mut index = bytes.len();
154 while let Some(last) = bytes[..index].last_mut() {
155 if *last == b':' {
156 *last = b'\'';
157 break;
158 }
159 index -= 1;
160 }
161}
162
163#[derive(Clone, Debug)]
164pub struct DateTime(String);
165
166impl Object {
167 fn datetime_string(&self) -> Option<String> {
169 if let Object::String(bytes, _) = self {
170 String::from_utf8(
171 bytes
172 .iter()
173 .filter(|b| ![b'D', b':', b'\''].contains(b))
174 .cloned()
175 .collect(),
176 )
177 .ok()
178 } else {
179 None
180 }
181 }
182
183 pub fn as_datetime(&self) -> Option<DateTime> {
184 self.datetime_string().map(DateTime)
185 }
186}
187
188#[cfg(feature = "chrono")]
189#[test]
190fn parse_datetime_local() {
191 use chrono::prelude::*;
192
193 let time = Local::now().with_nanosecond(0).unwrap();
194 let text: Object = time.into();
195 let time2: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
196 assert_eq!(time2, Some(time));
197}
198
199#[cfg(feature = "chrono")]
200#[test]
201fn parse_datetime_utc() {
202 use chrono::prelude::*;
203
204 let time = Utc::now().with_nanosecond(0).unwrap();
205 let text: Object = time.into();
206 let time2: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
207 assert_eq!(time2, Some(time.with_timezone(&Local)));
208}
209
210#[cfg(feature = "jiff")]
211#[test]
212fn parse_zoned() {
213 use jiff::Zoned;
214
215 let time = Zoned::now().with().subsec_nanosecond(0).build().unwrap();
216 let text: Object = time.clone().into();
217 let time2: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
218 assert_eq!(time2, Some(time));
219}
220
221#[cfg(feature = "jiff")]
222#[test]
223fn parse_timestamp() {
224 use jiff::Zoned;
225
226 let time = Zoned::now().with().subsec_nanosecond(0).build().unwrap();
227 let text: Object = time.timestamp().into();
228 let time2: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
229 assert_eq!(time2, Some(time));
230}
231
232#[cfg(feature = "chrono")]
233#[test]
234fn parse_datetime_seconds_missing_chrono() {
235 use chrono::prelude::*;
236
237 let text = Object::string_literal("D:199812231952-08'00'");
239 let dt: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
240 assert!(dt.is_some());
241}
242
243#[cfg(feature = "chrono")]
244#[test]
245fn parse_datetime_time_missing_chrono() {
246 use chrono::prelude::*;
247
248 let text = Object::string_literal("D:20040229");
249 let dt: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
250 assert!(dt.is_some());
251}
252
253#[cfg(feature = "jiff")]
254#[test]
255fn parse_datetime_seconds_missing_jiff() {
256 use jiff::Zoned;
257
258 let text = Object::string_literal("D:199812231952-08'00'");
260 let dt: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
261 assert!(dt.is_some());
262}
263
264#[cfg(feature = "jiff")]
265#[test]
266fn parse_datetime_time_missing_jiff() {
267 use jiff::Zoned;
268
269 let text = Object::string_literal("D:20040229");
270 let dt: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
271 assert!(dt.is_some());
272}
273
274#[cfg(feature = "time")]
275#[test]
276fn parse_datetime() {
277 use time::OffsetDateTime;
278
279 let time = OffsetDateTime::now_utc();
280
281 let text: Object = time.into();
282 let time2: OffsetDateTime = text.as_datetime().unwrap().try_into().unwrap();
283
284 assert_eq!(time2.date(), time.date());
285
286 assert_eq!(time2.time().hour(), time.time().hour());
289 assert_eq!(time2.time().minute(), time.time().minute());
290 assert_eq!(time2.time().second(), time.time().second());
291}