///|
let maximum_excel_serial_1900 = 2_958_465
///|
let maximum_excel_serial_1904 = 2_957_003
///|
/// Splits a finite, supported Excel serial after rounding its clock to whole
/// seconds. The returned day is also rounded when 23:59:59.5 spills into the
/// next date, and can therefore be checked before calendar conversion.
fn excel_serial_day_and_clock(
value : Double,
maximum_day : Int,
) -> (Int, Int, Int, Int)? {
if value.is_nan() ||
value.is_inf() ||
value < 0.0 ||
value >= Double::from_int(maximum_day + 1) {
return None
}
let mut serial_day = Double::to_int(Double::floor(value))
let fraction = value - Double::from_int(serial_day)
let mut total_seconds = Double::to_int(Double::round(fraction * 86400.0))
if total_seconds >= 86400 {
total_seconds = total_seconds - 86400
serial_day = serial_day + 1
}
if serial_day > maximum_day {
return None
}
Some(
(
serial_day,
total_seconds / 3600,
total_seconds % 3600 / 60,
total_seconds % 60,
),
)
}
///|
fn excel_serial_to_parts(value : Double) -> (Int, Int, Int, Int, Int, Int)? {
guard excel_serial_day_and_clock(value, maximum_excel_serial_1900)
is Some((serial_day, hour, minute, second)) else {
return None
}
if serial_day == 60 {
return Some((1900, 2, 29, hour, minute, second))
}
let civil_day = if serial_day > 59 { serial_day - 1 } else { serial_day }
let (year, month, day) = civil_from_days(
days_from_civil(1899, 12, 31) + civil_day,
)
Some((year, month, day, hour, minute, second))
}
///|
fn excel_serial_to_parts_1904(
value : Double,
) -> (Int, Int, Int, Int, Int, Int)? {
guard excel_serial_day_and_clock(value, maximum_excel_serial_1904)
is Some((serial_day, hour, minute, second)) else {
return None
}
let (year, month, day) = civil_from_days(
days_from_civil(1904, 1, 1) + serial_day,
)
Some((year, month, day, hour, minute, second))
}
///|
pub fn excel_date_to_time(
excel_date : Double,
use_1904_format? : Bool = false,
) -> @time.ZonedDateTime raise XlsxError {
if excel_date < 0.0 {
raise InvalidExcelDate(value=excel_date)
}
let parts = if use_1904_format {
excel_serial_to_parts_1904(excel_date)
} else {
excel_serial_to_parts(excel_date)
}
match parts {
Some((year, month, day, hour, minute, second)) =>
@time.date_time(
year,
month,
day,
hour~,
minute~,
second~,
nanosecond=0,
zone=@time.utc_zone,
) catch {
_ => raise InvalidExcelDate(value=excel_date)
}
None => raise InvalidExcelDate(value=excel_date)
}
}
///|
test "date convert wb: 1904 serial negative returns None" {
debug_inspect(excel_serial_to_parts_1904(-1.0), content="None")
}