mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 05:06:51 -06:00
Merge pull request #2493 from Harrison-Booth:feat/options-iteration-fields
# Conflicts: # docs/function-parity.md # docs/ui-parity.md
This commit is contained in:
@@ -156,6 +156,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
| 2026-10-16 | Memory: floating pictures are decoded at about the size they are drawn, with limits, into a cache bounded by bytes and dropped with their workbook, instead of at full size into one that never let go; two open workbooks no longer show each other's picture. 12 large pictures scrolled through keep 40 MB of textures instead of 258 MB, and none once the workbook closes |
|
||||
| 2026-10-16 | The web app keeps its preferences between visits (#1562): theme (dark mode), interface size, language, editing options and the rest of what the desktop app saves in `ui.json` and `prefs.json` are kept in the browser's `localStorage`, written as soon as they change |
|
||||
| 2026-10-16 | Memory: the interface and script fallback fonts are read once per run and shared with egui instead of copied into it: an empty workbook settles at 158 MB instead of 258 MB on macOS |
|
||||
| 2026-10-15 | File › Options › Formulas: Maximum Iterations and Maximum Change fields for iterative calculation, disabled while it is off, with Excel's limits (1 to 32767, 0 or more) |
|
||||
| 2026-10-15 | Function help follows the interface language: argument hints, autocomplete, Insert Function (search and readable categories) and the Formulas-tab menus read per-language tables of descriptions and argument names; Spanish keeps its 10 functions in the new format |
|
||||
| 2026-10-15 | Row auto-fit counts a cell's line breaks only when the cell wraps, as Excel does: a file whose unwrapped cells hold multi-line text no longer opens with tall rows (typing a break still turns wrap on and grows the row) |
|
||||
| 2026-10-14 | Lookups (MATCH, XMATCH, VLOOKUP, HLOOKUP, XLOOKUP, LOOKUP) compare numbers exactly like Excel, so a computed key such as `=0.1+0.2` no longer finds 0.3; `=` and COUNTIF/SUMIF keep their 15-digit comparison |
|
||||
|
||||
@@ -1000,10 +1000,65 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
if ui.checkbox(&mut iterative, lang.tr("Enable iterative calculation")).changed() {
|
||||
app.run_or_alert("formulas.calculationOptions", json!({"iterative": iterative}));
|
||||
}
|
||||
// Maximum Iterations and Maximum Change apply only while iteration is on, as in Excel.
|
||||
ui.add_enabled_ui(iterative, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
egui::Grid::new("backstage_options_iteration").num_columns(2).spacing(vec2(12.0, 6.0)).show(ui, |ui| {
|
||||
ui.label(lang.tr("Maximum Iterations:"));
|
||||
let id = ui.make_persistent_id("backstage_max_iterations");
|
||||
// Text left in the field when iteration is switched off (it loses focus) is dropped.
|
||||
if let Some(text) = crate::widgets::committed_text(ui, id, &calc.max_iterations.to_string(), Some(80.0)).filter(|_| iterative) {
|
||||
match parse_max_iterations(&text) {
|
||||
Some(n) if n != calc.max_iterations => {
|
||||
app.run_or_alert("formulas.calculationOptions", json!({"maxIterations": n}));
|
||||
}
|
||||
Some(_) => {}
|
||||
None => refuse(app, ui, "Maximum Iterations must be a whole number from 1 to 32767."),
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label(lang.tr("Maximum Change:"));
|
||||
let id = ui.make_persistent_id("backstage_max_change");
|
||||
if let Some(text) = crate::widgets::committed_text(ui, id, &calc.max_change.to_string(), Some(80.0)).filter(|_| iterative) {
|
||||
match parse_max_change(&text, lang.formula_locale()) {
|
||||
Some(d) if d != calc.max_change => {
|
||||
app.run_or_alert("formulas.calculationOptions", json!({"maxChange": d}));
|
||||
}
|
||||
Some(_) => {}
|
||||
None => refuse(app, ui, "Maximum Change must be a number of 0 or more."),
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
});
|
||||
// TODO: add "Set precision as displayed" here once `formulas.calculationOptions` takes
|
||||
// `precisionAsDisplayed` (PR #1366); the model already stores it.
|
||||
ui.add_space(6.0);
|
||||
dim(ui, lang.tr("Calculation settings are saved with the workbook."));
|
||||
}
|
||||
|
||||
/// Refuses a typed setting with a message, leaving the workbook as it was. The Enter that
|
||||
/// committed the field is consumed so it doesn't also dismiss the message box in this frame.
|
||||
fn refuse(app: &mut SheetApp, ui: &mut Ui, text: &str) {
|
||||
ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter));
|
||||
app.message = Some(("GridCraft".into(), app.ui.language.tr(text).into()));
|
||||
}
|
||||
|
||||
/// Maximum Iterations as typed: a whole number from 1 to 32767, or `None` (Excel refuses others).
|
||||
fn parse_max_iterations(text: &str) -> Option<u32> {
|
||||
let n = text.trim().parse::<u32>().ok()?;
|
||||
(1..=gridcraft_engine::model::CalcSettings::MAX_ITERATIONS).contains(&n).then_some(n)
|
||||
}
|
||||
|
||||
/// Maximum Change as typed: a finite number of 0 or more, or `None`. The interface's decimal
|
||||
/// separator is accepted as well as a dot.
|
||||
fn parse_max_change(text: &str, locale: gridcraft_engine::formula::FormulaLocale) -> Option<f64> {
|
||||
let d = text.trim().replace(locale.decimal_separator(), ".").parse::<f64>().ok()?;
|
||||
(d.is_finite() && d >= 0.0).then_some(d)
|
||||
}
|
||||
|
||||
/// The platform the app was built for, e.g. "macOS (aarch64)"; the web build reports "Web".
|
||||
fn platform() -> String {
|
||||
let os = match std::env::consts::OS {
|
||||
|
||||
@@ -331,6 +331,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Automaticky ukládat sešity .xlsx po každé změně",
|
||||
"Workbook calculation" => "Výpočet sešitu",
|
||||
"Enable iterative calculation" => "Povolit iterativní výpočet",
|
||||
"Maximum Iterations:" => "Maximální počet iterací:",
|
||||
"Maximum Change:" => "Maximální změna:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "Maximální počet iterací musí být celé číslo od 1 do 32767.",
|
||||
"Maximum Change must be a number of 0 or more." => "Maximální změna musí být číslo 0 nebo větší.",
|
||||
"Calculation settings are saved with the workbook." => "Nastavení výpočtů se ukládá se sešitem.",
|
||||
"100%" => "100 %",
|
||||
"Zoom to\nSelection" => "Přiblížit\nna výběr",
|
||||
|
||||
@@ -336,6 +336,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => ".xlsx-Arbeitsmappen nach jeder Änderung automatisch speichern",
|
||||
"Workbook calculation" => "Arbeitsmappenberechnung",
|
||||
"Enable iterative calculation" => "Iterative Berechnung aktivieren",
|
||||
"Maximum Iterations:" => "Maximale Iterationszahl:",
|
||||
"Maximum Change:" => "Maximale Änderung:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "Die maximale Iterationszahl muss eine ganze Zahl von 1 bis 32767 sein.",
|
||||
"Maximum Change must be a number of 0 or more." => "Die maximale Änderung muss eine Zahl größer oder gleich 0 sein.",
|
||||
"Calculation settings are saved with the workbook." => "Berechnungseinstellungen werden mit der Arbeitsmappe gespeichert.",
|
||||
"Interface size" => "Größe der Benutzeroberfläche",
|
||||
"Auto" => "Automatisch",
|
||||
|
||||
@@ -333,6 +333,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Guardar automáticamente los libros .xlsx tras cada cambio",
|
||||
"Workbook calculation" => "Cálculo del libro",
|
||||
"Enable iterative calculation" => "Habilitar cálculo iterativo",
|
||||
"Maximum Iterations:" => "Iteraciones máximas:",
|
||||
"Maximum Change:" => "Cambio máximo:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "Las iteraciones máximas deben ser un número entero de 1 a 32767.",
|
||||
"Maximum Change must be a number of 0 or more." => "El cambio máximo debe ser un número igual o mayor que 0.",
|
||||
"Calculation settings are saved with the workbook." => "La configuración de cálculo se guarda con el libro.",
|
||||
"Interface size" => "Tamaño de la interfaz",
|
||||
"Auto" => "Automático",
|
||||
|
||||
@@ -420,6 +420,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Enregistrer automatiquement les classeurs .xlsx après chaque modification",
|
||||
"Workbook calculation" => "Calcul du classeur",
|
||||
"Enable iterative calculation" => "Activer le calcul itératif",
|
||||
"Maximum Iterations:" => "Nombre maximal d'itérations :",
|
||||
"Maximum Change:" => "Écart maximal :",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => {
|
||||
"Le nombre maximal d'itérations doit être un entier compris entre 1 et 32767."
|
||||
}
|
||||
"Maximum Change must be a number of 0 or more." => "L'écart maximal doit être un nombre supérieur ou égal à 0.",
|
||||
"Calculation settings are saved with the workbook." => "Les paramètres de calcul sont enregistrés avec le classeur.",
|
||||
"Insert Sheet" => "Insérer une feuille",
|
||||
"Rename" => "Renommer",
|
||||
|
||||
@@ -414,6 +414,10 @@ pub(super) const CORE: &[(&str, &str)] = &[
|
||||
("Save .xlsx workbooks automatically after every change", "変更のたびに .xlsx ブックを自動的に保存する"),
|
||||
("Workbook calculation", "ブックの計算"),
|
||||
("Enable iterative calculation", "反復計算を行う"),
|
||||
("Maximum Iterations:", "最大反復回数:"),
|
||||
("Maximum Change:", "変化の最大値:"),
|
||||
("Maximum Iterations must be a whole number from 1 to 32767.", "最大反復回数には 1 から 32767 までの整数を指定してください。"),
|
||||
("Maximum Change must be a number of 0 or more.", "変化の最大値には 0 以上の数値を指定してください。"),
|
||||
("Calculation settings are saved with the workbook.", "計算の設定はブックと共に保存されます。"),
|
||||
];
|
||||
|
||||
|
||||
@@ -420,6 +420,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "변경할 때마다 .xlsx 통합 문서를 자동으로 저장",
|
||||
"Workbook calculation" => "통합 문서 계산",
|
||||
"Enable iterative calculation" => "반복 계산 사용",
|
||||
"Maximum Iterations:" => "최대 반복 횟수:",
|
||||
"Maximum Change:" => "변화 한도:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "최대 반복 횟수는 1에서 32767 사이의 정수여야 합니다.",
|
||||
"Maximum Change must be a number of 0 or more." => "변화 한도는 0 이상의 숫자여야 합니다.",
|
||||
"Calculation settings are saved with the workbook." => "계산 설정은 통합 문서와 함께 저장됩니다.",
|
||||
"Insert Sheet" => "시트 삽입",
|
||||
"Rename" => "이름 바꾸기",
|
||||
|
||||
@@ -413,6 +413,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Salvar pastas de trabalho .xlsx automaticamente após cada alteração",
|
||||
"Workbook calculation" => "Cálculo da pasta de trabalho",
|
||||
"Enable iterative calculation" => "Habilitar cálculo iterativo",
|
||||
"Maximum Iterations:" => "Número Máximo de Iterações:",
|
||||
"Maximum Change:" => "Alteração Máxima:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "O número máximo de iterações deve ser um número inteiro de 1 a 32767.",
|
||||
"Maximum Change must be a number of 0 or more." => "A alteração máxima deve ser um número maior ou igual a 0.",
|
||||
"Calculation settings are saved with the workbook." => "As configurações de cálculo são salvas com a pasta de trabalho.",
|
||||
"OK" => "OK",
|
||||
"Cancel" => "Cancelar",
|
||||
|
||||
@@ -420,6 +420,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Автоматически сохранять книги .xlsx после каждого изменения",
|
||||
"Workbook calculation" => "Вычисления в книге",
|
||||
"Enable iterative calculation" => "Включить итеративные вычисления",
|
||||
"Maximum Iterations:" => "Предельное число итераций:",
|
||||
"Maximum Change:" => "Максимальное изменение:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "Предельное число итераций должно быть целым числом от 1 до 32767.",
|
||||
"Maximum Change must be a number of 0 or more." => "Максимальное изменение должно быть числом не меньше 0.",
|
||||
"Calculation settings are saved with the workbook." => "Параметры вычислений сохраняются вместе с книгой.",
|
||||
"Insert Sheet" => "Вставить лист",
|
||||
"Rename" => "Переименовать",
|
||||
|
||||
@@ -412,6 +412,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "Automatski sačuvaj .xlsx radne sveske posle svake izmene",
|
||||
"Workbook calculation" => "Izračunavanje radne sveske",
|
||||
"Enable iterative calculation" => "Omogući iterativno izračunavanje",
|
||||
"Maximum Iterations:" => "Maksimalan broj iteracija:",
|
||||
"Maximum Change:" => "Maksimalna promena:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "Maksimalan broj iteracija mora biti ceo broj od 1 do 32767.",
|
||||
"Maximum Change must be a number of 0 or more." => "Maksimalna promena mora biti broj veći ili jednak 0.",
|
||||
"Calculation settings are saved with the workbook." => "Podešavanja izračunavanja se čuvaju sa radnom sveskom.",
|
||||
"Left Margin" => "Leva margina",
|
||||
"Right Margin" => "Desna margina",
|
||||
|
||||
@@ -412,6 +412,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Save .xlsx workbooks automatically after every change" => "每次更改后自动保存 .xlsx 工作簿",
|
||||
"Workbook calculation" => "工作簿计算",
|
||||
"Enable iterative calculation" => "启用迭代计算",
|
||||
"Maximum Iterations:" => "最多迭代次数:",
|
||||
"Maximum Change:" => "最大误差:",
|
||||
"Maximum Iterations must be a whole number from 1 to 32767." => "最多迭代次数必须是 1 到 32767 之间的整数。",
|
||||
"Maximum Change must be a number of 0 or more." => "最大误差必须是大于或等于 0 的数字。",
|
||||
"Calculation settings are saved with the workbook." => "计算设置随工作簿一起保存。",
|
||||
"Insert Sheet" => "插入工作表",
|
||||
"Rename" => "重命名",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
use gridcraft_engine::{Session, core::CellRef, core::Value};
|
||||
use gridcraft_ui_egui::i18n::Language;
|
||||
use gridcraft_ui_egui::{Services, SheetApp};
|
||||
@@ -331,6 +331,120 @@ fn a_cancelled_export_pdf_picker_writes_nothing() {
|
||||
assert!(h.state_mut().session.take_ui_requests().is_empty());
|
||||
}
|
||||
|
||||
/// Options › Formulas: Maximum Iterations and Maximum Change, with the checkbox that enables them.
|
||||
#[test]
|
||||
fn options_sets_maximum_iterations_and_change_only_while_iteration_is_on() {
|
||||
use egui::accesskit::Role;
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
// Tall enough that the Formulas section is on screen without scrolling.
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 1400.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Services::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.state_mut().run("file.options", serde_json::json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
let calc = |h: &egui_kittest::Harness<'static, SheetApp>| h.state().session.active().unwrap().wb.calc.clone();
|
||||
// The text field on the same row as `label`.
|
||||
let field = |h: &egui_kittest::Harness<'static, SheetApp>, label: &str| {
|
||||
let l = h.get_by_label(label).rect();
|
||||
h.get_all_by_role(Role::TextInput).map(|n| n.rect()).find(|r| r.left() > l.left() && (r.center().y - l.center().y).abs() < 8.0).unwrap()
|
||||
};
|
||||
let disabled = |h: &egui_kittest::Harness<'static, SheetApp>, rect: egui::Rect| {
|
||||
h.get_all_by_role(Role::TextInput).find(|n| n.rect() == rect).unwrap().accesskit_node().is_disabled()
|
||||
};
|
||||
let retype = |h: &mut egui_kittest::Harness<'static, SheetApp>, rect: egui::Rect, text: &str| {
|
||||
h.input_mut().events.push(egui::Event::PointerMoved(rect.center()));
|
||||
h.run_steps(1);
|
||||
h.get_all_by_role(Role::TextInput).find(|n| n.rect() == rect).unwrap().click();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.run_steps(1);
|
||||
for c in text.chars() {
|
||||
h.input_mut().events.push(egui::Event::Text(c.to_string()));
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(2);
|
||||
};
|
||||
|
||||
// Iteration is off: the two fields show the defaults, greyed out.
|
||||
let (iterations, change) = (field(&h, "Maximum Iterations:"), field(&h, "Maximum Change:"));
|
||||
assert!(disabled(&h, iterations) && disabled(&h, change), "the fields are disabled while iteration is off");
|
||||
assert_eq!(h.get_all_by_role(Role::TextInput).find(|n| n.rect() == iterations).unwrap().value().as_deref(), Some("100"));
|
||||
assert_eq!(h.get_all_by_role(Role::TextInput).find(|n| n.rect() == change).unwrap().value().as_deref(), Some("0.001"));
|
||||
|
||||
h.get_by_role_and_label(Role::CheckBox, "Enable iterative calculation").click();
|
||||
h.run_steps(3);
|
||||
assert!(calc(&h).iterative);
|
||||
let (iterations, change) = (field(&h, "Maximum Iterations:"), field(&h, "Maximum Change:"));
|
||||
assert!(!disabled(&h, iterations) && !disabled(&h, change));
|
||||
|
||||
retype(&mut h, iterations, "500");
|
||||
assert_eq!(calc(&h).max_iterations, 500);
|
||||
retype(&mut h, change, "0.01");
|
||||
assert_eq!(calc(&h).max_change, 0.01);
|
||||
assert!(h.state().message.is_none());
|
||||
|
||||
// Out-of-range entries are refused with a message and change nothing (the command alone would clamp them).
|
||||
for bad in ["0", "32768", "12.5", "lots"] {
|
||||
retype(&mut h, iterations, bad);
|
||||
assert_eq!(calc(&h).max_iterations, 500, "{bad}");
|
||||
assert!(h.state_mut().message.take().is_some(), "{bad} is reported");
|
||||
h.run_steps(2);
|
||||
}
|
||||
retype(&mut h, iterations, "32767");
|
||||
assert_eq!(calc(&h).max_iterations, 32767);
|
||||
for bad in ["-1", "x"] {
|
||||
retype(&mut h, change, bad);
|
||||
assert_eq!(calc(&h).max_change, 0.01, "{bad}");
|
||||
assert!(h.state_mut().message.take().is_some(), "{bad} is reported");
|
||||
h.run_steps(2);
|
||||
}
|
||||
retype(&mut h, change, "0");
|
||||
assert_eq!(calc(&h).max_change, 0.0);
|
||||
|
||||
// Undo puts the old value back in the field.
|
||||
h.state_mut().run("edit.undo", serde_json::json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert_eq!(calc(&h).max_change, 0.01);
|
||||
let change = field(&h, "Maximum Change:");
|
||||
assert_eq!(h.get_all_by_role(Role::TextInput).find(|n| n.rect() == change).unwrap().value().as_deref(), Some("0.01"));
|
||||
|
||||
// A value typed but not confirmed with Enter is dropped when the checkbox switches iteration off.
|
||||
let iterations = field(&h, "Maximum Iterations:");
|
||||
h.input_mut().events.push(egui::Event::PointerMoved(iterations.center()));
|
||||
h.run_steps(1);
|
||||
h.get_all_by_role(Role::TextInput).find(|n| n.rect() == iterations).unwrap().click();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.input_mut().events.push(egui::Event::Text("777".into()));
|
||||
h.run_steps(2);
|
||||
h.get_by_role_and_label(Role::CheckBox, "Enable iterative calculation").click();
|
||||
h.run_steps(3);
|
||||
assert!(!calc(&h).iterative);
|
||||
assert_eq!(calc(&h).max_iterations, 32767, "an unconfirmed entry is not committed once iteration is off");
|
||||
assert!(h.state().message.is_none());
|
||||
|
||||
// Spanish accepts a decimal comma in Maximum Change, and still a dot.
|
||||
h.get_by_role_and_label(Role::CheckBox, "Enable iterative calculation").click();
|
||||
h.run_steps(3);
|
||||
h.state_mut().run("app.language.set", serde_json::json!({"language": Language::Es.code()})).unwrap();
|
||||
h.state_mut().run("file.options", serde_json::json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
let change = field(&h, "Cambio máximo:");
|
||||
retype(&mut h, change, "0,02");
|
||||
assert_eq!(calc(&h).max_change, 0.02);
|
||||
retype(&mut h, change, "0.5");
|
||||
assert_eq!(calc(&h).max_change, 0.5);
|
||||
assert!(h.state().message.is_none());
|
||||
}
|
||||
|
||||
/// Share › Export as Web Page (and the native File menu's Save as Web Page) asks for a name in its
|
||||
/// own dialog. A bare name has no folder: written as typed it lands in the process's working
|
||||
/// directory, where nobody looks for it (#1212). The save picker chooses the folder instead.
|
||||
|
||||
@@ -88,7 +88,7 @@ This improves series preparation; it does not change the model-fitting or season
|
||||
| Array lifting: an array in a scalar argument evaluates the function per element | done. The Analysis ToolPak functions (EOMONTH, EDATE, WORKDAY(.INTL), NETWORKDAYS(.INTL), YEARFRAC, WEEKNUM, the COUP*, PRICE*, YIELD*, ODD*, TBILL*, ACCRINT*, AMOR*, DISC, DURATION, CUMIPMT/CUMPRINC, EFFECT, NOMINAL, DOLLARDE/FR, XNPV/XIRR rate and guess, FVSCHEDULE principal, ISEVEN/ISODD, QUOTIENT, MROUND, FACTDOUBLE, SQRTPI, RANDBETWEEN, SERIESSUM, the base conversions, CONVERT, DELTA, GESTEP, ERF/ERFC, BESSEL*, COMPLEX and IM* except IMSUM/IMPRODUCT) give `#VALUE!` for a reference to more than one cell in a value argument, as Excel 365 does in both dynamic-array and legacy formulas; array constants and computed arrays (`A1:A3+0`) still lift, and range arguments (holidays, schedule, cash flows) and the .INTL weekend accept ranges. Difference: BYROW/BYCOL pass their row or column to the LAMBDA as values, so `BYROW(A1:B3,LAMBDA(r,EOMONTH(r,0)))` lifts where Excel gives `#VALUE!` |
|
||||
| Trim references `A1:.E10` (trailing), `A1.:E10` (leading), `A1.:.E10` (both), also on whole rows/columns (`A:.A`) | done; evaluated as `TRIMRANGE(range, code, code)`, adjusted on insert/delete; files store `_xlfn.TRIMRANGE(range,code,code)` (Excel's own file spelling unconfirmed in public docs), `_xlfn._TRO_ALL/_TRO_LEADING/_TRO_TRAILING(...)` read too |
|
||||
| LET, LAMBDA, named LAMBDAs, ISOMITTED, MAP/REDUCE/SCAN/BYROW/BYCOL/MAKEARRAY | done. A LAMBDA that calls itself more than 140 times, or a formula nested past the depth limit, gives `#NUM!` (the target app allows deeper recursion, #1581). Calling a defined name that holds an error value gives that error, not `#NAME?` |
|
||||
| Iterative calculation for circular references | done: with Enable Iterative Calculation on (`formulas.calculationOptions` `{iterative, maxIterations, maxChange}`, saved as `<calcPr iterate iterateCount iterateDelta>`), loops are evaluated again from their previous values (blank = 0) until no cell on a loop changes by Maximum Change or Maximum Iterations is reached; off, cycles give `#CIRC!`. Differences: no Options dialog yet (command only); GridCraft recalculates on open, so a loop that never settles (`=A1+1`) advances again each time the file is opened |
|
||||
| Iterative calculation for circular references | done: with Enable Iterative Calculation on (`formulas.calculationOptions` `{iterative, maxIterations, maxChange}`, saved as `<calcPr iterate iterateCount iterateDelta>`), loops are evaluated again from their previous values (blank = 0) until no cell on a loop changes by Maximum Change or Maximum Iterations is reached; off, cycles give `#CIRC!`. Set in File › Options › Formulas (Enable iterative calculation, Maximum Iterations 1 to 32767, Maximum Change 0 or more; the two fields are disabled while iteration is off). Differences: GridCraft recalculates on open, so a loop that never settles (`=A1+1`) advances again each time the file is opened
|
||||
| 15-significant-digit comparison semantics | done (#57): `=0.1+0.2=0.3` is TRUE, and MATCH and COUNTIF compare the same way. Arithmetic keeps full precision, as Excel does — `=(0.1+0.2)-0.3` is 5.55E-17 |
|
||||
| External workbook references (`[Book.xlsx]Sheet1!A1`) | parsed; links dropped on open, no update |
|
||||
| Localized function names and `;` separators | **missing** (#45); en-US only |
|
||||
@@ -131,6 +131,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
| 2026-10-19 | minor | Long flat operator chains (#916): 4,096 operands (Excel's 8,192-character limit) no longer overflow the stack when entered, filled, adjusted or saved; chains past 64 operands are no longer saved as dynamic-array formulas; Spanish entry no longer refuses chains past 256 operands |
|
||||
| 2026-10-17 | minor | 15-significant-digit comparison moved from missing to done (#57), and the resolved caveats on the 3-D reference, dynamic-array and legacy-array rows cleared (#140, #133, #139, #156) |
|
||||
| 2026-10-16 | minor | FORECAST.ETS gap interpolation uses a forward-only known-point scan instead of rescanning each gap for every missing value; numerical behavior and missing-point limits are preserved (#1042) |
|
||||
| 2026-10-15 | minor | Iterative calculation: Maximum Iterations and Maximum Change are set in File › Options › Formulas (dropped "no Options dialog yet") |
|
||||
| 2026-10-15 | minor | MEDIAN and AGGREGATE median use an overflow-safe midpoint (#1579); regressions cover finite extremes, tiny values and dependent recalculation |
|
||||
| 2026-10-15 | minor | ADDRESS quotes a sheet name the way Excel does: names that read as an A1 cell (`A1`) or an R1C1 reference (`R1C1`, `RC`) and names starting with `.` get quotes; `TRUE` / `FALSE` stay bare (#1458) |
|
||||
| 2026-10-14 | minor | MATCH, XMATCH, VLOOKUP, HLOOKUP, XLOOKUP and LOOKUP compare numbers exactly in every match and search mode, as Excel 365 does: `MATCH(0.1+0.2,{0.3},0)` is `#N/A` and 0.3 sorts below 0.30000000000000004 in approximate and binary search. `=`, SWITCH and the criteria functions (COUNTIF(S), SUMIF(S), AVERAGEIF(S), MAXIFS…) keep comparing at 15 significant digits, also as observed |
|
||||
|
||||
+2
-1
@@ -65,7 +65,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
||||
equality (#57, so `=0.1+0.2=0.3` is TRUE and MATCH and COUNTIF agree), spill updates after
|
||||
structural edits and cleared blockers (#133, #139; PRs #148, #157), 3-D span deletion (#140),
|
||||
and qualified names after a sheet rename (#134). Iterative calculation for circular references
|
||||
landed too (no Options dialog yet, which is §13's).
|
||||
landed too, with its settings in File › Options › Formulas.
|
||||
- **Impact:** wrong numbers are worse than missing features, so this entry is kept until the
|
||||
oracle suite of §15 can say the category is clear rather than only these five reports being so.
|
||||
- **Estimate:** — [function-parity.md](function-parity.md#formula-language)
|
||||
@@ -185,6 +185,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
||||
| 2026-10-23 | minor | §8: PivotTable Show Details: double-clicking a value cell lists its source records on a new sheet (#2364) |
|
||||
| 2026-10-20 | minor | §11: the Japanese catalog covers the dialogs, messages and menus — 943 keys (~43%), matching the pt-BR coverage; a test keeps the two catalogs in step |
|
||||
| 2026-10-17 | minor | §6: the five reports it tracked are closed and their PRs merged (#57, #133, #139, #140, #134; PRs #148, #155, #156, #157); the entry stays open for §15's oracle suite |
|
||||
| 2026-10-15 | minor | §6: Maximum Iterations and Maximum Change in File › Options › Formulas; dropped the stale "no Options dialog yet" |
|
||||
| 2026-10-15 | minor | §13: Solver prepares reports before committing the solution, model and report sheets together; report refusal preserves the workbook and Undo/Redo (#1050) |
|
||||
| 2026-10-13 | trivial | §4: multi-level category axes landed |
|
||||
| 2026-10-12 | minor | §9: next/previous sheet commands retain the current visible sheet and selection when there is no visible destination |
|
||||
|
||||
+3
-2
@@ -29,9 +29,9 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); New sheet (+) right after the last tab, the strip capped at 60% of the bar (#943); manual wheel/trackpad browsing preserved; next/previous skips Hidden and VeryHidden sheets and retains the current sheet and selection when no visible destination exists; rename focus bug on macOS (#145) |
|
||||
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes; a right-click on the status bar opens a menu that turns the statistics on and off (Average, Count, Numerical Count, Minimum, Maximum, Sum; `view.statusBar`) | done for the statistics; the other status bar options are not customizable |
|
||||
| Task panes (Format Chart, Comments, Watch, Selection, PivotTable fields) | yes | yes; PivotTable source-field list scrollbar sits at the right edge of its pane | element-level Format pane for shapes and pictures missing |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | Options dialog and template gallery missing |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | template gallery missing |
|
||||
| Dark mode, system appearance | yes | yes | done (10-09); follows the system on first run, title-bar switch and File › Display theme (#194, #239); icons, reference colours, primary buttons and the comments pane follow the theme, and a unit test holds every token pair to WCAG 2 contrast in both themes |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | yes (#174, 10-11): the File tab opens a full-window view (Esc or the back arrow return) with Home, New (blank + the three samples), Open (recent + Browse; the recent list tells when each workbook was last changed, 10-12), Info (location, properties, Protect Workbook), Save, Save As, Print (page preview, active sheet or workbook, fit to page, Page Setup), Export (PDF, CSV, web page), Close and Options (`file.options`: theme, language, formula bar, AutoSave, calculation) | no Account, Share or template gallery beyond the samples; Options applies at once (no OK/Cancel dialog) and holds only the settings GridCraft has |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | yes (#174, 10-11): the File tab opens a full-window view (Esc or the back arrow return) with Home, New (blank + the three samples), Open (recent + Browse; the recent list tells when each workbook was last changed, 10-12), Info (location, properties, Protect Workbook), Save, Save As, Print (page preview, active sheet or workbook, fit to page, Page Setup), Export (PDF, CSV, web page), Close and Options (`file.options`: theme, language, formula bar, AutoSave, calculation mode and iterative calculation with Maximum Iterations and Maximum Change, 10-15) | no Account, Share or template gallery beyond the samples; Options applies at once (no OK/Cancel dialog) and holds only the settings GridCraft has |
|
||||
| Dark mode, system appearance | yes | yes | done (10-09) |
|
||||
| Page Layout view (paper view with rulers and header editing) | yes | yes (#329, 10-11): per-sheet view saved in XLSX, pages with margins and gaps on the print pagination, "Click to add data" pages, rulers (inches or centimetres), headers and footers drawn and edited in place with a Header & Footer tab, whose Different First Page and Different Odd & Even Pages give the first page and the even pages their own header and footer (#344), and whose Scale with Document and Align with Page Margins set whether header and footer text follows the print scale and the side margins, on paper and in the view | margins drag on the rulers (#792, with header and footer margins); header/footer pictures missing (1–3 h). Unverified: with Align with Page Margins off, header and footer text sits 0.7 in from the paper edge, a value not yet measured in the target app |
|
||||
| Multiple windows per workbook | yes | no (#162) | see [hardware-parity.md](hardware-parity.md) |
|
||||
@@ -218,6 +218,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
| 2026-10-16 | minor | Web printing registers one-shot `afterprint` cleanup before opening the dialog; missing content windows and setup, listener or print failures also release the hidden iframe and PDF blob URL (#1054) |
|
||||
| 2026-10-16 | minor | The shared UI clock delegates to the core Unix-millisecond clock, which reads Date.now() on wasm, instead of returning zero there (#1026). Native clock contract tested; browser timing remains unverified |
|
||||
| 2026-10-16 | trivial | Web: preferences (theme, interface size, language, editing options…) are kept in the browser's `localStorage` between visits, as the desktop app keeps `ui.json` and `prefs.json`; before, a reload brought back light mode and the defaults (#1562) |
|
||||
| 2026-10-15 | minor | File › Options › Formulas: Maximum Iterations (1 to 32767) and Maximum Change (0 or more) under Enable iterative calculation, disabled while it is off; invalid entries are refused with a message |
|
||||
| 2026-10-15 | minor | Number-format named colors use their RGB values; indexed `[Color1]`–`[Color56]` resolve through the shared legacy palette in grid and PDF (#1452) |
|
||||
| 2026-10-14 | minor | Spill-child formula bar uses the anchor formula with disabled theme text and blocks formula-bar edit entry; selection and recalculation restore ordinary behavior |
|
||||
| 2026-10-14 | minor | PivotTable source-field list fills the pane width so its scrollbar stays at the list's right edge |
|
||||
|
||||
Reference in New Issue
Block a user