oficina / sheets Public

fix: negative zero leaked a "-" through Display

val < 0 never clears the sign bit on -0.0 (IEEE 754: -0.0 < 0 is
false), so strconv.FormatFloat printed "-0" and normalizeInt passed
the leading "-" straight through as an ordinary literal character.
Switch to math.Abs(num), which does clear it. Pinned with
TestDisplayNegativeZero: -0.0 under "0.00" -> "0.00", under General
-> "0". Reachable in practice from a formula like =0*-1.

Found in code review (fix round 1).
Paul Campbell 4178d5642ee77562871aa61bc76a79ff85dd2e98 parent 6df85e5
2 files changed, +23 −4
  • internal/sheets/format/format.go +6 −4
  • internal/sheets/format/rounding_test.go +17 −0
diff --git a/internal/sheets/format/format.go b/internal/sheets/format/format.go
index aa30868..6e2e544 100644
--- a/internal/sheets/format/format.go
+++ b/internal/sheets/format/format.go
@@ -47,6 +47,7 @@
package format
import (
+ "math"
"strings"
"amadan.net/oficina/sheets/internal/sheets/calc"
@@ -104,10 +105,11 @@ func displayNumber(num float64, numFmt string) string {
}
sections := splitSections(code)
sec, negate := chooseNumberSection(sections, num)
- val := num
- if val < 0 {
- val = -val
- }
+ // math.Abs, not "if val < 0 { val = -val }": that guard leaves
+ // negative zero (-0.0 < 0 is false under IEEE 754) unclipped, and
+ // strconv.FormatFloat prints it as "-0" — a leaked sign on a cell
+ // that should read plain "0.00". math.Abs clears the sign bit.
+ val := math.Abs(num)
out, ok := renderSection(sec, val)
if !ok {
out, _ = renderSection("General", val)
diff --git a/internal/sheets/format/rounding_test.go b/internal/sheets/format/rounding_test.go
index 995f22b..df4d4aa 100644
--- a/internal/sheets/format/rounding_test.go
+++ b/internal/sheets/format/rounding_test.go
@@ -1,6 +1,7 @@
package format
import (
+ "math"
"testing"
"time"
@@ -28,6 +29,22 @@ func TestDisplayRoundsHalfAwayFromZero(t *testing.T) {
}
}
+// Negative zero (-0.0 < 0 is false under IEEE 754) must not leak a "-"
+// through to the display string. Reachable in practice from a formula
+// like =0*-1, which calc does not normalise away.
+func TestDisplayNegativeZero(t *testing.T) {
+ negZero := math.Copysign(0, -1)
+ if !math.Signbit(negZero) {
+ t.Fatal("test setup: math.Copysign(0, -1) did not produce a signed zero")
+ }
+ if got := Display(numValue(negZero), "0.00"); got != "0.00" {
+ t.Errorf("Display(-0.0, %q) = %q, want %q", "0.00", got, "0.00")
+ }
+ if got := Display(numValue(negZero), "General"); got != "0" {
+ t.Errorf("Display(-0.0, %q) = %q, want %q", "General", got, "0")
+ }
+}
+
// Every ErrKind must display as its Excel text under any number format,
// not just #DIV/0! under one format — the single case the brief pins.
func TestDisplayEveryErrorIgnoresNumberFormat(t *testing.T) {