jiff_icu/
lib.rs

1/*!
2This crate provides conversion routines between [`jiff`] and
3[`icu`](https://docs.rs/icu/2/).
4
5The conversion routines are implemented via conversion traits defined in this
6crate. The traits mirror the [`From`], [`Into`], [`TryFrom`] and [`TryInto`]
7traits from the standard library.
8
9# Available conversions
10
11* [`jiff::Zoned`] infallibly converts to
12[`icu::time::ZonedDateTime`](icu_time::ZonedDateTime).
13The reverse is unavailable.
14* [`jiff::civil::DateTime`] infallibly converts to
15[`icu::time::DateTime`](icu_time::DateTime).
16The reverse is fallible.
17* [`jiff::civil::Date`] infallibly converts to
18[`icu::calendar::Date`](icu_calendar::Date).
19The reverse is fallible.
20* [`jiff::civil::Time`] infallibly converts to
21[`icu::time::Time`](icu_time::Time).
22The reverse is fallible.
23* [`jiff::civil::Weekday`] infallibly converts to
24[`icu::calendar::types::Weekday`](icu_calendar::types::Weekday).
25The reverse is also infallible.
26* [`jiff::tz::TimeZone`] infallibly converts to
27[`icu::time::TimeZone`](icu_time::TimeZone). The reverse is unavailable.
28
29# Format a zoned datetime in another locale
30
31This example shows how one can bridge `jiff` to `icu` in order to
32format a zoned datetime in a particular locale. This makes use of the
33infallible [`ConvertFrom`] trait to convert a [`jiff::Zoned`] into a
34[`icu::time::ZonedDateTime`](icu_time::ZonedDateTime). In order to get `icu` to
35emit the time zone information, we need to add a zone marker to our fieldset:
36
37```
38use icu::{
39    calendar::Iso,
40    time::{ZonedDateTime, TimeZoneInfo, zone::models::AtTime},
41    datetime::{fieldsets, DateTimeFormatter},
42    locale::locale,
43};
44use jiff::Zoned;
45use jiff_icu::{ConvertFrom as _};
46
47let zdt: Zoned = "2024-09-10T23:37:20-04[America/New_York]".parse()?;
48let icu_zdt = ZonedDateTime::<Iso, TimeZoneInfo<AtTime>>::convert_from(&zdt);
49
50// Format for the en-GB locale:
51let formatter = DateTimeFormatter::try_new(
52    locale!("en-GB").into(),
53    fieldsets::YMDET::medium().with_zone(fieldsets::zone::SpecificLong),
54)?;
55assert_eq!(
56    formatter.format(&icu_zdt).to_string(),
57    "Tue, 10 Sept 2024, 23:37:20 Eastern Daylight Time",
58);
59
60// Or in the en-US locale:
61let formatter = DateTimeFormatter::try_new(
62    locale!("en-US").into(),
63    fieldsets::YMDET::medium().with_zone(fieldsets::zone::SpecificShort),
64)?;
65assert_eq!(
66    formatter.format(&icu_zdt).to_string(),
67    "Tue, Sep 10, 2024, 11:37:20 PM EDT",
68);
69
70// Or in the "unknown" locale:
71let formatter = DateTimeFormatter::try_new(
72    icu::locale::Locale::UNKNOWN.into(),
73    fieldsets::YMDET::medium().with_zone(fieldsets::zone::SpecificShort),
74)?;
75assert_eq!(
76    formatter.format(&icu_zdt).to_string(),
77    "2024 M09 10, Tue 23:37:20 GMT-4",
78);
79
80# Ok::<(), Box<dyn std::error::Error>>(())
81```
82
83# Format a civil datetime in another locale
84
85This example shows how one can bridge `jiff` to `icu` in order to format a
86datetime in a particular locale. This makes use of the infallible
87[`ConvertFrom`] trait to convert a [`jiff::civil::DateTime`] into a
88[`icu::time::DateTime`](icu_time::DateTime).
89
90```
91use icu::{
92    time::DateTime,
93    datetime::{fieldsets, DateTimeFormatter},
94    locale::locale,
95};
96use jiff::Zoned;
97use jiff_icu::{ConvertFrom as _};
98
99let zdt: Zoned = "2024-09-10T23:37:20-04[America/New_York]".parse()?;
100let icu_datetime = DateTime::convert_from(zdt.datetime());
101
102// Format for the en-GB locale:
103let formatter = DateTimeFormatter::try_new(
104    locale!("en-GB").into(),
105    fieldsets::YMDET::medium(),
106)?;
107assert_eq!(
108    formatter.format(&icu_datetime).to_string(),
109    "Tue, 10 Sept 2024, 23:37:20",
110);
111
112// Or in the en-US locale:
113let formatter = DateTimeFormatter::try_new(
114    locale!("en-US").into(),
115    fieldsets::YMDET::medium(),
116)?;
117assert_eq!(
118    formatter.format(&icu_datetime).to_string(),
119    "Tue, Sep 10, 2024, 11:37:20 PM",
120);
121
122# Ok::<(), Box<dyn std::error::Error>>(())
123```
124
125# Convert to another calendar
126
127While Jiff only supports the Gregorian calendar (technically the ISO
1288601 calendar), ICU4X supports [many calendars](icu_calendar). This
129example shows how to convert a date in the Hebrew calendar to a Jiff
130date. This makes use of the fallible [`ConvertTryFrom`] trait to
131convert a [`icu::time::DateTime`](icu_time::DateTime) into a
132[`jiff::civil::DateTime`]:
133
134```
135use icu::{
136    calendar::Date as IcuDate,
137    time::{DateTime as IcuDateTime, Time as IcuTime},
138};
139use jiff::civil::DateTime;
140use jiff_icu::{ConvertTryFrom as _};
141
142let datetime = IcuDateTime {
143    date: IcuDate::try_new_hebrew(5785, 5, 4)?,
144    time: IcuTime::try_new(17, 30, 0, 0)?,
145};
146let zdt = DateTime::convert_try_from(datetime)?.in_tz("America/New_York")?;
147assert_eq!(zdt.to_string(), "2025-02-02T17:30:00-05:00[America/New_York]");
148
149# Ok::<(), Box<dyn std::error::Error>>(())
150```
151*/
152
153#![no_std]
154#![deny(missing_docs)]
155// This adds Cargo feature annotations to items in the rustdoc output. Which is
156// sadly hugely beneficial for this crate due to the number of features.
157#![cfg_attr(docsrs_jiff, feature(doc_cfg))]
158
159#[cfg(any(test, feature = "std"))]
160extern crate std;
161
162#[cfg(any(test, feature = "alloc"))]
163extern crate alloc;
164
165use icu_calendar::{
166    types::Weekday as IcuWeekday, AsCalendar as IcuAsCalendar,
167    Date as IcuDate, Iso,
168};
169#[cfg(feature = "zoned")]
170#[allow(deprecated)]
171use icu_time::zone::models::Full;
172#[cfg(feature = "time")]
173use icu_time::{
174    zone::UtcOffset as IcuUtcOffset, DateTime as IcuDateTime, Time as IcuTime,
175};
176#[cfg(feature = "zoned")]
177use icu_time::{
178    zone::{models::AtTime, TimeZoneVariant},
179    TimeZone as IcuTimeZone, TimeZoneInfo as IcuTimeZoneInfo,
180    ZonedDateTime as IcuZonedDateTime,
181};
182
183use jiff::civil::{Date as JiffDate, Weekday as JiffWeekday};
184#[cfg(feature = "time")]
185use jiff::{
186    civil::{DateTime as JiffDateTime, Time as JiffTime},
187    tz::Offset as JiffOffset,
188};
189#[cfg(feature = "zoned")]
190use jiff::{tz::TimeZone as JiffTimeZone, Zoned as JiffZoned};
191
192use self::error::err;
193pub use self::{
194    error::Error,
195    traits::{ConvertFrom, ConvertInto, ConvertTryFrom, ConvertTryInto},
196};
197
198mod error;
199mod traits;
200
201/// Converts from a [`icu_time::DateTime<Iso>`](icu_time::DateTime) to
202/// a [`jiff::civil::DateTime`].
203///
204/// # Examples
205///
206/// ```
207/// use jiff_icu::{ConvertTryFrom as _};
208///
209/// let icu_datetime = icu_time::DateTime {
210///     date: icu_calendar::Date::try_new_iso(1970, 1, 1).unwrap(),
211///     time: icu_time::Time::try_new(0, 0, 0, 0).unwrap(),
212/// };
213/// let jiff_datetime = jiff::civil::DateTime::convert_try_from(icu_datetime)?;
214/// assert_eq!(jiff_datetime.to_string(), "1970-01-01T00:00:00");
215///
216/// let icu_datetime = icu_time::DateTime {
217///     date: icu_calendar::Date::try_new_iso(2025, 1, 30).unwrap(),
218///     time: icu_time::Time::try_new(17, 58, 30, 0).unwrap(),
219/// };
220/// let jiff_datetime = jiff::civil::DateTime::convert_try_from(icu_datetime)?;
221/// assert_eq!(jiff_datetime.to_string(), "2025-01-30T17:58:30");
222///
223/// let icu_datetime = icu_time::DateTime {
224///     date: icu_calendar::Date::try_new_iso(-9999, 1, 1).unwrap(),
225///     time: icu_time::Time::try_new(0, 0, 0, 0).unwrap(),
226/// };
227/// let jiff_datetime = jiff::civil::DateTime::convert_try_from(icu_datetime)?;
228/// assert_eq!(jiff_datetime.to_string(), "-009999-01-01T00:00:00");
229///
230/// let icu_datetime = icu_time::DateTime {
231///     date: icu_calendar::Date::try_new_iso(9999, 12, 31).unwrap(),
232///     time: icu_time::Time::try_new(23, 59, 59, 999_999_999).unwrap(),
233/// };
234/// let jiff_datetime = jiff::civil::DateTime::convert_try_from(icu_datetime)?;
235/// assert_eq!(jiff_datetime.to_string(), "9999-12-31T23:59:59.999999999");
236///
237/// let icu_datetime = icu_time::DateTime {
238///     date: icu_calendar::Date::try_new_iso(0, 1, 30).unwrap(),
239///     time: icu_time::Time::try_new(0, 0, 0, 0).unwrap(),
240/// };
241/// let jiff_datetime = jiff::civil::DateTime::convert_try_from(icu_datetime)?;
242/// assert_eq!(jiff_datetime.to_string(), "0000-01-30T00:00:00");
243///
244/// # Ok::<(), Box<dyn std::error::Error>>(())
245/// ```
246#[cfg(feature = "time")]
247impl<C: IcuAsCalendar> ConvertTryFrom<IcuDateTime<C>> for JiffDateTime {
248    type Error = Error;
249
250    fn convert_try_from(v: IcuDateTime<C>) -> Result<JiffDateTime, Error> {
251        let date: JiffDate = v.date.convert_try_into()?;
252        let time: JiffTime = v.time.convert_try_into()?;
253        Ok(JiffDateTime::from_parts(date, time))
254    }
255}
256
257/// Converts from a [`jiff::civil::DateTime`] to a
258/// [`icu_time::DateTime<Iso>`](icu_time::DateTime).
259///
260/// # Examples
261///
262/// ```
263/// use jiff_icu::{ConvertFrom as _};
264///
265/// let jiff_datetime = jiff::civil::date(2025, 1, 30).at(17, 58, 30, 0);
266/// let icu_datetime = icu_time::DateTime::convert_from(jiff_datetime);
267/// assert_eq!(
268///     format!("{:?}", icu_datetime.date),
269///     "Date(2025-1-30, default era, for calendar ISO)",
270/// );
271/// assert_eq!(
272///     format!("{:?}", icu_datetime.time),
273///     "Time { hour: Hour(17), minute: Minute(58), second: Second(30), subsecond: Nanosecond(0) }",
274/// );
275///
276/// let jiff_datetime = jiff::civil::DateTime::MIN;
277/// let icu_datetime = icu_time::DateTime::convert_from(jiff_datetime);
278/// assert_eq!(
279///     format!("{:?}", icu_datetime.date),
280///     "Date(-9999-1-1, default era, for calendar ISO)",
281/// );
282/// assert_eq!(
283///     format!("{:?}", icu_datetime.time),
284///     "Time { hour: Hour(0), minute: Minute(0), second: Second(0), subsecond: Nanosecond(0) }",
285/// );
286///
287/// let jiff_datetime = jiff::civil::DateTime::MAX;
288/// let icu_datetime = icu_time::DateTime::convert_from(jiff_datetime);
289/// assert_eq!(
290///     format!("{:?}", icu_datetime.date),
291///     "Date(9999-12-31, default era, for calendar ISO)",
292/// );
293/// assert_eq!(
294///     format!("{:?}", icu_datetime.time),
295///     "Time { hour: Hour(23), minute: Minute(59), second: Second(59), subsecond: Nanosecond(999999999) }",
296/// );
297///
298/// let jiff_datetime = jiff::civil::date(0, 1, 30).at(0, 0, 0, 0);
299/// let icu_datetime = icu_time::DateTime::convert_from(jiff_datetime);
300/// assert_eq!(
301///     format!("{:?}", icu_datetime.date),
302///     "Date(0-1-30, default era, for calendar ISO)",
303/// );
304/// assert_eq!(
305///     format!("{:?}", icu_datetime.time),
306///     "Time { hour: Hour(0), minute: Minute(0), second: Second(0), subsecond: Nanosecond(0) }",
307/// );
308/// ```
309#[cfg(feature = "time")]
310impl ConvertFrom<JiffDateTime> for IcuDateTime<Iso> {
311    fn convert_from(v: JiffDateTime) -> IcuDateTime<Iso> {
312        let date: IcuDate<Iso> = v.date().convert_into();
313        let time: IcuTime = v.time().convert_into();
314        IcuDateTime { date, time }
315    }
316}
317
318/// Converts from a [`icu_calendar::Date`] to a [`jiff::civil::Date`].
319///
320/// # Examples
321///
322/// ```
323/// use jiff_icu::{ConvertTryFrom as _};
324///
325/// let icu_date = icu_calendar::Date::try_new_iso(1970, 1, 1).unwrap();
326/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
327/// assert_eq!(jiff_date.to_string(), "1970-01-01");
328///
329/// let icu_date = icu_calendar::Date::try_new_iso(2025, 1, 30).unwrap();
330/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
331/// assert_eq!(jiff_date.to_string(), "2025-01-30");
332///
333/// let icu_date = icu_calendar::Date::try_new_iso(-9999, 1, 1).unwrap();
334/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
335/// assert_eq!(jiff_date.to_string(), "-009999-01-01");
336///
337/// let icu_date = icu_calendar::Date::try_new_iso(9999, 12, 31).unwrap();
338/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
339/// assert_eq!(jiff_date.to_string(), "9999-12-31");
340///
341/// let icu_date = icu_calendar::Date::try_new_iso(0, 1, 30).unwrap();
342/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
343/// assert_eq!(jiff_date.to_string(), "0000-01-30");
344///
345/// # Ok::<(), Box<dyn std::error::Error>>(())
346/// ```
347///
348/// This trait implementation is generic over calendars:
349///
350/// ```
351/// use jiff_icu::{ConvertTryFrom as _};
352///
353/// let icu_date = icu_calendar::Date::try_new_hebrew(5785, 5, 4).unwrap();
354/// let jiff_date = jiff::civil::Date::convert_try_from(icu_date)?;
355/// assert_eq!(jiff_date.to_string(), "2025-02-02");
356///
357/// # Ok::<(), Box<dyn std::error::Error>>(())
358/// ```
359impl<C: IcuAsCalendar> ConvertTryFrom<IcuDate<C>> for JiffDate {
360    type Error = Error;
361
362    fn convert_try_from(v: IcuDate<C>) -> Result<JiffDate, Error> {
363        let v = v.to_iso();
364        let year = v.extended_year();
365        let year = i16::try_from(year).map_err(|_| {
366            err!("failed to convert `icu` year of {year} to `i16`")
367        })?;
368
369        let month = v.month().ordinal;
370        let month = i8::try_from(month).map_err(|_| {
371            err!("failed to convert `icu` month of {month} to `i8`")
372        })?;
373
374        let day = v.day_of_month().0;
375        let day = i8::try_from(day).map_err(|_| {
376            err!("failed to convert `icu` day of {day} to `i8`")
377        })?;
378        Ok(JiffDate::new(year, month, day)?)
379    }
380}
381
382/// Converts from a [`jiff::civil::Date`] to a
383/// [`icu_calendar::Date<Iso>`](icu_calendar::Date).
384///
385/// # Examples
386///
387/// ```
388/// use jiff_icu::{ConvertFrom as _};
389///
390/// let jiff_date = jiff::civil::date(2025, 1, 30);
391/// let icu_date = icu_calendar::Date::convert_from(jiff_date);
392/// assert_eq!(
393///     format!("{icu_date:?}"),
394///     "Date(2025-1-30, default era, for calendar ISO)",
395/// );
396///
397/// let jiff_date = jiff::civil::Date::MIN;
398/// let icu_date = icu_calendar::Date::convert_from(jiff_date);
399/// assert_eq!(
400///     format!("{icu_date:?}"),
401///     "Date(-9999-1-1, default era, for calendar ISO)",
402/// );
403///
404/// let jiff_date = jiff::civil::Date::MAX;
405/// let icu_date = icu_calendar::Date::convert_from(jiff_date);
406/// assert_eq!(
407///     format!("{icu_date:?}"),
408///     "Date(9999-12-31, default era, for calendar ISO)",
409/// );
410///
411/// let jiff_date = jiff::civil::date(0, 1, 30);
412/// let icu_date = icu_calendar::Date::convert_from(jiff_date);
413/// assert_eq!(
414///     format!("{icu_date:?}"),
415///     "Date(0-1-30, default era, for calendar ISO)",
416/// );
417/// ```
418///
419/// While this trait implementation is not generic over calendars, one can
420/// convert an `icu_calendar::Date<Iso>` to a date in another calendar:
421///
422/// ```
423/// use jiff_icu::{ConvertFrom as _};
424///
425/// let jiff_date = jiff::civil::date(2025, 2, 2);
426/// let icu_iso_date = icu_calendar::Date::convert_from(jiff_date);
427/// let icu_hebrew_date = icu_iso_date.to_calendar(icu_calendar::cal::Hebrew);
428/// assert_eq!(
429///     format!("{icu_hebrew_date:?}"),
430///     "Date(5785-5-4, am era, for calendar Hebrew)",
431/// );
432/// ```
433impl ConvertFrom<JiffDate> for IcuDate<Iso> {
434    fn convert_from(v: JiffDate) -> IcuDate<Iso> {
435        let year = i32::from(v.year());
436        let month = v.month().unsigned_abs();
437        let day = v.day().unsigned_abs();
438        // All Jiff civil dates are valid ICU4X dates.
439        IcuDate::try_new_iso(year, month, day).unwrap()
440    }
441}
442
443/// Converts from a [`icu_time::Time`] to a [`jiff::civil::Time`].
444///
445/// # Examples
446///
447/// ```
448/// use jiff_icu::{ConvertTryFrom as _};
449///
450/// let icu_time = icu_time::Time::try_new(0, 0, 0, 0).unwrap();
451/// let jiff_time = jiff::civil::Time::convert_try_from(icu_time)?;
452/// assert_eq!(jiff_time, jiff::civil::time(0, 0, 0, 0));
453///
454/// let icu_time = icu_time::Time::try_new(23, 59, 59, 999_999_999).unwrap();
455/// let jiff_time = jiff::civil::Time::convert_try_from(icu_time)?;
456/// assert_eq!(jiff_time, jiff::civil::time(23, 59, 59, 999_999_999));
457///
458/// let icu_time = icu_time::Time::try_new(17, 59, 4, 0).unwrap();
459/// let jiff_time = jiff::civil::Time::convert_try_from(icu_time)?;
460/// assert_eq!(jiff_time, jiff::civil::time(17, 59, 4, 0));
461///
462/// # Ok::<(), Box<dyn std::error::Error>>(())
463/// ```
464#[cfg(feature = "time")]
465impl ConvertTryFrom<IcuTime> for JiffTime {
466    type Error = Error;
467
468    fn convert_try_from(v: IcuTime) -> Result<JiffTime, Error> {
469        // OK because it's guaranteed by API docs.
470        let mut hour = i8::try_from(v.hour.number()).unwrap();
471        if hour == 24 {
472            hour = 0;
473        }
474
475        // OK because it's guaranteed by API docs.
476        let minute = i8::try_from(v.minute.number()).unwrap();
477
478        // OK because it's guaranteed by API docs.
479        let second = i8::try_from(v.second.number()).unwrap();
480
481        // OK because it's guaranteed by API docs.
482        let subsec_nano = i32::try_from(v.subsecond.number()).unwrap();
483
484        Ok(JiffTime::new(hour, minute, second, subsec_nano)?)
485    }
486}
487
488/// Converts from a [`jiff::civil::Time`] to a [`icu_time::Time`].
489///
490/// # Examples
491///
492/// ```
493/// use jiff_icu::{ConvertFrom as _};
494///
495/// let jiff_time = jiff::civil::time(0, 0, 0, 0);
496/// let icu_time = icu_time::Time::convert_from(jiff_time);
497/// assert_eq!(
498///     format!("{icu_time:?}"),
499///     "Time { hour: Hour(0), minute: Minute(0), second: Second(0), subsecond: Nanosecond(0) }",
500/// );
501///
502/// let jiff_time = jiff::civil::time(17, 59, 4, 0);
503/// let icu_time = icu_time::Time::convert_from(jiff_time);
504/// assert_eq!(
505///     format!("{icu_time:?}"),
506///     "Time { hour: Hour(17), minute: Minute(59), second: Second(4), subsecond: Nanosecond(0) }",
507/// );
508/// ```
509#[cfg(feature = "time")]
510impl ConvertFrom<JiffTime> for IcuTime {
511    fn convert_from(v: JiffTime) -> IcuTime {
512        let hour = v.hour().unsigned_abs();
513        let minute = v.minute().unsigned_abs();
514        let second = v.second().unsigned_abs();
515        let subsec = v.subsec_nanosecond().unsigned_abs();
516        // All Jiff civil times are valid ICU4X times.
517        IcuTime::try_new(hour, minute, second, subsec).unwrap()
518    }
519}
520
521/// Converts from a [`icu_calendar::types::Weekday`] to a
522/// [`jiff::civil::Weekday`].
523///
524/// # Examples
525///
526/// ```
527/// use jiff_icu::{ConvertFrom as _};
528///
529/// let icu_weekday = icu_calendar::types::Weekday::Wednesday;
530/// let jiff_weekday = jiff::civil::Weekday::convert_from(icu_weekday);
531/// assert_eq!(jiff_weekday, jiff::civil::Weekday::Wednesday);
532/// ```
533impl ConvertFrom<IcuWeekday> for JiffWeekday {
534    fn convert_from(v: IcuWeekday) -> JiffWeekday {
535        match v {
536            IcuWeekday::Monday => JiffWeekday::Monday,
537            IcuWeekday::Tuesday => JiffWeekday::Tuesday,
538            IcuWeekday::Wednesday => JiffWeekday::Wednesday,
539            IcuWeekday::Thursday => JiffWeekday::Thursday,
540            IcuWeekday::Friday => JiffWeekday::Friday,
541            IcuWeekday::Saturday => JiffWeekday::Saturday,
542            IcuWeekday::Sunday => JiffWeekday::Sunday,
543        }
544    }
545}
546
547/// Converts from a [`jiff::civil::Weekday`] to a
548/// [`icu_calendar::types::Weekday`].
549///
550/// # Examples
551///
552/// ```
553/// use jiff_icu::{ConvertFrom as _};
554///
555/// let jiff_weekday = jiff::civil::Weekday::Wednesday;
556/// let icu_weekday = icu_calendar::types::Weekday::convert_from(jiff_weekday);
557/// assert_eq!(icu_weekday, icu_calendar::types::Weekday::Wednesday);
558/// ```
559impl ConvertFrom<JiffWeekday> for IcuWeekday {
560    fn convert_from(v: JiffWeekday) -> IcuWeekday {
561        match v {
562            JiffWeekday::Monday => IcuWeekday::Monday,
563            JiffWeekday::Tuesday => IcuWeekday::Tuesday,
564            JiffWeekday::Wednesday => IcuWeekday::Wednesday,
565            JiffWeekday::Thursday => IcuWeekday::Thursday,
566            JiffWeekday::Friday => IcuWeekday::Friday,
567            JiffWeekday::Saturday => IcuWeekday::Saturday,
568            JiffWeekday::Sunday => IcuWeekday::Sunday,
569        }
570    }
571}
572
573/// Converts from a [`jiff::tz::Offset`] to a [`icu_time::zone::UtcOffset`].
574///
575/// # Examples
576///
577/// ```
578/// use jiff_icu::{ConvertTryFrom as _};
579///
580/// let jiff_offset = jiff::tz::Offset::from_seconds(
581///     5 * 60 * 60 + 30 * 60,
582/// ).unwrap();
583/// let icu_tz = icu_time::zone::UtcOffset::convert_try_from(jiff_offset)?;
584/// assert_eq!(
585///     format!("{icu_tz:?}"),
586///     "UtcOffset(19800)",
587/// );
588///
589/// # Ok::<(), Box<dyn std::error::Error>>(())
590/// ```
591#[cfg(feature = "time")]
592impl ConvertTryFrom<JiffOffset> for IcuUtcOffset {
593    type Error = Error;
594
595    fn convert_try_from(v: JiffOffset) -> Result<IcuUtcOffset, Error> {
596        // I considered just rounding to the nearest eighth of a second,
597        // but Jiff's `Offset` supports a larger range than ICU4X's
598        // `UtcOffset`. In practice, this generally shouldn't matter, since
599        // all extant offsets will fit in ICU4X's `UtcOffset` anyway. But it's
600        // kinda hard to get a reasonable value in the case of failure.
601        IcuUtcOffset::try_from_seconds(v.seconds()).map_err(|err| {
602            err!(
603                "failed to convert Jiff UTC offset of \
604                 `{v}` to ICU4X offset: {err}",
605            )
606        })
607    }
608}
609
610/// Converts from a [`jiff::tz::TimeZone`] to a [`icu_time::TimeZone`].
611///
612/// # Examples
613///
614/// ```
615/// use jiff_icu::{ConvertFrom as _};
616///
617/// let jiff_tz = jiff::tz::TimeZone::get("America/New_York")?;
618/// let icu_tz = icu_time::TimeZone::convert_from(jiff_tz);
619/// assert_eq!(
620///     format!("{icu_tz:?}"),
621///     "TimeZone(Subtag(\"usnyc\"))",
622/// );
623///
624/// let jiff_tz = jiff::tz::TimeZone::get("us/eastern")?;
625/// let icu_tz = icu_time::TimeZone::convert_from(jiff_tz);
626/// assert_eq!(
627///     format!("{icu_tz:?}"),
628///     "TimeZone(Subtag(\"usnyc\"))",
629/// );
630///
631/// // If there's no IANA identifier for the Jiff time zone, then
632/// // this automatically results in an "unknown" ICU4X time zone:
633/// let jiff_tz = jiff::tz::TimeZone::fixed(jiff::tz::offset(-5));
634/// let icu_tz = icu_time::TimeZone::convert_from(jiff_tz);
635/// assert_eq!(
636///     format!("{icu_tz:?}"),
637///     "TimeZone(Subtag(\"unk\"))",
638/// );
639///
640/// // An explicitly unknown Jiff time zone is equivalent to an
641/// // unknown ICU4X time zone:
642/// let jiff_tz = jiff::tz::TimeZone::unknown();
643/// let icu_tz = icu_time::TimeZone::convert_from(jiff_tz);
644/// assert_eq!(
645///     format!("{icu_tz:?}"),
646///     "TimeZone(Subtag(\"unk\"))",
647/// );
648///
649/// # Ok::<(), Box<dyn std::error::Error>>(())
650/// ```
651#[cfg(feature = "zoned")]
652impl ConvertFrom<JiffTimeZone> for IcuTimeZone {
653    fn convert_from(v: JiffTimeZone) -> IcuTimeZone {
654        IcuTimeZone::convert_from(&v)
655    }
656}
657
658/// Converts from a [`&jiff::tz::TimeZone`](jiff::tz::TimeZone) to a
659/// [`icu_time::TimeZone`].
660#[cfg(feature = "zoned")]
661impl<'a> ConvertFrom<&'a JiffTimeZone> for IcuTimeZone {
662    fn convert_from(v: &'a JiffTimeZone) -> IcuTimeZone {
663        let Some(iana_name) = v.iana_name() else {
664            return IcuTimeZone::UNKNOWN;
665        };
666        icu_time::zone::iana::IanaParser::new().parse(iana_name)
667    }
668}
669
670/// Converts from a [`jiff::Zoned`] to a
671/// [`icu_time::ZonedDateTime<Iso, TimeZoneInfo<AtTime>>`](icu_time::ZonedDateTime).
672///
673/// # Examples
674///
675/// ```
676/// use icu_time::{TimeZoneInfo, zone::models::AtTime};
677/// use jiff_icu::{ConvertFrom as _};
678///
679/// let jiff_zdt = jiff::civil::date(2025, 1, 30)
680///     .at(17, 58, 30, 0)
681///     .in_tz("America/New_York")?;
682/// let icu_zdt = icu_time::ZonedDateTime::<_, TimeZoneInfo<AtTime>>::convert_from(&jiff_zdt);
683/// assert_eq!(
684///     format!("{:?}", icu_zdt.date),
685///     "Date(2025-1-30, default era, for calendar ISO)",
686/// );
687/// assert_eq!(
688///     format!("{:?}", icu_zdt.time),
689///     "Time { hour: Hour(17), minute: Minute(58), second: Second(30), subsecond: Nanosecond(0) }",
690/// );
691/// assert_eq!(
692///     format!("{:?}", (icu_zdt.zone.id(), icu_zdt.zone.offset())),
693///     "(TimeZone(Subtag(\"usnyc\")), Some(UtcOffset(-18000)))",
694/// );
695///
696/// # Ok::<(), Box<dyn std::error::Error>>(())
697/// ```
698#[cfg(feature = "zoned")]
699impl<'a> ConvertFrom<&'a JiffZoned>
700    for IcuZonedDateTime<Iso, IcuTimeZoneInfo<AtTime>>
701{
702    fn convert_from(
703        v: &'a JiffZoned,
704    ) -> IcuZonedDateTime<Iso, IcuTimeZoneInfo<AtTime>> {
705        let date = IcuDate::convert_from(v.date());
706        let time = IcuTime::convert_from(v.time());
707        let datetime = IcuDateTime { date, time };
708
709        let tz = IcuTimeZone::convert_from(v.time_zone());
710        let offset = IcuUtcOffset::convert_try_from(v.offset()).ok();
711        let tz_info_base = tz.with_offset(offset);
712        let zone = tz_info_base.at_date_time_iso(datetime);
713        IcuZonedDateTime { date, time, zone }
714    }
715}
716
717/// Converts from a [`jiff::Zoned`] to a
718/// [`icu_time::ZonedDateTime<Iso, TimeZoneInfo<Full>>`](icu_time::ZonedDateTime).
719///
720/// # Examples
721///
722/// ```
723/// use icu_time::{TimeZoneInfo, zone::models::Full};
724/// use jiff_icu::{ConvertFrom as _};
725///
726/// let jiff_zdt = jiff::civil::date(2025, 1, 30)
727///     .at(17, 58, 30, 0)
728///     .in_tz("America/New_York")?;
729/// let icu_zdt = icu_time::ZonedDateTime::<_, TimeZoneInfo<Full>>::convert_from(&jiff_zdt);
730/// assert_eq!(
731///     format!("{:?}", icu_zdt.date),
732///     "Date(2025-1-30, default era, for calendar ISO)",
733/// );
734/// assert_eq!(
735///     format!("{:?}", icu_zdt.time),
736///     "Time { hour: Hour(17), minute: Minute(58), second: Second(30), subsecond: Nanosecond(0) }",
737/// );
738/// assert_eq!(
739///     format!("{:?}", (icu_zdt.zone.id(), icu_zdt.zone.offset())),
740///     "(TimeZone(Subtag(\"usnyc\")), Some(UtcOffset(-18000)))",
741/// );
742///
743/// # Ok::<(), Box<dyn std::error::Error>>(())
744/// ```
745#[cfg(feature = "zoned")]
746#[allow(deprecated)]
747impl<'a> ConvertFrom<&'a JiffZoned>
748    for IcuZonedDateTime<Iso, IcuTimeZoneInfo<Full>>
749{
750    fn convert_from(
751        v: &'a JiffZoned,
752    ) -> IcuZonedDateTime<Iso, IcuTimeZoneInfo<Full>> {
753        let date = IcuDate::convert_from(v.date());
754        let time = IcuTime::convert_from(v.time());
755        let datetime = IcuDateTime { date, time };
756
757        let tz = IcuTimeZone::convert_from(v.time_zone());
758        let offset = IcuUtcOffset::convert_try_from(v.offset()).ok();
759        let dst = v.time_zone().to_offset_info(v.timestamp()).dst().is_dst();
760        let tz_info_base = tz.with_offset(offset);
761        let tz_info_at = tz_info_base.at_date_time_iso(datetime);
762        let zone = tz_info_at.with_variant(if dst {
763            TimeZoneVariant::Daylight
764        } else {
765            TimeZoneVariant::Standard
766        });
767        IcuZonedDateTime { date, time, zone }
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use jiff::ToSpan;
774
775    use super::*;
776
777    /// This exhaustively confirms that all valid Jiff dates are also valid
778    /// ICU4X dates.
779    ///
780    /// I believe the reverse is not true, although it's not quite clear from
781    /// ICU4X's docs. (Although I haven't done an exhaustive search.)
782    ///
783    /// This test is ignored because it takes forever in debug mode (sigh). In
784    /// release mode it is quite snappy. But I have run it. And the doc tests
785    /// above check the min and max values.
786    #[test]
787    #[ignore]
788    fn all_jiff_dates_are_valid_icu_dates() {
789        for jiff_date in jiff::civil::Date::MIN.series(1.day()) {
790            let icu_date: IcuDate<Iso> = jiff_date.convert_try_into().unwrap();
791            let got: jiff::civil::Date = icu_date.convert_try_into().unwrap();
792            assert_eq!(jiff_date, got);
793        }
794    }
795
796    /// Like the above, but for civil times.
797    ///
798    /// We skip nanoseconds because it would take too long to exhaustively
799    /// test. Without nanoseconds, this is quick enough to run in debug mode.
800    #[cfg(feature = "time")]
801    #[test]
802    fn all_jiff_times_are_valid_icu_times() {
803        for jiff_time in jiff::civil::Time::MIN.series(1.second()) {
804            let icu_time: IcuTime = jiff_time.convert_try_into().unwrap();
805            let got: jiff::civil::Time = icu_time.convert_try_into().unwrap();
806            assert_eq!(jiff_time, got);
807        }
808    }
809}