mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-30 21:56:53 -06:00
IMSQRT: reproduce Excel's stray real part for negative reals
Excel computes IMSQRT through the polar form, so IMSQRT("-4") is
1.22514845490862E-16+2i rather than 2i. Tests assert strings read back
from Excel; the function-parity doc lists the remaining IM* differences.
This commit is contained in:
@@ -710,11 +710,26 @@ im1!(imexp, |z: Cx| Ok(z.exp()));
|
||||
im1!(imln, |z: Cx| z.ln());
|
||||
im1!(imlog10, |z: Cx| z.ln().map(|l| Cx::new(l.re / std::f64::consts::LN_10, l.im / std::f64::consts::LN_10)));
|
||||
im1!(imlog2, |z: Cx| z.ln().map(|l| Cx::new(l.re / std::f64::consts::LN_2, l.im / std::f64::consts::LN_2)));
|
||||
// Excel takes the polar route, so IMSQRT("-4") is 1.22514845490862E-16+2i, not 2i. Its cosine
|
||||
// reduces its argument with a 64-bit-mantissa pi, which is PI/2 + 2.5e-20 and shows up as the
|
||||
// stray real part. `+ 0.0` turns a -0 imaginary part into +0, so the angle stays +pi.
|
||||
// shortcut: parts above ~1E154 or below ~1E-155 keep the exact result (Excel gives #NUM! or 0), no known use.
|
||||
const HALF_PI_EXCESS: f64 = 2.508_278_806_33e-20;
|
||||
im1!(imsqrt, |z: Cx| {
|
||||
let m = z.abs();
|
||||
let re = ((m + z.re) / 2.0).sqrt();
|
||||
let im = ((m - z.re) / 2.0).sqrt();
|
||||
Ok(Cx::new(re, if z.im < 0.0 { -im } else { im }))
|
||||
let im = z.im + 0.0;
|
||||
let theta = match z.re {
|
||||
x if x == 0.0 => {
|
||||
if im == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
std::f64::consts::FRAC_PI_2.copysign(im)
|
||||
}
|
||||
}
|
||||
x if x > 0.0 => (im / x).atan(),
|
||||
x => (im / x).atan() + if im < 0.0 { -std::f64::consts::PI } else { std::f64::consts::PI },
|
||||
};
|
||||
let (half, root) = (theta / 2.0, z.abs().sqrt());
|
||||
Ok(Cx::new(root * (half.cos() + HALF_PI_EXCESS * half.abs().sin()), root * half.sin()))
|
||||
});
|
||||
im1!(imsin, |z: Cx| Ok(z.sin()));
|
||||
im1!(imcos, |z: Cx| Ok(z.cos()));
|
||||
@@ -1275,9 +1290,36 @@ mod tests {
|
||||
cx(ev("IMLOG10", vec![t("3+4i")]), 0.698_970_004_336_018_8, 0.402_719_196_273_373_2);
|
||||
cx(ev("IMLOG2", vec![t("3+4i")]), 2.321_928_094_887_362, 1.337_804_212_450_976_8);
|
||||
cx(ev("IMSQRT", vec![t("1+i")]), 1.098_684_113_467_809_9, 0.455_089_860_562_227_3);
|
||||
cx(ev("IMSQRT", vec![t("-4")]), 0.0, 2.0);
|
||||
is_text(ev("IMSQRT", vec![t("-4")]), "2i");
|
||||
is_text(ev("IMSQRT", vec![t("-9")]), "3i");
|
||||
// Strings read back from Excel (16.0): its stray real part on negative reals included.
|
||||
for (input, want) in [
|
||||
("-4", "1.22514845490862E-16+2i"),
|
||||
("-9", "1.83772268236293E-16+3i"),
|
||||
("-1", "6.1257422745431E-17+i"),
|
||||
("-2", "8.66310780426106E-17+1.4142135623731i"),
|
||||
("-100", "6.1257422745431E-16+10i"),
|
||||
("-1E10", "6.1257422745431E-12+100000i"),
|
||||
("-1E-10", "6.1257422745431E-22+0.00001i"),
|
||||
("-4+0j", "1.22514845490862E-16+2j"),
|
||||
("-4-0i", "1.22514845490862E-16+2i"),
|
||||
("-4-1E-20i", "1.22514845490862E-16-2i"),
|
||||
("-1+1E-10i", "5.00000653944413E-11+i"),
|
||||
("-0.5-1E-12i", "7.07212958930194E-13-0.707106781186548i"),
|
||||
("4", "2"),
|
||||
("2", "1.4142135623731"),
|
||||
("0", "0"),
|
||||
("3+4i", "2+i"),
|
||||
("3+4j", "2+j"),
|
||||
("-3+4i", "1+2i"),
|
||||
("-3-4i", "1-2i"),
|
||||
("-2i", "1-i"),
|
||||
("2i", "1+i"),
|
||||
("i", "0.707106781186548+0.707106781186547i"),
|
||||
("5-12i", "3-2i"),
|
||||
("-5+12i", "2+3i"),
|
||||
("1E10+1E10i", "109868.411346781+45508.9860562227i"),
|
||||
] {
|
||||
is_text(ev("IMSQRT", vec![t(input)]), want);
|
||||
}
|
||||
is_text(ev("IMSQRT", vec![t("4")]), "2");
|
||||
is_text(ev("IMSQRT", vec![t("-2i")]), "1-i");
|
||||
is_text(ev("IMPOWER", vec![t("2+3i"), n(3.0)]), "-46+9i");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Function parity: worksheet functions and formulas
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-25 · **Change:** minor (the LAMBDA row lists the recursion limit and its `#NUM!`, and the error a defined name holding an error gives when called; earlier: number text with accounting parentheses coerces like Excel 365; INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-26 · **Change:** minor (IMSQRT reproduces the stray real part Excel gives for negative reals, and the other IM* differences from Excel are listed; earlier: the LAMBDA row lists the recursion limit and its `#NUM!`, and the error a defined name holding an error gives when called; earlier: number text with accounting parentheses coerces like Excel 365; INDEX with one index on a 2-D reference is #REF! as in Excel; earlier: SUBTOTAL and AGGREGATE cap the cells they visit, #2175) · **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).
|
||||
@@ -90,6 +90,7 @@ This improves series preparation; it does not change the model-fitting or season
|
||||
| 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!`. 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 |
|
||||
| IMSQRT and the other IM* functions against Excel's text | IMSQRT follows Excel's polar route, so a negative real gives Excel's stray real part: `IMSQRT("-4")` is `1.22514845490862E-16+2i` (checked in Excel 16.0 on 79 inputs). Differences kept: IMSQRT of a number with a part above about 1E154 or below about 1E-155 gives the exact root (Excel gives `#NUM!` or 0); IMPOWER, IMEXP, IMSIN, IMCOS, IMSINH and IMCOSH keep exact results where Excel shows the same kind of noise (`IMPOWER("-4",2)` is `16-3.92047505570758E-15i`) or different trailing digits near multiples of pi/2; IMCSC and IMCSCH differ by one unit in the 15th digit; IMARGUMENT returns 16–17 digits where Excel returns 15; the text of IM* and COMPLEX results differs for some magnitudes (Excel writes `COMPLEX(1E-10,1)` as `0.0000000001+i` but `COMPLEX(0.000000123456789012345,1)` as `1.23456789012345E-07+i`, and an integer past 15 digits rounded and zero-padded) |
|
||||
| External workbook references (`[Book.xlsx]Sheet1!A1`) | parsed; links dropped on open, no update |
|
||||
| Localized function names and `;` separators | **missing** (#45); en-US only |
|
||||
| Long flat operator chains (`=A1+A2+…`, `-`, `*`, `/`, `^`, `&`, comparisons) | done: no operand limit below Excel's 8,192-character formula limit (Excel, observed: `=1+1+…` with 4,096 operands is exactly 8,192 characters with the `=` and calculates; one more character is refused). Evaluated (#484), printed (#628), stored, copied, adjusted, saved and reopened without recursing once per operand, on a 1 MiB stack (#916); written to XLSX as plain formulas, not dynamic arrays; Spanish entry accepts them |
|
||||
@@ -123,6 +124,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-26 | minor | IMSQRT uses Excel's polar route with its cosine reduction, so `IMSQRT("-4")` is `1.22514845490862E-16+2i` as in Excel (exact `2i` before); the rows list the IM* differences still open |
|
||||
| 2026-10-25 | minor | LAMBDA row: a LAMBDA that calls itself more than 140 times, and a formula nested past the depth limit, give `#NUM!` (#1809; nesting gave `#CALC!` before). `=Foo(1)` where the defined name Foo holds `#N/A` gives `#N/A`, not `#NAME?` |
|
||||
| 2026-10-23 | minor | Number text coercion (VALUE, arithmetic, typed entry): a dollar sign before accounting parentheses (`$(1,234)`, `$ (5)`) and a percent sign after them (`(5)%`) read as negatives; `(-5)`, `((5))`, `($-5)`, `$5%`, `$(5)%` and dates or times in parentheses are `#VALUE!`, as observed in Excel 365 |
|
||||
| 2026-10-23 | minor | INDEX with only a row index on a reference of several rows and columns (`INDEX(A1:C3,2)`, `INDEX(A1:C3,0)`) is `#REF!`, as observed in Excel, so it also fails as a range endpoint and inside other functions; an empty column argument (`INDEX(A1:C3,2,)`) still gives the row. Checked that a column array with one index stays a 1×1 array (`TYPE` 64), which is what Excel returns |
|
||||
|
||||
Reference in New Issue
Block a user