Skip to content

Commit 261926e

Browse files
committed
Add posix time zone string support
1 parent 75661dc commit 261926e

File tree

8 files changed

+510
-14
lines changed

8 files changed

+510
-14
lines changed

provider/src/tzif.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl ZeroTzif<'_> {
6969
let mapped_local_records: Vec<LocalTimeRecord> =
7070
tzif.local_time_types.iter().map(Into::into).collect();
7171
let types = ZeroVec::alloc_from_slice(&mapped_local_records);
72-
let posix = String::from("TODO").into();
72+
let posix = Cow::from(data.posix_string.clone());
7373

7474
Self {
7575
transitions,

zoneinfo/examples/zoneinfo

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,33 @@ Rule Russia 1984 1995 - Sep lastSun 2:00s 0 -
304304
Rule Russia 1985 2010 - Mar lastSun 2:00s 1:00 S
305305
Rule Russia 1996 2010 - Oct lastSun 2:00s 0 -
306306

307+
# Minsk and Moscow are primarily added for their POSIX tz test case
308+
309+
Zone Europe/Minsk 1:50:16 - LMT 1880
310+
1:50 - MMT 1924 May 2 # Minsk Mean Time
311+
2:00 - EET 1930 Jun 21
312+
3:00 - MSK 1941 Jun 28
313+
1:00 C-Eur CE%sT 1944 Jul 3
314+
3:00 Russia MSK/MSD 1990
315+
3:00 - MSK 1991 Mar 31 2:00s
316+
2:00 Russia EE%sT 2011 Mar 27 2:00s
317+
3:00 - %z
318+
319+
Zone Europe/Moscow 2:30:17 - LMT 1880
320+
2:30:17 - MMT 1916 Jul 3 # Moscow Mean Time
321+
2:31:19 Russia %s 1919 Jul 1 0:00u
322+
3:00 Russia %s 1921 Oct
323+
3:00 Russia MSK/MSD 1922 Oct
324+
2:00 - EET 1930 Jun 21
325+
3:00 Russia MSK/MSD 1991 Mar 31 2:00s
326+
2:00 Russia EE%sT 1992 Jan 19 2:00s
327+
3:00 Russia MSK/MSD 2011 Mar 27 2:00s
328+
4:00 - MSK 2014 Oct 26 2:00s
329+
3:00 - MSK
330+
331+
332+
333+
307334
# Rule NAME FROM TO - IN ON AT SAVE LETTER/S
308335
Rule France 1916 only - Jun 14 23:00s 1:00 S
309336
Rule France 1916 1919 - Oct Sun>=1 23:00s 0 -

zoneinfo/src/compiler.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ pub struct CompiledTransitionsMap {
122122
// ==== ZoneInfoCompiler build / compile methods ====
123123

124124
use crate::{
125+
posix::PosixTimeZone,
125126
types::{QualifiedTimeKind, Time},
126127
tzif::TzifBlockV2,
127-
zone::ZoneBuildContext,
128+
zone::{ZoneBuildContext, ZoneRecord},
128129
ZoneInfoData,
129130
};
130131

@@ -185,15 +186,26 @@ impl ZoneInfoCompiler {
185186
}
186187
}
187188

188-
// TODO: POSIX tz string handling
189+
let posix_string = zone_table
190+
.get_posix_time_zone()
191+
.to_string()
192+
.expect("to_string only throws when a `write!` fails.");
189193

190194
CompiledTransitions {
191195
initial_record,
192196
transitions,
193-
posix_string: String::default(),
197+
posix_string,
194198
}
195199
}
196200

201+
pub fn get_posix_time_zone(&mut self, target: &str) -> Option<PosixTimeZone> {
202+
self.associate();
203+
self.data
204+
.zones
205+
.get(target)
206+
.map(ZoneRecord::get_posix_time_zone)
207+
}
208+
197209
/// Associates the current `ZoneTables` with their applicable rules.
198210
pub fn associate(&mut self) {
199211
for zones in self.data.zones.values_mut() {

zoneinfo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub(crate) mod utils;
5252

5353
pub mod compiler;
5454
pub mod parser;
55+
pub mod posix;
5556
pub mod rule;
5657
pub mod types;
5758
pub mod tzif;

zoneinfo/src/posix.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use crate::{
2+
rule::{LastRules, Rule},
3+
types::{DayOfMonth, Month, QualifiedTime, Sign, Time, WeekDay},
4+
utils::month_to_day,
5+
zone::ZoneEntry,
6+
};
7+
use alloc::string::String;
8+
use core::fmt::Write;
9+
10+
#[derive(Debug)]
11+
pub struct MonthWeekDay(pub Month, pub u8, pub WeekDay);
12+
13+
#[derive(Debug)]
14+
pub enum PosixDate {
15+
JulianNoLeap(u16),
16+
JulianLeap(u16),
17+
MonthWeekDay(MonthWeekDay),
18+
}
19+
20+
impl PosixDate {
21+
pub(crate) fn from_rule(rule: &Rule) -> Self {
22+
match rule.on_date {
23+
DayOfMonth::Day(day) if rule.in_month == Month::Jan || rule.in_month == Month::Feb => {
24+
PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16)
25+
}
26+
DayOfMonth::Day(day) => {
27+
PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16)
28+
}
29+
DayOfMonth::Last(wd) => PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, 5, wd)),
30+
DayOfMonth::WeekDayGEThanMonthDay(week_day, day_of_month) => {
31+
let week = 1 + (day_of_month - 1) / 7;
32+
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day))
33+
}
34+
DayOfMonth::WeekDayLEThanMonthDay(week_day, day_of_month) => {
35+
let week = day_of_month / 7;
36+
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day))
37+
}
38+
}
39+
}
40+
}
41+
42+
#[derive(Debug)]
43+
pub struct PosixDateTime {
44+
pub date: PosixDate,
45+
pub time: Time,
46+
}
47+
48+
impl PosixDateTime {
49+
pub(crate) fn from_rule_and_transition_info(rule: &Rule, offset: Time, savings: Time) -> Self {
50+
let date = PosixDate::from_rule(rule);
51+
let time = match rule.at {
52+
QualifiedTime::Local(time) => time,
53+
QualifiedTime::Standard(standard_time) => standard_time.add(rule.save),
54+
QualifiedTime::Universal(universal_time) => universal_time.add(offset).add(savings),
55+
};
56+
Self { date, time }
57+
}
58+
}
59+
60+
#[non_exhaustive]
61+
#[derive(Debug)]
62+
pub struct PosixTransition {
63+
pub abbr: PosixAbbreviation,
64+
pub savings: Time,
65+
pub start: PosixDateTime,
66+
pub end: PosixDateTime,
67+
}
68+
69+
#[non_exhaustive]
70+
#[derive(Debug)]
71+
pub struct PosixTimeZone {
72+
pub abbr: PosixAbbreviation,
73+
pub offset: Time,
74+
pub transition_info: Option<PosixTransition>,
75+
}
76+
77+
#[non_exhaustive]
78+
#[derive(Debug)]
79+
pub struct PosixAbbreviation {
80+
is_numeric: bool,
81+
formatted: String,
82+
}
83+
84+
impl PosixTimeZone {
85+
pub(crate) fn from_zone_and_savings(entry: &ZoneEntry, savings: Time) -> Self {
86+
let offset = entry.std_offset.add(savings);
87+
let formatted = entry
88+
.format
89+
.format(offset.as_secs(), None, savings != Time::default());
90+
let is_numeric = is_numeric(&formatted);
91+
let abbr = PosixAbbreviation {
92+
is_numeric,
93+
formatted,
94+
};
95+
Self {
96+
abbr,
97+
offset,
98+
transition_info: None,
99+
}
100+
}
101+
102+
pub(crate) fn from_zone_and_rules(entry: &ZoneEntry, rules: &LastRules) -> Self {
103+
let offset = entry.std_offset.add(rules.standard.save);
104+
let formatted = entry.format.format(
105+
entry.std_offset.as_secs(),
106+
rules.standard.letter.as_deref(),
107+
rules.standard.is_dst(),
108+
);
109+
let is_numeric = is_numeric(&formatted);
110+
let abbr = PosixAbbreviation {
111+
is_numeric,
112+
formatted,
113+
};
114+
115+
let transition_info = rules.saving.as_ref().map(|rule| {
116+
let formatted = entry.format.format(
117+
entry.std_offset.as_secs() + rule.save.as_secs(),
118+
rule.letter.as_deref(),
119+
rule.is_dst(),
120+
);
121+
let abbr = PosixAbbreviation {
122+
is_numeric,
123+
formatted,
124+
};
125+
let savings = rule.save;
126+
let start = PosixDateTime::from_rule_and_transition_info(
127+
rule,
128+
entry.std_offset,
129+
rules.standard.save,
130+
);
131+
let end = PosixDateTime::from_rule_and_transition_info(
132+
&rules.standard,
133+
entry.std_offset,
134+
rule.save,
135+
);
136+
PosixTransition {
137+
abbr,
138+
savings,
139+
start,
140+
end,
141+
}
142+
});
143+
144+
PosixTimeZone {
145+
abbr,
146+
offset,
147+
transition_info,
148+
}
149+
}
150+
}
151+
152+
impl PosixTimeZone {
153+
pub fn to_string(&self) -> Result<String, core::fmt::Error> {
154+
let mut posix_string = String::new();
155+
write_abbr(&self.abbr, &mut posix_string)?;
156+
write_inverted_time(&self.offset, &mut posix_string)?;
157+
158+
if let Some(transition_info) = &self.transition_info {
159+
write_abbr(&transition_info.abbr, &mut posix_string)?;
160+
if transition_info.savings != Time::one() {
161+
write_inverted_time(&self.offset.add(transition_info.savings), &mut posix_string)?;
162+
}
163+
write_date_time(&transition_info.start, &mut posix_string)?;
164+
write_date_time(&transition_info.end, &mut posix_string)?;
165+
}
166+
Ok(posix_string)
167+
}
168+
}
169+
170+
fn is_numeric(str: &str) -> bool {
171+
str.parse::<i16>().is_ok()
172+
}
173+
174+
fn write_abbr(posix_abbr: &PosixAbbreviation, output: &mut String) -> core::fmt::Result {
175+
if posix_abbr.is_numeric {
176+
write!(output, "<")?;
177+
write!(output, "{}", posix_abbr.formatted)?;
178+
write!(output, ">")?;
179+
return Ok(());
180+
}
181+
write!(output, "{}", posix_abbr.formatted)
182+
}
183+
184+
fn write_inverted_time(time: &Time, output: &mut String) -> core::fmt::Result {
185+
// Yep, it's inverted
186+
if time.sign == Sign::Positive && time.hour != 0 {
187+
write!(output, "-")?;
188+
}
189+
write_time(time, output)
190+
}
191+
192+
fn write_time(time: &Time, output: &mut String) -> core::fmt::Result {
193+
write!(output, "{}", time.hour)?;
194+
if time.minute == 0 && time.second == 0 {
195+
return Ok(());
196+
}
197+
write!(output, ":{}", time.minute)?;
198+
if time.second > 0 {
199+
write!(output, ":{}", time.second)?;
200+
}
201+
Ok(())
202+
}
203+
204+
fn write_date_time(datetime: &PosixDateTime, output: &mut String) -> core::fmt::Result {
205+
write!(output, ",")?;
206+
match datetime.date {
207+
PosixDate::JulianLeap(d) => write!(output, "{d}")?,
208+
PosixDate::JulianNoLeap(d) => write!(output, "J{d}")?,
209+
PosixDate::MonthWeekDay(MonthWeekDay(month, week, day)) => {
210+
write!(output, "M{}.{week}.{}", month as u8, day as u8)?
211+
}
212+
}
213+
if datetime.time != Time::two() {
214+
write!(output, "/")?;
215+
write_time(&datetime.time, output)?;
216+
}
217+
Ok(())
218+
}
219+
220+
#[cfg(test)]
221+
mod tests {
222+
#[cfg(feature = "std")]
223+
#[test]
224+
fn posix_string_test() {
225+
use std::path::Path;
226+
227+
use crate::{ZoneInfoCompiler, ZoneInfoData};
228+
229+
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
230+
let zoneinfo = ZoneInfoData::from_filepath(manifest_dir.join("examples/zoneinfo")).unwrap();
231+
232+
let mut zic = ZoneInfoCompiler::new(zoneinfo);
233+
234+
let chicago_posix = zic.get_posix_time_zone("America/Chicago").unwrap();
235+
assert_eq!(
236+
chicago_posix.to_string(),
237+
Ok("CST6CDT,M3.2.0,M11.1.0".into())
238+
);
239+
240+
let lord_howe_posix = zic.get_posix_time_zone("Australia/Lord_Howe").unwrap();
241+
assert_eq!(
242+
lord_howe_posix.to_string(),
243+
Ok("<+1030>-10:30<+11>-11,M10.1.0,M4.1.0".into())
244+
);
245+
246+
let troll_posix = zic.get_posix_time_zone("Antarctica/Troll").unwrap();
247+
assert_eq!(
248+
troll_posix.to_string(),
249+
Ok("<+00>0<+02>-2,M3.5.0/1,M10.5.0/3".into())
250+
);
251+
252+
let dublin_posix = zic.get_posix_time_zone("Europe/Dublin").unwrap();
253+
assert_eq!(
254+
dublin_posix.to_string(),
255+
Ok("IST-1GMT0,M10.5.0,M3.5.0/1".into())
256+
);
257+
258+
let minsk_posix = zic.get_posix_time_zone("Europe/Minsk").unwrap();
259+
assert_eq!(minsk_posix.to_string(), Ok("<+03>-3".into()));
260+
261+
let moscow_posix = zic.get_posix_time_zone("Europe/Moscow").unwrap();
262+
assert_eq!(moscow_posix.to_string(), Ok("MSK-3".into()));
263+
}
264+
}

0 commit comments

Comments
 (0)