rust_icu_ucal/
lib.rs

1// Copyright 2019 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Implementation of `ucal.h`.
16//!
17//! As a general piece of advice, since a lot of documentation is currently elided,
18//! see the unit tests for example uses of each of the wrapper functions.
19
20use {
21    log::trace, rust_icu_common as common, rust_icu_sys as sys, rust_icu_sys::versioned_function,
22    rust_icu_sys::*, rust_icu_uenum as uenum, rust_icu_ustring as ustring, std::convert::TryFrom,
23    std::ffi,
24};
25
26/// Implements the UCalendar type from `ucal.h`.
27///
28/// The naming `rust_icu_ucal::UCalendar` is a bit repetetetitive, but makes it
29/// a bit more obvious what ICU type it is wrapping.
30#[derive(Debug)]
31pub struct UCalendar {
32    // Internal representation of the UCalendar, a pointer to a C type from ICU.
33    //
34    // The representation is owned by this type, and must be deallocated by calling
35    // `sys::ucal_close`.
36    rep: *mut sys::UCalendar,
37}
38
39impl Drop for UCalendar {
40    /// Deallocates the internal representation of UCalendar.
41    ///
42    /// Implements `ucal_close`.
43    fn drop(&mut self) {
44        unsafe {
45            versioned_function!(ucal_close)(self.rep);
46        };
47    }
48}
49
50impl UCalendar {
51    /// Creates a new UCalendar from a `UChar` zone ID.
52    ///
53    /// Use `new` to construct this from rust types only.
54    fn new_from_uchar(
55        zone_id: &ustring::UChar,
56        locale: &str,
57        cal_type: sys::UCalendarType,
58    ) -> Result<UCalendar, common::Error> {
59        let mut status = common::Error::OK_CODE;
60        let asciiz_locale = ffi::CString::new(locale).map_err(common::Error::wrapper)?;
61        // Requires that zone_id contains a valid Unicode character representation with known
62        // beginning and length.  asciiz_locale must be a pointer to a valid C string.  The first
63        // condition is assumed to be satisfied by ustring::UChar, and the second should be
64        // satisfied by construction of asciiz_locale just above.
65        let raw_ucal = unsafe {
66            versioned_function!(ucal_open)(
67                zone_id.as_c_ptr(),
68                zone_id.len() as i32,
69                asciiz_locale.as_ptr(),
70                cal_type,
71                &mut status,
72            ) as *mut sys::UCalendar
73        };
74        common::Error::ok_or_warning(status)?;
75        Ok(UCalendar { rep: raw_ucal })
76    }
77
78    /// Creates a new UCalendar.
79    ///
80    /// Implements `ucal_open`.
81    pub fn new(
82        zone_id: &str,
83        locale: &str,
84        cal_type: sys::UCalendarType,
85    ) -> Result<UCalendar, common::Error> {
86        let zone_id_uchar = ustring::UChar::try_from(zone_id)?;
87        Self::new_from_uchar(&zone_id_uchar, locale, cal_type)
88    }
89    /// Returns this UCalendar's internal C representation.  Use only for interfacing with the C
90    /// low-level API.
91    pub fn as_c_calendar(&self) -> *const sys::UCalendar {
92        self.rep
93    }
94
95    /// Sets the calendar's current date/time in milliseconds since the epoch.
96    ///
97    /// Implements `ucal_setMillis`.
98    pub fn set_millis(&mut self, date_time: sys::UDate) -> Result<(), common::Error> {
99        let mut status = common::Error::OK_CODE;
100        unsafe {
101            versioned_function!(ucal_setMillis)(self.rep, date_time, &mut status);
102        };
103        common::Error::ok_or_warning(status)
104    }
105
106    /// Gets the calendar's current date/time in milliseconds since the epoch.
107    ///
108    /// Implements `ucal_getMillis`.
109    pub fn get_millis(&self) -> Result<sys::UDate, common::Error> {
110        let mut status = common::Error::OK_CODE;
111        let millis = unsafe { versioned_function!(ucal_getMillis)(self.rep, &mut status) };
112        common::Error::ok_or_warning(status)?;
113        Ok(millis)
114    }
115
116    /// Sets the calendar's current date in the calendar's local time zone.
117    ///
118    /// Note that `month` is 0-based.
119    ///
120    /// Implements `ucal_setDate`.
121    pub fn set_date(&mut self, year: i32, month: i32, date: i32) -> Result<(), common::Error> {
122        let mut status = common::Error::OK_CODE;
123        unsafe {
124            versioned_function!(ucal_setDate)(self.rep, year, month, date, &mut status);
125        }
126        common::Error::ok_or_warning(status)?;
127        Ok(())
128    }
129
130    /// Sets the calendar's current date and time in the calendar's local time zone.
131    ///
132    /// Note that `month` is 0-based.
133    ///
134    /// Implements `ucal_setDateTime`.
135    pub fn set_date_time(
136        &mut self,
137        year: i32,
138        month: i32,
139        date: i32,
140        hour: i32,
141        minute: i32,
142        second: i32,
143    ) -> Result<(), common::Error> {
144        let mut status = common::Error::OK_CODE;
145        unsafe {
146            versioned_function!(ucal_setDateTime)(
147                self.rep,
148                year,
149                month,
150                date,
151                hour,
152                minute,
153                second,
154                &mut status,
155            );
156        }
157        common::Error::ok_or_warning(status)?;
158        Ok(())
159    }
160
161    /// Returns the calendar's time zone's offset from UTC in milliseconds, for the calendar's
162    /// current date/time.
163    ///
164    /// This does not include the daylight savings offset, if any. Note that the calendar's current
165    /// date/time is significant because time zones are occasionally redefined -- a time zone that
166    /// has a +16.5 hour offset today might have had a +17 hour offset a decade ago.
167    ///
168    /// Wraps `ucal_get` for `UCAL_ZONE_OFFSET`.
169    pub fn get_zone_offset(&self) -> Result<i32, common::Error> {
170        self.get(UCalendarDateFields::UCAL_ZONE_OFFSET)
171    }
172
173    /// Returns the calendar's daylight savings offset from its non-DST time, in milliseconds, for
174    /// the calendar's current date/time. This may be 0 if the time zone does not observe DST at
175    /// all, or if the time zone is not in the daylight savings period at the calendar's current
176    /// date/time.
177    ///
178    /// Wraps `ucal_get` for `UCAL_ZONE_DST_OFFSET`.
179    pub fn get_dst_offset(&self) -> Result<i32, common::Error> {
180        self.get(UCalendarDateFields::UCAL_DST_OFFSET)
181    }
182
183    /// Returns true if the calendar is currently in daylight savings / summer time.
184    ///
185    /// Implements `ucal_inDaylightTime`.
186    pub fn in_daylight_time(&self) -> Result<bool, common::Error> {
187        let mut status = common::Error::OK_CODE;
188        let in_daylight_time: sys::UBool =
189            unsafe { versioned_function!(ucal_inDaylightTime)(self.as_c_calendar(), &mut status) };
190        common::Error::ok_or_warning(status)?;
191        Ok(in_daylight_time != 0)
192    }
193
194    /// Implements `ucal_get`.
195    ///
196    /// Consider using specific higher-level methods instead.
197    pub fn get(&self, field: UCalendarDateFields) -> Result<i32, common::Error> {
198        let mut status: UErrorCode = common::Error::OK_CODE;
199        let value =
200            unsafe { versioned_function!(ucal_get)(self.as_c_calendar(), field, &mut status) };
201        common::Error::ok_or_warning(status)?;
202        Ok(value)
203    }
204}
205
206/// Implements `ucal_setDefaultTimeZone`
207pub fn set_default_time_zone(zone_id: &str) -> Result<(), common::Error> {
208    let mut status = common::Error::OK_CODE;
209    let mut zone_id_uchar = ustring::UChar::try_from(zone_id)?;
210    zone_id_uchar.make_z();
211    // Requires zone_id_uchar to be a valid pointer until the function returns.
212    unsafe {
213        assert!(common::Error::is_ok(status));
214        versioned_function!(ucal_setDefaultTimeZone)(zone_id_uchar.as_c_ptr(), &mut status);
215    };
216    common::Error::ok_or_warning(status)
217}
218
219/// Implements `ucal_getDefaultTimeZone`
220pub fn get_default_time_zone() -> Result<String, common::Error> {
221    let mut status = common::Error::OK_CODE;
222
223    // Preflight the time zone first.
224    let time_zone_length = unsafe {
225        assert!(common::Error::is_ok(status));
226        versioned_function!(ucal_getDefaultTimeZone)(std::ptr::null_mut(), 0, &mut status)
227    } as usize;
228    common::Error::ok_preflight(status)?;
229
230    // Should this capacity include the terminating \u{0}?
231    let mut status = common::Error::OK_CODE;
232    let mut uchar = ustring::UChar::new_with_capacity(time_zone_length);
233    trace!("length: {}", time_zone_length);
234
235    // Requires that uchar is a valid buffer.  Should be guaranteed by the constructor above.
236    unsafe {
237        assert!(common::Error::is_ok(status));
238        versioned_function!(ucal_getDefaultTimeZone)(
239            uchar.as_mut_c_ptr(),
240            time_zone_length as i32,
241            &mut status,
242        )
243    };
244    common::Error::ok_or_warning(status)?;
245    trace!("result: {:?}", uchar);
246    String::try_from(&uchar)
247}
248
249/// Implements `ucal_getTZDataVersion`
250pub fn get_tz_data_version() -> Result<String, common::Error> {
251    let mut status = common::Error::OK_CODE;
252
253    let tz_data_version = unsafe {
254        let raw_cstring = versioned_function!(ucal_getTZDataVersion)(&mut status);
255        common::Error::ok_or_warning(status)?;
256        ffi::CStr::from_ptr(raw_cstring)
257            .to_string_lossy()
258            .into_owned()
259    };
260
261    Ok(tz_data_version)
262}
263
264/// Gets the current date and time, in milliseconds since the Epoch.
265///
266/// Implements `ucal_getNow`.
267pub fn get_now() -> f64 {
268    unsafe { versioned_function!(ucal_getNow)() as f64 }
269}
270
271/// Opens a list of available time zones for the given country.
272///
273/// Implements `ucal_openCountryTimeZones`.
274pub fn country_time_zones(country: &str) -> Result<uenum::Enumeration, common::Error> {
275    uenum::ucal_open_country_time_zones(country)
276}
277
278/// Opens a list of available time zone IDs with the given filters.
279///
280/// Implements `ucal_openTimeZoneIDEnumeration`
281pub fn time_zone_id_enumeration(
282    zone_type: sys::USystemTimeZoneType,
283    region: Option<&str>,
284    raw_offset: Option<i32>,
285) -> Result<uenum::Enumeration, common::Error> {
286    uenum::ucal_open_time_zone_id_enumeration(zone_type, region, raw_offset)
287}
288
289/// Opens a list of available time zones.
290///
291/// Implements `ucal_openTimeZones`
292pub fn time_zones() -> Result<uenum::Enumeration, common::Error> {
293    uenum::open_time_zones()
294}
295
296#[cfg(test)]
297mod tests {
298    use {
299        super::{UCalendar, *},
300        regex::Regex,
301        std::collections::HashSet,
302    };
303
304    #[test]
305    fn test_time_zones() {
306        let tz_iter = time_zones().expect("time zones opened");
307        assert_eq!(
308            tz_iter
309                .map(|r| { r.expect("time zone is available") })
310                .take(3)
311                .collect::<Vec<String>>(),
312            vec!["ACT", "AET", "AGT"]
313        );
314    }
315
316    #[test]
317    fn test_time_zone_id_enumeration_no_filters() {
318        let tz_iter =
319            time_zone_id_enumeration(sys::USystemTimeZoneType::UCAL_ZONE_TYPE_ANY, None, None)
320                .expect("time_zone_id_enumeration() opened");
321
322        let from_enumeration = tz_iter
323            .map(|r| r.expect("timezone is available"))
324            .collect::<Vec<String>>();
325
326        let from_time_zones = time_zones()
327            .expect("time_zones() opened")
328            .map(|r| r.expect("time zone is available"))
329            .collect::<Vec<String>>();
330
331        assert!(!from_time_zones.is_empty());
332
333        assert_eq!(from_enumeration, from_time_zones);
334    }
335
336    #[test]
337    fn test_time_zone_id_enumeration_by_type_region() {
338        let tz_iter = time_zone_id_enumeration(
339            sys::USystemTimeZoneType::UCAL_ZONE_TYPE_CANONICAL,
340            Some("us"),
341            None,
342        )
343        .expect("time_zone_id_enumeration() opened");
344        assert_eq!(
345            tz_iter
346                .map(|r| { r.expect("time zone is available") })
347                .take(3)
348                .collect::<Vec<String>>(),
349            vec!["America/Adak", "America/Anchorage", "America/Boise"]
350        );
351    }
352
353    #[test]
354    fn test_time_zone_id_enumeration_by_offset() {
355        let tz_iter = time_zone_id_enumeration(
356            sys::USystemTimeZoneType::UCAL_ZONE_TYPE_ANY,
357            None,
358            Some(0), /* GMT */
359        )
360        .expect("time_zone_id_enumeration() opened");
361        let tz_ids = tz_iter
362            .map(|r| r.expect("time zone is available"))
363            .collect::<HashSet<String>>();
364
365        assert!(tz_ids.contains("UTC"));
366        assert!(!tz_ids.contains("Etc/GMT-1"));
367    }
368
369    #[test]
370    fn test_country_time_zones() {
371        let tz_iter = country_time_zones("us").expect("time zones available");
372        assert_eq!(
373            tz_iter
374                .map(|r| { r.expect("time zone is available") })
375                .take(3)
376                .collect::<Vec<String>>(),
377            vec!["AST", "America/Adak", "America/Anchorage"]
378        );
379    }
380
381    #[test]
382    fn test_default_time_zone() {
383        super::set_default_time_zone("America/Adak").expect("time zone set with success");
384        assert_eq!(
385            super::get_default_time_zone().expect("time zone obtained"),
386            "America/Adak",
387        );
388    }
389
390    #[test]
391    fn test_get_tz_data_version() {
392        let re = Regex::new(r"^[0-9][0-9][0-9][0-9][a-z][a-z0-9]*$").expect("valid regex");
393        let tz_version = super::get_tz_data_version().expect("get_tz_data_version works");
394        assert!(re.is_match(&tz_version), "version was: {:?}", &tz_version);
395    }
396
397    #[test]
398    fn test_get_set_millis() -> Result<(), common::Error> {
399        let now = get_now();
400        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
401        // Assert that the times are basically the same.
402        // Let's assume that no more than 1 second might elapse between the execution of `get_now()`
403        // and `get_millis()`.
404        assert!((now - cal.get_millis()?).abs() <= 1000f64);
405
406        let arbitrary_delta_ms = 17.0;
407        let date = now + arbitrary_delta_ms;
408        cal.set_millis(date)?;
409        assert_eq!(cal.get_millis()?, date);
410        Ok(())
411    }
412
413    #[test]
414    fn test_set_date() -> Result<(), common::Error> {
415        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
416
417        // 2020-05-07T21:00:00.000-04:00
418        let time_a = 1588899600000f64;
419        // 2020-05-04T21:00:00.000-04:00
420        let time_b = 1588640400000f64;
421
422        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
423        cal.set_millis(time_a)?;
424        cal.set_date(2020, UCalendarMonths::UCAL_MAY as i32, 4)?;
425        assert_eq!(cal.get_millis()?, time_b);
426
427        Ok(())
428    }
429
430    #[test]
431    fn test_set_date_time() -> Result<(), common::Error> {
432        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
433
434        // 2020-05-07T21:26:55.898-04:00
435        let time_a = 1588901215898f64;
436        // 2020-05-04T21:00:00.898-04:00
437        let time_b = 1588640400898f64;
438
439        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
440        cal.set_millis(time_a)?;
441        cal.set_date_time(2020, UCalendarMonths::UCAL_MAY as i32, 4, 21, 0, 0)?;
442
443        assert_eq!(cal.get_millis()?, time_b);
444
445        Ok(())
446    }
447
448    #[test]
449    fn test_get() -> Result<(), common::Error> {
450        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
451
452        // 2020-05-07T21:26:55.898-04:00
453        let date_time = 1588901215898f64;
454
455        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
456        cal.set_millis(date_time)?;
457
458        assert_eq!(cal.get(UCalendarDateFields::UCAL_DAY_OF_MONTH)?, 7);
459
460        assert_eq!(cal.get(UCalendarDateFields::UCAL_MILLISECOND)?, 898);
461
462        Ok(())
463    }
464
465    #[test]
466    fn test_offsets_and_daylight_time() -> Result<(), common::Error> {
467        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
468
469        // -5 hours
470        let expected_zone_offset_ms: i32 = -5 * 60 * 60 * 1000;
471        // + 1 hour
472        let expected_dst_offset_ms: i32 = 1 * 60 * 60 * 1000;
473
474        cal.set_date_time(2020, UCalendarMonths::UCAL_MAY as i32, 7, 21, 0, 0)?;
475        assert_eq!(cal.get_zone_offset()?, expected_zone_offset_ms);
476        assert_eq!(cal.get_dst_offset()?, expected_dst_offset_ms);
477        assert!(cal.in_daylight_time()?);
478
479        // -5 hours
480        let expected_zone_offset: i32 = -5 * 60 * 60 * 1000;
481        // No offset
482        let expected_dst_offset: i32 = 0;
483
484        cal.set_date_time(2020, UCalendarMonths::UCAL_JANUARY as i32, 15, 12, 0, 0)?;
485        assert_eq!(cal.get_zone_offset()?, expected_zone_offset);
486        assert_eq!(cal.get_dst_offset()?, expected_dst_offset);
487        assert!(!cal.in_daylight_time()?);
488
489        Ok(())
490    }
491}