mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 11:16:49 -06:00
Merge pull request #68 from LloydNicholson/fix/date-1900-feb-29
Issue #124: DATE(1900,2,29) is serial 60, as in Excel
This commit is contained in:
+69
-13
@@ -69,26 +69,34 @@ pub fn days_in_month(y: i32, m: u32) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Days in a month of a date system's calendar: like [`days_in_month`], except that February
|
||||
/// 1900 has 29 days in the 1900 system (serial 60 is the fictitious 1900-02-29).
|
||||
pub fn days_in_month_in(sys: DateSystem, y: i32, m: u32) -> u32 {
|
||||
if sys == DateSystem::D1900 && y == 1900 && m == 2 { 29 } else { days_in_month(y, m) }
|
||||
}
|
||||
|
||||
/// Serial for a date. Month and day overflow roll over like `DATE()` (month 13 = January next
|
||||
/// year, day 0 = last day of the previous month). `None` before the epoch or after 9999-12-31.
|
||||
/// year, day 0 = last day of the previous month), in the date system's own calendar: in the 1900
|
||||
/// system February 1900 has 29 days, so `(1900, 2, 29)` and `(1900, 3, 0)` are serial 60.
|
||||
/// `None` before the epoch or after 9999-12-31.
|
||||
pub fn serial_from_ymd(sys: DateSystem, year: i64, month: i64, day: i64) -> Option<f64> {
|
||||
// DATE(): years 0–1899 are added to 1900.
|
||||
let mut y = if (0..1900).contains(&year) { year + 1900 } else { year };
|
||||
let m0 = month - 1;
|
||||
y += m0.div_euclid(12);
|
||||
let y = if (0..1900).contains(&year) { year + 1900 } else { year };
|
||||
let m0 = month.checked_sub(1)?;
|
||||
let y = y.checked_add(m0.div_euclid(12))?;
|
||||
let m = m0.rem_euclid(12) + 1;
|
||||
if !(1900..=9999).contains(&y) && sys == DateSystem::D1900 || !(1904..=9999).contains(&y) && sys == DateSystem::D1904 {
|
||||
return None;
|
||||
}
|
||||
let base = days_from_civil(y as i32, m as u32, 1) + day - 1;
|
||||
let serial = match sys {
|
||||
DateSystem::D1900 => {
|
||||
let s = base - UNIX_1899_12_30;
|
||||
// Before the fictitious 1900-02-29 serials are one lower than the day count from 1899-12-30.
|
||||
if s <= 60 { s - 1 } else { s }
|
||||
}
|
||||
DateSystem::D1904 => base - UNIX_1904_01_01,
|
||||
let first = days_from_civil(y as i32, m as u32, 1);
|
||||
// Serial of the first of the month; the day (and any overflow) counts on from there.
|
||||
let first = match sys {
|
||||
// Up to February 1900 the serial is one lower than the day count from 1899-12-30: the
|
||||
// fictitious 1900-02-29 (serial 60) comes before 1 March.
|
||||
DateSystem::D1900 => first - UNIX_1899_12_30 - i64::from(y == 1900 && m <= 2),
|
||||
DateSystem::D1904 => first - UNIX_1904_01_01,
|
||||
};
|
||||
let serial = first.checked_add(day)?.checked_sub(1)?;
|
||||
if !(0..=2_958_465).contains(&serial) {
|
||||
return None;
|
||||
}
|
||||
@@ -168,10 +176,58 @@ mod tests {
|
||||
assert_eq!(serial_from_ymd(DateSystem::D1904, 2000, 1, 1), Some(35064.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fictitious_1900_02_29() {
|
||||
// Excel's 1900 calendar has a 29 February 1900 (serial 60); day and month overflow count
|
||||
// through it.
|
||||
let s = DateSystem::D1900;
|
||||
assert_eq!(serial_from_ymd(s, 1900, 2, 29), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 3, 0), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 2, 30), Some(61.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 1, 60), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 1, 61), Some(61.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 1, 400), Some(400.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 4, -30), Some(61.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 4, -31), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 3, -1), Some(59.0));
|
||||
assert_eq!(serial_from_ymd(s, 1901, -10, 29), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 1, 0), Some(0.0));
|
||||
assert_eq!(serial_from_ymd(s, 1900, 1, -1), None);
|
||||
assert_eq!(serial_from_ymd(s, 1900, 0, 1), None);
|
||||
// Later years keep the real calendar.
|
||||
assert_eq!(serial_from_ymd(s, 1901, 2, 29), serial_from_ymd(s, 1901, 3, 1));
|
||||
assert_eq!(serial_from_ymd(s, 1904, 2, 29), Some(1521.0));
|
||||
// Every serial up to 1900-12-31 comes back from its own year, month and day.
|
||||
for serial in 0..=366 {
|
||||
let d = datetime_from_serial(s, serial as f64).unwrap();
|
||||
assert_eq!(serial_from_ymd(s, d.year as i64, d.month as i64, d.day as i64), Some(serial as f64), "{d:?}");
|
||||
}
|
||||
assert_eq!(days_in_month_in(s, 1900, 2), 29);
|
||||
assert_eq!(days_in_month_in(s, 1901, 2), 28);
|
||||
assert_eq!(days_in_month_in(DateSystem::D1904, 1900, 2), 28);
|
||||
// The 1904 system has no fictitious day: 1904 is a real leap year.
|
||||
let s = DateSystem::D1904;
|
||||
assert_eq!(serial_from_ymd(s, 1904, 2, 29), Some(59.0));
|
||||
assert_eq!(serial_from_ymd(s, 1904, 3, 0), Some(59.0));
|
||||
assert_eq!(serial_from_ymd(s, 1904, 3, 1), Some(60.0));
|
||||
assert_eq!(serial_from_ymd(s, 1904, 1, 0), None);
|
||||
assert_eq!(serial_from_ymd(s, 1900, 2, 29), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_month_and_day() {
|
||||
let s = DateSystem::D1900;
|
||||
assert_eq!(serial_from_ymd(s, 2000, i64::MIN, 1), None);
|
||||
assert_eq!(serial_from_ymd(s, 2000, i64::MAX, 1), None);
|
||||
assert_eq!(serial_from_ymd(s, 2000, 1, i64::MAX), None);
|
||||
assert_eq!(serial_from_ymd(s, 2000, 1, i64::MIN), None);
|
||||
assert_eq!(serial_from_ymd(s, i64::MAX, 1, 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let s = DateSystem::D1900;
|
||||
for serial in [1.0, 59.0, 61.0, 36526.0, 46302.0, 2958465.0] {
|
||||
for serial in [1.0, 59.0, 60.0, 61.0, 36526.0, 46302.0, 2958465.0] {
|
||||
let d = datetime_from_serial(s, serial).unwrap();
|
||||
assert_eq!(serial_from_ymd(s, d.year as i64, d.month as i64, d.day as i64), Some(serial));
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ fn valid_ymd(sys: DateSystem, y: i64, m: i64, d: i64) -> Option<f64> {
|
||||
if !(1..=12).contains(&m) || d < 1 {
|
||||
return None;
|
||||
}
|
||||
let max = if y == 1900 && m == 2 { 29 } else { crate::date::days_in_month(y as i32, m as u32) as i64 };
|
||||
let max = crate::date::days_in_month_in(sys, y as i32, m as u32) as i64;
|
||||
if d > max {
|
||||
return None;
|
||||
}
|
||||
@@ -357,4 +357,19 @@ mod tests {
|
||||
assert_eq!(p("10/7/2026 18:00").value, Value::Number(46302.75));
|
||||
assert_eq!(p("25:00").value, Value::Number(25.0 / 24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fictitious_1900_02_29() {
|
||||
// Excel accepts 29 February 1900 (serial 60) and keeps 1 March 1900 at 61.
|
||||
assert_eq!(p("2/29/1900").value, Value::Number(60.0));
|
||||
assert_eq!(p("1900-02-29").value, Value::Number(60.0));
|
||||
assert_eq!(p("29-Feb-1900").value, Value::Number(60.0));
|
||||
assert_eq!(p("2/28/1900").value, Value::Number(59.0));
|
||||
assert_eq!(p("3/1/1900").value, Value::Number(61.0));
|
||||
assert_eq!(p("2/29/1901").value, Value::text("2/29/1901"));
|
||||
assert_eq!(p("2/30/1900").value, Value::text("2/30/1900"));
|
||||
// The 1904 system starts after it.
|
||||
assert_eq!(parse_input("2/29/1900", DateSystem::D1904).value, Value::text("2/29/1900"));
|
||||
assert_eq!(parse_input("2/29/1904", DateSystem::D1904).value, Value::Number(59.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Date and time functions.
|
||||
|
||||
use gridcraft_core::date::{DateTime, datetime_from_serial, days_in_month, is_leap, serial_from_ymd};
|
||||
use gridcraft_core::date::{DateTime, datetime_from_serial, days_in_month, days_in_month_in, is_leap, serial_from_ymd};
|
||||
use gridcraft_core::parse::parse_input;
|
||||
use gridcraft_core::{CellError, DateSystem, Value};
|
||||
|
||||
@@ -164,7 +164,7 @@ fn edate(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
if !(1900..=9999).contains(&y) {
|
||||
return Err(CellError::Num);
|
||||
}
|
||||
let dd = (d.day.max(1) as i64).min(days_in_month(y as i32, mo as u32) as i64);
|
||||
let dd = (d.day.max(1) as i64).min(days_in_month_in(sys, y as i32, mo as u32) as i64);
|
||||
num_val(ymd_serial(sys, y, mo, dd)? as f64)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ fn eomonth(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
if !(1900..=9999).contains(&y) {
|
||||
return Err(CellError::Num);
|
||||
}
|
||||
num_val(ymd_serial(sys, y, mo, days_in_month(y as i32, mo as u32) as i64)? as f64)
|
||||
num_val(ymd_serial(sys, y, mo, days_in_month_in(sys, y as i32, mo as u32) as i64)? as f64)
|
||||
}
|
||||
|
||||
fn datedif(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
@@ -652,6 +652,36 @@ mod tests {
|
||||
is_err(ev("EDATE", vec![n(1.0), n(1e12)]), CellError::Num);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fictitious_1900_02_29() {
|
||||
// Excel's 1900 calendar has 29 February 1900 = serial 60, and DATE counts day and month
|
||||
// overflow through it.
|
||||
assert_eq!(d(1900.0, 2.0, 29.0), 60.0);
|
||||
assert_eq!(d(1900.0, 3.0, 0.0), 60.0);
|
||||
assert_eq!(d(1900.0, 2.0, 28.0), 59.0);
|
||||
assert_eq!(d(1900.0, 3.0, 1.0), 61.0);
|
||||
assert_eq!(d(1900.0, 2.0, 30.0), 61.0);
|
||||
assert_eq!(d(1900.0, 1.0, 60.0), 60.0);
|
||||
assert_eq!(d(1900.0, 4.0, -31.0), 60.0);
|
||||
assert_eq!(d(1901.0, -10.0, 29.0), 60.0);
|
||||
assert_eq!(d(0.0, 2.0, 29.0), 60.0);
|
||||
assert_eq!(d(1900.0, 1.0, 0.0), 0.0);
|
||||
close(ev("DAY", vec![n(d(1900.0, 3.0, 0.0))]), 29.0);
|
||||
close(ev("DATEVALUE", vec![t("1900-02-29")]), 60.0);
|
||||
close(ev("DATEVALUE", vec![t("2/29/1900")]), 60.0);
|
||||
close(ev("DATEVALUE", vec![t("3/1/1900")]), 61.0);
|
||||
is_err(ev("DATEVALUE", vec![t("2/29/1901")]), CellError::Value);
|
||||
// EDATE and EOMONTH land on it as the end of February 1900.
|
||||
close(ev("EOMONTH", vec![n(d(1900.0, 1.0, 15.0)), n(1.0)]), 60.0);
|
||||
close(ev("EOMONTH", vec![n(60.0), n(0.0)]), 60.0);
|
||||
close(ev("EOMONTH", vec![n(d(1900.0, 3.0, 15.0)), n(-1.0)]), 60.0);
|
||||
close(ev("EOMONTH", vec![n(60.0), n(12.0)]), d(1901.0, 2.0, 28.0));
|
||||
close(ev("EDATE", vec![n(d(1900.0, 1.0, 31.0)), n(1.0)]), 60.0);
|
||||
close(ev("EDATE", vec![n(d(1900.0, 3.0, 31.0)), n(-1.0)]), 60.0);
|
||||
close(ev("EDATE", vec![n(60.0), n(1.0)]), d(1900.0, 3.0, 29.0));
|
||||
close(ev("EDATE", vec![n(60.0), n(12.0)]), d(1901.0, 2.0, 28.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn datedif_units() {
|
||||
let s = n(d(2001.0, 6.0, 1.0));
|
||||
@@ -755,5 +785,16 @@ mod tests {
|
||||
close(crate::call(spec, &[n(0.0)], &mut ctx), 6.0);
|
||||
let spec = crate::lookup("YEAR").unwrap();
|
||||
close(crate::call(spec, &[n(0.0)], &mut ctx), 1904.0);
|
||||
// No fictitious day here: 1904 is a real leap year and 1900 is before the epoch.
|
||||
let spec = crate::lookup("DATE").unwrap();
|
||||
close(crate::call(spec, &[n(1904.0), n(2.0), n(29.0)], &mut ctx), 59.0);
|
||||
close(crate::call(spec, &[n(1904.0), n(3.0), n(0.0)], &mut ctx), 59.0);
|
||||
close(crate::call(spec, &[n(1904.0), n(3.0), n(1.0)], &mut ctx), 60.0);
|
||||
is_err(crate::call(spec, &[n(1900.0), n(2.0), n(29.0)], &mut ctx), CellError::Num);
|
||||
let spec = crate::lookup("EOMONTH").unwrap();
|
||||
close(crate::call(spec, &[n(31.0), n(0.0)], &mut ctx), 59.0);
|
||||
let spec = crate::lookup("DATEVALUE").unwrap();
|
||||
close(crate::call(spec, &[t("2/29/1904")], &mut ctx), 59.0);
|
||||
is_err(crate::call(spec, &[t("2/29/1900")], &mut ctx), CellError::Value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user