diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 9fe4d191f..b1e7428ee 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -2593,7 +2593,7 @@ FUNCTIONS FOR FILTER/PUT (class=math #args=1) Hyperbolic sine. sort - (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values. + (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. An additional "v" in that string means sort maps by value, rather than by key. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values. Examples: Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"]. Note that this is numbers before strings. @@ -2604,6 +2604,9 @@ FUNCTIONS FOR FILTER/PUT Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"]. Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1]. Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}. + Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}. + Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}. + Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}. splita (class=conversion #args=2) Splits string into array with type inference. First argument is string to split; second is the separator to split on. diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index e200a8696..c26ac0d5f 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -2572,7 +2572,7 @@ FUNCTIONS FOR FILTER/PUT (class=math #args=1) Hyperbolic sine. sort - (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values. + (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. An additional "v" in that string means sort maps by value, rather than by key. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values. Examples: Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"]. Note that this is numbers before strings. @@ -2583,6 +2583,9 @@ FUNCTIONS FOR FILTER/PUT Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"]. Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1]. Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}. + Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}. + Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}. + Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}. splita (class=conversion #args=2) Splits string into array with type inference. First argument is string to split; second is the separator to split on. diff --git a/docs/src/reference-dsl-builtin-functions.md b/docs/src/reference-dsl-builtin-functions.md index 051c89ea2..41318782a 100644 --- a/docs/src/reference-dsl-builtin-functions.md +++ b/docs/src/reference-dsl-builtin-functions.md @@ -716,7 +716,7 @@ Map example: select({"a":1, "b":3, "c":5}, func(k,v) {return v >= 3}) returns {" ### sort
-sort (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
+sort (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. An additional "v" in that string means sort maps by value, rather than by key. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
Examples:
Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"].
Note that this is numbers before strings.
@@ -727,6 +727,9 @@ Case-folded descending: sort(["E","a","c","B","d"], "cr") returns ["E", "d", "c"
Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"].
Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1].
Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}.
+Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}.
+Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}.
+Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}.
## Math functions
diff --git a/docs/src/sorting.md b/docs/src/sorting.md
index fa059ab96..68e1f4a02 100644
--- a/docs/src/sorting.md
+++ b/docs/src/sorting.md
@@ -302,7 +302,9 @@ a b c
mlr -n put '
end {
# Sort map with custom function: default ordering on values
- print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv){return av <=> bv});
+ print sort({"c":2, "a": 3, "b": 1}, "v");
+ # Same:
+ print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv) {return av <=> bv});
}
'
@@ -312,12 +314,19 @@ a b c
"c": 2,
"a": 3
}
+{
+ "b": 1,
+ "c": 2,
+ "a": 3
+}
mlr -n put '
end {
# Sort map with custom function: reverse-default ordering on values
+ print sort({"c":2, "a": 3, "b": 1}, "vr");
+ # Same:
print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv){return bv <=> av});
}
'
@@ -328,6 +337,11 @@ a b c
"c": 2,
"b": 1
}
+{
+ "a": 3,
+ "c": 2,
+ "b": 1
+}
diff --git a/docs/src/sorting.md.in b/docs/src/sorting.md.in
index 7a598db20..28617c697 100644
--- a/docs/src/sorting.md.in
+++ b/docs/src/sorting.md.in
@@ -128,7 +128,9 @@ GENMD-RUN-COMMAND
mlr -n put '
end {
# Sort map with custom function: default ordering on values
- print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv){return av <=> bv});
+ print sort({"c":2, "a": 3, "b": 1}, "v");
+ # Same:
+ print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv) {return av <=> bv});
}
'
GENMD-EOF
@@ -137,6 +139,8 @@ GENMD-RUN-COMMAND
mlr -n put '
end {
# Sort map with custom function: reverse-default ordering on values
+ print sort({"c":2, "a": 3, "b": 1}, "vr");
+ # Same:
print sort({"c":2, "a": 3, "b": 1}, func(ak,av,bk,bv){return bv <=> av});
}
'
diff --git a/internal/pkg/dsl/cst/builtin_function_manager.go b/internal/pkg/dsl/cst/builtin_function_manager.go
index e23012ac9..2df380bf7 100644
--- a/internal/pkg/dsl/cst/builtin_function_manager.go
+++ b/internal/pkg/dsl/cst/builtin_function_manager.go
@@ -1848,12 +1848,14 @@ key and value, and map-element key and value; it should return the updated accum
name: "sort",
class: FUNC_CLASS_HOFS,
help: `Given a map or array as first argument and string flags or function as optional second argument,
-returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and
-then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can
-contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural
-sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for
-arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively;
-for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or
+returns a sorted copy of the input. With one argument, sorts array elements with numbers first
+numerically and then strings lexically, and map elements likewise by map keys. If the second
+argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for
+case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse.
+An additional "v" in that string means sort maps by value, rather than by key. If the second
+argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or >
+0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av,
+bk, and bv, again returning < 0, 0, or
> 0, using a and b's keys and values.`,
examples: []string{
`Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"].`,
@@ -1865,6 +1867,9 @@ for maps the function should take four arguments ak, av, bk, and bv, again retur
`Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"].`,
`Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1].`,
`Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}.`,
+ `Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}.`,
+ `Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}.`,
+ `Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}.`,
},
variadicFuncWithState: SortHOF,
minimumVariadicArity: 1,
diff --git a/internal/pkg/dsl/cst/hofs.go b/internal/pkg/dsl/cst/hofs.go
index 1e0bf8d57..e47f32ce1 100644
--- a/internal/pkg/dsl/cst/hofs.go
+++ b/internal/pkg/dsl/cst/hofs.go
@@ -526,16 +526,16 @@ func SortHOF(
if inputs[0].IsArray() {
return sortA(inputs[0], "")
} else if inputs[0].IsMap() {
- return sortMK(inputs[0], "")
+ return sortM(inputs[0], "")
} else {
return mlrval.ERROR
}
- } else if inputs[1].IsString() {
+ } else if inputs[1].IsStringOrVoid() {
if inputs[0].IsArray() {
return sortA(inputs[0], inputs[1].String())
} else if inputs[0].IsMap() {
- return sortMK(inputs[0], inputs[1].String())
+ return sortM(inputs[0], inputs[1].String())
} else {
return mlrval.ERROR
}
@@ -572,9 +572,10 @@ const (
// decodeSortFlags maps strings like "cr" in the second argument to sort
// into sortType=sortTypeCaseFold and reverse=true, etc.
-func decodeSortFlags(flags string) (tSortType, bool) {
- var sortType tSortType = sortTypeNumerical
- reverse := false
+func decodeSortFlags(flags string) (sortType tSortType, reverse bool, byMapValue bool) {
+ sortType = sortTypeNumerical
+ reverse = false
+ byMapValue = false
for _, c := range flags {
switch c {
case 'n':
@@ -587,9 +588,11 @@ func decodeSortFlags(flags string) (tSortType, bool) {
sortType = sortTypeNatural
case 'r':
reverse = true
+ case 'v':
+ byMapValue = true
}
}
- return sortType, reverse
+ return sortType, reverse, byMapValue
}
// sortA implements sort on array, with string flags rather than callback UDF.
@@ -603,7 +606,8 @@ func sortA(
output := input1.Copy()
- sortType, reverse := decodeSortFlags(flags)
+ // byMapValue is ignored for sorting arrays
+ sortType, reverse, _ := decodeSortFlags(flags)
a := output.GetArray()
switch sortType {
@@ -668,8 +672,8 @@ func sortANatural(array []*mlrval.Mlrval, reverse bool) {
}
}
-// sortA implements sort on map, with string flags rather than callback UDF.
-func sortMK(
+// sortM implements sort on map, with string flags rather than callback UDF.
+func sortM(
input1 *mlrval.Mlrval,
flags string,
) *mlrval.Mlrval {
@@ -678,99 +682,153 @@ func sortMK(
return mlrval.ERROR
}
- // Copy the keys to an array for sorting.
- // TODO: make a helper function and share with BIF_get_keys
+ // Get sort-flags, if provided
+ sortType, reverse, byMapValue := decodeSortFlags(flags)
+
+ // Copy the entries to an array for sorting.
n := inmap.FieldCount
- keys := make([]string, n)
+ entries := make([]mlrval.MlrmapEntryForArray, n)
i := 0
for pe := inmap.Head; pe != nil; pe = pe.Next {
- keys[i] = pe.Key
+ entries[i].Key = pe.Key
+ entries[i].Value = pe.Value // pointer alias for now until new map at end of this function
i++
}
- // Get sort-flags, if provided
- sortType, reverse := decodeSortFlags(flags)
-
// Do the key-sort
switch sortType {
case sortTypeNumerical:
- sortMKNumerical(keys, reverse)
+ sortMNumerical(entries, reverse, byMapValue)
case sortTypeLexical:
- sortMKLexical(keys, reverse)
+ sortMLexical(entries, reverse, byMapValue)
case sortTypeCaseFold:
- sortMKCaseFold(keys, reverse)
+ sortMCaseFold(entries, reverse, byMapValue)
case sortTypeNatural:
- sortMKNatural(keys, reverse)
+ sortMNatural(entries, reverse, byMapValue)
}
- // Make a new map with keys in the new sort order.
+ // Make a new map with entries in the new sort order.
outmap := mlrval.NewMlrmap()
for i := int64(0); i < n; i++ {
- key := keys[i]
- outmap.PutCopy(key, inmap.Get(key))
+ entry := entries[i]
+ outmap.PutCopy(entry.Key, entry.Value)
}
return mlrval.FromMap(outmap)
}
-func sortMKNumerical(array []string, reverse bool) {
- if !reverse {
- sort.Slice(array, func(i, j int) bool {
- na, erra := strconv.ParseFloat(array[i], 64)
- nb, errb := strconv.ParseFloat(array[j], 64)
- if erra == nil && errb == nil {
- return na < nb
- } else {
- return array[i] < array[j]
- }
- })
+func sortMNumerical(array []mlrval.MlrmapEntryForArray, reverse bool, byMapValue bool) {
+ if !byMapValue {
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ na, erra := strconv.ParseFloat(array[i].Key, 64)
+ nb, errb := strconv.ParseFloat(array[j].Key, 64)
+ if erra == nil && errb == nil {
+ return na < nb
+ } else {
+ return array[i].Key < array[j].Key
+ }
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ na, erra := strconv.ParseFloat(array[i].Key, 64)
+ nb, errb := strconv.ParseFloat(array[j].Key, 64)
+ if erra == nil && errb == nil {
+ return na > nb
+ } else {
+ return array[i].Key > array[j].Key
+ }
+ })
+ }
} else {
- sort.Slice(array, func(i, j int) bool {
- na, erra := strconv.ParseFloat(array[i], 64)
- nb, errb := strconv.ParseFloat(array[j], 64)
- if erra == nil && errb == nil {
- return na > nb
- } else {
- return array[i] > array[j]
- }
- })
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return mlrval.LessThan(array[i].Value, array[j].Value)
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return mlrval.LessThan(array[j].Value, array[i].Value)
+ })
+ }
}
}
-func sortMKLexical(array []string, reverse bool) {
- if !reverse {
- // Or sort.Strings(keys) would work here as well.
- sort.Slice(array, func(i, j int) bool {
- return array[i] < array[j]
- })
+func sortMLexical(array []mlrval.MlrmapEntryForArray, reverse bool, byMapValue bool) {
+ if !byMapValue {
+ if !reverse {
+ // Or sort.Strings(keys) would work here as well.
+ sort.Slice(array, func(i, j int) bool {
+ return array[i].Key < array[j].Key
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return array[i].Key > array[j].Key
+ })
+ }
} else {
- sort.Slice(array, func(i, j int) bool {
- return array[i] > array[j]
- })
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return array[i].Value.String() < array[j].Value.String()
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return array[i].Value.String() > array[j].Value.String()
+ })
+ }
}
}
-func sortMKCaseFold(array []string, reverse bool) {
- if !reverse {
- sort.Slice(array, func(i, j int) bool {
- return strings.ToLower(array[i]) < strings.ToLower(array[j])
- })
+func sortMCaseFold(array []mlrval.MlrmapEntryForArray, reverse bool, byMapValue bool) {
+ if !byMapValue {
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return strings.ToLower(array[i].Key) < strings.ToLower(array[j].Key)
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return strings.ToLower(array[i].Key) > strings.ToLower(array[j].Key)
+ })
+ }
} else {
- sort.Slice(array, func(i, j int) bool {
- return strings.ToLower(array[i]) > strings.ToLower(array[j])
- })
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return strings.ToLower(array[i].Value.String()) < strings.ToLower(array[j].Value.String())
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return strings.ToLower(array[i].Value.String()) > strings.ToLower(array[j].Value.String())
+ })
+ }
}
}
-func sortMKNatural(array []string, reverse bool) {
- if !reverse {
- sort.Slice(array, func(i, j int) bool {
- return natsort.Compare(strings.ToLower(array[i]), strings.ToLower(array[j]))
- })
+func sortMNatural(array []mlrval.MlrmapEntryForArray, reverse bool, byMapValue bool) {
+ if !byMapValue {
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return natsort.Compare(strings.ToLower(array[i].Key), strings.ToLower(array[j].Key))
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return natsort.Compare(strings.ToLower(array[j].Key), strings.ToLower(array[i].Key))
+ })
+ }
} else {
- sort.Slice(array, func(i, j int) bool {
- return natsort.Compare(strings.ToLower(array[j]), strings.ToLower(array[i]))
- })
+ if !reverse {
+ sort.Slice(array, func(i, j int) bool {
+ return natsort.Compare(
+ strings.ToLower(array[i].Value.String()),
+ strings.ToLower(array[j].Value.String()),
+ )
+ })
+ } else {
+ sort.Slice(array, func(i, j int) bool {
+ return natsort.Compare(
+ strings.ToLower(array[j].Value.String()),
+ strings.ToLower(array[i].Value.String()),
+ )
+ })
+ }
}
}
diff --git a/internal/pkg/mlrval/mlrmap.go b/internal/pkg/mlrval/mlrmap.go
index e86929298..e2596f09d 100644
--- a/internal/pkg/mlrval/mlrmap.go
+++ b/internal/pkg/mlrval/mlrmap.go
@@ -83,6 +83,13 @@ type MlrmapEntry struct {
Next *MlrmapEntry
}
+// MlrmapEntryForArray is for use by sorting routines where the Prev/Next pointers
+// are irrelevant as well as ephemeral
+type MlrmapEntryForArray struct {
+ Key string
+ Value *Mlrval
+}
+
// Only used for sorting, map-to-pairs-array and pairs-array-to-map contexts.
type MlrmapPair struct {
Key string
diff --git a/man/manpage.txt b/man/manpage.txt
index e200a8696..c26ac0d5f 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -2572,7 +2572,7 @@ FUNCTIONS FOR FILTER/PUT
(class=math #args=1) Hyperbolic sine.
sort
- (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
+ (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. An additional "v" in that string means sort maps by value, rather than by key. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
Examples:
Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"].
Note that this is numbers before strings.
@@ -2583,6 +2583,9 @@ FUNCTIONS FOR FILTER/PUT
Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"].
Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1].
Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}.
+ Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}.
+ Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}.
+ Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}.
splita
(class=conversion #args=2) Splits string into array with type inference. First argument is string to split; second is the separator to split on.
diff --git a/man/mlr.1 b/man/mlr.1
index 488d4f4fb..9ed3f82ba 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -3955,7 +3955,7 @@ Map example: select({"a":1, "b":3, "c":5}, func(k,v) {return v >= 3}) returns {"
.RS 0
.\}
.nf
- (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
+ (class=higher-order-functions #args=1-2) Given a map or array as first argument and string flags or function as optional second argument, returns a sorted copy of the input. With one argument, sorts array elements with numbers first numerically and then strings lexically, and map elements likewise by map keys. If the second argument is a string, it can contain any of "f" for lexical ("n" is for the above default), "c" for case-folded lexical, or "t" for natural sort order. An additional "r" in that string is for reverse. An additional "v" in that string means sort maps by value, rather than by key. If the second argument is a function, then for arrays it should take two arguments a and b, returning < 0, 0, or > 0 as a < b, a == b, or a > b respectively; for maps the function should take four arguments ak, av, bk, and bv, again returning < 0, 0, or > 0, using a and b's keys and values.
Examples:
Default sorting: sort([3,"A",1,"B",22]) returns [1, 3, 20, "A", "B"].
Note that this is numbers before strings.
@@ -3966,6 +3966,9 @@ Case-folded descending: sort(["E","a","c","B","d"], "cr") returns ["E", "d", "c"
Natural sorting: sort(["a1","a10","a100","a2","a20","a200"], "t") returns ["a1", "a2", "a10", "a20", "a100", "a200"].
Array with function: sort([5,2,3,1,4], func(a,b) {return b <=> a}) returns [5,4,3,2,1].
Map with function: sort({"c":2,"a":3,"b":1}, func(ak,av,bk,bv) {return bv <=> av}) returns {"a":3,"c":2,"b":1}.
+Map without function: sort({"c":2,"a":3,"b":1}) returns {"a":3,"b":1,"c":2}.
+Map without function: sort({"c":2,"a":3,"b":1}, "v") returns {"b":1,"c":2,"a":3}.
+Map without function: sort({"c":2,"a":3,"b":1}, "vnr") returns {"a":3,"c":2,"b":1}.
.fi
.if n \{\
.RE
diff --git a/test/cases/dsl-sorts/sortmv/cmd b/test/cases/dsl-sorts/sortmv/cmd
new file mode 100644
index 000000000..826119ac2
--- /dev/null
+++ b/test/cases/dsl-sorts/sortmv/cmd
@@ -0,0 +1 @@
+mlr --json --from ${CASEDIR}/input put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-sorts/sortmv/experr b/test/cases/dsl-sorts/sortmv/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-sorts/sortmv/expout b/test/cases/dsl-sorts/sortmv/expout
new file mode 100644
index 000000000..cb9aa9f1c
--- /dev/null
+++ b/test/cases/dsl-sorts/sortmv/expout
@@ -0,0 +1,143 @@
+[
+{
+ "map": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ },
+ "default": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ },
+ "reverse": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ },
+ "numup": {
+ "3": 4,
+ "2": 5,
+ "1": 6
+ },
+ "numdown": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ },
+ "lexup": {
+ "3": 4,
+ "2": 5,
+ "1": 6
+ },
+ "lexdown": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ },
+ "foldup": {
+ "3": 4,
+ "2": 5,
+ "1": 6
+ },
+ "folddown": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ }
+},
+{
+ "map": {
+ "1": 2,
+ "2": 10,
+ "3": 1
+ },
+ "default": {
+ "1": 2,
+ "2": 10,
+ "3": 1
+ },
+ "reverse": {
+ "2": 10,
+ "1": 2,
+ "3": 1
+ },
+ "numup": {
+ "3": 1,
+ "1": 2,
+ "2": 10
+ },
+ "numdown": {
+ "2": 10,
+ "1": 2,
+ "3": 1
+ },
+ "lexup": {
+ "3": 1,
+ "2": 10,
+ "1": 2
+ },
+ "lexdown": {
+ "1": 2,
+ "2": 10,
+ "3": 1
+ },
+ "foldup": {
+ "3": 1,
+ "2": 10,
+ "1": 2
+ },
+ "folddown": {
+ "1": 2,
+ "2": 10,
+ "3": 1
+ }
+},
+{
+ "map": {
+ "1": "apple",
+ "2": "Ball",
+ "3": "cat"
+ },
+ "default": {
+ "1": "apple",
+ "2": "Ball",
+ "3": "cat"
+ },
+ "reverse": {
+ "3": "cat",
+ "1": "apple",
+ "2": "Ball"
+ },
+ "numup": {
+ "2": "Ball",
+ "1": "apple",
+ "3": "cat"
+ },
+ "numdown": {
+ "3": "cat",
+ "1": "apple",
+ "2": "Ball"
+ },
+ "lexup": {
+ "2": "Ball",
+ "1": "apple",
+ "3": "cat"
+ },
+ "lexdown": {
+ "3": "cat",
+ "1": "apple",
+ "2": "Ball"
+ },
+ "foldup": {
+ "1": "apple",
+ "2": "Ball",
+ "3": "cat"
+ },
+ "folddown": {
+ "3": "cat",
+ "2": "Ball",
+ "1": "apple"
+ }
+}
+]
diff --git a/test/cases/dsl-sorts/sortmv/input b/test/cases/dsl-sorts/sortmv/input
new file mode 100644
index 000000000..90e05f90e
--- /dev/null
+++ b/test/cases/dsl-sorts/sortmv/input
@@ -0,0 +1,21 @@
+{
+ "map": {
+ "1": 6,
+ "2": 5,
+ "3": 4
+ }
+}
+{
+ "map": {
+ "1": 2,
+ "2": 10,
+ "3": 1
+ }
+}
+{
+ "map": {
+ "1": "apple",
+ "2": "Ball",
+ "3": "cat"
+ }
+}
diff --git a/test/cases/dsl-sorts/sortmv/mlr b/test/cases/dsl-sorts/sortmv/mlr
new file mode 100644
index 000000000..6f772c91e
--- /dev/null
+++ b/test/cases/dsl-sorts/sortmv/mlr
@@ -0,0 +1,8 @@
+$default = sort($map);
+$reverse = sort($map, "vr");
+$numup = sort($map, "vn");
+$numdown = sort($map, "vnr");
+$lexup = sort($map, "vf");
+$lexdown = sort($map, "vfr");
+$foldup = sort($map, "vc");
+$folddown = sort($map, "vcr");