Merge pull request #160 from itrycodinganditsbad/fix-month-fill-clamp
fill: clamp month and year date series to the month's last day # Conflicts: # crates/engine/src/tests.rs
This commit is contained in:
commit
b07249de75
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, serial_from_ymd};
|
||||
use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, days_in_month, serial_from_ymd};
|
||||
use gridcraft_core::{CellRef, RangeRef, Value};
|
||||
use gridcraft_model::{Cell, Formula};
|
||||
|
||||
|
|
@ -264,15 +264,25 @@ impl Pattern {
|
|||
}
|
||||
Pattern::Months { last, first, months } => {
|
||||
let base = if forward { *last } else { *first };
|
||||
let d = datetime_from_serial(gridcraft_core::DateSystem::D1900, base)?;
|
||||
let m = d.month as i64 + months * k * sign;
|
||||
let s = serial_from_ymd(gridcraft_core::DateSystem::D1900, d.year as i64, m, d.day as i64)?;
|
||||
let s = add_months_clamped(gridcraft_core::DateSystem::D1900, base, months.checked_mul(k * sign)?)?;
|
||||
Some(Value::number(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `serial` shifted by `months` calendar months, keeping the day of month but clamping it to the
|
||||
/// target month's last day (Jan 31 + 1 month = Feb 28/29). Always measured from the original date,
|
||||
/// so a series does not drift: Jan 31, Feb 28, Mar 31, Apr 30.
|
||||
fn add_months_clamped(sys: gridcraft_core::DateSystem, serial: f64, months: i64) -> Option<f64> {
|
||||
let d = datetime_from_serial(sys, serial)?;
|
||||
let total = (d.year as i64).checked_mul(12)?.checked_add(d.month as i64 - 1)?.checked_add(months)?;
|
||||
let year = total.div_euclid(12);
|
||||
let month = total.rem_euclid(12) + 1;
|
||||
let last = days_in_month(i32::try_from(year).ok()?, month as u32);
|
||||
serial_from_ymd(sys, year, month, (d.day as i64).min(last as i64))
|
||||
}
|
||||
|
||||
fn round15(x: f64) -> f64 {
|
||||
if x == 0.0 || !x.is_finite() {
|
||||
return x;
|
||||
|
|
@ -316,8 +326,8 @@ pub fn series(cx: &mut Ctx, sheet: usize, r: RangeRef, rows: bool, kind: &str, s
|
|||
"date" => {
|
||||
let d = datetime_from_serial(sys, v);
|
||||
match (unit, d) {
|
||||
("month", Some(d)) => serial_from_ymd(sys, d.year as i64, d.month as i64 + step as i64, d.day as i64).unwrap_or(v),
|
||||
("year", Some(d)) => serial_from_ymd(sys, d.year as i64 + step as i64, d.month as i64, d.day as i64).unwrap_or(v),
|
||||
("month", Some(_)) => (step as i64).checked_mul(i as i64).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v),
|
||||
("year", Some(_)) => (step as i64).checked_mul(i as i64 * 12).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v),
|
||||
("weekday", Some(_)) => {
|
||||
let mut n = v;
|
||||
let mut left = step.abs() as i64;
|
||||
|
|
|
|||
|
|
@ -1555,3 +1555,53 @@ fn cell_picture_cut_uses_live_source_after_edit() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ymd(y: i64, m: i64, d: i64) -> Value {
|
||||
Value::Number(gridcraft_core::date::serial_from_ymd(gridcraft_core::DateSystem::D1900, y, m, d).unwrap())
|
||||
}
|
||||
|
||||
fn series_date(seed: &str, range: &str, unit: &str) -> Session {
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": seed})).unwrap();
|
||||
s.execute("edit.fillSeries", json!({"range": range, "direction": "columns", "type": "date", "dateUnit": unit})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_series_month_clamps_to_month_end() {
|
||||
let s = series_date("2026-01-31", "A1:A4", "month");
|
||||
assert_eq!(v(&s, "A1"), ymd(2026, 1, 31));
|
||||
assert_eq!(v(&s, "A2"), ymd(2026, 2, 28));
|
||||
assert_eq!(v(&s, "A3"), ymd(2026, 3, 31));
|
||||
assert_eq!(v(&s, "A4"), ymd(2026, 4, 30));
|
||||
// Mid-month seeds are untouched.
|
||||
let s = series_date("2026-01-15", "A1:A4", "month");
|
||||
assert_eq!(v(&s, "A4"), ymd(2026, 4, 15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_series_month_leap_year() {
|
||||
let s = series_date("2028-01-31", "A1:A3", "month");
|
||||
assert_eq!(v(&s, "A2"), ymd(2028, 2, 29));
|
||||
assert_eq!(v(&s, "A3"), ymd(2028, 3, 31));
|
||||
let s = series_date("1996-01-31", "A1:A3", "month");
|
||||
assert_eq!(v(&s, "A2"), ymd(1996, 2, 29));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_series_year_clamps_leap_day() {
|
||||
let s = series_date("2028-02-29", "A1:A5", "year");
|
||||
assert_eq!(v(&s, "A2"), ymd(2029, 2, 28));
|
||||
assert_eq!(v(&s, "A5"), ymd(2032, 2, 29));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autofill_month_pattern_clamps_to_month_end() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["2026-01-31"], ["2026-03-31"]]})).unwrap();
|
||||
s.execute("edit.autoFill", json!({"source": "A1:A2", "target": "A1:A6"})).unwrap();
|
||||
assert_eq!(v(&s, "A3"), ymd(2026, 5, 31));
|
||||
assert_eq!(v(&s, "A4"), ymd(2026, 7, 31));
|
||||
assert_eq!(v(&s, "A5"), ymd(2026, 9, 30));
|
||||
assert_eq!(v(&s, "A6"), ymd(2026, 11, 30));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue