Skip to main content

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    // Start with a large enough capacity to avoid preflighting in almost all cases.
222    let mut capacity = 64; 
223    
224    // Use a loop to recover if the time zone length changes between preflight and fetch.
225    loop {
226        let mut status = common::Error::OK_CODE;
227        let mut uchar = ustring::UChar::new_with_capacity(capacity);
228        
229        let len = unsafe {
230            assert!(common::Error::is_ok(status));
231            versioned_function!(ucal_getDefaultTimeZone)(
232                uchar.as_mut_c_ptr(), 
233                capacity as i32, 
234                &mut status
235            )
236        } as usize;
237
238        if status == sys::UErrorCode::U_BUFFER_OVERFLOW_ERROR
239            || (common::Error::is_ok(status) && len > capacity)
240        {
241            capacity = len;
242            continue; // Buffer was too small. Resize to required `len` and retry.
243        }
244        
245        common::Error::ok_or_warning(status)?;
246        uchar.resize(len);
247        trace!("result: {:?}", uchar);
248        return String::try_from(&uchar);
249    }
250}
251
252/// Implements `ucal_getTZDataVersion`
253pub fn get_tz_data_version() -> Result<String, common::Error> {
254    let mut status = common::Error::OK_CODE;
255
256    let tz_data_version = unsafe {
257        let raw_cstring = versioned_function!(ucal_getTZDataVersion)(&mut status);
258        common::Error::ok_or_warning(status)?;
259        ffi::CStr::from_ptr(raw_cstring)
260            .to_string_lossy()
261            .into_owned()
262    };
263
264    Ok(tz_data_version)
265}
266
267/// Gets the current date and time, in milliseconds since the Epoch.
268///
269/// Implements `ucal_getNow`.
270pub fn get_now() -> f64 {
271    unsafe { versioned_function!(ucal_getNow)() as f64 }
272}
273
274/// Opens a list of available time zones for the given country.
275///
276/// Implements `ucal_openCountryTimeZones`.
277pub fn country_time_zones(country: &str) -> Result<uenum::Enumeration, common::Error> {
278    uenum::ucal_open_country_time_zones(country)
279}
280
281/// Opens a list of available time zone IDs with the given filters.
282///
283/// Implements `ucal_openTimeZoneIDEnumeration`
284pub fn time_zone_id_enumeration(
285    zone_type: sys::USystemTimeZoneType,
286    region: Option<&str>,
287    raw_offset: Option<i32>,
288) -> Result<uenum::Enumeration, common::Error> {
289    uenum::ucal_open_time_zone_id_enumeration(zone_type, region, raw_offset)
290}
291
292/// Opens a list of available time zones.
293///
294/// Implements `ucal_openTimeZones`
295pub fn time_zones() -> Result<uenum::Enumeration, common::Error> {
296    uenum::open_time_zones()
297}
298
299#[cfg(test)]
300mod tests {
301    use {
302        super::{UCalendar, *},
303        regex::Regex,
304        std::collections::HashSet,
305    };
306
307    #[test]
308    fn test_time_zones() {
309        let tz_iter = time_zones().expect("time zones opened");
310        assert_eq!(
311            tz_iter
312                .map(|r| { r.expect("time zone is available") })
313                .take(3)
314                .collect::<Vec<String>>(),
315            vec!["ACT", "AET", "AGT"]
316        );
317    }
318
319    #[test]
320    fn test_time_zone_id_enumeration_no_filters() {
321        let tz_iter =
322            time_zone_id_enumeration(sys::USystemTimeZoneType::UCAL_ZONE_TYPE_ANY, None, None)
323                .expect("time_zone_id_enumeration() opened");
324
325        let from_enumeration = tz_iter
326            .map(|r| r.expect("timezone is available"))
327            .collect::<Vec<String>>();
328
329        let from_time_zones = time_zones()
330            .expect("time_zones() opened")
331            .map(|r| r.expect("time zone is available"))
332            .collect::<Vec<String>>();
333
334        assert!(!from_time_zones.is_empty());
335
336        assert_eq!(from_enumeration, from_time_zones);
337    }
338
339    #[test]
340    fn test_time_zone_id_enumeration_by_type_region() {
341        let tz_iter = time_zone_id_enumeration(
342            sys::USystemTimeZoneType::UCAL_ZONE_TYPE_CANONICAL,
343            Some("us"),
344            None,
345        )
346        .expect("time_zone_id_enumeration() opened");
347        assert_eq!(
348            tz_iter
349                .map(|r| { r.expect("time zone is available") })
350                .take(3)
351                .collect::<Vec<String>>(),
352            vec!["America/Adak", "America/Anchorage", "America/Boise"]
353        );
354    }
355
356    #[test]
357    fn test_time_zone_id_enumeration_by_offset() {
358        let tz_iter = time_zone_id_enumeration(
359            sys::USystemTimeZoneType::UCAL_ZONE_TYPE_ANY,
360            None,
361            Some(0), /* GMT */
362        )
363        .expect("time_zone_id_enumeration() opened");
364        let tz_ids = tz_iter
365            .map(|r| r.expect("time zone is available"))
366            .collect::<HashSet<String>>();
367
368        assert!(tz_ids.contains("UTC"));
369        assert!(!tz_ids.contains("Etc/GMT-1"));
370    }
371
372    #[test]
373    fn test_country_time_zones() {
374        let tz_iter = country_time_zones("us").expect("time zones available");
375        assert_eq!(
376            tz_iter
377                .map(|r| { r.expect("time zone is available") })
378                .take(3)
379                .collect::<Vec<String>>(),
380            vec!["AST", "America/Adak", "America/Anchorage"]
381        );
382    }
383
384    #[test]
385    fn test_default_time_zone() {
386        super::set_default_time_zone("America/Adak").expect("time zone set with success");
387        assert_eq!(
388            super::get_default_time_zone().expect("time zone obtained"),
389            "America/Adak",
390        );
391    }
392
393    #[test]
394    fn test_get_tz_data_version() {
395        let re = Regex::new(r"^[0-9][0-9][0-9][0-9][a-z][a-z0-9]*$").expect("valid regex");
396        let tz_version = super::get_tz_data_version().expect("get_tz_data_version works");
397        assert!(re.is_match(&tz_version), "version was: {:?}", &tz_version);
398    }
399
400    #[test]
401    fn test_get_set_millis() -> Result<(), common::Error> {
402        let now = get_now();
403        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
404        // Assert that the times are basically the same.
405        // Let's assume that no more than 1 second might elapse between the execution of `get_now()`
406        // and `get_millis()`.
407        assert!((now - cal.get_millis()?).abs() <= 1000f64);
408
409        let arbitrary_delta_ms = 17.0;
410        let date = now + arbitrary_delta_ms;
411        cal.set_millis(date)?;
412        assert_eq!(cal.get_millis()?, date);
413        Ok(())
414    }
415
416    #[test]
417    fn test_set_date() -> Result<(), common::Error> {
418        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
419
420        // 2020-05-07T21:00:00.000-04:00
421        let time_a = 1588899600000f64;
422        // 2020-05-04T21:00:00.000-04:00
423        let time_b = 1588640400000f64;
424
425        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
426        cal.set_millis(time_a)?;
427        cal.set_date(2020, UCalendarMonths::UCAL_MAY as i32, 4)?;
428        assert_eq!(cal.get_millis()?, time_b);
429
430        Ok(())
431    }
432
433    #[test]
434    fn test_set_date_time() -> Result<(), common::Error> {
435        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
436
437        // 2020-05-07T21:26:55.898-04:00
438        let time_a = 1588901215898f64;
439        // 2020-05-04T21:00:00.898-04:00
440        let time_b = 1588640400898f64;
441
442        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
443        cal.set_millis(time_a)?;
444        cal.set_date_time(2020, UCalendarMonths::UCAL_MAY as i32, 4, 21, 0, 0)?;
445
446        assert_eq!(cal.get_millis()?, time_b);
447
448        Ok(())
449    }
450
451    #[test]
452    fn test_get() -> Result<(), common::Error> {
453        // Timestamps hard-coded, not parsed, to avoid cyclic dependency on udat.
454
455        // 2020-05-07T21:26:55.898-04:00
456        let date_time = 1588901215898f64;
457
458        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
459        cal.set_millis(date_time)?;
460
461        assert_eq!(cal.get(UCalendarDateFields::UCAL_DAY_OF_MONTH)?, 7);
462
463        assert_eq!(cal.get(UCalendarDateFields::UCAL_MILLISECOND)?, 898);
464
465        Ok(())
466    }
467
468    #[test]
469    fn test_offsets_and_daylight_time() -> Result<(), common::Error> {
470        let mut cal = UCalendar::new("America/New_York", "en-US", UCalendarType::UCAL_GREGORIAN)?;
471
472        // -5 hours
473        let expected_zone_offset_ms: i32 = -5 * 60 * 60 * 1000;
474        // + 1 hour
475        let expected_dst_offset_ms: i32 = 1 * 60 * 60 * 1000;
476
477        cal.set_date_time(2020, UCalendarMonths::UCAL_MAY as i32, 7, 21, 0, 0)?;
478        assert_eq!(cal.get_zone_offset()?, expected_zone_offset_ms);
479        assert_eq!(cal.get_dst_offset()?, expected_dst_offset_ms);
480        assert!(cal.in_daylight_time()?);
481
482        // -5 hours
483        let expected_zone_offset: i32 = -5 * 60 * 60 * 1000;
484        // No offset
485        let expected_dst_offset: i32 = 0;
486
487        cal.set_date_time(2020, UCalendarMonths::UCAL_JANUARY as i32, 15, 12, 0, 0)?;
488        assert_eq!(cal.get_zone_offset()?, expected_zone_offset);
489        assert_eq!(cal.get_dst_offset()?, expected_dst_offset);
490        assert!(!cal.in_daylight_time()?);
491
492        Ok(())
493    }
494}