排序
Nushell提供了多种数据排序方式,具体使用哪种方法取决于问题和处理的数据类型。让我们看看一些可能的排序方式。
基本排序
列表
基本列表排序完全符合预期:
[9 3 8 1 4 6] | sort
# => ╭───┬───╮
# => │ 0 │ 1 │
# => │ 1 │ 3 │
# => │ 2 │ 4 │
# => │ 3 │ 6 │
# => │ 4 │ 8 │
# => │ 5 │ 9 │
# => ╰───┴───╯
但当混合不同类型时,情况会变得复杂。例如,看看当我们有包含数字和字符串的列表时会发生什么:
["hello" 4 9 2 1 "foobar" 8 6] | sort
# => ╭───┬────────╮
# => │ 0 │ 1 │
# => │ 1 │ 2 │
# => │ 2 │ 4 │
# => │ 3 │ 6 │
# => │ 4 │ 8 │
# => │ 5 │ 9 │
# => │ 6 │ foobar │
# => │ 7 │ hello │
# => ╰───┴────────╯
可以看到数字按顺序排序,字符串排在列表末尾,也按顺序排列。如果你来自其他编程语言,这可能不完全符合预期。在Nushell中,作为一般规则,数据总是可以无错误地排序。
提示
如果你确实希望包含不同类型的排序出错,请参阅严格排序。
Nushell的排序也是稳定的,意味着相等的值会保持它们原来的相对顺序。这里使用不区分大小写排序选项来说明:
["foo" "FOO" "BAR" "bar"] | sort -i
# => ╭───┬─────╮
# => │ 0 │ BAR │
# => │ 1 │ bar │
# => │ 2 │ foo │
# => │ 3 │ FOO │
# => ╰───┴─────╯
由于此排序不区分大小写,foo
和FOO
被视为相等,bar
和BAR
也是如此。在结果中,大写的BAR
排在小写的bar
之前,因为大写的BAR
在输入中也排在小写的bar
之前。同样,小写的foo
在输入和结果中都排在大写的FOO
之前。
记录
记录可以按两种方式排序:按键和按值。默认情况下,将记录传递给sort
会按键排序:
{x: 123, a: hello!, foo: bar} | sort
# => ╭─────┬────────╮
# => │ a │ hello! │
# => │ foo │ bar │
# => │ x │ 123 │
# => ╰─────┴────────╯
要按值排序,使用-v
标志:
{x: 123, a: hello! foo: bar} | sort -v
# => ╭─────┬────────╮
# => │ x │ 123 │
# => │ foo │ bar │
# => │ a │ hello! │
# => ╰─────┴────────╯
表格
表格行通过按列顺序比较行来排序。如果两行在第一列中的值相等,则按第二列排序。这重复进行,直到行排序不同或所有列都相等。
let items = [
{id: 100, quantity: 10, price: 5 }
{id: 100, quantity: 5, price: 8 }
{id: 100, quantity: 5, price: 1 }
]
$items | sort
# => ╭───┬─────┬──────────┬───────╮
# => │ # │ id │ quantity │ price │
# => ├───┼─────┼──────────┼───────┤
# => │ 0 │ 100 │ 5 │ 1 │
# => │ 1 │ 100 │ 5 │ 8 │
# => │ 2 │ 100 │ 10 │ 5 │
# => ╰───┴─────┴──────────┴───────╯
在这个例子中,所有项目的id
列都相等。然后,数量为5
的两个项目排在数量为10
的项目之前。最后,价格为1
的项目排在价格为8
的项目之前。
结构化数据排序
单元格路径
为了排序更复杂的类型,如表格,可以使用sort-by
命令。sort-by
可以通过单元格路径对其输入进行排序。
这是一个按文件大小排序的目录示例:
ls | sort-by size
# => ╭───┬─────────────────────┬──────┬──────────┬────────────────╮
# => │ # │ name │ type │ size │ modified │
# => ├───┼─────────────────────┼──────┼──────────┼────────────────┤
# => │ 0 │ my-secret-plans.txt │ file │ 100 B │ 10 minutes ago │
# => │ 1 │ shopping_list.txt │ file │ 100 B │ 2 months ago │
# => │ 2 │ myscript.nu │ file │ 1.1 KiB │ 2 weeks ago │
# => │ 3 │ bigfile.img │ file │ 10.0 MiB │ 3 weeks ago │
# => ╰───┴─────────────────────┴──────┴──────────┴────────────────╯
我们还可以向sort-by
提供多个单元格路径,这将按每个单元格路径的优先级顺序排序。你可以将提供多个单元格路径视为对具有相等值的元素的"决胜局"。让我们先按大小排序,然后按修改时间排序:
ls | sort-by size modified
# => ╭───┬─────────────────────┬──────┬──────────┬────────────────╮
# => │ # │ name │ type │ size │ modified │
# => ├───┼─────────────────────┼──────┼──────────┼────────────────┤
# => │ 0 │ shopping_list.txt │ file │ 100 B │ 2 months ago │
# => │ 1 │ my-secret-plans.txt │ file │ 100 B │ 10 minutes ago │
# => │ 2 │ myscript.nu │ file │ 1.1 KiB │ 2 weeks ago │
# => │ 3 │ bigfile.img │ file │ 10.0 MiB │ 3 weeks ago │
# => ╰───┴─────────────────────┴──────┴──────────┴────────────────╯
这次,shopping_list.txt
排在my-secret-plans.txt
之前,因为它有更早的修改时间,但两个较大的文件仍然排在.txt
文件之后。
此外,我们可以使用更复杂的单元格路径来排序嵌套数据:
let cities = [
{name: 'New York', info: { established: 1624, population: 18_819_000 } }
{name: 'Kyoto', info: { established: 794, population: 37_468_000 } }
{name: 'São Paulo', info: { established: 1554, population: 21_650_000 } }
]
$cities | sort-by info.established
# => ╭───┬───────────┬────────────────────────────╮
# => │ # │ name │ info │
# => ├───┼───────────┼────────────────────────────┤
# => │ 0 │ Kyoto │ ╭─────────────┬──────────╮ │
# => │ │ │ │ established │ 794 │ │
# => │ │ │ │ population │ 37468000 │ │
# => │ │ │ ╰─────────────┴──────────╯ │
# => │ 1 │ São Paulo │ ╭─────────────┬──────────╮ │
# => │ │ │ │ established │ 1554 │ │
# => │ │ │ │ population │ 21650000 │ │
# => │ │ │ ╰─────────────┴──────────╯ │
# => │ 2 │ New York │ ╭─────────────┬──────────╮ │
# => │ │ │ │ established │ 1624 │ │
# => │ │ │ │ population │ 18819000 │ │
# => │ │ │ ╰─────────────┴──────────╯ │
# => ╰───┴───────────┴────────────────────────────╯
按键闭包排序
有时,以比"递增"或"递减"更复杂的方式排序数据很有用。你可以提供一个闭包,它将每个值转换为排序键,而不改变底层数据。这是一个按键闭包的例子,我们想按平均成绩排序作业列表:
let assignments = [
{name: 'Homework 1', grades: [97 89 86 92 89] }
{name: 'Homework 2', grades: [91 100 60 82 91] }
{name: 'Exam 1', grades: [78 88 78 53 90] }
{name: 'Project', grades: [92 81 82 84 83] }
]
$assignments | sort-by { get grades | math avg }
# => ╭───┬────────────┬───────────────────────╮
# => │ # │ name │ grades │
# => ├───┼────────────┼───────────────────────┤
# => │ 0 │ Exam 1 │ [78, 88, 78, 53, 90] │
# => │ 1 │ Project │ [92, 81, 82, 84, 83] │
# => │ 2 │ Homework 2 │ [91, 100, 60, 82, 91] │
# => │ 3 │ Homework 1 │ [97, 89, 86, 92, 89] │
# => ╰───┴────────────┴───────────────────────╯
值被传递到键闭包的管道输入中,但你也可以将其用作参数:
let weight = {alpha: 10, beta: 5, gamma: 3}
[alpha gamma beta gamma alpha] | sort-by {|val| $weight | get $val }
# => ╭───┬───────╮
# => │ 0 │ gamma │
# => │ 1 │ gamma │
# => │ 2 │ beta │
# => │ 3 │ alpha │
# => │ 4 │ alpha │
# => ╰───┴───────╯
自定义排序顺序
除了按键闭包,sort-by
还支持指定自定义排序顺序的闭包。--custom
或-c
标志会告诉sort-by
将闭包解释为自定义排序闭包。自定义排序闭包有两个参数,并返回一个布尔值。如果第一个参数在排序顺序中应排在第二个参数之前,则闭包应返回true
。
对于一个简单的例子,我们可以将单元格路径排序重写为自定义排序。这可以理解为"如果$a.size
小于$b.size
,则a
应出现在排序顺序中的b
之前":
ls | sort-by -c {|a, b| $a.size < $b.size }
# => ╭───┬─────────────────────┬──────┬──────────┬────────────────╮
# => │ # │ name │ type │ size │ modified │
# => ├───┼─────────────────────┼──────┼──────────┼────────────────┤
# => │ 0 │ my-secret-plans.txt │ file │ 100 B │ 10 minutes ago │
# => │ 1 │ shopping_list.txt │ file │ 100 B │ 2 months ago │
# => │ 2 │ myscript.nu │ file │ 1.1 KiB │ 2 weeks ago │
# => │ 3 │ bigfile.img │ file │ 10.0 MiB │ 3 weeks ago │
# => ╰───┴─────────────────────┴──────┴──────────┴────────────────╯
提示
参数也作为两个元素的列表传递给自定义闭包,因此以下是等价的:
{|a, b| $a < $b }
{ $in.0 < $in.1 }
这是一个无法简单地写为键排序的自定义排序示例。在这个例子中,我们有一个任务队列,每个任务都有一定的工作时间和优先级。我们想按优先级排序(最高优先)。如果一个任务的工作时间为零,我们想立即安排它;否则,我们忽略工作时间。
let queue = [
{task: 139, work_time: 0, priority: 1 }
{task: 52, work_time: 355, priority: 8 }
{task: 948, work_time: 72, priority: 2 }
{task: 583, work_time: 0, priority: 5 }
]
let my_sort = {|a, b|
match [$a.work_time, $b.work_time] {
[0, 0] => ($a.priority > $b.priority) # 如果工作时间相等,回退到优先级
[0, _] => true, # 只有a的工作时间为0,所以a排在b之前
[_, 0] => false, # 只有b的工作时间为0,所以a排在b之后
_ => ($a.priority > $b.priority) # 两者都有非零工作时间,按优先级排序
}
}
$queue | sort-by -c $my_sort
特殊排序
不区分大小写排序
使用不区分大小写排序时,除大小写外相同的字符串(和通配符)将被视为相等,而其他类型不受影响:
let data = [
Nushell,
foobar,
10,
nushell,
FoOBaR,
9
]
$data | sort -i
# => ╭───┬─────────╮
# => │ 0 │ 9 │
# => │ 1 │ 10 │
# => │ 2 │ foobar │
# => │ 3 │ FoOBaR │
# => │ 4 │ Nushell │
# => │ 5 │ nushell │
# => ╰───┴─────────╯
自然排序
自然排序选项允许包含数字的字符串以与数字通常排序相同的方式排序。这适用于仅由数字组成的字符串,以及包含数字和字母的字符串:
let data = ["10", "9", "foo123", "foo20", "bar123", "bar20"]
$data | sort
# => ╭───┬────────╮
# => │ 0 │ 10 │
# => │ 1 │ 9 │
# => │ 2 │ bar123 │
# => │ 3 │ bar20 │
# => │ 4 │ foo123 │
# => │ 5 │ foo20 │
# => ╰───┴────────╯
# "1"排在"9"之前,所以"10"排在"9"之前
$data | sort -n
# => ╭───┬────────╮
# => │ 0 │ 9 │
# => │ 1 │ 10 │
# => │ 2 │ bar20 │
# => │ 3 │ bar123 │
# => │ 4 │ foo20 │
# => │ 5 │ foo123 │
# => ╰───┴────────╯
此外,自然排序允许你将数字与数字字符串一起排序:
let data = [4, "6.2", 1, "10", 2, 8.1, "3", 5.5, "9", 7]
$data | sort -n
# => ╭───┬──────╮
# => │ 0 │ 1 │
# => │ 1 │ 2 │
# => │ 2 │ 3 │
# => │ 3 │ 4 │
# => │ 4 │ 5.50 │
# => │ 5 │ 6.2 │
# => │ 6 │ 7 │
# => │ 7 │ 8.10 │
# => │ 8 │ 9 │
# => │ 9 │ 10 │
# => ╰───┴──────╯
混合类型排序
在某些情况下,你可能需要对包含混合类型的数据进行排序。排序混合类型时需要注意以下几点:
- 通常,相同类型的值会在排序顺序中相邻出现。例如,排序后的数字排在前面,然后是排序后的字符串,然后是排序后的列表。
- 某些类型会在排序顺序中混合。这些是:
- 整数和浮点数。例如,
[2.2, 1, 3]
将排序为[1, 2.2, 3]
。 - 字符串和通配符。例如,
[("b" | into glob) a c]
将排序为[a b c]
(其中b仍然是通配符)。 - 如果使用自然排序,整数、浮点数和字符串将如该节所述混合排序。
- 整数和浮点数。例如,
- 非混合类型之间的排序顺序不保证,除了
null
值,它总是被排序到列表的末尾。- 在同一个Nushell版本中,排序顺序应该总是一样的,但这不应被依赖。如果你的代码对跨类型的排序敏感,请考虑使用自定义排序来更好地表达你的需求。
如果你需要对可能包含混合类型的数据进行排序,请考虑以下策略之一:
严格排序
自定义排序闭包还提供了一种简单的方法来对数据进行排序,同时确保只有具有明确定义比较的类型才能一起排序。这利用了要求兼容数据类型的运算符:
let compatible = [8 3.2 null 58 2]
let incompatible = ["hello" 4 9 2 1 "meow" 8 6]
$compatible | sort-by -c {|a, b| $a < $b | default ($a != null) }
# => ╭───┬──────╮
# => │ 0 │ 2 │
# => │ 1 │ 3.20 │
# => │ 2 │ 8 │
# => │ 3 │ 58 │
# => │ 4 │ │
# => ╰───┴──────╯
$incompatible | sort-by -c {|a, b| $a < $b | default ($a != null) }
# => Error: nu::shell::type_mismatch
# =>
# => × Type mismatch during operation.
# => ╭─[entry #26:1:36]
# => 1 │ $incompatible | sort-by -c {|a, b| $a < $b | default ($a != null) }
# => · ─┬ ┬ ─┬
# => · │ │ ╰── string
# => · │ ╰── type mismatch for operator
# => · ╰── int
# => ╰────
null
值需要特殊处理,因为任何值与null
的比较都会返回null
。要拒绝null
值,请尝试以下方法:
let baddata = [8 3.2 null 58 2]
let strict = {|a, b|
match [$a, $b] {
[null, _] => (error make {msg: "Attempt to sort null"}),
[_, null] => (error make {msg: "Attempt to sort null"}),
_ => ($a < $b)
}
}
$baddata | sort-by -c $strict
# => Error: × Attempt to sort null
# => ╭─[entry #3:4:21]
# => 3 │ match [$a, $b] {
# => 4 │ [null, _] => (error make {msg: "Attempt to sort null"}),
# => · ─────┬────
# => · ╰── originates from here
# => 5 │ [_, null] => (error make {msg: "Attempt to sort null"}),
# => ╰────