Skip to main content

proto_types/common/
datetime.rs

1use core::fmt::{Display, Formatter};
2
3use thiserror::Error;
4
5use crate::{
6	Duration, String,
7	common::{DateTime, TimeZone, date_time::TimeOffset},
8};
9
10impl Display for TimeZone {
11	fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
12		write!(f, "{}", self.id)
13	}
14}
15
16impl Display for DateTime {
17	fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
18		if self.year != 0 {
19			write!(f, "{:04}-", self.year)?;
20		}
21		write!(f, "{:02}-{:02}", self.month, self.day)?;
22
23		write!(
24			f,
25			"T{:02}:{:02}:{:02}",
26			self.hours, self.minutes, self.seconds
27		)?;
28
29		if let Some(TimeOffset::UtcOffset(duration)) = &self.time_offset {
30			let total_offset_seconds = duration.normalized().seconds;
31			let is_negative = total_offset_seconds < 0;
32			let abs_total_offset_seconds = total_offset_seconds.abs();
33
34			let hours = abs_total_offset_seconds / 3600;
35			let minutes = (abs_total_offset_seconds % 3600) / 60;
36
37			if is_negative {
38				write!(f, "-{hours:02}:{minutes:02}")?
39			} else if total_offset_seconds == 0 && duration.nanos == 0 {
40				write!(f, "Z")? // 'Z' for UTC
41			} else {
42				write!(f, "+{hours:02}:{minutes:02}")?
43			}
44		}
45		Ok(())
46	}
47}
48
49/// Errors that can occur during the creation, conversion or validation of a [`DateTime`].
50#[derive(Debug, Error, PartialEq, Eq, Clone)]
51#[non_exhaustive]
52pub enum DateTimeError {
53	#[error(
54		"The year must be a value from 0 (to indicate a DateTime with no specific year) to 9999"
55	)]
56	InvalidYear,
57	#[error("If the year is set to 0, month and day cannot be set to 0")]
58	InvalidDate,
59	#[error("Invalid month value (must be within 1 and 12)")]
60	InvalidMonth,
61	#[error("Invalid day value (must be within 1 and 31)")]
62	InvalidDay,
63	#[error("Invalid hours value (must be within 0 and 23)")]
64	InvalidHours,
65	#[error("Invalid minutes value (must be within 0 and 59)")]
66	InvalidMinutes,
67	#[error("Invalid seconds value (must be within 0 and 59)")]
68	InvalidSeconds,
69	#[error("Invalid nanos value (must be within 0 and 999.999.999)")]
70	InvalidNanos,
71	#[error(
72		"DateTime has an invalid time component (e.g., hours, minutes, seconds, nanos out of range)"
73	)]
74	InvalidTime,
75	#[error("DateTime arithmetic resulted in a time outside its representable range")]
76	OutOfRange,
77	#[error("DateTime conversion error: {0}")]
78	ConversionError(String),
79}
80
81impl PartialOrd for TimeOffset {
82	#[inline]
83	fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
84		match (self, other) {
85			(Self::UtcOffset(a), Self::UtcOffset(b)) => a.partial_cmp(b),
86			// Can't determine order without timezone information
87			(Self::TimeZone(_) | Self::UtcOffset(_), Self::TimeZone(_))
88			| (Self::TimeZone(_), Self::UtcOffset(_)) => None,
89		}
90	}
91}
92
93impl PartialOrd for DateTime {
94	fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
95		if !(self.is_valid() && other.is_valid()) {
96			return None;
97		}
98
99		if (self.year == 0 && other.year != 0) || (self.year != 0 && other.year == 0) {
100			return None;
101		}
102
103		let ord = self
104			.year
105			.cmp(&other.year)
106			.then_with(|| self.month.cmp(&other.month))
107			.then_with(|| self.day.cmp(&other.day))
108			.then_with(|| self.hours.cmp(&other.hours))
109			.then_with(|| self.minutes.cmp(&other.minutes))
110			.then_with(|| self.seconds.cmp(&other.seconds))
111			.then_with(|| self.nanos.cmp(&other.nanos));
112
113		if ord != core::cmp::Ordering::Equal {
114			return Some(ord);
115		}
116
117		self.time_offset.partial_cmp(&other.time_offset)
118	}
119}
120
121#[allow(clippy::too_many_arguments)]
122fn datetime_is_valid(
123	year: i32,
124	month: i32,
125	day: i32,
126	hours: i32,
127	minutes: i32,
128	seconds: i32,
129	nanos: i32,
130) -> Result<(), DateTimeError> {
131	if !(0..=9999).contains(&year) {
132		return Err(DateTimeError::InvalidYear);
133	}
134	if !(1..=12).contains(&month) {
135		return Err(DateTimeError::InvalidMonth);
136	}
137	let max_days = crate::date::days_in_month(month, year);
138	if !(1..=max_days).contains(&day) {
139		return Err(DateTimeError::InvalidDay);
140	}
141
142	if year == 0 && (day == 0 || month == 0) {
143		return Err(DateTimeError::InvalidDate);
144	}
145
146	if !(0..=23).contains(&hours) {
147		return Err(DateTimeError::InvalidHours);
148	}
149	if !(0..=59).contains(&minutes) {
150		return Err(DateTimeError::InvalidMinutes);
151	}
152	if !(0..=59).contains(&seconds) {
153		return Err(DateTimeError::InvalidSeconds);
154	}
155	if !(0..=999_999_999).contains(&nanos) {
156		return Err(DateTimeError::InvalidNanos);
157	}
158
159	Ok(())
160}
161
162impl DateTime {
163	/// Checks if this [`DateTime`] instance represents a valid date and time, and returns the related error if it does not.
164	pub fn validate(&self) -> Result<(), DateTimeError> {
165		datetime_is_valid(
166			self.year,
167			self.month,
168			self.day,
169			self.hours,
170			self.minutes,
171			self.seconds,
172			self.nanos,
173		)
174	}
175
176	#[must_use]
177	#[inline]
178	/// Checks if this [`DateTime`] instance represents a valid date and time.
179	pub fn is_valid(&self) -> bool {
180		self.validate().is_ok()
181	}
182
183	#[must_use]
184	#[inline]
185	/// Returns `true` if the [`DateTime`] has a specific year (i.e., `year` is not 0).
186	pub const fn has_year(&self) -> bool {
187		self.year != 0
188	}
189
190	/// Returns true if the [`TimeOffset`] is a UtcOffset.
191	#[must_use]
192	#[inline]
193	pub const fn has_utc_offset(&self) -> bool {
194		matches!(self.time_offset, Some(TimeOffset::UtcOffset(_)))
195	}
196
197	/// Returns true if the [`TimeOffset`] is a TimeZone.
198	#[must_use]
199	#[inline]
200	pub const fn has_timezone(&self) -> bool {
201		matches!(self.time_offset, Some(TimeOffset::TimeZone(_)))
202	}
203
204	/// Returns true if the [`TimeOffset`] is None.
205	#[must_use]
206	#[inline]
207	pub const fn is_local(&self) -> bool {
208		self.time_offset.is_none()
209	}
210
211	/// Sets the `time_offset` to a UTC offset [`Duration`], clearing any existing time zone.
212	#[must_use]
213	#[inline]
214	pub fn with_utc_offset(mut self, offset: Duration) -> Self {
215		self.time_offset = Some(TimeOffset::UtcOffset(offset));
216		self
217	}
218
219	/// Sets the `time_offset` to a [`TimeZone`], clearing any existing UTC offset.
220	#[must_use]
221	#[inline]
222	pub fn with_time_zone(mut self, time_zone: TimeZone) -> Self {
223		self.time_offset = Some(TimeOffset::TimeZone(time_zone));
224		self
225	}
226}
227
228pub const UTC_OFFSET: Duration = Duration {
229	seconds: 0,
230	nanos: 0,
231};
232
233#[cfg(feature = "chrono")]
234mod chrono_impls {
235	use chrono::Utc;
236
237	use super::{DateTime, DateTimeError};
238	use crate::{Duration, String, ToString, date_time::TimeOffset, datetime::UTC_OFFSET, format};
239
240	impl DateTime {
241		#[cfg(any(feature = "std", feature = "chrono-wasm"))]
242		/// Returns the current [`DateTime`] with Utc offset.
243		#[must_use]
244		pub fn now_utc() -> Self {
245			Utc::now().into()
246		}
247
248		/// Converts this [`DateTime`] to [`chrono::DateTime`] Utc.
249		/// It succeeds if the [`TimeOffset`] is a UtcOffset with 0 seconds and nanos.
250		pub fn to_datetime_utc(self) -> Result<chrono::DateTime<chrono::Utc>, DateTimeError> {
251			self.try_into()
252		}
253
254		/// Converts this [`DateTime`] to [`chrono::DateTime`]<[`FixedOffset`](chrono::FixedOffset)>.
255		/// It succeeds if the [`TimeOffset`] is a UtcOffset that results in an unambiguous [`FixedOffset`](chrono::FixedOffset).
256		pub fn to_fixed_offset_datetime(
257			self,
258		) -> Result<chrono::DateTime<chrono::FixedOffset>, DateTimeError> {
259			self.try_into()
260		}
261
262		#[cfg(feature = "chrono-tz")]
263		/// Converts this [`DateTime`] to [`chrono::DateTime`]<[`Tz`](chrono_tz::Tz)>.
264		/// It succeeds if the [`TimeOffset`] is a [`TimeZone`] that maps to a valid [`Tz`](chrono_tz::Tz) or if the [`TimeOffset`] is a UtcOffset with 0 seconds and nanos.
265		pub fn to_datetime_with_tz(self) -> Result<chrono::DateTime<chrono_tz::Tz>, DateTimeError> {
266			self.try_into()
267		}
268	}
269
270	// FixedOffset conversions
271	// From FixedOffset to DateTime is not possible because the values for the offset are not retrievable
272
273	impl TryFrom<DateTime> for chrono::DateTime<chrono::FixedOffset> {
274		type Error = DateTimeError;
275		fn try_from(value: DateTime) -> Result<Self, Self::Error> {
276			use crate::date_time::TimeOffset;
277
278			match &value.time_offset {
279				Some(TimeOffset::UtcOffset(proto_duration)) => {
280					use crate::constants::NANOS_PER_SECOND;
281
282					let total_nanos_i128 = i128::from(proto_duration.seconds)
283						.checked_mul(i128::from(NANOS_PER_SECOND))
284						.ok_or(DateTimeError::ConversionError(
285							"UtcOffset seconds multiplied by NANOS_PER_SECOND overflowed i128"
286								.to_string(),
287						))?
288						.checked_add(i128::from(proto_duration.nanos))
289						.ok_or(DateTimeError::ConversionError(
290							"UtcOffset nanos addition overflowed i128".to_string(),
291						))?;
292
293					let total_seconds_i128 = total_nanos_i128
294            .checked_div(i128::from(NANOS_PER_SECOND))
295            .ok_or(DateTimeError::ConversionError(
296              "UtcOffset total nanoseconds division overflowed i128 (should not happen)"
297                .to_string(),
298            ))?; // Division by zero not possible for NANOS_PER_SECOND
299
300					let total_seconds_i32: i32 = total_seconds_i128.try_into().map_err(|_| {
301						DateTimeError::ConversionError(
302							"UtcOffset total seconds is outside of i32 range for FixedOffset"
303								.to_string(),
304						)
305					})?;
306
307					let offset = chrono::FixedOffset::east_opt(total_seconds_i32).ok_or_else(|| {
308            DateTimeError::ConversionError(
309              "Failed to convert proto::Duration to chrono::FixedOffset due to invalid offset values"
310                .to_string(),
311            )
312          })?;
313
314					let naive_dt: chrono::NaiveDateTime = value.try_into()?;
315
316					naive_dt
317						.and_local_timezone(offset)
318						.single() // Take the unique result if not ambiguous
319						.ok_or(DateTimeError::ConversionError(
320							"Ambiguous or invalid local time to FixedOffset conversion".to_string(),
321						))
322				}
323				Some(TimeOffset::TimeZone(tz_info)) => {
324					#[cfg(feature = "chrono-tz")]
325					{
326						use chrono::{Offset, TimeZone};
327						use core::str::FromStr;
328
329						// 1. Parse the string (e.g., "Europe/Paris")
330						let tz = chrono_tz::Tz::from_str(&tz_info.id).map_err(|_| {
331							DateTimeError::ConversionError(format!(
332								"Unknown TimeZone ID: {}",
333								tz_info.id
334							))
335						})?;
336
337						let naive_dt: chrono::NaiveDateTime = value.try_into()?;
338
339						// 2. Resolve the Timezone for this specific wall clock time.
340						// This handles DST. E.g., 12:00 in Summer might be +02:00, in Winter +01:00.
341						let dt_with_tz = tz.from_local_datetime(&naive_dt).single().ok_or(
342							DateTimeError::ConversionError(
343								"Ambiguous or invalid time for this timezone (DST gap/overlap)"
344									.into(),
345							),
346						)?;
347
348						// 3. Convert the dynamic Tz offset into a static FixedOffset
349						// .fix() extracts the computed offset (e.g., +02:00)
350						Ok(dt_with_tz.with_timezone(&dt_with_tz.offset().fix()))
351					}
352
353					#[cfg(not(feature = "chrono-tz"))]
354					{
355						Err(DateTimeError::ConversionError(
356              "Enable the 'chrono-tz' feature to convert named TimeZones to FixedOffset"
357                .to_string(),
358            ))
359					}
360				}
361				None => Err(DateTimeError::ConversionError(
362					"Cannot convert local DateTime (no offset) to FixedOffset. \
363           If you intended UTC, use .with_utc_offset() first."
364						.to_string(),
365				)),
366			}
367		}
368	}
369
370	// NaiveDateTime conversions
371
372	impl From<chrono::NaiveDateTime> for DateTime {
373		#[inline]
374		fn from(ndt: chrono::NaiveDateTime) -> Self {
375			use chrono::{Datelike, Timelike};
376
377			// NaiveDateTime has no offset, so DateTime will be local time
378			// Casting is safe due to chrono's constructor API
379			Self {
380				year: ndt.year(),
381				month: ndt.month().cast_signed(),
382				day: ndt.day().cast_signed(),
383				hours: ndt.hour().cast_signed(),
384				minutes: ndt.minute().cast_signed(),
385				seconds: ndt.second().cast_signed(),
386				nanos: ndt.nanosecond().cast_signed(),
387				time_offset: None,
388			}
389		}
390	}
391
392	impl TryFrom<DateTime> for chrono::NaiveDateTime {
393		type Error = DateTimeError;
394
395		fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
396			// NaiveDateTime does not support year 0, nor does it carry time offset.
397			if dt.year == 0 {
398				return Err(DateTimeError::ConversionError(
399					"Cannot convert DateTime with year 0 to NaiveDateTime".to_string(),
400				));
401			}
402
403			dt.validate()?;
404
405			// Casting is safe after validation
406			let date = chrono::NaiveDate::from_ymd_opt(
407				dt.year,
408				dt.month.cast_unsigned(),
409				dt.day.cast_unsigned(),
410			)
411			.ok_or(DateTimeError::InvalidDate)?;
412			let time = chrono::NaiveTime::from_hms_nano_opt(
413				dt.hours.cast_unsigned(),
414				dt.minutes.cast_unsigned(),
415				dt.seconds.cast_unsigned(),
416				dt.nanos.cast_unsigned(),
417			)
418			.ok_or(DateTimeError::InvalidTime)?;
419
420			Ok(Self::new(date, time))
421		}
422	}
423
424	// UTC Conversions
425
426	impl From<chrono::DateTime<chrono::Utc>> for DateTime {
427		#[inline]
428		fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
429			use chrono::{Datelike, Timelike};
430
431			use crate::date_time::TimeOffset;
432			// Casting is safe due to chrono's constructor API
433			Self {
434				year: value.year(),
435				month: value.month().cast_signed(),
436				day: value.day().cast_signed(),
437				hours: value.hour().cast_signed(),
438				minutes: value.minute().cast_signed(),
439				seconds: value.second().cast_signed(),
440				nanos: value.nanosecond().cast_signed(),
441				time_offset: Some(TimeOffset::UtcOffset(Duration::new(0, 0))),
442			}
443		}
444	}
445
446	impl TryFrom<DateTime> for chrono::DateTime<chrono::Utc> {
447		type Error = DateTimeError;
448		fn try_from(value: DateTime) -> Result<Self, Self::Error> {
449			match &value.time_offset {
450				Some(TimeOffset::UtcOffset(proto_duration)) => {
451					if *proto_duration != UTC_OFFSET {
452						return Err(DateTimeError::ConversionError(
453							"Cannot convert DateTime to TimeZone<Utc> when the UtcOffset is not 0."
454								.to_string(),
455						));
456					}
457				}
458				Some(TimeOffset::TimeZone(_)) | None => {
459					return Err(DateTimeError::ConversionError(
460						"Cannot convert DateTime to TimeZone<Utc> when a UtcOffset is not set."
461							.to_string(),
462					));
463				}
464			};
465
466			let naive_dt: chrono::NaiveDateTime = value.try_into()?;
467
468			Ok(naive_dt.and_utc())
469		}
470	}
471
472	#[cfg(feature = "chrono-tz")]
473	impl From<chrono_tz::Tz> for super::TimeZone {
474		fn from(value: chrono_tz::Tz) -> Self {
475			Self {
476				id: value.to_string(),
477				version: String::new(), // Version is optional according to the spec
478			}
479		}
480	}
481
482	// DateTime<Tz> conversions
483
484	#[cfg(feature = "chrono-tz")]
485	impl From<chrono::DateTime<chrono_tz::Tz>> for DateTime {
486		fn from(value: chrono::DateTime<chrono_tz::Tz>) -> Self {
487			use chrono::{Datelike, Timelike};
488
489			Self {
490				year: value.year(),
491				month: value.month().cast_signed(),
492				day: value.day().cast_signed(),
493				hours: value.hour().cast_signed(),
494				minutes: value.minute().cast_signed(),
495				seconds: value.second().cast_signed(),
496				nanos: value.nanosecond().cast_signed(),
497				time_offset: Some(TimeOffset::TimeZone(super::TimeZone {
498					id: value.timezone().to_string(),
499					version: String::new(), // Version is optional according to the spec
500				})),
501			}
502		}
503	}
504
505	#[cfg(feature = "chrono-tz")]
506	impl TryFrom<DateTime> for chrono::DateTime<chrono_tz::Tz> {
507		type Error = DateTimeError;
508
509		fn try_from(value: crate::DateTime) -> Result<Self, Self::Error> {
510			use core::str::FromStr;
511
512			use chrono::{NaiveDateTime, TimeZone};
513			use chrono_tz::Tz;
514
515			let timezone = match &value.time_offset {
516				Some(TimeOffset::UtcOffset(proto_duration)) => {
517					if *proto_duration == UTC_OFFSET {
518						Tz::UTC
519					} else {
520						return Err(DateTimeError::ConversionError(
521							"Cannot convert non-zero UtcOffset to a named TimeZone (Tz)"
522								.to_string(),
523						));
524					}
525				}
526				// Case B: TimeZone (named IANA string) -> Use chrono_tz::Tz::from_str
527				Some(TimeOffset::TimeZone(tz_name)) => Tz::from_str(&tz_name.id).map_err(|_| {
528					DateTimeError::ConversionError(format!(
529						"Unrecognized or invalid timezone name: {}",
530						tz_name.id
531					))
532				})?,
533				None => {
534					return Err(DateTimeError::ConversionError(
535            "Cannot convert local DateTime to named TimeZone (Tz) without explicit offset or name"
536              .to_string(),
537          ));
538				}
539			};
540
541			let naive_dt: NaiveDateTime = value.try_into()?;
542
543			timezone
544				.from_local_datetime(&naive_dt)
545				.single()
546				.ok_or(DateTimeError::ConversionError(
547					"Ambiguous or invalid local time to named TimeZone (Tz) conversion".to_string(),
548				))
549		}
550	}
551}
552
553#[cfg(test)]
554mod tests {
555	use super::*;
556	use crate::Duration;
557	use alloc::string::ToString;
558
559	fn dt(y: i32, m: i32, d: i32, h: i32, min: i32, s: i32, n: i32) -> DateTime {
560		DateTime {
561			year: y,
562			month: m,
563			day: d,
564			hours: h,
565			minutes: min,
566			seconds: s,
567			nanos: n,
568			time_offset: None,
569		}
570	}
571
572	#[test]
573	fn test_display_formatting() {
574		// 1. Standard Local
575		let d = dt(2024, 1, 15, 12, 30, 45, 0);
576		assert_eq!(d.to_string(), "2024-01-15T12:30:45");
577
578		// 2. Year 0 (No Year)
579		let no_year = dt(0, 12, 25, 8, 0, 0, 0);
580		assert_eq!(no_year.to_string(), "12-25T08:00:00");
581
582		// 3. UTC Offset (Positive)
583		let mut utc_plus = d.clone();
584		utc_plus.time_offset = Some(TimeOffset::UtcOffset(Duration {
585			seconds: 3600,
586			nanos: 0,
587		})); // +1h
588		assert_eq!(utc_plus.to_string(), "2024-01-15T12:30:45+01:00");
589
590		// 4. UTC Offset (Negative)
591		let mut utc_minus = d.clone();
592		utc_minus.time_offset = Some(TimeOffset::UtcOffset(Duration {
593			seconds: -5400,
594			nanos: 0,
595		})); // -1h 30m
596		assert_eq!(utc_minus.to_string(), "2024-01-15T12:30:45-01:30");
597
598		// 5. UTC Z
599		let mut utc_z = d.clone();
600		utc_z.time_offset = Some(TimeOffset::UtcOffset(Duration {
601			seconds: 0,
602			nanos: 0,
603		}));
604		assert_eq!(utc_z.to_string(), "2024-01-15T12:30:45Z");
605
606		// 6. Named TimeZone
607		let mut named = d;
608		named = named.with_time_zone(TimeZone {
609			id: "America/New_York".into(),
610			version: String::new(),
611		});
612		assert_eq!(named.to_string(), "2024-01-15T12:30:45");
613	}
614
615	#[test]
616	fn test_validation() {
617		// Range errors
618		assert!(dt(2024, 13, 1, 0, 0, 0, 0).validate().is_err()); // Month
619		assert!(dt(2024, 1, 1, 24, 0, 0, 0).validate().is_err()); // Hour
620
621		// Calendar logic
622		assert!(dt(2023, 2, 29, 12, 0, 0, 0).validate().is_err()); // Not leap year
623		assert!(dt(2024, 2, 29, 12, 0, 0, 0).validate().is_ok()); // Leap year
624
625		// Year 0 logic
626		assert!(dt(0, 1, 1, 0, 0, 0, 0).validate().is_ok());
627		assert!(dt(0, 0, 1, 0, 0, 0, 0).validate().is_err()); // Month 0
628	}
629
630	#[test]
631	fn test_partial_ord() {
632		let d1 = dt(2024, 1, 1, 10, 0, 0, 0);
633		let d2 = dt(2024, 1, 1, 11, 0, 0, 0);
634
635		assert!(d1 < d2);
636
637		// Year 0 vs Specific Year = Not Comparable
638		let d_year0 = dt(0, 1, 1, 10, 0, 0, 0);
639		assert_eq!(d1.partial_cmp(&d_year0), None);
640	}
641
642	#[cfg(feature = "chrono")]
643	mod chrono_tests {
644		use super::*;
645		use chrono::{Datelike, Timelike};
646
647		#[test]
648		fn test_to_naive_datetime() {
649			let d = dt(2024, 5, 20, 10, 30, 0, 500);
650			let naive: chrono::NaiveDateTime = d.try_into().unwrap();
651
652			assert_eq!(naive.year(), 2024);
653			assert_eq!(naive.hour(), 10);
654			assert_eq!(naive.nanosecond(), 500);
655		}
656
657		#[test]
658		fn test_to_fixed_offset() {
659			let mut d = dt(2024, 5, 20, 10, 0, 0, 0);
660			// Offset +1 hour
661			d = d.with_utc_offset(Duration {
662				seconds: 3600,
663				nanos: 0,
664			});
665
666			let fixed: chrono::DateTime<chrono::FixedOffset> = d.try_into().unwrap();
667
668			// The time should stay 10:00, but the offset is +1
669			assert_eq!(fixed.hour(), 10);
670			assert_eq!(fixed.offset().local_minus_utc(), 3600);
671		}
672
673		#[cfg(feature = "chrono-tz")]
674		#[test]
675		fn test_to_tz() {
676			use chrono_tz::US::Pacific;
677			let mut d = dt(2024, 1, 1, 12, 0, 0, 0);
678			d = d.with_time_zone(TimeZone {
679				id: "US/Pacific".into(),
680				version: String::new(),
681			});
682
683			let tz_dt: chrono::DateTime<chrono_tz::Tz> = d.try_into().unwrap();
684			assert_eq!(tz_dt.timezone(), Pacific);
685		}
686
687		#[cfg(feature = "chrono-tz")]
688		#[test]
689		fn test_named_tz_to_fixed_offset_dst() {
690			// New York Standard Time (Winter) -> UTC-5
691			let winter = dt(2024, 1, 1, 12, 0, 0, 0).with_time_zone(TimeZone {
692				id: "America/New_York".into(),
693				version: String::new(),
694			});
695
696			let fixed_winter: chrono::DateTime<chrono::FixedOffset> = winter.try_into().unwrap();
697			assert_eq!(fixed_winter.offset().local_minus_utc(), -5 * 3600);
698
699			// New York Daylight Time (Summer) -> UTC-4
700			let summer = dt(2024, 6, 1, 12, 0, 0, 0).with_time_zone(TimeZone {
701				id: "America/New_York".into(),
702				version: String::new(),
703			});
704
705			let fixed_summer: chrono::DateTime<chrono::FixedOffset> = summer.try_into().unwrap();
706			assert_eq!(fixed_summer.offset().local_minus_utc(), -4 * 3600);
707		}
708	}
709}