amadeus_types/
time.rs

1//! Implement [`Record`] for [`Time`], [`Date`], and [`DateTime`].
2
3#![allow(clippy::trivially_copy_pass_by_ref)]
4
5use chrono::{
6	offset::{Offset, TimeZone}, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc
7};
8use serde::{Deserialize, Serialize};
9use std::{
10	cmp::Ordering, convert::TryInto, error::Error, fmt::{self, Display}, str::FromStr
11};
12
13use super::AmadeusOrd;
14
15const JULIAN_DAY_OF_EPOCH: i64 = 2_440_588;
16const GREGORIAN_DAY_OF_EPOCH: i64 = 719_163;
17
18const TODO: &str = "not implemented yet";
19
20#[derive(Clone, PartialEq, Eq, Debug)]
21pub struct ParseDateError;
22impl Display for ParseDateError {
23	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24		write!(f, "error parsing date")
25	}
26}
27impl Error for ParseDateError {}
28
29/// A timezone. It can have a varying offset, like `Europe/London`, or fixed like `GMT+1`.
30// Tz with a FixedOffset: https://github.com/chronotope/chrono-tz/issues/11
31#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
32pub struct Timezone {
33	inner: TimezoneInner,
34}
35#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
36enum TimezoneInner {
37	Variable(chrono_tz::Tz), // variable offsets
38	Fixed(i32), // fixed offsets // can wrap FixedOffset on https://github.com/chronotope/chrono/issues/309
39}
40impl Timezone {
41	pub const UTC: Self = Self {
42		inner: TimezoneInner::Fixed(0),
43	};
44	pub const GMT: Self = Self {
45		inner: TimezoneInner::Fixed(0),
46	};
47	pub const GMT_MINUS_1: Self = Self {
48		inner: TimezoneInner::Fixed(-60 * 60),
49	};
50	pub const GMT_MINUS_2: Self = Self {
51		inner: TimezoneInner::Fixed(-2 * 60 * 60),
52	};
53	pub const GMT_MINUS_3: Self = Self {
54		inner: TimezoneInner::Fixed(-3 * 60 * 60),
55	};
56	pub const GMT_MINUS_4: Self = Self {
57		inner: TimezoneInner::Fixed(-4 * 60 * 60),
58	};
59	pub const GMT_MINUS_5: Self = Self {
60		inner: TimezoneInner::Fixed(-5 * 60 * 60),
61	};
62	pub const GMT_MINUS_6: Self = Self {
63		inner: TimezoneInner::Fixed(-6 * 60 * 60),
64	};
65	pub const GMT_MINUS_7: Self = Self {
66		inner: TimezoneInner::Fixed(-7 * 60 * 60),
67	};
68	pub const GMT_MINUS_8: Self = Self {
69		inner: TimezoneInner::Fixed(-8 * 60 * 60),
70	};
71	pub const GMT_MINUS_9: Self = Self {
72		inner: TimezoneInner::Fixed(-9 * 60 * 60),
73	};
74	pub const GMT_MINUS_10: Self = Self {
75		inner: TimezoneInner::Fixed(-10 * 60 * 60),
76	};
77	pub const GMT_MINUS_11: Self = Self {
78		inner: TimezoneInner::Fixed(-11 * 60 * 60),
79	};
80	pub const GMT_MINUS_12: Self = Self {
81		inner: TimezoneInner::Fixed(-12 * 60 * 60),
82	};
83	pub const GMT_PLUS_1: Self = Self {
84		inner: TimezoneInner::Fixed(60 * 60),
85	};
86	pub const GMT_PLUS_2: Self = Self {
87		inner: TimezoneInner::Fixed(2 * 60 * 60),
88	};
89	pub const GMT_PLUS_3: Self = Self {
90		inner: TimezoneInner::Fixed(3 * 60 * 60),
91	};
92	pub const GMT_PLUS_4: Self = Self {
93		inner: TimezoneInner::Fixed(4 * 60 * 60),
94	};
95	pub const GMT_PLUS_5: Self = Self {
96		inner: TimezoneInner::Fixed(4 * 60 * 60),
97	};
98	pub const GMT_PLUS_6: Self = Self {
99		inner: TimezoneInner::Fixed(5 * 60 * 60),
100	};
101	pub const GMT_PLUS_7: Self = Self {
102		inner: TimezoneInner::Fixed(6 * 60 * 60),
103	};
104	pub const GMT_PLUS_8: Self = Self {
105		inner: TimezoneInner::Fixed(7 * 60 * 60),
106	};
107	pub const GMT_PLUS_9: Self = Self {
108		inner: TimezoneInner::Fixed(8 * 60 * 60),
109	};
110	pub const GMT_PLUS_10: Self = Self {
111		inner: TimezoneInner::Fixed(10 * 60 * 60),
112	};
113	pub const GMT_PLUS_11: Self = Self {
114		inner: TimezoneInner::Fixed(11 * 60 * 60),
115	};
116	pub const GMT_PLUS_12: Self = Self {
117		inner: TimezoneInner::Fixed(12 * 60 * 60),
118	};
119	pub const GMT_PLUS_13: Self = Self {
120		inner: TimezoneInner::Fixed(13 * 60 * 60),
121	};
122	pub const GMT_PLUS_14: Self = Self {
123		inner: TimezoneInner::Fixed(14 * 60 * 60),
124	};
125
126	/// Create a new Timezone from a name in the [IANA Database](https://www.iana.org/time-zones).
127	pub fn from_name(name: &str) -> Option<Self> {
128		use chrono_tz::*;
129		#[rustfmt::skip]
130		#[allow(non_upper_case_globals)] // https://github.com/rust-lang/rust/issues/25207
131		let inner = match name.parse().ok()? {
132			UTC | UCT | Universal | Zulu | GMT | GMT0 | GMTMinus0 | GMTPlus0 | Greenwich |
133			Etc::UTC | Etc::UCT | Etc::Universal | Etc::Zulu | Etc::GMT | Etc::GMT0 | Etc::GMTMinus0 | Etc::GMTPlus0 | Etc::Greenwich => {
134				TimezoneInner::Fixed(0)
135			}
136			Etc::GMTPlus1 => TimezoneInner::Fixed(-60 * 60),
137			Etc::GMTPlus2 => TimezoneInner::Fixed(-2 * 60 * 60),
138			Etc::GMTPlus3 => TimezoneInner::Fixed(-3 * 60 * 60),
139			Etc::GMTPlus4 => TimezoneInner::Fixed(-4 * 60 * 60),
140			Etc::GMTPlus5 => TimezoneInner::Fixed(-5 * 60 * 60),
141			Etc::GMTPlus6 => TimezoneInner::Fixed(-6 * 60 * 60),
142			Etc::GMTPlus7 => TimezoneInner::Fixed(-7 * 60 * 60),
143			Etc::GMTPlus8 => TimezoneInner::Fixed(-8 * 60 * 60),
144			Etc::GMTPlus9 => TimezoneInner::Fixed(-9 * 60 * 60),
145			Etc::GMTPlus10 => TimezoneInner::Fixed(-10 * 60 * 60),
146			Etc::GMTPlus11 => TimezoneInner::Fixed(-11 * 60 * 60),
147			Etc::GMTPlus12 => TimezoneInner::Fixed(-12 * 60 * 60),
148			Etc::GMTMinus1 => TimezoneInner::Fixed(60 * 60),
149			Etc::GMTMinus2 => TimezoneInner::Fixed(2 * 60 * 60),
150			Etc::GMTMinus3 => TimezoneInner::Fixed(3 * 60 * 60),
151			Etc::GMTMinus4 => TimezoneInner::Fixed(4 * 60 * 60),
152			Etc::GMTMinus5 => TimezoneInner::Fixed(5 * 60 * 60),
153			Etc::GMTMinus6 => TimezoneInner::Fixed(6 * 60 * 60),
154			Etc::GMTMinus7 => TimezoneInner::Fixed(7 * 60 * 60),
155			Etc::GMTMinus8 => TimezoneInner::Fixed(8 * 60 * 60),
156			Etc::GMTMinus9 => TimezoneInner::Fixed(9 * 60 * 60),
157			Etc::GMTMinus10 => TimezoneInner::Fixed(10 * 60 * 60),
158			Etc::GMTMinus11 => TimezoneInner::Fixed(11 * 60 * 60),
159			Etc::GMTMinus12 => TimezoneInner::Fixed(12 * 60 * 60),
160			Etc::GMTMinus13 => TimezoneInner::Fixed(13 * 60 * 60),
161			Etc::GMTMinus14 => TimezoneInner::Fixed(14 * 60 * 60),
162			tz => TimezoneInner::Variable(tz),
163		};
164		Some(Self { inner })
165	}
166	/// Get the name of the timezone as in the [IANA Database](https://www.iana.org/time-zones). It might differ from (although still be equivalent to) the name given to `from_name`.
167	pub fn as_name(&self) -> Option<&'static str> {
168		use chrono_tz::*;
169		match self.inner {
170			TimezoneInner::Variable(tz) => Some(tz),
171			TimezoneInner::Fixed(offset) => match offset {
172				0 => Some(Etc::GMT),
173				-3600 => Some(Etc::GMTPlus1),
174				-7200 => Some(Etc::GMTPlus2),
175				-10800 => Some(Etc::GMTPlus3),
176				-14400 => Some(Etc::GMTPlus4),
177				-18000 => Some(Etc::GMTPlus5),
178				-21600 => Some(Etc::GMTPlus6),
179				-25200 => Some(Etc::GMTPlus7),
180				-28800 => Some(Etc::GMTPlus8),
181				-32400 => Some(Etc::GMTPlus9),
182				-36000 => Some(Etc::GMTPlus10),
183				-39600 => Some(Etc::GMTPlus11),
184				-43200 => Some(Etc::GMTPlus12),
185				3600 => Some(Etc::GMTMinus1),
186				7200 => Some(Etc::GMTMinus2),
187				10800 => Some(Etc::GMTMinus3),
188				14400 => Some(Etc::GMTMinus4),
189				18000 => Some(Etc::GMTMinus5),
190				21600 => Some(Etc::GMTMinus6),
191				25200 => Some(Etc::GMTMinus7),
192				28800 => Some(Etc::GMTMinus8),
193				32400 => Some(Etc::GMTMinus9),
194				36000 => Some(Etc::GMTMinus10),
195				39600 => Some(Etc::GMTMinus11),
196				43200 => Some(Etc::GMTMinus12),
197				46800 => Some(Etc::GMTMinus13),
198				50400 => Some(Etc::GMTMinus14),
199				_ => None,
200			},
201		}
202		.map(Tz::name)
203	}
204	/// Makes a new Timezone for the Eastern Hemisphere with given timezone difference. The negative seconds means the Western Hemisphere.
205	pub fn from_offset(seconds: i32) -> Option<Self> {
206		FixedOffset::east_opt(seconds).map(|offset| Self {
207			inner: TimezoneInner::Fixed(offset.local_minus_utc()),
208		})
209	}
210	/// Returns the number of seconds to add to convert from UTC to the local time.
211	pub fn as_offset(&self) -> Option<i32> {
212		match self.inner {
213			TimezoneInner::Variable(_tz) => None,
214			TimezoneInner::Fixed(seconds) => Some(seconds),
215		}
216	}
217	/// Returns the number of seconds to add to convert from UTC to the local time.
218	pub fn as_offset_at(&self, utc_date_time: &DateTime) -> i32 {
219		assert_eq!(utc_date_time.timezone, Self::UTC);
220		match self.inner {
221			TimezoneInner::Variable(tz) => tz
222				.offset_from_utc_datetime(&utc_date_time.date_time.as_chrono().expect(TODO))
223				.fix()
224				.local_minus_utc(),
225			TimezoneInner::Fixed(seconds) => seconds,
226		}
227	}
228	#[doc(hidden)]
229	pub fn from_chrono<Tz>(_timezone: &Tz, offset: &Tz::Offset) -> Self
230	where
231		Tz: TimeZone,
232	{
233		Self::from_offset(offset.fix().local_minus_utc()).unwrap() // TODO: this loses variable timezone
234	}
235	#[doc(hidden)]
236	pub fn as_chrono(&self) -> ChronoTimezone {
237		ChronoTimezone(*self)
238	}
239}
240impl PartialOrd for Timezone {
241	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
242		Some(Ord::cmp(self, other))
243	}
244}
245impl Ord for Timezone {
246	fn cmp(&self, other: &Self) -> Ordering {
247		match (self.inner, other.inner) {
248			(TimezoneInner::Variable(a), TimezoneInner::Variable(b)) => (a as u32).cmp(&(b as u32)),
249			(TimezoneInner::Fixed(a), TimezoneInner::Fixed(b)) => a.cmp(&b),
250			(TimezoneInner::Variable(_), _) => Ordering::Less,
251			(TimezoneInner::Fixed(_), _) => Ordering::Greater,
252		}
253	}
254}
255impl AmadeusOrd for Timezone {
256	fn amadeus_cmp(&self, other: &Self) -> Ordering {
257		Ord::cmp(self, other)
258	}
259}
260impl Display for Timezone {
261	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
262		self.as_chrono().fmt(f)
263	}
264}
265impl FromStr for Timezone {
266	type Err = ParseDateError;
267
268	fn from_str(_s: &str) -> Result<Self, Self::Err> {
269		unimplemented!()
270	}
271}
272
273#[doc(hidden)]
274#[derive(Clone, Debug)]
275pub struct ChronoTimezone(Timezone);
276#[doc(hidden)]
277#[derive(Clone, Debug)]
278pub struct ChronoTimezoneOffset(Timezone, FixedOffset);
279impl TimeZone for ChronoTimezone {
280	type Offset = ChronoTimezoneOffset;
281
282	fn from_offset(offset: &Self::Offset) -> Self {
283		ChronoTimezone(offset.0)
284	}
285	fn offset_from_local_date(&self, _local: &NaiveDate) -> chrono::LocalResult<Self::Offset> {
286		unimplemented!()
287	}
288	fn offset_from_local_datetime(
289		&self, _local: &NaiveDateTime,
290	) -> chrono::LocalResult<Self::Offset> {
291		unimplemented!()
292	}
293	fn offset_from_utc_date(&self, _utc: &NaiveDate) -> Self::Offset {
294		unimplemented!()
295	}
296	fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
297		ChronoTimezoneOffset(
298			self.0,
299			FixedOffset::east(self.0.as_offset_at(&DateTime::from_chrono(
300				&chrono::DateTime::<Utc>::from_utc(*utc, Utc),
301			))),
302		)
303	}
304}
305impl Offset for ChronoTimezoneOffset {
306	fn fix(&self) -> FixedOffset {
307		self.1
308	}
309}
310impl Display for ChronoTimezone {
311	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
312		match self.0.inner {
313			TimezoneInner::Variable(tz) => f.write_str(tz.name()),
314			TimezoneInner::Fixed(offset) => Display::fmt(&FixedOffset::east(offset), f),
315		}
316	}
317}
318impl Display for ChronoTimezoneOffset {
319	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
320		Display::fmt(&ChronoTimezone(self.0), f)
321	}
322}
323
324#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
325pub struct Date {
326	date: DateWithoutTimezone, // UTC
327	timezone: Timezone,
328}
329impl Date {
330	pub fn new(year: i64, month: u8, day: u8, timezone: Timezone) -> Option<Self> {
331		DateWithoutTimezone::new(year, month, day).map(|date| Self { date, timezone })
332	}
333	pub fn from_ordinal(year: i64, day: u16, timezone: Timezone) -> Option<Self> {
334		DateWithoutTimezone::from_ordinal(year, day).map(|date| Self { date, timezone })
335	}
336	pub fn year(&self) -> i64 {
337		self.date.year()
338	}
339	pub fn month(&self) -> u8 {
340		self.date.month()
341	}
342	pub fn day(&self) -> u8 {
343		self.date.day()
344	}
345	pub fn ordinal(&self) -> u16 {
346		self.date.ordinal()
347	}
348	pub fn without_timezone(&self) -> DateWithoutTimezone {
349		self.date
350	}
351	pub fn timezone(&self) -> Timezone {
352		self.timezone
353	}
354	/// Create a DateWithoutTimezone from the number of days since the Unix epoch
355	pub fn from_days(days: i64, timezone: Timezone) -> Option<Self> {
356		DateWithoutTimezone::from_days(days).map(|date| Self { date, timezone })
357	}
358	/// Get the number of days since the Unix epoch
359	pub fn as_days(&self) -> i64 {
360		self.date.as_days()
361	}
362	#[doc(hidden)]
363	pub fn from_chrono<Tz>(date: &chrono::Date<Tz>) -> Self
364	where
365		Tz: TimeZone,
366	{
367		Self::new(
368			date.year().into(),
369			date.month().try_into().unwrap(),
370			date.day().try_into().unwrap(),
371			Timezone::from_chrono(&date.timezone(), date.offset()),
372		)
373		.unwrap()
374	}
375	#[doc(hidden)]
376	pub fn as_chrono(&self) -> Option<chrono::Date<ChronoTimezone>> {
377		Some(
378			Utc.ymd(
379				self.year().try_into().ok()?,
380				self.month().into(),
381				self.day().into(),
382			)
383			.with_timezone(&ChronoTimezone(self.timezone)),
384		)
385	}
386}
387impl AmadeusOrd for Date {
388	fn amadeus_cmp(&self, other: &Self) -> Ordering {
389		Ord::cmp(self, other)
390	}
391}
392/// Corresponds to RFC 3339 and ISO 8601 string `%Y-%m-%d%:z`
393impl Display for Date {
394	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
395		write!(
396			f,
397			"{:04}-{:02}-{:02} {}",
398			self.year(),
399			self.month(),
400			self.day(),
401			self.timezone()
402		)
403	}
404}
405impl FromStr for Date {
406	type Err = ParseDateError;
407
408	fn from_str(s: &str) -> Result<Self, Self::Err> {
409		chrono::DateTime::parse_from_str(s, "%Y-%m-%d%:z")
410			.map(|date| Self::from_chrono(&date.date()))
411			.map_err(|_| ParseDateError)
412	}
413}
414
415#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
416pub struct Time {
417	time: TimeWithoutTimezone, // UTC
418	timezone: Timezone,
419}
420impl Time {
421	/// Create a TimeWithoutTimezone from hour, minute, second and nanosecond.
422	///
423	/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
424	///
425	/// Returns None on invalid hour, minute, second and/or nanosecond.
426	pub fn new(
427		hour: u8, minute: u8, second: u8, nanosecond: u32, timezone: Timezone,
428	) -> Option<Self> {
429		TimeWithoutTimezone::new(hour, minute, second, nanosecond)
430			.map(|time| Self { time, timezone })
431	}
432	/// Create a TimeWithoutTimezone from the number of seconds since midnight and nanosecond.
433	///
434	/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
435	///
436	/// Returns None on invalid number of seconds and/or nanosecond.
437	pub fn from_seconds(seconds: u32, nanosecond: u32, timezone: Timezone) -> Option<Self> {
438		TimeWithoutTimezone::from_seconds(seconds, nanosecond).map(|time| Self { time, timezone })
439	}
440	pub fn hour(&self) -> u8 {
441		self.time.hour()
442	}
443	pub fn minute(&self) -> u8 {
444		self.time.minute()
445	}
446	pub fn second(&self) -> u8 {
447		self.time.second()
448	}
449	pub fn nanosecond(&self) -> u32 {
450		self.time.nanosecond()
451	}
452	pub fn without_timezone(&self) -> TimeWithoutTimezone {
453		self.time
454	}
455	pub fn timezone(&self) -> Timezone {
456		self.timezone
457	}
458	pub fn truncate_minutes(&self, minutes: u8) -> Self {
459		Self {
460			time: self.time.truncate_minutes(minutes),
461			timezone: self.timezone,
462		}
463	}
464}
465impl AmadeusOrd for Time {
466	fn amadeus_cmp(&self, other: &Self) -> Ordering {
467		Ord::cmp(self, other)
468	}
469}
470/// Corresponds to RFC 3339 and ISO 8601 string `%H:%M:%S%.9f%:z`
471impl Display for Time {
472	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
473		write!(
474			f,
475			"{}{}",
476			self.time.as_chrono().expect(TODO).format("%H:%M:%S%.9f"),
477			ChronoTimezone(self.timezone)
478		)
479	}
480}
481impl FromStr for Time {
482	type Err = ParseDateError;
483
484	fn from_str(_s: &str) -> Result<Self, Self::Err> {
485		unimplemented!()
486	}
487}
488
489#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
490pub struct DateTime {
491	date_time: DateTimeWithoutTimezone, // UTC
492	timezone: Timezone,
493}
494impl DateTime {
495	#[allow(clippy::too_many_arguments)]
496	pub fn new(
497		year: i64, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32,
498		timezone: Timezone,
499	) -> Option<Self> {
500		DateTimeWithoutTimezone::new(year, month, day, hour, minute, second, nanosecond).map(
501			|date_time| Self {
502				date_time,
503				timezone,
504			},
505		)
506	}
507	/// Create a DateTimeWithoutTimezone from a [`DateWithoutTimezone`] and [`TimeWithoutTimezone`].
508	pub fn from_date_time(date: Date, time: Time) -> Option<Self> {
509		let timezone = date.timezone();
510		if timezone != time.timezone() {
511			return None;
512		}
513		let date_time = DateTimeWithoutTimezone::from_date_time(
514			date.without_timezone(),
515			time.without_timezone(),
516		)?;
517		Some(Self {
518			date_time,
519			timezone,
520		})
521	}
522	pub fn date(&self) -> DateWithoutTimezone {
523		self.date_time.date()
524	}
525	pub fn time(&self) -> TimeWithoutTimezone {
526		self.date_time.time()
527	}
528	pub fn year(&self) -> i64 {
529		self.date_time.year()
530	}
531	pub fn month(&self) -> u8 {
532		self.date_time.month()
533	}
534	pub fn day(&self) -> u8 {
535		self.date_time.day()
536	}
537	pub fn hour(&self) -> u8 {
538		self.date_time.hour()
539	}
540	pub fn minute(&self) -> u8 {
541		self.date_time.minute()
542	}
543	pub fn second(&self) -> u8 {
544		self.date_time.second()
545	}
546	pub fn nanosecond(&self) -> u32 {
547		self.date_time.nanosecond()
548	}
549	#[doc(hidden)]
550	pub fn from_chrono<Tz>(date_time: &chrono::DateTime<Tz>) -> Self
551	where
552		Tz: TimeZone,
553	{
554		Self::new(
555			date_time.year().into(),
556			date_time.month().try_into().unwrap(),
557			date_time.day().try_into().unwrap(),
558			date_time.hour().try_into().unwrap(),
559			date_time.minute().try_into().unwrap(),
560			date_time.second().try_into().unwrap(),
561			date_time.nanosecond(),
562			Timezone::from_chrono(&date_time.timezone(), date_time.offset()),
563		)
564		.unwrap()
565	}
566	#[doc(hidden)]
567	pub fn as_chrono(&self) -> Option<chrono::DateTime<ChronoTimezone>> {
568		Some(
569			chrono::DateTime::<Utc>::from_utc(self.date_time.as_chrono()?, Utc)
570				.with_timezone(&ChronoTimezone(self.timezone)),
571		)
572	}
573	pub fn truncate_minutes(&self, minutes: u8) -> Self {
574		Self {
575			date_time: self.date_time.truncate_minutes(minutes),
576			timezone: self.timezone,
577		}
578	}
579}
580impl AmadeusOrd for DateTime {
581	fn amadeus_cmp(&self, other: &Self) -> Ordering {
582		Ord::cmp(self, other)
583	}
584}
585/// Corresponds to RFC 3339 and ISO 8601 string `%Y-%m-%dT%H:%M:%S%.9f%:z`
586impl Display for DateTime {
587	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
588		write!(
589			f,
590			"{}",
591			self.as_chrono()
592				.expect(TODO)
593				.format("%Y-%m-%d %H:%M:%S%.9f %:z")
594		)
595	}
596}
597impl FromStr for DateTime {
598	type Err = ParseDateError;
599
600	fn from_str(s: &str) -> Result<Self, Self::Err> {
601		chrono::DateTime::<FixedOffset>::from_str(s)
602			.map(|date| Self::from_chrono(&date))
603			.map_err(|_| ParseDateError)
604	}
605}
606
607// https://github.com/chronotope/chrono/issues/52
608#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
609pub struct Duration {
610	months: i64,
611	days: i64,
612	nanos: i64,
613}
614impl AmadeusOrd for Duration {
615	fn amadeus_cmp(&self, other: &Self) -> Ordering {
616		Ord::cmp(self, other)
617	}
618}
619
620// Parquet's [Date logical type](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#date) is i32 days from Unix epoch
621// Postgres https://www.postgresql.org/docs/11/datatype-datetime.html is 4713 BC to 5874897 AD
622// MySQL https://dev.mysql.com/doc/refman/8.0/en/datetime.html is 1000-01-01 to 9999-12-31
623// Chrono https://docs.rs/chrono/0.4.6/chrono/naive/struct.NaiveDate.html Jan 1, 262145 BCE to Dec 31, 262143 CE
624// TODO: i33
625#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
626pub struct DateWithoutTimezone(i64);
627impl DateWithoutTimezone {
628	pub fn new(year: i64, month: u8, day: u8) -> Option<Self> {
629		NaiveDate::from_ymd_opt(
630			year.try_into().ok()?,
631			month.try_into().ok()?,
632			day.try_into().ok()?,
633		)
634		.as_ref()
635		.map(Self::from_chrono)
636	}
637	pub fn from_ordinal(year: i64, day: u16) -> Option<Self> {
638		NaiveDate::from_yo_opt(year.try_into().ok()?, day.try_into().ok()?)
639			.as_ref()
640			.map(Self::from_chrono)
641	}
642	pub fn year(&self) -> i64 {
643		i64::from(self.as_chrono().expect(TODO).year())
644	}
645	pub fn month(&self) -> u8 {
646		self.as_chrono().expect(TODO).month().try_into().unwrap()
647	}
648	pub fn day(&self) -> u8 {
649		self.as_chrono().expect(TODO).day().try_into().unwrap()
650	}
651	pub fn ordinal(&self) -> u16 {
652		self.as_chrono().expect(TODO).ordinal().try_into().unwrap()
653	}
654	pub fn with_timezone(self, timezone: Timezone) -> Date {
655		Date {
656			date: self,
657			timezone,
658		}
659	}
660	/// Create a DateWithoutTimezone from the number of days since the Unix epoch
661	pub fn from_days(days: i64) -> Option<Self> {
662		if JULIAN_DAY_OF_EPOCH + i64::from(i32::min_value()) <= days
663			&& days <= i64::from(i32::max_value())
664		{
665			Some(Self(days))
666		} else {
667			None
668		}
669	}
670	/// Get the number of days since the Unix epoch
671	pub fn as_days(&self) -> i64 {
672		self.0
673	}
674	#[doc(hidden)]
675	pub fn from_chrono(date: &NaiveDate) -> Self {
676		Self::from_days(i64::from(date.num_days_from_ce()) - GREGORIAN_DAY_OF_EPOCH).unwrap()
677	}
678	#[doc(hidden)]
679	pub fn as_chrono(&self) -> Option<NaiveDate> {
680		NaiveDate::from_num_days_from_ce_opt((self.0 + GREGORIAN_DAY_OF_EPOCH).try_into().ok()?)
681	}
682}
683impl AmadeusOrd for DateWithoutTimezone {
684	fn amadeus_cmp(&self, other: &Self) -> Ordering {
685		Ord::cmp(self, other)
686	}
687}
688impl Display for DateWithoutTimezone {
689	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
690		self.as_chrono().expect(TODO).fmt(f)
691	}
692}
693impl FromStr for DateWithoutTimezone {
694	type Err = ParseDateError;
695
696	fn from_str(s: &str) -> Result<Self, Self::Err> {
697		NaiveDate::from_str(s)
698			.map(|date| Self::from_chrono(&date))
699			.map_err(|_| ParseDateError)
700	}
701}
702
703// Parquet [Time logical type](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#time) number of microseconds since midnight
704// Postgres https://www.postgresql.org/docs/11/datatype-datetime.html 00:00:00 to 24:00:00, no :60
705// MySQL https://dev.mysql.com/doc/refman/8.0/en/time.html https://dev.mysql.com/doc/refman/5.7/en/time-zone-leap-seconds.html -838:59:59 to 838:59:59, no :60
706#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
707pub struct TimeWithoutTimezone(NaiveTime);
708impl TimeWithoutTimezone {
709	/// Create a TimeWithoutTimezone from hour, minute, second and nanosecond.
710	///
711	/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
712	///
713	/// Returns None on invalid hour, minute, second and/or nanosecond.
714	pub fn new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> Option<Self> {
715		NaiveTime::from_hms_nano_opt(hour.into(), minute.into(), second.into(), nanosecond)
716			.map(Self)
717	}
718	/// Create a TimeWithoutTimezone from the number of seconds since midnight and nanosecond.
719	///
720	/// The nanosecond part can exceed 1,000,000,000 in order to represent the leap second.
721	///
722	/// Returns None on invalid number of seconds and/or nanosecond.
723	pub fn from_seconds(seconds: u32, nanosecond: u32) -> Option<Self> {
724		NaiveTime::from_num_seconds_from_midnight_opt(seconds, nanosecond).map(Self)
725	}
726	pub fn hour(&self) -> u8 {
727		self.0.hour().try_into().unwrap()
728	}
729	pub fn minute(&self) -> u8 {
730		self.0.minute().try_into().unwrap()
731	}
732	pub fn second(&self) -> u8 {
733		self.0.second().try_into().unwrap()
734	}
735	pub fn nanosecond(&self) -> u32 {
736		self.0.nanosecond()
737	}
738	pub fn with_timezone(self, timezone: Timezone) -> Time {
739		Time {
740			time: self,
741			timezone,
742		}
743	}
744	#[doc(hidden)]
745	pub fn from_chrono(time: &NaiveTime) -> Self {
746		Self(*time)
747	}
748	#[doc(hidden)]
749	pub fn as_chrono(&self) -> Option<NaiveTime> {
750		Some(self.0)
751	}
752	pub fn truncate_minutes(&self, minutes: u8) -> Self {
753		assert!(
754			minutes != 0 && 60 % minutes == 0,
755			"minutes must be a divisor of 60"
756		);
757		Self::new(self.hour(), self.minute() / minutes * minutes, 0, 0).unwrap()
758	}
759	// /// Create a TimeWithoutTimezone from the number of milliseconds since midnight
760	// pub fn from_millis(millis: u32) -> Option<Self> {
761	// 	if millis < u32::try_from(SECONDS_PER_DAY * MILLIS_PER_SECOND).unwrap() {
762	// 		Some(Self(
763	// 			u64::from(millis)
764	// 				* u64::try_from(MICROS_PER_MILLI).unwrap()
765	// 				* u64::try_from(NANOS_PER_MICRO).unwrap(),
766	// 		))
767	// 	} else {
768	// 		None
769	// 	}
770	// }
771	// /// Create a TimeWithoutTimezone from the number of microseconds since midnight
772	// pub fn from_micros(micros: u64) -> Option<Self> {
773	// 	if micros < u64::try_from(SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI).unwrap() {
774	// 		Some(Self(micros * u64::try_from(NANOS_PER_MICRO).unwrap()))
775	// 	} else {
776	// 		None
777	// 	}
778	// }
779	// /// Create a TimeWithoutTimezone from the number of nanoseconds since midnight
780	// pub fn from_nanos(nanos: u64) -> Option<Self> {
781	// 	if nanos
782	// 		< u64::try_from(
783	// 			SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI * NANOS_PER_MICRO,
784	// 		)
785	// 		.unwrap()
786	// 	{
787	// 		Some(Self(nanos))
788	// 	} else {
789	// 		None
790	// 	}
791	// }
792	// /// Get the number of milliseconds since midnight
793	// pub fn as_millis(&self) -> u32 {
794	// 	(self.0
795	// 		/ u64::try_from(NANOS_PER_MICRO).unwrap()
796	// 		/ u64::try_from(MICROS_PER_MILLI).unwrap())
797	// 	.try_into()
798	// 	.unwrap()
799	// }
800	// /// Get the number of microseconds since midnight
801	// pub fn as_micros(&self) -> u64 {
802	// 	self.0 / u64::try_from(NANOS_PER_MICRO).unwrap()
803	// }
804	// /// Get the number of microseconds since midnight
805	// pub fn as_nanos(&self) -> u64 {
806	// 	self.0
807	// }
808}
809impl AmadeusOrd for TimeWithoutTimezone {
810	fn amadeus_cmp(&self, other: &Self) -> Ordering {
811		Ord::cmp(self, other)
812	}
813}
814impl Display for TimeWithoutTimezone {
815	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
816		self.as_chrono().expect(TODO).fmt(f)
817	}
818}
819impl FromStr for TimeWithoutTimezone {
820	type Err = ParseDateError;
821
822	fn from_str(s: &str) -> Result<Self, Self::Err> {
823		NaiveTime::from_str(s)
824			.map(|date| Self::from_chrono(&date))
825			.map_err(|_| ParseDateError)
826	}
827}
828
829// [`DateTimeWithoutTimezone`] corresponds to the [DateTimeWithoutTimezone logical type](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#timestamp).
830#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
831pub struct DateTimeWithoutTimezone {
832	date: DateWithoutTimezone,
833	time: TimeWithoutTimezone,
834}
835impl DateTimeWithoutTimezone {
836	pub fn new(
837		year: i64, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32,
838	) -> Option<Self> {
839		let date = DateWithoutTimezone::new(year, month, day)?;
840		let time = TimeWithoutTimezone::new(hour, minute, second, nanosecond)?;
841		Some(Self { date, time })
842	}
843	/// Create a DateTimeWithoutTimezone from a [`DateWithoutTimezone`] and [`TimeWithoutTimezone`].
844	pub fn from_date_time(date: DateWithoutTimezone, time: TimeWithoutTimezone) -> Option<Self> {
845		Some(Self { date, time })
846	}
847	pub fn date(&self) -> DateWithoutTimezone {
848		self.date
849	}
850	pub fn time(&self) -> TimeWithoutTimezone {
851		self.time
852	}
853	pub fn year(&self) -> i64 {
854		self.date.year()
855	}
856	pub fn month(&self) -> u8 {
857		self.date.month()
858	}
859	pub fn day(&self) -> u8 {
860		self.date.day()
861	}
862	pub fn hour(&self) -> u8 {
863		self.time.hour()
864	}
865	pub fn minute(&self) -> u8 {
866		self.time.minute()
867	}
868	pub fn second(&self) -> u8 {
869		self.time.second()
870	}
871	pub fn nanosecond(&self) -> u32 {
872		self.time.nanosecond()
873	}
874	pub fn with_timezone(self, timezone: Timezone) -> DateTime {
875		DateTime {
876			date_time: self,
877			timezone,
878		}
879	}
880	#[doc(hidden)]
881	pub fn from_chrono(date_time: &NaiveDateTime) -> Self {
882		Self::new(
883			date_time.year().into(),
884			date_time.month().try_into().unwrap(),
885			date_time.day().try_into().unwrap(),
886			date_time.hour().try_into().unwrap(),
887			date_time.minute().try_into().unwrap(),
888			date_time.second().try_into().unwrap(),
889			date_time.nanosecond(),
890		)
891		.unwrap()
892	}
893	#[doc(hidden)]
894	pub fn as_chrono(&self) -> Option<NaiveDateTime> {
895		Some(self.date.as_chrono()?.and_time(self.time.as_chrono()?))
896	}
897	pub fn truncate_minutes(&self, minutes: u8) -> Self {
898		Self {
899			date: self.date,
900			time: self.time.truncate_minutes(minutes),
901		}
902	}
903	// /// Create a DateTimeWithoutTimezone from the number of milliseconds since the Unix epoch
904	// pub fn from_millis(millis: i64) -> Self {
905	// 	let mut days = millis / (SECONDS_PER_DAY * MILLIS_PER_SECOND);
906	// 	let mut millis = millis % (SECONDS_PER_DAY * MILLIS_PER_SECOND);
907	// 	if millis < 0 {
908	// 		days -= 1;
909	// 		millis += SECONDS_PER_DAY * MILLIS_PER_SECOND;
910	// 	}
911	// 	Self::from_date_time(
912	// 		DateWithoutTimezone::from_days(days).unwrap(),
913	// 		TimeWithoutTimezone::from_millis(millis.try_into().unwrap()).unwrap(),
914	// 	)
915	// 	.unwrap()
916	// }
917	// /// Create a DateTimeWithoutTimezone from the number of microseconds since the Unix epoch
918	// pub fn from_micros(micros: i64) -> Self {
919	// 	let mut days = micros / (SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI);
920	// 	let mut micros = micros % (SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI);
921	// 	if micros < 0 {
922	// 		days -= 1;
923	// 		micros += SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI;
924	// 	}
925	// 	Self::from_date_time(
926	// 		DateWithoutTimezone::from_days(days).unwrap(),
927	// 		TimeWithoutTimezone::from_micros(micros.try_into().unwrap()).unwrap(),
928	// 	)
929	// 	.unwrap()
930	// }
931	// /// Create a DateTimeWithoutTimezone from the number of nanoseconds since the Unix epoch
932	// pub fn from_nanos(nanos: i64) -> Self {
933	// 	let mut days =
934	// 		nanos / (SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI * NANOS_PER_MICRO);
935	// 	let mut nanos =
936	// 		nanos % (SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI * NANOS_PER_MICRO);
937	// 	if nanos < 0 {
938	// 		days -= 1;
939	// 		nanos += SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI * NANOS_PER_MICRO;
940	// 	}
941	// 	Self::from_date_time(
942	// 		DateWithoutTimezone::from_days(days).unwrap(),
943	// 		TimeWithoutTimezone::from_nanos(nanos.try_into().unwrap()).unwrap(),
944	// 	)
945	// 	.unwrap()
946	// }
947	// /// Get the [`DateWithoutTimezone`] and [`TimeWithoutTimezone`] from this DateTimeWithoutTimezone.
948	// pub fn as_date_time(&self) -> (DateWithoutTimezone, TimeWithoutTimezone) {
949	// 	(self.date, self.time)
950	// }
951	// /// Get the number of milliseconds since the Unix epoch
952	// pub fn as_millis(&self) -> Option<i64> {
953	// 	Some(
954	// 		self.date
955	// 			.as_days()
956	// 			.checked_mul(SECONDS_PER_DAY * MILLIS_PER_SECOND)?
957	// 			.checked_add(i64::try_from(self.time.as_millis()).unwrap())?,
958	// 	)
959	// }
960	// /// Get the number of microseconds since the Unix epoch
961	// pub fn as_micros(&self) -> Option<i64> {
962	// 	Some(
963	// 		self.date
964	// 			.as_days()
965	// 			.checked_mul(SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI)?
966	// 			.checked_add(i64::try_from(self.time.as_micros()).unwrap())?,
967	// 	)
968	// }
969	// /// Get the number of nanoseconds since the Unix epoch
970	// pub fn as_nanos(&self) -> Option<i64> {
971	// 	Some(
972	// 		self.date
973	// 			.as_days()
974	// 			.checked_mul(
975	// 				SECONDS_PER_DAY * MILLIS_PER_SECOND * MICROS_PER_MILLI * NANOS_PER_MICRO,
976	// 			)?
977	// 			.checked_add(i64::try_from(self.time.as_nanos()).unwrap())?,
978	// 	)
979	// }
980}
981impl AmadeusOrd for DateTimeWithoutTimezone {
982	fn amadeus_cmp(&self, other: &Self) -> Ordering {
983		Ord::cmp(self, other)
984	}
985}
986impl Display for DateTimeWithoutTimezone {
987	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
988		self.as_chrono().expect(TODO).fmt(f)
989	}
990}
991impl FromStr for DateTimeWithoutTimezone {
992	type Err = ParseDateError;
993
994	fn from_str(s: &str) -> Result<Self, Self::Err> {
995		NaiveDateTime::from_str(s)
996			.map(|date| Self::from_chrono(&date))
997			.map_err(|_| ParseDateError)
998	}
999}
1000
1001#[cfg(test)]
1002mod tests {
1003	use super::*;
1004
1005	use chrono::NaiveDate;
1006
1007	const SECONDS_PER_DAY: i64 = 86_400;
1008
1009	// #[test]
1010	// fn test_int96() {
1011	// 	let value = DateTimeWithoutTimezone(Int96::from(vec![0, 0, 2454923]));
1012	// 	assert_eq!(value.as_millis().unwrap(), 1238544000000);
1013
1014	// 	let value = DateTimeWithoutTimezone(Int96::from(vec![4165425152, 13, 2454923]));
1015	// 	assert_eq!(value.as_millis().unwrap(), 1238544060000);
1016
1017	// 	let value = DateTimeWithoutTimezone(Int96::from(vec![0, 0, 0]));
1018	// 	assert_eq!(value.as_millis().unwrap(), -210866803200000);
1019	// }
1020
1021	#[test]
1022	fn timezone() {
1023		assert_eq!(
1024			Timezone::from_name("Etc/GMT-14")
1025				.unwrap()
1026				.as_offset()
1027				.unwrap(),
1028			14 * 60 * 60
1029		);
1030	}
1031
1032	#[test]
1033	fn test_convert_date_to_string() {
1034		fn check_date_conversion(y: i32, m: u32, d: u32) {
1035			let chrono_date = NaiveDate::from_ymd(y, m, d);
1036			let chrono_datetime = chrono_date.and_hms(0, 0, 0);
1037			assert_eq!(chrono_datetime.timestamp() % SECONDS_PER_DAY, 0);
1038			let date =
1039				DateWithoutTimezone::from_days(chrono_datetime.timestamp() / SECONDS_PER_DAY)
1040					.unwrap();
1041			assert_eq!(date.to_string(), chrono_date.to_string());
1042			let date2 = DateWithoutTimezone::from_chrono(&date.as_chrono().unwrap());
1043			assert_eq!(date, date2);
1044		}
1045
1046		check_date_conversion(-262_144, 1, 1);
1047		check_date_conversion(1969, 12, 31);
1048		check_date_conversion(1970, 1, 1);
1049		check_date_conversion(2010, 1, 2);
1050		check_date_conversion(2014, 5, 1);
1051		check_date_conversion(2016, 2, 29);
1052		check_date_conversion(2017, 9, 12);
1053		check_date_conversion(2018, 3, 31);
1054		check_date_conversion(262_143, 12, 31);
1055	}
1056
1057	#[test]
1058	fn test_convert_time_to_string() {
1059		fn check_time_conversion(h: u32, mi: u32, s: u32) {
1060			let chrono_time = NaiveTime::from_hms(h, mi, s);
1061			let time = TimeWithoutTimezone::from_chrono(&chrono_time);
1062			assert_eq!(time.to_string(), chrono_time.to_string());
1063		}
1064
1065		check_time_conversion(13, 12, 54);
1066		check_time_conversion(8, 23, 1);
1067		check_time_conversion(11, 6, 32);
1068		check_time_conversion(16, 38, 0);
1069		check_time_conversion(21, 15, 12);
1070	}
1071
1072	#[test]
1073	fn test_convert_timestamp_to_string() {
1074		#[allow(clippy::many_single_char_names)]
1075		fn check_datetime_conversion(y: i32, m: u32, d: u32, h: u32, mi: u32, s: u32) {
1076			let dt = NaiveDate::from_ymd(y, m, d).and_hms(h, mi, s);
1077			// let res = DateTimeWithoutTimezone::from_millis(dt.timestamp_millis()).to_string();
1078			let res = DateTimeWithoutTimezone::from_chrono(&dt).to_string();
1079			let exp = dt.to_string();
1080			assert_eq!(res, exp);
1081		}
1082
1083		check_datetime_conversion(2010, 1, 2, 13, 12, 54);
1084		check_datetime_conversion(2011, 1, 3, 8, 23, 1);
1085		check_datetime_conversion(2012, 4, 5, 11, 6, 32);
1086		check_datetime_conversion(2013, 5, 12, 16, 38, 0);
1087		check_datetime_conversion(2014, 11, 28, 21, 15, 12);
1088	}
1089}