pkg/eval: Implement "order".

This fixes #651.
This commit is contained in:
Qi Xiao 2020-05-25 17:45:08 +01:00
parent 0724478faf
commit 1cd192daaf
2 changed files with 298 additions and 1 deletions

View File

@ -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
}

View File

@ -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"),
)
}