diff --git a/internal/server/json.go b/internal/server/json.go index 762fcf1e..d8d3ed8b 100644 --- a/internal/server/json.go +++ b/internal/server/json.go @@ -40,6 +40,55 @@ func jsonString(s string) string { b[len(b)-1] = '"' return string(b) } + +func isJsonNumber(s string) bool { + // Returns true if the given string can be encoded as a JSON number value. + // See: + // https://json.org + // http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf + l := len(s) + if l == 0 { + return false + } + i := 0 + if s[i] == '-' { + i++ + } + switch { + case i == l: + return false + case s[i] == '0': // 0 must not be followed by any digits. + i++ + break + case s[i] >= '1' && s[0] <= '9': + for i++; i < l && s[i] >= '0' && s[i] <= '9'; i++ { // scan over digits + } + default: + return false + } + if i == l { + return true + } + if l-i > 1 && s[i] == '.' { + for i++; i < l && s[i] >= '0' && s[i] <= '9'; i++ { + } + } + if l-i > 1 && (s[i] == 'e' || s[i] == 'E') { + i++ + if s[i] == '-' || s[i] == '+' { + i++ + } + switch { + case i == l: + return false + case s[i] >= '0' && s[0] <= '9': + for i++; i < l && s[i] >= '0' && s[i] <= '9'; i++ { + } + } + } + return i == l +} + func appendJSONSimpleBounds(dst []byte, o geojson.Object) []byte { bbox := o.Rect() dst = append(dst, `{"sw":{"lat":`...) @@ -183,13 +232,7 @@ func (c *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err er if !str && !raw { switch val { default: - if len(val) > 0 { - if (val[0] >= '0' && val[0] <= '9') || val[0] == '-' { - if _, err := strconv.ParseFloat(val, 64); err == nil { - raw = true - } - } - } + raw = isJsonNumber(val) case "true", "false", "null": raw = true } diff --git a/internal/server/json_test.go b/internal/server/json_test.go index f9ccd4ee..db2f50da 100644 --- a/internal/server/json_test.go +++ b/internal/server/json_test.go @@ -18,3 +18,29 @@ func BenchmarkJSONMarshal(t *testing.B) { json.Marshal(s) } } + +func TestIsJsonNumber(t *testing.T) { + test := func(expected bool, val string) { + actual := isJsonNumber(val) + if expected != actual { + t.Fatalf("Expected %t == isJsonNumber(\"%s\") but was %t", expected, val, actual) + } + } + test(false, "") + test(false, "-") + test(false, "foo") + test(false, "0123") + test(false, "1.") + test(false, "1.0e") + test(false, "1.0e-") + test(false, "1.0E10NaN") + test(false, "1.0ENaN") + test(true, "-1") + test(true, "0") + test(true, "0.0") + test(true, "42") + test(true, "1.0E10") + test(true, "1.0e10") + test(true, "1E+5") + test(true, "1E-10") +} \ No newline at end of file diff --git a/tests/json_test.go b/tests/json_test.go index e0d02c1d..356a251a 100644 --- a/tests/json_test.go +++ b/tests/json_test.go @@ -5,6 +5,8 @@ import "testing" func subTestJSON(t *testing.T, mc *mockServer) { runStep(t, mc, "basic", json_JSET_basic_test) runStep(t, mc, "geojson", json_JSET_geojson_test) + runStep(t, mc, "number", json_JSET_number_test) + } func json_JSET_basic_test(mc *mockServer) error { return mc.DoBatch([][]interface{}{ @@ -39,3 +41,16 @@ func json_JSET_geojson_test(mc *mockServer) error { {"JDEL", "mykey", "myid1", "type"}, {"ERR missing type"}, }) } + +func json_JSET_number_test(mc *mockServer) error { + return mc.DoBatch([][]interface{}{ + {"JSET", "mykey", "myid1", "hello", "0"}, {"OK"}, + {"JGET", "mykey", "myid1"}, {`{"hello":0}`}, + {"JSET", "mykey", "myid1", "hello", "0123"}, {"OK"}, + {"JGET", "mykey", "myid1"}, {`{"hello":"0123"}`}, + {"JSET", "mykey", "myid1", "hello", "3.14"}, {"OK"}, + {"JGET", "mykey", "myid1"}, {`{"hello":3.14}`}, + {"JSET", "mykey", "myid1", "hello", "1.0e10"}, {"OK"}, + {"JGET", "mykey", "myid1"}, {`{"hello":1.0e10}`}, + }) +} \ No newline at end of file