diff --git a/Cargo.toml b/Cargo.toml index 073653d54..509df690e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ exclude = [ [workspace.dependencies] # Self temporal_rs = { version = "0.1.2", path = ".", default-features = false } -timezone_provider = { version = "0.1.2", path = "./provider" } +timezone_provider = { version = "0.1.2", path = "./provider", default-features = false } zoneinfo_rs = { version = "0.0.18", path = "./zoneinfo" } # Dependencies diff --git a/provider/Cargo.toml b/provider/Cargo.toml index f0bb50165..a47ff296f 100644 --- a/provider/Cargo.toml +++ b/provider/Cargo.toml @@ -57,7 +57,7 @@ zerovec = { workspace = true, features = ["derive", "alloc"] } tinystr = { workspace = true, features = ["zerovec"] } # IANA dependency -zoneinfo_rs = { workspace = true, features = ["std"], optional = true } +zoneinfo_rs = { workspace = true, features = ["std", "unstable"], optional = true } # tzif dependency tzif = { workspace = true, optional = true } diff --git a/zoneinfo/Cargo.toml b/zoneinfo/Cargo.toml index 3d441fc97..0048ddfff 100644 --- a/zoneinfo/Cargo.toml +++ b/zoneinfo/Cargo.toml @@ -7,7 +7,6 @@ rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -readme.workspace = true exclude.workspace = true include = [ "src/**/*", @@ -19,6 +18,7 @@ include = [ [features] std = [] +unstable = [] [dependencies] hashbrown = "0.16.0" diff --git a/zoneinfo/README.md b/zoneinfo/README.md index f5ad045e9..21513475f 100644 --- a/zoneinfo/README.md +++ b/zoneinfo/README.md @@ -9,8 +9,9 @@ zoneinfo files. ```rust use std::path::Path; use zoneinfo_rs::{ZoneInfoData, ZoneInfoCompiler}; -// Below assumes we are in the parent directory of `tzdata` +// Below assumes we are in the parent directory of `tzdata`. let zoneinfo_filepath = Path::new("./tzdata/"); +// Parse and then compile the files from the directory. let parsed_data = ZoneInfoData::from_zoneinfo_directory(zoneinfo_filepath)?; let _compiled_data = ZoneInfoCompiler::new(parsed_data).build(); ``` diff --git a/zoneinfo/src/compiler.rs b/zoneinfo/src/compiler.rs index 48f079b2e..710397b16 100644 --- a/zoneinfo/src/compiler.rs +++ b/zoneinfo/src/compiler.rs @@ -6,6 +6,16 @@ use alloc::collections::BTreeSet; use alloc::string::String; + +#[cfg(feature = "unstable")] +use crate::tzif::TzifBlockV2; +use crate::{ + posix::PosixTimeZone, + types::{QualifiedTimeKind, Time}, + zone::ZoneRecord, + ZoneInfoData, +}; + use hashbrown::HashMap; #[derive(Debug, Clone, PartialEq)] @@ -108,6 +118,7 @@ pub struct CompiledTransitions { // // I think I would prefer all of that live in the `tzif` crate, but that will // be a process to update. So implement it here, and then upstream it? +#[cfg(feature = "unstable")] impl CompiledTransitions { pub fn to_v2_data_block(&self) -> TzifBlockV2 { TzifBlockV2::from_transition_data(self) @@ -123,14 +134,6 @@ pub struct CompiledTransitionsMap { // ==== ZoneInfoCompiler build / compile methods ==== -use crate::{ - posix::PosixTimeZone, - types::{QualifiedTimeKind, Time}, - tzif::TzifBlockV2, - zone::ZoneRecord, - ZoneInfoData, -}; - /// The compiler for turning `ZoneInfoData` into `CompiledTransitionsData` pub struct ZoneInfoCompiler { data: ZoneInfoData, diff --git a/zoneinfo/src/lib.rs b/zoneinfo/src/lib.rs index 5c62361ad..338ab492c 100644 --- a/zoneinfo/src/lib.rs +++ b/zoneinfo/src/lib.rs @@ -54,9 +54,11 @@ pub mod parser; pub mod posix; pub mod rule; pub mod types; -pub mod tzif; pub mod zone; +#[cfg(feature = "unstable")] +pub mod tzif; + #[doc(inline)] pub use compiler::ZoneInfoCompiler; @@ -67,6 +69,7 @@ use rule::Rules; use zone::ZoneRecord; /// Well-known zone info file +#[doc(hidden)] pub const ZONEINFO_FILES: &[&str] = &[ "africa", "antarctica", diff --git a/zoneinfo/src/posix.rs b/zoneinfo/src/posix.rs index 5be5f634b..66efe948a 100644 --- a/zoneinfo/src/posix.rs +++ b/zoneinfo/src/posix.rs @@ -1,15 +1,22 @@ +//! POSIX time zone types and implementation +//! +//! For more information on POSIX time zones, see the [GNU docs][gnu-docs]. +//! +//! [gnu-docs]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + use crate::{ rule::{LastRules, Rule}, - types::{DayOfMonth, Month, QualifiedTime, Sign, Time, WeekDay}, + types::{ + rule::{DayOfMonth, WeekDay}, + Month, QualifiedTime, Sign, Time, + }, utils::month_to_day, zone::ZoneEntry, }; use alloc::string::String; use core::fmt::Write; -/// The POSIX time zone designated by the [GNU documentation][gnu-docs] -/// -/// [gnu-docs]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html +/// A parsed POSIX time zone #[derive(Debug, PartialEq)] pub struct PosixTimeZone { pub abbr: PosixAbbreviation, diff --git a/zoneinfo/src/rule.rs b/zoneinfo/src/rule.rs index 59a9ba4bc..6f6ac6f72 100644 --- a/zoneinfo/src/rule.rs +++ b/zoneinfo/src/rule.rs @@ -8,7 +8,10 @@ use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; use crate::{ parser::{next_split, ContextParse, LineParseContext, ZoneInfoParseError}, - types::{DayOfMonth, Month, QualifiedTime, Time, ToYear}, + types::{ + rule::{DayOfMonth, ToYear}, + Month, QualifiedTime, Time, + }, utils::{self, epoch_seconds_for_epoch_days}, }; @@ -301,7 +304,7 @@ impl Rule { #[cfg(test)] mod tests { use super::*; - use crate::types::{Sign, WeekDay}; + use crate::types::{rule::WeekDay, Sign}; const TEST_DATA: [&str; 22] = [ "Rule Algeria 1916 only - Jun 14 23:00s 1:00 S", diff --git a/zoneinfo/src/types.rs b/zoneinfo/src/types.rs index 872c13840..f596a9a21 100644 --- a/zoneinfo/src/types.rs +++ b/zoneinfo/src/types.rs @@ -2,179 +2,79 @@ //! //! This module contains general types that are present in a zone info //! file. +//! +//! For more information, see [How to Read tz Database Source Files][tz-how-to]. +//! +//! [tz-how-to]: https://data.iana.org/time-zones/tz-how-to.html -use core::fmt::Write; - -use alloc::{borrow::ToOwned, string::String}; +use alloc::borrow::ToOwned; use crate::{ - parser::{next_split, ContextParse, LineParseContext, TryFromStr, ZoneInfoParseError}, - rule::epoch_days_for_rule_date, + parser::{ContextParse, LineParseContext, TryFromStr, ZoneInfoParseError}, utils, }; -// ==== Zone Table specific types ==== - -#[derive(Debug, Clone, PartialEq)] -pub enum RuleIdentifier { - None, - Numeric(Time), - Named(String), -} - -impl TryFromStr for RuleIdentifier { - type Error = ZoneInfoParseError; - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - ctx.enter("RuleIdentifier"); - if s == "-" { - ctx.exit(); - return Ok(Self::None); - } - if s.contains(":") { - ctx.exit(); - return Time::try_from_str(s, ctx).map(Self::Numeric); - } - ctx.exit(); - Ok(Self::Named(s.to_owned())) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum AbbreviationFormat { - String(String), - Numeric, - Pair(String, String), - Formattable(FormattableAbbr), -} - -impl AbbreviationFormat { - pub fn format(&self, offset: i64, letter: Option<&str>, is_dst: bool) -> String { - match self { - Self::String(s) => s.clone(), - Self::Formattable(s) => s.to_formatted_string(letter.unwrap_or("")), - Self::Pair(std, dst) => { - if is_dst { - dst.clone() - } else { - std.clone() - } - } - Self::Numeric => offset_to_str(offset), - } - } -} - -fn offset_to_str(n: i64) -> String { - let mut output = String::new(); - if n.is_positive() { - write!(&mut output, "+").expect("failed to write"); - } else { - write!(&mut output, "-").expect("failed to write"); - } - let hour = n.abs().div_euclid(3600); - write!(&mut output, "{hour:02}").expect("failed to write"); - let minute = n.abs().rem_euclid(3600).div_euclid(60); - if minute > 0 { - write!(&mut output, "{minute:02}").expect("failed to write"); - } - output -} - -impl TryFromStr for AbbreviationFormat { - type Error = ZoneInfoParseError; - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - ctx.enter("Abbr. Format"); - let value = if s.contains("%s") { - Ok(Self::Formattable(FormattableAbbr(s.to_owned()))) - } else if s.contains("%z") { - Ok(Self::Numeric) - } else if s.contains("/") { - let (std, dst) = s - .split_once('/') - .ok_or(ZoneInfoParseError::unknown(s, ctx))?; - Ok(Self::Pair(std.to_owned(), dst.to_owned())) - } else { - Ok(AbbreviationFormat::String(s.to_owned())) - }; - ctx.exit(); - value - } -} +pub mod rule; +pub mod zone; -#[derive(Debug, Clone, PartialEq)] -pub struct FormattableAbbr(String); - -impl FormattableAbbr { - pub fn to_formatted_string(&self, letter: &str) -> String { - self.0.replace("%s", letter) - } -} +// General shared types between the two lines +/// An enum representing a three letter abbreviated month (e.g. `Jan`, `Sep`). +/// +/// The month value is present in the `IN` column of a rule line or the date +/// month portion in the [UNTIL] column of a zone line. +/// +/// Note: month is 1 based (1-12). #[derive(Debug, Clone, Copy, PartialEq)] -pub struct UntilDateTime { - pub date: Date, - pub time: QualifiedTime, +#[repr(u8)] +pub enum Month { + Jan = 1, + Feb, + Mar, + Apr, + May, + Jun, + Jul, + Aug, + Sep, + Oct, + Nov, + Dec, } -impl UntilDateTime { - pub fn as_date_secs(self) -> i64 { - self.date.as_secs() +impl Month { + /// Calculates the day of year for the start of the month + pub(crate) fn month_start_to_day_of_year(self, year: i32) -> i32 { + utils::month_to_day(self as u8, utils::num_leap_days(year)) } - pub fn as_precise_ut_time(self, std_offset: i64, save: i64) -> i64 { - self.as_date_secs() + self.time.to_universal_seconds(std_offset, save) + /// Calculates the day of year for the end of the month + pub(crate) fn month_end_to_day_of_year(self, year: i32) -> i32 { + utils::month_to_day(self as u8 + 1, utils::num_leap_days(year)) - 1 } } -impl TryFromStr for UntilDateTime { +impl TryFromStr for Month { type Error = ZoneInfoParseError; fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - ctx.enter("UntilDateTime"); - let mut splits = s.split_whitespace(); - let year = next_split(&mut splits, ctx)?.context_parse::(ctx)?; - let date_or_end = splits.next(); - let date = if let Some(month) = date_or_end { - let month = month.context_parse::(ctx)?; - let day = next_split(&mut splits, ctx) - .ok() - .map(|s| s.context_parse::(ctx)) - .transpose()? - .unwrap_or(DayOfMonth::Day(1)); - Date { year, month, day } - } else { - ctx.exit(); - return Ok(UntilDateTime { - date: Date { - year, - month: Month::Jan, - day: DayOfMonth::Day(1), - }, - time: QualifiedTime::Local(Time::default()), - }); + ctx.enter("Month"); + let result = match s { + "Jan" => Ok(Self::Jan), + "Feb" => Ok(Self::Feb), + "Mar" => Ok(Self::Mar), + "Apr" => Ok(Self::Apr), + "May" => Ok(Self::May), + "Jun" => Ok(Self::Jun), + "Jul" => Ok(Self::Jul), + "Aug" => Ok(Self::Aug), + "Sep" => Ok(Self::Sep), + "Oct" => Ok(Self::Oct), + "Nov" => Ok(Self::Nov), + "Dec" => Ok(Self::Dec), + _ => Err(ZoneInfoParseError::unknown(s, ctx)), }; - - let time = next_split(&mut splits, ctx) - .ok() - .map(|t| t.context_parse::(ctx)) - .transpose()? - .unwrap_or(QualifiedTime::Local(Time::default())); - ctx.exit(); - Ok(Self { date, time }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Date { - pub year: i32, - pub month: Month, - pub day: DayOfMonth, -} - -impl Date { - pub fn as_secs(&self) -> i64 { - let epoch_days = epoch_days_for_rule_date(self.year, self.month, self.day); - utils::epoch_seconds_for_epoch_days(epoch_days) + result } } @@ -187,6 +87,7 @@ pub struct Time { pub second: u8, } +/// A non zero sign type that represents whether a value is positive or negative. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(i8)] pub enum Sign { @@ -296,195 +197,7 @@ impl TryFromStr for Time { } } -// ==== Rule types ==== - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ToYear { - Max, - Year(u16), -} - -impl ToYear { - pub(crate) fn parse_optional_to_year( - s: &str, - ctx: &mut LineParseContext, - ) -> Result, ZoneInfoParseError> { - if s == "only" { - Ok(None) - } else { - s.context_parse::(ctx).map(Some) - } - } - - pub(crate) fn to_i32(self) -> i32 { - match self { - Self::Max => 275_760, - Self::Year(y) => y as i32, - } - } - - pub(crate) fn to_optional_u16(self) -> Option { - match self { - Self::Max => None, - Self::Year(y) => Some(y), - } - } -} - -impl TryFromStr for ToYear { - type Error = ZoneInfoParseError; - - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - if s == "max" { - return Ok(ToYear::Max); - } - s.context_parse::(ctx).map(ToYear::Year) - } -} - -// The default implementation -#[derive(Debug, Clone, Copy, PartialEq)] -#[repr(u8)] -pub enum Month { - Jan = 1, - Feb, - Mar, - Apr, - May, - Jun, - Jul, - Aug, - Sep, - Oct, - Nov, - Dec, -} - -impl Month { - /// Calculates the day of year for the start of the month - pub(crate) fn month_start_to_day_of_year(self, year: i32) -> i32 { - utils::month_to_day(self as u8, utils::num_leap_days(year)) - } - - /// Calculates the day of year for the end of the month - pub(crate) fn month_end_to_day_of_year(self, year: i32) -> i32 { - utils::month_to_day(self as u8 + 1, utils::num_leap_days(year)) - 1 - } -} - -impl TryFromStr for Month { - type Error = ZoneInfoParseError; - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - ctx.enter("Month"); - let result = match s { - "Jan" => Ok(Self::Jan), - "Feb" => Ok(Self::Feb), - "Mar" => Ok(Self::Mar), - "Apr" => Ok(Self::Apr), - "May" => Ok(Self::May), - "Jun" => Ok(Self::Jun), - "Jul" => Ok(Self::Jul), - "Aug" => Ok(Self::Aug), - "Sep" => Ok(Self::Sep), - "Oct" => Ok(Self::Oct), - "Nov" => Ok(Self::Nov), - "Dec" => Ok(Self::Dec), - _ => Err(ZoneInfoParseError::unknown(s, ctx)), - }; - ctx.exit(); - result - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum DayOfMonth { - // Again, hacky default. Not a fan - Last(WeekDay), - WeekDayGEThanMonthDay(WeekDay, u8), - // Potentially, depracated - WeekDayLEThanMonthDay(WeekDay, u8), - Day(u8), -} - -impl TryFromStr for DayOfMonth { - type Error = ZoneInfoParseError; - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - ctx.enter("DayOfMonth"); - let result = if let Some(weekday) = s.strip_prefix("last") { - Ok(DayOfMonth::Last(weekday.context_parse(ctx)?)) - } else if s.contains(">=") { - let (week_day, day) = parse_date_split(s, ">=", ctx)?; - Ok(DayOfMonth::WeekDayGEThanMonthDay(week_day, day)) - } else if s.contains("<=") { - let (week_day, day) = parse_date_split(s, "<=", ctx)?; - Ok(DayOfMonth::WeekDayLEThanMonthDay(week_day, day)) - } else { - s.context_parse(ctx).map(DayOfMonth::Day) - }; - ctx.exit(); - result - } -} - -fn parse_date_split( - s: &str, - pat: &str, - ctx: &mut LineParseContext, -) -> Result<(WeekDay, u8), ZoneInfoParseError> { - let (week_day, num) = s - .split_once(pat) - .ok_or(ZoneInfoParseError::unknown(s, ctx))?; - let w = week_day.context_parse::(ctx)?; - let d = num.context_parse(ctx)?; - Ok((w, d)) -} - -#[derive(Debug, Clone, Copy, PartialEq)] -#[repr(u8)] -pub enum WeekDay { - Sun = 0, - Mon, - Tues, - Wed, - Thurs, - Fri, - Sat, -} - -impl WeekDay { - pub(crate) fn from_u8(value: u8) -> Self { - match value { - 0 => Self::Sun, - 1 => Self::Mon, - 2 => Self::Tues, - 3 => Self::Wed, - 4 => Self::Thurs, - 5 => Self::Fri, - 6 => Self::Sat, - _ => unreachable!("invalid week day value"), - } - } -} - -impl TryFromStr for WeekDay { - type Error = ZoneInfoParseError; - fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { - match s { - "Mon" => Ok(Self::Mon), - "Tues" => Ok(Self::Tues), - "Wed" => Ok(Self::Wed), - "Thu" => Ok(Self::Thurs), - "Fri" => Ok(Self::Fri), - "Sat" => Ok(Self::Sat), - "Sun" => Ok(Self::Sun), - _ => Err(ZoneInfoParseError::UnknownValue( - ctx.line_number, - s.to_owned(), - )), - } - } -} - +/// This enum represents whether a time is local, standard, or universal. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QualifiedTimeKind { Local, @@ -558,9 +271,10 @@ where mod tests { use alloc::borrow::ToOwned; - use crate::types::FormattableAbbr; - - use super::{AbbreviationFormat, Sign, Time}; + use super::{ + zone::{AbbreviationFormat, FormattableAbbr}, + Sign, Time, + }; #[test] fn abbr_formatting() { diff --git a/zoneinfo/src/types/rule.rs b/zoneinfo/src/types/rule.rs new file mode 100644 index 000000000..d5ad359fe --- /dev/null +++ b/zoneinfo/src/types/rule.rs @@ -0,0 +1,148 @@ +//! Types used to represent a Rule line in a zoneinfo file. + +use alloc::borrow::ToOwned; + +use crate::parser::{ContextParse, LineParseContext, TryFromStr, ZoneInfoParseError}; + +/// The value present in the `TO` column of a rule line. +/// +/// This value can either be "max" or an unsigned integer representing the year. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ToYear { + Max, + Year(u16), +} + +impl ToYear { + pub(crate) fn parse_optional_to_year( + s: &str, + ctx: &mut LineParseContext, + ) -> Result, ZoneInfoParseError> { + if s == "only" { + Ok(None) + } else { + s.context_parse::(ctx).map(Some) + } + } + + pub(crate) fn to_i32(self) -> i32 { + match self { + Self::Max => 275_760, + Self::Year(y) => y as i32, + } + } + + pub(crate) fn to_optional_u16(self) -> Option { + match self { + Self::Max => None, + Self::Year(y) => Some(y), + } + } +} + +impl TryFromStr for ToYear { + type Error = ZoneInfoParseError; + + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + if s == "max" { + return Ok(ToYear::Max); + } + s.context_parse::(ctx).map(ToYear::Year) + } +} + +/// The day of the month as listed by the `ON` column of a rule line. +/// +/// The values can be a day, a GE or LE identifier (Sun>=8), or "lastSun", which +/// represents the last sunday of the month. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DayOfMonth { + // Again, hacky default. Not a fan + Last(WeekDay), + WeekDayGEThanMonthDay(WeekDay, u8), + // Potentially, depracated + WeekDayLEThanMonthDay(WeekDay, u8), + Day(u8), +} + +impl TryFromStr for DayOfMonth { + type Error = ZoneInfoParseError; + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + ctx.enter("DayOfMonth"); + let result = if let Some(weekday) = s.strip_prefix("last") { + Ok(DayOfMonth::Last(weekday.context_parse(ctx)?)) + } else if s.contains(">=") { + let (week_day, day) = parse_date_split(s, ">=", ctx)?; + Ok(DayOfMonth::WeekDayGEThanMonthDay(week_day, day)) + } else if s.contains("<=") { + let (week_day, day) = parse_date_split(s, "<=", ctx)?; + Ok(DayOfMonth::WeekDayLEThanMonthDay(week_day, day)) + } else { + s.context_parse(ctx).map(DayOfMonth::Day) + }; + ctx.exit(); + result + } +} + +fn parse_date_split( + s: &str, + pat: &str, + ctx: &mut LineParseContext, +) -> Result<(WeekDay, u8), ZoneInfoParseError> { + let (week_day, num) = s + .split_once(pat) + .ok_or(ZoneInfoParseError::unknown(s, ctx))?; + let w = week_day.context_parse::(ctx)?; + let d = num.context_parse(ctx)?; + Ok((w, d)) +} + +/// A week day value, this is used in the `ON` column values. +/// +/// NOTE: week days are zero based beginning with Sunday. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum WeekDay { + Sun = 0, + Mon, + Tues, + Wed, + Thurs, + Fri, + Sat, +} + +impl WeekDay { + pub(crate) fn from_u8(value: u8) -> Self { + match value { + 0 => Self::Sun, + 1 => Self::Mon, + 2 => Self::Tues, + 3 => Self::Wed, + 4 => Self::Thurs, + 5 => Self::Fri, + 6 => Self::Sat, + _ => unreachable!("invalid week day value"), + } + } +} + +impl TryFromStr for WeekDay { + type Error = ZoneInfoParseError; + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + match s { + "Mon" => Ok(Self::Mon), + "Tues" => Ok(Self::Tues), + "Wed" => Ok(Self::Wed), + "Thu" => Ok(Self::Thurs), + "Fri" => Ok(Self::Fri), + "Sat" => Ok(Self::Sat), + "Sun" => Ok(Self::Sun), + _ => Err(ZoneInfoParseError::UnknownValue( + ctx.line_number, + s.to_owned(), + )), + } + } +} diff --git a/zoneinfo/src/types/zone.rs b/zoneinfo/src/types/zone.rs new file mode 100644 index 000000000..fee0b6283 --- /dev/null +++ b/zoneinfo/src/types/zone.rs @@ -0,0 +1,189 @@ +//! Types used to define a zone table line. + +use core::fmt::Write; + +use alloc::{borrow::ToOwned, string::String}; + +use crate::{ + parser::{next_split, ContextParse, LineParseContext, TryFromStr, ZoneInfoParseError}, + rule::epoch_days_for_rule_date, + types::{rule::DayOfMonth, Month, QualifiedTime, Time}, + utils, +}; + +/// The value in the `NAME` column of a zone table that identifies the +/// active rule for that line. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum RuleIdentifier { + None, + Numeric(Time), + Named(String), +} + +impl TryFromStr for RuleIdentifier { + type Error = ZoneInfoParseError; + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + ctx.enter("RuleIdentifier"); + if s == "-" { + ctx.exit(); + return Ok(Self::None); + } + if s.contains(":") { + ctx.exit(); + return Time::try_from_str(s, ctx).map(Self::Numeric); + } + ctx.exit(); + Ok(Self::Named(s.to_owned())) + } +} + +/// [`AbbreviationFormat`] is the value present in the `FORMAT` column of +/// a zone line +#[derive(Debug, Clone, PartialEq)] +pub enum AbbreviationFormat { + String(String), + Numeric, + Pair(String, String), + Formattable(FormattableAbbr), +} + +impl AbbreviationFormat { + pub fn format(&self, offset: i64, letter: Option<&str>, is_dst: bool) -> String { + match self { + Self::String(s) => s.clone(), + Self::Formattable(s) => s.to_formatted_string(letter.unwrap_or("")), + Self::Pair(std, dst) => { + if is_dst { + dst.clone() + } else { + std.clone() + } + } + Self::Numeric => offset_to_str(offset), + } + } +} + +fn offset_to_str(n: i64) -> String { + let mut output = String::new(); + if n.is_positive() { + write!(&mut output, "+").expect("failed to write"); + } else { + write!(&mut output, "-").expect("failed to write"); + } + let hour = n.abs().div_euclid(3600); + write!(&mut output, "{hour:02}").expect("failed to write"); + let minute = n.abs().rem_euclid(3600).div_euclid(60); + if minute > 0 { + write!(&mut output, "{minute:02}").expect("failed to write"); + } + output +} + +impl TryFromStr for AbbreviationFormat { + type Error = ZoneInfoParseError; + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + ctx.enter("Abbr. Format"); + let value = if s.contains("%s") { + Ok(Self::Formattable(FormattableAbbr(s.to_owned()))) + } else if s.contains("%z") { + Ok(Self::Numeric) + } else if s.contains("/") { + let (std, dst) = s + .split_once('/') + .ok_or(ZoneInfoParseError::unknown(s, ctx))?; + Ok(Self::Pair(std.to_owned(), dst.to_owned())) + } else { + Ok(AbbreviationFormat::String(s.to_owned())) + }; + ctx.exit(); + value + } +} + +/// A formattable abbreviation (e.g. `C%sT`) +/// +/// This type will need to be further formatted with a `LETTER` value from +/// the active rule. +#[derive(Debug, Clone, PartialEq)] +pub struct FormattableAbbr(pub(crate) String); + +impl FormattableAbbr { + pub fn to_formatted_string(&self, letter: &str) -> String { + self.0.replace("%s", letter) + } +} + +/// Represents the value in the `[UNTIL]` column, which designates the final instant +/// that the current zone line is active. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct UntilDateTime { + pub date: Date, + pub time: QualifiedTime, +} + +impl UntilDateTime { + pub fn as_date_secs(self) -> i64 { + self.date.as_secs() + } + + pub fn as_precise_ut_time(self, std_offset: i64, save: i64) -> i64 { + self.as_date_secs() + self.time.to_universal_seconds(std_offset, save) + } +} + +impl TryFromStr for UntilDateTime { + type Error = ZoneInfoParseError; + fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { + ctx.enter("UntilDateTime"); + let mut splits = s.split_whitespace(); + let year = next_split(&mut splits, ctx)?.context_parse::(ctx)?; + let date_or_end = splits.next(); + let date = if let Some(month) = date_or_end { + let month = month.context_parse::(ctx)?; + let day = next_split(&mut splits, ctx) + .ok() + .map(|s| s.context_parse::(ctx)) + .transpose()? + .unwrap_or(DayOfMonth::Day(1)); + Date { year, month, day } + } else { + ctx.exit(); + return Ok(UntilDateTime { + date: Date { + year, + month: Month::Jan, + day: DayOfMonth::Day(1), + }, + time: QualifiedTime::Local(Time::default()), + }); + }; + + let time = next_split(&mut splits, ctx) + .ok() + .map(|t| t.context_parse::(ctx)) + .transpose()? + .unwrap_or(QualifiedTime::Local(Time::default())); + + ctx.exit(); + Ok(Self { date, time }) + } +} + +/// The date portion of an UNTIL date. +/// +/// This is typically represented as YEAR, MONTH, DAY-OF-MONTH +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Date { + pub year: i32, + pub month: Month, + pub day: DayOfMonth, +} + +impl Date { + pub fn as_secs(&self) -> i64 { + let epoch_days = epoch_days_for_rule_date(self.year, self.month, self.day); + utils::epoch_seconds_for_epoch_days(epoch_days) + } +} diff --git a/zoneinfo/src/zone.rs b/zoneinfo/src/zone.rs index 80813a635..fcbfd168b 100644 --- a/zoneinfo/src/zone.rs +++ b/zoneinfo/src/zone.rs @@ -12,7 +12,10 @@ use crate::{ }, posix::PosixTimeZone, rule::Rules, - types::{AbbreviationFormat, QualifiedTimeKind, RuleIdentifier, Time, UntilDateTime}, + types::{ + zone::{AbbreviationFormat, RuleIdentifier, UntilDateTime}, + QualifiedTimeKind, Time, + }, }; /// The zone build context. @@ -748,8 +751,7 @@ mod tests { use crate::{ parser::{LineParseContext, TryFromStr}, types::{ - AbbreviationFormat, Date, DayOfMonth, Month, QualifiedTime, RuleIdentifier, Sign, Time, - UntilDateTime, + zone::{AbbreviationFormat, Date, RuleIdentifier, UntilDateTime}, rule::DayOfMonth, Month, QualifiedTime, Sign, Time, }, };