use core::fmt;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleKind {
Julian,
DayOfYear,
MonthWeekDay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransitionRule {
pub kind: RuleKind,
pub day: i32,
pub week: i32,
pub mon: i32,
pub time: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PosixTz<'a> {
pub std_abbrev: &'a str,
pub std_offset: i32,
pub dst_abbrev: &'a str,
pub dst_offset: i32,
pub start: TransitionRule,
pub end: TransitionRule,
}
impl<'a> PosixTz<'a> {
pub fn has_dst(&self) -> bool {
!self.dst_abbrev.is_empty()
}
pub fn lookup(&self, unix: i64) -> (&'a str, i32, bool) {
if !self.has_dst() {
return (self.std_abbrev, self.std_offset, false);
}
let (year, yday, sec) = unix_to_yday_sec(unix);
let year_sec = yday * 86400 + sec;
let start_sec = rule_to_year_sec(self.start, year, self.std_offset);
let end_sec = rule_to_year_sec(self.end, year, self.dst_offset);
let in_dst = if start_sec < end_sec {
year_sec >= start_sec && year_sec < end_sec
} else {
year_sec >= start_sec || year_sec < end_sec
};
if in_dst {
(self.dst_abbrev, self.dst_offset, true)
} else {
(self.std_abbrev, self.std_offset, false)
}
}
pub fn transitions_for_year(&self, year: i32) -> Option<(i64, i64)> {
if !self.has_dst() {
return None;
}
let year_start = year_to_unix(year);
let start_sec = rule_to_year_sec(self.start, year, self.std_offset);
let end_sec = rule_to_year_sec(self.end, year, self.dst_offset);
Some((year_start + start_sec as i64, year_start + end_sec as i64))
}
}
impl fmt::Display for PosixTz<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_name(f, self.std_abbrev)?;
write_offset(f, -self.std_offset)?;
if !self.has_dst() {
return Ok(());
}
write_name(f, self.dst_abbrev)?;
if self.dst_offset != self.std_offset + 3600 {
write_offset(f, -self.dst_offset)?;
}
f.write_str(",")?;
write_rule(f, self.start)?;
f.write_str(",")?;
write_rule(f, self.end)
}
}
pub fn parse_posix_tz(s: &str) -> Result<PosixTz<'_>, Error> {
let (std_abbrev, rest) = parse_tz_name(s)?;
if std_abbrev.is_empty() {
return Err(Error::BadPosixTz("empty standard timezone name"));
}
let (off, rest) = parse_tz_offset(rest)?;
let std_offset = -off;
let mut p = PosixTz {
std_abbrev,
std_offset,
dst_abbrev: "",
dst_offset: 0,
start: DEFAULT_RULE,
end: DEFAULT_RULE,
};
if rest.is_empty() {
return Ok(p); }
let (dst_abbrev, rest) = parse_tz_name(rest)?;
if dst_abbrev.is_empty() {
return Err(Error::BadPosixTz("empty DST timezone name"));
}
p.dst_abbrev = dst_abbrev;
let rest = if !rest.is_empty() && !rest.starts_with(',') {
let (off, rest) = parse_tz_offset(rest)?;
p.dst_offset = -off;
rest
} else {
p.dst_offset = p.std_offset + 3600;
rest
};
if rest.is_empty() {
p.start = TransitionRule {
kind: RuleKind::MonthWeekDay,
mon: 3,
week: 2,
day: 0,
time: 7200,
};
p.end = TransitionRule {
kind: RuleKind::MonthWeekDay,
mon: 11,
week: 1,
day: 0,
time: 7200,
};
return Ok(p);
}
let rest = rest
.strip_prefix(',')
.ok_or(Error::BadPosixTz("expected ',' before transition rules"))?;
let (start, rest) = parse_tz_rule(rest)?;
p.start = start;
let rest = rest
.strip_prefix(',')
.ok_or(Error::BadPosixTz("expected ',' between transition rules"))?;
let (end, _rest) = parse_tz_rule(rest)?;
p.end = end;
Ok(p)
}
const DEFAULT_RULE: TransitionRule = TransitionRule {
kind: RuleKind::MonthWeekDay,
day: 0,
week: 0,
mon: 0,
time: 7200,
};
fn parse_tz_name(s: &str) -> Result<(&str, &str), Error> {
if s.is_empty() {
return Ok(("", ""));
}
let b = s.as_bytes();
if b[0] == b'<' {
let end = s
.find('>')
.ok_or(Error::BadPosixTz("unterminated '<' in TZ name"))?;
return Ok((&s[1..end], &s[end + 1..]));
}
let mut i = 0;
while i < b.len() && is_alpha(b[i]) {
i += 1;
}
Ok((&s[..i], &s[i..]))
}
fn parse_tz_offset(s: &str) -> Result<(i32, &str), Error> {
if s.is_empty() {
return Err(Error::BadPosixTz("expected offset"));
}
let mut rest = s;
let mut neg = false;
if let Some(r) = rest.strip_prefix('-') {
neg = true;
rest = r;
} else if let Some(r) = rest.strip_prefix('+') {
rest = r;
}
let (hours, mut rest) = parse_tz_num(rest, 0, 167)?;
let mut mins = 0;
let mut secs = 0;
if let Some(r) = rest.strip_prefix(':') {
let (m, r) = parse_tz_num(r, 0, 59)?;
mins = m;
rest = r;
if let Some(r) = rest.strip_prefix(':') {
let (sx, r) = parse_tz_num(r, 0, 59)?;
secs = sx;
rest = r;
}
}
let mut offset = hours * 3600 + mins * 60 + secs;
if neg {
offset = -offset;
}
Ok((offset, rest))
}
fn parse_tz_rule(s: &str) -> Result<(TransitionRule, &str), Error> {
if s.is_empty() {
return Err(Error::BadPosixTz("empty transition rule"));
}
let mut r = TransitionRule {
kind: RuleKind::DayOfYear,
day: 0,
week: 0,
mon: 0,
time: 7200,
};
let b = s.as_bytes();
let mut rest;
if b[0] == b'M' {
r.kind = RuleKind::MonthWeekDay;
let (mon, after) = parse_tz_num(&s[1..], 1, 12)?;
r.mon = mon;
rest = after
.strip_prefix('.')
.ok_or(Error::BadPosixTz("expected '.' after month in rule"))?;
let (week, after) = parse_tz_num(rest, 1, 5)?;
r.week = week;
rest = after
.strip_prefix('.')
.ok_or(Error::BadPosixTz("expected '.' after week in rule"))?;
let (day, after) = parse_tz_num(rest, 0, 6)?;
r.day = day;
rest = after;
} else if b[0] == b'J' {
r.kind = RuleKind::Julian;
let (day, after) = parse_tz_num(&s[1..], 1, 365)?;
r.day = day;
rest = after;
} else {
r.kind = RuleKind::DayOfYear;
let (day, after) = parse_tz_num(s, 0, 365)?;
r.day = day;
rest = after;
}
if let Some(after) = rest.strip_prefix('/') {
let (off, after) = parse_tz_offset(after)?;
r.time = off;
rest = after;
}
Ok((r, rest))
}
fn parse_tz_num(s: &str, min: i32, max: i32) -> Result<(i32, &str), Error> {
let b = s.as_bytes();
if b.is_empty() || !is_digit(b[0]) {
return Err(Error::BadPosixTz("expected digit"));
}
let mut n: i32 = 0;
let mut i = 0;
while i < b.len() && is_digit(b[i]) {
n = n * 10 + (b[i] - b'0') as i32;
i += 1;
}
if n < min || n > max {
return Err(Error::BadPosixTz("number out of range"));
}
Ok((n, &s[i..]))
}
fn is_alpha(c: u8) -> bool {
c.is_ascii_alphabetic()
}
fn is_digit(c: u8) -> bool {
c.is_ascii_digit()
}
fn is_leap_year(year: i32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
const DAYS_IN_MONTH: [i32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
fn year_to_unix(year: i32) -> i64 {
let y = year as i64 - 1970;
let mut days = 365 * y;
if year > 1970 {
days += (y + 1) / 4;
days -= (y + 69) / 100;
days += (y + 369) / 400;
} else if year < 1970 {
days += (y - 2) / 4;
days -= (y - 30) / 100;
days += (y - 30) / 400;
}
days * 86400
}
pub(crate) fn year_of(unix: i64) -> i32 {
unix_to_yday_sec(unix).0
}
fn unix_to_yday_sec(unix: i64) -> (i32, i32, i32) {
let mut unix = unix;
let mut sec = (unix % 86400) as i32;
if sec < 0 {
sec += 86400;
unix -= 86400;
}
let days = (unix / 86400) as i32;
let mut year = 1970 + days / 365;
loop {
let year_start = (year_to_unix(year) / 86400) as i32;
if year_start <= days {
let mut year_end = year_start + 365;
if is_leap_year(year) {
year_end += 1;
}
if days < year_end {
return (year, days - year_start, sec);
}
year += 1;
} else {
year -= 1;
}
}
}
fn rule_to_year_sec(r: TransitionRule, year: i32, offset: i32) -> i32 {
let leap = is_leap_year(year);
let yday = match r.kind {
RuleKind::Julian => {
let mut d = r.day - 1;
if leap && d >= 59 {
d += 1; }
d
}
RuleKind::DayOfYear => {
r.day
}
RuleKind::MonthWeekDay => {
let m = (r.mon - 1) as usize;
let mut first_yday = 0;
for (i, &dim) in DAYS_IN_MONTH.iter().enumerate().take(m) {
first_yday += dim;
if i == 1 && leap {
first_yday += 1;
}
}
let jan1_wday = (((year_to_unix(year) / 86400) % 7 + 4 + 7 * 53) % 7) as i32;
let first_wday = (jan1_wday + first_yday) % 7;
let days_until = (r.day - first_wday + 7) % 7;
let mut y = first_yday + days_until + (r.week - 1) * 7;
let mut month_days = DAYS_IN_MONTH[m];
if m == 1 && leap {
month_days += 1;
}
while y - first_yday >= month_days {
y -= 7;
}
y
}
};
yday * 86400 + r.time - offset
}
fn write_name(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
let needs_quote = name.bytes().any(|c| !is_alpha(c));
if needs_quote {
write!(f, "<{name}>")
} else {
f.write_str(name)
}
}
fn write_offset(f: &mut fmt::Formatter<'_>, posix_off: i32) -> fmt::Result {
let mut v = posix_off;
if v < 0 {
f.write_str("-")?;
v = -v;
}
let hours = v / 3600;
let mins = (v % 3600) / 60;
let secs = v % 60;
write!(f, "{hours}")?;
if mins != 0 || secs != 0 {
write!(f, ":{mins:02}")?;
if secs != 0 {
write!(f, ":{secs:02}")?;
}
}
Ok(())
}
fn write_rule(f: &mut fmt::Formatter<'_>, r: TransitionRule) -> fmt::Result {
match r.kind {
RuleKind::Julian => write!(f, "J{}", r.day)?,
RuleKind::DayOfYear => write!(f, "{}", r.day)?,
RuleKind::MonthWeekDay => write!(f, "M{}.{}.{}", r.mon, r.week, r.day)?,
}
if r.time != 7200 {
f.write_str("/")?;
write_offset(f, r.time)?;
}
Ok(())
}