Skip to content

Commit 6795e0e

Browse files
committed
msgpack_ext: added string() function for decimal
Added function for converting decimal type to string, added tests for this function. Fixed function name. Added test for decimal conversion. Added benchmark test confirming that using self-written function is faster than using function from library. Added #322
1 parent e3b956c commit 6795e0e

File tree

4 files changed

+818
-3
lines changed

4 files changed

+818
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1515
* Added missing IPROTO feature flags to greeting negotiation
1616
(iproto.IPROTO_FEATURE_IS_SYNC, iproto.IPROTO_FEATURE_INSERT_ARROW) (#466).
1717
* Added Future.cond (sync.Cond) and Future.finished bool. Added Future.finish() marks Future as done (#496).
18+
* Added function String() for type datetime, decimal (#322).
1819

1920
### Changed
2021

decimal/decimal.go

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ package decimal
2121

2222
import (
2323
"fmt"
24+
"math"
2425
"reflect"
26+
"strconv"
2527

2628
"github.com/shopspring/decimal"
2729
"github.com/vmihailenco/msgpack/v5"
@@ -96,7 +98,7 @@ func (d Decimal) MarshalMsgpack() ([]byte, error) {
9698
// +--------+-------------------+------------+===============+
9799
// | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal |
98100
// +--------+-------------------+------------+===============+
99-
strBuf := d.String()
101+
strBuf := d.Decimal.String()
100102
bcdBuf, err := encodeStringToBCD(strBuf)
101103
if err != nil {
102104
return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err)
@@ -144,6 +146,247 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
144146
return ptr.UnmarshalMsgpack(b)
145147
}
146148

149+
// This method converts the decimal type to a string.
150+
// Use shopspring/decimal by default.
151+
// String - optimized version for Tarantool Decimal
152+
// taking into account the limitations of int64 and support for large numbers via fallback
153+
// Tarantool decimal has 38 digits, which can exceed int64.
154+
// Therefore, we cannot use int64 for all cases.
155+
// For the general case, use shopspring/decimal.String().
156+
// For cases where it is known that numbers contain less than 26 characters,
157+
// you can use the optimized version.
158+
func (d Decimal) String() string {
159+
coefficient := d.Decimal.Coefficient() // Note: In shopspring/decimal
160+
// 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 [64]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+
// Special case: zero value.
199+
if value == 0 {
200+
return "0" // Always return "0" regardless of scale.
201+
}
202+
203+
if scale == 0 {
204+
// No fractional part.
205+
if pos+length > len(buf) {
206+
return d.Decimal.String()
207+
}
208+
copy(buf[pos:], str)
209+
pos += length
210+
return string(buf[:pos])
211+
}
212+
213+
if scale >= length {
214+
// Numbers like 0.00123.
215+
// Count trailing zeros in the fractional part.
216+
trailingZeros := 0
217+
// In this case, the fractional part consists
218+
// of (scale-length) zeros followed by the number.
219+
// We need to count trailing zeros in the actual number part.
220+
for i := length - 1; i >= 0 && str[i] == '0'; i-- {
221+
trailingZeros++
222+
}
223+
224+
effectiveDigits := length - trailingZeros
225+
226+
// If all digits are zeros after leading zeros, we need to adjust.
227+
if effectiveDigits == 0 {
228+
return "0"
229+
}
230+
231+
required := 2 + (scale - length) + effectiveDigits
232+
if pos+required > len(buf) {
233+
return d.Decimal.String()
234+
}
235+
236+
buf[pos] = '0'
237+
buf[pos+1] = '.'
238+
pos += 2
239+
240+
// Add leading zeros.
241+
zeros := scale - length
242+
for i := 0; i < zeros; i++ {
243+
buf[pos] = '0'
244+
pos++
245+
}
246+
247+
// Copy only significant digits (without trailing zeros).
248+
copy(buf[pos:], str[:effectiveDigits])
249+
pos += effectiveDigits
250+
} else {
251+
// Numbers like 123.45.
252+
integerLen := length - scale
253+
254+
// Count trailing zeros in fractional part.
255+
trailingZeros := 0
256+
for i := length - 1; i >= integerLen && str[i] == '0'; i-- {
257+
trailingZeros++
258+
}
259+
260+
effectiveScale := scale - trailingZeros
261+
262+
// If all fractional digits are zeros, return just integer part.
263+
if effectiveScale == 0 {
264+
if pos+integerLen > len(buf) {
265+
return d.Decimal.String()
266+
}
267+
copy(buf[pos:], str[:integerLen])
268+
pos += integerLen
269+
return string(buf[:pos])
270+
}
271+
272+
required := integerLen + 1 + effectiveScale
273+
if pos+required > len(buf) {
274+
return d.Decimal.String()
275+
}
276+
277+
// Integer part.
278+
copy(buf[pos:], str[:integerLen])
279+
pos += integerLen
280+
281+
// Decimal point.
282+
buf[pos] = '.'
283+
pos++
284+
285+
// Fractional part without trailing zeros.
286+
fractionalEnd := integerLen + effectiveScale
287+
copy(buf[pos:], str[integerLen:fractionalEnd])
288+
pos += effectiveScale
289+
}
290+
291+
return string(buf[:pos])
292+
}
293+
func (d Decimal) handleMinInt64(scale int) string {
294+
const minInt64Str = "9223372036854775808"
295+
296+
var buf [64]byte
297+
pos := 0
298+
299+
buf[pos] = '-'
300+
pos++
301+
302+
length := len(minInt64Str)
303+
304+
if scale == 0 {
305+
if pos+length > len(buf) {
306+
return "-" + minInt64Str
307+
}
308+
copy(buf[pos:], minInt64Str)
309+
pos += length
310+
return string(buf[:pos])
311+
}
312+
313+
if scale >= length {
314+
// Count trailing zeros in the actual number part.
315+
trailingZeros := 0
316+
for i := length - 1; i >= 0 && minInt64Str[i] == '0'; i-- {
317+
trailingZeros++
318+
}
319+
320+
effectiveDigits := length - trailingZeros
321+
322+
if effectiveDigits == 0 {
323+
return "0"
324+
}
325+
326+
required := 2 + (scale - length) + effectiveDigits
327+
if pos+required > len(buf) {
328+
return d.Decimal.String()
329+
}
330+
331+
buf[pos] = '0'
332+
buf[pos+1] = '.'
333+
pos += 2
334+
335+
zeros := scale - length
336+
for i := 0; i < zeros; i++ {
337+
buf[pos] = '0'
338+
pos++
339+
}
340+
341+
copy(buf[pos:], minInt64Str[:effectiveDigits])
342+
pos += effectiveDigits
343+
} else {
344+
integerLen := length - scale
345+
346+
// Count trailing zeros for minInt64Str fractional part.
347+
trailingZeros := 0
348+
for i := length - 1; i >= integerLen && minInt64Str[i] == '0'; i-- {
349+
trailingZeros++
350+
}
351+
352+
effectiveScale := scale - trailingZeros
353+
354+
if effectiveScale == 0 {
355+
if pos+integerLen > len(buf) {
356+
return d.Decimal.String()
357+
}
358+
copy(buf[pos:], minInt64Str[:integerLen])
359+
pos += integerLen
360+
return string(buf[:pos])
361+
}
362+
363+
required := integerLen + 1 + effectiveScale
364+
if pos+required > len(buf) {
365+
return d.Decimal.String()
366+
}
367+
368+
copy(buf[pos:], minInt64Str[:integerLen])
369+
pos += integerLen
370+
371+
buf[pos] = '.'
372+
pos++
373+
374+
fractionalEnd := integerLen + effectiveScale
375+
copy(buf[pos:], minInt64Str[integerLen:fractionalEnd])
376+
pos += effectiveScale
377+
}
378+
379+
return string(buf[:pos])
380+
}
381+
382+
func MustMakeDecimal(src string) Decimal {
383+
dec, err := MakeDecimalFromString(src)
384+
if err != nil {
385+
panic(fmt.Sprintf("MustMakeDecimalFromString: %v", err))
386+
}
387+
return dec
388+
}
389+
147390
func init() {
148391
msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder)
149392
msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder)

0 commit comments

Comments
 (0)