From 2ae9729396903f21350b4281762009a5bb1cbecc Mon Sep 17 00:00:00 2001 From: Nour Balaha Date: Fri, 14 Jun 2024 15:11:48 +0900 Subject: [PATCH] feat(server): implement geo field's position (#1176) * wip: position value * refactor to value * refactor 2 * wip: unit tests * fix: mapToFloat64 * add Test_propertyPosition_ToValue * add propertyPosition to registry * add more unit tests * fix: float32 to float64 conversion * add decimal numbers to unit tests * fix: bad import in list_test --- server/pkg/item/view/list_test.go | 2 +- server/pkg/value/position.go | 201 ++++++++++++++++++++++++++++++ server/pkg/value/position_test.go | 180 ++++++++++++++++++++++++++ server/pkg/value/registry.go | 1 + 4 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 server/pkg/value/position.go create mode 100644 server/pkg/value/position_test.go diff --git a/server/pkg/item/view/list_test.go b/server/pkg/item/view/list_test.go index 8617f0d56f..f33acacec4 100644 --- a/server/pkg/item/view/list_test.go +++ b/server/pkg/item/view/list_test.go @@ -3,8 +3,8 @@ package view import ( "testing" - "github.com/go-playground/assert/v2" "github.com/reearth/reearth-cms/server/pkg/id" + "github.com/stretchr/testify/assert" ) func TestList_Ordered(t *testing.T) { diff --git a/server/pkg/value/position.go b/server/pkg/value/position.go new file mode 100644 index 0000000000..9a91902cd9 --- /dev/null +++ b/server/pkg/value/position.go @@ -0,0 +1,201 @@ +package value + +import ( + "encoding/json" + "slices" + "strconv" + + "github.com/samber/lo" +) + +const TypePoint Type = "point" +// const TypeLineString Type = "lineString" +// const TypePolygon Type = "polygon" + +type propertyPosition struct{} + +type Position = []float64 + +func (p *propertyPosition) ToValue(i any) (any, bool) { + if i == nil { + return nil, true + } + + switch v := i.(type) { + case []float64: + return v, true + case []float32: + return mapFloat32ToFloat64(v) + case []int: + return mapIntegersToFloat64(v), true + case []int8: + return mapIntegersToFloat64(v), true + case []int16: + return mapIntegersToFloat64(v), true + case []int32: + return mapIntegersToFloat64(v), true + case []int64: + return mapIntegersToFloat64(v), true + case []uint: + return mapIntegersToFloat64(v), true + case []uint8: + return mapIntegersToFloat64(v), true + case []uint16: + return mapIntegersToFloat64(v), true + case []uint32: + return mapIntegersToFloat64(v), true + case []uint64: + return mapIntegersToFloat64(v), true + case []uintptr: + return mapIntegersToFloat64(v), true + case []json.Number: + return mapJSONNumbersToFloat64(v) + case []string: + return mapStringsToFloat64(v) + default: + return nil, false + } +} + +func mapIntegersToFloat64[T any](v []T) []float64 { + return lo.Map(v, func(n T, _ int) float64 { + return intToFloat64(n) + }) +} + +func intToFloat64(v any) float64 { + switch val := v.(type) { + case int: + return float64(val) + case int8: + return float64(val) + case int16: + return float64(val) + case int32: + return float64(val) + case int64: + return float64(val) + case uint: + return float64(val) + case uint8: + return float64(val) + case uint16: + return float64(val) + case uint32: + return float64(val) + case uint64: + return float64(val) + case uintptr: + return float64(val) + default: + return 0 + } +} + +func mapStringsToFloat64(v []string) ([]float64, bool) { + var err error + s := lo.Map(v, func(s string, _ int) float64 { + vv, err2 := strconv.ParseFloat(s, 64) + if err2 != nil { + err = err2 + return 0 + } + return vv + }) + if err != nil { + return nil, false + } + return s, true +} + +func mapJSONNumbersToFloat64(v []json.Number) ([]float64, bool) { + var err error + s := lo.Map(v, func(n json.Number, _ int) float64 { + vv, err2 := n.Float64() + if err2 != nil { + err = err2 + return 0 + } + return vv + }) + if err != nil { + return nil, false + } + return s, true +} + +func mapFloat32ToFloat64(v []float32) ([]float64, bool) { + var err error + s := lo.Map(v, func(n float32, _ int) float64 { + ss := strconv.FormatFloat(float64(n), 'f', -1, 32) + vv, err2 := strconv.ParseFloat(ss, 64) + if err2 != nil { + err = err2 + return 0 + } + return vv + }) + if err != nil { + return nil, false + } + return s, true +} + +func (*propertyPosition) ToInterface(v any) (any, bool) { + return v, true +} + +func (*propertyPosition) Validate(i any) bool { + v, ok := i.(Position) + if !ok { + return false + } + return len(v) >= 2 +} + +func (*propertyPosition) Equal(v, w any) bool { + vv := v.(Position) + ww := w.(Position) + if len(vv) != len(ww) { + return false + } + return slices.Equal(vv, ww) +} + +func (*propertyPosition) IsEmpty(i any) bool { + if i == nil { + return true + } + v, ok := i.(Position) + if !ok { + return true + } + return len(v) == 0 +} + +func (v *Value) ValuePosition() (vv Position, ok bool) { + if v == nil { + return + } + vv, ok = v.v.(Position) + if !ok { + return nil, false + } + if len(vv) > 3 { + return vv[:3], true // TODO: need to think about his case + } + return +} + +func (m *Multiple) ValuesPosition() (vv []Position, ok bool) { + if m == nil { + return + } + vv = lo.FilterMap(m.v, func(v *Value, _ int) (Position, bool) { + return v.ValuePosition() + }) + if len(vv) != len(m.v) { + return nil, false + } + return vv, true +} diff --git a/server/pkg/value/position_test.go b/server/pkg/value/position_test.go new file mode 100644 index 0000000000..c80cb5ad73 --- /dev/null +++ b/server/pkg/value/position_test.go @@ -0,0 +1,180 @@ +package value + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_propertyPosition_ToValue(t *testing.T) { + tests := []struct { + name string + arg any + want1 any + want2 bool + }{ + { + name: "nil", + arg: nil, + want1: nil, + want2: true, + }, + { + name: "string", + arg: []string{"1.12345", "2.12345"}, + want1: []float64{1.12345, 2.12345}, + want2: true, + }, + { + name: "json.Number", + arg: []json.Number{"1.12345", "2.12345"}, + want1: []float64{1.12345, 2.12345}, + want2: true, + }, + { + name: "float64", + arg: []float64{1.12345, 2.12345}, + want1: []float64{1.12345, 2.12345}, + want2: true, + }, + { + name: "float32", + arg: []float32{1.1234567, 2.12345}, + want1: []float64{1.1234567, 2.12345}, + want2: true, + }, + { + name: "int", + arg: []int{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "int8", + arg: []int8{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "int16", + arg: []int16{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "int32", + arg: []int32{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "int64", + arg: []int64{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uint", + arg: []uint{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uint8", + arg: []uint8{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uint16", + arg: []uint16{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uint32", + arg: []uint32{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uint64", + arg: []uint64{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + { + name: "uintptr", + arg: []uintptr{1, 2}, + want1: []float64{1.0, 2.0}, + want2: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + p := &propertyPosition{} + got1, got2 := p.ToValue(tt.arg) + assert.Equal(t, tt.want1, got1) + assert.Equal(t, tt.want2, got2) + }) + } +} + +func Test_propertyPosition_ToInterface(t *testing.T) { + v := []float64{1.1, 2.1, 3.1} + tt, ok := (&propertyPosition{}).ToInterface(v) + assert.Equal(t, v, tt) + assert.Equal(t, true, ok) +} + +func Test_propertyPosition_IsEmpty(t *testing.T) { + assert.True(t, (&propertyPosition{}).IsEmpty([]float64{})) + assert.False(t, (&propertyPosition{}).IsEmpty([]float64{1.1, 2.1, 3.1})) +} + +func Test_propertyPosition_Validate(t *testing.T) { + assert.True(t, (&propertyPosition{}).Validate([]float64{1.1, 2.1, 3.1})) + assert.False(t, (&propertyPosition{}).Validate([]float64{1.1})) + assert.False(t, (&propertyPosition{}).Validate([]int{1, 2, 3})) + assert.False(t, (&propertyPosition{}).Validate([]string{"1", "2", "3"})) + assert.False(t, (&propertyPosition{}).Validate(1)) +} + +func Test_propertyPosition_Equal(t *testing.T) { + ps := &propertyPosition{} + assert.True(t, ps.Equal(Position{1.1, 2.1, 3.1}, Position{1.1, 2.1, 3.1})) + ps1 := &propertyPosition{} + assert.False(t, ps1.Equal(Position{1.1, 2.1, 3.1}, Position{1.1, 2.1})) +} + +func TestValue_ValuePosition(t *testing.T) { + var v *Value + got, ok := v.ValuePosition() + assert.Equal(t, []float64(nil), got) + assert.Equal(t, false, ok) + + v = &Value{ + v: []float64{1.1, 2.1, 3.1}, + } + got, ok = v.ValuePosition() + assert.Equal(t, []float64{1.1, 2.1, 3.1}, got) + assert.Equal(t, true, ok) +} + +func TestMultiple_ValuesPosition(t *testing.T) { + var m *Multiple + got, ok := m.ValuesPosition() + var expected []Position + assert.Equal(t, expected, got) + assert.False(t, ok) + + m = NewMultiple(TypePoint, []any{Position{1.1, 2.1, 3.1}, Position{1.1, 2.1, 3.1}, Position{1.1, 2.1, 3.1}}) + expected = []Position{{1.1, 2.1, 3.1}, {1.1, 2.1, 3.1}, {1.1, 2.1, 3.1}} + got, ok = m.ValuesPosition() + assert.Equal(t, expected, got) + assert.True(t, ok) +} diff --git a/server/pkg/value/registry.go b/server/pkg/value/registry.go index 25efee2999..70c31c53ab 100644 --- a/server/pkg/value/registry.go +++ b/server/pkg/value/registry.go @@ -16,6 +16,7 @@ var defaultTypes = TypeRegistry{ TypeGroup: &propertyGroup{}, TypeReference: &propertyReference{}, TypeURL: &propertyURL{}, + TypePoint: &propertyPosition{}, } type TypeRegistry map[Type]TypeProperty