time_tz/posix_tz/
mod.rs

1// Copyright (c) 2022, Yuri6037
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13// * Neither the name of time-tz nor the names of its contributors
14// may be used to endorse or promote products derived from this software
15// without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29use crate::{Offset, TimeZone, Tz};
30use std::fmt::{Display, Formatter};
31use thiserror::Error;
32use time::{OffsetDateTime, UtcOffset};
33
34mod r#abstract;
35mod intermediate;
36mod parser;
37
38/// A range error returned when a field is out of the range defined in POSIX.
39#[derive(Debug)]
40pub enum RangeError {
41    /// One of the time field in the given string was out of range.
42    Time,
43
44    /// One of the date field in the given string was out of range.
45    Date,
46}
47
48impl Display for RangeError {
49    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50        match self {
51            RangeError::Time => f.write_str("time field out of range"),
52            RangeError::Date => f.write_str("date field out of range"),
53        }
54    }
55}
56
57/// The main type of error that is returned when a TZ POSIX string fails to parse.
58#[derive(Debug, Error)]
59pub enum ParseError<'a> {
60    /// A nom parsing error.
61    #[error("nom error: {:?}", .0)]
62    Nom(nom::error::ErrorKind),
63
64    /// In case a short format was given, the POSIX standard doesn't define what to do,
65    /// in this implementation we just try to match the first tzdb timezone containing the
66    /// short name; if none could be found this error variant is returned.
67    #[error("unknown short timezone name `{0}`")]
68    UnknownName(&'a str),
69
70    /// We've exceeded the range of a field when checking for conformance against the POSIX
71    /// standard.
72    #[error("range error: {0}")]
73    Range(RangeError),
74
75    /// We've exceeded the range of a date component when converting types to time-rs.
76    #[error("time component range error: {0}")]
77    ComponentRange(time::error::ComponentRange),
78}
79
80/// The type of error return when a given Offset/PrimitiveDateTime cannot be represented
81/// in a given POSIX TZ formatted "timezone".
82#[derive(Debug, Error)]
83pub enum Error {
84    /// We've exceeded the range of a date component when converting types to time-rs.
85    #[error("time component range error: {0}")]
86    ComponentRange(time::error::ComponentRange),
87
88    /// We've exceeded the maximum date supported by time-rs.
89    #[error("value of Date too large")]
90    DateTooLarge,
91}
92
93/// A POSIX "timezone" offset.
94pub struct PosixTzOffset<'a> {
95    inner: r#abstract::TzOrExpandedOffset<'a>,
96}
97
98impl<'a> Offset for PosixTzOffset<'a> {
99    fn to_utc(&self) -> UtcOffset {
100        match &self.inner {
101            r#abstract::TzOrExpandedOffset::Expanded(v) => v.to_utc(),
102            r#abstract::TzOrExpandedOffset::Tz(v) => v.to_utc(),
103        }
104    }
105
106    fn name(&self) -> &str {
107        match &self.inner {
108            r#abstract::TzOrExpandedOffset::Expanded(v) => v.name(),
109            r#abstract::TzOrExpandedOffset::Tz(v) => v.name(),
110        }
111    }
112
113    fn is_dst(&self) -> bool {
114        match &self.inner {
115            r#abstract::TzOrExpandedOffset::Expanded(v) => v.is_dst(),
116            r#abstract::TzOrExpandedOffset::Tz(v) => v.is_dst(),
117        }
118    }
119}
120
121/// A "timezone" in POSIX TZ format.
122pub struct PosixTz<'a> {
123    inner: r#abstract::TzOrExpanded<'a>,
124}
125
126impl<'a> PosixTz<'a> {
127    /// Parse the given POSIX TZ string.
128    ///
129    /// # Arguments
130    ///
131    /// * `input`: the string to parse.
132    ///
133    /// returns: Result<PosixTz, ParseError>
134    ///
135    /// # Errors
136    ///
137    /// Returns a [ParseError](crate::posix_tz::ParseError) if the given string is not a valid
138    /// POSIX "timezone".
139    pub fn parse(input: &'a str) -> Result<PosixTz<'a>, ParseError> {
140        let intermediate = intermediate::parse_intermediate(input)?;
141        let inner = r#abstract::parse_abstract(intermediate)?;
142        Ok(PosixTz { inner })
143    }
144
145    /// Convert the given date_time to this "timezone".
146    ///
147    /// # Arguments
148    ///
149    /// * `date_time`: the date time to convert.
150    ///
151    /// returns: Result<OffsetDateTime, Error>
152    ///
153    /// # Errors
154    ///
155    /// Returns an [Error](crate::posix_tz::Error) if the date_time cannot be represented in this
156    /// "timezone".
157    pub fn convert(&self, date_time: &OffsetDateTime) -> Result<OffsetDateTime, Error> {
158        let offset = self.get_offset(date_time)?;
159        Ok(date_time.to_offset(offset.to_utc()))
160    }
161
162    /// Calculates the offset to add to the given date_time to convert it to this "timezone".
163    ///
164    /// # Arguments
165    ///
166    /// * `date_time`: the date time to calculate offset of.
167    ///
168    /// returns: Result<OffsetDateTime, Error>
169    ///
170    /// # Errors
171    ///
172    /// Returns an [Error](crate::posix_tz::Error) if the date_time cannot be represented in this
173    /// "timezone".
174    pub fn get_offset(&self, date_time: &OffsetDateTime) -> Result<PosixTzOffset<'a>, Error> {
175        Ok(match &self.inner {
176            r#abstract::TzOrExpanded::Tz(v) => PosixTzOffset {
177                inner: r#abstract::TzOrExpandedOffset::Tz(v.get_offset_utc(date_time)),
178            },
179            r#abstract::TzOrExpanded::Expanded(v) => PosixTzOffset {
180                inner: r#abstract::TzOrExpandedOffset::Expanded(v.get_offset_utc(date_time)?),
181            },
182        })
183    }
184
185    /// Gets the current time in this "timezone".
186    ///
187    /// returns: Result<OffsetDateTime, Error>
188    ///
189    /// # Errors
190    ///
191    /// Returns an [Error](crate::posix_tz::Error) if the current time cannot be represented in
192    /// this "timezone".
193    pub fn now(&self) -> Result<OffsetDateTime, Error> {
194        self.convert(&OffsetDateTime::now_utc())
195    }
196
197    /// Returns the precise IANA TimeZone associated to this POSIX "timezone".
198    ///
199    /// If this POSIX "timezone" is not a precise IANA TimeZone, None is returned.
200    pub fn as_iana(&self) -> Option<&'static Tz> {
201        match &self.inner {
202            r#abstract::TzOrExpanded::Tz(v) => Some(*v),
203            r#abstract::TzOrExpanded::Expanded(_) => None,
204        }
205    }
206}