diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eddccb..2c0c879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm The structure and content of this file follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.26.0] - 2024-12-31 +### Added +- Support for non-selector scripts added to the jp package. See the + `jp.CompileScript` variable along with the `Proc` fragment type and + `Procedure` interface. + ## [1.25.1] - 2024-12-26 ### Fixed - Fixed precision loss with some fraction parsing. diff --git a/jp/get.go b/jp/get.go index 6126482..dc2e2ac 100644 --- a/jp/get.go +++ b/jp/get.go @@ -836,6 +836,28 @@ func (x Expr) Get(data any) (results []any) { stack = stack[:before] } } + case *Proc: + got := tf.Procedure.Get(prev) + if int(fi) == len(x)-1 { // last one + results = append(results, got...) + } else { + for i := len(got) - 1; 0 <= i; i-- { + v = got[i] + switch v.(type) { + case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String, + int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int: + case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed: + stack = append(stack, v) + default: + if rt := reflect.TypeOf(v); rt != nil { + switch rt.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map: + stack = append(stack, v) + } + } + } + } + } } if int(fi) < len(x)-1 { if _, ok := stack[len(stack)-1].(fragIndex); !ok { @@ -1626,6 +1648,28 @@ func (x Expr) FirstFound(data any) (any, bool) { return result, true } } + case *Proc: + if int(fi) == len(x)-1 { // last one + return tf.Procedure.First(prev), true + } else { + got := tf.Procedure.Get(prev) + for i := len(got) - 1; 0 <= i; i-- { + v = got[i] + switch v.(type) { + case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String, + int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int: + case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed: + stack = append(stack, v) + default: + if rt := reflect.TypeOf(v); rt != nil { + switch rt.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map: + stack = append(stack, v) + } + } + } + } + } } if int(fi) < len(x)-1 { if _, ok := stack[len(stack)-1].(fragIndex); !ok { diff --git a/jp/parse.go b/jp/parse.go index ff9d887..e7ee49c 100644 --- a/jp/parse.go +++ b/jp/parse.go @@ -213,7 +213,7 @@ func (p *parser) afterBracket() Frag { case '?': return p.readFilter() case '(': - p.raise("scripts not implemented yet") + return p.readProc() case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': var i int i, b = p.readInt(b) @@ -561,6 +561,19 @@ func (p *parser) readFilter() *Filter { return eq.Filter() } +func (p *parser) readProc() *Proc { + end := bytes.Index(p.buf, []byte{')', ']'}) + if end < 0 { + p.raise("not terminated") + } + end++ + code := p.buf[p.pos-1 : end] + p.pos = end + 1 + + return MustNewProc(code) + +} + // Reads an equation by reading the left value first and then seeing if there // is an operation after that. If so it reads the next equation and decides // based on precedent which is contained in the other. diff --git a/jp/parse_test.go b/jp/parse_test.go index 48fbdfe..7b21e89 100644 --- a/jp/parse_test.go +++ b/jp/parse_test.go @@ -17,6 +17,7 @@ type xdata struct { } func TestParse(t *testing.T) { + jp.CompileScript = nil for i, d := range []xdata{ {src: "@", expect: "@"}, {src: "$", expect: "$"}, @@ -87,7 +88,8 @@ func TestParse(t *testing.T) { {src: "[]", err: "parse error at 2 in []"}, {src: "[**", err: "not terminated at 4 in [**"}, {src: "['x'z]", err: "invalid bracket fragment at 6 in ['x'z]"}, - {src: "[(x)]", err: "scripts not implemented yet at 3 in [(x)]"}, + {src: "[(x)]", err: "jp.CompileScript has not been set"}, + {src: "[(x)", err: "not terminated at 3 in [(x)"}, {src: "[-x]", err: "expected a number at 4 in [-x]"}, {src: "[0x]", err: "invalid bracket fragment at 4 in [0x]"}, {src: "[x]", err: "parse error at 2 in [x]"}, diff --git a/jp/proc.go b/jp/proc.go new file mode 100644 index 0000000..6e72362 --- /dev/null +++ b/jp/proc.go @@ -0,0 +1,88 @@ +// Copyright (c) 2024, Peter Ohler, All rights reserved. + +package jp + +import ( + "fmt" +) + +// CompileScript if non-nil should return object that implments the Procedure +// interface. This function is called when a script notation bracketed by [( +// and )] is encountered. Note the string code argument will included the open +// and close parenthesis but not the square brackets. +var CompileScript func(code []byte) Procedure + +// Proc is a script used as a procedure which is a script not limited to being +// a selector. While both Locate() and Walk() are supported the results may +// not be as expected since the procedure can modify the original +// data. Remove() is not supported with this fragment type. +type Proc struct { + Procedure Procedure + Script []byte +} + +// MustNewProc creates a new Proc and panics on error. +func MustNewProc(code []byte) (p *Proc) { + if CompileScript == nil { + panic(fmt.Errorf("jp.CompileScript has not been set")) + } + return &Proc{ + Procedure: CompileScript(code), + Script: code, + } +} + +// String representation of the proc. +func (p *Proc) String() string { + return string(p.Append([]byte{}, true, false)) +} + +// Append a fragment string representation of the fragment to the buffer +// then returning the expanded buffer. +func (p *Proc) Append(buf []byte, _, _ bool) []byte { + buf = append(buf, "["...) + buf = append(buf, p.Script...) + + return append(buf, ']') +} + +func (p *Proc) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) { + got := p.Procedure.Get(data) + if len(rest) == 0 { // last one + for i := range got { + locs = locateAppendFrag(locs, pp, Nth(i)) + if 0 < max && max <= len(locs) { + break + } + } + } else { + cp := append(pp, nil) // place holder + for i, v := range got { + cp[len(pp)] = Nth(i) + locs = locateContinueFrag(locs, cp, v, rest, max) + if 0 < max && max <= len(locs) { + break + } + } + } + return +} + +// Walk each element returned from the procedure call. Note that this may or +// may not correspond to the original data as the procedure can modify not only +// the elements in the original data but also the contents of each. +func (p *Proc) Walk(rest, path Expr, nodes []any, cb func(path Expr, nodes []any)) { + path = append(path, nil) + data := nodes[len(nodes)-1] + nodes = append(nodes, nil) + + for i, v := range p.Procedure.Get(data) { + path[len(path)-1] = Nth(i) + nodes[len(nodes)-1] = v + if 0 < len(rest) { + rest[0].Walk(rest[1:], path, nodes, cb) + } else { + cb(path, nodes) + } + } +} diff --git a/jp/proc_test.go b/jp/proc_test.go new file mode 100644 index 0000000..ef2ebc7 --- /dev/null +++ b/jp/proc_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2024, Peter Ohler, All rights reserved. + +package jp_test + +import ( + "fmt" + "testing" + + "github.com/ohler55/ojg/jp" + "github.com/ohler55/ojg/pretty" + "github.com/ohler55/ojg/tt" +) + +type mathProc struct { + op rune + left int + right int +} + +func (mp *mathProc) Get(data any) []any { + return []any{mp.First(data)} +} + +func (mp *mathProc) First(data any) any { + a, ok := data.([]any) + if ok { + var ( + left int + right int + ) + if mp.left < len(a) { + left, _ = a[mp.left].(int) + } + if mp.right < len(a) { + right, _ = a[mp.right].(int) + } + switch mp.op { + case '+': + return left + right + case '-': + return left - right + } + return 0 + } + return nil +} + +func compileMathProc(code []byte) jp.Procedure { + var mp mathProc + _, _ = fmt.Sscanf(string(code), "(%c %d %d)", &mp.op, &mp.left, &mp.right) + + return &mp +} + +type mapProc struct{} + +func (mp mapProc) Get(data any) (result []any) { + a, _ := data.([]any) + for i, v := range a { + result = append(result, map[string]any{"i": i, "v": v}) + } + return +} + +func (mp mapProc) First(data any) any { + if a, _ := data.([]any); 0 < len(a) { + return map[string]any{"i": 0, "v": a[0]} + } + return nil +} + +func compileMapProc(code []byte) jp.Procedure { + return mapProc{} +} + +type mapIntProc struct{} + +func (mip mapIntProc) Get(data any) (result []any) { + a, _ := data.([]int) + for i, v := range a { + result = append(result, map[string]int{"i": i, "v": v}) + } + return +} + +func (mip mapIntProc) First(data any) any { + if a, _ := data.([]int); 0 < len(a) { + return map[string]int{"i": 0, "v": a[0]} + } + return nil +} + +func compileMapIntProc(code []byte) jp.Procedure { + return mapIntProc{} +} + +func TestProcLast(t *testing.T) { + jp.CompileScript = compileMathProc + + p := jp.MustNewProc([]byte("(+ 0 1)")) + tt.Equal(t, "[(+ 0 1)]", p.String()) + + x := jp.MustParseString("[(+ 0 1)]") + tt.Equal(t, "[(+ 0 1)]", x.String()) + + data := []any{2, 3, 4} + result := x.First(data) + tt.Equal(t, 5, result) + + got := x.Get(data) + tt.Equal(t, []any{5}, got) + + locs := x.Locate(data, 1) + tt.Equal(t, "[[0]]", pretty.SEN(locs)) + + var buf []byte + x.Walk(data, func(path jp.Expr, nodes []any) { + buf = fmt.Appendf(buf, "%s : %v\n", path, nodes) + }) + tt.Equal(t, "[0] : [[2 3 4] 5]\n", string(buf)) +} + +func TestProcNotLast(t *testing.T) { + jp.CompileScript = compileMapProc + + x := jp.MustParseString("[(quux)].v") + tt.Equal(t, "[(quux)].v", x.String()) + + data := []any{2, 3, 4} + result := x.First(data) + tt.Equal(t, 2, result) + + got := x.Get(data) + tt.Equal(t, []any{2, 3, 4}, got) + + locs := x.Locate(data, 2) + tt.Equal(t, "[[0 v] [1 v]]", pretty.SEN(locs)) + + var buf []byte + x.Walk(data, func(path jp.Expr, nodes []any) { + buf = fmt.Appendf(buf, "%s : %v\n", path, nodes) + }) + tt.Equal(t, `[0].v : [[2 3 4] map[i:0 v:2] 2] +[1].v : [[2 3 4] map[i:1 v:3] 3] +[2].v : [[2 3 4] map[i:2 v:4] 4] +`, string(buf)) +} + +func TestProcNotLastReflect(t *testing.T) { + jp.CompileScript = compileMapIntProc + + x := jp.MustParseString("[(quux)].v") + tt.Equal(t, "[(quux)].v", x.String()) + + data := []int{2, 3, 4} + result := x.First(data) + tt.Equal(t, 2, result) + + got := x.Get(data) + tt.Equal(t, []any{2, 3, 4}, got) +} diff --git a/jp/procedure.go b/jp/procedure.go new file mode 100644 index 0000000..4ddfd4c --- /dev/null +++ b/jp/procedure.go @@ -0,0 +1,14 @@ +// Copyright (c) 2025, Peter Ohler, All rights reserved. + +package jp + +// Procedure defines the interface for functions for script fragments between +// [( and )] delimiters. +type Procedure interface { + // Get should return a list of matching in the data element. + Get(data any) []any + + // First should return a single matching in the data element or nil if + // there are no matches. + First(data any) any +} diff --git a/notes b/notes index 5fc9bc8..d3a59ec 100644 --- a/notes +++ b/notes @@ -1,8 +1,12 @@ -- coverage - - option for fast vs accurate parse - - doc should indicate 16th place variation and diff in performance for float parse - +- option for fast vs accurate parse + - doc should indicate 16th place variation and diff in performance for float parse + +- jp [( scripts + - Get + + First + - Locate + - Walk - parse - add discover option to find JSON or SEN in a string or file