Merge PR #2131: Print: JIS B4/B5 and Japanese paper sizes (はがき・封筒・B6)

ROADMAP.md: one status line naming both changes; both feature-table and progress-log rows, newest first.
This commit is contained in:
aiongg committed 2026-10-22 02:36:10 +00:00
commit 635056bd23
7 files changed
+219 -12

No files matched your search

+4 -1
View File
@@ -2,7 +2,8 @@
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-19 · **Change:** minor (long flat operator chains up to Excel's formula limit) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-21 · **Change:** minor (Page Setup paper sizes: JIS B4/B5/B6, the Japanese postcards and envelopes, and A6; long flat operator chains up to Excel's formula limit) · **Target:** Microsoft Excel (Microsoft 365)
>>>>>>> upr/2131
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
@@ -132,6 +133,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | What landed |
|---|---|
| 2026-10-21 | Page Setup paper sizes match Japanese Excel: `B4`/`B5`/`B6` are the JIS sizes (ISO B4/B5/B6 stay reachable by name), so a Japanese B5 worksheet prints at 182×257 mm; Japanese Postcard (100×148 mm), Double Postcard (148×200 mm), Envelope #3/#4 (120×235/90×205 mm), Kaku #2/#3 (240×332/216×277 mm) and A6 are added, both sizes reach the PDF page (`file.exportPdf`/`file.print` MediaBox) and the size survives an XLSX round-trip; the Page Layout Size menu lists them. `B4`/`B5` used to be the ISO sizes, which is changed here |
| 2026-10-19 | Long flat operator chains (`=A1+A2+…`) up to Excel's 8,192-character limit (4,096 operands): entering, filling, inserting rows, undo, Evaluate Formula and XLSX save no longer overflow the stack; chains past 64 operands are saved as plain formulas, not dynamic arrays; Spanish formula entry accepts chains past 256 operands (#916) |
| 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
@@ -194,6 +196,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | minor | Page Setup paper sizes: JIS B4/B5/B6 (ISO by name), Japanese postcards and envelopes, A6; PDF page size follows, XLSX round-trips |
| 2026-10-19 | minor | Progress log: long flat operator chains (#916) |
| 2026-10-12 | minor | Rows auto-fit oversized fonts on open and whole-row/column formatting (#833); progress log row |
| 2026-10-12 | minor | Progress log: multi-area row/column delete (#758) |
+96 -4
View File
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Page Setup…",
["Page Layout", "Page Setup"],
None,
"{sheet?, orientation?: portrait|landscape, paper?: Letter|Legal|A4|A3|A5|Tabloid|Executive|Ledger|B5, margins?: {left,right,top,bottom,header,footer} (inches; also accepted as top-level keys), scale?: 10..400, fitWidth?, fitHeight? (pages; 0 = automatic), fitToPage?: bool, centerH?, centerV?, gridlines?, headings?, header?: \"&L&A&RPage &P of &N\", footer?, printArea?: \"A1:F50\" (\"\" clears), titleRows?: \"1:2\", titleCols?: \"A:A\", rowBreaks?: [46, 91], colBreaks?: [\"K\"]} → the sheet's print settings",
"{sheet?, orientation?: portrait|landscape, paper?: Letter|Legal|A4|A3|A5|A6|Tabloid|Executive|Ledger|B4|B5|B6|B4 (ISO)|B5 (ISO)|B6 (ISO)|Japanese Postcard|Japanese Double Postcard|Japanese Envelope #3|Japanese Envelope #4|Japanese Envelope Kaku #2|Japanese Envelope Kaku #3 (B4/B5/B6 are the JIS sizes), margins?: {left,right,top,bottom,header,footer} (inches; also accepted as top-level keys), scale?: 10..400, fitWidth?, fitHeight? (pages; 0 = automatic), fitToPage?: bool, centerH?, centerV?, gridlines?, headings?, header?: \"&L&A&RPage &P of &N\", footer?, printArea?: \"A1:F50\" (\"\" clears), titleRows?: \"1:2\", titleCols?: \"A:A\", rowBreaks?: [46, 91], colBreaks?: [\"K\"]} → the sheet's print settings",
has_doc,
page_setup
),
@@ -71,19 +71,36 @@ pub fn paper_size(name: &str) -> (f32, f32) {
known_paper(name).unwrap_or((612.0, 792.0))
}
/// Paper sizes in points, portrait. `B4`/`B5`/`B6` are the **JIS** sizes Japanese Excel and its
/// e-mail attachments use (257×364, 182×257 and 128×182 mm); the ISO B series stays reachable as
/// `B4 (ISO)`/`B5 (ISO)`/`B6 (ISO)`. The Japanese postcards and envelopes are the JIS ones too.
fn known_paper(name: &str) -> Option<(f32, f32)> {
let n = name.trim().to_ascii_lowercase().replace([' ', '-', '_'], "");
// Spaces, hyphens, underscores and parentheses don't matter: `B5 (JIS)`, `b5-jis` and `b5`
// are one name.
let n = name.trim().to_ascii_lowercase().replace([' ', '-', '_', '(', ')'], "");
Some(match n.as_str() {
"letter" | "usletter" => (612.0, 792.0),
"legal" | "uslegal" => (612.0, 1008.0),
"a4" => (595.28, 841.89),
"a3" => (841.89, 1190.55),
"a5" => (419.53, 595.28),
"a6" => (297.64, 419.53),
"tabloid" | "11x17" => (792.0, 1224.0),
"ledger" => (1224.0, 792.0),
"executive" => (522.0, 756.0),
"b5" => (498.9, 708.66),
"b4" => (708.66, 1000.63),
"b4" | "b4jis" => (728.50, 1031.81),
"b5" | "b5jis" => (515.91, 728.50),
"b6" | "b6jis" => (362.83, 515.91),
"b4iso" => (708.66, 1000.63),
"b5iso" => (498.90, 708.66),
"b6iso" => (354.33, 498.90),
"japanesepostcard" | "postcard" => (283.46, 419.53),
"japanesedoublepostcard" | "doublepostcard" => (419.53, 566.93),
"japaneseenvelope#3" | "chou#3" => (340.16, 666.14),
// 長形4号 is 90×205 mm; 90×225 mm is 長形40号, a different envelope, so it stays out.
"japaneseenvelope#4" | "chou#4" => (255.12, 581.10),
"japaneseenvelopekaku#2" | "kaku#2" => (680.31, 941.10),
"japaneseenvelopekaku#3" | "kaku#3" => (612.28, 785.20),
_ => return None,
})
}
@@ -2061,6 +2078,81 @@ mod tests {
assert!(s.doc().unwrap().wb.active().unwrap().print.print_area.is_some());
}
/// The Japanese paper sizes are the JIS ones: `B4`/`B5`/`B6` changed meaning from ISO, and the
/// ISO B series stays reachable by name. The postcards and envelopes are the JIS sizes too.
#[test]
fn japanese_paper_sizes_are_jis() {
for (name, w, h) in [
("A4", 595.28, 841.89), // unchanged
("B4", 728.50, 1031.81),
("B5", 515.91, 728.50),
("B6", 362.83, 515.91),
("B4 (JIS)", 728.50, 1031.81),
("b5-jis", 515.91, 728.50),
("B4 (ISO)", 708.66, 1000.63),
("B5 (ISO)", 498.90, 708.66),
("B6 (ISO)", 354.33, 498.90),
("A6", 297.64, 419.53),
("Japanese Postcard", 283.46, 419.53),
("japanese double postcard", 419.53, 566.93),
("Japanese Envelope #3", 340.16, 666.14),
("chou #4", 255.12, 581.10),
("Japanese Envelope Kaku #2", 680.31, 941.10),
("kaku #3", 612.28, 785.20),
] {
let got = paper_size(name);
assert!((got.0 - w).abs() < 0.01 && (got.1 - h).abs() < 0.01, "{name}: {got:?}");
}
// 長形4号 is 90×205 mm (90×225 mm is 長形40号), so its height is 205 mm = 581.10 pt — not the
// 225 mm it once had.
let chou4 = paper_size("Japanese Envelope #4");
assert_eq!((chou4.0, chou4.1), (255.12, 581.10));
assert_eq!(chou4, paper_size("chou #4"));
// Unknown names keep the old Letter fallback.
assert_eq!(paper_size("Napkin"), (612.0, 792.0));
}
/// Page Setup's paper reaches the PDF page: the MediaBox is the paper's, A4 is unchanged, B5 is
/// JIS B5, and the Japanese postcard prints at 283.46×419.53 pt. Landscape swaps the sides and
/// fit-to-page keeps the paper size.
#[test]
fn page_setup_paper_sizes_reach_the_pdf_page() {
let mut s = s();
s.execute("range.setValues", json!({"range": "A1", "values": [["x"]]})).unwrap();
let close = |v: &Json, want: f64| (v.as_f64().unwrap() - want).abs() < 0.01;
for (paper, w, h) in [
("A4", 595.28f64, 841.89f64),
("B5", 515.91, 728.50),
("Japanese Postcard", 283.46, 419.53),
("Japanese Double Postcard", 419.53, 566.93),
("Japanese Envelope Kaku #2", 680.31, 941.10),
] {
s.execute("pageLayout.pageSetup", json!({"paper": paper, "orientation": "portrait"})).unwrap();
let pv = s.execute("file.printPreview", json!({})).unwrap();
let page = &pv["pageList"][0];
assert_eq!(page["paper"], paper);
assert!(close(&page["width"], w) && close(&page["height"], h), "{paper}: {page}");
let (pdf, pages) = pdf_of(&mut s, json!({}));
assert_eq!(pages, 1);
let want = format!("/MediaBox [0 0 {w} {h}]");
assert!(contains(&pdf, &want), "{paper}: wanted {want}");
}
// Landscape swaps the sides, and fit-to-page does not change the paper.
s.execute("pageLayout.pageSetup", json!({"paper": "B5", "orientation": "landscape"})).unwrap();
let (pdf, pages) = pdf_of(&mut s, json!({}));
assert_eq!(pages, 1);
assert!(contains(&pdf, "/MediaBox [0 0 728.5 515.91]"));
s.execute("pageLayout.pageSetup", json!({"paper": "B5", "orientation": "portrait"})).unwrap();
let (pdf, _) = pdf_of(&mut s, json!({"fitToPage": true}));
assert!(contains(&pdf, "/MediaBox [0 0 515.91 728.5]"));
// `pageLayout.size` still takes any name, and an unknown one prints on Letter as before.
s.execute("pageLayout.size", json!({"paper": "Napkin"})).unwrap();
let pv = s.execute("file.printPreview", json!({})).unwrap();
assert!(close(&pv["pageList"][0]["width"], 612.0) && close(&pv["pageList"][0]["height"], 792.0));
// Page Setup refuses an unknown name.
assert!(s.execute("pageLayout.pageSetup", json!({"paper": "Napkin"})).is_err());
}
#[test]
fn hostile_settings_never_panic_and_cap_pages() {
let mut s = s();
+9 -1
View File
@@ -35,7 +35,15 @@ pub fn specs() -> Vec<CommandSpec> {
}),
cmd!("view.zoomToSelection", "Zoom to Selection", ["View", "Zoom"], None, "{viewWidth?, viewHeight?}", has_doc, zoom_selection),
cmd!("pageLayout.orientation", "Orientation", ["Page Layout", "Page Setup"], None, "{orientation: portrait|landscape}", has_doc, orientation),
cmd!("pageLayout.size", "Size", ["Page Layout", "Page Setup"], None, "{paper: Letter|Legal|A4|A3|A5|Tabloid|Executive}", has_doc, paper),
cmd!(
"pageLayout.size",
"Size",
["Page Layout", "Page Setup"],
None,
"{paper: Letter|Legal|A4|A3|A5|A6|Tabloid|Executive|Ledger|B4|B5|B6|B4 (ISO)|B5 (ISO)|B6 (ISO)|Japanese Postcard|Japanese Double Postcard|Japanese Envelope #3|Japanese Envelope #4|Japanese Envelope Kaku #2|Japanese Envelope Kaku #3 (B4/B5/B6 are the JIS sizes)}",
has_doc,
paper
),
cmd!(
"pageLayout.margins",
"Margins",
+23 -1
View File
@@ -1663,7 +1663,29 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) {
let s = big_button(ui, Icon::Paper, "Size", "Paper Size", true);
control_keytip(app, ui, &s, "pageLayout.size");
egui::Popup::menu(&s).show(|ui| {
for p in ["Letter", "Legal", "Tabloid", "Executive", "A3", "A4", "A5"] {
// B4/B5/B6 are the JIS sizes Japanese Excel uses; the ISO ones stay selectable by name.
for p in [
"Letter",
"Legal",
"Tabloid",
"Executive",
"A3",
"A4",
"A5",
"A6",
"B4",
"B5",
"B6",
"Japanese Postcard",
"Japanese Double Postcard",
"Japanese Envelope #3",
"Japanese Envelope #4",
"Japanese Envelope Kaku #2",
"Japanese Envelope Kaku #3",
"B4 (ISO)",
"B5 (ISO)",
"B6 (ISO)",
] {
if ui.button(p).clicked() {
act(app, "pageLayout.size", json!({"paper": p}));
}
+52 -3
View File
@@ -64,8 +64,10 @@ pub const INDEXED: [u32; 64] = [
0x003366, 0x339966, 0x003300, 0x333300, 0x993300, 0x993366, 0x333399, 0x333333, // 56-63
];
/// Paper sizes (`pageSetup paperSize`) we name.
const PAPERS: [(u32, &str); 14] = [
/// Paper sizes (`pageSetup paperSize`) we name. ID 12/13 are the JIS B4/B5 that
/// `pageLayout.pageSetup` calls `B4`/`B5`; 42 is ISO B4, and 43–88 are the Japanese postcards,
/// envelopes and B6. The engine's `known_paper` uses the same names and sizes.
const PAPERS: [(u32, &str); 23] = [
(1, "Letter"),
(3, "Tabloid"),
(4, "Ledger"),
@@ -80,14 +82,40 @@ const PAPERS: [(u32, &str); 14] = [
(20, "Envelope #10"),
(27, "Envelope DL"),
(34, "Envelope B5"),
(42, "B4 (ISO)"),
(43, "Japanese Postcard"),
(69, "Japanese Double Postcard"),
(70, "A6"),
(71, "Japanese Envelope Kaku #2"),
(72, "Japanese Envelope Kaku #3"),
(73, "Japanese Envelope #3"),
(74, "Japanese Envelope #4"),
(88, "B6"),
];
pub fn paper_name(id: u32) -> Option<&'static str> {
PAPERS.iter().find(|(i, _)| *i == id).map(|(_, n)| *n)
}
/// The file ID for a paper name. Spelling is loose like `known_paper`'s — spaces, hyphens,
/// underscores and parentheses don't matter, and the JIS/ISO and Japanese-envelope spellings the
/// page-setup commands take are folded onto the name in [`PAPERS`].
pub fn paper_id(name: &str) -> Option<u32> {
PAPERS.iter().find(|(_, n)| n.eq_ignore_ascii_case(name.trim())).map(|(i, _)| *i)
let norm = |s: &str| s.trim().to_ascii_lowercase().replace([' ', '-', '_', '(', ')'], "");
let normalized = norm(name);
let alias = match normalized.as_str() {
"b4jis" => "b4",
"b5jis" => "b5",
"b6jis" => "b6",
"postcard" => "japanesepostcard",
"doublepostcard" => "japanesedoublepostcard",
"chou#3" => "japaneseenvelope#3",
"chou#4" => "japaneseenvelope#4",
"kaku#2" => "japaneseenvelopekaku#2",
"kaku#3" => "japaneseenvelopekaku#3",
other => other,
};
PAPERS.iter().find(|(_, n)| norm(n) == alias).map(|(i, _)| *i)
}
/// Functions introduced after Excel 2007 that files store with the `_xlfn.` prefix. Entries with
@@ -339,5 +367,26 @@ mod tests {
assert_eq!(builtin_id(builtin_format(5).unwrap()), None);
assert_eq!(paper_name(9), Some("A4"));
assert_eq!(paper_id("a4"), Some(9));
// 12/13 are the JIS B4/B5 Japanese Excel uses; 42 is ISO B4.
assert_eq!(paper_name(12), Some("B4"));
assert_eq!(paper_id("b5"), Some(13));
assert_eq!(paper_id("B5 (JIS)"), Some(13));
assert_eq!(paper_id("B4 (ISO)"), Some(42));
// The Japanese papers page setup names.
assert_eq!(paper_name(43), Some("Japanese Postcard"));
assert_eq!(paper_name(88), Some("B6"));
for (name, id) in [
("Japanese Postcard", 43),
("japanese double postcard", 69),
("A6", 70),
("Japanese Envelope Kaku #2", 71),
("kaku #3", 72),
("Japanese Envelope #3", 73),
("chou #4", 74),
("B6 (JIS)", 88),
] {
assert_eq!(paper_id(name), Some(id), "{name}");
}
assert_eq!(paper_id("Napkin"), None);
}
}
+32
View File
@@ -1165,3 +1165,35 @@ fn doc_props_keep_boundary_whitespace() {
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
assert_eq!(back.props, wb.props);
}
/// Japanese paper sizes survive an XLSX save and reopen. IDs 12/13 are the JIS B4/B5; the
/// Japanese postcards and envelopes have their own IDs, and `B4 (ISO)` keeps ISO B4. The ISO B5/B6
/// names only exist in the engine: ECMA-376 has no `paperSize` id for them (only ISO B4), so they
/// are not part of the round-trip.
#[test]
fn japanese_paper_sizes_round_trip() {
for paper in [
"B4",
"B5",
"B6",
"B4 (ISO)",
"A6",
"Japanese Postcard",
"Japanese Double Postcard",
"Japanese Envelope #3",
"Japanese Envelope #4",
"Japanese Envelope Kaku #2",
"Japanese Envelope Kaku #3",
] {
let mut wb = Workbook::new();
wb.sheet_mut(0).unwrap().print.paper = paper.into();
let (back, report) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
assert!(report.warnings.is_empty(), "{paper}: {:?}", report.warnings);
assert_eq!(back.sheet(0).unwrap().print.paper, paper);
}
// An alias is written as the file's own name and read back as that name.
let mut wb = Workbook::new();
wb.sheet_mut(0).unwrap().print.paper = "B5 (JIS)".into();
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
assert_eq!(back.sheet(0).unwrap().print.paper, "B5");
}
+3 -2
View File
@@ -1,6 +1,6 @@
# Target-app parity: GridCraft vs Microsoft Excel
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (rows auto-fit oversized fonts on open and row formatting) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-21 · **Change:** minor (Page Setup paper sizes match Japanese Excel: B4/B5/B6 are the JIS sizes, with ISO B4/B5/B6 by name and the Japanese postcards, envelopes and A6 added; no score change) · **Target:** Microsoft Excel (Microsoft 365)
This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
[`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
@@ -86,7 +86,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2
| Sort, filter, tables, validation, outline | 8 | ~82% | ~70% | 10–16 | Multi-level sort, AutoFilter, Advanced Filter, tables with totals, validation, outline, subtotals, consolidate. Bugs: subtotal header (#131), validation edits (#129), table style panic caught (#130); no slicers on tables |
| PivotTables | 7 | ~65% | ~45% | 25–40 | Insert/Recommended, field list, three layouts, values on rows or columns, date grouping, show-values-as, refresh, calculated fields, XLSX round-trip. Missing: slicers, timelines, calculated items, PivotCharts, value/label filters, number grouping, GETPIVOTDATA, data-model pivots. Round-trip never checked in Excel |
| Charts and sparklines | 8 | ~50% | ~30% | 40–60 | 25 chart kinds, all 2-D. The chart model holds title, legend position, a data-labels flag, gridlines flag and axis titles; no axis scaling, trendlines, error bars, per-point formatting, chart sheets, 3-D, surface, map, Pareto. Several kinds don't survive XLSX round trip (open PRs #158, #161). [chart-parity.md](chart-parity.md) |
| Page layout, print, PDF | 5 | ~80% | ~55% | 12–20 | Page setup, print titles, breaks, Page Break Preview, headers/footers, PDF export and printing. No true Page Layout view; the PDF writer uses only the 14 standard fonts without embedding (no CJK or other non-Latin text in PDF); web print only exports (#141, #143) |
| Page layout, print, PDF | 5 | ~80% | ~55% | 12–20 | Page setup, print titles, breaks, Page Break Preview, headers/footers, PDF export and printing. Paper sizes match Japanese Excel: B4/B5/B6 are the JIS sizes, ISO B4/B5/B6 stay reachable by name, and Japanese Postcard (はがき), Japanese Double Postcard (往復はがき), the chou/kaku envelopes and A6 are available. No true Page Layout view; the PDF writer uses only the 14 standard fonts without embedding (no CJK or other non-Latin text in PDF); web print only exports (#141, #143) |
| What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables, Analysis ToolPak (13 of 19 tools, no charts), Solver (Simplex LP, a local nonlinear method and Evolutionary; Answer and Limits reports; no Sensitivity report or options dialog). Missing: Forecast Sheet, Analyze Data |
| Get & Transform, external data, data model | 5 | ~15% | ~10% | 60–100 | Import of CSV/TSV, JSON, HTML. No Power Query editor, no From Web, database or folder sources, no connections or refresh, no relationships or data model, no external workbook links (dropped with a warning) |
| Insert objects | 3 | ~55% | ~45% | 25–40 | Pictures, 8 shape kinds (Excel has ~170), text boxes, own icon set, ink with Ink to Shape, hyperlinks, checkboxes. Missing: SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot, pictures in cells (PR #105) |
@@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | minor | Page Setup paper sizes: `B4`/`B5`/`B6` are the JIS sizes Japanese Excel uses (ISO B4/B5/B6 by name), and Japanese Postcard, Japanese Double Postcard, the chou/kaku envelopes and A6 are added; PDF MediaBox and `file.print` follow, and the Size menu lists them |
| 2026-10-12 | minor | Essentials AutoFit evidence: rows auto-fit oversized fonts on open and row formatting (#833) |
| 2026-10-12 | minor | Sheet tabs: reveal the selected tab on navigation and resize while preserving manual scrolling (#681); no score change |
| 2026-10-12 | minor | PivotTables: values on rows (Σ Values in Rows at any position, all three layouts; `pivot.moveField` and the field pane move it; `dataOnRows` round-trips) (no score change) |