Skip to content

Commit 0954207

Browse files
committed
Add posix time zone string support
1 parent 2f115fe commit 0954207

File tree

8 files changed

+507
-26
lines changed

8 files changed

+507
-26
lines changed

provider/src/tzif.rs

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

7878
Self {
7979
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/lib.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use std::{io, path::Path};
2727
pub(crate) mod utils;
2828

2929
pub mod parser;
30+
pub mod posix;
3031
pub mod rule;
3132
pub mod types;
3233
pub mod tzif;
@@ -80,7 +81,7 @@ pub struct ZoneInfoLocalTimeRecord {
8081
pub struct ZoneInfoTransitionData {
8182
pub lmt: ZoneInfoLocalTimeRecord,
8283
pub transitions: BTreeSet<Transition>,
83-
pub posix_string: String, // TODO: Implement POSIX string building
84+
pub posix_string: String,
8485
}
8586

8687
impl ZoneInfoTransitionData {
@@ -183,12 +184,15 @@ impl ZoneInfoCompiler {
183184
}
184185
}
185186

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

188192
ZoneInfoTransitionData {
189193
lmt,
190194
transitions,
191-
posix_string: String::default(),
195+
posix_string,
192196
}
193197
}
194198

zoneinfo/src/posix.rs

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

0 commit comments

Comments
 (0)