diff --git a/cover.html b/cover.html
new file mode 100644
index 0000000..dd3bf7e
--- /dev/null
+++ b/cover.html
@@ -0,0 +1,1136 @@
+
+
+
+
+
+
+
package path
+
+import "time"
+
+func Equal(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, equal)
+}
+
+func equal(obj, match any, _ string) bool {
+ switch objt := obj.(type) {
+ case time.Time:
+ tm := match.(*timeVal)
+
+ // TODO: Replace Truncate() with a timezone-aware version
+ return tm.time.Equal(objt.Truncate(tm.precision))
+
+ default:
+ return obj == match
+ }
+}
+
+
+
package path
+
+import (
+ "time"
+
+ "cloud.google.com/go/civil"
+)
+
+func Greater(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, greater)
+}
+
+func greater(obj, match any, _ string) bool {
+ switch objt := obj.(type) {
+ case int:
+ return objt > match.(int)
+
+ case int64:
+ return objt > match.(int64)
+
+ case uint:
+ return objt > match.(uint)
+
+ case uint64:
+ return objt > match.(uint64)
+
+ case float32:
+ return objt > match.(float32)
+
+ case float64:
+ return objt > match.(float64)
+
+ case string:
+ return objt > match.(string)
+
+ case bool:
+ return objt && !match.(bool)
+
+ case time.Time:
+ tm := match.(*timeVal)
+
+ return objt.Truncate(tm.precision).After(tm.time)
+
+ case civil.Date:
+ return objt.After(match.(civil.Date))
+
+ default:
+ panic(obj)
+ }
+}
+
+
+
package path
+
+import (
+ "time"
+
+ "cloud.google.com/go/civil"
+)
+
+func GreaterEqual(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, greaterEqual)
+}
+
+func greaterEqual(obj, match any, _ string) bool {
+ switch objt := obj.(type) {
+ case int:
+ return objt >= match.(int)
+
+ case int64:
+ return objt >= match.(int64)
+
+ case uint:
+ return objt >= match.(uint)
+
+ case uint64:
+ return objt >= match.(uint64)
+
+ case float32:
+ return objt >= match.(float32)
+
+ case float64:
+ return objt >= match.(float64)
+
+ case string:
+ return objt >= match.(string)
+
+ case bool:
+ return objt || objt == match.(bool)
+
+ case time.Time:
+ tm := match.(*timeVal)
+ trunc := objt.Truncate(tm.precision)
+
+ return trunc.Equal(tm.time) || trunc.After(tm.time)
+
+ case civil.Date:
+ return objt == match.(civil.Date) || objt.After(match.(civil.Date))
+
+ default:
+ panic(obj)
+ }
+}
+
+
+
package path
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "cloud.google.com/go/civil"
+)
+
+func HasPrefix(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, hasPrefix)
+}
+
+func hasPrefix(obj, match any, matchStr string) bool {
+ var objStr string
+
+ switch objt := obj.(type) {
+ case int:
+ objStr = strconv.FormatInt(int64(objt), 10)
+
+ case int64:
+ objStr = strconv.FormatInt(objt, 10)
+
+ case uint:
+ objStr = strconv.FormatUint(uint64(objt), 10)
+
+ case uint64:
+ objStr = strconv.FormatUint(objt, 10)
+
+ case float32:
+ objStr = strconv.FormatFloat(float64(objt), 'f', -1, 32)
+
+ case float64:
+ objStr = strconv.FormatFloat(objt, 'f', -1, 64)
+
+ case string:
+ objStr = objt
+
+ case bool:
+ objStr = strconv.FormatBool(objt)
+
+ case time.Time:
+ objStr = objt.String()
+
+ case civil.Date:
+ objStr = objt.String()
+
+ default:
+ panic(obj)
+ }
+
+ return strings.HasPrefix(objStr, matchStr)
+}
+
+
+
package path
+
+func In(obj any, path string, matchStr string) (bool, error) {
+ return opList(obj, path, matchStr, equal)
+}
+
+
+
package path
+
+import (
+ "time"
+
+ "cloud.google.com/go/civil"
+)
+
+func Less(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, less)
+}
+
+func less(obj, match any, _ string) bool {
+ switch objt := obj.(type) {
+ case int:
+ return objt < match.(int)
+
+ case int64:
+ return objt < match.(int64)
+
+ case uint:
+ return objt < match.(uint)
+
+ case uint64:
+ return objt < match.(uint64)
+
+ case float32:
+ return objt < match.(float32)
+
+ case float64:
+ return objt < match.(float64)
+
+ case string:
+ return objt < match.(string)
+
+ case bool:
+ return !objt && match.(bool)
+
+ case time.Time:
+ tm := match.(*timeVal)
+
+ return objt.Truncate(tm.precision).Before(tm.time)
+
+ case civil.Date:
+ return objt.Before(match.(civil.Date))
+
+ default:
+ panic(obj)
+ }
+}
+
+
+
package path
+
+import (
+ "time"
+
+ "cloud.google.com/go/civil"
+)
+
+func LessEqual(obj any, path string, matchStr string) (bool, error) {
+ return op(obj, path, matchStr, lessEqual)
+}
+
+func lessEqual(obj, match any, _ string) bool {
+ switch objt := obj.(type) {
+ case int:
+ return objt <= match.(int)
+
+ case int64:
+ return objt <= match.(int64)
+
+ case uint:
+ return objt <= match.(uint)
+
+ case uint64:
+ return objt <= match.(uint64)
+
+ case float32:
+ return objt <= match.(float32)
+
+ case float64:
+ return objt <= match.(float64)
+
+ case string:
+ return objt <= match.(string)
+
+ case bool:
+ return !objt || objt == match.(bool)
+
+ case time.Time:
+ tm := match.(*timeVal)
+ trunc := objt.Truncate(tm.precision)
+
+ return trunc.Equal(tm.time) || trunc.Before(tm.time)
+
+ case civil.Date:
+ return objt == match.(civil.Date) || objt.Before(match.(civil.Date))
+
+ default:
+ panic(obj)
+ }
+}
+
+
+
package path
+
+import (
+ "encoding/json"
+ "reflect"
+
+ "github.com/gopatchy/jsrest"
+)
+
+func Merge(to, from any) {
+ MergeValue(reflect.ValueOf(to), reflect.ValueOf(from))
+}
+
+func MergeValue(to, from reflect.Value) {
+ to = reflect.Indirect(to)
+ from = reflect.Indirect(from)
+
+ for i := 0; i < to.NumField(); i++ {
+ toField := to.Field(i)
+ fromField := from.Field(i)
+
+ if fromField.IsZero() {
+ continue
+ }
+
+ if reflect.Indirect(fromField).Kind() == reflect.Struct {
+ MergeValue(toField, fromField)
+ continue
+ }
+
+ toField.Set(fromField)
+ }
+}
+
+func MergeMap(to any, from map[string]any) error {
+ m, err := ToMap(to)
+ if err != nil {
+ return jsrest.Errorf(jsrest.ErrInternalServerError, "converting to map failed (%w)", err)
+ }
+
+ MergeMaps(m, from)
+
+ return FromMap(to, m)
+}
+
+func MergeMaps(to map[string]any, from map[string]any) {
+ for k, v := range from {
+ if vMap, isMap := v.(map[string]any); isMap {
+ if _, ok := to[k].(map[string]any); !ok {
+ // Either key doesn't exist or it's a different type
+ // If different type, error will happen during json decode
+ to[k] = map[string]any{}
+ }
+
+ MergeMaps(to[k].(map[string]any), vMap)
+ } else {
+ to[k] = v
+ }
+ }
+}
+
+func ToMap(from any) (map[string]any, error) {
+ js, err := json.Marshal(from)
+ if err != nil {
+ return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
+ }
+
+ ret := map[string]any{}
+
+ err = json.Unmarshal(js, &ret)
+ if err != nil {
+ return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
+ }
+
+ return ret, nil
+}
+
+func FromMap(to any, from map[string]any) error {
+ js, err := json.Marshal(from)
+ if err != nil {
+ return jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
+ }
+
+ err = json.Unmarshal(js, to)
+ if err != nil {
+ return jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
+ }
+
+ return nil
+}
+
+
+
package path
+
+import (
+ "strings"
+)
+
+func op(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) {
+ objVal, err := Get(obj, path)
+ if err != nil {
+ return false, err
+ }
+
+ matchVal, err := parse(matchStr, objVal)
+ if err != nil {
+ return false, err
+ }
+
+ if isSlice(objVal) {
+ return anyTrue(objVal, func(x any, _ int) bool { return cb(x, matchVal, matchStr) }), nil
+ }
+
+ return cb(objVal, matchVal, matchStr), nil
+}
+
+func opList(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) {
+ objVal, err := Get(obj, path)
+ if err != nil {
+ return false, err
+ }
+
+ if objVal == nil {
+ return false, nil
+ }
+
+ matchVal := []any{}
+ matchParts := strings.Split(matchStr, ",")
+
+ for _, matchPart := range matchParts {
+ matchTmp, err := parse(matchPart, objVal)
+ if err != nil {
+ return false, err
+ }
+
+ matchVal = append(matchVal, matchTmp)
+ }
+
+ return anyTrue(matchVal, func(y any, i int) bool {
+ str := matchParts[i]
+
+ if isSlice(objVal) {
+ return anyTrue(objVal, func(x any, _ int) bool { return cb(x, y, str) })
+ }
+
+ return cb(objVal, y, str)
+ }), nil
+}
+
+
+
package path
+
+import (
+ "errors"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "cloud.google.com/go/civil"
+ "github.com/gopatchy/jsrest"
+)
+
+type timeVal struct {
+ time time.Time
+ precision time.Duration
+}
+
+var (
+ ErrUnsupportedType = errors.New("unsupported type")
+ ErrUnknownTimeFormat = errors.New("unknown time format")
+)
+
+func parse(str string, t any) (any, error) {
+ typ := reflect.TypeOf(t)
+
+ if typ.Kind() == reflect.Slice {
+ typ = typ.Elem()
+ }
+
+ if typ.Kind() == reflect.Pointer {
+ typ = typ.Elem()
+ }
+
+ // TODO: Consider attempting to convert to string in default case
+ switch typ.Kind() { //nolint:exhaustive
+ case reflect.Int:
+ return parseInt(str)
+
+ case reflect.Int64:
+ return strconv.ParseInt(str, 10, 64)
+
+ case reflect.Uint:
+ return parseUint(str)
+
+ case reflect.Uint64:
+ return strconv.ParseUint(str, 10, 64)
+
+ case reflect.Float32:
+ return parseFloat32(str)
+
+ case reflect.Float64:
+ return strconv.ParseFloat(str, 64)
+
+ case reflect.String:
+ return str, nil
+
+ case reflect.Bool:
+ return strconv.ParseBool(str)
+
+ case reflect.Struct:
+ switch typ {
+ case reflect.TypeOf(time.Time{}):
+ return parseTime(str)
+
+ case reflect.TypeOf(civil.Date{}):
+ return civil.ParseDate(str)
+ }
+ }
+
+ return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%T (%w)", t, ErrUnsupportedType)
+}
+
+func parseInt(str string) (int, error) {
+ val, err := strconv.ParseInt(str, 10, strconv.IntSize)
+
+ return int(val), err
+}
+
+func parseUint(str string) (uint, error) {
+ val, err := strconv.ParseUint(str, 10, strconv.IntSize)
+
+ return uint(val), err
+}
+
+func parseFloat32(str string) (float32, error) {
+ val, err := strconv.ParseFloat(str, 32)
+
+ return float32(val), err
+}
+
+type timeFormat struct {
+ format string
+ precision time.Duration
+}
+
+var timeFormats = []timeFormat{
+ {
+ format: "2006-01-02-07:00",
+ precision: 24 * time.Hour,
+ // TODO: Support field annotation to change start vs end of day
+ // TODO: Support timezone context passed down to allow naked date
+ },
+ {
+ format: "2006-01-02T15:04:05Z",
+ precision: 1 * time.Second,
+ },
+ {
+ format: "2006-01-02T15:04:05-07:00",
+ precision: 1 * time.Second,
+ },
+}
+
+func parseTime(str string) (*timeVal, error) {
+ if strings.ToLower(str) == "now" {
+ return &timeVal{
+ time: time.Now(),
+ precision: 1 * time.Nanosecond,
+ }, nil
+ }
+
+ for _, format := range timeFormats {
+ tm, err := time.Parse(format.format, str)
+ if err != nil {
+ continue
+ }
+
+ return &timeVal{
+ time: tm,
+ precision: format.precision,
+ }, nil
+ }
+
+ i, err := strconv.ParseInt(str, 10, 64)
+ if err != nil {
+ return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", str, ErrUnknownTimeFormat)
+ }
+
+ // UNIX Seconds: 2969-05-03
+ // UNIX Millis: 1971-01-01
+ // Intended to give us a wide range of useful values in both schemes
+ if i > 31536000000 {
+ return &timeVal{
+ time: time.UnixMilli(i),
+ precision: 1 * time.Millisecond,
+ }, nil
+ }
+
+ return &timeVal{
+ time: time.Unix(i, 0),
+ precision: 1 * time.Second,
+ }, nil
+}
+
+
+
package path
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "sort"
+ "strings"
+ "time"
+
+ "cloud.google.com/go/civil"
+ "github.com/gopatchy/jsrest"
+ "golang.org/x/exp/slices"
+)
+
+type WalkCallback func(string, []string, reflect.StructField)
+
+var (
+ TimeTimeType = reflect.TypeOf(time.Time{})
+ CivilDateType = reflect.TypeOf(civil.Date{})
+
+ ErrNotAStruct = errors.New("not a struct")
+ ErrUnknownFieldName = errors.New("unknown field name")
+)
+
+func Get(obj any, path string) (any, error) {
+ v, err := GetValue(reflect.ValueOf(obj), path)
+ if err != nil {
+ return nil, err
+ }
+
+ return v.Interface(), nil
+}
+
+func GetValue(v reflect.Value, path string) (reflect.Value, error) {
+ parts := strings.Split(path, ".")
+ return getRecursive(v, parts, []string{})
+}
+
+func getRecursive(v reflect.Value, parts []string, prev []string) (reflect.Value, error) {
+ if v.Kind() == reflect.Pointer {
+ if v.IsNil() {
+ v = reflect.Zero(v.Type().Elem())
+ } else {
+ v = reflect.Indirect(v)
+ }
+ }
+
+ if len(parts) == 0 {
+ return v, nil
+ }
+
+ if v.Kind() != reflect.Struct {
+ return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
+ }
+
+ part := parts[0]
+
+ sub, found := getField(v, part)
+ if !found {
+ return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
+ }
+
+ newPrev := []string{}
+ newPrev = append(newPrev, prev...)
+ newPrev = append(newPrev, part)
+
+ return getRecursive(sub, parts[1:], newPrev)
+}
+
+func getField(v reflect.Value, name string) (reflect.Value, bool) {
+ field, found := getStructField(v.Type(), name)
+ if !found {
+ return reflect.Value{}, false
+ }
+
+ return v.FieldByName(field.Name), true
+}
+
+func Set(obj any, path string, val string) error {
+ return SetValue(reflect.ValueOf(obj), path, val)
+}
+
+func SetValue(v reflect.Value, path string, val string) error {
+ parts := strings.Split(path, ".")
+ return setRecursive(v, parts, []string{}, val)
+}
+
+func setRecursive(v reflect.Value, parts []string, prev []string, val string) error {
+ if v.Kind() == reflect.Pointer {
+ if v.IsNil() {
+ v.Set(reflect.New(v.Type().Elem()))
+ }
+
+ v = reflect.Indirect(v)
+ }
+
+ if len(parts) == 0 {
+ n, err := parse(val, v.Interface())
+ if err != nil {
+ return err
+ }
+
+ if _, ok := n.(*timeVal); ok {
+ n = n.(*timeVal).time
+ }
+
+ v.Set(reflect.ValueOf(n))
+
+ return nil
+ }
+
+ if v.Kind() != reflect.Struct {
+ return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
+ }
+
+ part := parts[0]
+
+ sub, found := getField(v, part)
+ if !found {
+ return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
+ }
+
+ newPrev := []string{}
+ newPrev = append(newPrev, prev...)
+ newPrev = append(newPrev, part)
+
+ return setRecursive(sub, parts[1:], newPrev, val)
+}
+
+func List(obj any) []string {
+ return ListType(reflect.TypeOf(obj))
+}
+
+func ListType(t reflect.Type) []string {
+ list := []string{}
+
+ WalkType(t, func(path string, _ []string, field reflect.StructField) {
+ t := MaybeIndirectType(field.Type)
+ if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
+ return
+ }
+
+ list = append(list, path)
+ })
+
+ sort.Strings(list)
+
+ return list
+}
+
+func GetFieldType(t reflect.Type, path string) reflect.Type {
+ parts := strings.Split(path, ".")
+
+ for _, part := range parts {
+ field, found := getStructField(MaybeIndirectType(t), part)
+ if !found {
+ return nil
+ }
+
+ t = field.Type
+ }
+
+ return t
+}
+
+func FindTagValueType(t reflect.Type, key, value string) (string, bool) {
+ ret := ""
+
+ WalkType(t, func(path string, _ []string, field reflect.StructField) {
+ tag, found := field.Tag.Lookup(key)
+ if !found {
+ return
+ }
+
+ parts := strings.Split(tag, ",")
+
+ if slices.Contains(parts, value) {
+ ret = path
+ }
+ })
+
+ return ret, ret != ""
+}
+
+func Walk(obj any, cb WalkCallback) {
+ WalkType(reflect.TypeOf(obj), cb)
+}
+
+func WalkType(t reflect.Type, cb WalkCallback) {
+ walkRecursive(MaybeIndirectType(t), cb, []string{})
+}
+
+func walkRecursive(t reflect.Type, cb WalkCallback, prev []string) {
+ for i := 0; i < t.NumField(); i++ {
+ sub := t.Field(i)
+
+ newPrev := []string{}
+ newPrev = append(newPrev, prev...)
+
+ if !sub.Anonymous {
+ newPrev = append(newPrev, FieldName(sub))
+ }
+
+ t := MaybeIndirectType(sub.Type)
+
+ if len(newPrev) > 0 {
+ cb(strings.Join(newPrev, "."), newPrev, sub)
+ }
+
+ if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
+ walkRecursive(t, cb, newPrev)
+ }
+ }
+}
+
+func getStructField(t reflect.Type, name string) (reflect.StructField, bool) {
+ name = strings.ToLower(name)
+
+ return t.FieldByNameFunc(func(iterName string) bool {
+ iterField, iterOK := t.FieldByName(iterName)
+ if !iterOK {
+ panic(iterName)
+ }
+
+ return strings.ToLower(FieldName(iterField)) == name
+ })
+}
+
+func errorPath(prev []string, part string) string {
+ if len(prev) == 0 {
+ return part
+ }
+
+ return fmt.Sprintf("%s.%s", strings.Join(prev, "."), part)
+}
+
+func FieldName(field reflect.StructField) string {
+ tag := field.Tag.Get("json")
+ if tag != "" {
+ if tag == "-" {
+ return ""
+ }
+
+ parts := strings.SplitN(tag, ",", 2)
+
+ return parts[0]
+ }
+
+ return field.Name
+}
+
+func MaybeIndirectType(t reflect.Type) reflect.Type {
+ if t.Kind() == reflect.Pointer {
+ return t.Elem()
+ }
+
+ return t
+}
+
+
+
package path
+
+import "reflect"
+
+func isSlice(v any) bool {
+ return reflect.TypeOf(v).Kind() == reflect.Slice
+}
+
+func anyTrue(v any, cb func(any, int) bool) bool {
+ val := reflect.ValueOf(v)
+
+ for i := 0; i < val.Len(); i++ {
+ sub := val.Index(i)
+
+ if sub.Kind() == reflect.Pointer && sub.IsNil() {
+ continue
+ }
+
+ sub = reflect.Indirect(sub)
+
+ if cb(sub.Interface(), i) {
+ return true
+ }
+ }
+
+ return false
+}
+
+
+
package path
+
+import (
+ "errors"
+ "reflect"
+ "sort"
+ "time"
+
+ "cloud.google.com/go/civil"
+ "github.com/gopatchy/jsrest"
+)
+
+func Sort(objs any, path string) error {
+ as := newAnySlice(objs, path)
+ sort.Stable(as)
+
+ return as.err
+}
+
+func SortReverse(objs any, path string) error {
+ as := newAnySlice(objs, path)
+ sort.Stable(sort.Reverse(as))
+
+ return as.err
+}
+
+type anySlice struct {
+ path string
+ slice reflect.Value
+ swapper func(i, j int)
+ err error
+}
+
+var ErrUnsupportedSortType = errors.New("unsupported _sort type")
+
+func newAnySlice(objs any, path string) *anySlice {
+ return &anySlice{
+ path: path,
+ slice: reflect.ValueOf(objs),
+ swapper: reflect.Swapper(objs),
+ }
+}
+
+func (as *anySlice) Len() int {
+ return as.slice.Len()
+}
+
+func (as *anySlice) Less(i, j int) bool {
+ v1, err := Get(as.slice.Index(i).Interface(), as.path)
+ if err != nil {
+ as.err = err
+ // We have to obey the Less() contract even in error cases
+ return i < j
+ }
+
+ v2, err := Get(as.slice.Index(j).Interface(), as.path)
+ if err != nil {
+ as.err = err
+ return i < j
+ }
+
+ switch {
+ case v1 == nil && v2 == nil:
+ return false
+ case v1 == nil:
+ return true
+ case v2 == nil:
+ return false
+ }
+
+ switch t1 := v1.(type) {
+ case int:
+ return t1 < v2.(int)
+
+ case int64:
+ return t1 < v2.(int64)
+
+ case uint:
+ return t1 < v2.(uint)
+
+ case uint64:
+ return t1 < v2.(uint64)
+
+ case float32:
+ return t1 < v2.(float32)
+
+ case float64:
+ return t1 < v2.(float64)
+
+ case string:
+ return t1 < v2.(string)
+
+ case bool:
+ return !t1 && v2.(bool)
+
+ case time.Time:
+ return t1.Before(v2.(time.Time))
+
+ case civil.Date:
+ return t1.Before(v2.(civil.Date))
+
+ default:
+ as.err = jsrest.Errorf(jsrest.ErrBadRequest, "%s: %T (%w)", as.path, t1, ErrUnsupportedSortType)
+ return i < j
+ }
+}
+
+func (as *anySlice) Swap(i, j int) {
+ as.swapper(i, j)
+}
+
+
+
+
+
+
diff --git a/cover.out b/cover.out
new file mode 100644
index 0000000..6523a5c
--- /dev/null
+++ b/cover.out
@@ -0,0 +1,258 @@
+mode: atomic
+github.com/gopatchy/path/equal.go:5.65,7.2 1 55
+github.com/gopatchy/path/equal.go:9.43,10.28 1 203
+github.com/gopatchy/path/equal.go:11.17,15.52 2 28
+github.com/gopatchy/path/equal.go:17.10,18.22 1 175
+github.com/gopatchy/path/greater.go:9.67,11.2 1 40
+github.com/gopatchy/path/greater.go:13.45,14.28 1 64
+github.com/gopatchy/path/greater.go:15.11,16.28 1 8
+github.com/gopatchy/path/greater.go:18.13,19.30 1 8
+github.com/gopatchy/path/greater.go:21.12,22.29 1 8
+github.com/gopatchy/path/greater.go:24.14,25.31 1 8
+github.com/gopatchy/path/greater.go:27.15,28.32 1 5
+github.com/gopatchy/path/greater.go:30.15,31.32 1 5
+github.com/gopatchy/path/greater.go:33.14,34.31 1 5
+github.com/gopatchy/path/greater.go:36.12,37.31 1 5
+github.com/gopatchy/path/greater.go:39.17,42.52 2 6
+github.com/gopatchy/path/greater.go:44.18,45.40 1 6
+github.com/gopatchy/path/greater.go:47.10,48.13 1 0
+github.com/gopatchy/path/greaterequal.go:9.72,11.2 1 60
+github.com/gopatchy/path/greaterequal.go:13.50,14.28 1 94
+github.com/gopatchy/path/greaterequal.go:15.11,16.29 1 12
+github.com/gopatchy/path/greaterequal.go:18.13,19.31 1 12
+github.com/gopatchy/path/greaterequal.go:21.12,22.30 1 12
+github.com/gopatchy/path/greaterequal.go:24.14,25.32 1 12
+github.com/gopatchy/path/greaterequal.go:27.15,28.33 1 7
+github.com/gopatchy/path/greaterequal.go:30.15,31.33 1 7
+github.com/gopatchy/path/greaterequal.go:33.14,34.32 1 7
+github.com/gopatchy/path/greaterequal.go:36.12,37.38 1 7
+github.com/gopatchy/path/greaterequal.go:39.17,43.54 3 9
+github.com/gopatchy/path/greaterequal.go:45.18,46.70 1 9
+github.com/gopatchy/path/greaterequal.go:48.10,49.13 1 0
+github.com/gopatchy/path/hasprefix.go:11.69,13.2 1 18
+github.com/gopatchy/path/hasprefix.go:15.54,18.28 2 19
+github.com/gopatchy/path/hasprefix.go:19.11,20.46 1 2
+github.com/gopatchy/path/hasprefix.go:22.13,23.39 1 2
+github.com/gopatchy/path/hasprefix.go:25.12,26.48 1 2
+github.com/gopatchy/path/hasprefix.go:28.14,29.40 1 2
+github.com/gopatchy/path/hasprefix.go:31.15,32.59 1 2
+github.com/gopatchy/path/hasprefix.go:34.15,35.50 1 2
+github.com/gopatchy/path/hasprefix.go:37.14,38.16 1 5
+github.com/gopatchy/path/hasprefix.go:40.12,41.36 1 2
+github.com/gopatchy/path/hasprefix.go:43.17,44.25 1 0
+github.com/gopatchy/path/hasprefix.go:46.18,47.25 1 0
+github.com/gopatchy/path/hasprefix.go:49.10,50.13 1 0
+github.com/gopatchy/path/hasprefix.go:53.2,53.44 1 19
+github.com/gopatchy/path/in.go:3.62,5.2 1 40
+github.com/gopatchy/path/less.go:9.64,11.2 1 40
+github.com/gopatchy/path/less.go:13.42,14.28 1 58
+github.com/gopatchy/path/less.go:15.11,16.28 1 6
+github.com/gopatchy/path/less.go:18.13,19.30 1 6
+github.com/gopatchy/path/less.go:21.12,22.29 1 6
+github.com/gopatchy/path/less.go:24.14,25.31 1 6
+github.com/gopatchy/path/less.go:27.15,28.32 1 6
+github.com/gopatchy/path/less.go:30.15,31.32 1 6
+github.com/gopatchy/path/less.go:33.14,34.31 1 6
+github.com/gopatchy/path/less.go:36.12,37.31 1 6
+github.com/gopatchy/path/less.go:39.17,42.53 2 5
+github.com/gopatchy/path/less.go:44.18,45.41 1 5
+github.com/gopatchy/path/less.go:47.10,48.13 1 0
+github.com/gopatchy/path/lessequal.go:9.69,11.2 1 60
+github.com/gopatchy/path/lessequal.go:13.47,14.28 1 81
+github.com/gopatchy/path/lessequal.go:15.11,16.29 1 8
+github.com/gopatchy/path/lessequal.go:18.13,19.31 1 8
+github.com/gopatchy/path/lessequal.go:21.12,22.30 1 8
+github.com/gopatchy/path/lessequal.go:24.14,25.32 1 8
+github.com/gopatchy/path/lessequal.go:27.15,28.33 1 9
+github.com/gopatchy/path/lessequal.go:30.15,31.33 1 9
+github.com/gopatchy/path/lessequal.go:33.14,34.32 1 9
+github.com/gopatchy/path/lessequal.go:36.12,37.39 1 8
+github.com/gopatchy/path/lessequal.go:39.17,43.55 3 7
+github.com/gopatchy/path/lessequal.go:45.18,46.71 1 7
+github.com/gopatchy/path/lessequal.go:48.10,49.13 1 0
+github.com/gopatchy/path/merge.go:10.26,12.2 1 4
+github.com/gopatchy/path/merge.go:14.41,18.37 3 6
+github.com/gopatchy/path/merge.go:18.37,22.25 3 28
+github.com/gopatchy/path/merge.go:22.25,23.12 1 22
+github.com/gopatchy/path/merge.go:26.3,26.59 1 6
+github.com/gopatchy/path/merge.go:26.59,28.12 2 2
+github.com/gopatchy/path/merge.go:31.3,31.25 1 4
+github.com/gopatchy/path/merge.go:35.50,37.16 2 1
+github.com/gopatchy/path/merge.go:37.16,39.3 1 0
+github.com/gopatchy/path/merge.go:41.2,43.23 2 1
+github.com/gopatchy/path/merge.go:46.56,47.25 1 2
+github.com/gopatchy/path/merge.go:47.25,48.47 1 4
+github.com/gopatchy/path/merge.go:48.47,49.44 1 1
+github.com/gopatchy/path/merge.go:49.44,53.5 1 0
+github.com/gopatchy/path/merge.go:55.4,55.43 1 1
+github.com/gopatchy/path/merge.go:56.9,58.4 1 3
+github.com/gopatchy/path/merge.go:62.46,64.16 2 1
+github.com/gopatchy/path/merge.go:64.16,66.3 1 0
+github.com/gopatchy/path/merge.go:68.2,71.16 3 1
+github.com/gopatchy/path/merge.go:71.16,73.3 1 0
+github.com/gopatchy/path/merge.go:75.2,75.17 1 1
+github.com/gopatchy/path/merge.go:78.49,80.16 2 1
+github.com/gopatchy/path/merge.go:80.16,82.3 1 0
+github.com/gopatchy/path/merge.go:84.2,85.16 2 1
+github.com/gopatchy/path/merge.go:85.16,87.3 1 0
+github.com/gopatchy/path/merge.go:89.2,89.12 1 1
+github.com/gopatchy/path/op.go:7.94,9.16 2 273
+github.com/gopatchy/path/op.go:9.16,11.3 1 0
+github.com/gopatchy/path/op.go:13.2,14.16 2 273
+github.com/gopatchy/path/op.go:14.16,16.3 1 0
+github.com/gopatchy/path/op.go:18.2,18.21 1 273
+github.com/gopatchy/path/op.go:18.21,19.50 1 124
+github.com/gopatchy/path/op.go:19.50,19.86 1 245
+github.com/gopatchy/path/op.go:22.2,22.44 1 149
+github.com/gopatchy/path/op.go:25.98,27.16 2 40
+github.com/gopatchy/path/op.go:27.16,29.3 1 0
+github.com/gopatchy/path/op.go:31.2,31.19 1 40
+github.com/gopatchy/path/op.go:31.19,33.3 1 0
+github.com/gopatchy/path/op.go:35.2,38.39 3 40
+github.com/gopatchy/path/op.go:38.39,40.17 2 98
+github.com/gopatchy/path/op.go:40.17,42.4 1 0
+github.com/gopatchy/path/op.go:44.3,44.40 1 98
+github.com/gopatchy/path/op.go:47.2,47.51 1 40
+github.com/gopatchy/path/op.go:47.51,50.22 2 78
+github.com/gopatchy/path/op.go:50.22,51.51 1 39
+github.com/gopatchy/path/op.go:51.51,51.75 1 86
+github.com/gopatchy/path/op.go:54.3,54.28 1 39
+github.com/gopatchy/path/parse.go:24.44,27.33 2 380
+github.com/gopatchy/path/parse.go:27.33,29.3 1 173
+github.com/gopatchy/path/parse.go:31.2,31.35 1 380
+github.com/gopatchy/path/parse.go:31.35,33.3 1 2
+github.com/gopatchy/path/parse.go:36.2,36.20 1 380
+github.com/gopatchy/path/parse.go:37.19,38.23 1 40
+github.com/gopatchy/path/parse.go:40.21,41.39 1 37
+github.com/gopatchy/path/parse.go:43.20,44.24 1 36
+github.com/gopatchy/path/parse.go:46.22,47.40 1 36
+github.com/gopatchy/path/parse.go:49.23,50.27 1 36
+github.com/gopatchy/path/parse.go:52.23,53.37 1 36
+github.com/gopatchy/path/parse.go:55.22,56.18 1 39
+github.com/gopatchy/path/parse.go:58.20,59.32 1 38
+github.com/gopatchy/path/parse.go:61.22,62.14 1 82
+github.com/gopatchy/path/parse.go:63.36,64.25 1 48
+github.com/gopatchy/path/parse.go:66.37,67.31 1 34
+github.com/gopatchy/path/parse.go:71.2,71.83 1 0
+github.com/gopatchy/path/parse.go:74.40,78.2 2 40
+github.com/gopatchy/path/parse.go:80.42,84.2 2 36
+github.com/gopatchy/path/parse.go:86.48,90.2 2 36
+github.com/gopatchy/path/parse.go:114.46,115.35 1 48
+github.com/gopatchy/path/parse.go:115.35,120.3 1 1
+github.com/gopatchy/path/parse.go:122.2,122.37 1 47
+github.com/gopatchy/path/parse.go:122.37,124.17 2 103
+github.com/gopatchy/path/parse.go:124.17,125.12 1 63
+github.com/gopatchy/path/parse.go:128.3,131.9 1 40
+github.com/gopatchy/path/parse.go:134.2,135.16 2 7
+github.com/gopatchy/path/parse.go:135.16,137.3 1 0
+github.com/gopatchy/path/parse.go:142.2,142.21 1 7
+github.com/gopatchy/path/parse.go:142.21,147.3 1 4
+github.com/gopatchy/path/parse.go:149.2,152.8 1 3
+github.com/gopatchy/path/path.go:26.45,28.16 2 382
+github.com/gopatchy/path/path.go:28.16,30.3 1 0
+github.com/gopatchy/path/path.go:32.2,32.27 1 382
+github.com/gopatchy/path/path.go:35.68,38.2 2 382
+github.com/gopatchy/path/path.go:40.90,41.33 1 771
+github.com/gopatchy/path/path.go:41.33,42.16 1 388
+github.com/gopatchy/path/path.go:42.16,44.4 1 2
+github.com/gopatchy/path/path.go:44.9,46.4 1 386
+github.com/gopatchy/path/path.go:49.2,49.21 1 771
+github.com/gopatchy/path/path.go:49.21,51.3 1 382
+github.com/gopatchy/path/path.go:53.2,53.32 1 389
+github.com/gopatchy/path/path.go:53.32,55.3 1 0
+github.com/gopatchy/path/path.go:57.2,60.12 3 389
+github.com/gopatchy/path/path.go:60.12,62.3 1 0
+github.com/gopatchy/path/path.go:64.2,68.46 4 389
+github.com/gopatchy/path/path.go:71.67,73.12 2 398
+github.com/gopatchy/path/path.go:73.12,75.3 1 0
+github.com/gopatchy/path/path.go:77.2,77.40 1 398
+github.com/gopatchy/path/path.go:80.50,82.2 1 6
+github.com/gopatchy/path/path.go:84.63,87.2 2 6
+github.com/gopatchy/path/path.go:89.85,90.33 1 15
+github.com/gopatchy/path/path.go:90.33,91.16 1 9
+github.com/gopatchy/path/path.go:91.16,93.4 1 2
+github.com/gopatchy/path/path.go:95.3,95.26 1 9
+github.com/gopatchy/path/path.go:98.2,98.21 1 15
+github.com/gopatchy/path/path.go:98.21,100.17 2 6
+github.com/gopatchy/path/path.go:100.17,102.4 1 0
+github.com/gopatchy/path/path.go:104.3,104.32 1 6
+github.com/gopatchy/path/path.go:104.32,106.4 1 1
+github.com/gopatchy/path/path.go:108.3,110.13 2 6
+github.com/gopatchy/path/path.go:113.2,113.32 1 9
+github.com/gopatchy/path/path.go:113.32,115.3 1 0
+github.com/gopatchy/path/path.go:117.2,120.12 3 9
+github.com/gopatchy/path/path.go:120.12,122.3 1 0
+github.com/gopatchy/path/path.go:124.2,128.51 4 9
+github.com/gopatchy/path/path.go:131.29,133.2 1 1
+github.com/gopatchy/path/path.go:135.40,138.71 2 1
+github.com/gopatchy/path/path.go:138.71,140.76 2 7
+github.com/gopatchy/path/path.go:140.76,142.4 1 1
+github.com/gopatchy/path/path.go:144.3,144.28 1 6
+github.com/gopatchy/path/path.go:147.2,149.13 2 1
+github.com/gopatchy/path/path.go:152.61,155.29 2 2
+github.com/gopatchy/path/path.go:155.29,157.13 2 3
+github.com/gopatchy/path/path.go:157.13,159.4 1 0
+github.com/gopatchy/path/path.go:161.3,161.17 1 3
+github.com/gopatchy/path/path.go:164.2,164.10 1 2
+github.com/gopatchy/path/path.go:167.73,170.71 2 2
+github.com/gopatchy/path/path.go:170.71,172.13 2 14
+github.com/gopatchy/path/path.go:172.13,174.4 1 10
+github.com/gopatchy/path/path.go:176.3,178.36 2 4
+github.com/gopatchy/path/path.go:178.36,180.4 1 2
+github.com/gopatchy/path/path.go:183.2,183.23 1 2
+github.com/gopatchy/path/path.go:186.37,188.2 1 0
+github.com/gopatchy/path/path.go:190.48,192.2 1 3
+github.com/gopatchy/path/path.go:194.68,195.36 1 9
+github.com/gopatchy/path/path.go:195.36,201.21 4 24
+github.com/gopatchy/path/path.go:201.21,203.4 1 21
+github.com/gopatchy/path/path.go:205.3,207.23 2 24
+github.com/gopatchy/path/path.go:207.23,209.4 1 21
+github.com/gopatchy/path/path.go:211.3,211.76 1 24
+github.com/gopatchy/path/path.go:211.76,213.4 1 6
+github.com/gopatchy/path/path.go:217.78,220.54 2 401
+github.com/gopatchy/path/path.go:220.54,222.14 2 8955
+github.com/gopatchy/path/path.go:222.14,223.19 1 0
+github.com/gopatchy/path/path.go:226.3,226.55 1 8955
+github.com/gopatchy/path/path.go:230.51,231.20 1 0
+github.com/gopatchy/path/path.go:231.20,233.3 1 0
+github.com/gopatchy/path/path.go:235.2,235.60 1 0
+github.com/gopatchy/path/path.go:238.50,240.15 2 8976
+github.com/gopatchy/path/path.go:240.15,241.17 1 797
+github.com/gopatchy/path/path.go:241.17,243.4 1 0
+github.com/gopatchy/path/path.go:245.3,247.18 2 797
+github.com/gopatchy/path/path.go:250.2,250.19 1 8179
+github.com/gopatchy/path/path.go:253.53,254.33 1 37
+github.com/gopatchy/path/path.go:254.33,256.3 1 10
+github.com/gopatchy/path/path.go:258.2,258.10 1 27
+github.com/gopatchy/path/slice.go:5.26,7.2 1 351
+github.com/gopatchy/path/slice.go:9.50,12.33 2 203
+github.com/gopatchy/path/slice.go:12.33,15.51 2 410
+github.com/gopatchy/path/slice.go:15.51,16.12 1 1
+github.com/gopatchy/path/slice.go:19.3,21.29 2 409
+github.com/gopatchy/path/slice.go:21.29,23.4 1 102
+github.com/gopatchy/path/slice.go:26.2,26.14 1 101
+github.com/gopatchy/path/sort.go:13.40,18.2 3 11
+github.com/gopatchy/path/sort.go:20.47,25.2 3 1
+github.com/gopatchy/path/sort.go:36.51,42.2 1 12
+github.com/gopatchy/path/sort.go:44.31,46.2 1 12
+github.com/gopatchy/path/sort.go:48.41,50.16 2 34
+github.com/gopatchy/path/sort.go:50.16,54.3 2 0
+github.com/gopatchy/path/sort.go:56.2,57.16 2 34
+github.com/gopatchy/path/sort.go:57.16,60.3 2 0
+github.com/gopatchy/path/sort.go:62.2,62.9 1 34
+github.com/gopatchy/path/sort.go:63.30,64.15 1 0
+github.com/gopatchy/path/sort.go:65.17,66.14 1 0
+github.com/gopatchy/path/sort.go:67.17,68.15 1 0
+github.com/gopatchy/path/sort.go:71.2,71.25 1 34
+github.com/gopatchy/path/sort.go:72.11,73.23 1 8
+github.com/gopatchy/path/sort.go:75.13,76.25 1 3
+github.com/gopatchy/path/sort.go:78.12,79.24 1 3
+github.com/gopatchy/path/sort.go:81.14,82.26 1 3
+github.com/gopatchy/path/sort.go:84.15,85.27 1 3
+github.com/gopatchy/path/sort.go:87.15,88.27 1 3
+github.com/gopatchy/path/sort.go:90.14,91.26 1 3
+github.com/gopatchy/path/sort.go:93.12,94.26 1 2
+github.com/gopatchy/path/sort.go:96.17,97.35 1 3
+github.com/gopatchy/path/sort.go:99.18,100.36 1 3
+github.com/gopatchy/path/sort.go:102.10,104.15 2 0
+github.com/gopatchy/path/sort.go:108.36,110.2 1 21