From 679282c3b54e55393af257157093f502710c77e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D1=89=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=A1=D0=B0?= =?UTF-8?q?=D1=80=D0=B0?= Date: Thu, 6 Nov 2025 22:01:36 +0300 Subject: [PATCH] msgpack: add string() for decimal, datetime. interval Added a benchmark, which shows that the code for decimal is optimized two or more times for string conversion than the code from the library. Added a datetime, Interval type conversion function to a string, added tests for this function. Added #322 --- datetime/datetime.go | 5 + datetime/datetime_test.go | 17 ++ datetime/interval.go | 81 +++++++++ datetime/interval_test.go | 168 +++++++++++++++++ decimal/decimal.go | 171 +++++++++++++++++- decimal/decimal_bench_test.go | 330 ++++++++++++++++++++++++++++++++++ decimal/decimal_test.go | 216 ++++++++++++++++++++++ 7 files changed, 987 insertions(+), 1 deletion(-) create mode 100644 decimal/decimal_bench_test.go diff --git a/datetime/datetime.go b/datetime/datetime.go index 23901305..80faf3de 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -350,6 +350,11 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { return ptr.UnmarshalMsgpack(b) } +// This method converts Datetime to String - formats to ISO8601. +func (d Datetime) String() string { + return d.time.Format(time.RFC3339Nano) +} + func init() { msgpack.RegisterExtDecoder(datetimeExtID, Datetime{}, datetimeDecoder) msgpack.RegisterExtEncoder(datetimeExtID, Datetime{}, datetimeEncoder) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index d0115389..4ca00f13 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -1174,6 +1174,23 @@ func runTestMain(m *testing.M) int { return m.Run() } +func TestDatetimeString(t *testing.T) { + + tm, _ := time.Parse(time.RFC3339Nano, "2010-05-24T17:51:56.000000009Z") + dt, err := MakeDatetime(tm) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm, err) + } + + result := dt.String() + t.Logf("Result: %s", result) + + expected := "2010-05-24T17:51:56.000000009Z" + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + +} func TestMain(m *testing.M) { code := runTestMain(m) os.Exit(code) diff --git a/datetime/interval.go b/datetime/interval.go index e6d39e4d..58ccb22e 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -2,7 +2,9 @@ package datetime import ( "bytes" + "fmt" "reflect" + "strings" "github.com/vmihailenco/msgpack/v5" ) @@ -216,6 +218,85 @@ func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { return nil } +// Returns a human-readable string representation of the interval. +func (ival Interval) String() string { + if ival.countNonZeroFields() == 0 { + return "0 seconds" + } + + parts := make([]string, 0, 9) + + // Helper function for adding components. + addPart := func(value int64, singular, plural string) { + if value == 0 { + return + } + if value == 1 || value == -1 { + parts = append(parts, fmt.Sprintf("%d %s", value, singular)) + } else { + parts = append(parts, fmt.Sprintf("%d %s", value, plural)) + } + } + + addPart(ival.Year, "year", "years") + addPart(ival.Month, "month", "months") + addPart(ival.Week, "week", "weeks") + addPart(ival.Day, "day", "days") + addPart(ival.Hour, "hour", "hours") + addPart(ival.Min, "minute", "minutes") + + // Processing seconds and nanoseconds - combine if both are present. + if ival.Sec != 0 && ival.Nsec != 0 { + // Define a common symbol for proper formatting. + secSign := ival.Sec < 0 + nsecSign := ival.Nsec < 0 + + if secSign == nsecSign { + // Same signs - combine them. + absSec := ival.Sec + absNsec := ival.Nsec + if secSign { + absSec = -absSec + absNsec = -absNsec + } + parts = append(parts, fmt.Sprintf("%s%d.%09d seconds", + boolToSign(secSign), absSec, absNsec)) + } else { + // Different characters - output separately. + addPart(ival.Sec, "second", "seconds") + addPart(ival.Nsec, "nanosecond", "nanoseconds") + } + } else { + // Only seconds or only nanoseconds. + addPart(ival.Sec, "second", "seconds") + addPart(ival.Nsec, "nanosecond", "nanoseconds") + } + + return joinIntervalParts(parts) +} + +// Returns "-" for true and an empty string for false. +func boolToSign(negative bool) string { + if negative { + return "-" + } + return "" +} + +// Combines parts of an interval into a readable string. +func joinIntervalParts(parts []string) string { + switch len(parts) { + case 0: + return "0 seconds" + case 1: + return parts[0] + case 2: + return parts[0] + " and " + parts[1] + default: + return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1] + } +} + func init() { msgpack.RegisterExtEncoder(interval_extId, Interval{}, func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) { diff --git a/datetime/interval_test.go b/datetime/interval_test.go index 2f4bb8a6..3f727a08 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -133,3 +133,171 @@ func TestIntervalTarantoolEncoding(t *testing.T) { }) } } + +func TestIntervalString(t *testing.T) { + tests := []struct { + name string + interval Interval + expected string + }{ + { + name: "empty interval", + interval: Interval{}, + expected: "0 seconds", + }, + { + name: "single component - years", + interval: Interval{ + Year: 1, + }, + expected: "1 year", + }, + { + name: "multiple years", + interval: Interval{ + Year: 5, + }, + expected: "5 years", + }, + { + name: "multiple components", + interval: Interval{ + Year: 1, + Month: 2, + Day: 3, + }, + expected: "1 year, 2 months and 3 days", + }, + { + name: "time components", + interval: Interval{ + Hour: 1, + Min: 30, + Sec: 45, + }, + expected: "1 hour, 30 minutes and 45 seconds", + }, + { + name: "seconds with nanoseconds same sign", + interval: Interval{ + Sec: 5, + Nsec: 123456789, + }, + expected: "5.123456789 seconds", + }, + { + name: "negative seconds with nanoseconds", + interval: Interval{ + Sec: -5, + Nsec: -123456789, + }, + expected: "-5.123456789 seconds", + }, + { + name: "seconds and nanoseconds different signs", + interval: Interval{ + Sec: 5, + Nsec: -123456789, + }, + expected: "5 seconds and -123456789 nanoseconds", + }, + { + name: "only nanoseconds", + interval: Interval{ + Nsec: 500000000, + }, + expected: "500000000 nanoseconds", + }, + { + name: "weeks", + interval: Interval{ + Week: 2, + }, + expected: "2 weeks", + }, + { + name: "complex interval", + interval: Interval{ + Year: 1, + Month: 6, + Week: 2, + Day: 3, + Hour: 12, + Min: 30, + Sec: 45, + Nsec: 123456789, + }, + expected: "1 year, 6 months, 2 weeks, 3 days, 12 hours, 30 minutes and 45.123456789 seconds", + }, + { + name: "negative components", + interval: Interval{ + Year: -1, + Day: -2, + Hour: -3, + }, + expected: "-1 year, -2 days and -3 hours", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.interval.String() + if result != tt.expected { + t.Errorf("Interval.String() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestIntervalStringIntegration(t *testing.T) { + t.Run("implements Stringer", func(t *testing.T) { + var _ fmt.Stringer = Interval{} + }) + + t.Run("works with fmt package", func(t *testing.T) { + ival := Interval{Hour: 2, Min: 30} + result := fmt.Sprintf("%s", ival) + expected := "2 hours and 30 minutes" + if result != expected { + t.Errorf("fmt.Sprintf('%%s') = %v, want %v", result, expected) + } + + result = fmt.Sprintf("%v", ival) + if result != expected { + t.Errorf("fmt.Sprintf('%%v') = %v, want %v", result, expected) + } + }) +} + +func TestIntervalStringEdgeCases(t *testing.T) { + tests := []struct { + name string + interval Interval + }{ + { + name: "max values", + interval: Interval{Year: 1<<63 - 1, Month: 1<<63 - 1}, + }, + { + name: "min values", + interval: Interval{Year: -1 << 63, Month: -1 << 63}, + }, + { + name: "mixed signs complex", + interval: Interval{Year: 1, Month: -1, Day: 1, Hour: -1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.interval.String() + if result == "" { + t.Error("Interval.String() returned empty string") + } + if len(result) > 1000 { // Разумный лимит + t.Error("Interval.String() returned unexpectedly long string") + } + }) + } +} diff --git a/decimal/decimal.go b/decimal/decimal.go index 3c268123..b471f016 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -21,7 +21,10 @@ package decimal import ( "fmt" + "math" "reflect" + "strconv" + "strings" "github.com/shopspring/decimal" "github.com/vmihailenco/msgpack/v5" @@ -96,7 +99,7 @@ func (d Decimal) MarshalMsgpack() ([]byte, error) { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ - strBuf := d.String() + strBuf := d.Decimal.String() bcdBuf, err := encodeStringToBCD(strBuf) if err != nil { return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) @@ -144,6 +147,172 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { return ptr.UnmarshalMsgpack(b) } +// This method converts the decimal type to a string. +// Use shopspring/decimal by default. +// String - optimized version for Tarantool Decimal +// taking into account the limitations of int64 and support for large numbers via fallback +// Tarantool decimal has 38 digits, which can exceed int64. +// Therefore, we cannot use int64 for all cases. +// For the general case, use shopspring/decimal.String(). +// For cases where it is known that numbers contain less than 26 characters, +// you can use the optimized version. +func (d Decimal) String() string { + coefficient := d.Decimal.Coefficient() // Note: In shopspring/decimal, the number is stored as coefficient *10^exponent, where exponent can be negative. + exponent := d.Decimal.Exponent() + + // If exponent is positive, then we use the standard method. + if exponent > 0 { + return d.Decimal.String() + } + + scale := -exponent + + if !coefficient.IsInt64() { + return d.Decimal.String() + } + + int64Value := coefficient.Int64() + + return d.stringFromInt64(int64Value, int(scale)) +} + +// StringFromInt64 is an internal method for converting int64 +// and scale to a string (for numbers up to 19 digits). +func (d Decimal) stringFromInt64(value int64, scale int) string { + + var buf [48]byte + pos := 0 + + negative := value < 0 + if negative { + if value == math.MinInt64 { + return d.handleMinInt64(scale) + } + buf[pos] = '-' + pos++ + value = -value + } + + str := strconv.FormatInt(value, 10) + length := len(str) + + if scale == 0 { + if pos+length > len(buf) { + return d.Decimal.String() + } + copy(buf[pos:], str) + pos += length + return string(buf[:pos]) + } + + if scale >= length { + + required := 2 + (scale - length) + length + if pos+required > len(buf) { + return d.Decimal.String() + } + + buf[pos] = '0' + buf[pos+1] = '.' + pos += 2 + + zeros := scale - length + for i := 0; i < zeros; i++ { + buf[pos] = '0' + pos++ + } + + copy(buf[pos:], str) + pos += length + } else { + + integerLen := length - scale + + required := integerLen + 1 + scale + if pos+required > len(buf) { + return d.Decimal.String() + } + + copy(buf[pos:], str[:integerLen]) + pos += integerLen + + buf[pos] = '.' + pos++ + + copy(buf[pos:], str[integerLen:]) + pos += scale + } + + return string(buf[:pos]) +} +func (d Decimal) handleMinInt64(scale int) string { + const minInt64Str = "9223372036854775808" + + var buf [48]byte + pos := 0 + + buf[pos] = '-' + pos++ + + length := len(minInt64Str) + + if scale == 0 { + if pos+length > len(buf) { + return "-" + minInt64Str // Fallback. + } + copy(buf[pos:], minInt64Str) + pos += length + return string(buf[:pos]) + } + + if scale >= length { + required := 2 + (scale - length) + length + if pos+required > len(buf) { + // Fallback. + result := "0." + strings.Repeat("0", scale-length) + minInt64Str + return "-" + result + } + + buf[pos] = '0' + buf[pos+1] = '.' + pos += 2 + + zeros := scale - length + for i := 0; i < zeros; i++ { + buf[pos] = '0' + pos++ + } + + copy(buf[pos:], minInt64Str) + pos += length + } else { + integerLen := length - scale + required := integerLen + 1 + scale + if pos+required > len(buf) { + return d.Decimal.String() // Fallback + } + + copy(buf[pos:], minInt64Str[:integerLen]) + pos += integerLen + + buf[pos] = '.' + pos++ + + copy(buf[pos:], minInt64Str[integerLen:]) + pos += scale + } + + return string(buf[:pos]) +} + +func MustMakeDecimal(src string) Decimal { + dec, err := MakeDecimalFromString(src) + if err != nil { + panic(fmt.Sprintf("MustMakeDecimalFromString: %v", err)) + } + return dec +} + func init() { msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) diff --git a/decimal/decimal_bench_test.go b/decimal/decimal_bench_test.go new file mode 100644 index 00000000..6c7df10a --- /dev/null +++ b/decimal/decimal_bench_test.go @@ -0,0 +1,330 @@ +package decimal + +import ( + "math/rand" + "strconv" + "strings" + "testing" + "time" +) + +// Minimal benchmark without dependencies. +func BenchmarkMinimal(b *testing.B) { + dec := MustMakeDecimal("123.45") + + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = dec.String() + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = dec.Decimal.String() + } + }) +} + +// Benchmark for small numbers (optimized conversion path). +func BenchmarkDecimalString_SmallNumbers(b *testing.B) { + smallNumbers := []string{ + "123.45", + "-123.45", + "0.00123", + "100.00", + "999.99", + "42", + "-42", + "0.000001", + "1234567.89", + "-987654.32", + } + + decimals := make([]Decimal, len(smallNumbers)) + for i, str := range smallNumbers { + decimals[i] = MustMakeDecimal(str) + } + + b.ResetTimer() + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.String() + } + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.Decimal.String() + } + } + }) +} + +// A benchmark for the boundary cases of int64. +func BenchmarkDecimalString_Int64Boundaries(b *testing.B) { + boundaryNumbers := []string{ + "9223372036854775807", // max int64 + "-9223372036854775808", // min int64 + "9223372036854775806", + "-9223372036854775807", + } + + decimals := make([]Decimal, len(boundaryNumbers)) + for i, str := range boundaryNumbers { + decimals[i] = MustMakeDecimal(str) + } + + b.ResetTimer() + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.String() + } + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.Decimal.String() + } + } + }) +} + +// Benchmark for large numbers (fallback path). +func BenchmarkDecimalString_LargeNumbers(b *testing.B) { + largeNumbers := []string{ + "123456789012345678901234567890.123456789", + "-123456789012345678901234567890.123456789", + "99999999999999999999999999999999999999", + "-99999999999999999999999999999999999999", + } + + decimals := make([]Decimal, len(largeNumbers)) + for i, str := range largeNumbers { + decimals[i] = MustMakeDecimal(str) + } + + b.ResetTimer() + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.String() + } + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.Decimal.String() + } + } + }) +} + +// A benchmark for mixed numbers (real-world scenarios). +func BenchmarkDecimalString_Mixed(b *testing.B) { + mixedNumbers := []string{ + "0", + "1", + "-1", + "0.5", + "-0.5", + "123.456", + "1000000.000001", + "9223372036854775807", + "123456789012345678901234567890.123456789", + } + + decimals := make([]Decimal, len(mixedNumbers)) + for i, str := range mixedNumbers { + decimals[i] = MustMakeDecimal(str) + } + + b.ResetTimer() + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.String() + } + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.Decimal.String() + } + } + }) +} + +// A benchmark for numbers with different precision. +func BenchmarkDecimalString_DifferentPrecision(b *testing.B) { + testCases := []struct { + name string + value string + }{ + {"Integer", "1234567890"}, + {"SmallDecimal", "0.000000001"}, + {"MediumDecimal", "123.456789"}, + {"LargeDecimal", "1234567890.123456789"}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + dec := MustMakeDecimal(tc.value) + + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = dec.String() + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = dec.Decimal.String() + } + }) + }) + } +} + +// A benchmark with random numbers for statistical significance. +func BenchmarkDecimalString_Random(b *testing.B) { + rand.Seed(time.Now().UnixNano()) + + // Generating random numbers in the int64 range. + generateRandomDecimal := func() Decimal { + // 70% chance for small numbers, 30% for large numbers. + if rand.Float64() < 0.7 { + // Числа в диапазоне int64 + value := rand.Int63n(1000000000) - 500000000 + scale := rand.Intn(10) + + if scale == 0 { + return MustMakeDecimal(strconv.FormatInt(value, 10)) + } + + // For numbers with a fractional part. + str := strconv.FormatInt(value, 10) + if value < 0 { + str = str[1:] // убираем минус + } + + if len(str) > scale { + integerPart := str[:len(str)-scale] + fractionalPart := str[len(str)-scale:] + result := integerPart + "." + fractionalPart + if value < 0 { + result = "-" + result + } + return MustMakeDecimal(result) + } else { + zeros := scale - len(str) + result := "0." + strings.Repeat("0", zeros) + str + if value < 0 { + result = "-" + result + } + return MustMakeDecimal(result) + } + } else { + // Large numbers (fallback) - we generate correct strings. + // Generating a 30-digit number. + bigDigits := make([]byte, 30) + for i := range bigDigits { + bigDigits[i] = byte(rand.Intn(10) + '0') + } + // Убираем ведущие нули + for len(bigDigits) > 1 && bigDigits[0] == '0' { + bigDigits = bigDigits[1:] + } + + bigNum := string(bigDigits) + scale := rand.Intn(10) + + if scale == 0 { + if rand.Float64() < 0.5 { + bigNum = "-" + bigNum + } + return MustMakeDecimal(bigNum) + } + + if scale < len(bigNum) { + integerPart := bigNum[:len(bigNum)-scale] + fractionalPart := bigNum[len(bigNum)-scale:] + result := integerPart + "." + fractionalPart + if rand.Float64() < 0.5 { + result = "-" + result + } + return MustMakeDecimal(result) + } else { + zeros := scale - len(bigNum) + result := "0." + strings.Repeat("0", zeros) + bigNum + if rand.Float64() < 0.5 { + result = "-" + result + } + return MustMakeDecimal(result) + } + } + } + + b.ResetTimer() + b.Run("String", func(b *testing.B) { + total := 0 + for i := 0; i < b.N; i++ { + dec := generateRandomDecimal() + result := dec.String() + total += len(result) + } + _ = total + }) + + b.Run("StandardString", func(b *testing.B) { + total := 0 + for i := 0; i < b.N; i++ { + dec := generateRandomDecimal() + result := dec.Decimal.String() + total += len(result) + } + _ = total + }) +} + +// A benchmark for checking memory allocations. +func BenchmarkDecimalString_MemoryAllocations(b *testing.B) { + testNumbers := []string{ + "123.45", + "0.001", + "9223372036854775807", + "123456789012345678901234567890.123456789", + } + + decimals := make([]Decimal, len(testNumbers)) + for i, str := range testNumbers { + decimals[i] = MustMakeDecimal(str) + } + + b.ResetTimer() + + b.Run("String", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.String() + } + } + }) + + b.Run("StandardString", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, dec := range decimals { + _ = dec.Decimal.String() + } + } + }) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index f7549420..3d457c63 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" "github.com/vmihailenco/msgpack/v5" . "github.com/tarantool/go-tarantool/v3" @@ -701,3 +702,218 @@ func TestMain(m *testing.M) { code := runTestMain(m) os.Exit(code) } + +func TestDecimalString(t *testing.T) { + tests := []struct { + name string + input string + expected string + willUseOptimized bool + }{ + { + name: "small positive decimal", + input: "123.45", + expected: "123.45", + willUseOptimized: true, + }, + { + name: "small negative decimal", + input: "-123.45", + expected: "-123.45", + willUseOptimized: true, + }, + { + name: "zero", + input: "0", + expected: "0", + willUseOptimized: true, + }, + { + name: "integer", + input: "12345", + expected: "12345", + willUseOptimized: true, + }, + { + name: "small decimal with leading zeros", + input: "0.00123", + expected: "0.00123", + willUseOptimized: true, + }, + { + name: "max int64", + input: "9223372036854775807", + expected: "9223372036854775807", + willUseOptimized: true, + }, + { + name: "min int64", + input: "-9223372036854775808", + expected: "-9223372036854775808", + willUseOptimized: true, + }, + { + name: "number beyond int64 range", + input: "9223372036854775808", + expected: "9223372036854775808", + }, + { + name: "very large decimal", + input: "123456789012345678901234567890.123456789", + expected: "123456789012345678901234567890.123456789", + willUseOptimized: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec, err := MakeDecimalFromString(tt.input) + assert.NoError(t, err) + + result := dec.String() + + assert.Equal(t, tt.expected, result) + + assert.Equal(t, dec.Decimal.String(), result) + }) + } +} + +func TestTarantoolBCDCompatibility(t *testing.T) { + + testCases := []string{ + "123.45", + "-123.45", + "0.001", + "100.00", + "999999.999999", + } + + for _, input := range testCases { + t.Run(input, func(t *testing.T) { + + dec, err := MakeDecimalFromString(input) + assert.NoError(t, err) + + msgpackData, err := dec.MarshalMsgpack() + assert.NoError(t, err) + + var dec2 Decimal + err = dec2.UnmarshalMsgpack(msgpackData) + assert.NoError(t, err) + + originalStr := dec.String() + roundtripStr := dec2.String() + + assert.Equal(t, originalStr, roundtripStr, + "BCD roundtrip failed for input: %s", input) + }) + } +} + +func TestRealTarantoolUsage(t *testing.T) { + + operations := []struct { + name string + data map[string]interface{} + }{ + { + name: "insert operation", + data: map[string]interface{}{ + "id": 1, + "amount": MustMakeDecimal("123.45"), + "balance": MustMakeDecimal("-500.00"), + }, + }, + { + name: "update operation", + data: map[string]interface{}{ + "id": 2, + "price": MustMakeDecimal("99.99"), + "quantity": MustMakeDecimal("1000.000"), + }, + }, + } + + for _, op := range operations { + t.Run(op.name, func(t *testing.T) { + + for key, value := range op.data { + if dec, isDecimal := value.(Decimal); isDecimal { + str := dec.String() + + assert.NotEmpty(t, str) + assert.Contains(t, []string{".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"}, string(str[0])) + + assert.Equal(t, dec.Decimal.String(), str) + + t.Logf("%s: %s", key, str) + } + } + }) + } +} + +func Test100_00(t *testing.T) { + dec := MustMakeDecimal("100.00") + coefficient := dec.Decimal.Coefficient() + exponent := dec.Decimal.Exponent() + t.Logf("Coefficient: %v, Exponent: %v", coefficient, exponent) + t.Logf("Coefficient.IsInt64: %v", coefficient.IsInt64()) + if coefficient.IsInt64() { + t.Logf("Int64: %v", coefficient.Int64()) + } + result := dec.String() + t.Logf("String: %q", result) +} + +func TestLargeNumberString(t *testing.T) { + largeNumber := "123456789012345678901234567890.123456789" + dec, err := MakeDecimalFromString(largeNumber) + if err != nil { + t.Fatalf("Failed to create decimal: %v", err) + } + + // Check that the coefficient does not fit in int64. + coefficient := dec.Decimal.Coefficient() + if coefficient.IsInt64() { + t.Error("Expected coefficient to be too large for int64") + } + + optimized := dec.String() + standard := dec.Decimal.String() + + if optimized != standard { + t.Errorf("Results differ: optimized=%s, standard=%s", optimized, standard) + } + + if optimized != largeNumber { + t.Errorf("Expected %s, got %s", largeNumber, optimized) + } + + t.Logf("Large number handled via fallback: %s", optimized) +} + +func TestDecimalTrailingZeros(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"100.00", "100.00"}, + {"0.00", "0.00"}, + {"0.000", "0.000"}, + {"1.000", "1.000"}, + {"123.4500", "123.4500"}, + {"0.00100", "0.00100"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + dec := MustMakeDecimal(tt.input) + result := dec.String() + if result != tt.expected { + t.Errorf("For %s: expected %s, got %s", tt.input, tt.expected, result) + } + }) + } +}