ttf_view/types/longdatetime.rs
1use crate::util::DisplayBuffer;
2use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc};
3use std::fmt::Write;
4
5/// The [OpenType LongDateTime][spec] type.
6///
7/// `LongDateTime` is stored as a number of seconds since `1904-01-01T00:00:00Z`, and covers a
8/// riduculously large range of dates, extending to 278 billions of years before the formation of
9/// the universe (~13.8 billion BCE), and into the far-far future.
10///
11/// ```text
12/// chrono crate: -262_143-01-01 00:00:00 UTC ..= +262_142-12-31 23:59:59 UTC
13/// LongDateTime: -292_277_022_723-01-25 08:29:52 UTC ..= +292_277_026_530-12-04 15:30:07 UTC
14/// ```
15///
16/// Needless to say, `LongDateTime`'s formatting as civil time probably won't be accurate for really
17/// far away dates, since the Earth did not spin before the universe came into existence, and also
18/// because this implementation does not account for variable length of day, which will likely grow
19/// more than tenfold before an overflow occurs (if the Earth is still somehow not destroyed).
20///
21/// # Formatting
22///
23/// `LongDateTime`'s formatting impls mirror those of [`chrono::DateTime`].
24///
25/// [`Display`][std::fmt::Display] formats `LongDateTime` as a human-readable timestamp.
26///
27/// ```
28/// use ttf_view::types::LongDateTime;
29///
30/// // You can also use .to_string()
31/// let dt = LongDateTime::from_epoch_seconds(-1535200423);
32/// assert_eq!(format!("{}", dt), "1855-05-08 11:26:17 UTC");
33/// let dt = LongDateTime::from_epoch_seconds(3478852446);
34/// assert_eq!(format!("{}", dt), "2014-03-28 11:54:06 UTC");
35/// ```
36///
37/// [`Debug`][std::fmt::Debug] formats `LongDateTime` as an RFC 3339 and ISO 8601 timestamp.
38///
39/// ```
40/// use ttf_view::types::LongDateTime;
41///
42/// let dt = LongDateTime::from_epoch_seconds(-1535200423);
43/// assert_eq!(format!("{:?}", dt), "1855-05-08T11:26:17Z");
44/// let dt = LongDateTime::from_epoch_seconds(3478852446);
45/// assert_eq!(format!("{:?}", dt), "2014-03-28T11:54:06Z");
46/// ```
47///
48/// [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#data-types
49#[derive(Copy, Hash)]
50#[derive_const(Clone, PartialEq, Eq)]
51#[repr(transparent)]
52pub struct LongDateTime([u8; 8]);
53
54const EPOCH_NAIVE: NaiveDateTime =
55 NaiveDate::from_ymd_opt(1904, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
56
57impl LongDateTime {
58 /// The point of reference from which `LongDateTime` counts seconds.
59 ///
60 /// ```
61 /// # use ttf_view::types::LongDateTime;
62 /// assert_eq!(LongDateTime::EPOCH.to_string(), "1904-01-01 00:00:00 UTC");
63 /// ```
64 pub const EPOCH: DateTime<Utc> = EPOCH_NAIVE.and_utc();
65 /// The smallest representable [`LongDateTime`].
66 ///
67 /// # Examples
68 ///
69 /// ```
70 /// # use ttf_view::types::LongDateTime;
71 /// assert_eq!(LongDateTime::MIN.to_string(), "-292277022723-01-25 08:29:52 UTC");
72 /// ```
73 pub const MIN: Self = Self::from_epoch_seconds(i64::MIN);
74 /// The largest representable [`LongDateTime`].
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// # use ttf_view::types::LongDateTime;
80 /// assert_eq!(LongDateTime::MAX.to_string(), "+292277026530-12-04 15:30:07 UTC");
81 /// ```
82 pub const MAX: Self = Self::from_epoch_seconds(i64::MAX);
83
84 /// Creates a [`LongDateTime`] from [`chrono::DateTime<Utc>`]. Truncates sub-seconds.
85 ///
86 /// Always succeeds, since `chrono::DateTime`'s range is a subset of `LongDateTime`'s.
87 ///
88 /// # Examples
89 ///
90 /// ```
91 /// use chrono::{DateTime, Utc};
92 /// use ttf_view::types::LongDateTime;
93 ///
94 /// let dt: DateTime<Utc> = "2026-09-06 11:11:36.562 UTC".parse().unwrap();
95 /// assert_eq!(LongDateTime::new(dt).to_string(), "2026-09-06 11:11:36 UTC");
96 /// ```
97 pub const fn new(datetime: DateTime<Utc>) -> Self {
98 let delta = datetime.naive_utc().signed_duration_since(EPOCH_NAIVE);
99 Self::from_epoch_seconds(delta.num_seconds())
100 }
101 /// Returns a [`chrono::DateTime<Utc>`] representing this [`LongDateTime`] value.
102 ///
103 /// Returns `None` if it's out of `chrono::DateTime`'s range.
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use chrono::{DateTime, Utc};
109 /// use ttf_view::types::LongDateTime;
110 ///
111 /// let long = LongDateTime::from_epoch_seconds(0xFFFFFFFF);
112 /// assert_eq!(long.to_string(), "2040-02-06 06:28:15 UTC");
113 /// assert_eq!(long.datetime().unwrap().to_string(), "2040-02-06 06:28:15 UTC");
114 ///
115 /// // chrono::DateTime is limited to ±262000 years.
116 /// let long = LongDateTime::from_epoch_seconds(0xFFFFFFFFFFFFFF);
117 /// assert_eq!(long.to_string(), "+2283416158-11-23 12:52:15 UTC");
118 /// assert_eq!(long.datetime(), None);
119 /// ```
120 pub const fn datetime(&self) -> Option<DateTime<Utc>> {
121 let delta = TimeDelta::try_seconds(self.epoch_seconds())?;
122 Some(EPOCH_NAIVE.checked_add_signed(delta)?.and_utc())
123 }
124
125 /// Creates a [`LongDateTime`] from the number of seconds since [`EPOCH`][Self::EPOCH]
126 /// (`1904-01-01 00:00:00 UTC`).
127 ///
128 /// # Examples
129 ///
130 /// ```
131 /// use ttf_view::types::LongDateTime;
132 ///
133 /// assert_eq!(LongDateTime::from_epoch_seconds(-1535200423).to_string(), "1855-05-08 11:26:17 UTC");
134 /// assert_eq!(LongDateTime::from_epoch_seconds(3478852446).to_string(), "2014-03-28 11:54:06 UTC");
135 /// ```
136 pub const fn from_epoch_seconds(secs: i64) -> Self {
137 Self(i64::to_be_bytes(secs))
138 }
139 /// Returns the number of seconds from [`EPOCH`][Self::EPOCH] (`1904-01-01 00:00:00 UTC`)
140 /// to this [`LongDateTime`]'s value.
141 ///
142 /// # Examples
143 ///
144 /// ```
145 /// use chrono::{DateTime, Utc};
146 /// use ttf_view::types::LongDateTime;
147 ///
148 /// let dt: DateTime<Utc> = "1855-05-08 11:26:17 UTC".parse().unwrap();
149 /// assert_eq!(LongDateTime::new(dt).epoch_seconds(), -1535200423);
150 /// let dt: DateTime<Utc> = "2014-03-28 11:54:06 UTC".parse().unwrap();
151 /// assert_eq!(LongDateTime::new(dt).epoch_seconds(), 3478852446);
152 /// ```
153 pub const fn epoch_seconds(&self) -> i64 {
154 i64::from_be_bytes(self.0)
155 }
156
157 /// Creates a [`LongDateTime`] from big-endian bytes.
158 ///
159 /// # Examples
160 ///
161 /// ```
162 /// use ttf_view::types::LongDateTime;
163 ///
164 /// let raw = [0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x7E, 0xB3, 0x59];
165 /// assert_eq!(LongDateTime::from_be_bytes(raw).to_string(), "1855-05-08 11:26:17 UTC");
166 /// let raw = [0x00, 0x00, 0x00, 0x00, 0xCF, 0x5B, 0x13, 0x5E];
167 /// assert_eq!(LongDateTime::from_be_bytes(raw).to_string(), "2014-03-28 11:54:06 UTC");
168 /// ```
169 pub const fn from_be_bytes(bytes: [u8; 8]) -> Self {
170 Self(bytes)
171 }
172 /// Returns this [`LongDateTime`]'s big-endian bytes.
173 ///
174 /// # Examples
175 ///
176 /// ```
177 /// use chrono::{DateTime, Utc};
178 /// use ttf_view::types::LongDateTime;
179 ///
180 /// let dt: DateTime<Utc> = "1855-05-08 11:26:17 UTC".parse().unwrap();
181 /// let raw = [0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x7E, 0xB3, 0x59];
182 /// assert_eq!(LongDateTime::new(dt).to_be_bytes(), raw);
183 ///
184 /// let dt: DateTime<Utc> = "2014-03-28 11:54:06 UTC".parse().unwrap();
185 /// let raw = [0x00, 0x00, 0x00, 0x00, 0xCF, 0x5B, 0x13, 0x5E];
186 /// assert_eq!(LongDateTime::new(dt).to_be_bytes(), raw);
187 /// ```
188 pub const fn to_be_bytes(self) -> [u8; 8] {
189 self.0
190 }
191}
192
193// TODO: When [u8; 8]'s Default is constified, replace this impl with #[derive_const]
194#[allow(clippy::derivable_impls)]
195const impl Default for LongDateTime {
196 fn default() -> Self {
197 Self([0; 8])
198 }
199}
200// Note: PartialEq + Eq impls need to be explicit, because [u8; 8] is compared unsignedly.
201const impl PartialOrd for LongDateTime {
202 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
203 Some(self.cmp(other))
204 }
205}
206const impl Ord for LongDateTime {
207 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
208 self.epoch_seconds().cmp(&other.epoch_seconds())
209 }
210}
211
212const impl From<DateTime<Utc>> for LongDateTime {
213 fn from(value: DateTime<Utc>) -> Self {
214 Self::new(value)
215 }
216}
217const impl TryFrom<LongDateTime> for DateTime<Utc> {
218 type Error = ();
219 fn try_from(value: LongDateTime) -> Result<Self, Self::Error> {
220 value.datetime().ok_or(())
221 }
222}
223
224/// Formats [`LongDateTime`] as a human-readable timestamp. [See more above](#formatting)
225impl std::fmt::Display for LongDateTime {
226 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
227 fmt_longdatetime(*self, false, f)
228 }
229}
230/// Formats [`LongDateTime`] as an RFC 3339 and ISO 8601 timestamp. [See more above](#formatting)
231impl std::fmt::Debug for LongDateTime {
232 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
233 fmt_longdatetime(*self, true, f)
234 }
235}
236
237fn fmt_longdatetime(ldt: LongDateTime, iso: bool, f: &mut std::fmt::Formatter) -> std::fmt::Result {
238 let epoch_secs = ldt.epoch_seconds();
239
240 const SECS_PER_DAY: i64 = 24 * 60 * 60;
241 let epoch_days = epoch_secs.div_euclid(SECS_PER_DAY);
242 let epoch_secs = epoch_secs.rem_euclid(SECS_PER_DAY);
243
244 // Convert "days since 1904" to "days since 1970"
245 let (year, month, day) = ymd_from_days(epoch_days - 24107);
246
247 // Max length is 32: "-292277022723-01-25 08:29:52 UTC"
248 let mut buf = DisplayBuffer::<32>::new();
249
250 if year > 9999 {
251 // ISO 8601 requires the explicit sign for out-of-range years
252 buf.write_byte_unchecked(b'+');
253 } else if year < 0 {
254 // Write minus separately to avoid counting it towards min width in "{:04}"
255 buf.write_byte_unchecked(b'-');
256 }
257 let year = year.unsigned_abs();
258
259 // Write "{:04}" with year (fast path for 0..=9999)
260 if matches!(year, 0..=9999) {
261 buf.write_two_digits_unchecked((year / 100) as u8);
262 buf.write_two_digits_unchecked((year % 100) as u8);
263 } else {
264 write!(buf, "{:04}", year)?;
265 }
266
267 // Write "-{:02}-{:02}" with month and day
268 buf.write_byte_unchecked(b'-');
269 buf.write_two_digits_unchecked(month);
270 buf.write_byte_unchecked(b'-');
271 buf.write_two_digits_unchecked(day);
272
273 // In Debug use 'T' as separator, and in Display - ' '
274 buf.write_byte_unchecked(if iso { b'T' } else { b' ' });
275
276 // Write "{:02}:{:02}:{:02}" with hour, min, sec
277 buf.write_two_digits_unchecked((epoch_secs / 3600) as u8);
278 buf.write_byte_unchecked(b':');
279 buf.write_two_digits_unchecked(((epoch_secs % 3600) / 60) as u8);
280 buf.write_byte_unchecked(b':');
281 buf.write_two_digits_unchecked((epoch_secs % 60) as u8);
282
283 // In Debug use 'Z' for UTC, and in Display - " UTC"
284 if iso {
285 buf.write_byte_unchecked(b'Z');
286 } else {
287 buf.write_str_unchecked(" UTC");
288 }
289
290 f.write_str(buf.as_str())
291}
292
293fn ymd_from_days(unix_days: i64) -> (i64, u8, u8) {
294 // See https://howardhinnant.github.io/date_algorithms.html#civil_from_days
295 let z = unix_days + 719468;
296 let era = if z >= 0 { z } else { z - 146096 } / 146097;
297 let doe = (z - era * 146097) as u64;
298 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
299 let y = yoe as i64 + era * 400;
300 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
301 let mp = (5 * doy + 2) / 153;
302 let d = doy - (153 * mp + 2) / 5 + 1;
303 let m = if mp < 10 { mp + 3 } else { mp - 9 };
304 (y + (m <= 2) as i64, m as u8, d as u8)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn longdatetimes() {
313 let dates: [(u64, &'static str); _] = [
314 (0x8000000000000000, "-292277022723-01-25T08:29:52Z"),
315 (0xFFFFF00000000000, "-555571-10-24T14:19:44Z"),
316 (0xFFFFFF0000000000, "-32939-11-10T23:23:44Z"),
317 (0xFFFFFFF000000000, "-0274-05-13T16:27:44Z"),
318 (0xFFFFFFF200D00100, "-0001-01-01T00:00:00Z"),
319 (0xFFFFFFF202B13480, "0000-01-01T00:00:00Z"),
320 (0xFFFFFFF20493B980, "0001-01-01T00:00:00Z"),
321 (0x0000000000000000, "1904-01-01T00:00:00Z"),
322 (0x00000000E3D1B1DE, "2025-02-12T02:03:10Z"),
323 (0x00000000E6B686E6, "2026-08-28T00:29:26Z"),
324 (0x00000000FFFFFFFF, "2040-02-06T06:28:15Z"),
325 (0x000000FFFFFFFFFF, "+36746-02-19T00:36:15Z"),
326 (0x0000FFFFFFFFFFFF, "+8921490-12-06T10:44:15Z"),
327 (0x00FFFFFFFFFFFFFF, "+2283416158-11-23T12:52:15Z"),
328 (0x7FFFFFFFFFFFFFFF, "+292277026530-12-04T15:30:07Z"),
329 ];
330
331 for (stamp, iso_date) in dates {
332 let stamp = LongDateTime::from_epoch_seconds(stamp as i64);
333 assert_eq!(format!("{:?}", stamp), iso_date);
334
335 if let Some(chrono_date) = stamp.datetime() {
336 assert_eq!(format!("{:?}", chrono_date), iso_date);
337 assert_eq!(stamp, LongDateTime::new(chrono_date));
338 }
339 }
340 }
341}