Skip to content

Commit afca615

Browse files
committed
msgpack: add string() for decimal
Added a decimal type conversion function to a string, added tests for this function. Added #322
1 parent 8384443 commit afca615

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed

decimal/decimal.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ package decimal
2121

2222
import (
2323
"fmt"
24+
"math"
2425
"reflect"
26+
"strconv"
27+
"strings"
2528

2629
"github.com/shopspring/decimal"
2730
"github.com/vmihailenco/msgpack/v5"
@@ -144,6 +147,145 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
144147
return ptr.UnmarshalMsgpack(b)
145148
}
146149

150+
// This method converts the decimal type to a string.
151+
// Use shopspring/decimal by default.
152+
// StringOptimized - optimized version for Tarantool Decimal
153+
// taking into account the limitations of int64 and support for large numbers via fallback
154+
// Tarantool decimal has 38 digits, which can exceed int64.
155+
// Therefore, we cannot use int64 for all cases.
156+
// For the general case, use shopspring/decimal.String().
157+
// For cases where it is known that numbers contain less than 26 characters,
158+
// you can use the optimized version.
159+
func (d Decimal) String() string {
160+
coefficient := d.Decimal.Coefficient() // Note: In shopspring/decimal, the number is stored as coefficient *10^exponent, where exponent can be negative.
161+
exponent := d.Decimal.Exponent()
162+
163+
// If exponent is positive, then we use the standard method.
164+
if exponent > 0 {
165+
return d.Decimal.String()
166+
}
167+
168+
scale := -exponent
169+
170+
if !coefficient.IsInt64() {
171+
return d.Decimal.String()
172+
}
173+
174+
int64Value := coefficient.Int64()
175+
176+
return d.stringFromInt64(int64Value, int(scale))
177+
}
178+
179+
// StringFromInt64 is an internal method for converting int64
180+
// and scale to a string (for numbers up to 19 digits)
181+
func (d Decimal) stringFromInt64(value int64, scale int) string {
182+
var buf [32]byte
183+
pos := 0
184+
185+
negative := value < 0
186+
if negative {
187+
if value == math.MinInt64 {
188+
return d.handleMinInt64(scale)
189+
}
190+
buf[pos] = '-'
191+
pos++
192+
value = -value
193+
}
194+
195+
str := strconv.FormatInt(value, 10)
196+
length := len(str)
197+
198+
if scale == 0 {
199+
copy(buf[pos:], str)
200+
pos += length
201+
return string(buf[:pos])
202+
}
203+
204+
if scale >= length {
205+
buf[pos] = '0'
206+
buf[pos+1] = '.'
207+
pos += 2
208+
209+
zeros := scale - length
210+
for i := 0; i < zeros; i++ {
211+
buf[pos] = '0'
212+
pos++
213+
}
214+
215+
copy(buf[pos:], str)
216+
pos += length
217+
} else {
218+
integerLen := length - scale
219+
copy(buf[pos:], str[:integerLen])
220+
pos += integerLen
221+
222+
fractionalPart := str[integerLen:]
223+
224+
fractionalPart = strings.TrimRight(fractionalPart, "0")
225+
226+
if len(fractionalPart) > 0 {
227+
buf[pos] = '.'
228+
pos++
229+
copy(buf[pos:], fractionalPart)
230+
pos += len(fractionalPart)
231+
}
232+
}
233+
234+
return string(buf[:pos])
235+
}
236+
237+
func (d Decimal) handleMinInt64(scale int) string {
238+
239+
const minInt64Str = "9223372036854775808"
240+
241+
if scale == 0 {
242+
return "-" + minInt64Str
243+
}
244+
245+
var buf [32]byte
246+
pos := 0
247+
248+
buf[pos] = '-'
249+
pos++
250+
251+
length := len(minInt64Str)
252+
253+
if scale >= length {
254+
buf[pos] = '0'
255+
buf[pos+1] = '.'
256+
pos += 2
257+
258+
zeros := scale - length
259+
for i := 0; i < zeros; i++ {
260+
buf[pos] = '0'
261+
pos++
262+
}
263+
264+
copy(buf[pos:], minInt64Str)
265+
pos += length
266+
} else {
267+
integerLen := length - scale
268+
copy(buf[pos:], minInt64Str[:integerLen])
269+
pos += integerLen
270+
271+
buf[pos] = '.'
272+
pos++
273+
274+
copy(buf[pos:], minInt64Str[integerLen:])
275+
pos += scale
276+
}
277+
278+
return string(buf[:pos])
279+
}
280+
281+
func MustMakeDecimal(src string) Decimal {
282+
dec, err := MakeDecimalFromString(src)
283+
if err != nil {
284+
panic(fmt.Sprintf("MustMakeDecimalFromString: %v", err))
285+
}
286+
return dec
287+
}
288+
147289
func init() {
148290
msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder)
149291
msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder)

decimal/decimal_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/shopspring/decimal"
13+
"github.com/stretchr/testify/assert"
1314
"github.com/vmihailenco/msgpack/v5"
1415

1516
. "github.com/tarantool/go-tarantool/v3"
@@ -701,3 +702,154 @@ func TestMain(m *testing.M) {
701702
code := runTestMain(m)
702703
os.Exit(code)
703704
}
705+
706+
func TestDecimalStringOptimized(t *testing.T) {
707+
tests := []struct {
708+
name string
709+
input string
710+
expected string
711+
willUseOptimized bool
712+
}{
713+
{
714+
name: "small positive decimal",
715+
input: "123.45",
716+
expected: "123.45",
717+
willUseOptimized: true,
718+
},
719+
{
720+
name: "small negative decimal",
721+
input: "-123.45",
722+
expected: "-123.45",
723+
willUseOptimized: true,
724+
},
725+
{
726+
name: "zero",
727+
input: "0",
728+
expected: "0",
729+
willUseOptimized: true,
730+
},
731+
{
732+
name: "integer",
733+
input: "12345",
734+
expected: "12345",
735+
willUseOptimized: true,
736+
},
737+
{
738+
name: "small decimal with leading zeros",
739+
input: "0.00123",
740+
expected: "0.00123",
741+
willUseOptimized: true,
742+
},
743+
{
744+
name: "max int64",
745+
input: "9223372036854775807",
746+
expected: "9223372036854775807",
747+
willUseOptimized: true,
748+
},
749+
{
750+
name: "min int64",
751+
input: "-9223372036854775808",
752+
expected: "-9223372036854775808",
753+
willUseOptimized: true,
754+
},
755+
{
756+
name: "number beyond int64 range",
757+
input: "9223372036854775808",
758+
expected: "9223372036854775808",
759+
},
760+
{
761+
name: "very large decimal",
762+
input: "123456789012345678901234567890.123456789",
763+
expected: "123456789012345678901234567890.123456789",
764+
willUseOptimized: false,
765+
},
766+
}
767+
768+
for _, tt := range tests {
769+
t.Run(tt.name, func(t *testing.T) {
770+
dec, err := MakeDecimalFromString(tt.input)
771+
assert.NoError(t, err)
772+
773+
result := dec.String()
774+
775+
assert.Equal(t, tt.expected, result)
776+
777+
assert.Equal(t, dec.Decimal.String(), result)
778+
})
779+
}
780+
}
781+
782+
func TestTarantoolBCDCompatibility(t *testing.T) {
783+
784+
testCases := []string{
785+
"123.45",
786+
"-123.45",
787+
"0.001",
788+
"100.00",
789+
"999999.999999",
790+
}
791+
792+
for _, input := range testCases {
793+
t.Run(input, func(t *testing.T) {
794+
795+
dec, err := MakeDecimalFromString(input)
796+
assert.NoError(t, err)
797+
798+
msgpackData, err := dec.MarshalMsgpack()
799+
assert.NoError(t, err)
800+
801+
var dec2 Decimal
802+
err = dec2.UnmarshalMsgpack(msgpackData)
803+
assert.NoError(t, err)
804+
805+
originalStr := dec.String()
806+
roundtripStr := dec2.String()
807+
808+
assert.Equal(t, originalStr, roundtripStr,
809+
"BCD roundtrip failed for input: %s", input)
810+
})
811+
}
812+
}
813+
814+
func TestRealTarantoolUsage(t *testing.T) {
815+
816+
operations := []struct {
817+
name string
818+
data map[string]interface{}
819+
}{
820+
{
821+
name: "insert operation",
822+
data: map[string]interface{}{
823+
"id": 1,
824+
"amount": MustMakeDecimal("123.45"),
825+
"balance": MustMakeDecimal("-500.00"),
826+
},
827+
},
828+
{
829+
name: "update operation",
830+
data: map[string]interface{}{
831+
"id": 2,
832+
"price": MustMakeDecimal("99.99"),
833+
"quantity": MustMakeDecimal("1000.000"),
834+
},
835+
},
836+
}
837+
838+
for _, op := range operations {
839+
t.Run(op.name, func(t *testing.T) {
840+
841+
for key, value := range op.data {
842+
if dec, isDecimal := value.(Decimal); isDecimal {
843+
str := dec.String()
844+
845+
assert.NotEmpty(t, str)
846+
assert.Contains(t, []string{".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"}, string(str[0]))
847+
848+
assert.Equal(t, dec.Decimal.String(), str)
849+
850+
t.Logf("%s: %s", key, str)
851+
}
852+
}
853+
})
854+
}
855+
}

0 commit comments

Comments
 (0)