Packed fields option

This commit is contained in:
tidwall 2019-02-17 13:10:02 -07:00
parent ffdf90eebf
commit 30d31d0926
19 changed files with 1348 additions and 235 deletions

19
Gopkg.lock generated
View File

@ -123,6 +123,14 @@
pruneopts = "" pruneopts = ""
revision = "e8fc0692a7e26a05b06517348ed466349062eb47" revision = "e8fc0692a7e26a05b06517348ed466349062eb47"
[[projects]]
branch = "master"
digest = "1:de10194e2fb3787b2efdea394a7f710f49bfdcaae78f715b38a9057c602a4998"
name = "github.com/h2so5/half"
packages = ["."]
pruneopts = ""
revision = "c705bde19bd0aa32d718d167c961fe681ee91473"
[[projects]] [[projects]]
digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e" digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e"
name = "github.com/jmespath/go-jmespath" name = "github.com/jmespath/go-jmespath"
@ -224,11 +232,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:2fef6390e8d9118debd4937a699afad9e1629f2d5d3c965a58fc33afe04f9b46" digest = "1:4d2ec831fbaaf74fd75d2d9fe107e605c92489ec6cef6d36e1f23b678e9f2bd4"
name = "github.com/tidwall/buntdb" name = "github.com/tidwall/buntdb"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95" revision = "6249481c29c2cd96f53b691b74ac1893f72774c2"
[[projects]] [[projects]]
digest = "1:91acf4d86b348c1f1832336836035373b047ffcb16a0fde066bd531bbe3452b2" digest = "1:91acf4d86b348c1f1832336836035373b047ffcb16a0fde066bd531bbe3452b2"
@ -254,12 +262,12 @@
version = "v1.1.1" version = "v1.1.1"
[[projects]] [[projects]]
digest = "1:3ddca2bd5496c6922a2a9e636530e178a43c2a534ea6634211acdc7d10222794" digest = "1:eade4ea6782f5eed4a6b3138a648f9a332900650804fd206e5daaf99cc5613ea"
name = "github.com/tidwall/gjson" name = "github.com/tidwall/gjson"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "1e3f6aeaa5bad08d777ea7807b279a07885dd8b2" revision = "eee0b6226f0d1db2675a176fdfaa8419bcad4ca8"
version = "v1.1.3" version = "v1.2.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -466,6 +474,7 @@
"github.com/eclipse/paho.mqtt.golang", "github.com/eclipse/paho.mqtt.golang",
"github.com/golang/protobuf/proto", "github.com/golang/protobuf/proto",
"github.com/gomodule/redigo/redis", "github.com/gomodule/redigo/redis",
"github.com/h2so5/half",
"github.com/mmcloughlin/geohash", "github.com/mmcloughlin/geohash",
"github.com/nats-io/go-nats", "github.com/nats-io/go-nats",
"github.com/peterh/liner", "github.com/peterh/liner",

View File

@ -23,7 +23,8 @@
required = [ required = [
"github.com/tidwall/lotsa", "github.com/tidwall/lotsa",
"github.com/mmcloughlin/geohash", "github.com/mmcloughlin/geohash",
"github.com/tidwall/evio" "github.com/tidwall/evio",
"github.com/h2so5/half"
] ]
[[constraint]] [[constraint]]
@ -72,7 +73,7 @@ required = [
[[constraint]] [[constraint]]
name = "github.com/tidwall/gjson" name = "github.com/tidwall/gjson"
version = "1.0.1" version = "1.2.1"
[[constraint]] [[constraint]]
branch = "master" branch = "master"

View File

@ -111,7 +111,7 @@ func TestDescend(t *testing.T) {
var keys []string var keys []string
for i := 0; i < 1000; i += 10 { for i := 0; i < 1000; i += 10 {
keys = append(keys, fmt.Sprintf("%03d", i)) keys = append(keys, fmt.Sprintf("%03d", i))
tr.Set(item.New(keys[len(keys)-1], nil)) tr.Set(item.New(keys[len(keys)-1], nil, false))
} }
var exp []string var exp []string
tr.Reverse(func(item *item.Item) bool { tr.Reverse(func(item *item.Item) bool {
@ -162,7 +162,7 @@ func TestAscend(t *testing.T) {
var keys []string var keys []string
for i := 0; i < 1000; i += 10 { for i := 0; i < 1000; i += 10 {
keys = append(keys, fmt.Sprintf("%03d", i)) keys = append(keys, fmt.Sprintf("%03d", i))
tr.Set(item.New(keys[len(keys)-1], nil)) tr.Set(item.New(keys[len(keys)-1], nil, false))
} }
exp := keys exp := keys
for i := -1; i < 1000; i++ { for i := -1; i < 1000; i++ {
@ -205,7 +205,7 @@ func TestBTree(t *testing.T) {
// insert all items // insert all items
for _, key := range keys { for _, key := range keys {
value, replaced := tr.Set(item.New(key, testString(key))) value, replaced := tr.Set(item.New(key, testString(key), false))
if replaced { if replaced {
t.Fatal("expected false") t.Fatal("expected false")
} }
@ -362,7 +362,7 @@ func TestBTree(t *testing.T) {
// replace second half // replace second half
for _, key := range keys[len(keys)/2:] { for _, key := range keys[len(keys)/2:] {
value, replaced := tr.Set(item.New(key, testString(key))) value, replaced := tr.Set(item.New(key, testString(key), false))
if !replaced { if !replaced {
t.Fatal("expected true") t.Fatal("expected true")
} }
@ -420,7 +420,7 @@ func BenchmarkTidwallSequentialSet(b *testing.B) {
sort.Strings(keys) sort.Strings(keys)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
tr.Set(item.New(keys[i], nil)) tr.Set(item.New(keys[i], nil, false))
} }
} }
@ -429,7 +429,7 @@ func BenchmarkTidwallSequentialGet(b *testing.B) {
keys := randKeys(b.N) keys := randKeys(b.N)
sort.Strings(keys) sort.Strings(keys)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
tr.Set(item.New(keys[i], nil)) tr.Set(item.New(keys[i], nil, false))
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -442,7 +442,7 @@ func BenchmarkTidwallRandomSet(b *testing.B) {
keys := randKeys(b.N) keys := randKeys(b.N)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
tr.Set(item.New(keys[i], nil)) tr.Set(item.New(keys[i], nil, false))
} }
} }
@ -450,7 +450,7 @@ func BenchmarkTidwallRandomGet(b *testing.B) {
var tr BTree var tr BTree
keys := randKeys(b.N) keys := randKeys(b.N)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
tr.Set(item.New(keys[i], nil)) tr.Set(item.New(keys[i], nil, false))
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -528,11 +528,11 @@ func BenchmarkTidwallRandomGet(b *testing.B) {
func TestBTreeOne(t *testing.T) { func TestBTreeOne(t *testing.T) {
var tr BTree var tr BTree
tr.Set(item.New("1", testString("1"))) tr.Set(item.New("1", testString("1"), false))
tr.Delete("1") tr.Delete("1")
tr.Set(item.New("1", testString("1"))) tr.Set(item.New("1", testString("1"), false))
tr.Delete("1") tr.Delete("1")
tr.Set(item.New("1", testString("1"))) tr.Set(item.New("1", testString("1"), false))
tr.Delete("1") tr.Delete("1")
} }
@ -541,7 +541,7 @@ func TestBTree256(t *testing.T) {
var n int var n int
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
for _, i := range rand.Perm(256) { for _, i := range rand.Perm(256) {
tr.Set(item.New(fmt.Sprintf("%d", i), testString(fmt.Sprintf("%d", i)))) tr.Set(item.New(fmt.Sprintf("%d", i), testString(fmt.Sprintf("%d", i)), false))
n++ n++
if tr.Len() != n { if tr.Len() != n {
t.Fatalf("expected 256, got %d", n) t.Fatalf("expected 256, got %d", n)

View File

@ -57,36 +57,28 @@ func (item *Item) setIsPacked(isPacked bool) {
} }
} }
func (item *Item) fieldsLen() int { func (item *Item) fieldsDataSize() int {
return int(item.head[0] & 0x3FFFFFFF) return int(item.head[0] & 0x3FFFFFFF)
} }
func (item *Item) setFieldsLen(len int) { func (item *Item) setFieldsDataSize(len int) {
item.head[0] = item.head[0]>>30<<30 | uint32(len) item.head[0] = item.head[0]>>30<<30 | uint32(len)
} }
func (item *Item) idLen() int { func (item *Item) idDataSize() int {
return int(item.head[1]) return int(item.head[1])
} }
func (item *Item) setIDLen(len int) { func (item *Item) setIDDataSize(len int) {
item.head[1] = uint32(len) item.head[1] = uint32(len)
} }
// ID returns the items ID as a string // ID returns the items ID as a string
func (item *Item) ID() string { func (item *Item) ID() string {
return *(*string)((unsafe.Pointer)(&reflect.StringHeader{ return *(*string)((unsafe.Pointer)(&reflect.StringHeader{
Data: uintptr(unsafe.Pointer(item.data)) + uintptr(item.fieldsLen()), Data: uintptr(unsafe.Pointer(item.data)) +
Len: item.idLen(), uintptr(item.fieldsDataSize()),
})) Len: item.idDataSize(),
}
// Fields returns the field values
func (item *Item) fields() []float64 {
return *(*[]float64)((unsafe.Pointer)(&reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(item.data)),
Len: item.fieldsLen() / 8,
Cap: item.fieldsLen() / 8,
})) }))
} }
@ -112,7 +104,7 @@ func New(id string, obj geojson.Object, packed bool) *Item {
item = (*Item)(unsafe.Pointer(oitem)) item = (*Item)(unsafe.Pointer(oitem))
} }
item.setIsPacked(packed) item.setIsPacked(packed)
item.setIDLen(len(id)) item.setIDDataSize(len(id))
item.data = unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&id)).Data) item.data = unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&id)).Data)
return item return item
} }
@ -126,7 +118,7 @@ func (item *Item) WeightAndPoints() (weight, points int) {
} else if item.Obj() != nil { } else if item.Obj() != nil {
weight = len(item.Obj().String()) weight = len(item.Obj().String())
} }
weight += item.fieldsLen() + item.idLen() weight += item.fieldsDataSize() + item.idDataSize()
return weight, points return weight, points
} }
@ -144,21 +136,56 @@ func (item *Item) Less(other btree.Item, ctx interface{}) bool {
return item.ID() < other.(*Item).ID() return item.ID() < other.(*Item).ID()
} }
// fieldBytes returns the raw fields data section
func (item *Item) fieldsBytes() []byte {
return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(item.data)),
Len: item.fieldsDataSize(),
Cap: item.fieldsDataSize(),
}))
}
// Packed returns true when the item's fields are packed
func (item *Item) Packed() bool {
return item == nil || item.isPacked()
}
// CopyOverFields overwriting previous fields. Accepts an *Item or []float64 // CopyOverFields overwriting previous fields. Accepts an *Item or []float64
func (item *Item) CopyOverFields(from interface{}) { func (item *Item) CopyOverFields(from interface{}) {
if item == nil {
return
}
var values []float64 var values []float64
var fieldBytes []byte
var directCopy bool
switch from := from.(type) { switch from := from.(type) {
case *Item: case *Item:
values = from.fields() if item.Packed() == from.Packed() {
// direct copy the bytes
fieldBytes = from.fieldsBytes()
directCopy = true
} else {
// get the values through iteration
item.ForEachField(-1, func(value float64) bool {
values = append(values, value)
return true
})
}
case []float64: case []float64:
values = from values = from
} }
fieldBytes := floatsToBytes(values) if !directCopy {
oldData := item.dataBytes() if item.Packed() {
newData := make([]byte, len(fieldBytes)+item.idLen()) fieldBytes = item.packedGenerateFieldBytes(values)
} else {
fieldBytes = item.unpackedGenerateFieldBytes(values)
}
}
id := item.ID()
newData := make([]byte, len(fieldBytes)+len(id))
copy(newData, fieldBytes) copy(newData, fieldBytes)
copy(newData[len(fieldBytes):], oldData[item.fieldsLen():]) copy(newData[len(fieldBytes):], id)
item.setFieldsLen(len(fieldBytes)) item.setFieldsDataSize(len(fieldBytes))
if len(newData) > 0 { if len(newData) > 0 {
item.data = unsafe.Pointer(&newData[0]) item.data = unsafe.Pointer(&newData[0])
} else { } else {
@ -166,54 +193,15 @@ func (item *Item) CopyOverFields(from interface{}) {
} }
} }
func getFieldAt(data unsafe.Pointer, index int) float64 {
return *(*float64)(unsafe.Pointer(uintptr(data) + uintptr(index*8)))
}
func setFieldAt(data unsafe.Pointer, index int, value float64) {
*(*float64)(unsafe.Pointer(uintptr(data) + uintptr(index*8))) = value
}
// SetField set a field value at specified index. // SetField set a field value at specified index.
func (item *Item) SetField(index int, value float64) (updated bool) { func (item *Item) SetField(index int, value float64) (updated bool) {
numFields := item.fieldsLen() / 8 if item == nil {
if index < numFields { return false
// field exists
if getFieldAt(item.data, index) == value {
return false
}
} else {
// make room for new field
oldBytes := item.dataBytes()
newData := make([]byte, (index+1)*8+item.idLen())
// copy the existing fields
copy(newData, oldBytes[:item.fieldsLen()])
// copy the id
copy(newData[(index+1)*8:], oldBytes[item.fieldsLen():])
// update the fields length
item.setFieldsLen((index + 1) * 8)
// update the raw data
item.data = unsafe.Pointer(&newData[0])
} }
// set the new field if item.Packed() {
setFieldAt(item.data, index, value) return item.packedSetField(index, value)
return true }
} return item.unpackedSetField(index, value)
func (item *Item) dataBytes() []byte {
return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(item.data)),
Len: item.fieldsLen() + item.idLen(),
Cap: item.fieldsLen() + item.idLen(),
}))
}
func floatsToBytes(f []float64) []byte {
return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{
Data: ((*reflect.SliceHeader)(unsafe.Pointer(&f))).Data,
Len: len(f) * 8,
Cap: len(f) * 8,
}))
} }
// ForEachField iterates over each field. The count param is the number of // ForEachField iterates over each field. The count param is the number of
@ -222,27 +210,11 @@ func (item *Item) ForEachField(count int, iter func(value float64) bool) {
if item == nil { if item == nil {
return return
} }
fields := item.fields() if item.Packed() {
var n int item.packedForEachField(count, iter)
if count < 0 {
n = len(fields)
} else { } else {
n = count item.unpackedForEachField(count, iter)
} }
for i := 0; i < n; i++ {
var field float64
if i < len(fields) {
field = fields[i]
}
if !iter(field) {
return
}
}
}
// Packed returns true when the item's fields are packed
func (item *Item) Packed() bool {
return item == nil || item.isPacked()
} }
// GetField returns the value for a field at index. // GetField returns the value for a field at index.
@ -254,26 +226,12 @@ func (item *Item) GetField(index int) float64 {
return 0 return 0
} }
if item.Packed() { if item.Packed() {
var fvalue float64 return item.packedGetField(index)
var idx int
item.ForEachField(-1, func(value float64) bool {
if idx == index {
fvalue = value
return false
}
idx++
return true
})
return fvalue
} }
numFields := item.fieldsLen() / 8 return item.unpackedGetField(index)
if index < numFields {
return getFieldAt(item.data, index)
}
return 0
} }
// HasFields returns true when item has fields // HasFields returns true when item has fields
func (item *Item) HasFields() bool { func (item *Item) HasFields() bool {
return item != nil && item.fieldsLen() > 0 return item != nil && item.fieldsDataSize() > 0
} }

View File

@ -10,23 +10,17 @@ import (
"github.com/tidwall/geojson/geometry" "github.com/tidwall/geojson/geometry"
) )
func init() {
seed := time.Now().UnixNano()
println(seed)
rand.Seed(seed)
}
func testRandItemHead(t *testing.T, idx int, item *Item) { func testRandItemHead(t *testing.T, idx int, item *Item) {
t.Helper() t.Helper()
if idx == 0 { if idx == 0 {
if item.isPoint() { if item.isPoint() {
t.Fatalf("expected false") t.Fatalf("expected false")
} }
if item.fieldsLen() != 0 { if item.fieldsDataSize() != 0 {
t.Fatalf("expected '%v', got '%v'", 0, item.fieldsLen()) t.Fatalf("expected '%v', got '%v'", 0, item.fieldsDataSize())
} }
if item.idLen() != 0 { if item.idDataSize() != 0 {
t.Fatalf("expected '%v', got '%v'", 0, item.idLen()) t.Fatalf("expected '%v', got '%v'", 0, item.idDataSize())
} }
} }
isPoint := rand.Int()%2 == 0 isPoint := rand.Int()%2 == 0
@ -34,17 +28,17 @@ func testRandItemHead(t *testing.T, idx int, item *Item) {
idLen := int(rand.Uint32()) idLen := int(rand.Uint32())
item.setIsPoint(isPoint) item.setIsPoint(isPoint)
item.setFieldsLen(fieldsLen) item.setFieldsDataSize(fieldsLen)
item.setIDLen(idLen) item.setIDDataSize(idLen)
if item.isPoint() != isPoint { if item.isPoint() != isPoint {
t.Fatalf("isPoint: expected '%v', got '%v'", isPoint, item.isPoint()) t.Fatalf("isPoint: expected '%v', got '%v'", isPoint, item.isPoint())
} }
if item.fieldsLen() != fieldsLen { if item.fieldsDataSize() != fieldsLen {
t.Fatalf("fieldsLen: expected '%v', got '%v'", fieldsLen, item.fieldsLen()) t.Fatalf("fieldsLen: expected '%v', got '%v'", fieldsLen, item.fieldsDataSize())
} }
if item.idLen() != idLen { if item.idDataSize() != idLen {
t.Fatalf("idLen: expected '%v', got '%v'", idLen, item.idLen()) t.Fatalf("idLen: expected '%v', got '%v'", idLen, item.idDataSize())
} }
} }
@ -62,7 +56,6 @@ func testRandItem(t *testing.T) {
keyb := make([]byte, rand.Int()%16) keyb := make([]byte, rand.Int()%16)
rand.Read(keyb) rand.Read(keyb)
key := string(keyb) key := string(keyb)
packed := rand.Int()%2 == 0 packed := rand.Int()%2 == 0
values := make([]float64, rand.Int()%64) values := make([]float64, rand.Int()%64)
for i := range values { for i := range values {
@ -111,10 +104,10 @@ func testRandItem(t *testing.T) {
} }
for _, i := range setValues { for _, i := range setValues {
if item.GetField(i) != values[i] { if item.GetField(i) != values[i] {
t.Fatalf("expected '%v', got '%v'", values[i], item.GetField(i)) t.Fatalf("expected '%v', got '%v' for index %d", values[i], item.GetField(i), i)
} }
} }
fields := item.fields() fields := itemFields(item)
for i := 0; i < len(fields); i++ { for i := 0; i < len(fields); i++ {
for _, j := range setValues { for _, j := range setValues {
if i == j { if i == j {
@ -126,8 +119,9 @@ func testRandItem(t *testing.T) {
} }
} }
weight, points := item.WeightAndPoints() weight, points := item.WeightAndPoints()
if weight != len(fields)*8+len(key)+points*16 { if weight != item.fieldsDataSize()+len(key)+points*16 {
t.Fatalf("expected '%v', got '%v'", len(fields)*8+len(key)+points*16, weight) t.Fatalf("expected '%v', got '%v'",
item.fieldsDataSize()+len(key)+points*16, weight)
} }
if points != 1 { if points != 1 {
t.Fatalf("expected '%v', got '%v'", 1, points) t.Fatalf("expected '%v', got '%v'", 1, points)
@ -167,8 +161,11 @@ func testRandItem(t *testing.T) {
fvalues = append(fvalues, value) fvalues = append(fvalues, value)
return true return true
}) })
for len(fvalues) < len(values) {
fvalues = append(fvalues, 0)
}
if !floatsEquals(values, fvalues) { if !floatsEquals(values, fvalues) {
t.Fatalf("expected '%v', got '%v'", 1, len(fvalues)) t.Fatalf("expected true")
} }
// should not fail, must allow nil receiver // should not fail, must allow nil receiver
@ -185,25 +182,25 @@ func testRandItem(t *testing.T) {
} }
item.CopyOverFields(values) item.CopyOverFields(values)
weight, points := item.WeightAndPoints() weight, points := item.WeightAndPoints()
if weight != len(values)*8+len(key)+points*16 { if weight != item.fieldsDataSize()+len(key)+points*16 {
t.Fatalf("expected '%v', got '%v'", len(values)*8+len(key)+points*16, weight) t.Fatalf("expected '%v', got '%v'", item.fieldsDataSize()+len(key)+points*16, weight)
} }
if points != 1 { if points != 1 {
t.Fatalf("expected '%v', got '%v'", 1, points) t.Fatalf("expected '%v', got '%v'", 1, points)
} }
if !floatsEquals(item.fields(), values) { if !floatsEquals(itemFields(item), values) {
t.Fatalf("expected '%v', got '%v'", values, item.fields()) t.Fatalf("expected '%v', got '%v'", values, itemFields(item))
} }
item.CopyOverFields(item) item.CopyOverFields(item)
weight, points = item.WeightAndPoints() weight, points = item.WeightAndPoints()
if weight != len(values)*8+len(key)+points*16 { if weight != item.fieldsDataSize()+len(key)+points*16 {
t.Fatalf("expected '%v', got '%v'", len(values)*8+len(key)+points*16, weight) t.Fatalf("expected '%v', got '%v'", item.fieldsDataSize()+len(key)+points*16, weight)
} }
if points != 1 { if points != 1 {
t.Fatalf("expected '%v', got '%v'", 1, points) t.Fatalf("expected '%v', got '%v'", 1, points)
} }
if !floatsEquals(item.fields(), values) { if !floatsEquals(itemFields(item), values) {
t.Fatalf("expected '%v', got '%v'", values, item.fields()) t.Fatalf("expected '%v', got '%v'", values, itemFields(item))
} }
if len(values) > 0 && !item.HasFields() { if len(values) > 0 && !item.HasFields() {
t.Fatal("expected true") t.Fatal("expected true")
@ -217,8 +214,8 @@ func testRandItem(t *testing.T) {
if points != 1 { if points != 1 {
t.Fatalf("expected '%v', got '%v'", 1, points) t.Fatalf("expected '%v', got '%v'", 1, points)
} }
if len(item.fields()) != 0 { if len(itemFields(item)) != 0 {
t.Fatalf("expected '%#v', got '%#v'", 0, len(item.fields())) t.Fatalf("expected '%#v', got '%#v'", 0, len(itemFields(item)))
} }
if item.ID() != key { if item.ID() != key {
t.Fatalf("expected '%v', got '%v'", key, item.ID()) t.Fatalf("expected '%v', got '%v'", key, item.ID())
@ -229,7 +226,20 @@ func testRandItem(t *testing.T) {
} }
func itemFields(item *Item) []float64 {
var values []float64
item.ForEachField(-1, func(value float64) bool {
values = append(values, value)
return true
})
return values
}
func TestItem(t *testing.T) { func TestItem(t *testing.T) {
seed := time.Now().UnixNano()
seed = 1550371581595971000
println("TestItem seed", seed)
rand.Seed(seed)
start := time.Now() start := time.Now()
for time.Since(start) < time.Second/2 { for time.Since(start) < time.Second/2 {
testRandItem(t) testRandItem(t)

View File

@ -0,0 +1,284 @@
package item
import (
"fmt"
"unsafe"
"github.com/h2so5/half"
)
// kind bits bytes values min max
// --------------------------------------------------------------------
// 0 5 1 32 -16 15
// 1 13 2 16384 -4095 4095
// 2 21 3 2097152 -1048576 1048575
// 3 29 4 536870912 -268435456 268435455
// 4 16 3 -- standard 16-bit floating point --
// 5 32 5 -- standard 32-bit floating point --
// 6 64 9 -- standard 64-bit floating point --
const maxFieldBytes = 9
const (
maxInt5 = 15
maxInt13 = 4095
maxInt21 = 1048575
maxInt29 = 268435455
)
func appendPacked(dst []byte, f64 float64) []byte {
if f64 == 0 {
return append(dst, 0)
}
i64 := int64(f64)
if f64 == float64(i64) {
// whole number
var signed byte
if i64 < 0 {
i64 *= -1
signed = 16
}
if i64 <= maxInt5 {
return append(dst, 0<<5|signed|
byte(i64))
}
if i64 <= maxInt13 {
return append(dst, 1<<5|signed|
byte(i64>>8), byte(i64))
}
if i64 <= maxInt21 {
return append(dst, 2<<5|signed|
byte(i64>>16), byte(i64>>8), byte(i64))
}
if i64 <= maxInt29 {
return append(dst, 3<<5|signed|
byte(i64>>24), byte(i64>>16), byte(i64>>8), byte(i64))
}
// fallthrough
}
f32 := float32(f64)
if f64 == float64(f32) {
f16 := half.NewFloat16(f32)
if f32 == f16.Float32() {
dst = append(dst, 4<<5, 0, 0)
*(*half.Float16)(unsafe.Pointer(&dst[len(dst)-2])) = f16
return dst
}
dst = append(dst, 5<<5, 0, 0, 0, 0)
*(*float32)(unsafe.Pointer(&dst[len(dst)-4])) = f32
return dst
}
dst = append(dst, 6<<5, 0, 0, 0, 0, 0, 0, 0, 0)
*(*float64)(unsafe.Pointer(&dst[len(dst)-8])) = f64
return dst
}
func skipPacked(data []byte, count int) (out []byte, read int) {
var i int
for i < len(data) {
if read >= count {
return data[i:], read
}
kind := data[i] >> 5
if kind < 4 {
i += int(kind) + 1
} else if kind == 4 {
i += 3
} else if kind == 5 {
i += 5
} else {
i += 9
}
read++
}
return nil, read
}
func readPacked(data []byte) ([]byte, float64) {
if len(data) == 0 {
return nil, 0
}
if data[0] == 0 {
return data[1:], 0
}
kind := data[0] >> 5
switch kind {
case 0, 1, 2, 3:
// whole number
var value float64
if kind == 0 {
value = float64(
uint32(data[0] & 0xF),
)
} else if kind == 1 {
value = float64(
uint32(data[0]&0xF)<<8 | uint32(data[1]),
)
} else if kind == 2 {
value = float64(
uint32(data[0]&0xF)<<16 | uint32(data[1])<<8 |
uint32(data[2]),
)
} else {
value = float64(
uint32(data[0]&0xF)<<24 | uint32(data[1])<<16 |
uint32(data[2])<<8 | uint32(data[3]),
)
}
if data[0]&0x10 != 0 {
value *= -1
}
return data[kind+1:], value
case 4:
// 16-bit float
return data[3:],
float64((*half.Float16)(unsafe.Pointer(&data[1])).Float32())
case 5:
// 32-bit float
return data[5:],
float64(*(*float32)(unsafe.Pointer(&data[1])))
case 6:
// 64-bit float
return data[9:], *(*float64)(unsafe.Pointer(&data[1]))
}
panic("invalid data")
}
func (item *Item) packedGenerateFieldBytes(values []float64) []byte {
var dst []byte
for i := 0; i < len(values); i++ {
dst = appendPacked(dst, values[i])
}
return dst
}
func (item *Item) packedSetField(index int, value float64) (updated bool) {
if false {
func() {
data := item.fieldsBytes()
fmt.Printf("%v >> [%x]", value, data)
defer func() {
data := item.fieldsBytes()
fmt.Printf(" >> [%x]\n", data)
}()
}()
}
/////////////////////////////////////////////////////////////////
// original field bytes
headBytes := item.fieldsBytes()
// quickly skip over head fields.
// returns the start of the field at index, and the number of valid
// fields that were read.
fieldBytes, read := skipPacked(headBytes, index)
// number of empty/blank bytes that need to be added between the
// head bytes and the new field bytes.
var blankSpace int
// data a that follows the new field bytes
var tailBytes []byte
if len(fieldBytes) == 0 {
// field at index was not found.
if value == 0 {
// zero value is the default, so we can assume that the fields was
// not updated.
return false
}
// set the blank space
blankSpace = index - read
fieldBytes = nil
} else {
// field at index was found.
// truncate the head bytes to reflect only the bytes up to
// the current field.
headBytes = headBytes[:len(headBytes)-len(fieldBytes)]
// read the current value and get the tail data following the
// current field.
var cvalue float64
tailBytes, cvalue = readPacked(fieldBytes)
if cvalue == value {
// no change to value
return false
}
// truncate the field bytes to exactly match current field.
fieldBytes = fieldBytes[:len(fieldBytes)-len(tailBytes)]
}
// create the new field bytes
{
var buf [maxFieldBytes]byte
newFieldBytes := appendPacked(buf[:0], value)
if len(newFieldBytes) == len(fieldBytes) {
// no change in data size, update in place
copy(fieldBytes, newFieldBytes)
return true
}
// reassign the field bytes
fieldBytes = newFieldBytes
}
// hang on to the item id
id := item.ID()
// create a new byte slice
// head+blank+field+tail+id
nbytes := make([]byte,
len(headBytes)+blankSpace+len(fieldBytes)+len(tailBytes)+len(id))
// fill the data
copy(nbytes, headBytes)
copy(nbytes[len(headBytes)+blankSpace:], fieldBytes)
copy(nbytes[len(headBytes)+blankSpace+len(fieldBytes):], tailBytes)
copy(nbytes[len(headBytes)+blankSpace+len(fieldBytes)+len(tailBytes):], id)
// update the field size
item.setFieldsDataSize(len(nbytes) - len(id))
// update the data pointer
item.data = unsafe.Pointer(&nbytes[0])
return true
}
func (item *Item) packedForEachField(count int, iter func(value float64) bool) {
data := item.fieldsBytes()
if count < 0 {
// iterate over of the known the values
for len(data) > 0 {
var value float64
data, value = readPacked(data)
if !iter(value) {
return
}
}
} else {
for i := 0; i < count; i++ {
var value float64
data, value = readPacked(data)
if !iter(value) {
return
}
}
}
}
func (item *Item) packedGetField(index int) float64 {
var idx int
var fvalue float64
item.packedForEachField(-1, func(value float64) bool {
if idx == index {
fvalue = value
return false
}
idx++
return true
})
return fvalue
}

View File

@ -0,0 +1,117 @@
package item
import (
"math/rand"
"testing"
"time"
)
func TestPacked(t *testing.T) {
start := time.Now()
for time.Since(start) < time.Second/2 {
testPacked(t)
}
}
func testPacked(t *testing.T) {
n := rand.Int() % 1024
if n%2 == 1 {
n++
}
values := make([]float64, n)
for i := 0; i < len(values); i++ {
switch rand.Int() % 9 {
case 0:
values[i] = 0
case 1:
values[i] = float64((rand.Int() % 32) - 32/2)
case 2:
values[i] = float64((rand.Int() % 128))
case 3:
values[i] = float64((rand.Int() % 8191) - 8191/2)
case 4:
values[i] = float64((rand.Int() % 2097152) - 2097152/2)
case 5:
values[i] = float64((rand.Int() % 536870912) - 536870912/2)
case 6:
values[i] = float64(rand.Int() % 500)
switch rand.Int() % 4 {
case 1:
values[i] = 0.25
case 2:
values[i] = 0.50
case 3:
values[i] = 0.75
}
case 7:
values[i] = float64(rand.Float32())
case 8:
values[i] = rand.Float64()
}
}
var dst []byte
for i := 0; i < len(values); i++ {
dst = appendPacked(dst, values[i])
}
data := dst
var pvalues []float64
for {
var value float64
data, value = readPacked(data)
if data == nil {
break
}
pvalues = append(pvalues, value)
}
if !floatsEquals(values, pvalues) {
if len(values) != len(pvalues) {
t.Fatalf("sizes not equal")
}
for i := 0; i < len(values); i++ {
if values[i] != pvalues[i] {
t.Fatalf("expected '%v', got '%v'", values[i], pvalues[i])
}
}
}
data = dst
var read int
data, read = skipPacked(data, len(values)/2)
if read != len(values)/2 {
t.Fatalf("expected '%v', got '%v'", len(values)/2, read)
}
data, read = skipPacked(data, len(values)/2)
if read != len(values)/2 {
t.Fatalf("expected '%v', got '%v'", len(values)/2, read)
}
if len(data) != 0 {
t.Fatalf("expected '%v', got '%v'", 0, len(data))
}
}
// func TestPackedItem(t *testing.T) {
// item := New("hello", nil, true)
// values := []float64{0, 1, 1, 0, 0, 1, 1, 0, 1} //, 1} //, 1, 0, 1, 0, 0, 1}
// fmt.Println(values)
// for i := 0; i < len(values); i++ {
// item.SetField(i, values[i])
// }
// fmt.Print("[")
// for j := 0; j < len(values); j++ {
// if j > 0 {
// print(" ")
// }
// fmt.Print(item.GetField(j))
// }
// print("]")
// println(item.ID())
// // for i := 0; i < len(values); i++ {
// // fmt.Println(values[i], item.GetField(i))
// // }
// // fmt.Println(item.GetField(0))
// // println(">>", item.ID())
// }

View File

@ -0,0 +1,95 @@
package item
import (
"reflect"
"unsafe"
)
func getFieldAt(data unsafe.Pointer, index int) float64 {
return *(*float64)(unsafe.Pointer(uintptr(data) + uintptr(index*8)))
}
func setFieldAt(data unsafe.Pointer, index int, value float64) {
*(*float64)(unsafe.Pointer(uintptr(data) + uintptr(index*8))) = value
}
func (item *Item) dataBytes() []byte {
return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(item.data)),
Len: item.fieldsDataSize() + item.idDataSize(),
Cap: item.fieldsDataSize() + item.idDataSize(),
}))
}
func bytesToFloats(f []byte) []float64 {
return *(*[]float64)((unsafe.Pointer)(&reflect.SliceHeader{
Data: ((*reflect.SliceHeader)(unsafe.Pointer(&f))).Data,
Len: len(f) / 8,
Cap: len(f) / 8,
}))
}
func (item *Item) unpackedGenerateFieldBytes(values []float64) []byte {
return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{
Data: ((*reflect.SliceHeader)(unsafe.Pointer(&values))).Data,
Len: len(values) * 8,
Cap: len(values) * 8,
}))
}
func (item *Item) unpackedSetField(index int, value float64) (updated bool) {
numFields := item.fieldsDataSize() / 8
if index < numFields {
// field exists
if getFieldAt(item.data, index) == value {
return false
}
} else if value == 0 {
return false
} else {
// make room for new field
oldBytes := item.dataBytes()
newData := make([]byte, (index+1)*8+item.idDataSize())
// copy the existing fields
copy(newData, oldBytes[:item.fieldsDataSize()])
// copy the id
copy(newData[(index+1)*8:], oldBytes[item.fieldsDataSize():])
// update the fields length
item.setFieldsDataSize((index + 1) * 8)
// update the raw data
item.data = unsafe.Pointer(&newData[0])
}
// set the new field
setFieldAt(item.data, index, value)
return true
}
func (item *Item) unpackedForEachField(
count int, iter func(value float64) bool,
) {
fields := bytesToFloats(item.fieldsBytes())
var n int
if count < 0 {
n = len(fields)
} else {
n = count
}
for i := 0; i < n; i++ {
var field float64
if i < len(fields) {
field = fields[i]
}
if !iter(field) {
return
}
}
}
func (item *Item) unpackedGetField(index int) float64 {
numFields := item.fieldsDataSize() / 8
if index < numFields {
return getFieldAt(item.data, index)
}
return 0
}

View File

@ -46,7 +46,7 @@ func randPoints(N int) []*item.Item {
box.min[j] = rand.Float64() box.min[j] = rand.Float64()
} }
box.max = box.min box.max = box.min
boxes[i] = item.New(fmt.Sprintf("%d", i), box) boxes[i] = item.New(fmt.Sprintf("%d", i), box, false)
} }
return boxes return boxes
} }
@ -68,7 +68,7 @@ func randBoxes(N int) []*item.Item {
if box.max[0] > 180 || box.max[1] > 90 { if box.max[0] > 180 || box.max[1] > 90 {
i-- i--
} }
boxes[i] = item.New(fmt.Sprintf("%d", i), box) boxes[i] = item.New(fmt.Sprintf("%d", i), box, false)
} }
return boxes return boxes
} }
@ -264,7 +264,7 @@ func testBoxesVarious(t *testing.T, items []*item.Item, label string) {
nbox.max[j] = box.max[j] + (rand.Float64() - 0.5) nbox.max[j] = box.max[j] + (rand.Float64() - 0.5)
} }
} }
nboxes[i] = item.New(fmt.Sprintf("%d", i), nbox) nboxes[i] = item.New(fmt.Sprintf("%d", i), nbox, false)
} }
for i := 0; i < N; i++ { for i := 0; i < N; i++ {
tr.Insert(boxMin(nboxes[i]), boxMax(nboxes[i]), nboxes[i]) tr.Insert(boxMin(nboxes[i]), boxMax(nboxes[i]), nboxes[i])

22
vendor/github.com/h2so5/half/.gitignore generated vendored Normal file
View File

@ -0,0 +1,22 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe

121
vendor/github.com/h2so5/half/LICENSE.txt generated vendored Normal file
View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

6
vendor/github.com/h2so5/half/README.md generated vendored Normal file
View File

@ -0,0 +1,6 @@
half
==========
IEEE 754 binary16 half precision format for Go
[![GoDoc](https://godoc.org/github.com/h2so5/half?status.svg)](https://godoc.org/github.com/h2so5/half)

59
vendor/github.com/h2so5/half/float16.go generated vendored Normal file
View File

@ -0,0 +1,59 @@
/*
go-float16 - IEEE 754 binary16 half precision format
Written in 2013 by h2so5 <mail@h2so5.net>
To the extent possible under law, the author(s) have dedicated all copyright and
related and neighboring rights to this software to the public domain worldwide.
This software is distributed without any warranty.
You should have received a copy of the CC0 Public Domain Dedication along with this software.
If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
*/
// Package half is an IEEE 754 binary16 half precision format.
package half
import "math"
// A Float16 represents a 16-bit floating point number.
type Float16 uint16
// NewFloat16 allocates and returns a new Float16 set to f.
func NewFloat16(f float32) Float16 {
i := math.Float32bits(f)
sign := uint16((i >> 31) & 0x1)
exp := (i >> 23) & 0xff
exp16 := int16(exp) - 127 + 15
frac := uint16(i>>13) & 0x3ff
if exp == 0 {
exp16 = 0
} else if exp == 0xff {
exp16 = 0x1f
} else {
if exp16 > 0x1e {
exp16 = 0x1f
frac = 0
} else if exp16 < 0x01 {
exp16 = 0
frac = 0
}
}
f16 := (sign << 15) | uint16(exp16<<10) | frac
return Float16(f16)
}
// Float32 returns the float32 representation of f.
func (f Float16) Float32() float32 {
sign := uint32((f >> 15) & 0x1)
exp := (f >> 10) & 0x1f
exp32 := uint32(exp) + 127 - 15
if exp == 0 {
exp32 = 0
} else if exp == 0x1f {
exp32 = 0xff
}
frac := uint32(f & 0x3ff)
i := (sign << 31) | (exp32 << 23) | (frac << 13)
return math.Float32frombits(i)
}

55
vendor/github.com/h2so5/half/float16_test.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
/*
go-float16 - IEEE 754 binary16 half precision format
Written in 2013 by h2so5 <mail@h2so5.net>
To the extent possible under law, the author(s) have dedicated all copyright and
related and neighboring rights to this software to the public domain worldwide.
This software is distributed without any warranty.
You should have received a copy of the CC0 Public Domain Dedication along with this software.
If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
*/
package half
import (
"math"
"testing"
)
func getFloatTable() map[Float16]float32 {
table := map[Float16]float32{
0x3c00: 1,
0x4000: 2,
0xc000: -2,
0x7bfe: 65472,
0x7bff: 65504,
0xfbff: -65504,
0x0000: 0,
0x8000: float32(math.Copysign(0, -1)),
0x7c00: float32(math.Inf(1)),
0xfc00: float32(math.Inf(-1)),
0x5b8f: 241.875,
0x48c8: 9.5625,
}
return table
}
func TestFloat32(t *testing.T) {
for k, v := range getFloatTable() {
f := k.Float32()
if f != v {
t.Errorf("ToFloat32(%d) = %f, want %f.", k, f, v)
}
}
}
func TestNewFloat16(t *testing.T) {
for k, v := range getFloatTable() {
i := NewFloat16(v)
if i != k {
t.Errorf("FromFloat32(%f) = %d, want %d.", v, i, k)
}
}
}

View File

@ -88,43 +88,14 @@ The dot and wildcard characters can be escaped with '\\'.
``` ```
You can also query an array for the first match by using `#[...]`, or find all matches with `#[...]#`. You can also query an array for the first match by using `#[...]`, or find all matches with `#[...]#`.
Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators and the simple pattern matching `%` operator. Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators and the simple pattern matching `%` (like) and `!%` (not like) operators.
``` ```
friends.#[last=="Murphy"].first >> "Dale" friends.#[last=="Murphy"].first >> "Dale"
friends.#[last=="Murphy"]#.first >> ["Dale","Jane"] friends.#[last=="Murphy"]#.first >> ["Dale","Jane"]
friends.#[age>45]#.last >> ["Craig","Murphy"] friends.#[age>45]#.last >> ["Craig","Murphy"]
friends.#[first%"D*"].last >> "Murphy" friends.#[first%"D*"].last >> "Murphy"
``` friends.#[first!%"D*"].last >> "Craig"
## JSON Lines
There's support for [JSON Lines](http://jsonlines.org/) using the `..` prefix, which treats a multilined document as an array.
For example:
```
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}
```
```
..# >> 4
..1 >> {"name": "Alexa", "age": 34}
..3 >> {"name": "Deloise", "age": 44}
..#.name >> ["Gilbert","Alexa","May","Deloise"]
..#[name="May"].age >> 57
```
The `ForEachLines` function will iterate through JSON lines.
```go
gjson.ForEachLine(json, func(line gjson.Result) bool{
println(line.String())
return true
})
``` ```
## Result Type ## Result Type
@ -193,6 +164,114 @@ result.Int() int64 // -9223372036854775808 to 9223372036854775807
result.Uint() int64 // 0 to 18446744073709551615 result.Uint() int64 // 0 to 18446744073709551615
``` ```
## Modifiers and path chaining
New in version 1.2 is support for modifier functions and path chaining.
A modifier is a path component that performs custom processing on the
json.
Multiple paths can be "chained" together using the pipe character.
This is useful for getting results from a modified query.
For example, using the built-in `@reverse` modifier on the above json document,
we'll get `children` array and reverse the order:
```
"children|@reverse" >> ["Jack","Alex","Sara"]
"children|@reverse|#" >> "Jack"
```
There are currently three built-in modifiers:
- `@reverse`: Reverse an array or the members of an object.
- `@ugly`: Remove all whitespace from a json document.
- `@pretty`: Make the json document more human readable.
### Modifier arguments
A modifier may accept an optional argument. The argument can be a valid JSON
document or just characters.
For example, the `@pretty` modifier takes a json object as its argument.
```
@pretty:{"sortKeys":true}
```
Which makes the json pretty and orders all of its keys.
```json
{
"age":37,
"children": ["Sara","Alex","Jack"],
"fav.movie": "Deer Hunter",
"friends": [
{"age": 44, "first": "Dale", "last": "Murphy"},
{"age": 68, "first": "Roger", "last": "Craig"},
{"age": 47, "first": "Jane", "last": "Murphy"}
],
"name": {"first": "Tom", "last": "Anderson"}
}
```
*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`.
Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.*
### Custom modifiers
You can also add custom modifiers.
For example, here we create a modifier that makes the entire json document upper
or lower case.
```go
gjson.AddModifier("case", func(json, arg string) string {
if arg == "upper" {
return strings.ToUpper(json)
}
if arg == "lower" {
return strings.ToLower(json)
}
return json
})
```
```
"children|@case:upper" >> ["SARA","ALEX","JACK"]
"children|@case:lower|@reverse" >> ["jack","alex","sara"]
```
## JSON Lines
There's support for [JSON Lines](http://jsonlines.org/) using the `..` prefix, which treats a multilined document as an array.
For example:
```
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}
```
```
..# >> 4
..1 >> {"name": "Alexa", "age": 34}
..3 >> {"name": "Deloise", "age": 44}
..#.name >> ["Gilbert","Alexa","May","Deloise"]
..#[name="May"].age >> 57
```
The `ForEachLines` function will iterate through JSON lines.
```go
gjson.ForEachLine(json, func(line gjson.Result) bool{
println(line.String())
return true
})
```
## Get nested array values ## Get nested array values
Suppose you want all the last names from the following json: Suppose you want all the last names from the following json:

View File

@ -15,6 +15,7 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/tidwall/match" "github.com/tidwall/match"
"github.com/tidwall/pretty"
) )
// Type is Result type // Type is Result type
@ -695,6 +696,8 @@ func parseLiteral(json string, i int) (int, string) {
type arrayPathResult struct { type arrayPathResult struct {
part string part string
path string path string
pipe string
piped bool
more bool more bool
alogok bool alogok bool
arrch bool arrch bool
@ -710,6 +713,14 @@ type arrayPathResult struct {
func parseArrayPath(path string) (r arrayPathResult) { func parseArrayPath(path string) (r arrayPathResult) {
for i := 0; i < len(path); i++ { for i := 0; i < len(path); i++ {
if !DisableChaining {
if path[i] == '|' {
r.part = path[:i]
r.pipe = path[i+1:]
r.piped = true
return
}
}
if path[i] == '.' { if path[i] == '.' {
r.part = path[:i] r.part = path[:i]
r.path = path[i+1:] r.path = path[i+1:]
@ -755,7 +766,7 @@ func parseArrayPath(path string) (r arrayPathResult) {
if i < len(path) { if i < len(path) {
s = i s = i
if path[i] == '!' { if path[i] == '!' {
if i < len(path)-1 && path[i+1] == '=' { if i < len(path)-1 && (path[i+1] == '=' || path[i+1] == '%') {
i++ i++
} }
} else if path[i] == '<' || path[i] == '>' { } else if path[i] == '<' || path[i] == '>' {
@ -828,15 +839,28 @@ func parseArrayPath(path string) (r arrayPathResult) {
return return
} }
// DisableChaining will disable the chaining (pipe) syntax
var DisableChaining = false
type objectPathResult struct { type objectPathResult struct {
part string part string
path string path string
wild bool pipe string
more bool piped bool
wild bool
more bool
} }
func parseObjectPath(path string) (r objectPathResult) { func parseObjectPath(path string) (r objectPathResult) {
for i := 0; i < len(path); i++ { for i := 0; i < len(path); i++ {
if !DisableChaining {
if path[i] == '|' {
r.part = path[:i]
r.pipe = path[i+1:]
r.piped = true
return
}
}
if path[i] == '.' { if path[i] == '.' {
r.part = path[:i] r.part = path[:i]
r.path = path[i+1:] r.path = path[i+1:]
@ -934,6 +958,10 @@ func parseObject(c *parseContext, i int, path string) (int, bool) {
var pmatch, kesc, vesc, ok, hit bool var pmatch, kesc, vesc, ok, hit bool
var key, val string var key, val string
rp := parseObjectPath(path) rp := parseObjectPath(path)
if !rp.more && rp.piped {
c.pipe = rp.pipe
c.piped = true
}
for i < len(c.json) { for i < len(c.json) {
for ; i < len(c.json); i++ { for ; i < len(c.json); i++ {
if c.json[i] == '"' { if c.json[i] == '"' {
@ -1099,6 +1127,8 @@ func queryMatches(rp *arrayPathResult, value Result) bool {
return value.Str >= rpv return value.Str >= rpv
case "%": case "%":
return match.Match(value.Str, rpv) return match.Match(value.Str, rpv)
case "!%":
return !match.Match(value.Str, rpv)
} }
case Number: case Number:
rpvn, _ := strconv.ParseFloat(rpv, 64) rpvn, _ := strconv.ParseFloat(rpv, 64)
@ -1157,6 +1187,10 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
partidx = int(n) partidx = int(n)
} }
} }
if !rp.more && rp.piped {
c.pipe = rp.pipe
c.piped = true
}
for i < len(c.json)+1 { for i < len(c.json)+1 {
if !rp.arrch { if !rp.arrch {
pmatch = partidx == h pmatch = partidx == h
@ -1351,6 +1385,8 @@ func ForEachLine(json string, iterator func(line Result) bool) {
type parseContext struct { type parseContext struct {
json string json string
value Result value Result
pipe string
piped bool
calcd bool calcd bool
lines bool lines bool
} }
@ -1388,6 +1424,22 @@ type parseContext struct {
// If you are consuming JSON from an unpredictable source then you may want to // If you are consuming JSON from an unpredictable source then you may want to
// use the Valid function first. // use the Valid function first.
func Get(json, path string) Result { func Get(json, path string) Result {
if !DisableModifiers {
if len(path) > 1 && path[0] == '@' {
// possible modifier
var ok bool
var rjson string
path, rjson, ok = execModifier(json, path)
if ok {
if len(path) > 0 && path[0] == '|' {
res := Get(rjson, path[1:])
res.Index = 0
return res
}
return Parse(rjson)
}
}
}
var i int var i int
var c = &parseContext{json: json} var c = &parseContext{json: json}
if len(path) >= 2 && path[0] == '.' && path[1] == '.' { if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
@ -1407,6 +1459,11 @@ func Get(json, path string) Result {
} }
} }
} }
if c.piped {
res := c.value.Get(c.pipe)
res.Index = 0
return res
}
fillIndex(json, c) fillIndex(json, c)
return c.value return c.value
} }
@ -1626,7 +1683,11 @@ func GetMany(json string, path ...string) []Result {
// The return value is a Result array where the number of items // The return value is a Result array where the number of items
// will be equal to the number of input paths. // will be equal to the number of input paths.
func GetManyBytes(json []byte, path ...string) []Result { func GetManyBytes(json []byte, path ...string) []Result {
return GetMany(string(json), path...) res := make([]Result, len(path))
for i, path := range path {
res[i] = GetBytes(json, path)
}
return res
} }
var fieldsmu sync.RWMutex var fieldsmu sync.RWMutex
@ -2032,7 +2093,7 @@ func validnull(data []byte, i int) (outi int, ok bool) {
// value := gjson.Get(json, "name.last") // value := gjson.Get(json, "name.last")
// //
func Valid(json string) bool { func Valid(json string) bool {
_, ok := validpayload([]byte(json), 0) _, ok := validpayload(stringBytes(json), 0)
return ok return ok
} }
@ -2108,3 +2169,146 @@ func floatToInt(f float64) (n int64, ok bool) {
} }
return 0, false return 0, false
} }
// execModifier parses the path to find a matching modifier function.
// then input expects that the path already starts with a '@'
func execModifier(json, path string) (pathOut, res string, ok bool) {
name := path[1:]
var hasArgs bool
for i := 1; i < len(path); i++ {
if path[i] == ':' {
pathOut = path[i+1:]
name = path[1:i]
hasArgs = len(pathOut) > 0
break
}
if !DisableChaining {
if path[i] == '|' {
pathOut = path[i:]
name = path[1:i]
break
}
}
}
if fn, ok := modifiers[name]; ok {
var args string
if hasArgs {
var parsedArgs bool
switch pathOut[0] {
case '{', '[', '"':
res := Parse(pathOut)
if res.Exists() {
_, args = parseSquash(pathOut, 0)
pathOut = pathOut[len(args):]
parsedArgs = true
}
}
if !parsedArgs {
idx := -1
if !DisableChaining {
idx = strings.IndexByte(pathOut, '|')
}
if idx == -1 {
args = pathOut
pathOut = ""
} else {
args = pathOut[:idx]
pathOut = pathOut[idx:]
}
}
}
return pathOut, fn(json, args), true
}
return pathOut, res, false
}
// DisableModifiers will disable the modifier syntax
var DisableModifiers = false
var modifiers = map[string]func(json, arg string) string{
"pretty": modPretty,
"ugly": modUgly,
"reverse": modReverse,
}
// AddModifier binds a custom modifier command to the GJSON syntax.
// This operation is not thread safe and should be executed prior to
// using all other gjson function.
func AddModifier(name string, fn func(json, arg string) string) {
modifiers[name] = fn
}
// ModifierExists returns true when the specified modifier exists.
func ModifierExists(name string, fn func(json, arg string) string) bool {
_, ok := modifiers[name]
return ok
}
// @pretty modifier makes the json look nice.
func modPretty(json, arg string) string {
if len(arg) > 0 {
opts := *pretty.DefaultOptions
Parse(arg).ForEach(func(key, value Result) bool {
switch key.String() {
case "sortKeys":
opts.SortKeys = value.Bool()
case "indent":
opts.Indent = value.String()
case "prefix":
opts.Prefix = value.String()
case "width":
opts.Width = int(value.Int())
}
return true
})
return bytesString(pretty.PrettyOptions(stringBytes(json), &opts))
}
return bytesString(pretty.Pretty(stringBytes(json)))
}
// @ugly modifier removes all whitespace.
func modUgly(json, arg string) string {
return bytesString(pretty.Ugly(stringBytes(json)))
}
// @reverse reverses array elements or root object members.
func modReverse(json, arg string) string {
res := Parse(json)
if res.IsArray() {
var values []Result
res.ForEach(func(_, value Result) bool {
values = append(values, value)
return true
})
out := make([]byte, 0, len(json))
out = append(out, '[')
for i, j := len(values)-1, 0; i >= 0; i, j = i-1, j+1 {
if j > 0 {
out = append(out, ',')
}
out = append(out, values[i].Raw...)
}
out = append(out, ']')
return bytesString(out)
}
if res.IsObject() {
var keyValues []Result
res.ForEach(func(key, value Result) bool {
keyValues = append(keyValues, key, value)
return true
})
out := make([]byte, 0, len(json))
out = append(out, '{')
for i, j := len(keyValues)-2, 0; i >= 0; i, j = i-2, j+1 {
if j > 0 {
out = append(out, ',')
}
out = append(out, keyValues[i+0].Raw...)
out = append(out, ':')
out = append(out, keyValues[i+1].Raw...)
}
out = append(out, '}')
return bytesString(out)
}
return json
}

View File

@ -1,4 +1,4 @@
//+build appengine //+build appengine js
package gjson package gjson
@ -8,3 +8,11 @@ func getBytes(json []byte, path string) Result {
func fillIndex(json string, c *parseContext) { func fillIndex(json string, c *parseContext) {
// noop. Use zero for the Index value. // noop. Use zero for the Index value.
} }
func stringBytes(s string) []byte {
return []byte(s)
}
func bytesString(b []byte) string {
return string(b)
}

View File

@ -1,4 +1,5 @@
//+build !appengine //+build !appengine
//+build !js
package gjson package gjson
@ -15,45 +16,40 @@ func getBytes(json []byte, path string) Result {
if json != nil { if json != nil {
// unsafe cast to string // unsafe cast to string
result = Get(*(*string)(unsafe.Pointer(&json)), path) result = Get(*(*string)(unsafe.Pointer(&json)), path)
result = fromBytesGet(result) // safely get the string headers
} rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw))
return result strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str))
} // create byte slice headers
rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len}
func fromBytesGet(result Result) Result { strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len}
// safely get the string headers if strh.Data == 0 {
rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw)) // str is nil
strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str)) if rawh.Data == 0 {
// create byte slice headers // raw is nil
rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len} result.Raw = ""
strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len} } else {
if strh.Data == 0 { // raw has data, safely copy the slice header to a string
// str is nil result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
if rawh.Data == 0 { }
result.Str = ""
} else if rawh.Data == 0 {
// raw is nil // raw is nil
result.Raw = "" result.Raw = ""
} else { // str has data, safely copy the slice header to a string
// raw has data, safely copy the slice header to a string result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
} else if strh.Data >= rawh.Data &&
int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len {
// Str is a substring of Raw.
start := int(strh.Data - rawh.Data)
// safely copy the raw slice header
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
// substring the raw
result.Str = result.Raw[start : start+strh.Len]
} else {
// safely copy both the raw and str slice headers to strings
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
} }
result.Str = ""
} else if rawh.Data == 0 {
// raw is nil
result.Raw = ""
// str has data, safely copy the slice header to a string
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
} else if strh.Data >= rawh.Data &&
int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len {
// Str is a substring of Raw.
start := int(strh.Data - rawh.Data)
// safely copy the raw slice header
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
// substring the raw
result.Str = result.Raw[start : start+strh.Len]
} else {
// safely copy both the raw and str slice headers to strings
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
} }
return result return result
} }
@ -71,3 +67,15 @@ func fillIndex(json string, c *parseContext) {
} }
} }
} }
func stringBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: (*reflect.StringHeader)(unsafe.Pointer(&s)).Data,
Len: len(s),
Cap: len(s),
}))
}
func bytesString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -11,6 +11,8 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/tidwall/pretty"
) )
// TestRandomData is a fuzzing test that throws random data at the Parse // TestRandomData is a fuzzing test that throws random data at the Parse
@ -403,6 +405,10 @@ func TestBasic2(t *testing.T) {
if mtok.String() != "aaaa" { if mtok.String() != "aaaa" {
t.Fatalf("expected %v, got %v", "aaaa", mtok.String()) t.Fatalf("expected %v, got %v", "aaaa", mtok.String())
} }
mtok = get(basicJSON, `loggy.programmers.#[firstName !% "Bre*"].email`)
if mtok.String() != "bbbb" {
t.Fatalf("expected %v, got %v", "bbbb", mtok.String())
}
mtok = get(basicJSON, `loggy.programmers.#[firstName == "Brett"].email`) mtok = get(basicJSON, `loggy.programmers.#[firstName == "Brett"].email`)
if mtok.String() != "aaaa" { if mtok.String() != "aaaa" {
t.Fatalf("expected %v, got %v", "aaaa", mtok.String()) t.Fatalf("expected %v, got %v", "aaaa", mtok.String())
@ -1357,7 +1363,7 @@ null
} }
func TestNumUint64String(t *testing.T) { func TestNumUint64String(t *testing.T) {
i := 9007199254740993 //2^53 + 1 var i int64 = 9007199254740993 //2^53 + 1
j := fmt.Sprintf(`{"data": [ %d, "hello" ] }`, i) j := fmt.Sprintf(`{"data": [ %d, "hello" ] }`, i)
res := Get(j, "data.0") res := Get(j, "data.0")
if res.String() != "9007199254740993" { if res.String() != "9007199254740993" {
@ -1366,7 +1372,7 @@ func TestNumUint64String(t *testing.T) {
} }
func TestNumInt64String(t *testing.T) { func TestNumInt64String(t *testing.T) {
i := -9007199254740993 var i int64 = -9007199254740993
j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i) j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i)
res := Get(j, "data.1") res := Get(j, "data.1")
if res.String() != "-9007199254740993" { if res.String() != "-9007199254740993" {
@ -1384,7 +1390,7 @@ func TestNumBigString(t *testing.T) {
} }
func TestNumFloatString(t *testing.T) { func TestNumFloatString(t *testing.T) {
i := -9007199254740993 var i int64 = -9007199254740993
j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i) //No quotes around value!! j := fmt.Sprintf(`{"data":[ "hello", %d ]}`, i) //No quotes around value!!
res := Get(j, "data.1") res := Get(j, "data.1")
if res.String() != "-9007199254740993" { if res.String() != "-9007199254740993" {
@ -1427,3 +1433,74 @@ func TestArrayValues(t *testing.T) {
} }
} }
func BenchmarkValid(b *testing.B) {
for i := 0; i < b.N; i++ {
Valid(complicatedJSON)
}
}
func BenchmarkValidBytes(b *testing.B) {
complicatedJSON := []byte(complicatedJSON)
for i := 0; i < b.N; i++ {
ValidBytes(complicatedJSON)
}
}
func BenchmarkGoStdlibValidBytes(b *testing.B) {
complicatedJSON := []byte(complicatedJSON)
for i := 0; i < b.N; i++ {
json.Valid(complicatedJSON)
}
}
func TestModifier(t *testing.T) {
json := `{"other":{"hello":"world"},"arr":[1,2,3,4,5,6]}`
opts := *pretty.DefaultOptions
opts.SortKeys = true
exp := string(pretty.PrettyOptions([]byte(json), &opts))
res := Get(json, `@pretty:{"sortKeys":true}`).String()
if res != exp {
t.Fatalf("expected '%v', got '%v'", exp, res)
}
res = Get(res, "@pretty|@reverse|@ugly").String()
if res != json {
t.Fatalf("expected '%v', got '%v'", json, res)
}
res = Get(res, "@pretty|@reverse|arr|@reverse|2").String()
if res != "4" {
t.Fatalf("expected '%v', got '%v'", "4", res)
}
AddModifier("case", func(json, arg string) string {
if arg == "upper" {
return strings.ToUpper(json)
}
if arg == "lower" {
return strings.ToLower(json)
}
return json
})
res = Get(json, "other|@case:upper").String()
if res != `{"HELLO":"WORLD"}` {
t.Fatalf("expected '%v', got '%v'", `{"HELLO":"WORLD"}`, res)
}
}
func TestChaining(t *testing.T) {
json := `{
"friends": [
{"first": "Dale", "last": "Murphy", "age": 44},
{"first": "Roger", "last": "Craig", "age": 68},
{"first": "Jane", "last": "Murphy", "age": 47}
]
}`
res := Get(json, "friends|0|first").String()
if res != "Dale" {
t.Fatalf("expected '%v', got '%v'", "Dale", res)
}
res = Get(json, "friends|@reverse|0|age").String()
if res != "47" {
t.Fatalf("expected '%v', got '%v'", "47", res)
}
}