time-tz-2.0.0/.cargo_vcs_info.json0000644000000001360000000000100124240ustar { "git": { "sha1": "3f235f9a729ae50e86cac1e8ef4b03993a87b71d" }, "path_in_vcs": "" }time-tz-2.0.0/.github/workflows/dev.yml000064400000000000000000000023701046102023000161140ustar 00000000000000name: time-tz CI build on: - push - pull_request jobs: check-audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: recursive # garbage peace of shit mother fucking github downloading broken repositories!!!!! # GitLab is far better because it is not downloading broken repositories. - name: Install cargo-audit run: cargo install cargo-audit - name: Run audit run: cargo audit check-clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: recursive # garbage peace of shit mother fucking github downloading broken repositories!!!!! # GitLab is far better because it is not downloading broken repositories. - name: Install clippy run: rustup component add clippy - name: Run clippy run: cargo clippy build-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: recursive # garbage peace of shit mother fucking github downloading broken repositories!!!!! # GitLab is far better because it is not downloading broken repositories. - name: Build and test run: cargo test --all-features time-tz-2.0.0/.gitignore000064400000000000000000000003201046102023000131770ustar 00000000000000# Binary directory target # Sometime Cargo.lock causes merge bugs so ignore it Cargo.lock # Temporary CI file versionfile.txt # IDE files .idea .vscode # Apple specific .DS_Store .AppleDouble .LSOverride time-tz-2.0.0/.gitmodules000064400000000000000000000001041046102023000133640ustar 00000000000000[submodule "tz"] path = tz url = https://github.com/eggert/tz.git time-tz-2.0.0/Cargo.toml0000644000000041220000000000100104210ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "time-tz" version = "2.0.0" authors = ["Yuri Edward "] description = "Implementation of tz database (IANA) for the time Rust crate." readme = "./README.MD" keywords = [ "time", "tz", ] categories = ["date-and-time"] license = "BSD-3-Clause" repository = "https://github.com/Yuri6037/time-tz" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.cfg-if] version = "1.0.0" [dependencies.nom] version = "7.1.0" optional = true [dependencies.phf] version = "0.11.1" [dependencies.thiserror] version = "1.0.30" optional = true [dependencies.time] version = "0.3.7" features = ["macros"] [build-dependencies.parse-zoneinfo] version = "0.3" [build-dependencies.phf_codegen] version = "0.11.1" [build-dependencies.serde] version = "1.0.136" features = ["derive"] [build-dependencies.serde-xml-rs] version = "0.5.1" [features] db = [] default = ["db"] posix-tz = [ "nom", "thiserror", "db", ] system = [ "windows-sys", "js-sys", "thiserror", "db", ] [target."cfg(not(target_family = \"wasm\"))".dependencies.time] version = "0.3.7" features = ["macros"] [target."cfg(target_family = \"wasm\")".dependencies.js-sys] version = "0.3.64" optional = true [target."cfg(target_family = \"wasm\")".dependencies.time] version = "0.3.7" features = [ "macros", "wasm-bindgen", ] [target."cfg(target_family = \"wasm\")".dependencies.wasm-bindgen] version = "0.2.87" [target."cfg(windows)".dependencies.windows-sys] version = "0.32.0" features = [ "Win32_System_Time", "Win32_Foundation", ] optional = true time-tz-2.0.0/Cargo.toml.orig0000644000000026670000000000100113740ustar [package] name = "time-tz" version = "2.0.0" edition = "2021" authors = ["Yuri Edward "] description = "Implementation of tz database (IANA) for the time Rust crate." license = "BSD-3-Clause" repository = "https://github.com/Yuri6037/time-tz" readme = "./README.MD" keywords = ["time", "tz"] categories = ["date-and-time"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] phf = "0.11.1" time = { version = "0.3.7", features = ["macros"] } cfg-if = "1.0.0" thiserror = { version = "1.0.30", optional = true } nom = { version = "7.1.0", optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] time = { version = "0.3.7", features = ["macros"] } [target.'cfg(target_family = "wasm")'.dependencies] js-sys = { version = "0.3.64", optional = true } time = { version = "0.3.7", features = ["macros", "wasm-bindgen"] } wasm-bindgen = "0.2.87" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.32.0", features = ["Win32_System_Time", "Win32_Foundation"], optional = true } [build-dependencies] parse-zoneinfo = { version = "0.3" } phf_codegen = "0.11.1" serde-xml-rs = "0.5.1" serde = { version = "1.0.136", features = ["derive"] } [features] default = ["db"] system = ["windows-sys", "js-sys", "thiserror", "db"] posix-tz = ["nom", "thiserror", "db"] db = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] time-tz-2.0.0/Cargo.toml.orig000064400000000000000000000026671046102023000141160ustar 00000000000000[package] name = "time-tz" version = "2.0.0" edition = "2021" authors = ["Yuri Edward "] description = "Implementation of tz database (IANA) for the time Rust crate." license = "BSD-3-Clause" repository = "https://github.com/Yuri6037/time-tz" readme = "./README.MD" keywords = ["time", "tz"] categories = ["date-and-time"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] phf = "0.11.1" time = { version = "0.3.7", features = ["macros"] } cfg-if = "1.0.0" thiserror = { version = "1.0.30", optional = true } nom = { version = "7.1.0", optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] time = { version = "0.3.7", features = ["macros"] } [target.'cfg(target_family = "wasm")'.dependencies] js-sys = { version = "0.3.64", optional = true } time = { version = "0.3.7", features = ["macros", "wasm-bindgen"] } wasm-bindgen = "0.2.87" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.32.0", features = ["Win32_System_Time", "Win32_Foundation"], optional = true } [build-dependencies] parse-zoneinfo = { version = "0.3" } phf_codegen = "0.11.1" serde-xml-rs = "0.5.1" serde = { version = "1.0.136", features = ["derive"] } [features] default = ["db"] system = ["windows-sys", "js-sys", "thiserror", "db"] posix-tz = ["nom", "thiserror", "db"] db = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] time-tz-2.0.0/LICENSE.txt000064400000000000000000000027361046102023000130470ustar 00000000000000Copyright (c) 2022, Yuri6037 All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of time-tz nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.time-tz-2.0.0/README.MD000064400000000000000000000033411046102023000123740ustar 00000000000000# time-tz An implementation of the tz database for the time-rs Rust crate. [![chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://bp3d.zulipchat.com/#narrow/stream/322108-projects.2Ftime-tz) This implementation is based off of chrono-tz (https://github.com/chronotope/chrono-tz) but uses time-rs instead of chrono. This is designed to replace use of chono dependency which is impacted by CVE-2020-26235 (localtime_r thread safety issue linked to std::env::set_var). ## Features - Injects an `assume_timezone` member function to any `PrimitiveDateTime`. - Injects a `to_timezone` member function to any `OffsetDateTime`. - Provides a `timezones::get_by_name` function to get a timezone by name. - Supports finding the closest IANA match from a windows timezone name. - Supports obtaining system's current timezone (through the `system` feature). ## Usage ```rust use time::macros::datetime; use time_tz::{PrimitiveDateTimeExt, OffsetDateTimeExt, timezones}; fn main() { // =========================================== // Create a new datetime in a given timezone // =========================================== // First we have to get the source timezone: let london = timezones::db::europe::LONDON; // Now we can create a primitive date time and call the extension function: let dt = datetime!(2016-10-8 17:0:0).assume_timezone_utc(london); // =========================== // Convert to a new timezone // =========================== // First we get the target timezone: let berlin = timezones::db::europe::BERLIN; // Now we can convert (again by calling an extension function): let converted = dt.to_timezone(berlin); // ... do something with converted } ``` time-tz-2.0.0/build.rs000064400000000000000000000224721046102023000126700ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Code inspired from https://github.com/chronotope/chrono-tz/blob/main/chrono-tz-build/src/lib.rs use parse_zoneinfo::line::{Line, LineParser}; use parse_zoneinfo::table::{Table, TableBuilder}; use parse_zoneinfo::transitions::TableTransitions; use serde::Deserialize; use serde_xml_rs::from_str; use std::collections::{BTreeSet, HashMap}; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; const PARSE_FAILURE: &str = "Failed to parse one or more tz databse file(s)"; #[derive(Debug, Deserialize, PartialEq)] #[serde(rename = "mapZone")] struct MapZone { other: String, territory: String, r#type: String, } #[derive(Debug, Deserialize, PartialEq)] struct MapTimezones { #[serde(rename = "$value")] content: Vec, } #[derive(Debug, Deserialize, PartialEq)] struct WindowsZones { #[serde(rename = "mapTimezones")] map_timezones: MapTimezones, } #[derive(Debug, Deserialize, PartialEq)] #[serde(rename = "supplementalData")] struct SupplementalData { #[serde(rename = "windowsZones")] windows_zones: WindowsZones, } fn parse_win_cldr_db() -> phf_codegen::Map { let path = Path::new("win_cldr_data/windowsZones.xml"); let data = std::fs::read_to_string(path).expect("Failed to read windows CLDR database"); let data: SupplementalData = from_str(&data).expect("Failed to parse windows CLDR database"); let mut map = phf_codegen::Map::new(); for mapping in data.windows_zones.map_timezones.content { let zone_name_statics = get_zone_name_static(&mapping.r#type); let mut str = String::new(); let mut split = zone_name_statics.split(' ').peekable(); while let Some(item) = split.next() { str += "&internal_tz_new(&"; str += item; str.push(')'); if split.peek().is_some() { str += ", "; } } let zone_name_static = format!("&[{}]", str); if mapping.territory == "001" { map.entry(mapping.other, &zone_name_static); } else { map.entry( format!("{}/{}", mapping.other, mapping.territory), &zone_name_static, ); } } map } //Linked-list kind of structure: needed to support recursive module generation. struct TimeZone<'a> { pub name: &'a str, pub name_static: String, } struct ModuleTree<'a> { pub name: &'a str, pub items: Vec>, pub sub_modules: HashMap<&'a str, ModuleTree<'a>>, } impl<'a> ModuleTree<'a> { pub fn new(name: &'a str) -> ModuleTree<'a> { ModuleTree { name, items: Vec::new(), sub_modules: HashMap::new(), } } fn insert_zone(&mut self, item: TimeZone<'a>) { self.items.push(item); } pub fn insert(&mut self, zone_name: &'a str, zone_static: String) { let mut path = zone_name.split('/').peekable(); let mut tree = self; while let Some(module) = path.next() { if path.peek().is_none() { tree.insert_zone(TimeZone { name: module, name_static: zone_static, }); break; } else { tree = tree .sub_modules .entry(module) .or_insert_with(|| ModuleTree::new(module)); } } } } fn intermal_write_module_tree( file: &mut BufWriter, tree: &ModuleTree, ) -> std::io::Result<()> { writeln!(file, "pub mod {} {{", tree.name.to_lowercase())?; for zone in &tree.items { writeln!(file, "pub const {}: &crate::Tz = &crate::timezone_impl::internal_tz_new(&crate::timezones::{});", zone.name .to_uppercase() .replace('-', "_") .replace('+', "_PLUS_"), zone.name_static)?; } for subtree in tree.sub_modules.values() { intermal_write_module_tree(file, subtree)?; } writeln!(file, "}}")?; Ok(()) } fn get_zone_name_static(zone: &str) -> String { zone.replace('/', "__") .replace('-', "_") .replace('+', "plus") .to_uppercase() } fn internal_write_timezones(file: &mut BufWriter, table: &Table) -> std::io::Result<()> { let zones: BTreeSet<&String> = table.zonesets.keys().chain(table.links.keys()).collect(); let mut map = phf_codegen::Map::new(); let mut root = ModuleTree::new("db"); for zone in zones { let timespans = table.timespans(zone).unwrap(); let zone_name_static = get_zone_name_static(zone); writeln!( file, "const {}: FixedTimespanSet = FixedTimespanSet {{", zone_name_static )?; writeln!(file, " name: \"{}\",", zone)?; writeln!( file, " first: FixedTimespan {{ utc_offset: {}, dst_offset: {}, name: \"{}\" }},", timespans.first.utc_offset, timespans.first.dst_offset, timespans.first.name )?; writeln!(file, " others: &[")?; for (start, span) in timespans.rest { writeln!( file, " ({}, FixedTimespan {{ utc_offset: {}, dst_offset: {}, name: \"{}\" }}),", start, span.utc_offset, span.dst_offset, span.name )?; } writeln!(file, " ]")?; writeln!(file, "}};")?; map.entry(zone, &format!("&internal_tz_new(&{})", zone_name_static)); root.insert(zone, zone_name_static); } let win_cldr_to_iana = parse_win_cldr_db(); writeln!( file, "static WIN_TIMEZONES: Map<&'static str, &'static [&'static Tz]> = {};", win_cldr_to_iana.build() )?; writeln!( file, "static TIMEZONES: Map<&'static str, &'static Tz> = {};", map.build() )?; intermal_write_module_tree(file, &root)?; Ok(()) } fn write_timezones_file(table: &Table) { let path = std::env::var_os("OUT_DIR") .map(PathBuf::from) .expect("Couldn't obtain cargo OUT_DIR") .join("timezones.rs"); let file = File::create(path).expect("Couldn't create timezones file"); let mut writer = BufWriter::new(file); internal_write_timezones(&mut writer, table).expect("Couldn't write timezones file"); } fn main() { let tzfiles = [ "tz/africa", "tz/antarctica", "tz/asia", "tz/australasia", "tz/backward", "tz/etcetera", "tz/europe", "tz/northamerica", "tz/southamerica", ]; let lines = tzfiles .iter() .map(Path::new) .map(File::open) .map(|v| v.expect("Failed to open one or more tz databse file(s)")) .map(BufReader::new) .flat_map(|v| v.lines()) .map(|v| v.expect("Failed to read one or more tz databse file(s)")) .filter_map(|mut v| { //Pre-filter to get rid of comment-only lines as there are a lot. if let Some(i) = v.find('#') { v.truncate(i); } if v.is_empty() { None } else { Some(v) } }); let mut builder = TableBuilder::new(); let parser = LineParser::new(); for line in lines { match parser.parse_str(&line).expect(PARSE_FAILURE) { Line::Space => {} Line::Zone(v) => builder.add_zone_line(v).expect(PARSE_FAILURE), Line::Continuation(v) => builder.add_continuation_line(v).expect(PARSE_FAILURE), Line::Rule(v) => builder.add_rule_line(v).expect(PARSE_FAILURE), Line::Link(v) => builder.add_link_line(v).expect(PARSE_FAILURE), } } let table = builder.build(); write_timezones_file(&table); //Only run when tz database actually changes (avoids re-running complex logic). println!("cargo:rerun-if-changed=./tz"); } time-tz-2.0.0/src/binary_search.rs000064400000000000000000000051221046102023000151620ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use std::cmp::Ordering; pub fn binary_search Ordering>(start: usize, end: usize, cmp: F) -> Option { if start >= end { return None; } let half = (end - start) / 2; let mid = start + half; match cmp(mid) { Ordering::Greater => binary_search(start, mid, cmp), Ordering::Equal => Some(mid), Ordering::Less => binary_search(mid + 1, end, cmp), } } #[cfg(test)] mod tests { #[test] fn test_binary_search() { assert_eq!(super::binary_search(0, 8, |x| x.cmp(&6)), Some(6)); assert_eq!(super::binary_search(0, 5000, |x| x.cmp(&1337)), Some(1337)); assert_eq!(super::binary_search(0, 5000, |x| x.cmp(&9000)), None); assert_eq!(super::binary_search(30, 50, |x| x.cmp(&42)), Some(42)); assert_eq!(super::binary_search(300, 500, |x| x.cmp(&42)), None); assert_eq!( super::binary_search(0, 500, |x| if x < 42 { super::Ordering::Less } else { super::Ordering::Greater }), None ); } }time-tz-2.0.0/src/interface.rs000064400000000000000000000124731046102023000143200ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use time::{OffsetDateTime, UtcOffset}; /// This trait represents a particular timezone offset. pub trait Offset { /// Converts this timezone offset to a [UtcOffset](time::UtcOffset). fn to_utc(&self) -> UtcOffset; /// Returns the name of this offset. fn name(&self) -> &str; /// Returns true if this offset is DST in the corresponding timezone, false otherwise. fn is_dst(&self) -> bool; } /// This trait represents a timezone provider. pub trait TimeZone { /// The type of offset. type Offset: Offset; /// Search for the given date time offset (assuming it is UTC) in this timezone. fn get_offset_utc(&self, date_time: &OffsetDateTime) -> Self::Offset; /// Search for the given date time offset (assuming it is already local) in this timezone. fn get_offset_local(&self, date_time: &OffsetDateTime) -> OffsetResult; /// Gets the main/default offset in this timezone. fn get_offset_primary(&self) -> Self::Offset; /// Returns the name of this timezone. fn name(&self) -> &str; } /// This represents the possible types of errors when trying to find a local offset. #[derive(Clone, Copy, Debug)] pub enum OffsetResult { /// The date time is not ambiguous (exactly 1 is possible). Some(T), /// The date time is ambiguous (2 are possible). Ambiguous(T, T), /// The date time is invalid. None, } impl OffsetResult { /// Unwraps this OffsetResult assuming ambiguity is an error. pub fn unwrap(self) -> T { match self { OffsetResult::Some(v) => v, OffsetResult::Ambiguous(_, _) => panic!("Attempt to unwrap an ambiguous offset"), OffsetResult::None => panic!("Attempt to unwrap an invalid offset"), } } /// Unwraps this OffsetResult resolving ambiguity by taking the first result. pub fn unwrap_first(self) -> T { match self { OffsetResult::Some(v) => v, OffsetResult::Ambiguous(v, _) => v, OffsetResult::None => panic!("Attempt to unwrap an invalid offset"), } } /// Unwraps this OffsetResult resolving ambiguity by taking the second result. pub fn unwrap_second(self) -> T { match self { OffsetResult::Some(v) => v, OffsetResult::Ambiguous(_, v) => v, OffsetResult::None => panic!("Attempt to unwrap an invalid offset"), } } /// Turns this OffsetResult into an Option assuming ambiguity is an error. pub fn take(self) -> Option { match self { OffsetResult::Some(v) => Some(v), OffsetResult::Ambiguous(_, _) => None, OffsetResult::None => None, } } /// Turns this OffsetResult into an Option resolving ambiguity by taking the first result. pub fn take_first(self) -> Option { match self { OffsetResult::Some(v) => Some(v), OffsetResult::Ambiguous(v, _) => Some(v), OffsetResult::None => None, } } /// Turns this OffsetResult into an Option resolving ambiguity by taking the second result. pub fn take_second(self) -> Option { match self { OffsetResult::Some(v) => Some(v), OffsetResult::Ambiguous(_, v) => Some(v), OffsetResult::None => None, } } /// Returns true if this OffsetResult is None. pub fn is_none(&self) -> bool { match self { OffsetResult::Some(_) => false, OffsetResult::Ambiguous(_, _) => false, OffsetResult::None => true, } } /// Returns true if this OffsetResult is ambiguous. pub fn is_ambiguous(&self) -> bool { match self { OffsetResult::Some(_) => false, OffsetResult::Ambiguous(_, _) => true, OffsetResult::None => false, } } } time-tz-2.0.0/src/lib.rs000064400000000000000000000172521046102023000131260ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! This provides traits and utilities to work with timezones to time-rs and additionally has an //! implementation of IANA timezone database. To disable the integrated IANA/windows databases, //! one can simply remove the `db` default feature. // See https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html & https://github.com/rust-lang/rust/pull/89596 #![cfg_attr(docsrs, feature(doc_auto_cfg))] use time::{OffsetDateTime, PrimitiveDateTime}; mod sealing { pub trait OffsetDateTimeExt {} pub trait PrimitiveDateTimeExt {} impl OffsetDateTimeExt for time::OffsetDateTime {} impl PrimitiveDateTimeExt for time::PrimitiveDateTime {} } // This trait is sealed and is only implemented in this library. pub trait OffsetDateTimeExt: sealing::OffsetDateTimeExt { /// Converts this OffsetDateTime to a different TimeZone. fn to_timezone(&self, tz: &T) -> OffsetDateTime; } /// This trait is sealed and is only implemented in this library. pub trait PrimitiveDateTimeExt: sealing::PrimitiveDateTimeExt { /// Creates a new OffsetDateTime from a PrimitiveDateTime by assigning the main offset of the /// target timezone. /// /// *This assumes the PrimitiveDateTime is already in the target timezone.* /// /// # Arguments /// /// * `tz`: the target timezone. /// /// returns: `OffsetResult` fn assume_timezone(&self, tz: &T) -> OffsetResult; /// Creates a new OffsetDateTime with the proper offset in the given timezone. /// /// *This assumes the PrimitiveDateTime is in UTC offset.* /// /// # Arguments /// /// * `tz`: the target timezone. /// /// returns: OffsetDateTime fn assume_timezone_utc(&self, tz: &T) -> OffsetDateTime; } impl PrimitiveDateTimeExt for PrimitiveDateTime { fn assume_timezone(&self, tz: &T) -> OffsetResult { match tz.get_offset_local(&self.assume_utc()) { OffsetResult::Some(a) => OffsetResult::Some(self.assume_offset(a.to_utc())), OffsetResult::Ambiguous(a, b) => OffsetResult::Ambiguous( self.assume_offset(a.to_utc()), self.assume_offset(b.to_utc()), ), OffsetResult::None => OffsetResult::None, } } fn assume_timezone_utc(&self, tz: &T) -> OffsetDateTime { let offset = tz.get_offset_utc(&self.assume_utc()); self.assume_offset(offset.to_utc()) } } impl OffsetDateTimeExt for OffsetDateTime { fn to_timezone(&self, tz: &T) -> OffsetDateTime { let offset = tz.get_offset_utc(self); self.to_offset(offset.to_utc()) } } mod binary_search; mod interface; mod timezone_impl; #[cfg(feature = "db")] pub mod timezones; pub use interface::*; #[cfg(feature = "system")] pub mod system; #[cfg(feature = "posix-tz")] pub mod posix_tz; #[cfg(feature = "db")] pub use timezone_impl::Tz; #[cfg(test)] mod tests { use crate::timezones; use crate::Offset; use crate::OffsetDateTimeExt; use crate::PrimitiveDateTimeExt; use crate::TimeZone; use time::macros::{datetime, offset}; use time::OffsetDateTime; #[test] fn names() { //This test verifies that windows timezone names work fine. let shanghai = timezones::get_by_name("Asia/Shanghai"); let china = timezones::get_by_name("China Standard Time"); assert!(shanghai.is_some()); assert!(china.is_some()); assert_eq!(shanghai, china); } #[test] fn find() { let zones_iana = timezones::find_by_name("Asia"); //let zones_win = timezones::find_by_name("China Standard Time"); assert!(zones_iana.len() > 1); //assert!(zones_win.len() > 1); } #[test] fn offsets_and_name() { let tz = timezones::db::europe::LONDON; assert_eq!(tz.name(), "Europe/London"); let offset = tz.get_offset_utc(&OffsetDateTime::now_utc()); assert!(!offset.name().is_empty()); } #[test] fn london_to_berlin() { let dt = datetime!(2016-10-8 17:0:0).assume_timezone_utc(timezones::db::europe::LONDON); let converted = dt.to_timezone(timezones::db::europe::BERLIN); let expected = datetime!(2016-10-8 18:0:0).assume_timezone_utc(timezones::db::europe::BERLIN); assert_eq!(converted, expected); } #[test] fn dst() { let london = timezones::db::europe::LONDON; let odt1 = datetime!(2021-01-01 12:0:0 UTC); assert_eq!(odt1.to_timezone(london), datetime!(2021-01-01 12:0:0 +0)); let odt2 = datetime!(2021-07-01 12:0:0 UTC); // Adding offset to datetime call causes VERY surprising result: hours randomly changes!! // When using UTC followed by .to_offset no surprising result. assert_eq!( odt2.to_timezone(london), datetime!(2021-07-01 12:0:0 UTC).to_offset(offset!(+1)) ); } #[test] fn handles_forward_changeover() { assert_eq!( datetime!(2022-03-27 01:30) .assume_timezone(timezones::db::CET) .unwrap(), datetime!(2022-03-27 01:30 +01:00) ); } #[test] fn handles_after_changeover() { assert_eq!( datetime!(2022-03-27 03:30) .assume_timezone(timezones::db::CET) .unwrap(), datetime!(2022-03-27 03:30 +02:00) ); } #[test] fn handles_broken_time() { assert!(datetime!(2022-03-27 02:30) .assume_timezone(timezones::db::CET) .is_none()); } #[test] fn handles_backward_changeover() { // During backward changeover, the hour between 02:00 and 03:00 occurs twice, so either answer is correct assert_eq!( datetime!(2022-10-30 02:30) .assume_timezone(timezones::db::CET) .unwrap_first(), datetime!(2022-10-30 02:30 +02:00) ); assert_eq!( datetime!(2022-10-30 02:30) .assume_timezone(timezones::db::CET) .unwrap_second(), datetime!(2022-10-30 02:30 +01:00) ); } } time-tz-2.0.0/src/posix_tz/abstract.rs000064400000000000000000000150421046102023000160350ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::posix_tz::intermediate::ParsedTz; use crate::posix_tz::parser::Date; use crate::posix_tz::{Error, ParseError}; use crate::timezone_impl::TzOffset; use crate::Tz; use time::{OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; pub enum TzOrExpandedOffset<'a> { Expanded(ExpandedTzOffset<'a>), Tz(TzOffset), } pub struct ExpandedTzOffset<'a> { mode: ExpandedMode<'a>, is_dst: bool, } impl<'a> crate::Offset for ExpandedTzOffset<'a> { fn to_utc(&self) -> UtcOffset { self.mode.offset } fn name(&self) -> &str { self.mode.name } fn is_dst(&self) -> bool { self.is_dst } } #[derive(Copy, Clone)] pub struct ExpandedMode<'a> { name: &'a str, offset: UtcOffset, //There's always an offset, even if not defined it's assumed to be +1. } pub struct Rule { //Pre-compute the rule dates start: (Date, Time), end: (Date, Time), } pub struct ExpandedTz<'a> { std: ExpandedMode<'a>, dst: Option>, rule: Option, } impl<'a> ExpandedTz<'a> { pub fn get_offset_utc( &self, date_time: &OffsetDateTime, ) -> Result, Error> { match self.dst { None => Ok(ExpandedTzOffset { mode: self.std, is_dst: false, }), Some(dst) => match &self.rule { None => { use crate::Offset; use crate::TimeZone; let timezone = crate::timezones::db::america::NEW_YORK; let tz_offset = timezone.get_offset_utc(date_time); Ok(ExpandedTzOffset { mode: if tz_offset.is_dst() { dst } else { self.std }, is_dst: tz_offset.is_dst(), }) } Some(rule) => { let start = PrimitiveDateTime::new( rule.start.0.to_date(date_time.year())?, rule.start.1, ) .assume_offset(self.std.offset); let end = PrimitiveDateTime::new(rule.end.0.to_date(date_time.year())?, rule.end.1) .assume_offset(dst.offset); if date_time >= &start && date_time < &end { Ok(ExpandedTzOffset { mode: dst, is_dst: true, }) } else { Ok(ExpandedTzOffset { mode: self.std, is_dst: false, }) } } }, } } } pub enum TzOrExpanded<'a> { Tz(&'static Tz), Expanded(ExpandedTz<'a>), } pub fn parse_abstract(input: ParsedTz) -> Result { match input { ParsedTz::Existing(v) => Ok(TzOrExpanded::Tz(v)), ParsedTz::Expanded((std, dst)) => { //Take the oposite of offset because POSIX assumes it at inverse: // local + offset = utc instead of utc + offset = local. let tmp = -std.offset.to_seconds(); let std_offset = UtcOffset::from_whole_seconds(tmp).map_err(ParseError::ComponentRange)?; let std = ExpandedMode { name: std.name, offset: std_offset, }; let (dst, rule) = match dst { None => (None, None), Some(v) => { // If no offset is specified the POSIX standard defines +1 hour in standard // time as default. let offset = v.offset.map(|v| -v.to_seconds()).unwrap_or(tmp + 3600); let dst_offset = UtcOffset::from_whole_seconds(offset) .map_err(ParseError::ComponentRange)?; let rule = match &v.rule { None => None, Some(v) => { // SAFETY: This must be safe as never ever depends on user input. let default = unsafe { time::Time::from_hms(2, 0, 0).unwrap_unchecked() }; let start_time = v.start.1.as_ref().map(|v| v.to_time()).unwrap_or(default); let end_time = v.end.1.as_ref().map(|v| v.to_time()).unwrap_or(default); Some(Rule { start: (v.start.0, start_time), end: (v.end.0, end_time), }) } }; ( Some(ExpandedMode { name: v.name, offset: dst_offset, }), rule, ) } }; Ok(TzOrExpanded::Expanded(ExpandedTz { std, dst, rule })) } } } time-tz-2.0.0/src/posix_tz/intermediate.rs000064400000000000000000000160571046102023000167130ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use super::parser::Date; use super::parser::Offset; use super::parser::Time; use crate::posix_tz::parser::{entry, Dst, Std, Tz}; use crate::posix_tz::{Error, ParseError, RangeError}; use nom::Err; use time::{Duration, Month, Weekday}; impl Time { fn to_seconds(&self) -> u32 { self.hh as u32 * 3600 + self.mm.unwrap_or(0) as u32 * 60 + self.ss.unwrap_or(0) as u32 } pub fn to_time(&self) -> time::Time { // Here unwrap should always pass because component range is checked in is_valid_range. time::Time::from_hms(self.hh, self.mm.unwrap_or(0), self.ss.unwrap_or(0)).unwrap() } fn is_valid_range(&self) -> bool { self.hh <= 24 && self.mm.unwrap_or(0) <= 59 && self.ss.unwrap_or(0) <= 59 } } impl Offset { pub fn to_seconds(&self) -> i32 { match self.positive { true => self.time.to_seconds() as i32, false => -(self.time.to_seconds() as i32), } } } impl Date { pub fn to_date(self, year: i32) -> Result { match self { Date::J(n) => { // Hack: the basic idea is 2021 was not a leap year so february only // contains 28 days instead of 29 which matches the POSIX spec. let date = time::Date::from_ordinal_date(2021, n).map_err(Error::ComponentRange)?; // Not sure if that will work in all cases though... time::Date::from_calendar_date(year, date.month(), date.day()) .map_err(Error::ComponentRange) } // ComponentRange errors should be prevented by is_valid_range. Date::N(n) => time::Date::from_ordinal_date(year, n + 1).map_err(Error::ComponentRange), Date::M { m, n, d } => { // One more hack: here w're trying to match Date::from_iso_week_date. // SAFETY: This always works because m >= 1 // and m <= 12 (see is_valid_range). let month = unsafe { Month::try_from(m).unwrap_unchecked() }; let day = match d { 0 => Weekday::Sunday, 1 => Weekday::Monday, 2 => Weekday::Tuesday, 3 => Weekday::Wednesday, 4 => Weekday::Thursday, 5 => Weekday::Friday, 6 => Weekday::Saturday, // SAFETY: This is basically impossible because d is a u8 so by definition // cannot be < 0 and d <= 6 (see is_valid_range). _ => unsafe { std::hint::unreachable_unchecked() }, }; let mut date = time::Date::from_calendar_date(year, month, 1) .map_err(Error::ComponentRange)?; while date.weekday() != day { date = date.next_day().ok_or(Error::DateTooLarge)?; } let next_month = date.month().next(); // Advance of (n - 1) * 7 days. date = date .checked_add(Duration::days((n as i64 - 1) * 7)) .ok_or(Error::DateTooLarge)?; if n == 5 && date.month() == next_month { date -= Duration::days(7); //Shift back of 7 days. } Ok(date) } } } fn is_valid_range(&self) -> bool { match self { Date::J(v) => (1..=365).contains(v), Date::N(v) => v <= &365, Date::M { m, n, d } => d <= &6 && (1..=5).contains(n) && (1..=12).contains(m), } } } impl<'a> Tz<'a> { fn ensure_valid_range(&self) -> Result<(), RangeError> { if let Tz::Expanded { std, dst } = self { if !std.offset.time.is_valid_range() { return Err(RangeError::Time); } if let Some(dst) = dst { if let Some(offset) = &dst.offset { if !offset.time.is_valid_range() { return Err(RangeError::Time); } } if let Some(rule) = &dst.rule { if !rule.start.0.is_valid_range() || !rule.end.0.is_valid_range() { return Err(RangeError::Date); } if let Some(time) = &rule.start.1 { if !time.is_valid_range() { return Err(RangeError::Time); } } if let Some(time) = &rule.end.1 { if !time.is_valid_range() { return Err(RangeError::Time); } } } } } Ok(()) } } pub enum ParsedTz<'a> { Existing(&'static crate::Tz), Expanded((Std<'a>, Option>)), } pub fn parse_intermediate(input: &str) -> Result { let (_, inner) = entry(input).map_err(|v| match v { Err::Incomplete(_) => { panic!("According to nom docs this case is impossible with complete API.") } Err::Error(e) => ParseError::Nom(e.code), Err::Failure(e) => ParseError::Nom(e.code), })?; inner.ensure_valid_range().map_err(ParseError::Range)?; Ok(match inner { Tz::Short(name) => { let tz = crate::timezones::find_by_name(name) .first() .copied() .ok_or(ParseError::UnknownName(name))?; ParsedTz::Existing(tz) } Tz::Expanded { std, dst } => ParsedTz::Expanded((std, dst)), }) } time-tz-2.0.0/src/posix_tz/mod.rs000064400000000000000000000162511046102023000150140ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{Offset, TimeZone, Tz}; use std::fmt::{Display, Formatter}; use thiserror::Error; use time::{OffsetDateTime, UtcOffset}; mod r#abstract; mod intermediate; mod parser; /// A range error returned when a field is out of the range defined in POSIX. #[derive(Debug)] pub enum RangeError { /// One of the time field in the given string was out of range. Time, /// One of the date field in the given string was out of range. Date, } impl Display for RangeError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RangeError::Time => f.write_str("time field out of range"), RangeError::Date => f.write_str("date field out of range"), } } } /// The main type of error that is returned when a TZ POSIX string fails to parse. #[derive(Debug, Error)] pub enum ParseError<'a> { /// A nom parsing error. #[error("nom error: {:?}", .0)] Nom(nom::error::ErrorKind), /// In case a short format was given, the POSIX standard doesn't define what to do, /// in this implementation we just try to match the first tzdb timezone containing the /// short name; if none could be found this error variant is returned. #[error("unknown short timezone name `{0}`")] UnknownName(&'a str), /// We've exceeded the range of a field when checking for conformance against the POSIX /// standard. #[error("range error: {0}")] Range(RangeError), /// We've exceeded the range of a date component when converting types to time-rs. #[error("time component range error: {0}")] ComponentRange(time::error::ComponentRange), } /// The type of error return when a given Offset/PrimitiveDateTime cannot be represented /// in a given POSIX TZ formatted "timezone". #[derive(Debug, Error)] pub enum Error { /// We've exceeded the range of a date component when converting types to time-rs. #[error("time component range error: {0}")] ComponentRange(time::error::ComponentRange), /// We've exceeded the maximum date supported by time-rs. #[error("value of Date too large")] DateTooLarge, } /// A POSIX "timezone" offset. pub struct PosixTzOffset<'a> { inner: r#abstract::TzOrExpandedOffset<'a>, } impl<'a> Offset for PosixTzOffset<'a> { fn to_utc(&self) -> UtcOffset { match &self.inner { r#abstract::TzOrExpandedOffset::Expanded(v) => v.to_utc(), r#abstract::TzOrExpandedOffset::Tz(v) => v.to_utc(), } } fn name(&self) -> &str { match &self.inner { r#abstract::TzOrExpandedOffset::Expanded(v) => v.name(), r#abstract::TzOrExpandedOffset::Tz(v) => v.name(), } } fn is_dst(&self) -> bool { match &self.inner { r#abstract::TzOrExpandedOffset::Expanded(v) => v.is_dst(), r#abstract::TzOrExpandedOffset::Tz(v) => v.is_dst(), } } } /// A "timezone" in POSIX TZ format. pub struct PosixTz<'a> { inner: r#abstract::TzOrExpanded<'a>, } impl<'a> PosixTz<'a> { /// Parse the given POSIX TZ string. /// /// # Arguments /// /// * `input`: the string to parse. /// /// returns: Result /// /// # Errors /// /// Returns a [ParseError](crate::posix_tz::ParseError) if the given string is not a valid /// POSIX "timezone". pub fn parse(input: &'a str) -> Result, ParseError> { let intermediate = intermediate::parse_intermediate(input)?; let inner = r#abstract::parse_abstract(intermediate)?; Ok(PosixTz { inner }) } /// Convert the given date_time to this "timezone". /// /// # Arguments /// /// * `date_time`: the date time to convert. /// /// returns: Result /// /// # Errors /// /// Returns an [Error](crate::posix_tz::Error) if the date_time cannot be represented in this /// "timezone". pub fn convert(&self, date_time: &OffsetDateTime) -> Result { let offset = self.get_offset(date_time)?; Ok(date_time.to_offset(offset.to_utc())) } /// Calculates the offset to add to the given date_time to convert it to this "timezone". /// /// # Arguments /// /// * `date_time`: the date time to calculate offset of. /// /// returns: Result /// /// # Errors /// /// Returns an [Error](crate::posix_tz::Error) if the date_time cannot be represented in this /// "timezone". pub fn get_offset(&self, date_time: &OffsetDateTime) -> Result, Error> { Ok(match &self.inner { r#abstract::TzOrExpanded::Tz(v) => PosixTzOffset { inner: r#abstract::TzOrExpandedOffset::Tz(v.get_offset_utc(date_time)), }, r#abstract::TzOrExpanded::Expanded(v) => PosixTzOffset { inner: r#abstract::TzOrExpandedOffset::Expanded(v.get_offset_utc(date_time)?), }, }) } /// Gets the current time in this "timezone". /// /// returns: Result /// /// # Errors /// /// Returns an [Error](crate::posix_tz::Error) if the current time cannot be represented in /// this "timezone". pub fn now(&self) -> Result { self.convert(&OffsetDateTime::now_utc()) } /// Returns the precise IANA TimeZone associated to this POSIX "timezone". /// /// If this POSIX "timezone" is not a precise IANA TimeZone, None is returned. pub fn as_iana(&self) -> Option<&'static Tz> { match &self.inner { r#abstract::TzOrExpanded::Tz(v) => Some(*v), r#abstract::TzOrExpanded::Expanded(_) => None, } } } time-tz-2.0.0/src/posix_tz/parser.rs000064400000000000000000000154371046102023000155360ustar 00000000000000// Copyright (c) 2022, Yuri6037 // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of time-tz nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use nom::branch::alt; use nom::bytes::complete::{is_not, take_while_m_n}; use nom::character::complete::{char as cchar, digit1}; use nom::combinator::{eof, map_res, opt}; use nom::sequence::{delimited, preceded, terminated, tuple}; use nom::IResult; const TZNAME_MAX: usize = 16; type Result<'a, T> = IResult<&'a str, T>; #[derive(Eq, PartialEq, Debug)] pub struct Time { pub hh: u8, pub mm: Option, pub ss: Option, } #[derive(Eq, PartialEq, Debug)] pub struct Offset { pub positive: bool, pub time: Time, } #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum Date { J(u16), N(u16), M { m: u8, n: u8, d: u8 }, } #[derive(Eq, PartialEq, Debug)] pub struct Rule { pub start: (Date, Option