|
@@ -1,6 +1,7 @@
|
|
|
package eval
|
|
|
|
|
|
import (
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
"sort"
|
|
|
|
|
@@ -25,6 +26,8 @@ func init() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+var ErrKeyAndLessThanOpts = errors.New("options &key and &less-than are mutually exclusive")
|
|
|
+
|
|
|
//elvdoc:fn all
|
|
|
//
|
|
|
// ```elvish
|
|
@@ -343,23 +346,34 @@ func count(fm *Frame, args ...any) (int, error) {
|
|
|
//elvdoc:fn order
|
|
|
//
|
|
|
// ```elvish
|
|
|
-// order &reverse=$false $less-than=$nil $inputs?
|
|
|
+// order &reverse=$false &key=$nil &less-than=$nil $inputs?
|
|
|
// ```
|
|
|
//
|
|
|
-// Outputs the [value inputs](#value-inputs) sorted
|
|
|
-// in ascending order. The sorting process is guaranteed to be
|
|
|
-// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
|
|
|
+// Outputs the [value inputs](#value-inputs) sorted in ascending order (ignoring
|
|
|
+// the behavior of any `&less-than` callable). The sorting process is guaranteed
|
|
|
+// to be [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
|
|
|
+//
|
|
|
+// The `&reverse` option, if true, reverses the order of output; e.g.,
|
|
|
+// descending rather than ascending.
|
|
|
//
|
|
|
-// The `&reverse` option, if true, reverses the order of output.
|
|
|
+// The `&key` option, if not `$nil` (the default value), is a function that
|
|
|
+// accepts an item to be sorted (string, list, map, etc.) and outputs a single
|
|
|
+// value (which could be a list or map) that is passed to the default
|
|
|
+// comparison function. If the key function throws an exception, `order`
|
|
|
+// rethrows the exception.
|
|
|
//
|
|
|
-// The `&less-than` option, if given, establishes the ordering of the elements.
|
|
|
-// Its value should be a function that takes two arguments and outputs a single
|
|
|
-// boolean indicating whether the first argument is less than the second
|
|
|
+// The `&less-than` option, if not `$nil` (the default value), establishes the
|
|
|
+// ordering of the items. Its value should be a function that takes two
|
|
|
+// arguments and outputs `$true` if the first argument is less than the second
|
|
|
// argument. If the function throws an exception, `order` rethrows the exception
|
|
|
-// without outputting any value.
|
|
|
+// without outputting any value. If `&less-than` has value `$nil` (the default
|
|
|
+// if not set), it is equivalent to `{|a b| == -1 (compare $a $b) }`.
|
|
|
//
|
|
|
-// If `&less-than` has value `$nil` (the default if not set), it is equivalent
|
|
|
-// to `{|a b| eq -1 (compare $a $b) }`.
|
|
|
+// You can use `&key` or `&less-than` but not both at the same time. A `&key`
|
|
|
+// function is usually preferable to a `&less-than` function because it is more
|
|
|
+// efficient extract the key for each item just once rather than on each
|
|
|
+// comparison. If both options are `$nil` then the entirety of each item is
|
|
|
+// compared using the equivalent of `{|a b| == -1 (compare $a $b) }`.
|
|
|
//
|
|
|
// Examples:
|
|
|
//
|
|
@@ -410,20 +424,103 @@ func count(fm *Frame, args ...any) (int, error) {
|
|
|
|
|
|
type orderOptions struct {
|
|
|
Reverse bool
|
|
|
+ Key Callable
|
|
|
LessThan Callable
|
|
|
}
|
|
|
|
|
|
func (opt *orderOptions) SetDefaultOptions() {}
|
|
|
|
|
|
func order(fm *Frame, opts orderOptions, inputs Inputs) error {
|
|
|
+ if opts.Key != nil && opts.LessThan != nil {
|
|
|
+ return ErrKeyAndLessThanOpts
|
|
|
+ }
|
|
|
+
|
|
|
+ values, err := collectValues(fm, opts, inputs)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ var sortErr error
|
|
|
+ cmpFn := getCmpFunction(fm, opts, values, &sortErr)
|
|
|
+ sort.SliceStable(values, cmpFn)
|
|
|
+ if sortErr != nil {
|
|
|
+ return sortErr
|
|
|
+ }
|
|
|
+
|
|
|
+ out := fm.ValueOutput()
|
|
|
+ for _, v := range values {
|
|
|
+ var err error
|
|
|
+ if opts.Key == nil {
|
|
|
+ // No `&key` option was used so simply output the original value.
|
|
|
+ err = out.Put(v)
|
|
|
+ } else {
|
|
|
+ // Ignore the key generated by the `&key` function to output the
|
|
|
+ // original value.
|
|
|
+ err = out.Put(v.([2]any)[1])
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func collectValues(fm *Frame, opts orderOptions, inputs Inputs) ([]any, error) {
|
|
|
var values []any
|
|
|
- inputs(func(v any) { values = append(values, v) })
|
|
|
+ if opts.Key == nil {
|
|
|
+ inputs(func(v any) { values = append(values, v) })
|
|
|
+ } else {
|
|
|
+ var keyErr error
|
|
|
+ inputs(func(v any) {
|
|
|
+ if keyErr != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ outputs, err := fm.CaptureOutput(func(fm *Frame) error {
|
|
|
+ return opts.Key.Call(fm, []any{v}, NoOpts)
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ keyErr = err
|
|
|
+ } else if len(outputs) != 1 {
|
|
|
+ keyErr = errors.New("&key function output more than one value")
|
|
|
+ } else {
|
|
|
+ t := [2]any{outputs[0], v}
|
|
|
+ values = append(values, t)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if keyErr != nil {
|
|
|
+ return values, keyErr
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return values, nil
|
|
|
+}
|
|
|
+
|
|
|
+func getCmpFunction(fm *Frame, opts orderOptions, values []any, sortErr *error) func(i, j int) bool {
|
|
|
+ if opts.Key != nil {
|
|
|
+ // Use the default comparison but restricted to the key precomputed by
|
|
|
+ // the `&key` function.
|
|
|
+ return func(i, j int) bool {
|
|
|
+ if *sortErr != nil {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ ki := values[i].([2]any)[0]
|
|
|
+ kj := values[j].([2]any)[0]
|
|
|
+ o := cmp(ki, kj)
|
|
|
+ if o == uncomparable {
|
|
|
+ *sortErr = ErrUncomparable
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if opts.Reverse {
|
|
|
+ return o == more
|
|
|
+ }
|
|
|
+ return o == less
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- var errSort error
|
|
|
- var lessFn func(i, j int) bool
|
|
|
if opts.LessThan != nil {
|
|
|
- lessFn = func(i, j int) bool {
|
|
|
- if errSort != nil {
|
|
|
+ // Use the custom function provided by the user to compare the value of
|
|
|
+ // each item.
|
|
|
+ return func(i, j int) bool {
|
|
|
+ if *sortErr != nil {
|
|
|
return true
|
|
|
}
|
|
|
var args []any
|
|
@@ -436,11 +533,11 @@ func order(fm *Frame, opts orderOptions, inputs Inputs) error {
|
|
|
return opts.LessThan.Call(fm, args, NoOpts)
|
|
|
})
|
|
|
if err != nil {
|
|
|
- errSort = err
|
|
|
+ *sortErr = err
|
|
|
return true
|
|
|
}
|
|
|
if len(outputs) != 1 {
|
|
|
- errSort = errs.BadValue{
|
|
|
+ *sortErr = errs.BadValue{
|
|
|
What: "output of the &less-than callback",
|
|
|
Valid: "a single boolean",
|
|
|
Actual: fmt.Sprintf("%d values", len(outputs))}
|
|
@@ -449,40 +546,27 @@ func order(fm *Frame, opts orderOptions, inputs Inputs) error {
|
|
|
if b, ok := outputs[0].(bool); ok {
|
|
|
return b
|
|
|
}
|
|
|
- errSort = errs.BadValue{
|
|
|
+ *sortErr = errs.BadValue{
|
|
|
What: "output of the &less-than callback",
|
|
|
Valid: "boolean", Actual: vals.Kind(outputs[0])}
|
|
|
return true
|
|
|
}
|
|
|
- } else {
|
|
|
- // Use default comparison implemented by cmp.
|
|
|
- lessFn = func(i, j int) bool {
|
|
|
- if errSort != nil {
|
|
|
- return true
|
|
|
- }
|
|
|
- o := cmp(values[i], values[j])
|
|
|
- if o == uncomparable {
|
|
|
- errSort = ErrUncomparable
|
|
|
- return true
|
|
|
- }
|
|
|
- if opts.Reverse {
|
|
|
- return o == more
|
|
|
- }
|
|
|
- return o == less
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
- sort.SliceStable(values, lessFn)
|
|
|
-
|
|
|
- if errSort != nil {
|
|
|
- return errSort
|
|
|
- }
|
|
|
- out := fm.ValueOutput()
|
|
|
- for _, v := range values {
|
|
|
- err := out.Put(v)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+ // Use the default comparison of each item. This is the common case when
|
|
|
+ // there is no `&key` or `&less-than` option.
|
|
|
+ return func(i, j int) bool {
|
|
|
+ if *sortErr != nil {
|
|
|
+ return true
|
|
|
}
|
|
|
+ o := cmp(values[i], values[j])
|
|
|
+ if o == uncomparable {
|
|
|
+ *sortErr = ErrUncomparable
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if opts.Reverse {
|
|
|
+ return o == more
|
|
|
+ }
|
|
|
+ return o == less
|
|
|
}
|
|
|
- return nil
|
|
|
}
|