diff --git a/.deepsource.toml b/.deepsource.toml index 22454d8..9944f86 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -5,4 +5,4 @@ name = "go" enabled = true [analyzers.meta] - import_paths = ["github.com/robaho/fixed"] + import_paths = ["github.com/PKartaviy/fixed"] diff --git a/_examples/decimal.go b/_examples/decimal.go index 1ff52d1..075ea1c 100644 --- a/_examples/decimal.go +++ b/_examples/decimal.go @@ -2,7 +2,8 @@ package main import ( "fmt" - "github.com/robaho/fixed" + + "github.com/PKartaviy/fixed" ) func main() { diff --git a/comprehensive_test.go b/comprehensive_test.go new file mode 100644 index 0000000..6eef564 --- /dev/null +++ b/comprehensive_test.go @@ -0,0 +1,579 @@ +package fixed_test + +import ( + "math" + "testing" + + . "github.com/PKartaviy/fixed" +) + +// TestSignConsistency ensures hi and lo always have consistent signs +func TestSignConsistency(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"positive", "123.456"}, + {"negative", "-123.456"}, + {"negative fraction", "-0.456"}, + {"positive fraction", "0.456"}, + {"negative small", "-0.000000000000000001"}, + {"positive small", "0.000000000000000001"}, + {"negative large", "-999999999999999999"}, + {"positive large", "999999999999999999"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewS(tt.input) + if f.IsNaN() { + t.Fatalf("failed to parse %s", tt.input) + } + + // Verify round-trip + if f.String() != tt.input { + t.Errorf("round-trip failed: got %s, want %s", f.String(), tt.input) + } + + // Verify sign consistency by checking operations + doubled := f.Add(f) + mulByTwo := f.Mul(NewI(2, 0)) + + // Both should be valid or both should be NaN (overflow) + if doubled.IsNaN() != mulByTwo.IsNaN() { + t.Errorf("sign inconsistency detected: add vs mul differ (NaN mismatch)") + } else if !doubled.IsNaN() && !doubled.Equal(mulByTwo) { + t.Errorf("sign inconsistency detected: add vs mul differ (values: %s vs %s)", doubled.String(), mulByTwo.String()) + } + }) + } +} + +// TestNegativeZeroHandling tests the special case of -0.xxx +func TestNegativeZeroHandling(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"-0.5", "-0.5"}, + {"-0.000000000000000001", "-0.000000000000000001"}, + {"-0.999999999999999999", "-0.999999999999999999"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + f := NewS(tt.input) + if f.String() != tt.expected { + t.Errorf("got %s, want %s", f.String(), tt.expected) + } + + // Verify sign + if f.Sign() != -1 { + t.Errorf("sign should be -1, got %d", f.Sign()) + } + + // Verify operations preserve sign + doubled := f.Add(f) + if doubled.Sign() != -1 { + t.Errorf("doubling negative should stay negative") + } + }) + } +} + +// TestArithmeticOverflow tests operations that exceed the valid range +func TestArithmeticOverflow(t *testing.T) { + maxVal := NewS("999999999999999999.999999999999999999") + one := NewI(1, 0) + + // Addition overflow + result := maxVal.Add(one) + if !result.IsNaN() { + t.Error("addition overflow should produce NaN") + } + + // Subtraction underflow + minVal := NewS("-999999999999999999.999999999999999999") + result = minVal.Sub(one) + if !result.IsNaN() { + t.Error("subtraction underflow should produce NaN") + } + + // Multiplication overflow + large := NewS("1000000000000.0") + result = large.Mul(large) + if !result.IsNaN() { + t.Error("multiplication overflow should produce NaN") + } +} + +// TestMultiplicationAccuracy validates multiplication precision +func TestMultiplicationAccuracy(t *testing.T) { + tests := []struct { + a, b, expected string + }{ + {"123.456", "789.012", "97408.265472"}, + {"0.000000000000000001", "999999999999999999", "0.999999999999999999"}, + {"999999999999999999", "0.000000000000000001", "0.999999999999999999"}, + {"-123.456", "789.012", "-97408.265472"}, + {"-123.456", "-789.012", "97408.265472"}, + {"0.333333333333333333", "3", "0.999999999999999999"}, + } + + for _, tt := range tests { + t.Run(tt.a+"*"+tt.b, func(t *testing.T) { + a := NewS(tt.a) + b := NewS(tt.b) + expected := NewS(tt.expected) + + result := a.Mul(b) + if !result.Equal(expected) { + t.Errorf("got %s, want %s", result.String(), expected.String()) + } + }) + } +} + +// TestDivisionAccuracy validates division precision +func TestDivisionAccuracy(t *testing.T) { + tests := []struct { + a, b, expected string + }{ + {"1", "3", "0.333333333333333333"}, + {"2", "3", "0.666666666666666667"}, + {"1", "7", "0.142857142857142857"}, + {"1", "9", "0.111111111111111111"}, + {"10", "3", "3.333333333333333333"}, + {"-1", "3", "-0.333333333333333333"}, + {"1", "-3", "-0.333333333333333333"}, + {"-1", "-3", "0.333333333333333333"}, + } + + for _, tt := range tests { + t.Run(tt.a+"/"+tt.b, func(t *testing.T) { + a := NewS(tt.a) + b := NewS(tt.b) + expected := NewS(tt.expected) + + result := a.Div(b) + if !result.Equal(expected) { + t.Errorf("got %s, want %s", result.String(), expected.String()) + } + }) + } +} + +// TestDivisionByZero validates NaN handling +func TestDivisionByZero(t *testing.T) { + one := NewI(1, 0) + zero := NewI(0, 0) + + result := one.Div(zero) + if !result.IsNaN() { + t.Error("division by zero should produce NaN") + } +} + +// TestNaNPropagation ensures NaN propagates through operations +func TestNaNPropagation(t *testing.T) { + nan := NaN + one := NewI(1, 0) + + tests := []struct { + name string + result Fixed + }{ + {"add", nan.Add(one)}, + {"sub", nan.Sub(one)}, + {"mul", nan.Mul(one)}, + {"div", nan.Div(one)}, + {"add reverse", one.Add(nan)}, + {"sub reverse", one.Sub(nan)}, + {"mul reverse", one.Mul(nan)}, + {"div reverse", one.Div(nan)}, + {"abs", nan.Abs()}, + {"round", nan.Round(2)}, + {"floor", nan.Floor(2)}, + {"ceil", nan.Ceil(2)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !tt.result.IsNaN() { + t.Error("NaN should propagate through operation") + } + }) + } +} + +// TestRoundingEdgeCases tests rounding at various decimal places +func TestRoundingEdgeCases(t *testing.T) { + tests := []struct { + input string + places int + expected string + }{ + {"1.5", 0, "2"}, + {"1.4", 0, "1"}, + {"-1.5", 0, "-2"}, + {"-1.4", 0, "-1"}, + {"1.555555555555555555", 1, "1.6"}, + {"1.444444444444444444", 1, "1.4"}, + {"123.456789012345678", 10, "123.4567890123"}, + {"123.456789012345678", 18, "123.456789012345678"}, + {"999.999999999999999", 0, "1000"}, + {"-999.999999999999999", 0, "-1000"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + f := NewS(tt.input) + result := f.Round(tt.places) + expected := NewS(tt.expected) + + if !result.Equal(expected) { + t.Errorf("Round(%d): got %s, want %s", tt.places, result.String(), expected.String()) + } + }) + } +} + +// TestFloorCeilEdgeCases tests floor and ceil operations +func TestFloorCeilEdgeCases(t *testing.T) { + tests := []struct { + input string + places int + expectedFloor string + expectedCeil string + }{ + {"1.9", 0, "1", "2"}, + {"1.1", 0, "1", "2"}, + {"-1.9", 0, "-2", "-1"}, + {"-1.1", 0, "-2", "-1"}, + {"0.1", 0, "0", "1"}, + {"-0.1", 0, "-1", "0"}, + {"123.456789012345678", 5, "123.45678", "123.45679"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + f := NewS(tt.input) + + floor := f.Floor(tt.places) + expectedFloor := NewS(tt.expectedFloor) + if !floor.Equal(expectedFloor) { + t.Errorf("Floor(%d): got %s, want %s", tt.places, floor.String(), expectedFloor.String()) + } + + ceil := f.Ceil(tt.places) + expectedCeil := NewS(tt.expectedCeil) + if !ceil.Equal(expectedCeil) { + t.Errorf("Ceil(%d): got %s, want %s", tt.places, ceil.String(), expectedCeil.String()) + } + }) + } +} + +// TestComparisonEdgeCases tests comparison operations +func TestComparisonEdgeCases(t *testing.T) { + tests := []struct { + a, b string + cmp int + }{ + {"0", "0", 0}, + {"1", "0", 1}, + {"0", "1", -1}, + {"-1", "0", -1}, + {"0", "-1", 1}, + {"0.000000000000000001", "0", 1}, + {"0", "0.000000000000000001", -1}, + {"-0.000000000000000001", "0", -1}, + {"0", "-0.000000000000000001", 1}, + {"999999999999999999.999999999999999999", "999999999999999999.999999999999999998", 1}, + {"-999999999999999999.999999999999999999", "-999999999999999999.999999999999999998", -1}, + } + + for _, tt := range tests { + t.Run(tt.a+" vs "+tt.b, func(t *testing.T) { + a := NewS(tt.a) + b := NewS(tt.b) + + cmp := a.Cmp(b) + if cmp != tt.cmp { + t.Errorf("Cmp: got %d, want %d", cmp, tt.cmp) + } + + // Test Equal consistency + if tt.cmp == 0 && !a.Equal(b) { + t.Error("Equal should return true when Cmp returns 0") + } + if tt.cmp != 0 && a.Equal(b) { + t.Error("Equal should return false when Cmp returns non-zero") + } + }) + } +} + +// TestBinarySerializationRoundTrip tests serialization edge cases +func TestBinarySerializationRoundTrip(t *testing.T) { + tests := []string{ + "0", + "1", + "-1", + "0.000000000000000001", + "-0.000000000000000001", + "999999999999999999.999999999999999999", + "-999999999999999999.999999999999999999", + "123.456789012345678", + "-123.456789012345678", + } + + for _, tt := range tests { + t.Run(tt, func(t *testing.T) { + original := NewS(tt) + + data, err := original.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary failed: %v", err) + } + + var restored Fixed + err = restored.UnmarshalBinary(data) + if err != nil { + t.Fatalf("UnmarshalBinary failed: %v", err) + } + + if !original.Equal(restored) { + t.Errorf("round-trip failed: got %s, want %s", restored.String(), original.String()) + } + }) + } + + // Test NaN serialization + t.Run("NaN", func(t *testing.T) { + data, err := NaN.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary failed: %v", err) + } + + var restored Fixed + err = restored.UnmarshalBinary(data) + if err != nil { + t.Fatalf("UnmarshalBinary failed: %v", err) + } + + if !restored.IsNaN() { + t.Error("NaN should serialize and deserialize as NaN") + } + }) +} + +// TestStringParsingEdgeCases tests string parsing edge cases +func TestStringParsingEdgeCases(t *testing.T) { + tests := []struct { + input string + shouldParse bool + expected string + }{ + {"0", true, "0"}, + {".5", true, "0.5"}, + {"-.5", true, "-0.5"}, + {"5.", true, "5"}, + {"-5.", true, "-5"}, + {"00123.45", true, "123.45"}, + {"-00123.45", true, "-123.45"}, + {"123.450000000000000000", true, "123.45"}, + {"+123", true, "123"}, + {"", false, ""}, + {"abc", false, ""}, + {"12.34.56", false, ""}, + {"--123", false, ""}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + f, err := NewSErr(tt.input) + + if tt.shouldParse { + if err != nil { + t.Errorf("should parse successfully, got error: %v", err) + } + if f.String() != tt.expected { + t.Errorf("got %s, want %s", f.String(), tt.expected) + } + } else { + if err == nil { + t.Error("should fail to parse, but succeeded") + } + } + }) + } +} + +// TestOperationCommutativity tests commutative properties +func TestOperationCommutativity(t *testing.T) { + tests := []struct { + a, b string + }{ + {"123.456", "789.012"}, + {"0.000000000000000001", "999999999999999999"}, + {"-123.456", "789.012"}, + {"-123.456", "-789.012"}, + } + + for _, tt := range tests { + t.Run(tt.a+","+tt.b, func(t *testing.T) { + a := NewS(tt.a) + b := NewS(tt.b) + + // Addition commutativity + if !a.Add(b).Equal(b.Add(a)) { + t.Error("addition is not commutative") + } + + // Multiplication commutativity + if !a.Mul(b).Equal(b.Mul(a)) { + t.Error("multiplication is not commutative") + } + }) + } +} + +// TestOperationAssociativity tests associative properties +func TestOperationAssociativity(t *testing.T) { + a := NewS("123.456") + b := NewS("789.012") + c := NewS("345.678") + + // Addition associativity: (a + b) + c = a + (b + c) + left := a.Add(b).Add(c) + right := a.Add(b.Add(c)) + if !left.Equal(right) { + t.Errorf("addition is not associative: %s != %s", left.String(), right.String()) + } + + // Multiplication associativity: (a * b) * c = a * (b * c) + left = a.Mul(b).Mul(c) + right = a.Mul(b.Mul(c)) + if !left.Equal(right) { + t.Errorf("multiplication is not associative: %s != %s", left.String(), right.String()) + } +} + +// TestOperationIdentities tests identity elements +func TestOperationIdentities(t *testing.T) { + values := []string{ + "123.456", + "-123.456", + "0.000000000000000001", + "999999999999999999", + } + + zero := NewI(0, 0) + one := NewI(1, 0) + + for _, v := range values { + t.Run(v, func(t *testing.T) { + f := NewS(v) + + // Addition identity: a + 0 = a + if !f.Add(zero).Equal(f) { + t.Error("addition identity failed") + } + + // Multiplication identity: a * 1 = a + if !f.Mul(one).Equal(f) { + t.Error("multiplication identity failed") + } + + // Division identity: a / 1 = a + if !f.Div(one).Equal(f) { + t.Error("division identity failed") + } + }) + } +} + +// TestFloatConversionPrecision tests float64 conversion limits +func TestFloatConversionPrecision(t *testing.T) { + tests := []float64{ + 0, + 1, + -1, + 0.5, + -0.5, + 123.456, + -123.456, + 1.0 / 3.0, + 2.0 / 3.0, + math.Pi, + math.E, + 1e15, // Near float64 precision limit + -1e15, + } + + for _, v := range tests { + t.Run(formatFloat(v), func(t *testing.T) { + f := NewF(v) + + if f.IsNaN() { + t.Error("valid float converted to NaN") + } + + // Convert back and check relative error + back := f.Float() + relError := math.Abs((back - v) / v) + + // Allow small relative error due to float64 precision + if v != 0 && relError > 1e-15 { + t.Errorf("float round-trip error too large: %.20f vs %.20f (rel error: %e)", back, v, relError) + } + }) + } +} + +// TestMaxValueHandling tests operations near maximum values +func TestMaxValueHandling(t *testing.T) { + // Test values just below max + almostMax := NewS("999999999999999999") + one := NewI(1, 0) + tiny := NewS("0.000000000000000001") + + // Should succeed + result := almostMax.Add(tiny) + if result.IsNaN() { + t.Error("adding tiny to almost-max should not overflow") + } + + // Should overflow + result = almostMax.Add(one) + if !result.IsNaN() { + t.Error("adding 1 to almost-max should overflow") + } +} + +// TestIntegerOperations tests operations on integer values +func TestIntegerOperations(t *testing.T) { + a := NewI(100, 0) + b := NewI(50, 0) + + if a.Add(b).String() != "150" { + t.Errorf("integer addition failed: got %s", a.Add(b).String()) + } + + if a.Sub(b).String() != "50" { + t.Errorf("integer subtraction failed: got %s", a.Sub(b).String()) + } + + if a.Mul(b).String() != "5000" { + t.Errorf("integer multiplication failed: got %s", a.Mul(b).String()) + } + + if a.Div(b).String() != "2" { + t.Errorf("integer division failed: got %s", a.Div(b).String()) + } +} + +func formatFloat(f float64) string { + return NewF(f).String() +} diff --git a/decomposer.go b/decomposer.go index 29875cf..73da232 100644 --- a/decomposer.go +++ b/decomposer.go @@ -4,9 +4,8 @@ package fixed import ( - "encoding/binary" "errors" - "fmt" + "math/big" ) // See https://godoc.org/github.com/golang-sql/decomposer for the decomposer.Decimal @@ -16,25 +15,32 @@ import ( // If the provided buf has sufficient capacity, buf may be returned as the coefficient with // the value set and length set as appropriate. func (f Fixed) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) { - if f.fp == nan { + if f.IsNaN() { form = 2 return } - if f.fp == 0 { + if f.hi == 0 && f.lo == 0 { return } - c := f.fp - if c < 0 { - negative = true - c = -c + + negative = f.hi < 0 || (f.hi == 0 && f.lo < 0) + + // Combine hi and lo into single coefficient + // coefficient = hi * scale + lo (absolute values) + absHi := f.hi + if absHi < 0 { + absHi = -absHi } - if cap(buf) >= 8 { - coefficient = buf[:8] - } else { - coefficient = make([]byte, 8) + absLo := f.lo + if absLo < 0 { + absLo = -absLo } - binary.BigEndian.PutUint64(coefficient, uint64(c)) - exponent = -nPlaces + + bigCoef := new(big.Int).Mul(big.NewInt(absHi), big.NewInt(scale)) + bigCoef.Add(bigCoef, big.NewInt(absLo)) + + coefficient = bigCoef.Bytes() // Big-endian encoding + exponent = -18 return } @@ -45,55 +51,50 @@ func (f *Fixed) Compose(form byte, negative bool, coefficient []byte, exponent i return errors.New("Fixed must not be nil") } switch form { + case 1, 2: // Infinite or NaN + f.hi = nanHi + f.lo = nanLo + return nil + case 0: // Finite + // Continue below default: return errors.New("invalid form") - case 0: - // Finite form, see below. - case 1: - // Infinite form, turn into NaN. - f.fp = nan - return nil - case 2: - f.fp = nan - return nil - } - // Finite form. - - var c uint64 - maxi := len(coefficient) - 1 - for i := range coefficient { - v := coefficient[maxi-i] - if i < 8 { - c |= uint64(v) << (uint(i) * 8) - } else if v != 0 { - return fmt.Errorf("coefficent too large") - } } - dividePower := int(exponent) + nPlaces - ct := dividePower - if ct < 0 { - ct = -ct - } - var power uint64 = 1 - for i := 0; i < ct; i++ { - power *= 10 - } - checkC := c - if dividePower < 0 { - c = c / power - if c*power != checkC { - return fmt.Errorf("unable to store decimal, greater then 7 decimals") + // Parse coefficient from bytes + bigCoef := new(big.Int).SetBytes(coefficient) + + // Adjust for exponent + dividePower := int(exponent) + 18 + if dividePower != 0 { + ct := dividePower + if ct < 0 { + ct = -ct } - } else if dividePower > 0 { - c = c * power - if c/power != checkC { - return fmt.Errorf("enable to store decimal, too large") + adjuster := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(ct)), nil) + if dividePower < 0 { + bigCoef.Div(bigCoef, adjuster) + } else { + bigCoef.Mul(bigCoef, adjuster) } } - f.fp = int64(c) + + // Split into hi and lo + bigScale := big.NewInt(scale) + hi := new(big.Int).Div(bigCoef, bigScale) + lo := new(big.Int).Mod(bigCoef, bigScale) + + if !hi.IsInt64() || !lo.IsInt64() { + return errTooLarge + } + + f.hi = hi.Int64() + f.lo = lo.Int64() + if negative { - f.fp = -f.fp + f.hi = -f.hi + f.lo = -f.lo } + return nil } diff --git a/decomposer_test.go b/decomposer_test.go index 6e81892..cb25413 100644 --- a/decomposer_test.go +++ b/decomposer_test.go @@ -57,7 +57,7 @@ func TestDecomposerCompose(t *testing.T) { {N: "PosExp-2", S: "-123456000", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: 3}, {N: "AllDec-1", S: "0.123456", Coef: []byte{0x01, 0xE2, 0x40}, Exp: -6}, {N: "AllDec-2", S: "-0.123456", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: -6}, - {N: "TooSmall-1", S: "-0.00123456", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: -8, Err: true}, + {N: "TooSmall-1", S: "-0.001234567", Neg: true, Coef: []byte{0x12, 0xD6, 0x87}, Exp: -9, Err: false}, {N: "LeadingZero-1", S: "123.456", Coef: []byte{0, 0, 0, 0, 0, 0, 0, 0x01, 0xE2, 0x40}, Exp: -3}, {N: "NaN-1", S: "NaN", Form: 2}, } diff --git a/fixed.go b/fixed.go index fc0c8cb..e6c44e1 100644 --- a/fixed.go +++ b/fixed.go @@ -8,31 +8,70 @@ import ( "fmt" "io" "math" + "math/big" "strconv" "strings" ) -// Fixed is a fixed precision 38.24 number (supports 11.7 digits). It supports NaN. +// Fixed is a fixed precision 18.18 number (18 integer digits, 18 decimal digits). It supports NaN. type Fixed struct { - fp int64 + hi int64 // Integer part: -999999999999999999 to 999999999999999999 + lo int64 // Decimal part scaled by 10^18, carries same sign as hi } // the following constants can be changed to configure a different number of decimal places - these are // the only required changes. only 18 significant digits are supported due to NaN -const nPlaces = 7 -const scale = int64(10 * 10 * 10 * 10 * 10 * 10 * 10) -const zeros = "0000000" -const MAX = float64(99999999999.9999999) +const nPlaces = 18 +const scale = int64(1000000000000000000) // 10^18 +const zeros = "000000000000000000" +const MAX = float64(999999999999999999.999999999999999999) +const maxHi = int64(999999999999999999) // Maximum valid hi value (18 digits) -const nan = int64(1<<63 - 1) +// NaN representation: both fields set to int64 max +const nanHi = int64(1<<63 - 1) +const nanLo = int64(1<<63 - 1) -var NaN = Fixed{fp: nan} -var ZERO = Fixed{fp: 0} +var NaN = Fixed{hi: nanHi, lo: nanLo} +var ZERO = Fixed{hi: 0, lo: 0} var errTooLarge = errors.New("significand too large") var errFormat = errors.New("invalid encoding") +var pow10table = [19]int64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 10000000000000, + 100000000000000, + 1000000000000000, + 10000000000000000, + 100000000000000000, + 1000000000000000000, +} + +var errPow10Overflow = errors.New("pow10: exponent out of int64 range") + +func ipow10(n int) (int64, error) { + if n < 0 { + return 0, errPow10Overflow + } + if n > 18 { + return pow10table[18], errPow10Overflow + } + return pow10table[n], nil +} + // NewS creates a new Fixed from a string, returning NaN if the string could not be parsed func NewS(s string) Fixed { f, _ := NewSErr(s) @@ -52,41 +91,47 @@ func NewSErr(s string) (Fixed, error) { return NaN, nil } period := strings.Index(s, ".") - var i int64 - var f int64 + var hi int64 + var lo int64 var sign int64 = 1 var err error if period == -1 { - i, err = strconv.ParseInt(s, 10, 64) + hi, err = strconv.ParseInt(s, 10, 64) if err != nil { return NaN, errors.New("cannot parse") } - if i < 0 { + if hi < 0 { sign = -1 - i = i * -1 + hi = hi * -1 } } else { - if len(s[:period]) > 0 { - i, err = strconv.ParseInt(s[:period], 10, 64) + intPart := s[:period] + if len(intPart) > 0 && intPart != "-" && intPart != "+" { + hi, err = strconv.ParseInt(intPart, 10, 64) if err != nil { return NaN, errors.New("cannot parse") } - if i < 0 || s[0] == '-' { + if hi < 0 { sign = -1 - i = i * -1 + hi = hi * -1 } } + // Check for sign prefix even if no integer digits + if len(intPart) > 0 && intPart[0] == '-' { + sign = -1 + } fs := s[period+1:] fs = fs + zeros[:max(0, nPlaces-len(fs))] - f, err = strconv.ParseInt(fs[0:nPlaces], 10, 64) + lo, err = strconv.ParseInt(fs[0:nPlaces], 10, 64) if err != nil { return NaN, errors.New("cannot parse") } } - if float64(i) > MAX { + // Check for overflow - hi must fit within 18 digits + if hi > maxHi { return NaN, errTooLarge } - return Fixed{fp: sign * (i*scale + f)}, nil + return Fixed{hi: sign * hi, lo: sign * lo}, nil } // Parse creates a new Fixed from a string, returning NaN, and error if the string could not be parsed. Same as NewSErr @@ -111,37 +156,148 @@ func max(a, b int) int { return b } -// NewF creates a Fixed from an float64, rounding at the 8th decimal place +// normalize ensures lo is within proper range and adjusts hi accordingly +func normalize(hi, lo int64) (int64, int64) { + if lo >= scale { + hi += lo / scale + lo = lo % scale + } else if lo <= -scale { + hi += lo / scale // lo/scale is negative + lo = lo % scale + } + + // Handle sign consistency: both parts should have same sign + // Special case: hi=0 with non-zero lo keeps lo's sign + if hi > 0 && lo < 0 { + hi-- + lo += scale + } else if hi < 0 && lo > 0 { + hi++ + lo -= scale + } + + return hi, lo +} + +// NewF creates a Fixed from a float64, zero-allocation. func NewF(f float64) Fixed { if math.IsNaN(f) { - return Fixed{fp: nan} + return NaN } if f >= MAX || f <= -MAX { return NaN } - round := .5 - if f < 0 { - round = -0.5 + if f == 0 { + return ZERO + } + + // Format into stack buffer (no heap allocation) + var buf [64]byte + b := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) + + // Parse bytes directly + i := 0 + neg := false + if b[0] == '-' { + neg = true + i++ + } + + // Scan integer digits + var hi int64 + for i < len(b) && b[i] != '.' { + hi = hi*10 + int64(b[i]-'0') + i++ + } + + // Scan fractional digits + var lo int64 + var nFrac int + if i < len(b) && b[i] == '.' { + i++ // skip '.' + for i < len(b) && nFrac < nPlaces { + lo = lo*10 + int64(b[i]-'0') + nFrac++ + i++ + } + } + + // Pad lo to nPlaces digits + for nFrac < nPlaces { + lo *= 10 + nFrac++ + } + + if hi > maxHi { + return NaN } - return Fixed{fp: int64(f*float64(scale) + round)} + if neg { + hi = -hi + lo = -lo + } + + return Fixed{hi: hi, lo: lo} +} + +// NewFSlow creates a Fixed from a float64 via string conversion (allocates). +func NewFSlow(f float64) Fixed { + if math.IsNaN(f) { + return NaN + } + if f >= MAX || f <= -MAX { + return NaN + } + + // Convert to string with 15 significant figures (float64's precision limit) + // then parse the string to avoid float64 precision artifacts + s := strconv.FormatFloat(f, 'f', -1, 64) + + // Parse and return - this handles the conversion cleanly + result, err := NewSErr(s) + if err != nil { + // Fallback: direct conversion + hi := int64(math.Trunc(f)) + frac := f - float64(hi) + lo := int64(frac * float64(scale)) + hi, lo = normalize(hi, lo) + return Fixed{hi: hi, lo: lo} + } + + return result } // NewI creates a Fixed for an integer, moving the decimal point n places to the left -// For example, NewI(123,1) becomes 12.3. If n > 7, the value is truncated +// For example, NewI(123,1) becomes 12.3. If n > 18, the value is truncated func NewI(i int64, n uint) Fixed { if n > nPlaces { - i = i / int64(math.Pow10(int(n-nPlaces))) + p, err := ipow10(int(n - nPlaces)) + if err != nil { + panic(err) + } + i = i / p n = nPlaces } - i = i * int64(math.Pow10(int(nPlaces-n))) + // Split i into integer and decimal portions based on n + divisor, _ := ipow10(int(n)) + hi := i / divisor + remainder := i % divisor + + // Scale decimal portion to 18 digits + p, _ := ipow10(int(nPlaces - n)) + lo := remainder * p + + // Check for overflow + if hi > maxHi || hi < -maxHi { + return NaN + } - return Fixed{fp: i} + return Fixed{hi: hi, lo: lo} } func (f Fixed) IsNaN() bool { - return f.fp == nan + return f.hi == nanHi && f.lo == nanLo } func (f Fixed) IsZero() bool { @@ -157,7 +313,13 @@ func (f Fixed) Sign() int { if f.IsNaN() { return 0 } - return f.Cmp(ZERO) + if f.hi < 0 || (f.hi == 0 && f.lo < 0) { + return -1 + } + if f.hi > 0 || (f.hi == 0 && f.lo > 0) { + return 1 + } + return 0 } // Float converts the Fixed to a float64 @@ -165,7 +327,7 @@ func (f Fixed) Float() float64 { if f.IsNaN() { return math.NaN() } - return float64(f.fp) / float64(scale) + return float64(f.hi) + float64(f.lo)/float64(scale) } // Add adds f0 to f producing a Fixed. If either operand is NaN, NaN is returned @@ -173,7 +335,16 @@ func (f Fixed) Add(f0 Fixed) Fixed { if f.IsNaN() || f0.IsNaN() { return NaN } - return Fixed{fp: f.fp + f0.fp} + hi := f.hi + f0.hi + lo := f.lo + f0.lo + hi, lo = normalize(hi, lo) + + // Check for overflow + if hi > maxHi || hi < -maxHi { + return NaN + } + + return Fixed{hi: hi, lo: lo} } // Sub subtracts f0 from f producing a Fixed. If either operand is NaN, NaN is returned @@ -181,7 +352,16 @@ func (f Fixed) Sub(f0 Fixed) Fixed { if f.IsNaN() || f0.IsNaN() { return NaN } - return Fixed{fp: f.fp - f0.fp} + hi := f.hi - f0.hi + lo := f.lo - f0.lo + hi, lo = normalize(hi, lo) + + // Check for overflow + if hi > maxHi || hi < -maxHi { + return NaN + } + + return Fixed{hi: hi, lo: lo} } // Abs returns the absolute value of f. If f is NaN, NaN is returned @@ -192,8 +372,8 @@ func (f Fixed) Abs() Fixed { if f.Sign() >= 0 { return f } - f0 := Fixed{fp: f.fp * -1} - return f0 + // Negate both parts + return Fixed{hi: -f.hi, lo: -f.lo} } func abs(i int64) int64 { @@ -203,38 +383,467 @@ func abs(i int64) int64 { return i * -1 } -// Mul multiplies f by f0 returning a Fixed. If either operand is NaN, NaN is returned +// half is 10^9, used as the base for schoolbook multiplication in Mul. +const half = int64(1000000000) + +// Mul multiplies f by f0 returning a Fixed. If either operand is NaN, NaN is returned. +// Zero-allocation implementation using base-10^9 schoolbook multiplication. func (f Fixed) Mul(f0 Fixed) Fixed { if f.IsNaN() || f0.IsNaN() { return NaN } - fp_a := f.fp / scale - fp_b := f.fp % scale + // Determine result sign, then work with absolute values + signA := f.Sign() + signB := f0.Sign() + if signA == 0 || signB == 0 { + return ZERO + } + negative := signA != signB + + aHi := f.hi + aLo := f.lo + if aHi < 0 { + aHi = -aHi + } + if aLo < 0 { + aLo = -aLo + } + + bHi := f0.hi + bLo := f0.lo + if bHi < 0 { + bHi = -bHi + } + if bLo < 0 { + bLo = -bLo + } + + // Decompose each operand into 4 digits in base half (10^9): + // value = (d[0]*half + d[1]) * scale + (d[2]*half + d[3]) + // where scale = half * half = 10^18 + var a [4]int64 + a[0] = aHi / half + a[1] = aHi % half + a[2] = aLo / half + a[3] = aLo % half + + var b [4]int64 + b[0] = bHi / half + b[1] = bHi % half + b[2] = bLo / half + b[3] = bLo % half + + // Convolution: p[k] = sum of a[i]*b[j] where i+j=k + // The full product has digits p[0]..p[6]. Dividing by scale (= half^2) + // shifts by 2 digit positions, so the result digits are p[1]..p[4]. + // p[0] must be zero (else overflow). p[5],p[6] are truncated. + // Each product a[i]*b[j] < (10^9)^2 = 10^18, at most 4 terms per p[k], + // so p[k] < 4*10^18 < math.MaxInt64. No overflow in int64. + var p [7]int64 + p[0] = a[0] * b[0] + p[1] = a[0]*b[1] + a[1]*b[0] + p[2] = a[0]*b[2] + a[1]*b[1] + a[2]*b[0] + p[3] = a[0]*b[3] + a[1]*b[2] + a[2]*b[1] + a[3]*b[0] + p[4] = a[1]*b[3] + a[2]*b[2] + a[3]*b[1] + p[5] = a[2]*b[3] + a[3]*b[2] + p[6] = a[3] * b[3] + + // Carry-propagate from p[6] up to p[0] + p[5] += p[6] / half + p[6] = p[6] % half + p[4] += p[5] / half + p[5] = p[5] % half + p[3] += p[4] / half + p[4] = p[4] % half + p[2] += p[3] / half + p[3] = p[3] % half + p[1] += p[2] / half + p[2] = p[2] % half + p[0] += p[1] / half + p[1] = p[1] % half + + // Overflow check: the top digit must be zero after carry + if p[0] != 0 { + return NaN + } + + // Reconstruct hi and lo from result digits [1..4] + // The product has 7 digits (p[0]..p[6]). Dividing by scale=half^2 shifts + // by 2 digit positions, so integer part = p[0]*B^4 + p[1]*B^3 + p[2]*B^2 + p[3]*B + p[4]. + // With p[0]=0: hi = p[1]*half + p[2], lo = p[3]*half + p[4]. + // Digits p[5], p[6] are truncated (division toward zero). + resHi := p[1]*half + p[2] + resLo := p[3]*half + p[4] + + // For negative results with a non-zero truncated remainder, adjust to match + // floor division (round toward -infinity) semantics used by MulSlow's big.Int.Div. + // Truncation gives |result|, floor division gives |result|+1 when remainder > 0. + if negative && (p[5] != 0 || p[6] != 0) { + resLo++ + if resLo >= scale { + resLo -= scale + resHi++ + } + } + + // Check if result exceeds valid range + if resHi > maxHi { + return NaN + } + + // Apply sign + if negative && (resHi != 0 || resLo != 0) { + resHi = -resHi + resLo = -resLo + } + + return Fixed{hi: resHi, lo: resLo} +} + +// MulSlow multiplies f by f0 returning a Fixed. If either operand is NaN, NaN is returned +// Uses math/big for all multiplications — correct but allocates. +func (f Fixed) MulSlow(f0 Fixed) Fixed { + if f.IsNaN() || f0.IsNaN() { + return NaN + } + + // Use math/big for all multiplications to avoid overflow + // Convert to single big integer (value * scale), multiply, then split back + bigScale := big.NewInt(scale) - fp0_a := f0.fp / scale - fp0_b := f0.fp % scale + // value1 = hi1 * scale + lo1 + value1 := new(big.Int).Mul(big.NewInt(f.hi), bigScale) + value1.Add(value1, big.NewInt(f.lo)) - var _sign = int64(f.Sign() * f0.Sign()) + // value2 = hi2 * scale + lo2 + value2 := new(big.Int).Mul(big.NewInt(f0.hi), bigScale) + value2.Add(value2, big.NewInt(f0.lo)) - var result int64 + // product = value1 * value2 / scale (to maintain scale) + product := new(big.Int).Mul(value1, value2) + product.Div(product, bigScale) - if fp0_a != 0 { - result = fp_a*fp0_a*scale + fp_b*fp0_a + // Split back into hi and lo + hi := new(big.Int).Div(product, bigScale) + lo := new(big.Int).Mod(product, bigScale) + + // Check overflow + if !hi.IsInt64() || !lo.IsInt64() { + return NaN } - if fp0_b != 0 { - result = result + (fp_a * fp0_b) + ((fp_b)*fp0_b+5*_sign*(scale/10))/scale + + // Normalize to ensure sign consistency + resHi, resLo := normalize(hi.Int64(), lo.Int64()) + + // Check if result exceeds valid range + if resHi > maxHi || resHi < -maxHi { + return NaN } - return Fixed{fp: result} + return Fixed{hi: resHi, lo: resLo} } -// Div divides f by f0 returning a Fixed. If either operand is NaN, NaN is returned +// Div divides f by f0 returning a Fixed. If either operand is NaN, NaN is returned. +// Zero-allocation implementation using base-10^9 long division (Knuth's Algorithm D). func (f Fixed) Div(f0 Fixed) Fixed { if f.IsNaN() || f0.IsNaN() { return NaN } - return NewF(f.Float() / f0.Float()) + if f0.hi == 0 && f0.lo == 0 { + return NaN // division by zero + } + + // Determine result sign, then work with absolute values + signA := f.Sign() + signB := f0.Sign() + if signA == 0 { + return ZERO + } + negative := signA != signB + + aHi := f.hi + aLo := f.lo + if aHi < 0 { + aHi = -aHi + } + if aLo < 0 { + aLo = -aLo + } + + bHi := f0.hi + bLo := f0.lo + if bHi < 0 { + bHi = -bHi + } + if bLo < 0 { + bLo = -bLo + } + + // Decompose dividend into base-B (B=half=10^9) digits. + // Dividend = (aHi*scale + aLo) * scale = aHi*B^4 + aLo*B^2 + // 7 digits: u[0]=0 (leading zero for Algorithm D), u[1..4] from value, u[5..6]=0 from ×scale + var u [7]int64 + u[1] = aHi / half + u[2] = aHi % half + u[3] = aLo / half + u[4] = aLo % half + + // Decompose divisor: bHi*scale + bLo = bHi*B^2 + bLo + // Up to 4 digits, strip leading zeros to get n digits + var vBuf [4]int64 + vBuf[0] = bHi / half + vBuf[1] = bHi % half + vBuf[2] = bLo / half + vBuf[3] = bLo % half + + vStart := 0 + for vStart < 3 && vBuf[vStart] == 0 { + vStart++ + } + n := 4 - vStart + + var v [4]int64 + for i := 0; i < n; i++ { + v[i] = vBuf[vStart+i] + } + + const m = 6 // dividend has m+1 = 7 digits (u[0]..u[6]) + + var q [7]int64 // quotient digits + + if n == 1 { + // Simple single-digit long division + rem := int64(0) + for i := 0; i <= m; i++ { + cur := rem*half + u[i] + q[i] = cur / v[0] + rem = cur % v[0] + } + + // Overflow check: first 3 quotient digits must be zero + if q[0] != 0 || q[1] != 0 || q[2] != 0 { + return NaN + } + + resHi := q[3]*half + q[4] + resLo := q[5]*half + q[6] + + // Round to nearest: if 2*rem >= divisor, round up (away from zero) + if 2*rem >= v[0] { + resLo++ + if resLo >= scale { + resLo -= scale + resHi++ + } + } + + if resHi > maxHi { + return NaN + } + + if negative && (resHi != 0 || resLo != 0) { + resHi = -resHi + resLo = -resLo + } + + return Fixed{hi: resHi, lo: resLo} + } + + // n >= 2: Knuth's Algorithm D + + // D1. Normalize: multiply u and v by d = B/(v[0]+1) so that v[0] >= B/2. + // All intermediate products fit in int64: digit*d < B*B = 10^18 < maxInt64. + d := half / (v[0] + 1) + + // Multiply u[0..m] by d (right to left, propagating carry) + carry := int64(0) + for i := m; i >= 0; i-- { + tmp := u[i]*d + carry + u[i] = tmp % half + carry = tmp / half + } + // carry == 0: u[0] was 0 and absorbs any carry from u[1..6] + + // Multiply v[0..n-1] by d + carry = 0 + for i := n - 1; i >= 0; i-- { + tmp := v[i]*d + carry + v[i] = tmp % half + carry = tmp / half + } + // After normalization: v[0] >= floor(B/2) + + // D2-D7. Main loop: compute quotient digits q[0..m-n] + for j := 0; j <= m-n; j++ { + // D3. Calculate trial quotient q̂ + qhat := (u[j]*half + u[j+1]) / v[0] + rhat := (u[j]*half + u[j+1]) % v[0] + + // Refine q̂ using second divisor digit + for qhat >= half || qhat*v[1] > rhat*half+u[j+2] { + qhat-- + rhat += v[0] + if rhat >= half { + break + } + } + + // D4. Multiply and subtract: u[j..j+n] -= qhat * v[0..n-1] + carry = 0 + borrow := int64(0) + for k := n - 1; k >= 0; k-- { + p := qhat*v[k] + carry + carry = p / half + pLo := p % half + + diff := u[j+1+k] - pLo - borrow + if diff < 0 { + diff += half + borrow = 1 + } else { + borrow = 0 + } + u[j+1+k] = diff + } + u[j] -= carry + borrow + + // D5. Set quotient digit + q[j] = qhat + + if u[j] < 0 { + // D6. Add back (rare: qhat was one too large) + q[j]-- + carry = 0 + for k := n - 1; k >= 0; k-- { + sum := u[j+1+k] + v[k] + carry + u[j+1+k] = sum % half + carry = sum / half + } + u[j] += carry + } + } + + // Quotient is in q[0..m-n], total qLen = m-n+1 = 7-n digits. + // For the result to fit in 4 base-B digits, leading digits must be zero. + qLen := m - n + 1 + for i := 0; i < qLen-4; i++ { + if q[i] != 0 { + return NaN + } + } + + // Extract 4-digit quotient (pad with leading zeros if qLen < 4) + var qd [4]int64 + for i := 0; i < 4; i++ { + idx := qLen - 4 + i + if idx >= 0 { + qd[i] = q[idx] + } + } + + resHi := qd[0]*half + qd[1] + resLo := qd[2]*half + qd[3] + + // Round to nearest: compare 2×remainder with divisor. + // Both are still multiplied by normalization factor d, so comparison is valid. + // Remainder is in u[m-n+1..m] (n digits), divisor is v[0..n-1] (n digits). + roundUp := false + carry2 := int64(0) + var rem2 [4]int64 + for i := n - 1; i >= 0; i-- { + tmp := u[m-n+1+i]*2 + carry2 + rem2[i] = tmp % half + carry2 = tmp / half + } + if carry2 > 0 { + roundUp = true + } else { + for i := 0; i < n; i++ { + if rem2[i] > v[i] { + roundUp = true + break + } else if rem2[i] < v[i] { + break + } + } + } + + if roundUp { + resLo++ + if resLo >= scale { + resLo -= scale + resHi++ + } + } + + if resHi > maxHi { + return NaN + } + + if negative && (resHi != 0 || resLo != 0) { + resHi = -resHi + resLo = -resLo + } + + return Fixed{hi: resHi, lo: resLo} +} + +// DivSlow divides f by f0 returning a Fixed. If either operand is NaN, NaN is returned +// Uses arbitrary precision math/big for accurate division +func (f Fixed) DivSlow(f0 Fixed) Fixed { + if f.IsNaN() || f0.IsNaN() { + return NaN + } + if f0.hi == 0 && f0.lo == 0 { + return NaN // division by zero + } + + // Convert to big.Int for full precision + dividend := new(big.Int).Mul(big.NewInt(f.hi), big.NewInt(scale)) + dividend.Add(dividend, big.NewInt(f.lo)) + + divisor := new(big.Int).Mul(big.NewInt(f0.hi), big.NewInt(scale)) + divisor.Add(divisor, big.NewInt(f0.lo)) + + // Scale dividend by 10^18 for proper decimal places + dividend.Mul(dividend, big.NewInt(scale)) + + // Perform division with rounding toward nearest + // Use QuoRem for truncated division (toward zero) instead of DivMod (Euclidean) + result := new(big.Int) + remainder := new(big.Int) + result.QuoRem(dividend, divisor, remainder) + + // Round to nearest: if abs(2*remainder) >= abs(divisor), round away from zero + absRemainder := new(big.Int).Abs(remainder) + absRemainder.Mul(absRemainder, big.NewInt(2)) + absDivisor := new(big.Int).Abs(divisor) + + if absRemainder.Cmp(absDivisor) >= 0 { + if result.Sign() >= 0 { + result.Add(result, big.NewInt(1)) + } else { + result.Sub(result, big.NewInt(1)) + } + } + + // Split result back into hi and lo using truncated division + // to avoid Euclidean division sign issues + bigScale := big.NewInt(scale) + hi := new(big.Int).Div(result, bigScale) + + // Calculate lo as: lo = result - hi*scale (preserves sign correctly) + lo := new(big.Int).Mul(hi, bigScale) + lo.Sub(result, lo) + + if !hi.IsInt64() || !lo.IsInt64() { + return NaN + } + + // Normalize to ensure sign consistency + resHi, resLo := normalize(hi.Int64(), lo.Int64()) + return Fixed{hi: resHi, lo: resLo} } func sign(fp int64) int64 { @@ -249,45 +858,75 @@ func (f Fixed) Round(n int) Fixed { if f.IsNaN() { return NaN } - - fraction := f.fp % scale - intpart := f.fp - fraction + if n >= 18 { + return f + } if n >= 0 { - f0 := fraction / int64(math.Pow10(nPlaces-n-1)) - digit := abs(f0 % 10) - f0 = (f0 / 10) - if digit >= 5 { - f0 += 1 * sign(f.fp) + // Rounding decimal part (lo) + divisor, _ := ipow10(18 - n) + remainder := f.lo % divisor + absRemainder := remainder + if absRemainder < 0 { + absRemainder = -absRemainder } - f0 = f0 * int64(math.Pow10(nPlaces-n)) - fp := intpart + f0 + newLo := (f.lo / divisor) * divisor - return Fixed{fp: fp} + // Half-up rounding + halfDivisor := divisor / 2 + if absRemainder >= halfDivisor { + if f.lo >= 0 { + newLo += divisor + } else { + newLo -= divisor + } + } + hi, lo := normalize(f.hi, newLo) + return Fixed{hi: hi, lo: lo} } else { - f0 := intpart / int64(math.Pow10(nPlaces-n-1)) - digit := abs(f0 % 10) - f0 = (f0 / 10) - if digit >= 5 { - f0 += 1 * sign(f.fp) + // Rounding integer part (hi), zero out lo + divisor, err := ipow10(-n) + if err != nil { + panic(err) } - f0 = f0 * int64(math.Pow10(nPlaces-n)) + remainder := f.hi % divisor + absRemainder := remainder + if absRemainder < 0 { + absRemainder = -absRemainder + } + + newHi := (f.hi / divisor) * divisor - return Fixed{fp: f0} + if absRemainder >= divisor/2 { + if f.hi >= 0 { + newHi += divisor + } else { + newHi -= divisor + } + } + + return Fixed{hi: newHi, lo: 0} } } // Ceil returns f rounded up to n decimal places func (f Fixed) Ceil(n int) Fixed { + if f.IsNaN() { + return NaN + } f0 := f.Round(n) if f0.Cmp(f) >= 0 { return f0 } adj := int64(1) if n < 0 { - adj = adj * int64(math.Pow10(-n)) + p, err := ipow10(-n) + if err != nil { + panic(err) + } + adj = adj * p n = 0 } return f0.Add(NewI(adj, uint(n))) @@ -295,13 +934,20 @@ func (f Fixed) Ceil(n int) Fixed { // Floor returns f rounded down to n decimal places func (f Fixed) Floor(n int) Fixed { + if f.IsNaN() { + return NaN + } f0 := f.Round(n) if f0.Cmp(f) <= 0 { return f0 } adj := int64(-1) if n < 0 { - adj = adj * int64(math.Pow10(-n)) + p, err := ipow10(-n) + if err != nil { + panic(err) + } + adj = adj * p n = 0 } return f0.Add(NewI(adj, uint(n))) @@ -349,13 +995,22 @@ func (f Fixed) Cmp(f0 Fixed) int { return -1 } - if f.fp == f0.fp { - return 0 + if f.hi < f0.hi { + return -1 } - if f.fp < f0.fp { + if f.hi > f0.hi { + return 1 + } + + // hi parts equal, compare lo parts + if f.lo < f0.lo { return -1 } - return 1 + if f.lo > f0.lo { + return 1 + } + + return 0 } // String converts a Fixed to a string, dropping trailing zeros @@ -389,43 +1044,71 @@ func (f Fixed) StringN(decimals int) string { } func (f Fixed) tostr() (string, int) { - fp := f.fp - if fp == 0 { + if f.hi == 0 && f.lo == 0 { return "0." + zeros, 1 } - if fp == nan { + if f.IsNaN() { return "NaN", -1 } - b := make([]byte, 24) - b = itoa(b, fp) + // Determine sign + negative := f.hi < 0 || (f.hi == 0 && f.lo < 0) - return string(b), len(b) - nPlaces - 1 -} + // Work with absolute values + absHi := f.hi + if absHi < 0 { + absHi = -absHi + } + absLo := f.lo + if absLo < 0 { + absLo = -absLo + } -func itoa(buf []byte, val int64) []byte { - neg := val < 0 - if neg { - val = val * -1 + // Build string right-to-left in a stack-allocated buffer. + // Max length: '-' (1) + 18 integer digits + '.' (1) + 18 fractional digits = 38 + var buf [40]byte + pos := len(buf) + + // Write lo digits right-to-left (always nPlaces digits, zero-padded) + for i := 0; i < nPlaces; i++ { + pos-- + buf[pos] = byte(absLo%10) + '0' + absLo /= 10 } - i := len(buf) - 1 - idec := i - nPlaces - for val >= 10 || i >= idec { - buf[i] = byte(val%10 + '0') - i-- - if i == idec { - buf[i] = '.' - i-- + // Decimal point + pos-- + buf[pos] = '.' + point := pos + + // Write hi digits right-to-left + if absHi == 0 { + pos-- + buf[pos] = '0' + } else { + for absHi > 0 { + pos-- + buf[pos] = byte(absHi%10) + '0' + absHi /= 10 } - val /= 10 } - buf[i] = byte(val + '0') - if neg { - i-- - buf[i] = '-' + + // Sign + if negative { + pos-- + buf[pos] = '-' } - return buf[i:] + + return string(buf[pos:]), point - pos +} + +func itoa(buf []byte, val int64) []byte { + // Note: This function is deprecated in favor of tostr() + // but kept for backward compatibility with MarshalJSON + // We'll use a Fixed value to leverage tostr() + f := Fixed{hi: val / scale, lo: val % scale} + s, _ := f.tostr() + return []byte(s) } // Int return the integer portion of the Fixed, or 0 if NaN @@ -433,7 +1116,7 @@ func (f Fixed) Int() int64 { if f.IsNaN() { return 0 } - return f.fp / scale + return f.hi } // Frac return the fractional portion of the Fixed, or NaN if NaN @@ -441,38 +1124,55 @@ func (f Fixed) Frac() float64 { if f.IsNaN() { return math.NaN() } - return float64(f.fp%scale) / float64(scale) + return float64(f.lo) / float64(scale) } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface func (f *Fixed) UnmarshalBinary(data []byte) error { - fp, n := binary.Varint(data) - if n < 0 { + hi, n := binary.Varint(data) + if n <= 0 { + return errFormat + } + + lo, m := binary.Varint(data[n:]) + if m <= 0 { return errFormat } - f.fp = fp + + f.hi = hi + f.lo = lo return nil } // MarshalBinary implements the encoding.BinaryMarshaler interface. func (f Fixed) MarshalBinary() (data []byte, err error) { - var buffer [binary.MaxVarintLen64]byte - n := binary.PutVarint(buffer[:], f.fp) + var buffer [2 * binary.MaxVarintLen64]byte + n := binary.PutVarint(buffer[:], f.hi) + n += binary.PutVarint(buffer[n:], f.lo) return buffer[:n], nil } // WriteTo write the Fixed to an io.Writer, returning the number of bytes written func (f Fixed) WriteTo(w io.ByteWriter) error { - return writeVarint(w, f.fp) + if err := writeVarint(w, f.hi); err != nil { + return err + } + return writeVarint(w, f.lo) } // ReadFrom reads a Fixed from an io.Reader func ReadFrom(r io.ByteReader) (Fixed, error) { - fp, err := binary.ReadVarint(r) + hi, err := binary.ReadVarint(r) if err != nil { return NaN, err } - return Fixed{fp: fp}, nil + + lo, err := binary.ReadVarint(r) + if err != nil { + return NaN, err + } + + return Fixed{hi: hi, lo: lo}, nil } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -499,6 +1199,5 @@ func (f Fixed) MarshalJSON() ([]byte, error) { if f.IsNaN() { return []byte("\"NaN\""), nil } - buffer := make([]byte, 24) - return itoa(buffer, f.fp), nil + return []byte(f.String()), nil } diff --git a/fixed_bench_test.go b/fixed_bench_test.go index 3f26e2c..f7fd4b0 100644 --- a/fixed_bench_test.go +++ b/fixed_bench_test.go @@ -52,6 +52,14 @@ func BenchmarkMulFixed(b *testing.B) { f0.Mul(f1) } } +func BenchmarkMulSlowFixed(b *testing.B) { + f0 := NewF(123456789.0) + f1 := NewF(1234.0) + + for i := 0; i < b.N; i++ { + f0.MulSlow(f1) + } +} func BenchmarkMulDecimal(b *testing.B) { f0 := decimal.NewFromFloat(123456789.0) f1 := decimal.NewFromFloat(1234.0) @@ -87,6 +95,14 @@ func BenchmarkDivFixed(b *testing.B) { f0.Div(f1) } } +func BenchmarkDivSlowFixed(b *testing.B) { + f0 := NewF(123456789.0) + f1 := NewF(1234.0) + + for i := 0; i < b.N; i++ { + f0.DivSlow(f1) + } +} func BenchmarkDivDecimal(b *testing.B) { f0 := decimal.NewFromFloat(123456789.0) f1 := decimal.NewFromFloat(1234.0) @@ -183,6 +199,25 @@ func BenchmarkStringBigFloat(b *testing.B) { } } +func BenchmarkMarshalJSONFixed(b *testing.B) { + f0 := NewF(123456789.12345) + + for i := 0; i < b.N; i++ { + f0.MarshalJSON() + } +} + +func BenchmarkNewFFixed(b *testing.B) { + for i := 0; i < b.N; i++ { + NewF(123456789.12345) + } +} +func BenchmarkNewFSlowFixed(b *testing.B) { + for i := 0; i < b.N; i++ { + NewFSlow(123456789.12345) + } +} + func BenchmarkWriteTo(b *testing.B) { f0 := NewF(123456789.0) diff --git a/fixed_test.go b/fixed_test.go index a34b95a..4b0bf02 100644 --- a/fixed_test.go +++ b/fixed_test.go @@ -4,9 +4,10 @@ import ( "bytes" "encoding/json" "math" + "math/rand" "testing" - . "github.com/robaho/fixed" + . "github.com/PKartaviy/fixed" ) func TestBasic(t *testing.T) { @@ -98,8 +99,8 @@ func TestNewI(t *testing.T) { t.Error("should be equal", f, "123") } f = NewI(123456789012, 9) - if f.String() != "123.456789" { - t.Error("should be equal", f, "123.456789") + if f.String() != "123.456789012" { + t.Error("should be equal", f, "123.456789012") } f = NewI(123456789012, 9) if f.StringN(7) != "123.4567890" { @@ -129,37 +130,37 @@ func TestSign(t *testing.T) { } func TestMaxValue(t *testing.T) { - f0 := NewS("12345678901") - if f0.String() != "12345678901" { - t.Error("should be equal", f0, "12345678901") + f0 := NewS("1234567890") + if f0.String() != "1234567890" { + t.Error("should be equal", f0, "1234567890") } f0 = NewS("123456789012") - if f0.String() != "NaN" { - t.Error("should be equal", f0, "NaN") + if f0.String() != "123456789012" { + t.Error("should be equal", f0, "123456789012") + } + f0 = NewS("-1234567890") + if f0.String() != "-1234567890" { + t.Error("should be equal", f0, "-1234567890") } f0 = NewS("-12345678901") if f0.String() != "-12345678901" { t.Error("should be equal", f0, "-12345678901") } - f0 = NewS("-123456789012") - if f0.String() != "NaN" { - t.Error("should be equal", f0, "NaN") - } - f0 = NewS("99999999999") - if f0.String() != "99999999999" { - t.Error("should be equal", f0, "99999999999") + f0 = NewS("9999999999") + if f0.String() != "9999999999" { + t.Error("should be equal", f0, "9999999999") } - f0 = NewS("9.9999999") - if f0.String() != "9.9999999" { - t.Error("should be equal", f0, "9.9999999") + f0 = NewS("9.99999999") + if f0.String() != "9.99999999" { + t.Error("should be equal", f0, "9.99999999") } - f0 = NewS("99999999999.9999999") - if f0.String() != "99999999999.9999999" { - t.Error("should be equal", f0, "99999999999.9999999") + f0 = NewS("9999999999.99999999") + if f0.String() != "9999999999.99999999" { + t.Error("should be equal", f0, "9999999999.99999999") } - f0 = NewS("99999999999.12345678901234567890") - if f0.String() != "99999999999.1234567" { - t.Error("should be equal", f0, "99999999999.1234567") + f0 = NewS("9999999999.12345678901234567890") + if f0.String() != "9999999999.123456789012345678" { + t.Error("should be equal", f0, "9999999999.123456789012345678") } } @@ -321,8 +322,8 @@ func TestMulDiv(t *testing.T) { f1 = NewS("3") f2 = f0.Div(f1) - if f2.String() != "0.6666667" { - t.Error("should be equal", f2.String(), "0.6666667") + if f2.String() != "0.666666666666666667" { + t.Error("should be equal", f2.String(), "0.666666666666666667") } f0 = NewS("1000") @@ -353,16 +354,16 @@ func TestMulDiv(t *testing.T) { f1 = NewS("0.066248") f2 = f0.Mul(f1) - if f2.String() != "0.0000001" { - t.Error("should be equal", f2.String(), "0.0000001") + if f2.String() != "0.000000066248" { + t.Error("should be equal", f2.String(), "0.000000066248") } f0 = NewS("-0.000001") f1 = NewS("0.066248") f2 = f0.Mul(f1) - if f2.String() != "-0.0000001" { - t.Error("should be equal", f2.String(), "-0.0000001") + if f2.String() != "-0.000000066248" { + t.Error("should be equal", f2.String(), "-0.000000066248") } } @@ -397,16 +398,16 @@ func TestOverflow(t *testing.T) { t.Error("should be equal", f0.String(), "1.1234567") } f0 = NewF(1.123456789123) - if f0.String() != "1.1234568" { - t.Error("should be equal", f0.String(), "1.1234568") + if f0.String() != "1.123456789123" { + t.Error("should be equal", f0.String(), "1.123456789123") } f0 = NewF(1.0 / 3.0) - if f0.String() != "0.3333333" { - t.Error("should be equal", f0.String(), "0.3333333") + if f0.String() != "0.3333333333333333" { + t.Error("should be equal", f0.String(), "0.3333333333333333") } f0 = NewF(2.0 / 3.0) - if f0.String() != "0.6666667" { - t.Error("should be equal", f0.String(), "0.6666667") + if f0.String() != "0.6666666666666666" { + t.Error("should be equal", f0.String(), "0.6666666666666666") } } @@ -719,3 +720,253 @@ func TestJSON_NaN(t *testing.T) { t.Error("did not decode NaN", j.F, f) } } + +func TestMulVsMulSlow(t *testing.T) { + type pair struct { + name string + a, b Fixed + } + + one := NewS("1") + negOne := NewS("-1") + zero := NewS("0") + + cases := []pair{ + {"pos*pos", NewS("123.456"), NewS("789.012")}, + {"pos*neg", NewS("123.456"), NewS("-789.012")}, + {"neg*neg", NewS("-123.456"), NewS("-789.012")}, + {"zero*pos", zero, NewS("123.456")}, + {"pos*zero", NewS("123.456"), zero}, + {"zero*neg", zero, NewS("-123.456")}, + {"neg*zero", NewS("-123.456"), zero}, + {"zero*zero", zero, zero}, + {"one*val", one, NewS("999.999")}, + {"negone*val", negOne, NewS("999.999")}, + {"frac*frac", NewS("0.000001"), NewS("0.066248")}, + {"neg_frac*frac", NewS("-0.000001"), NewS("0.066248")}, + {"large_int", NewS("10000.1"), NewS("10000")}, + {"near_max", NewS("999999999"), NewS("999999999")}, + {"small_fracs", NewS("0.000000000000000001"), NewS("1")}, + {"mixed1", NewS("123456789.123456789"), NewS("0.000000001")}, + {"mixed2", NewS("1.999999999999999999"), NewS("1.999999999999999999")}, + // Large hi values (a[0] > 0 in base-10^9 decomposition) + {"large_hi*frac", NewS("1000000001.5"), NewS("0.5")}, + {"large_hi*one", NewS("999999999999999999"), NewS("1")}, + {"large_hi*small", NewS("999999999999999999"), NewS("0.000000000000000001")}, + {"large_hi*neg_frac", NewS("999999999999999999"), NewS("-0.000000000000000001")}, + {"large_hi*large_frac", NewS("123456789012345678"), NewS("0.000000001")}, + {"neg_large*frac", NewS("-123456789012345678"), NewS("0.5")}, + // Overflow cases + {"overflow", NewS("999999999999999999"), NewS("999999999999999999")}, + {"near_overflow", NewS("999999999999999999.999999999999999999"), NewS("1.000000000000000001")}, + {"both_nan", NaN, NaN}, + {"nan_left", NaN, one}, + {"nan_right", one, NaN}, + } + + for _, tc := range cases { + fast := tc.a.Mul(tc.b) + slow := tc.a.MulSlow(tc.b) + if fast.IsNaN() && slow.IsNaN() { + continue + } + if !fast.Equal(slow) { + t.Errorf("%s: Mul(%s, %s) = %s, MulSlow = %s", + tc.name, tc.a, tc.b, fast, slow) + } + } + + // Random/fuzz loop with two ranges: + // 1) Small hi (products usually in range) — tests normal paths + // 2) Large hi with small multiplier — tests a[0]>0 digit decomposition + rng := rand.New(rand.NewSource(42)) + + compare := func(tag string, i int, a, b Fixed) { + fast := a.Mul(b) + slow := a.MulSlow(b) + if fast.IsNaN() && slow.IsNaN() { + return + } + if !fast.Equal(slow) { + t.Errorf("%s iter %d: Mul(%s, %s) = %s, MulSlow = %s", + tag, i, a, b, fast, slow) + } + } + + for i := 0; i < 10000; i++ { + // Small hi: [-10^9, 10^9] + aHi := rng.Int63n(2000000000) - 1000000000 + aLo := rng.Int63n(1000000000000000000) + bHi := rng.Int63n(2000000000) - 1000000000 + bLo := rng.Int63n(1000000000000000000) + if aHi < 0 { + aLo = -aLo + } + if bHi < 0 { + bLo = -bLo + } + a := NewI(aHi, 0).Add(NewI(aLo, 18)) + b := NewI(bHi, 0).Add(NewI(bLo, 18)) + compare("small", i, a, b) + } + + for i := 0; i < 10000; i++ { + // Large hi * small value — exercises a[0] > 0 without always overflowing + aHi := rng.Int63n(999999999999999999) + 1 // [1, maxHi] + aLo := rng.Int63n(1000000000000000000) + // Small multiplier: pure fraction in [0, 1) + bLo := rng.Int63n(1000000000000000000) + sign := int64(1) + if rng.Intn(2) == 0 { + sign = -1 + } + a := NewI(sign*aHi, 0).Add(NewI(sign*aLo, 18)) + b := NewI(bLo, 18) + compare("large", i, a, b) + } +} + +func TestDivVsDivSlow(t *testing.T) { + type pair struct { + name string + a, b Fixed + } + + one := NewS("1") + negOne := NewS("-1") + zero := NewS("0") + + cases := []pair{ + {"1/3", one, NewS("3")}, + {"2/3", NewS("2"), NewS("3")}, + {"1/7", one, NewS("7")}, + {"1/9", one, NewS("9")}, + {"10/3", NewS("10"), NewS("3")}, + {"-1/3", negOne, NewS("3")}, + {"1/-3", one, NewS("-3")}, + {"-1/-3", negOne, NewS("-3")}, + {"5/5", NewS("5"), NewS("5")}, + {"large/small", NewS("999999999999999999"), NewS("0.000000000000000001")}, + {"small/large", NewS("0.000000000000000001"), NewS("999999999999999999")}, + {"max/1", NewS("999999999999999999.999999999999999999"), one}, + {"max/-1", NewS("999999999999999999.999999999999999999"), negOne}, + {"frac/frac", NewS("0.123456789"), NewS("0.987654321")}, + {"-frac/frac", NewS("-0.123456789"), NewS("0.987654321")}, + {"large_int/large_int", NewS("123456789012345678"), NewS("987654321")}, + {"1/1", one, one}, + {"0/1", zero, one}, + {"0/-1", zero, negOne}, + {"div_by_zero", one, zero}, + {"mixed", NewS("123456789.123456789"), NewS("0.000000001")}, + {"near_max_result", NewS("999999999999999999"), NewS("1.000000000000000001")}, + {"both_nan", NaN, NaN}, + {"nan_left", NaN, one}, + {"nan_right", one, NaN}, + } + + for _, tc := range cases { + fast := tc.a.Div(tc.b) + slow := tc.a.DivSlow(tc.b) + if fast.IsNaN() && slow.IsNaN() { + continue + } + if !fast.Equal(slow) { + t.Errorf("%s: Div(%s, %s) = %s, DivSlow = %s", + tc.name, tc.a, tc.b, fast, slow) + } + } + + // Random/fuzz loop + rng := rand.New(rand.NewSource(42)) + + compare := func(tag string, i int, a, b Fixed) { + fast := a.Div(b) + slow := a.DivSlow(b) + if fast.IsNaN() && slow.IsNaN() { + return + } + if !fast.Equal(slow) { + t.Errorf("%s iter %d: Div(%s, %s) = %s, DivSlow = %s", + tag, i, a, b, fast, slow) + } + } + + for i := 0; i < 10000; i++ { + // Random values with moderate hi + aHi := rng.Int63n(2000000000) - 1000000000 + aLo := rng.Int63n(1000000000000000000) + bHi := rng.Int63n(2000000000) - 1000000000 + bLo := rng.Int63n(1000000000000000000) + if aHi < 0 { + aLo = -aLo + } + if bHi < 0 { + bLo = -bLo + } + a := NewI(aHi, 0).Add(NewI(aLo, 18)) + b := NewI(bHi, 0).Add(NewI(bLo, 18)) + // Skip division by zero + if b.Sign() == 0 { + continue + } + compare("small", i, a, b) + } + + for i := 0; i < 10000; i++ { + // Large dividend / small divisor — exercises quotient overflow paths + aHi := rng.Int63n(999999999999999999) + 1 + aLo := rng.Int63n(1000000000000000000) + // Divisor: small positive value to avoid trivial overflow + bHi := rng.Int63n(1000) + 1 + bLo := rng.Int63n(1000000000000000000) + sign := int64(1) + if rng.Intn(2) == 0 { + sign = -1 + } + a := NewI(sign*aHi, 0).Add(NewI(sign*aLo, 18)) + b := NewI(bHi, 0).Add(NewI(bLo, 18)) + compare("large", i, a, b) + } +} + +func TestNewFVsNewFSlow(t *testing.T) { + specific := []float64{ + 0, -0.0, 1, -1, 0.5, -0.5, + 123.456, -123.456, + 0.0001, -0.0001, + 1.0 / 3.0, 2.0 / 3.0, + math.Pi, math.E, + 1e15, -1e15, + 999999999999999999.0, + -999999999999999999.0, + 0.000000000000000001, + 42.0, + 0.1, 0.2, 0.3, + 1234567890.123456789, + } + + match := func(label string, v float64) { + fast := NewF(v) + slow := NewFSlow(v) + // NaN.Equal(NaN) is false, so compare IsNaN separately + if fast.IsNaN() && slow.IsNaN() { + return + } + if !fast.Equal(slow) { + t.Errorf("%s: NewF(%v) = %s, NewFSlow(%v) = %s", label, v, fast.String(), v, slow.String()) + } + } + + for _, v := range specific { + match("specific", v) + } + + rng := rand.New(rand.NewSource(42)) + for i := 0; i < 10000; i++ { + f := (rng.Float64()*2 - 1) * 1e18 + if f >= MAX || f <= -MAX { + continue + } + match("random", f) + } +} diff --git a/go.mod b/go.mod index 62feed5..2144a0d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ -module github.com/robaho/fixed +module github.com/PKartaviy/fixed go 1.21.5 require ( github.com/mattn/go-sqlite3 v1.14.22 - github.com/shopspring/decimal v1.4.0 + github.com/shopspring/decimal v1.3.1 ) diff --git a/go.sum b/go.sum index 7efc79a..3ac08ed 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/readme.md b/readme.md index 07716e5..33de659 100644 --- a/readme.md +++ b/readme.md @@ -4,8 +4,8 @@ A fixed place numeric library designed for performance. The C++ version is available [here](https://github.com/robaho/cpp_fixed). -All numbers have a fixed 7 decimal places (18 digits total), and the maximum permitted value is +- 99999999999, -or just under 100 billion. NaN is supported. +All numbers have a fixed 8 decimal places (18 digits total), and the maximum permitted value is +- 9999999999, +or just under 10 billion. NaN is supported. The library is safe for concurrent use. Fixed values are immutable. It has built-in support for binary and json marshalling. diff --git a/tostr_test.go b/tostr_test.go new file mode 100644 index 0000000..e3aaeac --- /dev/null +++ b/tostr_test.go @@ -0,0 +1,557 @@ +package fixed_test + +import ( + "encoding/json" + "math" + "strings" + "testing" + + . "github.com/PKartaviy/fixed" +) + +// TestTostrViaString thoroughly tests the tostr() implementation via String() +func TestTostrViaString(t *testing.T) { + tests := []struct { + name string + input string + expect string + }{ + // Zero + {"zero", "0", "0"}, + {"zero from 0.0", "0.0", "0"}, + {"zero from 0.000", "0.000", "0"}, + + // Simple positive integers + {"one", "1", "1"}, + {"nine", "9", "9"}, + {"ten", "10", "10"}, + {"hundred", "100", "100"}, + {"thousand", "1000", "1000"}, + {"million", "1000000", "1000000"}, + {"billion", "1000000000", "1000000000"}, + + // Simple negative integers + {"neg one", "-1", "-1"}, + {"neg nine", "-9", "-9"}, + {"neg ten", "-10", "-10"}, + {"neg hundred", "-100", "-100"}, + {"neg thousand", "-1000", "-1000"}, + {"neg million", "-1000000", "-1000000"}, + + // Simple decimals + {"0.1", "0.1", "0.1"}, + {"0.5", "0.5", "0.5"}, + {"0.9", "0.9", "0.9"}, + {"0.01", "0.01", "0.01"}, + {"0.001", "0.001", "0.001"}, + {"0.0001", "0.0001", "0.0001"}, + {"0.12345", "0.12345", "0.12345"}, + + // Negative decimals + {"-0.1", "-0.1", "-0.1"}, + {"-0.5", "-0.5", "-0.5"}, + {"-0.01", "-0.01", "-0.01"}, + {"-0.001", "-0.001", "-0.001"}, + {"-0.12345", "-0.12345", "-0.12345"}, + + // Mixed integer and decimal + {"1.1", "1.1", "1.1"}, + {"1.5", "1.5", "1.5"}, + {"12.34", "12.34", "12.34"}, + {"123.456", "123.456", "123.456"}, + {"1234.5678", "1234.5678", "1234.5678"}, + {"123456789.123456789", "123456789.123456789", "123456789.123456789"}, + {"-1.1", "-1.1", "-1.1"}, + {"-12.34", "-12.34", "-12.34"}, + {"-123.456", "-123.456", "-123.456"}, + + // Trailing zeros should be stripped by String() + {"trailing zeros", "1.10", "1.1"}, + {"trailing zeros 2", "1.100", "1.1"}, + {"trailing zeros 3", "100.00", "100"}, + {"trailing zeros 4", "0.10", "0.1"}, + {"trailing zeros 5", "0.010", "0.01"}, + + // Leading zeros in fractional part (critical for zero-padding) + {"leading zero frac", "1.01", "1.01"}, + {"leading zero frac 2", "1.001", "1.001"}, + {"leading zero frac 3", "1.0001", "1.0001"}, + {"leading zero frac 4", "1.00001", "1.00001"}, + {"leading zero frac 5", "1.000001", "1.000001"}, + {"leading zero frac 6", "1.0000001", "1.0000001"}, + {"leading zero frac 7", "1.00000001", "1.00000001"}, + {"leading zero frac 8", "1.000000001", "1.000000001"}, + {"leading zero frac 9", "1.0000000001", "1.0000000001"}, + {"leading zero frac 17", "1.00000000000000001", "1.00000000000000001"}, + + // Minimum fractional value (1 in the lowest decimal place) + {"min frac", "0.000000000000000001", "0.000000000000000001"}, + {"-min frac", "-0.000000000000000001", "-0.000000000000000001"}, + {"1 + min frac", "1.000000000000000001", "1.000000000000000001"}, + + // Maximum values (18 integer digits) + {"max hi", "999999999999999999", "999999999999999999"}, + {"-max hi", "-999999999999999999", "-999999999999999999"}, + + // Max fractional + {"max frac", "0.999999999999999999", "0.999999999999999999"}, + {"-max frac", "-0.999999999999999999", "-0.999999999999999999"}, + + // Max combined + {"max combined", "999999999999999999.999999999999999999", "999999999999999999.999999999999999999"}, + {"-max combined", "-999999999999999999.999999999999999999", "-999999999999999999.999999999999999999"}, + + // Powers of 10 + {"10^1", "10", "10"}, + {"10^2", "100", "100"}, + {"10^3", "1000", "1000"}, + {"10^6", "1000000", "1000000"}, + {"10^9", "1000000000", "1000000000"}, + {"10^12", "1000000000000", "1000000000000"}, + {"10^15", "1000000000000000", "1000000000000000"}, + {"10^17", "100000000000000000", "100000000000000000"}, + + // Single digits in various positions + {"hi=1 lo=0", "1", "1"}, + {"hi=0 lo=1", "0.000000000000000001", "0.000000000000000001"}, + {"hi=1 lo=1", "1.000000000000000001", "1.000000000000000001"}, + {"hi=9 lo=9", "9.000000000000000009", "9.000000000000000009"}, + + // Patterns that stress zero-padding + {"100000000000000000.1", "100000000000000000.1", "100000000000000000.1"}, + {"1.100000000000000000", "1.1", "1.1"}, + + // NaN + {"NaN", "NaN", "NaN"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewS(tt.input) + got := f.String() + if got != tt.expect { + t.Errorf("NewS(%q).String() = %q, want %q", tt.input, got, tt.expect) + } + }) + } +} + +// TestTostrViaStringN tests StringN() output for various decimal place counts +func TestTostrViaStringN(t *testing.T) { + tests := []struct { + name string + input string + decimals int + expect string + }{ + // Zero with various decimal places + {"zero N=0", "0", 0, "0"}, + {"zero N=1", "0", 1, "0.0"}, + {"zero N=2", "0", 2, "0.00"}, + {"zero N=5", "0", 5, "0.00000"}, + {"zero N=18", "0", 18, "0.000000000000000000"}, + + // Integer with decimal places + {"1 N=0", "1", 0, "1"}, + {"1 N=1", "1", 1, "1.0"}, + {"1 N=2", "1", 2, "1.00"}, + {"1 N=5", "1", 5, "1.00000"}, + + // Truncation (not rounding) + {"1.129 N=2", "1.129", 2, "1.12"}, + {"1.999 N=2", "1.999", 2, "1.99"}, + {"1.999 N=1", "1.999", 1, "1.9"}, + {"1.999 N=0", "1.999", 0, "1"}, + + // Exact decimal places + {"1.12 N=2", "1.12", 2, "1.12"}, + {"1.123 N=3", "1.123", 3, "1.123"}, + + // Padding with zeros + {"1.1 N=5", "1.1", 5, "1.10000"}, + {"1.12 N=5", "1.12", 5, "1.12000"}, + + // Negative values + {"-1.129 N=2", "-1.129", 2, "-1.12"}, + {"-1.1 N=5", "-1.1", 5, "-1.10000"}, + {"-1 N=2", "-1", 2, "-1.00"}, + + // Large values + {"large N=2", "999999999999999999.999", 2, "999999999999999999.99"}, + {"large N=0", "999999999999999999.999", 0, "999999999999999999"}, + {"large N=18", "999999999999999999.999999999999999999", 18, "999999999999999999.999999999999999999"}, + + // Min fractional + {"min frac N=18", "0.000000000000000001", 18, "0.000000000000000001"}, + {"min frac N=17", "0.000000000000000001", 17, "0.00000000000000000"}, + {"min frac N=1", "0.000000000000000001", 1, "0.0"}, + + // NaN + {"NaN N=2", "NaN", 2, "NaN"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewS(tt.input) + got := f.StringN(tt.decimals) + if got != tt.expect { + t.Errorf("NewS(%q).StringN(%d) = %q, want %q", tt.input, tt.decimals, got, tt.expect) + } + }) + } +} + +// TestTostrPointPosition verifies that the point position returned by tostr() +// is correct by checking that StringN works properly (it depends on point position) +func TestTostrPointPosition(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"zero", "0"}, + {"positive int", "123"}, + {"negative int", "-123"}, + {"positive decimal", "123.456"}, + {"negative decimal", "-123.456"}, + {"small decimal", "0.001"}, + {"negative small decimal", "-0.001"}, + {"large", "999999999999999999.999999999999999999"}, + {"negative large", "-999999999999999999.999999999999999999"}, + {"single digit", "1"}, + {"neg single digit", "-1"}, + {"min frac", "0.000000000000000001"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewS(tt.input) + // StringN(0) should give just the integer part — verifies point is correct + sn0 := f.StringN(0) + full := f.String() + + // The integer part from StringN(0) should be a prefix of String() output + // (possibly followed by decimal point and more digits) + if !strings.HasPrefix(full, sn0) && !strings.HasPrefix(full, sn0+".") { + // For integers, String() drops the decimal, so they should be equal + if full != sn0 { + t.Errorf("StringN(0)=%q is not a prefix of String()=%q", sn0, full) + } + } + + // StringN(18) should give all 18 decimal places + sn18 := f.StringN(18) + if f.String() != "NaN" && len(sn18) < len(sn0)+19 { // +1 for dot + 18 digits + t.Errorf("StringN(18)=%q too short (StringN(0)=%q)", sn18, sn0) + } + }) + } +} + +// TestTostrFromNewF tests String() output for values constructed via NewF (float64) +func TestTostrFromNewF(t *testing.T) { + tests := []struct { + name string + input float64 + expect string + }{ + {"zero", 0.0, "0"}, + {"one", 1.0, "1"}, + {"neg one", -1.0, "-1"}, + {"0.5", 0.5, "0.5"}, + {"-0.5", -0.5, "-0.5"}, + {"0.1", 0.1, "0.1"}, + {"0.01", 0.01, "0.01"}, + {"0.001", 0.001, "0.001"}, + {"0.0001", 0.0001, "0.0001"}, + {"123.456", 123.456, "123.456"}, + {"-123.456", -123.456, "-123.456"}, + {"NaN", math.NaN(), "NaN"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewF(tt.input) + got := f.String() + if got != tt.expect { + t.Errorf("NewF(%v).String() = %q, want %q", tt.input, got, tt.expect) + } + }) + } +} + +// TestTostrFromNewI tests String() for values constructed via NewI +func TestTostrFromNewI(t *testing.T) { + tests := []struct { + name string + val int64 + decimals uint + expect string + }{ + {"0,0", 0, 0, "0"}, + {"1,0", 1, 0, "1"}, + {"-1,0", -1, 0, "-1"}, + {"123,1", 123, 1, "12.3"}, + {"-123,1", -123, 1, "-12.3"}, + {"123,0", 123, 0, "123"}, + {"100,2", 100, 2, "1"}, + {"1,18", 1, 18, "0.000000000000000001"}, + {"-1,18", -1, 18, "-0.000000000000000001"}, + {"123456789012,9", 123456789012, 9, "123.456789012"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewI(tt.val, tt.decimals) + got := f.String() + if got != tt.expect { + t.Errorf("NewI(%d, %d).String() = %q, want %q", tt.val, tt.decimals, got, tt.expect) + } + }) + } +} + +// TestTostrMarshalJSON tests that MarshalJSON produces correct output +func TestTostrMarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expect string + }{ + {"zero", "0", "0"}, + {"positive", "123.456", "123.456"}, + {"negative", "-123.456", "-123.456"}, + {"integer", "42", "42"}, + {"small decimal", "0.000000000000000001", "0.000000000000000001"}, + {"max value", "999999999999999999.999999999999999999", "999999999999999999.999999999999999999"}, + {"NaN", "NaN", "\"NaN\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewS(tt.input) + got, err := f.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON error: %v", err) + } + if string(got) != tt.expect { + t.Errorf("NewS(%q).MarshalJSON() = %q, want %q", tt.input, string(got), tt.expect) + } + }) + } +} + +// TestTostrMarshalJSONRoundTrip tests JSON marshal/unmarshal round-trip +func TestTostrMarshalJSONRoundTrip(t *testing.T) { + type wrapper struct { + V Fixed `json:"v"` + } + + values := []string{ + "0", "1", "-1", + "0.1", "-0.1", + "0.000000000000000001", "-0.000000000000000001", + "123.456", "-123.456", + "999999999999999999", "-999999999999999999", + "999999999999999999.999999999999999999", + "NaN", + } + + for _, v := range values { + t.Run(v, func(t *testing.T) { + original := NewS(v) + w := wrapper{V: original} + + data, err := json.Marshal(w) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + + var w2 wrapper + err = json.Unmarshal(data, &w2) + if err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + + if original.IsNaN() { + if !w2.V.IsNaN() { + t.Errorf("expected NaN after round-trip, got %v", w2.V) + } + return + } + + if !w2.V.Equal(original) { + t.Errorf("round-trip mismatch: %q -> marshal -> unmarshal -> %q", original.String(), w2.V.String()) + } + }) + } +} + +// TestTostrAfterArithmetic tests String() output after arithmetic operations +// to ensure tostr() handles all resulting internal states correctly +func TestTostrAfterArithmetic(t *testing.T) { + t.Run("addition", func(t *testing.T) { + f := NewS("0.1").Add(NewS("0.2")) + got := f.String() + if got != "0.3" { + t.Errorf("0.1 + 0.2 = %q, want %q", got, "0.3") + } + }) + + t.Run("subtraction to zero", func(t *testing.T) { + f := NewS("1.5").Sub(NewS("1.5")) + got := f.String() + if got != "0" { + t.Errorf("1.5 - 1.5 = %q, want %q", got, "0") + } + }) + + t.Run("subtraction to negative", func(t *testing.T) { + f := NewS("1").Sub(NewS("2")) + got := f.String() + if got != "-1" { + t.Errorf("1 - 2 = %q, want %q", got, "-1") + } + }) + + t.Run("multiplication", func(t *testing.T) { + f := NewS("3").Mul(NewS("7")) + got := f.String() + if got != "21" { + t.Errorf("3 * 7 = %q, want %q", got, "21") + } + }) + + t.Run("division producing repeating decimal", func(t *testing.T) { + f := NewS("1").Div(NewS("3")) + got := f.String() + if got != "0.333333333333333333" { + t.Errorf("1 / 3 = %q, want %q", got, "0.333333333333333333") + } + }) + + t.Run("division producing 2/3", func(t *testing.T) { + f := NewS("2").Div(NewS("3")) + got := f.String() + if got != "0.666666666666666667" { + t.Errorf("2 / 3 = %q, want %q", got, "0.666666666666666667") + } + }) + + t.Run("large multiplication", func(t *testing.T) { + f := NewS("999999999").Mul(NewS("999999999")) + got := f.String() + if got != "999999998000000001" { + t.Errorf("999999999 * 999999999 = %q, want %q", got, "999999998000000001") + } + }) +} + +// TestTostrStringConsistency verifies String() and StringN(18) consistency +// (StringN(18) should be String() + trailing zeros if String() has fewer than 18 decimal places) +func TestTostrStringConsistency(t *testing.T) { + values := []string{ + "0", "1", "-1", + "0.1", "-0.1", + "123.456", "-123.456", + "0.000000000000000001", + "999999999999999999.999999999999999999", + "100", "1000000", + } + + for _, v := range values { + t.Run(v, func(t *testing.T) { + f := NewS(v) + s := f.String() + sn18 := f.StringN(18) + + if s == "NaN" { + return + } + + // Find the decimal point in both + dotS := strings.Index(s, ".") + dotSN := strings.Index(sn18, ".") + + if dotS == -1 { + // String() has no decimal point — it's a pure integer + // StringN(18) should be integer + "." + 18 zeros + expected := s + "." + strings.Repeat("0", 18) + if sn18 != expected { + t.Errorf("StringN(18)=%q, want %q (String()=%q)", sn18, expected, s) + } + } else { + // Both should share the same integer part + if s[:dotS] != sn18[:dotSN] { + t.Errorf("integer parts differ: String()=%q, StringN(18)=%q", s, sn18) + } + // StringN(18) fractional part should start with String()'s fractional part + sFrac := s[dotS+1:] + snFrac := sn18[dotSN+1:] + if !strings.HasPrefix(snFrac, sFrac) { + t.Errorf("fractional mismatch: String() frac=%q, StringN(18) frac=%q", sFrac, snFrac) + } + // Remaining chars in StringN(18) should all be zeros + remainder := snFrac[len(sFrac):] + if remainder != strings.Repeat("0", len(remainder)) { + t.Errorf("StringN(18) has non-zero padding: %q", remainder) + } + } + }) + } +} + +// TestTostrNewSRoundTrip verifies that NewS(f.String()) == f for various values +func TestTostrNewSRoundTrip(t *testing.T) { + values := []string{ + "0", + "1", "-1", + "0.1", "-0.1", + "0.5", "-0.5", + "0.000000000000000001", "-0.000000000000000001", + "123456789.123456789", "-123456789.123456789", + "999999999999999999", "-999999999999999999", + "999999999999999999.999999999999999999", "-999999999999999999.999999999999999999", + "100000000000000000", "100000000000000000.1", + "1.000000000000000001", + } + + for _, v := range values { + t.Run(v, func(t *testing.T) { + f1 := NewS(v) + s := f1.String() + f2 := NewS(s) + + if !f1.Equal(f2) { + t.Errorf("round-trip failed: %q -> String() -> %q -> NewS() -> %q", v, s, f2.String()) + } + }) + } +} + +// TestTostrStringLength ensures the output never exceeds expected bounds +func TestTostrStringLength(t *testing.T) { + values := []string{ + "0", + "1", "-1", + "999999999999999999.999999999999999999", + "-999999999999999999.999999999999999999", + "0.000000000000000001", + "-0.000000000000000001", + } + + for _, v := range values { + t.Run(v, func(t *testing.T) { + f := NewS(v) + s := f.String() + // Max: '-' (1) + 18 digits + '.' (1) + 18 digits = 38 + if len(s) > 38 { + t.Errorf("String() too long (%d chars): %q", len(s), s) + } + + sn18 := f.StringN(18) + if sn18 != "NaN" && len(sn18) > 38 { + t.Errorf("StringN(18) too long (%d chars): %q", len(sn18), sn18) + } + }) + } +}