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}