From 1cd192daaf40ef46fd5378881ec832fa7faaaa85 Mon Sep 17 00:00:00 2001 From: Qi Xiao Date: Mon, 25 May 2020 17:45:08 +0100 Subject: [PATCH] pkg/eval: Implement "order". This fixes #651. --- pkg/eval/builtin_fn_container.go | 235 ++++++++++++++++++++++++++ pkg/eval/builtin_fn_container_test.go | 64 ++++++- 2 files changed, 298 insertions(+), 1 deletion(-) diff --git a/pkg/eval/builtin_fn_container.go b/pkg/eval/builtin_fn_container.go index c41fe0f3..52b327fd 100644 --- a/pkg/eval/builtin_fn_container.go +++ b/pkg/eval/builtin_fn_container.go @@ -3,6 +3,8 @@ package eval import ( "errors" "fmt" + "math" + "sort" "github.com/elves/elvish/pkg/eval/errs" "github.com/elves/elvish/pkg/eval/vals" @@ -391,6 +393,8 @@ func init() { "count": count, "keys": keys, + + "order": order, }) } @@ -626,3 +630,234 @@ func keys(fm *Frame, v interface{}) error { return true }) } + +//elvdoc:fn order +// +// ```elvish +// order &reverse=$false &stable=$false $less-than=$nil~ $inputs? +// ``` +// +// Outputs the input values sorted in ascending order. +// +// The `&reverse` option, if true, reverses the order of output. +// +// The `&stable` option, if true, makes the sort +// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). +// +// 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 +// argument. If the function throws an exception, `order` rethrows the exception +// without outputting any value. +// +// If `&less-than` has value `$nil` (the default if not set), the following +// comparison algorithm is used: +// +// - Numbers are compared numerically. For the sake of sorting, `NaN` is treated +// as smaller than all other numbers. +// +// - Strings are compared lexicographically by bytes, which is equivalent to +// comparing by codepoints under UTF-8. +// +// - Lists are compared lexicographically by elements, if the elements at the +// same positions are comparable. +// +// If the ordering between two elements are not defined by the conditions above, +// no value is outputted and an exception is thrown. +// +// Examples: +// +// ```elvish-transcript +// ~> put foo bar ipsum | order +// ▶ bar +// ▶ foo +// ▶ ipsum +// ~> order [(float64 10) (float64 1) (float64 5)] +// ▶ (float64 1) +// ▶ (float64 5) +// ▶ (float64 10) +// ~> order [[a b] [a] [b b] [a c]] +// ▶ [a] +// ▶ [a b] +// ▶ [a c] +// ▶ [b b] +// ~> order &reverse [a c b] +// ▶ c +// ▶ b +// ▶ a +// ~> order &less-than=[a b]{ eq $a x } &stable [l x o r x e x m] +// ▶ x +// ▶ x +// ▶ x +// ▶ l +// ▶ o +// ▶ r +// ▶ e +// ▶ m +// ``` +// +// Beware that strings that look like numbers are treated as strings, not +// numbers. To sort strings as numbers, use an explicit `&less-than` option: +// +// ```elvish-transcript +// ~> order [5 1 10] +// ▶ 1 +// ▶ 10 +// ▶ 5 +// ~> order &less-than=[a b]{ < $a $b } [5 1 10] +// ▶ 1 +// ▶ 5 +// ▶ 10 +// ``` + +type orderOptions struct { + Reverse bool + Stable bool + LessThan Callable +} + +func (opt *orderOptions) SetDefaultOptions() {} + +var errUncomparable = errs.BadValue{ + What: `inputs to "order"`, + Valid: "comparable values", Actual: "uncomparable values"} + +func order(fm *Frame, opts orderOptions, inputs Inputs) error { + var values []interface{} + inputs(func(v interface{}) { values = append(values, v) }) + + var errSort error + var lessFn func(i, j int) bool + if opts.LessThan != nil { + lessFn = func(i, j int) bool { + if errSort != nil { + return true + } + var args []interface{} + if opts.Reverse { + args = []interface{}{values[j], values[i]} + } else { + args = []interface{}{values[i], values[j]} + } + outputs, err := fm.CaptureOutput(func(fm *Frame) error { + return opts.LessThan.Call(fm, args, NoOpts) + }) + if err != nil { + errSort = err + return true + } + if len(outputs) != 1 { + errSort = errs.BadValue{ + What: "output of the &less-than callback", + Valid: "a single boolean", + Actual: fmt.Sprintf("%d values", len(outputs))} + return true + } + if b, ok := outputs[0].(bool); ok { + return b + } + errSort = errs.BadValue{ + What: "output of the &less-than callback", + Valid: "boolean", Actual: vals.Kind(outputs[0])} + return true + } + } else { + // Use default comparison implemented by compare. + lessFn = func(i, j int) bool { + if errSort != nil { + return true + } + o := compare(values[i], values[j]) + if o == uncomparable { + errSort = errUncomparable + return true + } + if opts.Reverse { + return o == more + } + return o == less + } + } + + if opts.Stable { + sort.SliceStable(values, lessFn) + } else { + sort.Slice(values, lessFn) + } + + if errSort != nil { + return errSort + } + for _, v := range values { + fm.OutputChan() <- v + } + return nil +} + +type ordering uint8 + +const ( + less ordering = iota + equal + more + uncomparable +) + +func compare(a, b interface{}) ordering { + switch a := a.(type) { + case float64: + if b, ok := b.(float64); ok { + switch { + case math.IsNaN(a): + if math.IsNaN(b) { + return equal + } + return less + case math.IsNaN(b): + return more + case a == b: + return equal + case a < b: + return less + default: + // a > b + return more + } + } + case string: + if b, ok := b.(string); ok { + switch { + case a == b: + return equal + case a < b: + return less + default: + // a > b + return more + } + } + case vals.List: + if b, ok := b.(vals.List); ok { + aIt := a.Iterator() + bIt := b.Iterator() + for aIt.HasElem() && bIt.HasElem() { + o := compare(aIt.Elem(), bIt.Elem()) + if o != equal { + return o + } + aIt.Next() + bIt.Next() + } + switch { + case a.Len() == b.Len(): + return equal + case a.Len() < b.Len(): + return less + default: + // a.Len() > b.Len() + return more + } + } + } + return uncomparable +} diff --git a/pkg/eval/builtin_fn_container_test.go b/pkg/eval/builtin_fn_container_test.go index ecce1d7f..dd7ec882 100644 --- a/pkg/eval/builtin_fn_container_test.go +++ b/pkg/eval/builtin_fn_container_test.go @@ -1,6 +1,8 @@ package eval import ( + "errors" + "math" "testing" "github.com/elves/elvish/pkg/eval/errs" @@ -77,6 +79,66 @@ func TestBuiltinFnContainer(t *testing.T) { That(`keys [&a=foo]`).Puts("a"), // Windows does not have an external sort command. Disabled until we have a // builtin sort command. - // That(`keys [&a=foo &b=bar] | each echo | sort | each $put~`).Puts("a", "b"), + That(`keys [&a=foo &b=bar] | order`).Puts("a", "b"), + + // Ordering strings + That("put foo bar ipsum | order").Puts("bar", "foo", "ipsum"), + That("put foo bar bar | order").Puts("bar", "bar", "foo"), + That("put 10 1 5 2 | order").Puts("1", "10", "2", "5"), + // Ordering numbers + That("put 10 1 5 2 | each $float64~ | order").Puts(1.0, 2.0, 5.0, 10.0), + That("put 10 1 1 | each $float64~ | order").Puts(1.0, 1.0, 10.0), + That("put 10 NaN 1 | each $float64~ | order").Puts(math.NaN(), 1.0, 10.0), + That("put NaN NaN 1 | each $float64~ | order"). + Puts(math.NaN(), math.NaN(), 1.0), + // Ordering lists + That("put [b] [a] | order").Puts(vals.MakeList("a"), vals.MakeList("b")), + That("put [a] [b] [a] | order"). + Puts(vals.MakeList("a"), vals.MakeList("a"), vals.MakeList("b")), + That("put [(float64 10)] [(float64 2)] | order"). + Puts(vals.MakeList(2.0), vals.MakeList(10.0)), + That("put [a b] [b b] [a c] | order"). + Puts( + vals.MakeList("a", "b"), + vals.MakeList("a", "c"), vals.MakeList("b", "b")), + That("put [a] [] [a (float64 2)] [a (float64 1)] | order"). + Puts(vals.EmptyList, vals.MakeList("a"), + vals.MakeList("a", 1.0), vals.MakeList("a", 2.0)), + // Attempting to order uncomparable values + That("put a (float64 1) b (float64 2) | order"). + Throws(errUncomparable, "order"), + That("put [a] [(float64 1)] | order"). + Throws(errUncomparable, "order"), + // &reverse + That("put foo bar ipsum | order &reverse").Puts("ipsum", "foo", "bar"), + // &less-than + That("put 1 10 2 5 | order &less-than=[a b]{ < $a $b }"). + Puts("1", "2", "5", "10"), + // &less-than writing more than one value + That("put 1 10 2 5 | order &less-than=[a b]{ put $true $false }"). + Throws( + errs.BadValue{ + What: "output of the &less-than callback", + Valid: "a single boolean", Actual: "2 values"}, + "order &less-than=[a b]{ put $true $false }"), + // &less-than writing non-boolean value + That("put 1 10 2 5 | order &less-than=[a b]{ put x }"). + Throws( + errs.BadValue{ + What: "output of the &less-than callback", + Valid: "boolean", Actual: "string"}, + "order &less-than=[a b]{ put x }"), + // &less-than throwing an exception + That("put 1 10 2 5 | order &less-than=[a b]{ fail bad }"). + Throws( + errors.New("bad"), + "fail bad ", "order &less-than=[a b]{ fail bad }"), + // &less-than and &reverse + That("put 1 10 2 5 | order &reverse &less-than=[a b]{ < $a $b }"). + Puts("10", "5", "2", "1"), + // &stable - test by pretending that all values but one are equal, and + // check that the order among them has not changed + That("put l x o x r x e x m | order &stable &less-than=[a b]{ eq $a x }"). + Puts("x", "x", "x", "x", "l", "o", "r", "e", "m"), ) }