Skip to content

Commit 5a03715

Browse files
babyTsakhesoleg-jukovec
authored andcommitted
decimal: added Decimal.String()
The commit adds a method 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. Closes #322
1 parent 109f02f commit 5a03715

4 files changed

Lines changed: 889 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
2828
power-of-two sized byte slices (#493).
2929
* New `Opts.Allocator` option to configure a custom allocator for a
3030
connection (#493).
31+
* Method String() for type decimal (#322).
3132

3233
### Changed
3334

decimal/decimal.go

Lines changed: 239 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,242 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
144146
return ptr.UnmarshalMsgpack(b)
145147
}
146148

149+
// String converts the decimal type to a string representation.
150+
// For numbers within int64 range, it uses an optimized conversion path.
151+
// For larger numbers (Tarantool decimal has 38 digits, which can exceed int64),
152+
// it falls back to shopspring/decimal.String().
153+
func (d Decimal) String() string {
154+
coefficient := d.Decimal.Coefficient() // Note: In shopspring/decimal
155+
// the number is stored as coefficient *10^exponent, where exponent can be negative.
156+
exponent := d.Decimal.Exponent()
157+
158+
// If exponent is positive, then we use the standard method.
159+
if exponent > 0 {
160+
return d.Decimal.String()
161+
}
162+
163+
scale := -exponent
164+
165+
if !coefficient.IsInt64() {
166+
return d.Decimal.String()
167+
}
168+
169+
int64Value := coefficient.Int64()
170+
171+
return d.stringFromInt64(int64Value, int(scale))
172+
}
173+
174+
// StringFromInt64 is an internal method for converting int64
175+
// and scale to a string (for numbers up to 19 digits).
176+
func (d Decimal) stringFromInt64(value int64, scale int) string {
177+
var buf [64]byte
178+
pos := 0
179+
180+
negative := value < 0
181+
if negative {
182+
if value == math.MinInt64 {
183+
return d.handleMinInt64(scale)
184+
}
185+
buf[pos] = '-'
186+
pos++
187+
value = -value
188+
}
189+
190+
str := strconv.FormatInt(value, 10)
191+
length := len(str)
192+
193+
// Special case: zero value.
194+
if value == 0 {
195+
return "0" // Always return "0" regardless of scale.
196+
}
197+
198+
if scale == 0 {
199+
// No fractional part.
200+
if pos+length > len(buf) {
201+
return d.Decimal.String()
202+
}
203+
copy(buf[pos:], str)
204+
pos += length
205+
return string(buf[:pos])
206+
}
207+
208+
if scale >= length {
209+
// Numbers like 0.00123.
210+
// Count trailing zeros in the fractional part.
211+
trailingZeros := 0
212+
// In this case, the fractional part consists
213+
// of (scale-length) zeros followed by the number.
214+
// We need to count trailing zeros in the actual number part.
215+
for i := length - 1; i >= 0 && str[i] == '0'; i-- {
216+
trailingZeros++
217+
}
218+
219+
effectiveDigits := length - trailingZeros
220+
221+
// If all digits are zeros after leading zeros, we need to adjust.
222+
if effectiveDigits == 0 {
223+
return "0"
224+
}
225+
226+
required := 2 + (scale - length) + effectiveDigits
227+
if pos+required > len(buf) {
228+
return d.Decimal.String()
229+
}
230+
231+
buf[pos] = '0'
232+
buf[pos+1] = '.'
233+
pos += 2
234+
235+
// Add leading zeros.
236+
zeros := scale - length
237+
for i := 0; i < zeros; i++ {
238+
buf[pos] = '0'
239+
pos++
240+
}
241+
242+
// Copy only significant digits (without trailing zeros).
243+
copy(buf[pos:], str[:effectiveDigits])
244+
pos += effectiveDigits
245+
} else {
246+
// Numbers like 123.45.
247+
integerLen := length - scale
248+
249+
// Count trailing zeros in fractional part.
250+
trailingZeros := 0
251+
for i := length - 1; i >= integerLen && str[i] == '0'; i-- {
252+
trailingZeros++
253+
}
254+
255+
effectiveScale := scale - trailingZeros
256+
257+
// If all fractional digits are zeros, return just integer part.
258+
if effectiveScale == 0 {
259+
if pos+integerLen > len(buf) {
260+
return d.Decimal.String()
261+
}
262+
copy(buf[pos:], str[:integerLen])
263+
pos += integerLen
264+
return string(buf[:pos])
265+
}
266+
267+
required := integerLen + 1 + effectiveScale
268+
if pos+required > len(buf) {
269+
return d.Decimal.String()
270+
}
271+
272+
// Integer part.
273+
copy(buf[pos:], str[:integerLen])
274+
pos += integerLen
275+
276+
// Decimal point.
277+
buf[pos] = '.'
278+
pos++
279+
280+
// Fractional part without trailing zeros.
281+
fractionalEnd := integerLen + effectiveScale
282+
copy(buf[pos:], str[integerLen:fractionalEnd])
283+
pos += effectiveScale
284+
}
285+
286+
return string(buf[:pos])
287+
}
288+
func (d Decimal) handleMinInt64(scale int) string {
289+
const minInt64Str = "9223372036854775808"
290+
291+
var buf [64]byte
292+
pos := 0
293+
294+
buf[pos] = '-'
295+
pos++
296+
297+
length := len(minInt64Str)
298+
299+
if scale == 0 {
300+
if pos+length > len(buf) {
301+
return "-" + minInt64Str
302+
}
303+
copy(buf[pos:], minInt64Str)
304+
pos += length
305+
return string(buf[:pos])
306+
}
307+
308+
if scale >= length {
309+
// Count trailing zeros in the actual number part.
310+
trailingZeros := 0
311+
for i := length - 1; i >= 0 && minInt64Str[i] == '0'; i-- {
312+
trailingZeros++
313+
}
314+
315+
effectiveDigits := length - trailingZeros
316+
317+
if effectiveDigits == 0 {
318+
return "0"
319+
}
320+
321+
required := 2 + (scale - length) + effectiveDigits
322+
if pos+required > len(buf) {
323+
return d.Decimal.String()
324+
}
325+
326+
buf[pos] = '0'
327+
buf[pos+1] = '.'
328+
pos += 2
329+
330+
zeros := scale - length
331+
for i := 0; i < zeros; i++ {
332+
buf[pos] = '0'
333+
pos++
334+
}
335+
336+
copy(buf[pos:], minInt64Str[:effectiveDigits])
337+
pos += effectiveDigits
338+
} else {
339+
integerLen := length - scale
340+
341+
// Count trailing zeros for minInt64Str fractional part.
342+
trailingZeros := 0
343+
for i := length - 1; i >= integerLen && minInt64Str[i] == '0'; i-- {
344+
trailingZeros++
345+
}
346+
347+
effectiveScale := scale - trailingZeros
348+
349+
if effectiveScale == 0 {
350+
if pos+integerLen > len(buf) {
351+
return d.Decimal.String()
352+
}
353+
copy(buf[pos:], minInt64Str[:integerLen])
354+
pos += integerLen
355+
return string(buf[:pos])
356+
}
357+
358+
required := integerLen + 1 + effectiveScale
359+
if pos+required > len(buf) {
360+
return d.Decimal.String()
361+
}
362+
363+
copy(buf[pos:], minInt64Str[:integerLen])
364+
pos += integerLen
365+
366+
buf[pos] = '.'
367+
pos++
368+
369+
fractionalEnd := integerLen + effectiveScale
370+
copy(buf[pos:], minInt64Str[integerLen:fractionalEnd])
371+
pos += effectiveScale
372+
}
373+
374+
return string(buf[:pos])
375+
}
376+
377+
func MustMakeDecimal(src string) Decimal {
378+
dec, err := MakeDecimalFromString(src)
379+
if err != nil {
380+
panic(fmt.Sprintf("MustMakeDecimalFromString: %v", err))
381+
}
382+
return dec
383+
}
384+
147385
func init() {
148386
msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder)
149387
msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder)

0 commit comments

Comments
 (0)