use super::{State, TZ_MAX_TIMES, Tti, YEARS_PER_REPEAT};
const SECS_PER_HOUR: i64 = 3600;
const SECS_PER_DAY: i64 = 86400;
const EPOCH_YEAR: i64 = 1970;
const MON_LENGTHS: [[i64; 12]; 2] = [
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
];
const YEAR_LENGTHS: [i64; 2] = [365, 366];
const DEFAULT_RULES: &[u8] = b",M3.2.0,M11.1.0";
#[derive(Debug, Clone, Copy)]
enum Day {
Julian(i64),
OfYear(i64),
OfWeek { month: i64, week: i64, day: i64 },
}
#[derive(Debug, Clone, Copy)]
struct Rule {
day: Day,
time: i64,
}
fn is_leap(year: i64) -> usize {
usize::from(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))
}
fn at(text: &[u8], i: usize) -> u8 {
text.get(i).copied().unwrap_or(0)
}
fn getzname(text: &[u8], mut i: usize) -> usize {
while !matches!(at(text, i), 0 | b'0'..=b'9' | b',' | b'-' | b'+') {
i += 1;
}
i
}
fn getname(text: &[u8], i: usize) -> Option<(&[u8], usize)> {
if at(text, i) == b'<' {
let start = i + 1;
let mut end = start;
while !matches!(at(text, end), 0 | b'>') {
end += 1;
}
if at(text, end) != b'>' {
return None;
}
return Some((&text[start..end], end + 1));
}
let end = getzname(text, i);
Some((&text[i..end], end))
}
fn getnum(text: &[u8], mut i: usize, min: i64, max: i64) -> Option<(usize, i64)> {
if !at(text, i).is_ascii_digit() {
return None;
}
let mut num = 0;
while at(text, i).is_ascii_digit() {
num = num * 10 + i64::from(at(text, i) - b'0');
if num > max {
return None;
}
i += 1;
}
(num >= min).then_some((i, num))
}
fn getsecs(text: &[u8], i: usize) -> Option<(usize, i64)> {
let (mut i, hours) = getnum(text, i, 0, 24 * 7 - 1)?;
let mut secs = hours * SECS_PER_HOUR;
if at(text, i) == b':' {
let minutes;
(i, minutes) = getnum(text, i + 1, 0, 59)?;
secs += minutes * 60;
if at(text, i) == b':' {
let seconds;
(i, seconds) = getnum(text, i + 1, 0, 60)?;
secs += seconds;
}
}
Some((i, secs))
}
fn getoffset(text: &[u8], i: usize) -> Option<(usize, i64)> {
let (negative, i) = match at(text, i) {
b'-' => (true, i + 1),
b'+' => (false, i + 1),
_ => (false, i),
};
let (i, secs) = getsecs(text, i)?;
Some((i, if negative { -secs } else { secs }))
}
fn getrule(text: &[u8], i: usize) -> Option<(usize, Rule)> {
let (mut i, day) = match at(text, i) {
b'J' => {
let (i, day) = getnum(text, i + 1, 1, 365)?;
(i, Day::Julian(day))
}
b'M' => {
let (i, month) = getnum(text, i + 1, 1, 12)?;
if at(text, i) != b'.' {
return None;
}
let (i, week) = getnum(text, i + 1, 1, 5)?;
if at(text, i) != b'.' {
return None;
}
let (i, day) = getnum(text, i + 1, 0, 6)?;
(i, Day::OfWeek { month, week, day })
}
b'0'..=b'9' => {
let (i, day) = getnum(text, i, 0, 365)?;
(i, Day::OfYear(day))
}
_ => return None,
};
let mut time = 2 * SECS_PER_HOUR;
if at(text, i) == b'/' {
(i, time) = getoffset(text, i + 1)?;
}
Some((i, Rule { day, time }))
}
fn transtime(year: i64, rule: &Rule, offset: i64) -> i64 {
let leap = is_leap(year);
let value = match rule.day {
Day::Julian(day) => {
let mut value = (day - 1) * SECS_PER_DAY;
if leap == 1 && day >= 60 {
value += SECS_PER_DAY;
}
value
}
Day::OfYear(day) => day * SECS_PER_DAY,
Day::OfWeek { month, week, day } => {
let m1 = (month + 9) % 12 + 1;
let yy0 = if month <= 2 { year - 1 } else { year };
let (yy1, yy2) = (yy0 / 100, yy0 % 100);
let mut dow = ((26 * m1 - 2) / 10 + 1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7;
if dow < 0 {
dow += 7;
}
let mut d = day - dow;
if d < 0 {
d += 7;
}
let month = month as usize - 1;
for _ in 1..week {
if d + 7 >= MON_LENGTHS[leap][month] {
break;
}
d += 7;
}
d * SECS_PER_DAY + MON_LENGTHS[leap][..month].iter().sum::<i64>() * SECS_PER_DAY
}
};
value + rule.time + offset
}
pub(super) fn tzparse(text: &[u8], lastditch: bool) -> Option<State> {
let (stdname, stdoffset, mut i) = if lastditch {
(text, 0, text.len())
} else {
let (name, i) = getname(text, 0)?;
if at(text, i) == 0 {
return None;
}
let (i, offset) = getoffset(text, i)?;
(name, offset, i)
};
let mut state = State::default();
state.chars.extend_from_slice(stdname);
state.chars.push(0);
#[allow(clippy::cast_possible_truncation)]
let tti =
|offset: i64, isdst: bool, desigidx: usize| Tti { utoff: -offset as i32, isdst, desigidx };
if at(text, i) == 0 {
state.ttis.push(tti(stdoffset, false, 0));
return Some(state);
}
let (dstname, next) = getname(text, i)?;
i = next;
if dstname.is_empty() {
return None;
}
state.chars.extend_from_slice(dstname);
state.chars.push(0);
let mut dstoffset = stdoffset - SECS_PER_HOUR;
if !matches!(at(text, i), 0 | b',' | b';') {
(i, dstoffset) = getoffset(text, i)?;
}
let rules = if at(text, i) == 0 { DEFAULT_RULES } else { &text[i..] };
if !matches!(at(rules, 0), b',' | b';') {
return None;
}
let (i, start) = getrule(rules, 1)?;
if at(rules, i) != b',' {
return None;
}
let (i, end) = getrule(rules, i + 1)?;
if at(rules, i) != 0 {
return None;
}
state.ttis = vec![tti(stdoffset, false, 0), tti(dstoffset, true, stdname.len() + 1)];
let mut janfirst: i64 = 0;
let mut yearbeg = EPOCH_YEAR;
loop {
janfirst -= YEAR_LENGTHS[is_leap(yearbeg - 1)] * SECS_PER_DAY;
yearbeg -= 1;
if EPOCH_YEAR - YEARS_PER_REPEAT / 2 >= yearbeg {
break;
}
}
let mut yearlim = yearbeg + YEARS_PER_REPEAT + 1;
let mut year = yearbeg;
while year < yearlim {
let mut starttime = transtime(year, &start, stdoffset);
let mut endtime = transtime(year, &end, dstoffset);
let yearsecs = YEAR_LENGTHS[is_leap(year)] * SECS_PER_DAY;
let reversed = endtime < starttime;
if reversed {
std::mem::swap(&mut starttime, &mut endtime);
}
if reversed
|| (starttime < endtime && endtime - starttime < yearsecs + (stdoffset - dstoffset))
{
if TZ_MAX_TIMES - 2 < state.ats.len() {
break;
}
state.ats.push(janfirst + starttime);
state.types.push(u8::from(!reversed));
state.ats.push(janfirst + endtime);
state.types.push(u8::from(reversed));
yearlim = year + YEARS_PER_REPEAT + 1;
}
janfirst += yearsecs;
year += 1;
}
if state.ats.is_empty() {
state.ttis.remove(0);
} else if YEARS_PER_REPEAT < year - yearbeg {
state.goback = true;
state.goahead = true;
}
Some(state)
}