Merge pull request #599 from MarekSpodBiedry:issue-565

This commit is contained in:
Brandon Thomas 2026-10-12 23:35:48 -04:00
commit 8d3e440a8b
3 changed files with 23 additions and 4 deletions

View File

@ -103,6 +103,14 @@ 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) }
}
/// Last serial that is a valid date in a date system: 9999-12-31 (2958465 in the 1900 system, 2957003 in the 1904 system).
pub fn max_serial(sys: DateSystem) -> i64 {
match sys {
DateSystem::D1900 => 2_958_465,
DateSystem::D1904 => 2_958_465 - 1462,
}
}
/// 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), 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.
@ -125,7 +133,7 @@ pub fn serial_from_ymd(sys: DateSystem, year: i64, month: i64, day: i64) -> Opti
DateSystem::D1904 => first - UNIX_1904_01_01,
};
let serial = first.checked_add(day)?.checked_sub(1)?;
if !(0..=2_958_465).contains(&serial) {
if !(0..=max_serial(sys)).contains(&serial) {
return None;
}
Some(serial as f64)
@ -133,7 +141,7 @@ pub fn serial_from_ymd(sys: DateSystem, year: i64, month: i64, day: i64) -> Opti
/// Date/time from a serial. `None` for negative or too-large serials.
pub fn datetime_from_serial(sys: DateSystem, serial: f64) -> Option<DateTime> {
if !serial.is_finite() || !(0.0..2_958_466.0).contains(&serial) {
if !serial.is_finite() || !(0.0..max_serial(sys) as f64 + 1.0).contains(&serial) {
return None;
}
let mut day = serial.floor() as i64;
@ -205,6 +213,10 @@ mod tests {
assert_eq!(serial_from_ymd(s, 2000, 1, 1), Some(36526.0));
assert_eq!(serial_from_ymd(s, 2026, 10, 7), Some(46302.0));
assert_eq!(serial_from_ymd(s, 9999, 12, 31), Some(2958465.0));
assert_eq!(serial_from_ymd(DateSystem::D1904, 9999, 12, 31), Some(2957003.0));
assert_eq!(serial_from_ymd(DateSystem::D1904, 9999, 12, 32), None);
assert!(datetime_from_serial(DateSystem::D1904, 2957003.5).is_some());
assert!(datetime_from_serial(DateSystem::D1904, 2957004.0).is_none());
assert_eq!(serial_from_ymd(s, 2020, 13, 1), serial_from_ymd(s, 2021, 1, 1));
assert_eq!(serial_from_ymd(s, 2020, 3, 0), serial_from_ymd(s, 2020, 2, 29));
assert_eq!(serial_from_ymd(s, 120, 1, 1), serial_from_ymd(s, 2020, 1, 1));

View File

@ -1,7 +1,7 @@
//! Rendering a value with a parsed section.
use gridcraft_core::DateSystem;
use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial};
use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, max_serial};
use crate::decimal::{decimal_digits, fixed, fixed_from_digits, format_general_fit};
use crate::parse::{DatePart, Section, Tok};
@ -58,7 +58,7 @@ pub(crate) fn render_general(sec: &Section, v: f64, width: usize) -> Out {
/// Renders a date/time. `None` when the serial is outside the supported range.
pub(crate) fn render_date(sec: &Section, v: f64, sys: DateSystem) -> Option<Out> {
if !v.is_finite() || !(0.0..2_958_466.0).contains(&v) {
if !v.is_finite() || !(0.0..max_serial(sys) as f64 + 1.0).contains(&v) {
return None;
}
let sub_digits = sec.toks.iter().filter_map(|t| if let Tok::Date(DatePart::SubSec(n)) = t { Some(*n) } else { None }).max().unwrap_or(0).min(3);

View File

@ -247,6 +247,11 @@ fn dates_and_times() {
assert_eq!(f("m/d/yyyy", 60.0), "2/29/1900");
assert_eq!(f("m/d/yyyy", 0.0), "1/0/1900");
assert_eq!(format_value(&Value::Number(0.0), &NumberFormat::parse("m/d/yyyy"), DateSystem::D1904).text, "1/1/1904");
let fmt = NumberFormat::parse("m/d/yyyy");
assert_eq!(format_value(&Value::Number(2957003.5), &fmt, DateSystem::D1904).text, "12/31/9999");
assert_eq!(format_value(&Value::Number(2957004.0), &fmt, DateSystem::D1904).text, "########");
assert_eq!(format_value(&Value::Number(2958465.0), &fmt, DateSystem::D1900).text, "12/31/9999");
assert_eq!(format_value(&Value::Number(2958466.0), &fmt, DateSystem::D1900).text, "########");
assert_eq!(f("m/d/yyyy", -1.0), "########");
assert_eq!(f("m/d/yyyy", 3e6), "########");
assert_eq!(f("[$-409]m/d/yyyy", 46302.0), "10/7/2026");
@ -339,6 +344,8 @@ fn text_fn() {
assert_eq!(text_function(&Value::Empty, "0.00", S), Ok("0.00".into()));
assert_eq!(text_function(&Value::Number(5.0), "", S), Ok("".into()));
assert_eq!(text_function(&Value::Number(-1.0), "m/d/yyyy", S), Err(CellError::Value));
assert_eq!(text_function(&Value::Number(2957003.0), "m/d/yyyy", DateSystem::D1904), Ok("12/31/9999".into()));
assert_eq!(text_function(&Value::Number(2957004.0), "m/d/yyyy", DateSystem::D1904), Err(CellError::Value));
assert_eq!(text_function(&Value::Bool(true), "0", S), Ok("TRUE".into()));
// Booleans go through the text section as TRUE/FALSE (observed Excel results).
let cases: &[(bool, &str, &str)] = &[