Skip to main content

android_chrono_tz/
lib.rs

1// Copyright 2026 The android-chrono-tz Authors.
2// This project is dual-licensed under Apache 2.0 and MIT terms.
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! This crates provides [`Local`], a chrono `TimeZone` implementation to correctly fetch the local
6//! timezone on Android, using the `localtime_rz` and `mktime_z` functions from Bionic.
7//!
8//! Unlike `localtime` and `mktime` these functions are fully thread-safe. They were added in
9//! Android 15 (API level 35) so won't work in earlier Android versions. If you're using this crate
10//! in an Android app then ensure your `minSdk` is set to 35 or higher.
11//!
12//! It also provides [`Tz`] to use arbitrary timezones from the system timezone database, by Olson
13//! ID.
14
15mod inner;
16mod local;
17mod sys;
18
19use crate::inner::TzInner;
20pub use crate::local::Local;
21use chrono::{FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone};
22use libc::time_t;
23use std::{
24    ffi::NulError,
25    fmt::{self, Debug, Display, Formatter},
26    io,
27    sync::Arc,
28};
29use thiserror::Error;
30
31/// An error getting a timezone from the system database.
32#[derive(Debug, Error)]
33pub enum TzError {
34    /// The given timezone ID string contained a NUL byte.
35    #[error("Invalid string for timezone ID: {0}")]
36    InvalidId(#[from] NulError),
37    /// There was an error allocating a timezone with the given ID.
38    #[error("Error allocating timezone: {0}")]
39    Io(#[from] io::Error),
40}
41
42/// An offset for a timezone from the system database.
43#[derive(Clone, Debug)]
44pub struct TzOffset {
45    timezone: Arc<TzInner>,
46    time: time_t,
47}
48
49impl TzOffset {
50    /// Returns the name of the timezone offset.
51    pub fn name(&self) -> &str {
52        self.timezone.name_at_utc_timestamp(self.time)
53    }
54}
55
56impl Display for TzOffset {
57    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
58        f.write_str(self.name())
59    }
60}
61
62impl Offset for TzOffset {
63    fn fix(&self) -> FixedOffset {
64        self.timezone.offset_at_utc_timestamp(self.time)
65    }
66}
67
68/// A named timezone.
69#[derive(Clone, Debug)]
70pub struct Tz {
71    timezone: Arc<TzInner>,
72}
73
74impl Tz {
75    /// Returns a new `Tz` for the timezone with the given Olson ID.
76    pub fn new(olson_id: &str) -> Result<Self, TzError> {
77        Ok(Self {
78            timezone: Arc::new(TzInner::new(olson_id)?),
79        })
80    }
81
82    /// Returns a new `Tz` for the current local system timezone as seen in Settings, from the
83    /// `persist.sys.timezone` property.
84    ///
85    /// Note that this ignores the `TZ` environment variable.
86    pub fn local() -> Result<Self, TzError> {
87        Ok(Self {
88            timezone: Arc::new(TzInner::local()?),
89        })
90    }
91}
92
93impl TimeZone for Tz {
94    type Offset = TzOffset;
95
96    fn from_offset(offset: &TzOffset) -> Self {
97        Self {
98            timezone: offset.timezone.clone(),
99        }
100    }
101
102    fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<TzOffset> {
103        self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN))
104    }
105
106    fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<TzOffset> {
107        self.timezone
108            .timestamp_from_local_datetime(local)
109            .map(|time| TzOffset {
110                timezone: self.timezone.clone(),
111                time,
112            })
113    }
114
115    fn offset_from_utc_date(&self, utc: &NaiveDate) -> TzOffset {
116        self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN))
117    }
118
119    fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> TzOffset {
120        TzOffset {
121            timezone: self.timezone.clone(),
122            time: utc.and_utc().timestamp() as time_t,
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn london_names() {
133        let london = Tz::new("Europe/London").unwrap();
134
135        let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
136        assert_eq!(winter.offset().name(), "GMT");
137
138        let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
139        assert_eq!(summer.offset().name(), "BST");
140    }
141
142    #[test]
143    fn london_offsets() {
144        let london = Tz::new("Europe/London").unwrap();
145
146        let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
147        assert_eq!(winter.offset().fix(), FixedOffset::east_opt(0).unwrap());
148
149        let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
150        assert_eq!(summer.offset().fix(), FixedOffset::east_opt(3600).unwrap());
151    }
152
153    #[test]
154    fn dst_boundaries() {
155        let london = Tz::new("Europe/London").unwrap();
156
157        // Start of BST.
158        let start_local = NaiveDate::from_ymd_opt(2026, 3, 29)
159            .unwrap()
160            .and_hms_opt(1, 30, 0)
161            .unwrap();
162        assert_eq!(
163            london
164                .offset_from_local_datetime(&start_local)
165                .map(|offset| offset.fix()),
166            MappedLocalTime::None
167        );
168
169        // End of BST.
170        let end_local = NaiveDate::from_ymd_opt(2026, 10, 25)
171            .unwrap()
172            .and_hms_opt(1, 30, 0)
173            .unwrap();
174        assert_eq!(
175            london
176                .offset_from_local_datetime(&end_local)
177                .map(|offset| offset.fix()),
178            MappedLocalTime::Ambiguous(
179                FixedOffset::east_opt(0).unwrap(),
180                FixedOffset::east_opt(3600).unwrap()
181            )
182        );
183    }
184
185    #[test]
186    fn offsets_from_utc() {
187        let london = Tz::new("Europe/London").unwrap();
188
189        let winter = NaiveDate::from_ymd_opt(2026, 3, 1)
190            .unwrap()
191            .and_hms_opt(0, 0, 0)
192            .unwrap();
193        assert_eq!(
194            london.offset_from_utc_datetime(&winter).fix(),
195            FixedOffset::east_opt(0).unwrap()
196        );
197
198        let summer = NaiveDate::from_ymd_opt(2026, 4, 1)
199            .unwrap()
200            .and_hms_opt(0, 0, 0)
201            .unwrap();
202        assert_eq!(
203            london.offset_from_utc_datetime(&summer).fix(),
204            FixedOffset::east_opt(3600).unwrap()
205        );
206    }
207
208    #[test]
209    fn local_same() {
210        let local = Local.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
211        let local_from_tz = Tz::local()
212            .unwrap()
213            .with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
214            .unwrap();
215        assert_eq!(local, local_from_tz);
216    }
217}