├── .gitignore ├── doc.go ├── go.mod ├── codecs ├── nil.go ├── encode_test.go ├── size_test.go ├── decode_test.go ├── errors.go ├── bool.go ├── int8.go ├── uint8.go ├── int16.go ├── uint16.go ├── int32.go ├── int64.go ├── uint32.go ├── uint64.go ├── float32.go ├── float64.go ├── string.go ├── nil_test.go ├── bool_test.go ├── encode.go ├── decode.go ├── uint8_test.go ├── uint16_test.go ├── uint64_test.go ├── uint32_test.go ├── time_test.go ├── int8_test.go ├── int16_test.go ├── int64_test.go ├── float32_test.go ├── float64_test.go ├── int32_test.go ├── string_test.go ├── size.go └── time.go ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── go.sum ├── example_test.go ├── bingo.go ├── bench_test.go ├── schema.go ├── bytes ├── invert_test.go └── invert.go ├── bingo_test.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bingo packs values into composite keys, using a byte encoding that 3 | preserves the sort order of the original values. 4 | 5 | Many of the encoding formats are the same as those used in HBase's OrderedBytes 6 | class, which were in turn modeled after encoding formats defined by SQLite4 and 7 | Orderly. 8 | */ 9 | package bingo 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iancmcc/bingo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/goccy/go-reflect v1.2.0 7 | github.com/smartystreets/goconvey v1.7.2 8 | ) 9 | 10 | require ( 11 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 12 | github.com/jtolds/gls v4.20.0+incompatible // indirect 13 | github.com/smartystreets/assertions v1.2.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /codecs/nil.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | const ( 4 | typeByteNil = 0x05 5 | typeByteNilInverse = typeByteNil ^ 0xff 6 | sizeNil = 1 7 | ) 8 | 9 | func encodeNil(b []byte, inverse bool) (int, error) { 10 | if cap(b) < sizeNil { 11 | return 0, ErrByteSliceSize 12 | } 13 | b = b[:1] 14 | if inverse { 15 | b[0] = typeByteNilInverse 16 | } else { 17 | b[0] = typeByteNil 18 | } 19 | return sizeNil, nil 20 | } 21 | -------------------------------------------------------------------------------- /codecs/encode_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/iancmcc/bingo/codecs" 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestEncode(t *testing.T) { 11 | Convey("encoding a value", t, func() { 12 | Convey("should return an error if a type is unknown", func() { 13 | m := map[string]int{} 14 | 15 | _, err := EncodeValue([]byte{}, m, false) 16 | So(err, ShouldEqual, ErrUnknownType) 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.22 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v -coverprofile=profile.cov ./... 26 | 27 | - name: Send coverage 28 | uses: shogo82148/actions-goveralls@v1.5.1 29 | 30 | -------------------------------------------------------------------------------- /codecs/size_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/iancmcc/bingo/codecs" 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestSize(t *testing.T) { 11 | Convey("getting the encoded size of a value", t, func() { 12 | Convey("should return an error if a type is unknown", func() { 13 | m := map[string]int{} 14 | _, err := EncodedSize(m) 15 | So(err, ShouldEqual, ErrUnknownType) 16 | }) 17 | }) 18 | 19 | Convey("returning the size of the next field in a byte array", t, func() { 20 | Convey("should return an error if a type is unknown", func() { 21 | b := []byte{0xff, 0, 0, 0, 0, 0} 22 | _, err := SizeNext(b) 23 | So(err, ShouldEqual, ErrUnknownType) 24 | }) 25 | }) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /codecs/decode_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | 8 | . "github.com/iancmcc/bingo/codecs" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestDecode(t *testing.T) { 13 | 14 | Convey("decoding a value", t, func() { 15 | 16 | var a int32 = int32(rand.Int63n(math.MaxInt32 - 1)) 17 | b := make([]byte, 16) 18 | EncodeValue(b, a, false) 19 | 20 | Convey("should fail if a non-pointer is passed as a target", func() { 21 | var v int32 22 | _, err := DecodeValue(b, v) 23 | So(err, ShouldEqual, ErrInvalidTarget) 24 | }) 25 | 26 | Convey("should fail if an unknown type is passed", func() { 27 | b[0] = 0xFF 28 | var v int32 29 | _, err := DecodeValue(b, &v) 30 | So(err, ShouldEqual, ErrUnknownType) 31 | 32 | }) 33 | 34 | }) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /codecs/errors.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrInvalidTarget is returned when the target is not a pointer. 7 | ErrInvalidTarget = errors.New("must pass a pointer to decode a value to") 8 | // ErrUnknownType is returned when the type is unknown. 9 | ErrUnknownType = errors.New("unknown type") 10 | // ErrByteSliceSize is returned when the byte slice provided doesn't have 11 | // enough capacity to hold the encoded value. 12 | ErrByteSliceSize = errors.New("receiving byte array doesn't have enough capacity") 13 | // ErrNullByte is returned when a string to be encoded contains a null byte. 14 | ErrNullByte = errors.New("can't encode strings that contain a null byte") 15 | // ErrInvalidTime is returned when an encoded time can't be marshaled to time.Time. 16 | ErrInvalidTime = errors.New("can't marshal to time.Time") 17 | ) 18 | -------------------------------------------------------------------------------- /codecs/bool.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/goccy/go-reflect" 7 | "github.com/iancmcc/bingo/bytes" 8 | ) 9 | 10 | const ( 11 | typeByteBool byte = 0x04 12 | typeByteBoolInverse = typeByteBool ^ 0xff 13 | sizeBool = 2 14 | ) 15 | 16 | func encodeBool(b []byte, v bool, inverse bool) (int, error) { 17 | if cap(b) < sizeBool { 18 | return 0, ErrByteSliceSize 19 | } 20 | b = b[:sizeBool] 21 | b[0] = typeByteBool 22 | if v { 23 | b[1] = 0xff 24 | } 25 | if inverse { 26 | bytes.InvertArraySmall(b) 27 | } 28 | return sizeBool, nil 29 | } 30 | 31 | func decodeBool(b []byte, v reflect.Value) (int, error) { 32 | val := b[1] == 0xff 33 | if b[0] == typeByteBoolInverse { 34 | val = !val 35 | } 36 | ptr := v.Pointer() 37 | **(**bool)(unsafe.Pointer(&ptr)) = *(*bool)(unsafe.Pointer(&val)) 38 | return sizeBool, nil 39 | } 40 | -------------------------------------------------------------------------------- /codecs/int8.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/goccy/go-reflect" 7 | "github.com/iancmcc/bingo/bytes" 8 | ) 9 | 10 | const ( 11 | typeByteInt8 byte = 0x29 12 | typeByteInt8Inverse = typeByteInt8 ^ 0xff 13 | sizeInt8 = int(unsafe.Sizeof(int8(0))) + 1 14 | ) 15 | 16 | func encodeInt8(b []byte, v int8, inverse bool) (int, error) { 17 | if cap(b) < sizeInt8 { 18 | return 0, ErrByteSliceSize 19 | } 20 | b = b[:sizeInt8] 21 | b[0] = typeByteInt8 22 | b[1] = byte(uint8(v) ^ 1<<7) 23 | if inverse { 24 | bytes.InvertArraySmall(b) 25 | } 26 | return sizeInt8, nil 27 | } 28 | 29 | func decodeInt8(b []byte, v reflect.Value) (int, error) { 30 | encoded := b[1] 31 | if b[0] == typeByteInt8Inverse { 32 | encoded = bytes.InvertByte(encoded) 33 | } 34 | val := int8((encoded ^ 0x80) & 0xff) 35 | ptr := v.Pointer() 36 | **(**int8)(unsafe.Pointer(&ptr)) = *(*int8)(unsafe.Pointer(&val)) 37 | return sizeInt8, nil 38 | } 39 | -------------------------------------------------------------------------------- /codecs/uint8.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/goccy/go-reflect" 7 | "github.com/iancmcc/bingo/bytes" 8 | ) 9 | 10 | const ( 11 | typeByteUint8 byte = 0x19 12 | typeByteUint8Inverse = typeByteUint8 ^ 0xff 13 | sizeUint8 = int(unsafe.Sizeof(uint8(0))) + 1 14 | ) 15 | 16 | func encodeUint8(b []byte, v uint8, inverse bool) (int, error) { 17 | if cap(b) < sizeUint8 { 18 | return 0, ErrByteSliceSize 19 | } 20 | b = b[:sizeUint8] 21 | b[0] = typeByteUint8 22 | b[1] = byte(uint8(v) ^ 1<<7) 23 | if inverse { 24 | bytes.InvertArraySmall(b) 25 | } 26 | return sizeUint8, nil 27 | } 28 | 29 | func decodeUint8(b []byte, v reflect.Value) (int, error) { 30 | encoded := b[1] 31 | if b[0] == typeByteUint8Inverse { 32 | encoded = bytes.InvertByte(encoded) 33 | } 34 | val := uint8((encoded ^ 0x80) & 0xff) 35 | ptr := v.Pointer() 36 | **(**uint8)(unsafe.Pointer(&ptr)) = *(*uint8)(unsafe.Pointer(&val)) 37 | return sizeUint8, nil 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Ian McCracken 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /codecs/int16.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteInt16 byte = 0x2a 13 | typeByteInt16Inverse = typeByteInt16 ^ 0xff 14 | sizeInt16 = int(unsafe.Sizeof(int16(0))) + 1 15 | ) 16 | 17 | func encodeInt16(b []byte, v int16, inverse bool) (int, error) { 18 | if cap(b) < sizeInt16 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeInt16] 22 | b[0] = typeByteInt16 23 | binary.BigEndian.PutUint16(b[1:], uint16(v)^(1<<15)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeInt16, nil 28 | } 29 | 30 | func decodeInt16(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:3] 32 | if b[0] == typeByteInt16Inverse { 33 | encoded = make([]byte, 2) 34 | copy(encoded, b[1:3]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := int16((encoded[0] ^ 0x80) & 0xff) 38 | val = (val << 8) + int16(encoded[1]&0xff) 39 | 40 | ptr := v.Pointer() 41 | **(**int16)(unsafe.Pointer(&ptr)) = *(*int16)(unsafe.Pointer(&val)) 42 | return sizeInt16, nil 43 | } 44 | -------------------------------------------------------------------------------- /codecs/uint16.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteUint16 byte = 0x1a 13 | typeByteUint16Inverse = typeByteUint16 ^ 0xff 14 | sizeUint16 = int(unsafe.Sizeof(uint16(0))) + 1 15 | ) 16 | 17 | func encodeUint16(b []byte, v uint16, inverse bool) (int, error) { 18 | if cap(b) < sizeUint16 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeUint16] 22 | b[0] = typeByteUint16 23 | binary.BigEndian.PutUint16(b[1:], uint16(v)^(1<<15)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeUint16, nil 28 | } 29 | 30 | func decodeUint16(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:3] 32 | if b[0] == typeByteUint16Inverse { 33 | encoded = make([]byte, 2) 34 | copy(encoded, b[1:3]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := uint16((encoded[0] ^ 0x80) & 0xff) 38 | val = (val << 8) + uint16(encoded[1]&0xff) 39 | 40 | ptr := v.Pointer() 41 | **(**uint16)(unsafe.Pointer(&ptr)) = *(*uint16)(unsafe.Pointer(&val)) 42 | return sizeUint16, nil 43 | } 44 | -------------------------------------------------------------------------------- /codecs/int32.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteInt32 byte = 0x2b 13 | typeByteInt32Inverse = typeByteInt32 ^ 0xff 14 | sizeInt32 = int(unsafe.Sizeof(int32(0))) + 1 15 | ) 16 | 17 | func encodeInt32(b []byte, v int32, inverse bool) (int, error) { 18 | if cap(b) < sizeInt32 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeInt32] 22 | b[0] = typeByteInt32 23 | binary.BigEndian.PutUint32(b[1:], uint32(v)^(1<<31)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeInt32, nil 28 | } 29 | 30 | func decodeInt32(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:5] 32 | if b[0] == typeByteInt32Inverse { 33 | encoded = make([]byte, 4) 34 | copy(encoded, b[1:5]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := int32((encoded[0] ^ 0x80) & 0xff) 38 | for i := 1; i < 4; i++ { 39 | val = (val << 8) + int32(encoded[i]&0xff) 40 | } 41 | uptr := v.Pointer() 42 | **(**int32)(unsafe.Pointer(&uptr)) = *(*int32)(unsafe.Pointer(&val)) 43 | return sizeInt32, nil 44 | } 45 | -------------------------------------------------------------------------------- /codecs/int64.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteInt64 byte = 0x2c 13 | typeByteInt64Inverse = typeByteInt64 ^ 0xff 14 | sizeInt64 = int(unsafe.Sizeof(int64(0))) + 1 15 | ) 16 | 17 | func encodeInt64(b []byte, v int64, inverse bool) (int, error) { 18 | if cap(b) < sizeInt64 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeInt64] 22 | b[0] = typeByteInt64 23 | binary.BigEndian.PutUint64(b[1:], uint64(v)^(1<<63)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeInt64, nil 28 | } 29 | 30 | func decodeInt64(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:9] 32 | if b[0] == typeByteInt64Inverse { 33 | encoded = make([]byte, 8) 34 | copy(encoded, b[1:9]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := int64((encoded[0] ^ 0x80) & 0xff) 38 | for i := 1; i < 8; i++ { 39 | val = (val << 8) + int64(encoded[i]&0xff) 40 | } 41 | ptr := v.Pointer() 42 | **(**int64)(unsafe.Pointer(&ptr)) = *(*int64)(unsafe.Pointer(&val)) 43 | return sizeInt64, nil 44 | } 45 | -------------------------------------------------------------------------------- /codecs/uint32.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteUint32 byte = 0x1b 13 | typeByteUint32Inverse = typeByteUint32 ^ 0xff 14 | sizeUint32 = int(unsafe.Sizeof(uint32(0))) + 1 15 | ) 16 | 17 | func encodeUint32(b []byte, v uint32, inverse bool) (int, error) { 18 | if cap(b) < sizeUint32 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeUint32] 22 | b[0] = typeByteUint32 23 | binary.BigEndian.PutUint32(b[1:], uint32(v)^(1<<31)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeUint32, nil 28 | } 29 | 30 | func decodeUint32(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:5] 32 | if b[0] == typeByteUint32Inverse { 33 | encoded = make([]byte, 4) 34 | copy(encoded, b[1:5]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := uint32((encoded[0] ^ 0x80) & 0xff) 38 | for i := 1; i < 4; i++ { 39 | val = (val << 8) + uint32(encoded[i]&0xff) 40 | } 41 | uptr := v.Pointer() 42 | **(**uint32)(unsafe.Pointer(&uptr)) = *(*uint32)(unsafe.Pointer(&val)) 43 | return sizeUint32, nil 44 | } 45 | -------------------------------------------------------------------------------- /codecs/uint64.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteUint64 byte = 0x1c 13 | typeByteUint64Inverse = typeByteUint64 ^ 0xff 14 | sizeUint64 = int(unsafe.Sizeof(uint64(0))) + 1 15 | ) 16 | 17 | func encodeUint64(b []byte, v uint64, inverse bool) (int, error) { 18 | if cap(b) < sizeUint64 { 19 | return 0, ErrByteSliceSize 20 | } 21 | b = b[:sizeUint64] 22 | b[0] = typeByteUint64 23 | binary.BigEndian.PutUint64(b[1:], uint64(v)^(1<<63)) 24 | if inverse { 25 | bytes.InvertArraySmall(b) 26 | } 27 | return sizeUint64, nil 28 | } 29 | 30 | func decodeUint64(b []byte, v reflect.Value) (int, error) { 31 | encoded := b[1:9] 32 | if b[0] == typeByteUint64Inverse { 33 | encoded = make([]byte, 8) 34 | copy(encoded, b[1:9]) 35 | bytes.InvertArraySmall(encoded) 36 | } 37 | val := uint64((encoded[0] ^ 0x80) & 0xff) 38 | for i := 1; i < 8; i++ { 39 | val = (val << 8) + uint64(encoded[i]&0xff) 40 | } 41 | ptr := v.Pointer() 42 | **(**uint64)(unsafe.Pointer(&ptr)) = *(*uint64)(unsafe.Pointer(&val)) 43 | return sizeUint64, nil 44 | } 45 | -------------------------------------------------------------------------------- /codecs/float32.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | "unsafe" 7 | 8 | "github.com/goccy/go-reflect" 9 | "github.com/iancmcc/bingo/bytes" 10 | ) 11 | 12 | const ( 13 | typeByteFloat32 byte = 0x30 14 | typeByteFloat32Inverse = typeByteFloat32 ^ 0xff 15 | sizeFloat32 = int(unsafe.Sizeof(float32(0))) + 1 16 | ) 17 | 18 | func encodeFloat32(b []byte, v float32, inverse bool) (int, error) { 19 | if cap(b) < sizeFloat32 { 20 | return 0, ErrByteSliceSize 21 | } 22 | b = b[:sizeFloat32] 23 | b[0] = typeByteFloat32 24 | int32Val := int32(math.Float32bits(v)) 25 | int32Val ^= (int32Val >> 31) | (-1 << 31) 26 | binary.BigEndian.PutUint32(b[1:], uint32(int32Val)) 27 | if inverse { 28 | bytes.InvertArraySmall(b) 29 | } 30 | return sizeFloat32, nil 31 | } 32 | 33 | func decodeFloat32(b []byte, v reflect.Value) (int, error) { 34 | encoded := b[1:5] 35 | if b[0] == typeByteFloat32Inverse { 36 | encoded = make([]byte, 4) 37 | copy(encoded, b[1:5]) 38 | bytes.InvertArraySmall(encoded) 39 | } 40 | val := int32(binary.BigEndian.Uint32(encoded)) 41 | val ^= (^val >> 31) | (-1 << 31) 42 | fv := math.Float32frombits(uint32(val)) 43 | ptr := v.Pointer() 44 | **(**float32)(unsafe.Pointer(&ptr)) = *(*float32)(unsafe.Pointer(&fv)) 45 | return sizeFloat32, nil 46 | } 47 | -------------------------------------------------------------------------------- /codecs/float64.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | "unsafe" 7 | 8 | "github.com/goccy/go-reflect" 9 | "github.com/iancmcc/bingo/bytes" 10 | ) 11 | 12 | const ( 13 | typeByteFloat64 byte = 0x31 14 | typeByteFloat64Inverse = typeByteFloat64 ^ 0xff 15 | sizeFloat64 = int(unsafe.Sizeof(float64(0))) + 1 16 | ) 17 | 18 | func encodeFloat64(b []byte, v float64, inverse bool) (int, error) { 19 | if cap(b) < sizeFloat64 { 20 | return 0, ErrByteSliceSize 21 | } 22 | b = b[:sizeFloat64] 23 | int64Val := int64(math.Float64bits(v)) 24 | int64Val ^= (int64Val >> 63) | (-1 << 63) 25 | b[0] = typeByteFloat64 26 | binary.BigEndian.PutUint64(b[1:], uint64(int64Val)) 27 | if inverse { 28 | bytes.InvertArraySmall(b) 29 | } 30 | return sizeFloat64, nil 31 | } 32 | 33 | func decodeFloat64(b []byte, v reflect.Value) (int, error) { 34 | encoded := b[1:9] 35 | if b[0] == typeByteFloat64Inverse { 36 | encoded = make([]byte, 8) 37 | copy(encoded, b[1:9]) 38 | bytes.InvertArraySmall(encoded) 39 | } 40 | val := int64(binary.BigEndian.Uint64(encoded)) 41 | val ^= (^val >> 63) | (-1 << 63) 42 | fv := math.Float64frombits(uint64(val)) 43 | ptr := v.Pointer() 44 | **(**float64)(unsafe.Pointer(&ptr)) = *(*float64)(unsafe.Pointer(&fv)) 45 | return sizeFloat64, nil 46 | } 47 | -------------------------------------------------------------------------------- /codecs/string.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "strings" 5 | "unsafe" 6 | 7 | "github.com/goccy/go-reflect" 8 | "github.com/iancmcc/bingo/bytes" 9 | ) 10 | 11 | const ( 12 | typeByteString byte = 0x34 13 | terminatorByte byte = 0x00 14 | typeByteStringInverse = typeByteString ^ 0xff 15 | terminatorByteInverse = terminatorByte ^ 0xff 16 | ) 17 | 18 | func encodeString(b []byte, v string, inverse bool) (int, error) { 19 | size := len(v) + 2 20 | if cap(b) < size { 21 | return 0, ErrByteSliceSize 22 | } 23 | // Apparently an upfront check + copy is faster than checking during range 24 | if strings.IndexByte(v, terminatorByte) > -1 { 25 | return 0, ErrNullByte 26 | } 27 | b = b[:size] 28 | b[0] = typeByteString 29 | copy(b[1:len(v)+1], v) 30 | b[len(v)+1] = terminatorByte 31 | if inverse { 32 | bytes.InvertArray(b) 33 | } 34 | return size, nil 35 | } 36 | 37 | func decodeString(b []byte, v reflect.Value) (int, error) { 38 | var ( 39 | encoded []byte 40 | ) 41 | idx, err := SizeNext(b) 42 | if err != nil { 43 | return 0, err 44 | } 45 | encoded = b[1 : idx-1] 46 | 47 | if b[0] == typeByteStringInverse { 48 | bytes.InvertArray(encoded) 49 | } 50 | 51 | ptr := v.Pointer() 52 | **(**string)(unsafe.Pointer(&ptr)) = *(*string)(unsafe.Pointer(&encoded)) 53 | 54 | return idx, nil 55 | } 56 | -------------------------------------------------------------------------------- /codecs/nil_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/iancmcc/bingo/codecs" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestNil(t *testing.T) { 12 | 13 | Convey("nil value", t, func() { 14 | 15 | var a interface{} 16 | expectedSize, err := EncodedSize(a) 17 | So(err, ShouldBeNil) 18 | 19 | for _, inverse := range []bool{true, false} { 20 | b := make([]byte, expectedSize, expectedSize) 21 | 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | So(err, ShouldBeNil) 27 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 28 | 29 | n, err := EncodeValue(b, a, inverse) 30 | So(n, ShouldEqual, expectedSize) 31 | So(err, ShouldBeNil) 32 | nn, err := SizeNext(b) 33 | So(nn, ShouldEqual, expectedSize) 34 | So(err, ShouldBeNil) 35 | 36 | Convey("and decoded into a nil pointer", func() { 37 | var v interface{} 38 | n, err := DecodeValue(b, &v) 39 | So(n, ShouldEqual, expectedSize) 40 | So(err, ShouldBeNil) 41 | So(a, ShouldBeNil) 42 | }) 43 | 44 | }) 45 | } 46 | 47 | Convey("returns an error when byte array is insufficient", func() { 48 | var v interface{} 49 | b := make([]byte, expectedSize-1) 50 | _, err := EncodeValue(b, v, false) 51 | So(err, ShouldEqual, ErrByteSliceSize) 52 | }) 53 | }) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /codecs/bool_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/iancmcc/bingo/codecs" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestBool(t *testing.T) { 12 | 13 | for _, a := range []bool{false, true} { 14 | desc := "false" 15 | if a { 16 | desc = "true" 17 | } 18 | Convey(fmt.Sprintf("a %s bool", desc), t, func() { 19 | expectedSize, err := EncodedSize(a) 20 | So(err, ShouldBeNil) 21 | for _, inverse := range []bool{false, true} { 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 27 | b := make([]byte, expectedSize, expectedSize) 28 | n, err := EncodeValue(b, a, inverse) 29 | So(n, ShouldEqual, expectedSize) 30 | So(err, ShouldBeNil) 31 | nn, err := SizeNext(b) 32 | So(nn, ShouldEqual, expectedSize) 33 | So(err, ShouldBeNil) 34 | 35 | Convey("and decoded into a bool pointer", func() { 36 | var v bool 37 | n, err := DecodeValue(b, &v) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | So(v, ShouldEqual, a) 41 | }) 42 | }) 43 | } 44 | Convey("throws an error when encoded into an insufficient array", func() { 45 | b := make([]byte, expectedSize-1) 46 | _, err := EncodeValue(b, a, false) 47 | So(err, ShouldEqual, ErrByteSliceSize) 48 | }) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms= 2 | github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE= 3 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 4 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 5 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 6 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 7 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 8 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 9 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 10 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 15 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 16 | -------------------------------------------------------------------------------- /codecs/encode.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "time" 5 | "unsafe" 6 | ) 7 | 8 | const intsize = unsafe.Sizeof(int(0)) 9 | 10 | // EncodeValue encodes value v to the beginning of byte slice b, optionally in 11 | // inverse order, and returns the number of bytes written. 12 | func EncodeValue(b []byte, v interface{}, inverse bool) (int, error) { 13 | if v == nil { 14 | return encodeNil(b, inverse) 15 | } 16 | switch c := v.(type) { 17 | case bool: 18 | return encodeBool(b, c, inverse) 19 | case uint8: 20 | return encodeUint8(b, c, inverse) 21 | case uint16: 22 | return encodeUint16(b, c, inverse) 23 | case uint: 24 | if intsize == 4 { 25 | return encodeUint32(b, uint32(c), inverse) 26 | } 27 | return encodeUint64(b, uint64(c), inverse) 28 | case uint32: 29 | return encodeUint32(b, c, inverse) 30 | case uint64: 31 | return encodeUint64(b, c, inverse) 32 | case int8: 33 | return encodeInt8(b, c, inverse) 34 | case int16: 35 | return encodeInt16(b, c, inverse) 36 | case int: 37 | if intsize == 4 { 38 | return encodeInt32(b, int32(c), inverse) 39 | } 40 | return encodeInt64(b, int64(c), inverse) 41 | case int32: 42 | return encodeInt32(b, c, inverse) 43 | case int64: 44 | return encodeInt64(b, c, inverse) 45 | case float32: 46 | return encodeFloat32(b, c, inverse) 47 | case float64: 48 | return encodeFloat64(b, c, inverse) 49 | case string: 50 | return encodeString(b, c, inverse) 51 | case time.Time: 52 | return encodeTime(b, c, inverse) 53 | default: 54 | return 0, ErrUnknownType 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /codecs/decode.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "github.com/goccy/go-reflect" 5 | ) 6 | 7 | // DecodeValue decodes the first value in slice b into the location defined by 8 | // pointer v. 9 | func DecodeValue(b []byte, v interface{}) (int, error) { 10 | rv := reflect.ValueNoEscapeOf(v) 11 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 12 | return 0, ErrInvalidTarget 13 | } 14 | switch b[0] { 15 | case typeByteNil, typeByteNilInverse: 16 | return 1, nil 17 | case typeByteBool, typeByteBoolInverse: 18 | return decodeBool(b, rv) 19 | case typeByteUint8, typeByteUint8Inverse: 20 | return decodeUint8(b, rv) 21 | case typeByteUint16, typeByteUint16Inverse: 22 | return decodeUint16(b, rv) 23 | case typeByteUint32, typeByteUint32Inverse: 24 | return decodeUint32(b, rv) 25 | case typeByteUint64, typeByteUint64Inverse: 26 | return decodeUint64(b, rv) 27 | case typeByteInt8, typeByteInt8Inverse: 28 | return decodeInt8(b, rv) 29 | case typeByteInt16, typeByteInt16Inverse: 30 | return decodeInt16(b, rv) 31 | case typeByteInt32, typeByteInt32Inverse: 32 | return decodeInt32(b, rv) 33 | case typeByteInt64, typeByteInt64Inverse: 34 | return decodeInt64(b, rv) 35 | case typeByteFloat32, typeByteFloat32Inverse: 36 | return decodeFloat32(b, rv) 37 | case typeByteFloat64, typeByteFloat64Inverse: 38 | return decodeFloat64(b, rv) 39 | case typeByteTime, typeByteTimeInverse: 40 | return decodeTime(b, rv) 41 | case typeByteString, typeByteStringInverse: 42 | return decodeString(b, rv) 43 | default: 44 | return 0, ErrUnknownType 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /codecs/uint8_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestUint8(t *testing.T) { 15 | Convey("a uint8", t, func() { 16 | var a uint8 = uint8(rand.Int63n(math.MaxUint8 - 1)) 17 | expectedSize, err := EncodedSize(a) 18 | So(err, ShouldBeNil) 19 | for _, inverse := range []bool{false, true} { 20 | invdesc := "natural" 21 | if inverse { 22 | invdesc = "inverse" 23 | } 24 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 25 | 26 | b := make([]byte, expectedSize, expectedSize) 27 | n, err := EncodeValue(b, a, inverse) 28 | So(n, ShouldEqual, expectedSize) 29 | So(err, ShouldBeNil) 30 | nn, err := SizeNext(b) 31 | So(nn, ShouldEqual, expectedSize) 32 | So(err, ShouldBeNil) 33 | 34 | Convey("and decoded into a uint8 pointer", func() { 35 | var v uint8 36 | n, err := DecodeValue(b, &v) 37 | 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | So(v, ShouldEqual, a) 41 | }) 42 | Convey("and maintain lexicographical order", func() { 43 | c := make([]byte, expectedSize, expectedSize) 44 | var addend uint8 = 1 45 | if inverse { 46 | EncodeValue(c, a-addend, inverse) 47 | } else { 48 | EncodeValue(c, a+addend, inverse) 49 | } 50 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 51 | }) 52 | }) 53 | } 54 | Convey("throws an error when encoded into an insufficient array", func() { 55 | b := make([]byte, expectedSize-1) 56 | _, err := EncodeValue(b, a, false) 57 | So(err, ShouldEqual, ErrByteSliceSize) 58 | }) 59 | }) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /codecs/uint16_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestUint16(t *testing.T) { 15 | 16 | Convey("a uuint16", t, func() { 17 | 18 | var a uint16 = uint16(rand.Int63n(math.MaxUint16 - 1)) 19 | expectedSize, err := EncodedSize(a) 20 | So(err, ShouldBeNil) 21 | for _, inverse := range []bool{false, true} { 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 27 | 28 | b := make([]byte, expectedSize, expectedSize) 29 | n, err := EncodeValue(b, a, inverse) 30 | So(n, ShouldEqual, expectedSize) 31 | So(err, ShouldBeNil) 32 | nn, err := SizeNext(b) 33 | So(nn, ShouldEqual, expectedSize) 34 | So(err, ShouldBeNil) 35 | 36 | Convey("and decoded into an uint16 pointer", func() { 37 | var v uint16 38 | n, err := DecodeValue(b, &v) 39 | 40 | So(n, ShouldEqual, expectedSize) 41 | So(err, ShouldBeNil) 42 | So(v, ShouldEqual, a) 43 | }) 44 | Convey("and maintain lexicographical order", func() { 45 | c := make([]byte, expectedSize, expectedSize) 46 | var addend uint16 = 1 47 | if inverse { 48 | EncodeValue(c, a-addend, inverse) 49 | } else { 50 | EncodeValue(c, a+addend, inverse) 51 | } 52 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 53 | }) 54 | }) 55 | } 56 | Convey("throws an error when encoded into an insufficient array", func() { 57 | b := make([]byte, expectedSize-1) 58 | _, err := EncodeValue(b, a, false) 59 | So(err, ShouldEqual, ErrByteSliceSize) 60 | }) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /codecs/uint64_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestUint64(t *testing.T) { 15 | 16 | Convey("a uint64", t, func() { 17 | 18 | var a uint64 = uint64(rand.Int63n(math.MaxInt64 - 1)) 19 | expectedSize, err := EncodedSize(a) 20 | So(err, ShouldBeNil) 21 | for _, inverse := range []bool{false, true} { 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 27 | 28 | b := make([]byte, expectedSize, expectedSize) 29 | n, err := EncodeValue(b, a, inverse) 30 | So(n, ShouldEqual, expectedSize) 31 | So(err, ShouldBeNil) 32 | nn, err := SizeNext(b) 33 | So(nn, ShouldEqual, expectedSize) 34 | So(err, ShouldBeNil) 35 | 36 | Convey("and decoded into an uint64 pointer", func() { 37 | var v uint64 38 | n, err := DecodeValue(b, &v) 39 | 40 | So(n, ShouldEqual, expectedSize) 41 | So(err, ShouldBeNil) 42 | So(v, ShouldEqual, a) 43 | }) 44 | 45 | Convey("and maintain lexicographical order", func() { 46 | c := make([]byte, expectedSize, expectedSize) 47 | var addend uint64 = 1 48 | if inverse { 49 | EncodeValue(c, a-addend, inverse) 50 | } else { 51 | EncodeValue(c, a+addend, inverse) 52 | } 53 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 54 | }) 55 | }) 56 | } 57 | Convey("throws an error when encoded into an insufficient array", func() { 58 | b := make([]byte, expectedSize-1) 59 | _, err := EncodeValue(b, a, false) 60 | So(err, ShouldEqual, ErrByteSliceSize) 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /codecs/uint32_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestUint32(t *testing.T) { 15 | 16 | Convey("a uint32", t, func() { 17 | 18 | var a uint32 = uint32(rand.Int63n(math.MaxUint32 - 1)) 19 | expectedSize, err := EncodedSize(a) 20 | So(err, ShouldBeNil) 21 | for _, inverse := range []bool{false, true} { 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | So(err, ShouldBeNil) 27 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 28 | 29 | b := make([]byte, expectedSize, expectedSize) 30 | n, err := EncodeValue(b, a, inverse) 31 | So(n, ShouldEqual, expectedSize) 32 | So(err, ShouldBeNil) 33 | nn, err := SizeNext(b) 34 | So(nn, ShouldEqual, expectedSize) 35 | So(err, ShouldBeNil) 36 | 37 | Convey("and decoded into an uint32 pointer", func() { 38 | var v uint32 39 | n, err := DecodeValue(b, &v) 40 | 41 | So(n, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | So(v, ShouldEqual, a) 44 | }) 45 | 46 | Convey("and maintain lexicographical order", func() { 47 | c := make([]byte, expectedSize, expectedSize) 48 | var addend uint32 = 1 49 | if inverse { 50 | EncodeValue(c, a-addend, inverse) 51 | } else { 52 | EncodeValue(c, a+addend, inverse) 53 | } 54 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 55 | }) 56 | }) 57 | } 58 | Convey("throws an error when encoded into an insufficient array", func() { 59 | b := make([]byte, expectedSize-1) 60 | _, err := EncodeValue(b, a, false) 61 | So(err, ShouldEqual, ErrByteSliceSize) 62 | }) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /codecs/time_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestTime(t *testing.T) { 15 | 16 | Convey("time value", t, func() { 17 | 18 | var a = RandomTime() 19 | expectedSize, err := EncodedSize(a) 20 | So(err, ShouldBeNil) 21 | 22 | for _, inverse := range []bool{true, false} { 23 | invdesc := "natural" 24 | if inverse { 25 | invdesc = "inverse" 26 | } 27 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 28 | 29 | b := make([]byte, expectedSize, expectedSize) 30 | n, err := EncodeValue(b, a, inverse) 31 | So(n, ShouldEqual, expectedSize) 32 | So(err, ShouldBeNil) 33 | nn, err := SizeNext(b) 34 | So(nn, ShouldEqual, expectedSize) 35 | So(err, ShouldBeNil) 36 | 37 | Convey("and decoded into a nil pointer", func() { 38 | var v time.Time 39 | n, err := DecodeValue(b, &v) 40 | So(n, ShouldEqual, expectedSize) 41 | So(err, ShouldBeNil) 42 | So(v, ShouldEqual, a) 43 | }) 44 | 45 | Convey("and maintain lexicographical order", func() { 46 | c := make([]byte, expectedSize, expectedSize) 47 | var addend = time.Second 48 | if inverse { 49 | addend = -addend 50 | } 51 | EncodeValue(c, a.Add(addend), inverse) 52 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 53 | }) 54 | 55 | }) 56 | } 57 | 58 | Convey("returns an error when byte array is insufficient", func() { 59 | b := make([]byte, expectedSize-1) 60 | _, err := EncodeValue(b, a, false) 61 | So(err, ShouldEqual, ErrByteSliceSize) 62 | }) 63 | 64 | }) 65 | 66 | } 67 | 68 | func RandomTime() time.Time { 69 | return time.Time(time.Unix(int64(rand.Int31()), int64(rand.Int31()))) 70 | } 71 | -------------------------------------------------------------------------------- /codecs/int8_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestInt8(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s int8", negdesc), t, func() { 22 | 23 | var a int8 = int8(rand.Int63n(math.MaxInt8 - 1)) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 35 | 36 | b := make([]byte, expectedSize, expectedSize) 37 | n, err := EncodeValue(b, a, inverse) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | nn, err := SizeNext(b) 41 | So(nn, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | 44 | Convey("and decoded into an int8 pointer", func() { 45 | var v int8 46 | n, err := DecodeValue(b, &v) 47 | 48 | So(n, ShouldEqual, expectedSize) 49 | So(err, ShouldBeNil) 50 | So(v, ShouldEqual, a) 51 | }) 52 | Convey("and maintain lexicographical order", func() { 53 | c := make([]byte, expectedSize, expectedSize) 54 | var addend int8 = 1 55 | if inverse { 56 | addend *= -1 57 | } 58 | EncodeValue(c, a+addend, inverse) 59 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 60 | }) 61 | }) 62 | } 63 | Convey("throws an error when encoded into an insufficient array", func() { 64 | b := make([]byte, expectedSize-1) 65 | _, err := EncodeValue(b, a, false) 66 | So(err, ShouldEqual, ErrByteSliceSize) 67 | }) 68 | }) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /codecs/int16_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestInt16(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s int16", negdesc), t, func() { 22 | 23 | var a int16 = int16(rand.Int63n(math.MaxInt16 - 1)) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 35 | 36 | b := make([]byte, expectedSize, expectedSize) 37 | n, err := EncodeValue(b, a, inverse) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | nn, err := SizeNext(b) 41 | So(nn, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | 44 | Convey("and decoded into an int16 pointer", func() { 45 | var v int16 46 | n, err := DecodeValue(b, &v) 47 | 48 | So(n, ShouldEqual, expectedSize) 49 | So(err, ShouldBeNil) 50 | So(v, ShouldEqual, a) 51 | }) 52 | Convey("and maintain lexicographical order", func() { 53 | c := make([]byte, expectedSize, expectedSize) 54 | var addend int16 = 1 55 | if inverse { 56 | addend *= -1 57 | } 58 | EncodeValue(c, a+addend, inverse) 59 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 60 | }) 61 | }) 62 | } 63 | Convey("throws an error when encoded into an insufficient array", func() { 64 | b := make([]byte, expectedSize-1) 65 | _, err := EncodeValue(b, a, false) 66 | So(err, ShouldEqual, ErrByteSliceSize) 67 | }) 68 | }) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /codecs/int64_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestInt64(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s int64", negdesc), t, func() { 22 | 23 | var a int64 = int64(rand.Int63n(math.MaxInt64 - 1)) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 35 | 36 | b := make([]byte, expectedSize, expectedSize) 37 | n, err := EncodeValue(b, a, inverse) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | nn, err := SizeNext(b) 41 | So(nn, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | 44 | Convey("and decoded into an int64 pointer", func() { 45 | var v int64 46 | n, err := DecodeValue(b, &v) 47 | 48 | So(n, ShouldEqual, expectedSize) 49 | So(err, ShouldBeNil) 50 | So(v, ShouldEqual, a) 51 | }) 52 | 53 | Convey("and maintain lexicographical order", func() { 54 | c := make([]byte, expectedSize, expectedSize) 55 | var addend int64 = 1 56 | if inverse { 57 | addend *= -1 58 | } 59 | EncodeValue(c, a+addend, inverse) 60 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 61 | }) 62 | }) 63 | } 64 | Convey("throws an error when encoded into an insufficient array", func() { 65 | b := make([]byte, expectedSize-1) 66 | _, err := EncodeValue(b, a, false) 67 | So(err, ShouldEqual, ErrByteSliceSize) 68 | }) 69 | }) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /codecs/float32_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestFloat32(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s float32", negdesc), t, func() { 22 | 23 | var a float32 = float32(rand.Float32() * math.MaxInt16) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 35 | 36 | b := make([]byte, expectedSize, expectedSize) 37 | n, err := EncodeValue(b, a, inverse) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | nn, err := SizeNext(b) 41 | So(nn, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | 44 | Convey("and decoded into a float32 pointer", func() { 45 | var v float32 46 | n, err := DecodeValue(b, &v) 47 | 48 | So(n, ShouldEqual, expectedSize) 49 | So(err, ShouldBeNil) 50 | So(v, ShouldEqual, a) 51 | }) 52 | Convey("and maintain lexicographical order", func() { 53 | c := make([]byte, expectedSize, expectedSize) 54 | var addend float32 = 1 55 | if inverse { 56 | addend *= -1 57 | } 58 | EncodeValue(c, a+addend, inverse) 59 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 60 | }) 61 | }) 62 | } 63 | Convey("throws an error when encoded into an insufficient array", func() { 64 | b := make([]byte, expectedSize-1) 65 | _, err := EncodeValue(b, a, false) 66 | So(err, ShouldEqual, ErrByteSliceSize) 67 | }) 68 | }) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /codecs/float64_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestFloat64(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s float64", negdesc), t, func() { 22 | 23 | var a float64 = float64(rand.Float64() * math.MaxInt32) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 35 | 36 | b := make([]byte, expectedSize, expectedSize) 37 | n, err := EncodeValue(b, a, inverse) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | nn, err := SizeNext(b) 41 | So(nn, ShouldEqual, expectedSize) 42 | So(err, ShouldBeNil) 43 | 44 | Convey("and decoded into a float64 pointer", func() { 45 | var v float64 46 | n, err := DecodeValue(b, &v) 47 | 48 | So(n, ShouldEqual, expectedSize) 49 | So(err, ShouldBeNil) 50 | So(v, ShouldEqual, a) 51 | }) 52 | Convey("and maintain lexicographical order", func() { 53 | c := make([]byte, expectedSize, expectedSize) 54 | var addend float64 = 1 55 | if inverse { 56 | addend *= -1 57 | } 58 | EncodeValue(c, a+addend, inverse) 59 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 60 | }) 61 | }) 62 | } 63 | Convey("throws an error when encoded into an insufficient array", func() { 64 | b := make([]byte, expectedSize-1) 65 | _, err := EncodeValue(b, a, false) 66 | So(err, ShouldEqual, ErrByteSliceSize) 67 | }) 68 | }) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /codecs/int32_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "testing" 9 | 10 | . "github.com/iancmcc/bingo/codecs" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestInt32(t *testing.T) { 15 | 16 | for _, negative := range []bool{false, true} { 17 | negdesc := "positive" 18 | if negative { 19 | negdesc = "negative" 20 | } 21 | Convey(fmt.Sprintf("a %s int32", negdesc), t, func() { 22 | 23 | var a int32 = int32(rand.Int63n(math.MaxInt32 - 1)) 24 | if negative { 25 | a = -a 26 | } 27 | expectedSize, err := EncodedSize(a) 28 | So(err, ShouldBeNil) 29 | for _, inverse := range []bool{false, true} { 30 | invdesc := "natural" 31 | if inverse { 32 | invdesc = "inverse" 33 | } 34 | So(err, ShouldBeNil) 35 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 36 | 37 | b := make([]byte, expectedSize, expectedSize) 38 | n, err := EncodeValue(b, a, inverse) 39 | So(n, ShouldEqual, expectedSize) 40 | So(err, ShouldBeNil) 41 | nn, err := SizeNext(b) 42 | So(nn, ShouldEqual, expectedSize) 43 | So(err, ShouldBeNil) 44 | 45 | Convey("and decoded into an int32 pointer", func() { 46 | var v int32 47 | n, err := DecodeValue(b, &v) 48 | 49 | So(n, ShouldEqual, expectedSize) 50 | So(err, ShouldBeNil) 51 | So(v, ShouldEqual, a) 52 | }) 53 | 54 | Convey("and maintain lexicographical order", func() { 55 | c := make([]byte, expectedSize, expectedSize) 56 | var addend int32 = 1 57 | if inverse { 58 | addend *= -1 59 | } 60 | EncodeValue(c, a+addend, inverse) 61 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 62 | }) 63 | }) 64 | } 65 | Convey("throws an error when encoded into an insufficient array", func() { 66 | b := make([]byte, expectedSize-1) 67 | _, err := EncodeValue(b, a, false) 68 | So(err, ShouldEqual, ErrByteSliceSize) 69 | }) 70 | }) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package bingo_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "time" 8 | 9 | "github.com/iancmcc/bingo" 10 | ) 11 | 12 | func ExamplePack() { 13 | // Pack some values into keys 14 | a, _ := bingo.Pack(1, "b", float32(4.2)) 15 | b, _ := bingo.Pack(1, "b", float32(3.1)) 16 | c, _ := bingo.Pack(1, "a", float32(7.5)) 17 | 18 | // Sort the resulting byte slices 19 | packed := [][]byte{a, b, c} 20 | sort.SliceStable(packed, func(p, q int) bool { 21 | return bytes.Compare(packed[p], packed[q]) < 0 22 | }) 23 | 24 | // Unpack again and witness the glorious order 25 | var ( 26 | first int 27 | second string 28 | third float32 29 | ) 30 | for _, bs := range packed { 31 | bingo.Unpack(bs, &first, &second, &third) 32 | fmt.Println(first, second, third) 33 | } 34 | // Output: 1 a 7.5 35 | // 1 b 3.1 36 | // 1 b 4.2 37 | } 38 | 39 | func ExampleWithDesc() { 40 | // Create a schema that packs the second value in descending order 41 | schema := bingo.WithDesc(false, true, false) 42 | 43 | // Pack some values into keys 44 | a, _ := schema.Pack(1, "b", float32(4.2)) 45 | b, _ := schema.Pack(1, "b", float32(3.1)) 46 | c, _ := schema.Pack(1, "a", float32(7.5)) 47 | 48 | // Sort the resulting byte slices 49 | packed := [][]byte{a, b, c} 50 | sort.SliceStable(packed, func(p, q int) bool { 51 | return bytes.Compare(packed[p], packed[q]) < 0 52 | }) 53 | 54 | // Unpack to see the order 55 | var ( 56 | first int 57 | second string 58 | third float32 59 | ) 60 | for _, bs := range packed { 61 | bingo.Unpack(bs, &first, &second, &third) 62 | fmt.Println(first, second, third) 63 | } 64 | // Output: 1 b 3.1 65 | // 1 b 4.2 66 | // 1 a 7.5 67 | } 68 | 69 | func ExamplePackTo() { 70 | // Pack requires an allocation for the resulting byte array, but you can 71 | // also pack to a byte array you already have 72 | 73 | values := []interface{}{1, "a", time.Now()} 74 | 75 | // Get the size you'll need 76 | size, _ := bingo.PackedSize(values) 77 | dest := make([]byte, size) 78 | 79 | bingo.PackTo(dest, values...) 80 | } 81 | 82 | func ExampleWritePackedTo() { 83 | var buf bytes.Buffer 84 | 85 | bingo.WritePackedTo(&buf, 1, "a") 86 | 87 | var ( 88 | first int 89 | second string 90 | ) 91 | bingo.Unpack(buf.Bytes(), &first, &second) 92 | 93 | fmt.Println(first, second) 94 | // Output: 1 a 95 | } 96 | -------------------------------------------------------------------------------- /codecs/string_test.go: -------------------------------------------------------------------------------- 1 | package codecs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | 9 | . "github.com/iancmcc/bingo/codecs" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestString(t *testing.T) { 14 | 15 | Convey("a string value", t, func() { 16 | 17 | var a = RandomString(rand.Intn(1024)) 18 | expectedSize, err := EncodedSize(a) 19 | So(err, ShouldBeNil) 20 | 21 | for _, inverse := range []bool{true, false} { 22 | invdesc := "natural" 23 | if inverse { 24 | invdesc = "inverse" 25 | } 26 | Convey(fmt.Sprintf("can be encoded in %s order into a sufficient byte array", invdesc), func() { 27 | b := make([]byte, expectedSize, expectedSize) 28 | n, err := EncodeValue(b, a, inverse) 29 | So(n, ShouldEqual, expectedSize) 30 | So(err, ShouldBeNil) 31 | nn, err := SizeNext(b) 32 | So(nn, ShouldEqual, expectedSize) 33 | So(err, ShouldBeNil) 34 | 35 | Convey("and decoded into a string pointer", func() { 36 | var v string 37 | n, err := DecodeValue(b, &v) 38 | So(n, ShouldEqual, expectedSize) 39 | So(err, ShouldBeNil) 40 | So(a, ShouldEqual, a) 41 | }) 42 | 43 | Convey("and maintain lexicographical order", func() { 44 | c := make([]byte, expectedSize+1, expectedSize+1) 45 | var addend string = "a" 46 | EncodeValue(c, a+addend, inverse) 47 | if inverse { 48 | So(bytes.Compare(c, b), ShouldBeLessThan, 0) 49 | } else { 50 | So(bytes.Compare(b, c), ShouldBeLessThan, 0) 51 | } 52 | }) 53 | 54 | }) 55 | Convey(fmt.Sprintf("returns an error when the %s-order string contains a null byte", invdesc), func() { 56 | b := make([]byte, expectedSize+1, expectedSize+1) 57 | c := []byte(a) 58 | c[rand.Intn(len(c))] = 0 59 | d := string(c) 60 | _, err := EncodeValue(b, d, inverse) 61 | So(err, ShouldEqual, ErrNullByte) 62 | }) 63 | } 64 | 65 | Convey("returns an error when byte array is insufficient", func() { 66 | b := make([]byte, expectedSize-1) 67 | _, err := EncodeValue(b, a, false) 68 | So(err, ShouldEqual, ErrByteSliceSize) 69 | }) 70 | }) 71 | 72 | } 73 | 74 | func RandomString(n int) string { 75 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 76 | 77 | s := make([]rune, n) 78 | for i := range s { 79 | s[i] = letters[rand.Intn(len(letters))] 80 | } 81 | return string(s) 82 | } 83 | -------------------------------------------------------------------------------- /codecs/size.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | ) 7 | 8 | // EncodedSize returns the number of bytes required to encode the value v. 9 | func EncodedSize(v interface{}) (int, error) { 10 | if v == nil { 11 | return 1, nil 12 | } 13 | switch t := v.(type) { 14 | case bool: 15 | return sizeBool, nil 16 | case uint8: 17 | return sizeUint8, nil 18 | case int8: 19 | return sizeInt8, nil 20 | case uint16: 21 | return sizeUint16, nil 22 | case int16: 23 | return sizeInt16, nil 24 | case int: 25 | if intsize == 4 { 26 | return sizeInt32, nil 27 | } 28 | return sizeInt64, nil 29 | case uint: 30 | if intsize == 4 { 31 | return sizeUint32, nil 32 | } 33 | return sizeUint64, nil 34 | case uint32: 35 | return sizeUint32, nil 36 | case int32: 37 | return sizeInt32, nil 38 | case uint64: 39 | return sizeUint64, nil 40 | case int64: 41 | return sizeInt64, nil 42 | case float32: 43 | return sizeFloat32, nil 44 | case float64: 45 | return sizeFloat64, nil 46 | case time.Time: 47 | return sizeTime, nil 48 | case string: 49 | return len(t) + 2, nil 50 | default: 51 | return 0, ErrUnknownType 52 | } 53 | } 54 | 55 | // SizeNext returns the number of bytes encompassing the next encoded value in 56 | // the byte slice. 57 | func SizeNext(b []byte) (int, error) { 58 | switch b[0] { 59 | case typeByteBool, typeByteBoolInverse: 60 | return sizeBool, nil 61 | case typeByteNil, typeByteNilInverse: 62 | return sizeNil, nil 63 | case typeByteUint8, typeByteUint8Inverse: 64 | return sizeUint8, nil 65 | case typeByteUint16, typeByteUint16Inverse: 66 | return sizeUint16, nil 67 | case typeByteUint32, typeByteUint32Inverse: 68 | return sizeUint32, nil 69 | case typeByteUint64, typeByteUint64Inverse: 70 | return sizeUint64, nil 71 | case typeByteInt8, typeByteInt8Inverse: 72 | return sizeInt8, nil 73 | case typeByteInt16, typeByteInt16Inverse: 74 | return sizeInt16, nil 75 | case typeByteInt32, typeByteInt32Inverse: 76 | return sizeInt32, nil 77 | case typeByteInt64, typeByteInt64Inverse: 78 | return sizeInt64, nil 79 | case typeByteFloat32, typeByteFloat32Inverse: 80 | return sizeFloat32, nil 81 | case typeByteFloat64, typeByteFloat64Inverse: 82 | return sizeFloat64, nil 83 | case typeByteTime, typeByteTimeInverse: 84 | return sizeTime, nil 85 | case typeByteString: 86 | return bytes.IndexByte(b, terminatorByte) + 1, nil 87 | case typeByteStringInverse: 88 | return bytes.IndexByte(b, terminatorByteInverse) + 1, nil 89 | default: 90 | return 0, ErrUnknownType 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /bingo.go: -------------------------------------------------------------------------------- 1 | package bingo 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/iancmcc/bingo/codecs" 8 | ) 9 | 10 | const defaultSchema Schema = 0 11 | 12 | // Pack encodes the values passed, returning the resulting byte slice. 13 | // This requires 1 heap alloc for the return value. 14 | func Pack(vals ...interface{}) ([]byte, error) { 15 | return defaultSchema.pack(vals) 16 | } 17 | 18 | // MustPack encodes the values passed, returning the resulting byte slice. 19 | // Panics on error. 20 | func MustPack(vals ...interface{}) []byte { 21 | return defaultSchema.mustPack(vals) 22 | } 23 | 24 | // PackTo encodes the values passed into the provided byte slice, returning the 25 | // number of bytes written. 26 | func PackTo(b []byte, vals ...interface{}) (n int, err error) { 27 | return defaultSchema.packTo(b, vals) 28 | } 29 | 30 | // MustPackTo encodes the values passed into the provided byte slice, returning the 31 | // number of bytes written. Panics on error. 32 | func MustPackTo(b []byte, vals ...interface{}) int { 33 | return defaultSchema.mustPackTo(b, vals) 34 | } 35 | 36 | // WritePackedTo encodes the values passed and writes the result to the 37 | // io.Writer specified. Note: this requires 1 heap alloc for the intermediate 38 | // byte array. 39 | func WritePackedTo(w io.Writer, vals ...interface{}) (n int, err error) { 40 | return defaultSchema.writePackedTo(w, vals) 41 | } 42 | 43 | // Unpack unpacks b into the targets provided. 44 | func Unpack(b []byte, dests ...interface{}) error { 45 | for _, dest := range dests { 46 | var ( 47 | n int 48 | err error 49 | ) 50 | if dest == nil { 51 | // Skip this one 52 | n, err = codecs.SizeNext(b) 53 | } else { 54 | n, err = codecs.DecodeValue(b, dest) 55 | } 56 | if err != nil { 57 | return err 58 | } 59 | b = b[n:] 60 | } 61 | return nil 62 | } 63 | 64 | // UnpackIndex unpacks the idx'th value from b into the target provided. 65 | func UnpackIndex(b []byte, idx int, dest interface{}) error { 66 | var n int 67 | for i := 0; i < idx; i++ { 68 | if n >= len(b) { 69 | return fmt.Errorf("No data at index %d", idx) 70 | } 71 | nn, err := codecs.SizeNext(b[n:]) 72 | if err != nil { 73 | return err 74 | } 75 | n += nn 76 | } 77 | return Unpack(b[n:], dest) 78 | } 79 | 80 | // PackedSize returns the number of bytes required to pack the values passed 81 | func PackedSize(vals []interface{}) (int, error) { 82 | var size int 83 | for _, v := range vals { 84 | n, err := codecs.EncodedSize(v) 85 | if err != nil { 86 | return 0, err 87 | } 88 | size += n 89 | } 90 | return size, nil 91 | } 92 | -------------------------------------------------------------------------------- /codecs/time.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | "unsafe" 7 | 8 | "github.com/goccy/go-reflect" 9 | "github.com/iancmcc/bingo/bytes" 10 | ) 11 | 12 | const ( 13 | typeByteTime byte = 0x35 14 | typeByteTimeInverse = typeByteTime ^ 0xff 15 | sizeTime = 16 16 | ) 17 | 18 | func encodeTime(b []byte, v time.Time, inverse bool) (int, error) { 19 | if cap(b) < sizeTime { 20 | return 0, ErrByteSliceSize 21 | } 22 | b = b[:sizeTime] 23 | b[0] = typeByteTime 24 | if err := timeMarshalBinary(b[1:], v); err != nil { 25 | return 0, ErrInvalidTime 26 | } 27 | if inverse { 28 | bytes.InvertArraySmall(b) 29 | } 30 | return sizeTime, nil 31 | } 32 | 33 | func decodeTime(b []byte, v reflect.Value) (int, error) { 34 | encoded := b[1:sizeTime] 35 | if b[0] == typeByteTimeInverse { 36 | encoded = make([]byte, 15) 37 | copy(encoded, b[1:16]) 38 | bytes.InvertArraySmall(encoded) 39 | } 40 | var t time.Time 41 | err := t.UnmarshalBinary(encoded) 42 | if err != nil { 43 | return 0, err 44 | } 45 | ptr := v.Pointer() 46 | **(**time.Time)(unsafe.Pointer(&ptr)) = *(*time.Time)(unsafe.Pointer(&t)) 47 | return sizeTime, nil 48 | } 49 | 50 | //go:linkname time_sec time.(*Time).sec 51 | //go:noescape 52 | func time_sec(*time.Time) int64 53 | 54 | //go:linkname time_nsec time.(*Time).nsec 55 | //go:noescape 56 | func time_nsec(*time.Time) int32 57 | 58 | const timeBinaryVersion byte = 1 59 | 60 | func timeMarshalBinary(b []byte, t time.Time) error { 61 | var offsetMin int16 // minutes east of UTC. -1 is UTC. 62 | 63 | if t.Location() == time.UTC { 64 | offsetMin = -1 65 | } else { 66 | _, offset := t.Zone() 67 | if offset%60 != 0 { 68 | return errors.New("Time.MarshalBinary: zone offset has fractional minute") 69 | } 70 | offset /= 60 71 | if offset < -32768 || offset == -1 || offset > 32767 { 72 | return errors.New("Time.MarshalBinary: unexpected zone offset") 73 | } 74 | offsetMin = int16(offset) 75 | } 76 | 77 | sec := time_sec(&t) 78 | nsec := time_nsec(&t) 79 | 80 | b[0] = timeBinaryVersion // byte 0 : version 81 | b[1] = byte(sec >> 56) // bytes 1-8: seconds 82 | b[2] = byte(sec >> 48) 83 | b[3] = byte(sec >> 40) 84 | b[4] = byte(sec >> 32) 85 | b[5] = byte(sec >> 24) 86 | b[6] = byte(sec >> 16) 87 | b[7] = byte(sec >> 8) 88 | b[8] = byte(sec) 89 | b[9] = byte(nsec >> 24) // bytes 9-12: nanoseconds 90 | b[10] = byte(nsec >> 16) 91 | b[11] = byte(nsec >> 8) 92 | b[12] = byte(nsec) 93 | b[13] = byte(offsetMin >> 8) // bytes 13-14: zone offset in minutes 94 | b[14] = byte(offsetMin) 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package bingo_test 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | 9 | . "github.com/iancmcc/bingo" 10 | ) 11 | 12 | func BenchmarkCodecs(b *testing.B) { 13 | fns := []struct { 14 | name string 15 | v interface{} 16 | }{ 17 | { 18 | name: "int8", 19 | v: int8(rand.Intn(math.MaxInt8)), 20 | }, { 21 | name: "int16", 22 | v: int16(rand.Intn(math.MaxInt16)), 23 | }, { 24 | name: "int32", 25 | v: int32(rand.Intn(math.MaxInt32)), 26 | }, { 27 | name: "int64", 28 | v: int64(rand.Intn(math.MaxInt64)), 29 | }, { 30 | name: "float32", 31 | v: float32(rand.Float32() * math.MaxInt16), 32 | }, { 33 | name: "float64", 34 | v: float64(rand.Float64() * math.MaxInt32), 35 | }, { 36 | name: "string", 37 | v: randomString(256), 38 | }, { 39 | name: "time", 40 | v: time.Now(), 41 | }, 42 | } 43 | b.Run("packing", func(b *testing.B) { 44 | now := time.Now() 45 | s := "a, b, c" 46 | vals := []interface{}{1, 2, 3, s, now, 3.141592653} 47 | b.Run("Pack", func(b *testing.B) { 48 | b.ReportAllocs() 49 | b.ResetTimer() 50 | for j := 0; j < b.N; j++ { 51 | Pack(vals...) 52 | } 53 | }) 54 | b.Run("PackTo", func(b *testing.B) { 55 | size, _ := PackedSize(vals) 56 | buf := make([]byte, size, size) 57 | b.ReportAllocs() 58 | b.ResetTimer() 59 | for j := 0; j < b.N; j++ { 60 | PackTo(buf, vals...) 61 | //PackTo(buf, 1, 2, 3, s, now, 3.141592653) 62 | } 63 | }) 64 | }) 65 | for _, fn := range fns { 66 | b.Run(fn.name, func(b *testing.B) { 67 | buf := make([]byte, 1024, 1024) 68 | invbuf := make([]byte, 1024, 1024) 69 | b.Run("encode", func(b *testing.B) { 70 | b.Run("natural", func(b *testing.B) { 71 | b.ReportAllocs() 72 | b.ResetTimer() 73 | for j := 0; j < b.N; j++ { 74 | PackTo(buf, fn.v) 75 | } 76 | }) 77 | b.Run("inverse", func(b *testing.B) { 78 | b.ReportAllocs() 79 | b.ResetTimer() 80 | for j := 0; j < b.N; j++ { 81 | WithDesc(true).PackTo(invbuf, fn.v) 82 | } 83 | }) 84 | }) 85 | b.Run("decode", func(b *testing.B) { 86 | b.Run("natural", func(b *testing.B) { 87 | b.ReportAllocs() 88 | b.ResetTimer() 89 | for j := 0; j < b.N; j++ { 90 | Unpack(buf, &(fn.v)) 91 | } 92 | }) 93 | b.Run("inverse", func(b *testing.B) { 94 | b.ReportAllocs() 95 | b.ResetTimer() 96 | for j := 0; j < b.N; j++ { 97 | Unpack(invbuf, &(fn.v)) 98 | } 99 | }) 100 | }) 101 | }) 102 | } 103 | } 104 | 105 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 106 | 107 | //go:noinline 108 | func randomString(n int) string { 109 | s := make([]rune, n) 110 | for i := range s { 111 | s[i] = letters[rand.Intn(len(letters))] 112 | } 113 | return string(s) 114 | } 115 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package bingo 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/iancmcc/bingo/codecs" 7 | ) 8 | 9 | type ( 10 | // Schema captures whether fields of a key should be encoded in inverse or 11 | // natural order. 12 | Schema uint64 13 | ) 14 | 15 | // WithDesc returns a Schema that will produce packed keys with the indicated 16 | // values encoded to sort in descending order. 17 | func WithDesc(cols ...bool) Schema { 18 | var s Schema 19 | for i, t := range cols { 20 | if t { 21 | s |= (1 << i) 22 | } 23 | } 24 | return s 25 | } 26 | 27 | // Pack encodes the values passed, returning the resulting byte slice. 28 | func (s Schema) Pack(vals ...interface{}) ([]byte, error) { 29 | return s.pack(vals) 30 | } 31 | 32 | // MustPack encodes the values passed, returning the resulting byte slice and 33 | // panicking on error. 34 | func (s Schema) MustPack(vals ...interface{}) []byte { 35 | result, err := s.pack(vals) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return result 40 | } 41 | 42 | // PackTo encodes the values passed into the provided byte slice, returning 43 | // the number of bytes written. 44 | func (s Schema) PackTo(b []byte, vals ...interface{}) (n int, err error) { 45 | return s.packTo(b, vals) 46 | } 47 | 48 | // MustPackTo encodes the values passed into the provided byte slice, returning 49 | // the number of bytes written. Panics on error. 50 | func (s Schema) MustPackTo(b []byte, vals ...interface{}) int { 51 | result, err := s.packTo(b, vals) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return result 56 | } 57 | 58 | // WritePackedTo encodes the values passed and writes the result to the 59 | // io.Writer specified. Note: this requires 1 heap alloc for the intermediate 60 | // byte array. 61 | func (s Schema) WritePackedTo(w io.Writer, vals ...interface{}) (n int, err error) { 62 | return s.writePackedTo(w, vals) 63 | } 64 | 65 | func (s Schema) pack(vals []interface{}) ([]byte, error) { 66 | size, err := PackedSize(vals) 67 | if err != nil { 68 | return nil, err 69 | } 70 | buf := make([]byte, size, size) 71 | _, err = s.packTo(buf, vals) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return buf, nil 76 | } 77 | 78 | func (s Schema) mustPack(vals []interface{}) []byte { 79 | result, err := s.pack(vals) 80 | if err != nil { 81 | panic(err) 82 | } 83 | return result 84 | } 85 | 86 | func (s Schema) packTo(b []byte, vals []interface{}) (n int, err error) { 87 | for i, v := range vals { 88 | desc := s&(1< 0 89 | m, err := codecs.EncodeValue(b[n:], v, desc) 90 | if err != nil { 91 | return n, err 92 | } 93 | n += m 94 | } 95 | return 96 | } 97 | 98 | func (s Schema) mustPackTo(b []byte, vals []interface{}) int { 99 | result, err := s.packTo(b, vals) 100 | if err != nil { 101 | panic(err) 102 | } 103 | return result 104 | } 105 | 106 | func (s Schema) writePackedTo(w io.Writer, vals []interface{}) (n int, err error) { 107 | b, err := s.pack(vals) 108 | if err != nil { 109 | return 0, err 110 | } 111 | return w.Write(b) 112 | } 113 | -------------------------------------------------------------------------------- /bytes/invert_test.go: -------------------------------------------------------------------------------- 1 | package bytes_test 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | 9 | . "github.com/iancmcc/bingo/bytes" 10 | ) 11 | 12 | type inverter func([]byte) 13 | 14 | func testInverter(name string, f inverter, t *testing.T) { 15 | a := make([]byte, 2) 16 | b := make([]byte, 2) 17 | rand.Read(a) 18 | for i := range a { 19 | b[i] = a[i] ^ 0xff 20 | } 21 | f(a) 22 | if !bytes.Equal(a, b) { 23 | t.Fatalf("%s failed to invert properly", name) 24 | } 25 | } 26 | 27 | func TestInvertByte(t *testing.T) { 28 | a := rand.Intn(256) 29 | b := InvertByte(byte(a)) 30 | if b^0xff != byte(a) { 31 | t.Fatalf("InvertByte failed to invert properly") 32 | } 33 | } 34 | 35 | func TestInvertSmall(t *testing.T) { 36 | testInverter("InvertArraySmall", InvertArraySmall, t) 37 | } 38 | 39 | func TestInvertLarge(t *testing.T) { 40 | testInverter("InvertArrayLarge", InvertArrayLarge, t) 41 | } 42 | 43 | func TestInvertOptimized(t *testing.T) { 44 | testInverter("InvertArray", InvertArray, t) 45 | } 46 | 47 | var sizes = []int{ 48 | 2, 49 | 3, 50 | 4, 51 | 8, 52 | 16, 53 | 32, 54 | 64, 55 | 128, 56 | 512, 57 | 1024, 58 | 4096, 59 | 16384, 60 | } 61 | 62 | func TestInvert(t *testing.T) { 63 | fns := []struct { 64 | name string 65 | fn inverter 66 | }{ 67 | { 68 | name: "small", 69 | fn: InvertArraySmall, 70 | }, 71 | { 72 | name: "large", 73 | fn: InvertArrayLarge, 74 | }, 75 | { 76 | name: "default", 77 | fn: InvertArray, 78 | }, 79 | } 80 | 81 | for _, size := range sizes { 82 | p := make([]byte, size) 83 | rand.Read(p) 84 | cmp := make([]byte, size) 85 | for i := range p { 86 | cmp[i] = p[i] ^ 0xff 87 | } 88 | t.Run(strconv.Itoa(size), func(t *testing.T) { 89 | for _, fn := range fns { 90 | t.Run(fn.name, func(t *testing.T) { 91 | cp := make([]byte, size) 92 | copy(cp, p) 93 | fn.fn(cp) 94 | if !bytes.Equal(cp, cmp) { 95 | t.Fatalf("%v != %v", cp, cmp) 96 | } 97 | }) 98 | } 99 | }) 100 | } 101 | 102 | } 103 | 104 | func BenchmarkInvert(b *testing.B) { 105 | fns := []struct { 106 | name string 107 | fn func(b *testing.B, a []byte) 108 | }{ 109 | { 110 | name: "default", 111 | fn: func(b *testing.B, a []byte) { 112 | for i := 0; i < b.N; i++ { 113 | InvertArray(a) 114 | } 115 | }, 116 | }, 117 | { 118 | name: "small", 119 | fn: func(b *testing.B, a []byte) { 120 | for i := 0; i < b.N; i++ { 121 | InvertArraySmall(a) 122 | } 123 | }, 124 | }, 125 | { 126 | name: "large", 127 | fn: func(b *testing.B, a []byte) { 128 | for i := 0; i < b.N; i++ { 129 | InvertArrayLarge(a) 130 | } 131 | }, 132 | }, 133 | } 134 | 135 | for _, size := range sizes { 136 | p := make([]byte, size) 137 | rand.Read(p) 138 | b.Run(strconv.Itoa(size), func(b *testing.B) { 139 | for _, fn := range fns { 140 | b.Run(fn.name, func(b *testing.B) { 141 | b.ReportAllocs() 142 | b.SetBytes(int64(size)) 143 | b.ResetTimer() 144 | fn.fn(b, p) 145 | }) 146 | } 147 | }) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /bingo_test.go: -------------------------------------------------------------------------------- 1 | package bingo_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | . "github.com/iancmcc/bingo" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestBingo(t *testing.T) { 13 | 14 | Convey("Bingo", t, func() { 15 | 16 | Convey("should pack values while preserving order", func() { 17 | a, err := Pack(1, "hi", int64(67)) 18 | So(err, ShouldBeNil) 19 | b, err := Pack(1, "hi 1", int64(67)) 20 | So(err, ShouldBeNil) 21 | So(bytes.Compare(a, b), ShouldEqual, -1) 22 | }) 23 | 24 | Convey("should pack values panicking on invalid type", func() { 25 | So(func() { MustPack(1, "hi", int64(67)) }, ShouldNotPanic) 26 | So(func() { MustPack(1, struct{}{}, int64(67)) }, ShouldPanic) 27 | 28 | So(func() { WithDesc().MustPack(1, "hi", int64(67)) }, ShouldNotPanic) 29 | So(func() { WithDesc().MustPack(1, struct{}{}, int64(67)) }, ShouldPanic) 30 | }) 31 | 32 | Convey("should pack values into a given array while preserving order", func() { 33 | buf := make([]byte, 32) 34 | bufd := make([]byte, 32) 35 | PackTo(buf, 1, "hi", int64(67)) 36 | PackTo(bufd, 1, "hi 1", int64(67)) 37 | So(bytes.Compare(buf, bufd), ShouldEqual, -1) 38 | }) 39 | 40 | Convey("should pack values to a given array panicking on invalid type", func() { 41 | buf := make([]byte, 32) 42 | So(func() { MustPackTo(buf, 1, "hi", int64(67)) }, ShouldNotPanic) 43 | So(func() { MustPackTo(buf, 1, struct{}{}, int64(67)) }, ShouldPanic) 44 | 45 | So(func() { WithDesc().MustPackTo(buf, 1, "hi", int64(67)) }, ShouldNotPanic) 46 | So(func() { WithDesc().MustPackTo(buf, 1, struct{}{}, int64(67)) }, ShouldPanic) 47 | }) 48 | 49 | Convey("should pack mixed-order values while preserving order", func() { 50 | s := WithDesc(false, true, false) 51 | a, err := s.Pack(1, "hi", int64(67)) 52 | So(err, ShouldBeNil) 53 | b, err := s.Pack(1, "hi 1", int64(67)) 54 | So(err, ShouldBeNil) 55 | So(bytes.Compare(a, b), ShouldEqual, 1) 56 | }) 57 | 58 | Convey("should pack mixed-order values into a given byte array while preserving order", func() { 59 | buf := make([]byte, 32) 60 | bufd := make([]byte, 32) 61 | s := WithDesc(false, true, false) 62 | s.PackTo(buf, 1, "hi", int64(67)) 63 | s.PackTo(bufd, 1, "hi 1", int64(67)) 64 | So(bytes.Compare(buf, bufd), ShouldEqual, 1) 65 | }) 66 | 67 | Convey("should unpack values", func() { 68 | a := "this is a test" 69 | b := int8(69) 70 | c := float32(1.61803398875) 71 | packed, err := Pack(a, b, c) 72 | So(err, ShouldBeNil) 73 | 74 | var ( 75 | adest string 76 | bdest int8 77 | cdest float32 78 | ) 79 | Unpack(packed, &adest, &bdest, &cdest) 80 | So(a, ShouldEqual, adest) 81 | So(b, ShouldEqual, bdest) 82 | So(c, ShouldEqual, cdest) 83 | }) 84 | 85 | Convey("should unpack only those asked for", func() { 86 | a := "this is a test" 87 | b := int8(69) 88 | c := float32(1.61803398875) 89 | packed, err := Pack(a, b, c) 90 | So(err, ShouldBeNil) 91 | 92 | var ( 93 | adest string 94 | cdest float32 95 | ) 96 | Unpack(packed, &adest, nil, &cdest) 97 | So(a, ShouldEqual, adest) 98 | So(c, ShouldEqual, cdest) 99 | }) 100 | 101 | Convey("should unpack a value at a specific index", func() { 102 | a := "this is a test" 103 | b := int8(69) 104 | c := float32(1.61803398875) 105 | packed, err := Pack(a, b, c) 106 | So(err, ShouldBeNil) 107 | 108 | var ( 109 | adest string 110 | cdest float32 111 | ) 112 | 113 | UnpackIndex(packed, 2, &cdest) 114 | So(c, ShouldEqual, cdest) 115 | 116 | UnpackIndex(packed, 0, &adest) 117 | So(a, ShouldEqual, adest) 118 | }) 119 | 120 | Convey("should error when a nonexistent index is requested", func() { 121 | a := "this is a test" 122 | b := int8(69) 123 | c := float32(1.61803398875) 124 | packed, err := Pack(a, b, c) 125 | So(err, ShouldBeNil) 126 | 127 | var cdest float32 128 | So(UnpackIndex(packed, 7, &cdest), ShouldResemble, errors.New("No data at index 7")) 129 | }) 130 | 131 | Convey("should error when unpacking a random string", func() { 132 | b := []byte("abcde") 133 | var adest string 134 | So(Unpack(b, &adest), ShouldResemble, errors.New("unknown type")) 135 | }) 136 | 137 | }) 138 | 139 | } 140 | -------------------------------------------------------------------------------- /bytes/invert.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "crypto/subtle" 7 | ) 8 | 9 | var word uint64 = 0xffffffffffffffff 10 | var maxarray = make([]byte, 4096) 11 | 12 | func init() { 13 | for i := 0; i < 4096; i++ { 14 | maxarray[i] = 0xff 15 | } 16 | } 17 | 18 | // InvertByte inverts a single byte. 19 | func InvertByte(b byte) byte { 20 | return b ^ 0xff 21 | } 22 | 23 | // InvertArrayLarge inverts the byte array using the optimized XORBytes 24 | // function from the crypto/subtle package. This is fastest for byte arrays 25 | // larger than 128 bytes. 26 | func InvertArrayLarge(a []byte) { 27 | for len(a) >= 4096 { 28 | subtle.XORBytes(a[:4096], a[:4096], maxarray) 29 | a = a[4096:] 30 | } 31 | subtle.XORBytes(a, a, maxarray) 32 | } 33 | 34 | // InvertArray inverts the byte array, choosing the optimal inversion function. 35 | func InvertArray(a []byte) { 36 | if len(a) <= 128 { 37 | InvertArraySmall(a) 38 | } else { 39 | InvertArrayLarge(a) 40 | } 41 | } 42 | 43 | // InvertArraySmall inverts a byte array a word at a time, which is fastest up 44 | // to 128 bytes. 45 | func InvertArraySmall(b []byte) { 46 | if len(b) >= 8 { 47 | for len(b) >= 128 { 48 | v := binary.LittleEndian.Uint64(b) 49 | binary.LittleEndian.PutUint64(b, v^word) 50 | v = binary.LittleEndian.Uint64(b[8:]) 51 | binary.LittleEndian.PutUint64(b[8:], v^word) 52 | v = binary.LittleEndian.Uint64(b[16:]) 53 | binary.LittleEndian.PutUint64(b[16:], v^word) 54 | v = binary.LittleEndian.Uint64(b[24:]) 55 | binary.LittleEndian.PutUint64(b[24:], v^word) 56 | v = binary.LittleEndian.Uint64(b[32:]) 57 | binary.LittleEndian.PutUint64(b[32:], v^word) 58 | v = binary.LittleEndian.Uint64(b[40:]) 59 | binary.LittleEndian.PutUint64(b[40:], v^word) 60 | v = binary.LittleEndian.Uint64(b[48:]) 61 | binary.LittleEndian.PutUint64(b[48:], v^word) 62 | v = binary.LittleEndian.Uint64(b[56:]) 63 | binary.LittleEndian.PutUint64(b[56:], v^word) 64 | v = binary.LittleEndian.Uint64(b[64:]) 65 | binary.LittleEndian.PutUint64(b[64:], v^word) 66 | v = binary.LittleEndian.Uint64(b[72:]) 67 | binary.LittleEndian.PutUint64(b[72:], v^word) 68 | v = binary.LittleEndian.Uint64(b[80:]) 69 | binary.LittleEndian.PutUint64(b[80:], v^word) 70 | v = binary.LittleEndian.Uint64(b[88:]) 71 | binary.LittleEndian.PutUint64(b[88:], v^word) 72 | v = binary.LittleEndian.Uint64(b[96:]) 73 | binary.LittleEndian.PutUint64(b[96:], v^word) 74 | v = binary.LittleEndian.Uint64(b[104:]) 75 | binary.LittleEndian.PutUint64(b[104:], v^word) 76 | v = binary.LittleEndian.Uint64(b[112:]) 77 | binary.LittleEndian.PutUint64(b[112:], v^word) 78 | v = binary.LittleEndian.Uint64(b[120:]) 79 | binary.LittleEndian.PutUint64(b[120:], v^word) 80 | b = b[128:] 81 | } 82 | for len(b) >= 64 { 83 | v := binary.LittleEndian.Uint64(b) 84 | binary.LittleEndian.PutUint64(b, v^word) 85 | v = binary.LittleEndian.Uint64(b[8:]) 86 | binary.LittleEndian.PutUint64(b[8:], v^word) 87 | v = binary.LittleEndian.Uint64(b[16:]) 88 | binary.LittleEndian.PutUint64(b[16:], v^word) 89 | v = binary.LittleEndian.Uint64(b[24:]) 90 | binary.LittleEndian.PutUint64(b[24:], v^word) 91 | v = binary.LittleEndian.Uint64(b[32:]) 92 | binary.LittleEndian.PutUint64(b[32:], v^word) 93 | v = binary.LittleEndian.Uint64(b[40:]) 94 | binary.LittleEndian.PutUint64(b[40:], v^word) 95 | v = binary.LittleEndian.Uint64(b[48:]) 96 | binary.LittleEndian.PutUint64(b[48:], v^word) 97 | v = binary.LittleEndian.Uint64(b[56:]) 98 | binary.LittleEndian.PutUint64(b[56:], v^word) 99 | b = b[64:] 100 | } 101 | for len(b) >= 32 { 102 | v := binary.LittleEndian.Uint64(b) 103 | binary.LittleEndian.PutUint64(b, v^word) 104 | v = binary.LittleEndian.Uint64(b[8:]) 105 | binary.LittleEndian.PutUint64(b[8:], v^word) 106 | v = binary.LittleEndian.Uint64(b[16:]) 107 | binary.LittleEndian.PutUint64(b[16:], v^word) 108 | v = binary.LittleEndian.Uint64(b[24:]) 109 | binary.LittleEndian.PutUint64(b[24:], v^word) 110 | b = b[32:] 111 | } 112 | for len(b) >= 16 { 113 | v := binary.LittleEndian.Uint64(b) 114 | binary.LittleEndian.PutUint64(b, v^word) 115 | v = binary.LittleEndian.Uint64(b[8:]) 116 | binary.LittleEndian.PutUint64(b[8:], v^word) 117 | b = b[16:] 118 | } 119 | } 120 | for len(b) >= 8 { 121 | v := binary.LittleEndian.Uint64(b) 122 | binary.LittleEndian.PutUint64(b, v^word) 123 | b = b[8:] 124 | } 125 | for i := range b { 126 | b[i] ^= 0xff 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bingo 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/iancmcc/bingo?style=flat-square)](https://goreportcard.com/report/github.com/iancmcc/bingo) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/iancmcc/bingo.svg)](https://pkg.go.dev/github.com/iancmcc/bingo) 5 | ![Tests](https://github.com/iancmcc/bingo/actions/workflows/tests.yml/badge.svg) 6 | [![Coverage](https://coveralls.io/repos/github/iancmcc/bingo/badge.svg?branch=main)](https://coveralls.io/github/iancmcc/bingo?branch=main) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 8 | 9 | Fast, zero-allocation, lexicographic-order-preserving packing/unpacking of native Go types to bytes. 10 | 11 | ## Features 12 | 13 | * Encode `bool`, `string`, `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64`, and `time.Time` 14 | * Packed values maintain original sort order 15 | * Pack values in descending order 16 | * Pack to an existing byte slice with no additional allocations 17 | * Create and pack values to a new byte slice (one allocation) 18 | * Unpack all values or just specific indexes 19 | 20 | ## Usage 21 | 22 | Import `bingo`: 23 | 24 | ```go 25 | import "github.com/iancmcc/bingo" 26 | ``` 27 | 28 | ### Packing 29 | 30 | ```go 31 | // Create and return a byte slice with encoded values 32 | key := bingo.MustPack(uint8(12), "cool string bro") 33 | 34 | // Now unpack 35 | var ( 36 | first uint8 37 | second string 38 | ) 39 | bingo.Unpack(key, &first, &second) 40 | 41 | 42 | // Pack so results will sort the second value descending 43 | key = bingo.WithDesc(false, true, false).MustPack(1, time.Now(), true) 44 | 45 | // Just unpack the middle value 46 | var t time.Time 47 | bingo.UnpackIndex(key, 1, &t) 48 | 49 | 50 | // Pack to an existing byte slice 51 | existingSlice := make([]byte, 100) 52 | bingo.MustPackTo(existingSlice, uint16(7), "abc123") 53 | ``` 54 | 55 | ## Benchmarks 56 | 57 | ```sh 58 | $ go test -bench BenchmarkCodecs 59 | ``` 60 | 61 | ``` 62 | goos: linux 63 | goarch: amd64 64 | pkg: github.com/iancmcc/bingo 65 | cpu: Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz 66 | BenchmarkCodecs/int8/encode/natural-8 100000000 10.35 ns/op 0 B/op 0 allocs/op 67 | BenchmarkCodecs/int8/encode/inverse-8 89794872 12.60 ns/op 0 B/op 0 allocs/op 68 | BenchmarkCodecs/int8/decode/natural-8 65864187 17.11 ns/op 0 B/op 0 allocs/op 69 | BenchmarkCodecs/int8/decode/inverse-8 63386660 17.27 ns/op 0 B/op 0 allocs/op 70 | BenchmarkCodecs/int16/encode/natural-8 100000000 10.04 ns/op 0 B/op 0 allocs/op 71 | BenchmarkCodecs/int16/encode/inverse-8 85618111 13.08 ns/op 0 B/op 0 allocs/op 72 | BenchmarkCodecs/int16/decode/natural-8 64825372 16.55 ns/op 0 B/op 0 allocs/op 73 | BenchmarkCodecs/int16/decode/inverse-8 58359490 19.44 ns/op 0 B/op 0 allocs/op 74 | BenchmarkCodecs/int32/encode/natural-8 120393369 9.942 ns/op 0 B/op 0 allocs/op 75 | BenchmarkCodecs/int32/encode/inverse-8 80418766 13.67 ns/op 0 B/op 0 allocs/op 76 | BenchmarkCodecs/int32/decode/natural-8 53423986 21.03 ns/op 0 B/op 0 allocs/op 77 | BenchmarkCodecs/int32/decode/inverse-8 47975257 24.68 ns/op 0 B/op 0 allocs/op 78 | BenchmarkCodecs/int64/encode/natural-8 100000000 10.95 ns/op 0 B/op 0 allocs/op 79 | BenchmarkCodecs/int64/encode/inverse-8 67093402 17.79 ns/op 0 B/op 0 allocs/op 80 | BenchmarkCodecs/int64/decode/natural-8 47149765 23.97 ns/op 0 B/op 0 allocs/op 81 | BenchmarkCodecs/int64/decode/inverse-8 39063907 27.36 ns/op 0 B/op 0 allocs/op 82 | BenchmarkCodecs/float32/encode/natural-8 100000000 10.42 ns/op 0 B/op 0 allocs/op 83 | BenchmarkCodecs/float32/encode/inverse-8 79910834 14.11 ns/op 0 B/op 0 allocs/op 84 | BenchmarkCodecs/float32/decode/natural-8 63494347 16.88 ns/op 0 B/op 0 allocs/op 85 | BenchmarkCodecs/float32/decode/inverse-8 46677241 25.55 ns/op 0 B/op 0 allocs/op 86 | BenchmarkCodecs/float64/encode/natural-8 100000000 11.05 ns/op 0 B/op 0 allocs/op 87 | BenchmarkCodecs/float64/encode/inverse-8 62377978 17.48 ns/op 0 B/op 0 allocs/op 88 | BenchmarkCodecs/float64/decode/natural-8 62370994 17.26 ns/op 0 B/op 0 allocs/op 89 | BenchmarkCodecs/float64/decode/inverse-8 54139144 20.68 ns/op 0 B/op 0 allocs/op 90 | BenchmarkCodecs/string/encode/natural-8 47404435 23.80 ns/op 0 B/op 0 allocs/op 91 | BenchmarkCodecs/string/encode/inverse-8 29144544 39.74 ns/op 0 B/op 0 allocs/op 92 | BenchmarkCodecs/string/decode/natural-8 39683684 31.39 ns/op 0 B/op 0 allocs/op 93 | BenchmarkCodecs/string/decode/inverse-8 23373333 48.06 ns/op 0 B/op 0 allocs/op 94 | BenchmarkCodecs/time/encode/natural-8 47027752 24.72 ns/op 0 B/op 0 allocs/op 95 | BenchmarkCodecs/time/encode/inverse-8 38363001 29.22 ns/op 0 B/op 0 allocs/op 96 | BenchmarkCodecs/time/decode/natural-8 42566337 26.76 ns/op 0 B/op 0 allocs/op 97 | BenchmarkCodecs/time/decode/inverse-8 30851250 36.88 ns/op 0 B/op 0 allocs/op 98 | PASS 99 | ok github.com/iancmcc/bingo 37.644s 100 | ``` 101 | --------------------------------------------------------------------------------