From cecf90fa40b052c3a0a36dca6ff29b629bac8750 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 14 May 2026 20:57:05 +0800 Subject: [PATCH] feat: add min/max filtering and saturating subtraction utilities Introduce generic and reflection-based utilities for filtering slices and maps by minimum/maximum thresholds, along with saturating subtraction. The `obiutils` package provides type-safe generic implementations alongside dynamic reflection dispatchers to handle arbitrary ordered and numeric types. These are exposed as GVAL expression functions in `obiseq`, extending the language's built-in filtering and numeric capabilities. --- pkg/obiseq/language.go | 13 +++ pkg/obiutils/minmax.go | 241 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) diff --git a/pkg/obiseq/language.go b/pkg/obiseq/language.go index e248b6a..9f0297b 100644 --- a/pkg/obiseq/language.go +++ b/pkg/obiseq/language.go @@ -141,6 +141,19 @@ var OBILang = gval.NewLanguage( gval.Function("max", func(args ...interface{}) (interface{}, error) { return obiutils.Max(args[0]) }), + + gval.Function("filtermin", func(args ...interface{}) (interface{}, error) { + return obiutils.FilterMin(args[0], args[1]) + }), + + gval.Function("filtermax", func(args ...interface{}) (interface{}, error) { + return obiutils.FilterMax(args[0], args[1]) + }), + + gval.Function("saturatingsub", func(args ...interface{}) (interface{}, error) { + return obiutils.SaturatingSub(args[0], args[1]) + }), + gval.Function("contains", func(args ...interface{}) (interface{}, error) { if obiutils.IsAMap(args[0]) { val := reflect.ValueOf(args[0]).MapIndex(reflect.ValueOf(args[1])) diff --git a/pkg/obiutils/minmax.go b/pkg/obiutils/minmax.go index b01dfc1..1312e06 100644 --- a/pkg/obiutils/minmax.go +++ b/pkg/obiutils/minmax.go @@ -34,6 +34,26 @@ func MinMaxSlice[T constraints.Ordered](vec []T) (min, max T) { return } +func FilterMinSlice[T constraints.Ordered](vec []T, minimum T) []T { + result := make([]T, 0, len(vec)) + for _, v := range vec { + if v >= minimum { + result = append(result, v) + } + } + return result +} + +func FilterMaxSlice[T constraints.Ordered](vec []T, maximum T) []T { + result := make([]T, 0, len(vec)) + for _, v := range vec { + if v <= maximum { + result = append(result, v) + } + } + return result +} + func MaxMap[K comparable, T constraints.Ordered](values map[K]T) (K, T, error) { var maxKey K var maxValue T @@ -73,6 +93,46 @@ func MinMap[K comparable, T constraints.Ordered](values map[K]T) (K, T, error) { return minKey, minValue, nil } +func FilterMinMap[K comparable, T constraints.Ordered](values map[K]T, minimum T) map[K]T { + result := make(map[K]T) + for k, v := range values { + if v >= minimum { + result[k] = v + } + } + return result +} + +func FilterMaxMap[K comparable, T constraints.Ordered](values map[K]T, maximum T) map[K]T { + result := make(map[K]T) + for k, v := range values { + if v <= maximum { + result[k] = v + } + } + return result +} + +func SaturatingSubSlice[T Numeric](vec []T, sub T) []T { + result := make([]T, len(vec)) + for i, v := range vec { + if v > sub { + result[i] = v - sub + } + } + return result +} + +func SaturatingSubMap[K comparable, T Numeric](values map[K]T, sub T) map[K]T { + result := make(map[K]T) + for k, v := range values { + if v > sub { + result[k] = v - sub + } + } + return result +} + // Min returns the smallest element in a slice/array or map, // or the value itself if data is a single comparable value. // Returns an error if the container is empty or the type is unsupported. @@ -135,6 +195,116 @@ func Max(data interface{}) (interface{}, error) { } } +func FilterMin(data interface{}, minimum interface{}) (interface{}, error) { + v := reflect.ValueOf(data) + switch v.Kind() { + case reflect.Slice, reflect.Array: + if v.Len() == 0 { + return nil, errors.New("empty slice or array") + } + return filterMinFromIterable(v, minimum) + case reflect.Map: + if v.Len() == 0 { + return nil, errors.New("empty map") + } + return filterMinFromMap(v, minimum) + default: + if !isOrderedKind(v.Kind()) { + return nil, fmt.Errorf("unsupported type: %s", v.Kind()) + } + return data, nil + } +} + +func FilterMax(data interface{}, maximum interface{}) (interface{}, error) { + v := reflect.ValueOf(data) + switch v.Kind() { + case reflect.Slice, reflect.Array: + if v.Len() == 0 { + return nil, errors.New("empty slice or array") + } + return filterMaxFromIterable(v, maximum) + case reflect.Map: + if v.Len() == 0 { + return nil, errors.New("empty map") + } + return filterMaxFromMap(v, maximum) + default: + if !isOrderedKind(v.Kind()) { + return nil, fmt.Errorf("unsupported type: %s", v.Kind()) + } + return data, nil + } +} + +func SaturatingSub(data interface{}, sub interface{}) (interface{}, error) { + v := reflect.ValueOf(data) + switch v.Kind() { + case reflect.Slice, reflect.Array: + return saturatingSubFromIterable(v, sub) + case reflect.Map: + return saturatingSubFromMap(v, sub) + default: + if !isNumericKind(v.Kind()) { + return nil, fmt.Errorf("unsupported type: %s", v.Kind()) + } + r, err := saturatingSubValues(v, reflect.ValueOf(sub)) + if err != nil { + return nil, err + } + return r.Interface(), nil + } +} + +func saturatingSubFromIterable(v reflect.Value, sub interface{}) (interface{}, error) { + subVal := reflect.ValueOf(sub) + result := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) + for i := 0; i < v.Len(); i++ { + r, err := saturatingSubValues(v.Index(i), subVal) + if err != nil { + return nil, err + } + result.Index(i).Set(r) + } + return result.Interface(), nil +} + +func saturatingSubFromMap(v reflect.Value, sub interface{}) (interface{}, error) { + subVal := reflect.ValueOf(sub) + result := reflect.MakeMap(v.Type()) + for _, key := range v.MapKeys() { + r, err := saturatingSubValues(v.MapIndex(key), subVal) + if err != nil { + return nil, err + } + if !r.IsZero() { + result.SetMapIndex(key, r) + } + } + return result.Interface(), nil +} + +func saturatingSubValues(a, b reflect.Value) (reflect.Value, error) { + result := reflect.New(a.Type()).Elem() + switch a.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if av, bv := a.Int(), b.Int(); av > bv { + result.SetInt(av - bv) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if av, bv := a.Uint(), b.Uint(); av > bv { + result.SetUint(av - bv) + } + case reflect.Float32, reflect.Float64: + if av, bv := a.Float(), b.Float(); av > bv { + result.SetFloat(av - bv) + } + default: + return reflect.Value{}, fmt.Errorf("unsupported type for saturating subtraction: %s", a.Kind()) + } + return result, nil +} + // maxFromIterable scans a slice/array to find the maximum. func maxFromIterable(v reflect.Value) (interface{}, error) { var best reflect.Value @@ -165,6 +335,66 @@ func minFromIterable(v reflect.Value) (interface{}, error) { return minVal.Interface(), nil } +func filterMinFromIterable(v reflect.Value, minimum interface{}) (interface{}, error) { + minVal := reflect.ValueOf(minimum) + result := reflect.MakeSlice(v.Type(), 0, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if !less(elem, minVal) { // elem >= minimum + result = reflect.Append(result, elem) + } + } + return result.Interface(), nil +} + +func filterMaxFromIterable(v reflect.Value, maximum interface{}) (interface{}, error) { + maxVal := reflect.ValueOf(maximum) + result := reflect.MakeSlice(v.Type(), 0, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if !greater(elem, maxVal) { // elem <= maximum + result = reflect.Append(result, elem) + } + } + return result.Interface(), nil +} + +func filterMinFromMap(v reflect.Value, minimum interface{}) (interface{}, error) { + minVal := reflect.ValueOf(minimum) + result := reflect.MakeMap(v.Type()) + for _, key := range v.MapKeys() { + elem := v.MapIndex(key) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if !less(elem, minVal) { // elem >= minimum + result.SetMapIndex(key, elem) + } + } + return result.Interface(), nil +} + +func filterMaxFromMap(v reflect.Value, maximum interface{}) (interface{}, error) { + maxVal := reflect.ValueOf(maximum) + result := reflect.MakeMap(v.Type()) + for _, key := range v.MapKeys() { + elem := v.MapIndex(key) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if !greater(elem, maxVal) { // elem <= maximum + result.SetMapIndex(key, elem) + } + } + return result.Interface(), nil +} + // maxFromMap scans map values to find the maximum. func maxFromMap(v reflect.Value) (interface{}, error) { var best reflect.Value @@ -199,6 +429,17 @@ func minFromMap(v reflect.Value) (interface{}, error) { return minVal.Interface(), nil } +func isNumericKind(k reflect.Kind) bool { + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + // isOrderedKind reports whether k supports comparison ordering. func isOrderedKind(k reflect.Kind) bool { switch k {