use super::parse_java_i32;
const CUTOVER_DAYS: i64 = -141_427;
pub(crate) fn parse_epoch_milliseconds(text: &str) -> Option<i64> {
let units: Vec<u16> = text.encode_utf16().collect();
let (negative_era, mut at) = match units.first().copied() {
Some(unit) if unit == u16::from(b'-') => (true, 1),
Some(unit) if unit == u16::from(b'+') => (false, 1),
_ => (false, 0),
};
let year = field(&units, at, 4)?;
at += 4;
at = delimiter(&units, at, b'-')?;
let month = field(&units, at, 2)?;
at += 2;
at = delimiter(&units, at, b'-')?;
let day = field(&units, at, 2)?;
at += 2;
at = delimiter(&units, at, b'T')?;
let hour = field(&units, at, 2)?;
at += 2;
at = delimiter(&units, at, b':')?;
let minute = field(&units, at, 2)?;
at += 2;
at = delimiter(&units, at, b':')?;
let second = field(&units, at, 2)?;
at += 2;
at = delimiter(&units, at, b'.')?;
let millisecond = field(&units, at, 3)?;
at += 3;
let offset_minutes = zone_offset_minutes(units.get(at..)?)?;
let (calendar_year, astronomical_year) = if negative_era || year == 0 {
(year + 1, -year)
} else {
(year, year)
};
if calendar_year < 1 || !(-9999..=9999).contains(&astronomical_year) {
return None;
}
if !(1..=12).contains(&month)
|| day < 1
|| !(0..=23).contains(&hour)
|| !(0..=59).contains(&minute)
|| !(0..=59).contains(&second)
|| !(0..=999).contains(&millisecond)
{
return None;
}
let days = days_since_epoch(
i64::from(astronomical_year),
i64::from(month),
i64::from(day),
)?;
let seconds =
days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second)
- i64::from(offset_minutes) * 60;
Some(seconds * 1_000 + i64::from(millisecond))
}
fn field(units: &[u16], at: usize, width: usize) -> Option<i32> {
parse_java_i32(units.get(at..at + width)?)
}
fn delimiter(units: &[u16], at: usize, expected: u8) -> Option<usize> {
(*units.get(at)? == u16::from(expected)).then_some(at + 1)
}
fn zone_offset_minutes(designator: &[u16]) -> Option<i32> {
let text = String::from_utf16(designator).ok()?;
if matches!(text.as_str(), "" | "Z" | "0") {
return Some(0);
}
let bytes = text.as_bytes();
if bytes.len() != 6 || bytes[3] != b':' {
return None;
}
let sign = match bytes[0] {
b'+' => 1,
b'-' => -1,
_ => return None,
};
let digit = |at: usize| -> Option<i32> {
bytes[at]
.is_ascii_digit()
.then(|| i32::from(bytes[at] - b'0'))
};
let hours = digit(1)? * 10 + digit(2)?;
let minutes = digit(4)? * 10 + digit(5)?;
if hours > 23 || minutes > 59 {
return None;
}
Some(sign * (hours * 60 + minutes))
}
fn days_since_epoch(year: i64, month: i64, day: i64) -> Option<i64> {
let gregorian = day_number(year, month, day, true);
if gregorian >= CUTOVER_DAYS {
return (day <= days_in_month(year, month, true)).then_some(gregorian);
}
let julian = day_number(year, month, day, false);
if julian >= CUTOVER_DAYS {
return None;
}
(day <= days_in_month(year, month, false)).then_some(julian)
}
fn day_number(year: i64, month: i64, day: i64, gregorian: bool) -> i64 {
let leap_shift = (14 - month).div_euclid(12);
let shifted_year = year + 4800 - leap_shift;
let shifted_month = month + 12 * leap_shift - 3;
let base = day
+ (153 * shifted_month + 2).div_euclid(5)
+ 365 * shifted_year
+ shifted_year.div_euclid(4);
let julian_day = if gregorian {
base - shifted_year.div_euclid(100) + shifted_year.div_euclid(400) - 32_045
} else {
base - 32_083
};
julian_day - 2_440_588
}
fn days_in_month(year: i64, month: i64, gregorian: bool) -> i64 {
const LENGTHS: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if month == 2 && is_leap_year(year, gregorian) {
return 29;
}
LENGTHS[(month - 1) as usize]
}
fn is_leap_year(year: i64, gregorian: bool) -> bool {
if year.rem_euclid(4) != 0 {
return false;
}
!gregorian || year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0
}