ソースを参照

Merge pull request #120 from relops/decimal_type

Marshaling for decimals
Ben Hood 11 年 前
コミット
29eae46b45
3 ファイル変更162 行追加5 行削除
  1. 11 5
      cassandra_test.go
  2. 90 0
      marshal.go
  3. 61 0
      marshal_test.go

+ 11 - 5
cassandra_test.go

@@ -9,6 +9,7 @@ import (
 	"flag"
 	"reflect"
 	"sort"
+	"speter.net/go/exp/math/dec/inf"
 	"strings"
 	"sync"
 	"testing"
@@ -78,6 +79,7 @@ func TestCRUD(t *testing.T) {
 			views       bigint,
 			protected   boolean,
 			modified    timestamp,
+			rating      decimal,
 			tags        set<varchar>,
 			attachments map<varchar, text>,
 			PRIMARY KEY (title, revid)
@@ -87,10 +89,10 @@ func TestCRUD(t *testing.T) {
 
 	for _, page := range pageTestData {
 		if err := session.Query(`INSERT INTO page
-			(title, revid, body, views, protected, modified, tags, attachments)
-			VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+			(title, revid, body, views, protected, modified, rating, tags, attachments)
+			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
 			page.Title, page.RevId, page.Body, page.Views, page.Protected,
-			page.Modified, page.Tags, page.Attachments).Exec(); err != nil {
+			page.Modified, page.Rating, page.Tags, page.Attachments).Exec(); err != nil {
 			t.Fatal("insert:", err)
 		}
 	}
@@ -106,11 +108,11 @@ func TestCRUD(t *testing.T) {
 	for _, original := range pageTestData {
 		page := new(Page)
 		err := session.Query(`SELECT title, revid, body, views, protected, modified,
-			tags, attachments
+			tags, attachments, rating
 			FROM page WHERE title = ? AND revid = ? LIMIT 1`,
 			original.Title, original.RevId).Scan(&page.Title, &page.RevId,
 			&page.Body, &page.Views, &page.Protected, &page.Modified, &page.Tags,
-			&page.Attachments)
+			&page.Attachments, &page.Rating)
 		if err != nil {
 			t.Error("select page:", err)
 			continue
@@ -301,17 +303,21 @@ type Page struct {
 	Views       int64
 	Protected   bool
 	Modified    time.Time
+	Rating      *inf.Dec
 	Tags        []string
 	Attachments map[string]Attachment
 }
 
 type Attachment []byte
 
+var rating, _ = inf.NewDec(0, 0).SetString("0.131")
+
 var pageTestData = []*Page{
 	&Page{
 		Title:    "Frontpage",
 		RevId:    TimeUUID(),
 		Body:     "Welcome to this wiki page!",
+		Rating:   rating,
 		Modified: time.Date(2013, time.August, 13, 9, 52, 3, 0, time.UTC),
 		Tags:     []string{"start", "important", "test"},
 		Attachments: map[string]Attachment{

+ 90 - 0
marshal.go

@@ -8,10 +8,16 @@ import (
 	"bytes"
 	"fmt"
 	"math"
+	"math/big"
 	"reflect"
+	"speter.net/go/exp/math/dec/inf"
 	"time"
 )
 
+var (
+	bigOne = big.NewInt(1)
+)
+
 // Marshaler is the interface implemented by objects that can marshal
 // themselves into values understood by Cassandra.
 type Marshaler interface {
@@ -43,6 +49,8 @@ func Marshal(info *TypeInfo, value interface{}) ([]byte, error) {
 		return marshalFloat(info, value)
 	case TypeDouble:
 		return marshalDouble(info, value)
+	case TypeDecimal:
+		return marshalDecimal(info, value)
 	case TypeTimestamp:
 		return marshalTimestamp(info, value)
 	case TypeList, TypeSet:
@@ -76,6 +84,8 @@ func Unmarshal(info *TypeInfo, data []byte, value interface{}) error {
 		return unmarshalFloat(info, data, value)
 	case TypeDouble:
 		return unmarshalDouble(info, data, value)
+	case TypeDecimal:
+		return unmarshalDecimal(info, data, value)
 	case TypeTimestamp:
 		return unmarshalTimestamp(info, data, value)
 	case TypeList, TypeSet:
@@ -675,6 +685,86 @@ func unmarshalDouble(info *TypeInfo, data []byte, value interface{}) error {
 	return unmarshalErrorf("can not unmarshal %s into %T", info, value)
 }
 
+func marshalDecimal(info *TypeInfo, value interface{}) ([]byte, error) {
+	switch v := value.(type) {
+	case Marshaler:
+		return v.MarshalCQL(info)
+	case *inf.Dec:
+
+		if v == nil {
+			return nil, nil
+		}
+
+		unscaled := encBigInt2C(v.UnscaledBig())
+		if unscaled == nil {
+			return nil, marshalErrorf("can not marshal %T into %s", value, info)
+		}
+
+		buf := make([]byte, 4+len(unscaled))
+		copy(buf[0:4], encInt(int32(v.Scale())))
+		copy(buf[4:], unscaled)
+		return buf, nil
+	}
+	return nil, marshalErrorf("can not marshal %T into %s", value, info)
+}
+
+func unmarshalDecimal(info *TypeInfo, data []byte, value interface{}) error {
+	switch v := value.(type) {
+	case Unmarshaler:
+		return v.UnmarshalCQL(info, data)
+	case **inf.Dec:
+		if len(data) > 4 {
+			scale := decInt(data[0:4])
+			unscaled := decBigInt2C(data[4:])
+			*v = inf.NewDecBig(unscaled, inf.Scale(scale))
+			return nil
+		} else if len(data) == 0 {
+			*v = nil
+			return nil
+		} else {
+			return unmarshalErrorf("can not unmarshal %s into %T", info, value)
+		}
+	}
+	return unmarshalErrorf("can not unmarshal %s into %T", info, value)
+}
+
+// decBigInt2C sets the value of n to the big-endian two's complement
+// value stored in the given data. If data[0]&80 != 0, the number
+// is negative. If data is empty, the result will be 0.
+func decBigInt2C(data []byte) *big.Int {
+	n := new(big.Int).SetBytes(data)
+	if len(data) > 0 && data[0]&0x80 > 0 {
+		n.Sub(n, new(big.Int).Lsh(bigOne, uint(len(data))*8))
+	}
+	return n
+}
+
+// encBigInt2C returns the big-endian two's complement
+// form of n.
+func encBigInt2C(n *big.Int) []byte {
+	switch n.Sign() {
+	case 0:
+		return []byte{0}
+	case 1:
+		b := n.Bytes()
+		if b[0]&0x80 > 0 {
+			b = append([]byte{0}, b...)
+		}
+		return b
+	case -1:
+		length := uint(n.BitLen()/8+1) * 8
+		b := new(big.Int).Add(n, new(big.Int).Lsh(bigOne, length)).Bytes()
+		// When the most significant bit is on a byte
+		// boundary, we can get some extra significant
+		// bits, so strip them off when that happens.
+		if len(b) >= 2 && b[0] == 0xff && b[1]&0x80 != 0 {
+			b = b[1:]
+		}
+		return b
+	}
+	return nil
+}
+
 func marshalTimestamp(info *TypeInfo, value interface{}) ([]byte, error) {
 	switch v := value.(type) {
 	case Marshaler:

+ 61 - 0
marshal_test.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"math"
 	"reflect"
+	"speter.net/go/exp/math/dec/inf"
 	"strings"
 	"testing"
 	"time"
@@ -117,6 +118,61 @@ var marshalTests = []struct {
 		[]byte("\x40\x09\x21\xfb\x53\xc8\xd4\xf1"),
 		float64(3.14159265),
 	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x00\x00"),
+		inf.NewDec(0, 0),
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x00\x64"),
+		inf.NewDec(100, 0),
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x02\x19"),
+		decimalize("0.25"),
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x13\xD5\a;\x20\x14\xA2\x91"),
+		decimalize("-0.0012095473475870063"), // From the iconara/cql-rb test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x13*\xF8\xC4\xDF\xEB]o"),
+		decimalize("0.0012095473475870063"), // From the iconara/cql-rb test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x12\xF2\xD8\x02\xB6R\x7F\x99\xEE\x98#\x99\xA9V"),
+		decimalize("-1042342234234.123423435647768234"), // From the iconara/cql-rb test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\r\nJ\x04\"^\x91\x04\x8a\xb1\x18\xfe"),
+		decimalize("1243878957943.1234124191998"), // From the datastax/python-driver test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x06\xe5\xde]\x98Y"),
+		decimalize("-112233.441191"), // From the datastax/python-driver test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x14\x00\xfa\xce"),
+		decimalize("0.00000000000000064206"), // From the datastax/python-driver test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\x00\x00\x00\x14\xff\x052"),
+		decimalize("-0.00000000000000064206"), // From the datastax/python-driver test suite
+	},
+	{
+		&TypeInfo{Type: TypeDecimal},
+		[]byte("\xff\xff\xff\x9c\x00\xfa\xce"),
+		inf.NewDec(64206, -100), // From the datastax/python-driver test suite
+	},
 	{
 		&TypeInfo{Type: TypeTimestamp},
 		[]byte("\x00\x00\x01\x40\x77\x16\xe1\xb8"),
@@ -186,6 +242,11 @@ var marshalTests = []struct {
 	},
 }
 
+func decimalize(s string) *inf.Dec {
+	i, _ := new(inf.Dec).SetString(s)
+	return i
+}
+
 func TestMarshal(t *testing.T) {
 	for i, test := range marshalTests {
 		data, err := Marshal(test.Info, test.Value)