From f9324dd8f4b938928bfae54d85ac505ca244b846 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 13 May 2025 16:03:03 +0200 Subject: [PATCH] add min and max to the obitools expression language --- pkg/obingslibrary/marker.go | 12 ++- pkg/obioptions/version.go | 2 +- pkg/obiseq/language.go | 6 ++ pkg/obiutils/minmax.go | 202 ++++++++++++++++++++++++++++++++++-- 4 files changed, 210 insertions(+), 12 deletions(-) diff --git a/pkg/obingslibrary/marker.go b/pkg/obingslibrary/marker.go index 01f66b7..4f41a85 100644 --- a/pkg/obingslibrary/marker.go +++ b/pkg/obingslibrary/marker.go @@ -268,7 +268,11 @@ func (marker *Marker) CheckTagLength() error { reverse_length[len(tags.Reverse)]++ } - maxfl, _ := obiutils.MaxMap(forward_length) + maxfl, _, err := obiutils.MaxMap(forward_length) + + if err != nil { + return err + } if len(forward_length) > 1 { others := make([]int, 0) @@ -280,7 +284,11 @@ func (marker *Marker) CheckTagLength() error { return fmt.Errorf("forward tag length %d is not the same for all the PCRs : %v", maxfl, others) } - maxrl, _ := obiutils.MaxMap(reverse_length) + maxrl, _, err := obiutils.MaxMap(reverse_length) + + if err != nil { + return err + } if len(reverse_length) > 1 { others := make([]int, 0) diff --git a/pkg/obioptions/version.go b/pkg/obioptions/version.go index 11a9013..33cb754 100644 --- a/pkg/obioptions/version.go +++ b/pkg/obioptions/version.go @@ -8,7 +8,7 @@ import ( // corresponds to the last commit, and not the one when the file will be // commited -var _Commit = "e065e29" +var _Commit = "f1b9ac4" var _Version = "Release 4.4.0" // Version returns the version of the obitools package. diff --git a/pkg/obiseq/language.go b/pkg/obiseq/language.go index 26c7afc..e248b6a 100644 --- a/pkg/obiseq/language.go +++ b/pkg/obiseq/language.go @@ -135,6 +135,12 @@ var OBILang = gval.NewLanguage( length := obiutils.Len(args[0]) return (float64)(length), nil }), + gval.Function("min", func(args ...interface{}) (interface{}, error) { + return obiutils.Min(args[0]) + }), + gval.Function("max", func(args ...interface{}) (interface{}, error) { + return obiutils.Max(args[0]) + }), 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 ca9adc8..f6b0e40 100644 --- a/pkg/obiutils/minmax.go +++ b/pkg/obiutils/minmax.go @@ -1,7 +1,10 @@ package obiutils import ( - log "github.com/sirupsen/logrus" + "errors" + "fmt" + "reflect" + "golang.org/x/exp/constraints" ) @@ -31,19 +34,200 @@ func MinMaxSlice[T constraints.Ordered](vec []T) (min, max T) { return } -func MaxMap[K comparable, T constraints.Ordered](values map[K]T) (K, T) { - - if len(values) == 0 { - log.Panicf("empty map") - } - +func MaxMap[K comparable, T constraints.Ordered](values map[K]T) (K, T, error) { var maxKey K var maxValue T + + if len(values) == 0 { + return maxKey, maxValue, errors.New("Empty map") + } + + first := true for k, v := range values { - if v > maxValue { + if v > maxValue || first { maxValue = v maxKey = k + first = false } } - return maxKey, maxValue + + return maxKey, maxValue, nil +} + +func MinMap[K comparable, T constraints.Ordered](values map[K]T) (K, T, error) { + var minKey K + var minValue T + if len(values) == 0 { + return minKey, minValue, errors.New("Empty map") + } + + first := true + for k, v := range values { + if v < minValue || first { + minValue = v + minKey = k + first = false + } + } + + return minKey, minValue, nil +} + +// 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. +func Min(data 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 minFromIterable(v) + case reflect.Map: + if v.Len() == 0 { + return nil, errors.New("empty map") + } + return minFromMap(v) + default: + if !isOrderedKind(v.Kind()) { + return nil, fmt.Errorf("unsupported type: %s", v.Kind()) + } + // single comparable value → return it + return data, nil + } +} + +// Max returns the largest 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. +func Max(data 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 maxFromIterable(v) + case reflect.Map: + if v.Len() == 0 { + return nil, errors.New("empty map") + } + return maxFromMap(v) + default: + if !isOrderedKind(v.Kind()) { + return nil, fmt.Errorf("unsupported type: %s", v.Kind()) + } + return data, nil + } +} + +// maxFromIterable scans a slice/array to find the maximum. +func maxFromIterable(v reflect.Value) (interface{}, error) { + var best reflect.Value + 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 i == 0 || greater(elem, best) { + best = elem + } + } + return best.Interface(), nil +} + +// minFromIterable finds min in slice or array. +func minFromIterable(v reflect.Value) (interface{}, error) { + var minVal reflect.Value + 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 i == 0 || less(elem, minVal) { + minVal = elem + } + } + return minVal.Interface(), nil +} + +// maxFromMap scans map values to find the maximum. +func maxFromMap(v reflect.Value) (interface{}, error) { + var best reflect.Value + first := true + for _, key := range v.MapKeys() { + elem := v.MapIndex(key) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if first || greater(elem, best) { + best = elem + first = false + } + } + return best.Interface(), nil +} + +// minFromMap finds min among map values. +func minFromMap(v reflect.Value) (interface{}, error) { + var minVal reflect.Value + first := true + for _, key := range v.MapKeys() { + elem := v.MapIndex(key) + if !isOrderedKind(elem.Kind()) { + return nil, fmt.Errorf("unsupported element type: %s", elem.Kind()) + } + if first || less(elem, minVal) { + minVal = elem + first = false + } + } + return minVal.Interface(), nil +} + +// isOrderedKind reports whether k supports comparison ordering. +func isOrderedKind(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, + reflect.String: + return true + default: + return false + } +} + +// less returns true if a < b for supported kinds. +func less(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return a.Int() < b.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + default: + // should never happen if caller checked isOrderedKind + return false + } +} + +// greater returns true if a > b for supported kinds. +func greater(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return a.Int() > b.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return a.Uint() > b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() > b.Float() + case reflect.String: + return a.String() > b.String() + default: + return false + } }