Skip to content

Commit ee41244

Browse files
committed
decimal: add support decimal type in msgpack
This patch provides decimal support for all space operations and as function return result. Decimal type was introduced in Tarantool 2.2. See more about decimal type in [1] and [2]. To use decimal with github.com/shopspring/decimal in msgpack, import tarantool/decimal submodule. 1. https://www.tarantool.io/en/doc/latest/book/box/data_model/ 2. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type 3. https://github.com/douglascrockford/DEC64/blob/663f562a5f0621021b98bfdd4693571993316174/dec64_test.c#L62-L104 4. https://github.com/shopspring/decimal/blob/v1.3.1/decimal_test.go#L27-L64 5. https://github.com/tarantool/tarantool/blob/60fe9d14c1c7896aa7d961e4b68649eddb4d2d6c/test/unit/decimal.c#L154-L171 Lua snippet for encoding number to MsgPack representation: local decimal = require('decimal') local function mp_encode_dec(num) local dec = msgpack.encode(decimal.new(num)) return dec:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) end print(mp_encode_dec(-12.34)) -- 0xd6010201234d Follows up tarantool/tarantool#692 Closes #96
1 parent de95e31 commit ee41244

File tree

9 files changed

+898
-0
lines changed

9 files changed

+898
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1717
- Go modules support (#91)
1818
- queue-utube handling (#85)
1919
- Master discovery (#113)
20+
- Support decimal type in msgpack (#96)
2021

2122
### Fixed
2223

Makefile

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ test-main:
4545
go clean -testcache
4646
go test . -v -p 1
4747

48+
.PHONY: test-decimal
49+
test-decimal:
50+
@echo "Running tests in decimal package"
51+
go clean -testcache
52+
go test ./decimal/ -v -p 1
53+
4854
.PHONY: coverage
4955
coverage:
5056
go clean -testcache

decimal/bcd.go

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Package implements methods to encode and decode BCD.
2+
//
3+
// BCD (Binary-Coded Decimal) is a sequence of bytes representing decimal
4+
// digits of the encoded number (each byte has two decimal digits each encoded
5+
// using 4-bit nibbles), so byte >> 4 is the first digit and byte & 0x0f is the
6+
// second digit. The leftmost digit in the array is the most significant. The
7+
// rightmost digit in the array is the least significant.
8+
//
9+
// The first byte of the BCD array contains the first digit of the number,
10+
// represented as follows:
11+
//
12+
// | 4 bits | 4 bits |
13+
// = 0x = the 1st digit
14+
//
15+
// (The first nibble contains 0 if the decimal number has an even number of
16+
// digits). The last byte of the BCD array contains the last digit of the
17+
// number and the final nibble, represented as follows:
18+
//
19+
// | 4 bits | 4 bits |
20+
// = the last digit = nibble
21+
//
22+
// The final nibble represents the number's sign: 0x0a, 0x0c, 0x0e, 0x0f stand
23+
// for plus, 0x0b and 0x0d stand for minus.
24+
//
25+
// Examples:
26+
//
27+
// The decimal -12.34 will be encoded as 0xd6, 0x01, 0x02, 0x01, 0x23, 0x4d:
28+
//
29+
// |MP_EXT (fixext 4) | MP_DECIMAL | scale | 1 | 2,3 | 4 (minus) |
30+
// | 0xd6 | 0x01 | 0x02 | 0x01 | 0x23 | 0x4d |
31+
//
32+
// The decimal 0.000000000000000000000000000000000010 will be encoded as
33+
// 0xc7, 0x03, 0x01, 0x24, 0x01, 0x0c:
34+
//
35+
// | MP_EXT (ext 8) | length | MP_DECIMAL | scale | 1 | 0 (plus) |
36+
// | 0xc7 | 0x03 | 0x01 | 0x24 | 0x01 | 0x0c |
37+
//
38+
// See also:
39+
//
40+
// * MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/
41+
//
42+
// * An implementation in C language https://github.com/tarantool/decNumber/blob/master/decPacked.c
43+
//
44+
// TODO: BitReader https://go.dev/play/p/Wyr_K9YAro
45+
package decimal
46+
47+
import (
48+
"fmt"
49+
"strconv"
50+
"strings"
51+
"unicode"
52+
)
53+
54+
var mapRuneToByteSign = map[rune]byte{
55+
'+': 0x0a,
56+
'-': 0x0b,
57+
}
58+
59+
var isNegative = map[byte]bool{
60+
0x0a: false,
61+
0x0b: true,
62+
0x0c: false,
63+
0x0d: true,
64+
0x0e: false,
65+
0x0f: false,
66+
}
67+
68+
// Convert string buffer to BCD Packed Decimal.
69+
//
70+
// The number is converted to a BCD packed decimal byte array, right aligned in
71+
// the BCD array, whose length is indicated by the second parameter. The final
72+
// 4-bit nibble in the array will be a sign nibble, 0x0a for "+" and 0x0b for
73+
// "-". Unused bytes and nibbles to the left of the number are set to 0. scale
74+
// is set to the scale of the number (this is the exponent, negated).
75+
func EncodeStringToBCD(buf string) ([]byte, error) {
76+
sign := '+' // By default number is positive.
77+
if res := strings.HasPrefix(buf, "-"); res == true {
78+
sign = '-'
79+
}
80+
81+
// Calculate a number of digits in the decimal number. Leading and
82+
// trialing zeros do not count.
83+
// Examples:
84+
// 0.0000000000000001 - 1 digit
85+
// 00012.34 - 4 digits
86+
// 0.340 - 2 digits
87+
s := strings.ReplaceAll(buf, "-", "") // Remove a sign.
88+
s = strings.ReplaceAll(s, "+", "") // Remove a sign.
89+
s = strings.ReplaceAll(s, ".", "") // Remove a dot.
90+
s = strings.TrimRight(s, "0") // Remove trailing zeros.
91+
s = strings.TrimLeft(s, "0") // Remove leading zeros.
92+
c := len(s)
93+
94+
// The first nibble should contain 0, if the decimal number has an even
95+
// number of digits. Therefore highNibble is false when decimal number
96+
// is even.
97+
highNibble := true
98+
if c%2 == 0 {
99+
highNibble = false
100+
}
101+
fmt.Println("c", c, "highNibble is", highNibble)
102+
scale := 0 // By default decimal number is integer.
103+
var byteBuf []byte
104+
for i, ch := range buf {
105+
// Skip leading zeros.
106+
if (len(byteBuf) == 0) && ch == '0' {
107+
continue
108+
}
109+
if (i == 0) && (ch == '-' || ch == '+') {
110+
continue
111+
}
112+
// Calculate a number of digits after the decimal point.
113+
// TODO: len(s) - strings.IndexAny(s, ".") - 1
114+
if ch == '.' {
115+
scale = len(buf) - i - 1
116+
fmt.Println("Scale", scale)
117+
continue
118+
}
119+
if !unicode.IsDigit(ch) {
120+
return nil, fmt.Errorf("Symbol in position %d is not a digit: %c", i, ch)
121+
}
122+
123+
d, err := strconv.Atoi(string(ch))
124+
if err != nil {
125+
return nil, fmt.Errorf("Failed to convert symbol '%c' to a digit: %s", ch, err)
126+
}
127+
digit := byte(d)
128+
if highNibble == true {
129+
// High nibble.
130+
digit = digit << 4
131+
byteBuf = append(byteBuf, digit)
132+
highNibble = false
133+
} else {
134+
if len(byteBuf) == 0 {
135+
byteBuf = make([]byte, 1)
136+
}
137+
// Low nibble.
138+
lowByteIdx := len(byteBuf) - 1
139+
byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | digit
140+
highNibble = true
141+
}
142+
fmt.Printf("DEBUG: idx == %d, buf %x\n", i, byteBuf)
143+
}
144+
fmt.Printf("DEBUG: Final buf %x\n", byteBuf)
145+
if highNibble == true {
146+
// Put a sign to a high nibble.
147+
byteBuf = append(byteBuf, mapRuneToByteSign[sign])
148+
} else {
149+
// Put a sign to a low nibble.
150+
lowByteIdx := len(byteBuf) - 1
151+
byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | mapRuneToByteSign[sign]
152+
}
153+
byteBuf = append([]byte{byte(scale)}, byteBuf...)
154+
155+
return byteBuf, nil
156+
}
157+
158+
// Convert BCD Packed Decimal to a string buffer.
159+
//
160+
// The BCD packed decimal byte array, together with an associated scale, is
161+
// converted to a string. The BCD array is assumed full of digits, and must be
162+
// ended by a 4-bit sign nibble in the least significant four bits of the final
163+
// byte. The scale is used (negated) as the exponent of the decimal number.
164+
// Note that zeros may have a sign and/or a scale.
165+
func DecodeStringFromBCD(bcdBuf []byte) (string, error) {
166+
const scaleIdx = 0 // Index of a byte with scale.
167+
scale := int(bcdBuf[scaleIdx])
168+
// Get a BCD buffer without a byte with scale.
169+
bcdBuf = bcdBuf[scaleIdx+1:]
170+
length := len(bcdBuf)
171+
var digits []string
172+
for i, bcdByte := range bcdBuf {
173+
highNibble := int(bcdByte >> 4)
174+
if !(len(digits) == 0 && highNibble == 0) || len(bcdBuf) == 1 {
175+
digits = append(digits, strconv.Itoa(highNibble))
176+
}
177+
lowNibble := int(bcdByte & 0x0f)
178+
if !(len(digits) == 0 && lowNibble == 0) && i != length-1 {
179+
digits = append(digits, strconv.Itoa(int(lowNibble)))
180+
}
181+
}
182+
183+
// Add missing zeros when scale is less than current length.
184+
l := len(digits)
185+
if scale >= l {
186+
var zeros []string
187+
for i := 0; i <= scale-l; i++ {
188+
zeros = append(zeros, "0")
189+
digits = append(zeros, digits...)
190+
}
191+
}
192+
193+
// Add a dot when number is fractional.
194+
if scale != 0 {
195+
idx := len(digits) - scale
196+
digits = append(digits, "X") // [1 2 3 X]
197+
copy(digits[idx:], digits[idx-1:]) // [1 2 2 3]
198+
digits[idx] = "." // [1 . 2 3]
199+
}
200+
201+
// Add a sign, it is encoded in a low nibble of a last byte.
202+
lastByte := bcdBuf[length-1]
203+
sign := lastByte & 0x0f
204+
if isNegative[sign] {
205+
digits = append([]string{"-"}, digits...)
206+
}
207+
208+
// Merge slice to a single string.
209+
str := strings.Join(digits, "")
210+
211+
return str, nil
212+
}

decimal/config.lua

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
local decimal = require('decimal')
2+
local msgpack = require('msgpack')
3+
4+
-- Do not set listen for now so connector won't be
5+
-- able to send requests until everything is configured.
6+
box.cfg{
7+
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
8+
}
9+
10+
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
11+
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
12+
13+
local decimal_msgpack_supported = pcall(msgpack.encode, decimal.new(1))
14+
if not decimal_msgpack_supported then
15+
error('Decimal unsupported, use Tarantool 2.2 or newer')
16+
end
17+
18+
local s = box.schema.space.create('testDecimal', {
19+
id = 524,
20+
if_not_exists = true,
21+
})
22+
s:create_index('primary', {
23+
type = 'TREE',
24+
parts = {
25+
{
26+
field = 1,
27+
type = 'decimal',
28+
},
29+
},
30+
if_not_exists = true
31+
})
32+
s:truncate()
33+
34+
box.schema.user.grant('test', 'read,write', 'space', 'testDecimal', { if_not_exists = true })
35+
36+
-- Set listen only when every other thing is configured.
37+
box.cfg{
38+
listen = os.getenv("TEST_TNT_LISTEN"),
39+
}
40+
41+
require('console').start()

decimal/decimal.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Package with support of Tarantool's decimal data type.
2+
//
3+
// Decimal data type supported in Tarantool since 2.2.
4+
//
5+
// Since: 1.6
6+
//
7+
// See also:
8+
//
9+
// * Tarantool MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
10+
//
11+
// * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/
12+
//
13+
// * Tarantool issue for support decimal type https://github.com/tarantool/tarantool/issues/692
14+
package decimal
15+
16+
import (
17+
"fmt"
18+
"reflect"
19+
20+
"github.com/shopspring/decimal"
21+
"gopkg.in/vmihailenco/msgpack.v2"
22+
)
23+
24+
// Decimal external type.
25+
const decimal_extId = 1
26+
27+
func encodeDecimal(e *msgpack.Encoder, v reflect.Value) error {
28+
number := v.Interface().(decimal.Decimal)
29+
strBuf := number.String()
30+
bcdBuf, err := EncodeStringToBCD(strBuf)
31+
if err != nil {
32+
return fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err)
33+
}
34+
if _, err = e.Writer().Write(bcdBuf); err != nil {
35+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
36+
}
37+
38+
return nil
39+
}
40+
41+
func decodeDecimal(d *msgpack.Decoder, v reflect.Value) error {
42+
// On attempt to decode MessagePack buffer c7030100088c (88) msgpack
43+
// pass 00088a to decodeDecimal(). length field (03) is unavailable in
44+
// function body.
45+
// FIXME: works with -12.34 only.
46+
var bytesCount int = 4
47+
b := make([]byte, bytesCount)
48+
49+
_, err := d.Buffered().Read(b)
50+
if err != nil {
51+
return fmt.Errorf("msgpack: can't read bytes on decimal decode: %w", err)
52+
}
53+
digits, err := DecodeStringFromBCD(b)
54+
if err != nil {
55+
return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err)
56+
}
57+
dec, err := decimal.NewFromString(digits)
58+
if err != nil {
59+
return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err)
60+
}
61+
62+
v.Set(reflect.ValueOf(dec))
63+
64+
return nil
65+
}
66+
67+
func init() {
68+
msgpack.Register(reflect.TypeOf((*decimal.Decimal)(nil)).Elem(), encodeDecimal, decodeDecimal)
69+
msgpack.RegisterExt(decimal_extId, (*decimal.Decimal)(nil))
70+
}

0 commit comments

Comments
 (0)