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}