From fed39969440b33bad4f3eece47c2a29e7640c6a2 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sun, 11 Oct 2026 11:51:34 +0200 Subject: [PATCH] Functions: ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD (#275) Odd first and last coupon periods via quasi-coupon periods, from the formulas on Microsoft's public function pages. ODDLYIELD is closed form; ODDFYIELD bisects ODDFPRICE (bounded). Financial is now 55 / 55. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- ROADMAP.md | 8 +- crates/functions/src/financial.rs | 317 ++++++++++++++++++++++++++++++ docs/function-parity.md | 16 +- docs/gaps.md | 5 +- docs/target-app-parity.md | 11 +- 6 files changed, 340 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 770b3fe..8e65dbb 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ egui frontend is a separate crate, so the UI can be swapped without touching the GridCraft is in **alpha**: the core workflow works end to end, but depth, fidelity and file compatibility are still rough. It covers 89% of the Excel ribbon commands in our catalog -([`docs/parity-checklist.md`](docs/parity-checklist.md)) and 501 of Excel's 523 worksheet functions +([`docs/parity-checklist.md`](docs/parity-checklist.md)) and 505 of Excel's 523 worksheet functions ([`docs/function-parity.md`](docs/function-parity.md)); weighted by what Excel users rely on, we estimate it is about 50% ready for real work. Next up: XLSX fidelity on real-world files, very large workbooks and multi-threaded recalc, chart depth, text in every script, and `.xls` import. Stage, diff --git a/ROADMAP.md b/ROADMAP.md index 1189fd7..431eeba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (worksheet functions 505 / 523) · **Target:** Microsoft Excel (Microsoft 365) 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. @@ -21,7 +21,7 @@ This page is the summary. The detail is in: | Number | Value | Kind | |---|---|---| -| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | +| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 505 / 523 (96.6%; 496 working, 94.8%) | | **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) | | **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) | | **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) | @@ -74,7 +74,7 @@ collaboration, mobile, AI). | Area | Breadth | Ready | Hours | Doc | |---|---|---|---|---| | Formula language and calculation | ~92% | ~72% | 20–30 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | -| Worksheet functions | 95.8% measured | ~82% | 18–30 | [function-parity.md](docs/function-parity.md) | +| Worksheet functions | 96.6% measured | ~82% | 16–26 | [function-parity.md](docs/function-parity.md) | | Editing, navigation, clipboard, fill | ~90% | ~70% | 15–25 | [ui-parity.md](docs/ui-parity.md) | | Cell formatting, styles, conditional formatting | ~95% | ~80% | 8–14 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | | Sort, filter, tables, validation, outline | ~82% | ~70% | 10–16 | [target-app-parity.md](docs/target-app-parity.md#feature-areas) | @@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-11 | Odd-period bond functions ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD: Financial 55 / 55 (#275) | | 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed | | 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 | | 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows | @@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Worksheet functions 505 / 523 (ODD* bond functions); progress log row | | 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) | | 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha | diff --git a/crates/functions/src/financial.rs b/crates/functions/src/financial.rs index 17cc789..5177e2a 100644 --- a/crates/functions/src/financial.rs +++ b/crates/functions/src/financial.rs @@ -1099,6 +1099,212 @@ fn mduration(a: &[Arg], _c: &mut dyn Ctx) -> R { num_val(d / (1.0 + y / f)) } +// --------------------------------------------------------------------------------------------- +// Odd first and last coupon periods +// --------------------------------------------------------------------------------------------- +// +// An odd period is split into quasi-coupon periods: the regular coupon periods the security +// would have had, counted back from the first coupon (ODDF*) or forward from the last interest +// date (ODDL*). For quasi-period i, NL is its normal length, DC the days of it inside the odd +// period, A the accrued days of it before settlement and DSC the days of it from settlement on. + +/// Most quasi-coupon periods an odd period may span (the date range allows ~32,400 quarters). +const MAX_QUASI: i64 = 40_000; + +/// Normal length of the coupon period `[start, end]` under a basis (COUPDAYS semantics). +fn normal_length(start: f64, end: f64, freq: f64, basis: u32) -> f64 { + match basis { + 1 => end - start, + 3 => 365.0 / freq, + _ => 360.0 / freq, + } +} + +/// Days of `[from, to]` under a basis, or 0 when the span is empty. +fn span(from: f64, to: f64, basis: u32) -> R { + if to > from { day_count(from, to, basis) } else { Ok(0.0) } +} + +/// The odd-first-period terms that ODDFPRICE and ODDFYIELD share. +struct OddFirst { + /// Coupons after the first coupon up to maturity. + n: f64, + /// Whole quasi-coupon periods between settlement and the first coupon. + nq: f64, + /// DSC / NL of the quasi-period holding settlement. + frac: f64, + /// Σ DCi / NLi over the odd period. + dc: f64, + /// Σ Ai / NLi over the odd period. + acc: f64, + freq: f64, +} + +fn odd_first(settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u32) -> R { + if !(mat > first && first > settle && settle > issue) { + return Err(CellError::Num); + } + let anchor = ymd(first)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + let quasi = |k: i64| shift_months(anchor, -k * step, eom); + let (mut dc, mut acc) = (0.0, 0.0); + let (mut nq, mut frac) = (None, 0.0); + // Walk back from the first coupon: period k runs from quasi(k + 1) to quasi(k). + let mut k = 0; + loop { + if k >= MAX_QUASI { + return Err(CellError::Num); + } + let (end, start) = (quasi(k)?, quasi(k + 1)?); + let nl = normal_length(start, end, freq, basis); + if nl <= 0.0 { + return Err(CellError::Num); + } + let from = start.max(issue); + dc += span(from, end, basis)? / nl; + acc += span(from, settle.min(end), basis)? / nl; + if start <= settle && settle < end { + nq = Some(k as f64); + frac = span(settle, end, basis)? / nl; + } + if start <= issue { + break; + } + k += 1; + } + let nq = nq.ok_or(CellError::Num)?; + let n = coupons(first, mat, freq)?.num; + Ok(OddFirst { n, nq, frac, dc, acc, freq }) +} + +fn odd_first_price(t: &OddFirst, rate: f64, yld: f64, red: f64) -> f64 { + let coupon = 100.0 * rate / t.freq; + let base = 1.0 + yld / t.freq; + let lead = t.nq + t.frac; + let mut p = red / base.powf(t.n + lead) + coupon * t.dc / base.powf(lead); + let mut k = 1.0; + while k <= t.n { + p += coupon / base.powf(k + lead); + k += 1.0; + } + p - coupon * t.acc +} + +/// settlement, maturity, issue, first_coupon, rate, yld/pr, redemption, frequency, [basis]. +fn odd_first_args(a: &[Arg]) -> R<(OddFirst, f64, f64, f64)> { + let (s, m, issue, first) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, date(a, 3)?); + let (rate, x, red) = (num(a, 4)?, num(a, 5)?, num(a, 6)?); + let (f, basis) = (freq_arg(a, 7)?, basis_arg(a, 8)?); + if rate < 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + Ok((odd_first(s, m, issue, first, f, basis)?, rate, x, red)) +} + +fn oddfprice(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (t, rate, yld, red) = odd_first_args(a)?; + if yld < 0.0 { + return Err(CellError::Num); + } + num_val(odd_first_price(&t, rate, yld, red)) +} + +fn oddfyield(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (t, rate, pr, red) = odd_first_args(a)?; + if pr <= 0.0 { + return Err(CellError::Num); + } + // The price falls as the yield rises: bracket the root, then bisect. + let g = |y: f64| odd_first_price(&t, rate, y, red) - pr; + let (mut lo, mut hi) = (-0.999_999 * t.freq, 1.0); + let mut guard = 0; + while g(hi) > 0.0 && guard < 60 { + hi *= 2.0; + guard += 1; + } + // NaN fails both comparisons. + if !(g(lo) >= 0.0 && g(hi) <= 0.0) { + return Err(CellError::Num); + } + for _ in 0..200 { + let mid = 0.5 * (lo + hi); + if g(mid) > 0.0 { + lo = mid; + } else { + hi = mid; + } + } + num_val(0.5 * (lo + hi)) +} + +/// Σ DCi/NLi, Σ Ai/NLi and Σ DSCi/NLi over an odd last period. +fn odd_last(settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, f64, f64)> { + if !(mat > settle && settle > last) { + return Err(CellError::Num); + } + let anchor = ymd(last)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + let (mut dc, mut acc, mut dsc) = (0.0, 0.0, 0.0); + // Walk forward from the last interest date: period k runs from quasi(k - 1) to quasi(k). + let mut start = last; + let mut k = 1; + loop { + if k > MAX_QUASI { + return Err(CellError::Num); + } + let end = shift_months(anchor, k * step, eom)?; + let nl = normal_length(start, end, freq, basis); + if nl <= 0.0 { + return Err(CellError::Num); + } + let stop = end.min(mat); + dc += span(start, stop, basis)? / nl; + acc += span(start, settle.min(stop), basis)? / nl; + dsc += span(settle.max(start), stop, basis)? / nl; + if end >= mat { + break; + } + start = end; + k += 1; + } + Ok((dc, acc, dsc)) +} + +/// settlement, maturity, last_interest, rate, yld/pr, redemption, frequency, [basis]. +fn odd_last_args(a: &[Arg]) -> R<((f64, f64, f64), f64, f64, f64, f64)> { + let (s, m, last) = (date(a, 0)?, date(a, 1)?, date(a, 2)?); + let (rate, x, red) = (num(a, 3)?, num(a, 4)?, num(a, 5)?); + let (f, basis) = (freq_arg(a, 6)?, basis_arg(a, 7)?); + if rate < 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + Ok((odd_last(s, m, last, f, basis)?, rate, x, red, f)) +} + +fn oddlprice(a: &[Arg], _c: &mut dyn Ctx) -> R { + let ((dc, acc, dsc), rate, yld, red, f) = odd_last_args(a)?; + if yld < 0.0 { + return Err(CellError::Num); + } + let coupon = 100.0 * rate / f; + num_val((red + dc * coupon) / (1.0 + dsc * yld / f) - acc * coupon) +} + +fn oddlyield(a: &[Arg], _c: &mut dyn Ctx) -> R { + let ((dc, acc, dsc), rate, pr, red, f) = odd_last_args(a)?; + if pr <= 0.0 { + return Err(CellError::Num); + } + let coupon = 100.0 * rate / f; + let paid = pr + acc * coupon; + if paid <= 0.0 || dsc <= 0.0 { + return Err(CellError::Num); + } + num_val((red + dc * coupon - paid) / paid * f / dsc) +} + pub(crate) fn specs() -> Vec { vec![ f!( @@ -1440,6 +1646,46 @@ pub(crate) fn specs() -> Vec { "Modified duration of a security with periodic interest.", mduration ), + f!( + "ODDFPRICE", + 8, + 9, + Financial, + S, + "ODDFPRICE(settlement, maturity, issue, first_coupon, rate, yld, redemption, frequency, [basis])", + "Price per 100 face value of a security with an odd first period.", + oddfprice + ), + f!( + "ODDFYIELD", + 8, + 9, + Financial, + S, + "ODDFYIELD(settlement, maturity, issue, first_coupon, rate, pr, redemption, frequency, [basis])", + "Yield of a security with an odd first period.", + oddfyield + ), + f!( + "ODDLPRICE", + 7, + 8, + Financial, + S, + "ODDLPRICE(settlement, maturity, last_interest, rate, yld, redemption, frequency, [basis])", + "Price per 100 face value of a security with an odd last period.", + oddlprice + ), + f!( + "ODDLYIELD", + 7, + 8, + Financial, + S, + "ODDLYIELD(settlement, maturity, last_interest, rate, pr, redemption, frequency, [basis])", + "Yield of a security with an odd last period.", + oddlyield + ), ] } @@ -1599,6 +1845,77 @@ mod tests { close_tol(ev("YIELD", vec![d(2008, 2, 15), d(2008, 6, 15), n(0.05), n(pv), n(100.0), n(2.0)]), 0.04, 1e-9); } + fn number(v: Value) -> f64 { + match v { + Value::Number(x) => x, + other => panic!("expected a number, got {other:?}"), + } + } + + #[test] + fn odd_periods() { + // The worked examples on Microsoft's function pages (results as printed there). + let first = |x: f64, rest: &[f64]| { + let mut a = vec![d(2008, 11, 11), d(2021, 3, 1), d(2008, 10, 15), d(2009, 3, 1), n(x)]; + a.extend(rest.iter().map(|v| n(*v))); + a + }; + close_tol(ev("ODDFPRICE", first(0.0785, &[0.0625, 100.0, 2.0, 1.0])), 113.60, 0.005 / 113.6); + close_tol(ev("ODDFYIELD", first(0.0575, &[84.50, 100.0, 2.0, 0.0])), 0.0772, 0.00005); + let lp = vec![d(2008, 2, 7), d(2008, 6, 15), d(2007, 10, 15), n(0.0375), n(0.0405), n(100.0), n(2.0), n(0.0)]; + close_tol(ev("ODDLPRICE", lp), 99.88, 0.005 / 99.88); + let ly = vec![d(2008, 4, 20), d(2008, 6, 15), d(2007, 12, 24), n(0.0375), n(99.875), n(100.0), n(2.0), n(0.0)]; + close_tol(ev("ODDLYIELD", ly), 0.04519, 0.000005); + + // Yield → price → yield, for a short and a long odd first period and a long odd last period. + for (issue, basis) in [(d(2008, 10, 15), 0.0), (d(2007, 1, 20), 1.0), (d(2008, 1, 31), 3.0)] { + let args = |x: f64| vec![d(2008, 11, 11), d(2021, 3, 1), issue.clone(), d(2009, 3, 1), n(0.06), n(x), n(100.0), n(2.0), n(basis)]; + let p = number(ev("ODDFPRICE", args(0.05))); + close_tol(ev("ODDFYIELD", args(p)), 0.05, 1e-9); + } + let last = |x: f64| vec![d(2009, 2, 1), d(2009, 12, 15), d(2008, 3, 15), n(0.04), n(x), n(100.0), n(4.0), n(1.0)]; + let p = number(ev("ODDLPRICE", last(0.07))); + close_tol(ev("ODDLYIELD", last(p)), 0.07, 1e-9); + + // Argument checks: frequency, basis, date order, negative yield, non-positive price. + is_err(ev("ODDFPRICE", first(0.0785, &[0.0625, 100.0, 3.0, 1.0])), CellError::Num); + is_err(ev("ODDFPRICE", first(0.0785, &[0.0625, 100.0, 2.0, 5.0])), CellError::Num); + is_err(ev("ODDFPRICE", first(0.0785, &[-0.01, 100.0, 2.0, 1.0])), CellError::Num); + is_err(ev("ODDFYIELD", first(0.0575, &[0.0, 100.0, 2.0, 0.0])), CellError::Num); + is_err( + ev("ODDFPRICE", vec![d(2009, 4, 1), d(2021, 3, 1), d(2008, 10, 15), d(2009, 3, 1), n(0.07), n(0.06), n(100.0), n(2.0)]), + CellError::Num, + ); + is_err(ev("ODDLPRICE", vec![d(2007, 10, 1), d(2008, 6, 15), d(2007, 10, 15), n(0.0375), n(0.0405), n(100.0), n(2.0)]), CellError::Num); + is_err(ev("ODDLYIELD", vec![d(2008, 4, 20), d(2008, 6, 15), d(2007, 12, 24), n(0.0375), n(-1.0), n(100.0), n(2.0)]), CellError::Num); + is_err( + ev("ODDFPRICE", first(0.0785, &[0.0625, 100.0, 2.0, 1.0]).into_iter().take(4).chain([t("x"), n(0.06), n(100.0), n(2.0)]).collect()), + CellError::Value, + ); + } + + #[test] + fn odd_periods_survive_hostile_input() { + // Quarterly quasi-periods across the whole date range, huge rates and yields: a value or + // an error, never a panic or a hang. + let wide = |x: f64, y: f64| vec![n(2_957_000.0), n(2_958_465.0), n(400.0), n(2_957_900.0), n(x), n(y), n(1e300), n(4.0), n(1.0)]; + for (x, y) in [(0.05, 0.05), (1e300, 1e300), (0.0, 1e-300)] { + let _ = ev("ODDFPRICE", wide(x, y)); + let _ = ev("ODDFYIELD", wide(x, y)); + } + // The long walk itself works: settled early in an odd first period of ~32,000 quarters, + // a small yield round-trips. + let long = |y: f64| vec![n(500.0), n(2_958_465.0), n(400.0), n(2_957_900.0), n(0.05), n(y), n(100.0), n(4.0), n(1.0)]; + let p = number(ev("ODDFPRICE", long(0.0001))); + close_tol(ev("ODDFYIELD", long(p)), 0.0001, 1e-9); + let last = |x: f64, y: f64| vec![n(2_958_000.0), n(2_958_465.0), n(400.0), n(x), n(y), n(1e300), n(4.0), n(1.0)]; + for (x, y) in [(0.05, 0.05), (1e300, 1e300), (0.0, f64::MAX)] { + let _ = ev("ODDLPRICE", last(x, y)); + let _ = ev("ODDLYIELD", last(x, y)); + } + is_err(ev("ODDFPRICE", vec![n(-5.0), n(1e20), n(0.0), n(1e10), n(0.05), n(0.05), n(100.0), n(2.0)]), CellError::Num); + } + #[test] fn lifting() { let v = ev("SLN", vec![av(row(&[100.0, 200.0])), n(0.0), n(10.0)]); diff --git a/docs/function-parity.md b/docs/function-parity.md index 8ff9d49..2f9178f 100644 --- a/docs/function-parity.md +++ b/docs/function-parity.md @@ -1,6 +1,6 @@ # Function parity: worksheet functions and formulas -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created; measured against Microsoft's published Excel function list) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD added) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Excel's worksheet functions, category by category, against GridCraft's function library. Part of [target-app-parity.md](target-app-parity.md); gaps feed [gaps.md](gaps.md). @@ -12,7 +12,7 @@ Excel's worksheet functions, category by category, against GridCraft's function them), plus 14 text functions the page lists under combined entries (FIND/FINDB, LEFT/LEFTB, LEN/LENB, MID/MIDB, REPLACE/REPLACEB, RIGHT/RIGHTB, SEARCH/SEARCHB), BETA.INV, and COPILOT and PY (Microsoft 365 functions not on that page when fetched). -- **Ours (501):** every function spec in `crates/functions/src/*.rs` (`f!("NAME", …)`) plus the +- **Ours (505):** every function spec in `crates/functions/src/*.rs` (`f!("NAME", …)`) plus the evaluator's special forms in `crates/calc/src/eval.rs` (`SPECIAL_FUNCTIONS`: IF, IFS, CHOOSE, OFFSET, INDIRECT, INDEX, SUBTOTAL, AGGREGATE, TEXT, LET, LAMBDA, MAP, REDUCE, SCAN, BYROW, BYCOL, MAKEARRAY…), excluding the test-only `ADD2`. Every one is an Excel name. @@ -23,13 +23,13 @@ Excel's worksheet functions, category by category, against GridCraft's function ## Coverage -**Measured: 501 / 523 by name (95.8%); 492 / 523 working (94.1%).** +**Measured: 505 / 523 by name (96.6%); 496 / 523 working (94.8%).** | Category | Excel | Ours | Working | % working | Missing | |---|---|---|---|---|---| | Statistical | 111 | 107 | 107 | 96% | FORECAST.ETS, FORECAST.ETS.CONFINT, FORECAST.ETS.SEASONALITY, FORECAST.ETS.STAT | | Math and trigonometry | 80 | 79 | 79 | 99% | PERCENTOF | -| Financial | 55 | 51 | 51 | 93% | ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD | +| Financial | 55 | 55 | 55 | 100% | — | | Engineering | 54 | 54 | 54 | 100% | — | | Text | 50 | 47 | 47 | 94% | BAHTTEXT, DETECTLANGUAGE, TRANSLATE | | Lookup and reference | 40 | 36 | 36 | 90% | GETPIVOTDATA, IMAGE, RTD, TRIMRANGE | @@ -42,9 +42,9 @@ Excel's worksheet functions, category by category, against GridCraft's function | Web | 3 | 3 | 1 | 33% | (WEBSERVICE, FILTERXML are stubs; ENCODEURL works) | | Add-in and Automation | 3 | 0 | 0 | 0% | CALL, EUROCONVERT, REGISTER.ID | | AI and Python | 2 | 0 | 0 | 0% | COPILOT, PY | -| **Total** | **523** | **501** | **492** | **94.1%** | 22 missing, 9 stubs | +| **Total** | **523** | **505** | **496** | **94.8%** | 18 missing, 9 stubs | -By the Excel version that introduced them (working / total): before 2010 342 / 360, 2010 +By the Excel version that introduced them (working / total): before 2010 346 / 360, 2010 56 / 56, 2013 49 / 51, 2016 1 / 5, 2019 6 / 6, 2021 11 / 11, 2024 22 / 23, Microsoft 365 5 / 11. Dynamic-array and LAMBDA-era functions (FILTER, SORT, UNIQUE, XLOOKUP, VSTACK, TAKE, GROUPBY, PIVOTBY, REGEXTEST…) are all present. @@ -54,7 +54,6 @@ PIVOTBY, REGEXTEST…) are all present. | Function(s) | In scope? | Estimate | Note | |---|---|---|---| | FORECAST.ETS family (4) | yes | 4–6 h | AAA exponential smoothing with seasonality detection; needed for Forecast Sheet too | -| ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD | yes | 2–4 h | Odd first/last period bond maths | | GETPIVOTDATA | yes | 2–3 h | Needs the pivot engine's result cache; also the "Generate GetPivotData" option | | TRIMRANGE, PERCENTOF, BAHTTEXT | yes | 1–2 h | TRIMRANGE also implies the `.:` trim-ref operators | | IMAGE | yes | 3–5 h | Pictures in cells (see PR #105); fetching URLs needs a privacy decision | @@ -89,7 +88,7 @@ reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past 10-09). Statistical distributions and financial functions with iterative solvers (RATE, IRR, XIRR, YIELD) are the highest-risk for last-digit differences. -Work to close (18–30 h in total): the in-scope missing functions (12–20 h with the table above), +Work to close (16–26 h in total): the in-scope missing functions (10–16 h with the table above), an oracle corpus of Excel-computed results per function (owner runs Excel on generated workbooks; results checked in as data, not Excel files) and fixing what it finds (6–10 h). @@ -97,4 +96,5 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD added: Financial 55 / 55; 505 / 523 by name, 496 working | | 2026-10-10 | major | Created: 501 / 523 by name, 492 working, per category, against Microsoft's published list | diff --git a/docs/gaps.md b/docs/gaps.md index aa752b8..1c3e3dc 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from the full re-measure against Excel for Mac 16.113.4 and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (§15: the ODD* bond functions landed) · **Target:** Microsoft Excel (Microsoft 365) Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone using GridCraft for real work first). This is the work list: pick from the top, check the open @@ -106,7 +106,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- #143). **Estimate:** 12–20 h. ### 15. Remaining functions and an oracle suite (F) -- 22 missing, 9 stubs; no Excel-computed oracle corpus; #137, #138. **Estimate:** 18–30 h. +- 18 missing, 9 stubs; no Excel-computed oracle corpus; #137, #138. **Estimate:** 16–26 h. [function-parity.md](function-parity.md) ### 16. Collaboration and review (F) @@ -149,6 +149,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | §15: ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD added (18 functions missing) | | 2026-10-10 | minor | Updated for the 10-10 merges (#17, #31, #38, #62, #82, #104, #116) and the new chart resize bug (#190) | | 2026-10-10 | minor | Noted that no gap blocks the alpha gate | | 2026-10-10 | major | Created: 24 ranked gaps with evidence, from the full re-measure and the 60 open issues | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2e5ed01..3cfedf5 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Target-app parity: GridCraft vs Microsoft Excel -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (worksheet functions 505 / 523: the four ODD* bond functions) · **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: @@ -14,7 +14,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md). |---|---|---| | Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements | | Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | -| Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) | +| Worksheet functions, by name | **505 / 523 (96.6%)** | measured: registered functions vs Microsoft's published function list (method below) | | Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) | | **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) | | **Mainstream practitioner** | **~50%** | estimated; [method](#mainstream-practitioner) (69.5 depth × 0.90 × 0.92 × 0.88 = 50.6) | @@ -53,7 +53,7 @@ long `.xls` track, so about half parallelizes (≈30–50 wall-clock hours with LEFT/LEFTB, LEN/LENB, MID/MIDB, REPLACE/REPLACEB, RIGHT/RIGHTB, SEARCH/SEARCHB, BETA.INV) and COPILOT and PY: **523 functions**. Ours: every `f!(…)` spec in `crates/functions/src/*.rs` plus `SPECIAL_FUNCTIONS` in `crates/calc/src/eval.rs` (evaluator special forms), minus the test-only - `ADD2`: **501**, all of them Excel names. + `ADD2`: **505**, all of them Excel names. - **Ribbon catalog:** `crates/engine/src/catalog.rs` (290 unique ids across 15 tabs) against the 313 command ids registered in `crates/engine/src/cmd/*.rs`, recomputed from source by script and identical to the committed `parity-checklist.md`. The catalog is our own transcription of @@ -80,7 +80,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2 | Area | Weight | Breadth | Ready | Hours | Evidence | |---|---|---|---|---|---| | Formula language and calculation | 18 | ~92% | ~72% | 20–30 | A1/R1C1 parser, reference adjustment, dependency-graph recalc, dynamic arrays and `#SPILL!`, `A1#`, LET/LAMBDA and helpers, structured references, names, 3-D references. Open correctness bugs: 15-digit equality (#57), stale spills after structural edits and cleared blockers (#133, #139), 3-D span endpoint deletion (#140), sheet rename and qualified names (#134). Single-threaded | -| Worksheet functions | 12 | 95.8% (measured) | ~82% | 18–30 | 501 / 523 names, 492 working. Missing: FORECAST.ETS family, ODDF*/ODDL*, GETPIVOTDATA, TRIMRANGE, PERCENTOF, BAHTTEXT, IMAGE plus cloud functions. No conformance suite against Excel-computed oracles; edge-case bugs keep surfacing (#137, #138). [function-parity.md](function-parity.md) | +| Worksheet functions | 12 | 96.6% (measured) | ~82% | 16–26 | 505 / 523 names, 496 working. Missing: FORECAST.ETS family, GETPIVOTDATA, TRIMRANGE, PERCENTOF, BAHTTEXT, IMAGE plus cloud functions. No conformance suite against Excel-computed oracles; edge-case bugs keep surfacing (#137, #138). [function-parity.md](function-parity.md) | | Editing, navigation, clipboard, fill | 12 | ~90% | ~70% | 15–25 | Point mode, F4, AutoComplete, Paste Special, AutoFill, Flash Fill, Find/Replace, Go To Special. Bugs: rectangular drag-select (#28), monthly fill (#132), cut across sheets (#128), Insert Copied Cells missing (#21), key tips only on part of the Home tab (#43; #33, #49). [ui-parity.md](ui-parity.md) | | Cell formatting, styles, conditional formatting | 10 | ~95% | ~80% | 8–14 | Full number-format language, borders, 47 cell styles, 60 table styles, all CF rule kinds including data bars, colour scales and icon sets. Gaps: font weights beyond regular/bold (#168), system fonts list (#4), themes | | 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 | @@ -277,7 +277,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land - Engine: A1/R1C1 formula parser with reference adjustment, dependency-graph recalculation with dynamic arrays (spill + `#SPILL!`), cycle detection, LET/LAMBDA/MAP/REDUCE/SCAN/BYROW/BYCOL/ MAKEARRAY, structured table references, defined names, INDIRECT/OFFSET/INDEX references, - 501 worksheet functions (478 at M0) (math, statistics and distributions, text and regex, lookup incl. + 505 worksheet functions (478 at M0) (math, statistics and distributions, text and regex, lookup incl. XLOOKUP/FILTER/SORT/UNIQUE/VSTACK, dates, financial incl. bonds, engineering incl. complex numbers and Bessel, database, information). - Number formats: Excel's full format-code language (sections, conditions, colours, dates and @@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Worksheet functions 505 / 523 (96.6%; 496 working): ODDFPRICE, ODDFYIELD, ODDLPRICE, ODDLYIELD | | 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) | | 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |