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 <noreply@anthropic.com>
This commit is contained in:
vangel76 2026-10-11 11:51:34 +02:00
parent 9f896c1249
commit fed3996944
6 changed files with 340 additions and 19 deletions

View File

@ -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,

View File

@ -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 |

View File

@ -1099,6 +1099,212 @@ fn mduration(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
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<f64> {
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<OddFirst> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<FnSpec> {
vec![
f!(
@ -1440,6 +1646,46 @@ pub(crate) fn specs() -> Vec<FnSpec> {
"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)]);

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |