Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 6cdd6db150 engine: show overlong format codes as General without hashing them per cell
The display cache hashed the full format code on every lookup, so one
huge code shared by many cells cost its length for every cell drawn.
A code with more than 4 × 255 bytes can't be within Excel's 255
characters and is now formatted as General straight away.
2026-11-03 14:07:22 +00:00
3 changed files with 36 additions and 21 deletions
+22
View File
@@ -10,6 +10,11 @@ use gridcraft_numfmt::{Formatted, NumberFormat, format_value};
/// Parsed number formats are cached by code.
fn parsed(code: &str) -> NumberFormat {
static CACHE: std::sync::LazyLock<Mutex<HashMap<String, NumberFormat>>> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
// A code of more bytes than 4 × the character limit can't be within it: it shows as General
// without being hashed or scanned again for every cell that shares it.
if code.len() > 4 * gridcraft_numfmt::MAX_FORMAT_CODE_CHARS {
return NumberFormat::parse("General");
}
let lock = || CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(f) = lock().get(code) {
return f.clone();
@@ -226,3 +231,20 @@ pub fn stats(sheet: &Sheet, ranges: &[RangeRef]) -> Stats {
}
s
}
#[cfg(test)]
mod tests {
use super::*;
/// A format code far past Excel's 255 characters shows its values as General, for every cell
/// that shares it, without being hashed into the cache each time.
#[test]
fn an_overlong_code_formats_as_general() {
let wb = Workbook::new();
let code = format!("0.00{}", "\"x\"".repeat(400_000));
for _ in 0..1_000 {
assert_eq!(format(&Value::Number(1.5), &code, &wb).text, "1.5");
}
assert_eq!(format(&Value::Number(1.5), "0.00", &wb).text, "1.50");
}
}
+14 -3
View File
@@ -575,9 +575,20 @@ pub fn series(cx: &mut Ctx, sheet: usize, options: SeriesOptions<'_>) -> Result<
match (unit, d) {
("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),
// Whole weeks at a time: a huge step costs no more than a small one.
("weekday", Some(_)) => add_weekdays(sys, v, step as i64)
.ok_or_else(|| crate::EngineError::Other("The weekday series runs past the supported date range.".into()))?,
("weekday", Some(_)) => {
let mut n = v;
let mut left = step.abs() as i64;
while left > 0 {
n += step.signum();
let Some(day) = datetime_from_serial(sys, n) else {
return Err(crate::EngineError::Other("The weekday series runs past the supported date range.".into()));
};
if day.weekday != 0 && day.weekday != 6 {
left -= 1;
}
}
n
}
_ => v + step,
}
}
-18
View File
@@ -3975,24 +3975,6 @@ fn fill_series_weekday_out_of_range_step_errors() {
assert_eq!(v(&s, "A2"), Value::Empty);
}
/// A step of a million weekdays moves whole weeks at a time instead of visiting every day, and
/// lands where counting day by day would.
#[test]
fn fill_series_weekday_large_step_is_arithmetic() {
let mut s = s();
// 45005 is Monday 20 March 2023; five weekdays on from a Monday is the next Monday.
s.execute("cell.set", json!({"cell": "A1", "input": "45005"})).unwrap();
s.execute("edit.fillSeries", json!({"range": "A1:A3", "direction": "columns", "type": "date", "dateUnit": "weekday", "step": 1_000_000}))
.unwrap();
assert_eq!(v(&s, "A2"), Value::Number(45005.0 + 1_400_000.0));
assert_eq!(v(&s, "A3"), Value::Number(45005.0 + 2_800_000.0));
s.execute("cell.set", json!({"cell": "B1", "input": "45009"})).unwrap();
s.execute("edit.fillSeries", json!({"range": "B1:B3", "direction": "columns", "type": "date", "dateUnit": "weekday", "step": -1})).unwrap();
// Friday 24 March 2023 back one weekday at a time: Thursday, Wednesday.
assert_eq!(v(&s, "B2"), Value::Number(45008.0));
assert_eq!(v(&s, "B3"), Value::Number(45007.0));
}
#[test]
fn fill_series_month_leap_year() {
let s = series_date("2028-01-31", "A1:A3", "month");