| 1 | package calc |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | // evalNum evaluates src and requires the result to be KindNum. Like |
| 6 | // mustParse (eval_test.go), it never puts src into a failure message — |
| 7 | // formula text is cell content, and this codebase's discipline is that |
| 8 | // cell content never appears in a string a person might read off a log |
| 9 | // or a test report. |
| 10 | func evalNum(t *testing.T, src string, env Env) float64 { |
| 11 | t.Helper() |
| 12 | v := Eval(mustParse(t, src), env) |
| 13 | if v.Kind != KindNum { |
| 14 | t.Fatalf("result Kind = %v, want KindNum (value %+v)", v.Kind, v) |
| 15 | } |
| 16 | return v.Num |
| 17 | } |
| 18 | |
| 19 | func TestSum(t *testing.T) { |
| 20 | if got := evalNum(t, "SUM(1,2,3)", emptyEnv{}); got != 6 { |
| 21 | t.Errorf("SUM(1,2,3) = %v, want 6", got) |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | func TestSumIgnoresBlankAndText(t *testing.T) { |
| 26 | env := mapEnv{{1, 1}: {Kind: KindNum, Num: 5}, {1, 2}: {Kind: KindStr, Str: "x"}} |
| 27 | // A1=5 (num), B1="x" (str, ignored), C1 blank (never set, ignored). |
| 28 | if got := evalNum(t, "SUM(A1,B1,C1)", env); got != 5 { |
| 29 | t.Errorf("SUM(A1,B1,C1) = %v, want 5", got) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestSumPropagatesError(t *testing.T) { |
| 34 | env := mapEnv{{1, 1}: {Kind: KindErr, Err: ErrRef}} |
| 35 | v := Eval(mustParse(t, "SUM(A1,1)"), env) |
| 36 | if v.Kind != KindErr || v.Err != ErrRef { |
| 37 | t.Fatalf("SUM(A1,1) = %v, want #REF!", v) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestAverage(t *testing.T) { |
| 42 | if got := evalNum(t, "AVERAGE(2,4,6)", emptyEnv{}); got != 4 { |
| 43 | t.Errorf("AVERAGE(2,4,6) = %v, want 4", got) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // A literal non-numeric string passed directly is #VALUE! (see |
| 48 | // TestSumLiteralNonNumericStringIsValueError), not silently ignored — so |
| 49 | // this test reaches "zero numeric values" the way Excel actually allows: |
| 50 | // text read FROM references, which AVERAGE skips rather than errors on. |
| 51 | func TestAverageOfNoNumbersIsDiv0(t *testing.T) { |
| 52 | env := mapEnv{{1, 1}: {Kind: KindStr, Str: "a"}, {1, 2}: {Kind: KindStr, Str: "b"}} |
| 53 | v := Eval(mustParse(t, "AVERAGE(A1,B1)"), env) |
| 54 | if v.Kind != KindErr || v.Err != ErrDiv0 { |
| 55 | t.Fatalf("AVERAGE(A1,B1) = %v, want #DIV/0!", v) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // COUNT counts only numbers; COUNTA counts everything non-blank; neither |
| 60 | // counts a blank cell — the exact distinction the brief calls out. |
| 61 | func TestCountVsCounta(t *testing.T) { |
| 62 | env := mapEnv{ |
| 63 | {1, 1}: {Kind: KindNum, Num: 1}, |
| 64 | {1, 2}: {Kind: KindStr, Str: "x"}, |
| 65 | {1, 3}: {Kind: KindBool, Bool: true}, |
| 66 | // D1 (col 4) left unset: blank. |
| 67 | } |
| 68 | if got := evalNum(t, "COUNT(A1,B1,C1,D1)", env); got != 1 { |
| 69 | t.Errorf("COUNT = %v, want 1", got) |
| 70 | } |
| 71 | if got := evalNum(t, "COUNTA(A1,B1,C1,D1)", env); got != 3 { |
| 72 | t.Errorf("COUNTA = %v, want 3", got) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | func TestMinMax(t *testing.T) { |
| 77 | if got := evalNum(t, "MIN(4,2,9,1)", emptyEnv{}); got != 1 { |
| 78 | t.Errorf("MIN = %v, want 1", got) |
| 79 | } |
| 80 | if got := evalNum(t, "MAX(4,2,9,1)", emptyEnv{}); got != 9 { |
| 81 | t.Errorf("MAX = %v, want 9", got) |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func TestRoundHalfAwayFromZero(t *testing.T) { |
| 86 | if got := evalNum(t, "ROUND(2.5,0)", emptyEnv{}); got != 3 { |
| 87 | t.Errorf("ROUND(2.5,0) = %v, want 3", got) |
| 88 | } |
| 89 | if got := evalNum(t, "ROUND(-2.5,0)", emptyEnv{}); got != -3 { |
| 90 | t.Errorf("ROUND(-2.5,0) = %v, want -3", got) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestRoundUpDown(t *testing.T) { |
| 95 | if got := evalNum(t, "ROUNDUP(3.001,0)", emptyEnv{}); got != 4 { |
| 96 | t.Errorf("ROUNDUP(3.001,0) = %v, want 4", got) |
| 97 | } |
| 98 | if got := evalNum(t, "ROUNDDOWN(3.99,0)", emptyEnv{}); got != 3 { |
| 99 | t.Errorf("ROUNDDOWN(3.99,0) = %v, want 3", got) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | func TestAbsSqrtPower(t *testing.T) { |
| 104 | if got := evalNum(t, "ABS(-7)", emptyEnv{}); got != 7 { |
| 105 | t.Errorf("ABS(-7) = %v, want 7", got) |
| 106 | } |
| 107 | if got := evalNum(t, "SQRT(16)", emptyEnv{}); got != 4 { |
| 108 | t.Errorf("SQRT(16) = %v, want 4", got) |
| 109 | } |
| 110 | if got := evalNum(t, "POWER(2,10)", emptyEnv{}); got != 1024 { |
| 111 | t.Errorf("POWER(2,10) = %v, want 1024", got) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func TestSqrtOfNegativeIsNum(t *testing.T) { |
| 116 | v := Eval(mustParse(t, "SQRT(-1)"), emptyEnv{}) |
| 117 | if v.Kind != KindErr || v.Err != ErrNum { |
| 118 | t.Fatalf("SQRT(-1) = %v, want #NUM!", v) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // MOD takes the sign of the divisor, matching Excel — not Go's %. |
| 123 | func TestModSignFollowsDivisor(t *testing.T) { |
| 124 | if got := evalNum(t, "MOD(-10,3)", emptyEnv{}); got != 2 { |
| 125 | t.Errorf("MOD(-10,3) = %v, want 2", got) |
| 126 | } |
| 127 | if got := evalNum(t, "MOD(10,-3)", emptyEnv{}); got != -2 { |
| 128 | t.Errorf("MOD(10,-3) = %v, want -2", got) |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | func TestModByZeroIsDiv0(t *testing.T) { |
| 133 | v := Eval(mustParse(t, "MOD(10,0)"), emptyEnv{}) |
| 134 | if v.Kind != KindErr || v.Err != ErrDiv0 { |
| 135 | t.Fatalf("MOD(10,0) = %v, want #DIV/0!", v) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func TestMathWrongArgCountIsValueError(t *testing.T) { |
| 140 | for _, formula := range []string{"ROUND(1)", "ABS()", "ABS(1,2)", "POWER(1)", "MOD(1)"} { |
| 141 | v := Eval(mustParse(t, formula), emptyEnv{}) |
| 142 | if v.Kind != KindErr || v.Err != ErrValue { |
| 143 | t.Errorf("%s = %v, want #VALUE!", formula, v) |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | // POWER guards its result, not just its inputs: a negative base with a |
| 149 | // non-integer exponent makes math.Pow return NaN even though -8 and |
| 150 | // 1.0/3 are each individually fine — this is a cube root written as a |
| 151 | // fractional power, and Excel returns #NUM!, not a NaN masquerading as |
| 152 | // a number. |
| 153 | func TestPowerNaNResultIsNum(t *testing.T) { |
| 154 | v := Eval(mustParse(t, "POWER(-8,1/3)"), emptyEnv{}) |
| 155 | if v.Kind != KindErr || v.Err != ErrNum { |
| 156 | t.Fatalf("POWER(-8,1/3) = %v, want #NUM!", v) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // An overflowing result is also #NUM!, not +Inf. |
| 161 | func TestPowerOverflowIsNum(t *testing.T) { |
| 162 | v := Eval(mustParse(t, "POWER(10,400)"), emptyEnv{}) |
| 163 | if v.Kind != KindErr || v.Err != ErrNum { |
| 164 | t.Fatalf("POWER(10,400) = %v, want #NUM!", v) |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // The literal-vs-reference distinction (taggedValue, fn_math.go): |
| 169 | // SUM/AVERAGE/COUNT/MIN/MAX all treat a boolean or a numeric-looking |
| 170 | // string typed DIRECTLY as an argument differently from the same kind of |
| 171 | // value read FROM a cell reference. These tests pin that distinction |
| 172 | // directly, closing the gap fn_math.go's first version flagged but did |
| 173 | // not fix (caught by code review against a real-Excel-behavior corpus |
| 174 | // entry for COUNT). |
| 175 | |
| 176 | func TestSumLiteralBooleanCoerces(t *testing.T) { |
| 177 | if got := evalNum(t, "SUM(TRUE,1)", emptyEnv{}); got != 2 { |
| 178 | t.Errorf("SUM(TRUE,1) = %v, want 2 (literal TRUE counts as 1)", got) |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | func TestSumReferenceBooleanIgnored(t *testing.T) { |
| 183 | env := mapEnv{{1, 1}: {Kind: KindBool, Bool: true}} |
| 184 | if got := evalNum(t, "SUM(A1,1)", env); got != 1 { |
| 185 | t.Errorf("SUM(A1,1) with A1=TRUE = %v, want 1 (reference TRUE ignored)", got) |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | func TestSumLiteralNonNumericStringIsValueError(t *testing.T) { |
| 190 | v := Eval(mustParse(t, `SUM(1,"abc")`), emptyEnv{}) |
| 191 | if v.Kind != KindErr || v.Err != ErrValue { |
| 192 | t.Fatalf(`SUM(1,"abc") = %v, want #VALUE! (a literal you meant as a number, not silently skipped)`, v) |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | func TestMinMaxLiteralBooleanCoerces(t *testing.T) { |
| 197 | if got := evalNum(t, "MIN(TRUE,5)", emptyEnv{}); got != 1 { |
| 198 | t.Errorf("MIN(TRUE,5) = %v, want 1", got) |
| 199 | } |
| 200 | if got := evalNum(t, "MAX(FALSE,5)", emptyEnv{}); got != 5 { |
| 201 | t.Errorf("MAX(FALSE,5) = %v, want 5", got) |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | // The corpus row this exact case pins (testdata/corpus/formulas.json): |
| 206 | // a logical value typed directly IS counted, but "a" isn't numeric so |
| 207 | // it isn't — 3, not 4 and not 2. |
| 208 | func TestCountLiteralBooleanAndNumericString(t *testing.T) { |
| 209 | if got := evalNum(t, `COUNT(1,2,"a",TRUE)`, emptyEnv{}); got != 3 { |
| 210 | t.Errorf(`COUNT(1,2,"a",TRUE) = %v, want 3`, got) |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | func TestCountReferenceBooleanNotCounted(t *testing.T) { |
| 215 | env := mapEnv{{1, 1}: {Kind: KindBool, Bool: true}} |
| 216 | if got := evalNum(t, "COUNT(A1)", env); got != 0 { |
| 217 | t.Errorf("COUNT(A1) with A1=TRUE = %v, want 0 (reference logical not counted)", got) |
| 218 | } |
| 219 | } |
| 220 | |