Skip to content

Conversation

@babyTsakhes
Copy link
Collaborator

msgpack_ext: add string() for decimal

Added a decimal type conversion function to a string, added tests for this function. Fixed name of function. Add test for decimal convertion. I tried to write an optimized version, but there is a problem with values when working with BCD, insignificant zeros after the decimal point are lost.

Added #322

@babyTsakhes
Copy link
Collaborator Author

TestTarantoolBCDCompatibility is FAIL.

@babyTsakhes
Copy link
Collaborator Author

return d.Decimal.String() - this method true, but slowly. I will correct the code and comments, at the moment I am looking for a solution to the problem.

Copy link
Collaborator

@oleg-jukovec oleg-jukovec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, squash commits into a single relevant-one.

return ptr.UnmarshalMsgpack(b)
}

// This method converts Datetime to String - formats to ISO8601.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The go-idiomatic way to comment a method.

Suggested change
// This method converts Datetime to String - formats to ISO8601.
// String converts Datetime to String - formats to ISO8601.

}

func TestDatetimeString(t *testing.T) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment on lines 161 to 381
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])
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code works faster than if you call the method d.Decimal.String() . Is it okay to leave it?

Copy link
Collaborator

@oleg-jukovec oleg-jukovec Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, rename it to String and add benchmarks to compare with shopspring.Decimal if so.

Copy link
Collaborator Author

@babyTsakhes babyTsakhes Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I found out what the problem was with launching benchmarks, I just had to launch it without Tarantool.
Benchmark String Optimized for Decimal

Copy link
Collaborator Author

@babyTsakhes babyTsakhes Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the results, my conversion function is twice as good if we have a lot of examples of numbers int64. After int64, it is slower by 100 nanoseconds. Thus, we have a compromise: for small numbers (which fit into an int64) there is a gain, for large numbers there is a small loss.
But in real-world scenarios, small numbers are probably more common. It is also worth noting that the difference in performance for large numbers is not very big (866.4 ns/op vs. 788.5 ns/op),
while for small numbers, the gain was several times.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note that for numbers that do not fit into Int 64, the Decimal.String() function is also used, but not immediately, but after checking the number for the value, due to this, the stringOptimized function, which I will soon rename to just String(), works a little slower.
And there are extra allocations in the case of large numbers, d.Decimal.Efficient() - creates a copy of big.Int
d.Decimal.Exponent() is a simple field, but it's still a method call.
coefficient.IsInt64() - big size check.Int
For large numbers, these operations create additional allocations and take time, even when we know that a fallback is inevitable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Therefore, I will try to make a simpler check for the value of the number.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestRealTarantoolUsage is broken. We need to decide whether the String() method for Decimal should return insignificant zeros after . or not. In general, in Tarantula, it is expected that 100.00 will be like 100, but the BCDCompability test expects the opposite. BCDCompability is not broken because of the String() method.

@babyTsakhes babyTsakhes force-pushed the babyTsakhes/gh-322-add-string-convertion-for-decimal-datetime-interval branch 2 times, most recently from a66d851 to 679282c Compare November 6, 2025 19:01
@babyTsakhes
Copy link
Collaborator Author

babyTsakhes commented Nov 11, 2025

FAIL: ExampleInterval_Add (0.00s)
got:
11 years, 2 months, 3 weeks, 30 minutes and 10 seconds
want:
{11 2 3 0 0 30 10 0 1}
=== RUN ExampleInterval_Sub
--- FAIL: ExampleInterval_Sub (0.00s)
got:
-9 years, 2 months, 3 weeks, -30 minutes and 10 seconds
want:
{-9 2 3 0 0 -30 10 0 1}
FAIL
FAIL github.com/tarantool/go-tarantool/v3/datetime 0.174s

What behavior should this test demonstrate now? After I added String(), it now gets a string, and expects a structure. Should I change the expectation of this test or not?

@babyTsakhes babyTsakhes marked this pull request as ready for review November 11, 2025 10:19
@babyTsakhes
Copy link
Collaborator Author

@babyTsakhes babyTsakhes force-pushed the babyTsakhes/gh-322-add-string-convertion-for-decimal-datetime-interval branch 3 times, most recently from 200537c to 7bd96f5 Compare November 11, 2025 12:56
@babyTsakhes
Copy link
Collaborator Author

func TestTarantoolBCDCompatibility(t *testing.T) {

func TestRealTarantoolUsage(t *testing.T) {

@babyTsakhes
Copy link
Collaborator Author

This TestRealTarantoolUsage test fails because the string conversion code retains insignificant zeros after the dot. Keep insignificant zeros after the dot? The TestTarantoolBCDCompatibility test fails due to Unmarshal, and String() does not apply to operation. In principle, we have already discussed that it is possible to leave zeros after the dot.

@babyTsakhes babyTsakhes force-pushed the babyTsakhes/gh-322-add-string-convertion-for-decimal-datetime-interval branch from 7bd96f5 to 6688ae7 Compare November 11, 2025 13:29
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
@babyTsakhes babyTsakhes force-pushed the babyTsakhes/gh-322-add-string-convertion-for-decimal-datetime-interval branch from 338c283 to 47fc723 Compare November 13, 2025 00:12
@babyTsakhes
Copy link
Collaborator Author

Decimal now removes insignificant zeros after the decimal point, as it works in Tarantula. The tests are passing. The updated String function for decimal numbers is still 2 times faster than the library function. Please review my code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants