1use nu_engine::{ClosureEval, command_prelude::*};
2use nu_protocol::engine::Closure;
3use nu_protocol::shell_error::generic::GenericError;
4
5#[derive(Clone)]
6pub struct Reduce;
7
8impl Command for Reduce {
9 fn name(&self) -> &str {
10 "reduce"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("reduce")
15 .input_output_types(vec![
16 (Type::List(Box::new(Type::Any)), Type::Any),
17 (Type::table(), Type::Any),
18 (Type::Range, Type::Any),
19 ])
20 .named(
21 "fold",
22 SyntaxShape::Any,
23 "Reduce with initial value.",
24 Some('f'),
25 )
26 .required(
27 "closure",
28 SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])),
29 "Reducing function.",
30 )
31 .allow_variants_without_examples(true)
32 .category(Category::Filters)
33 }
34
35 fn description(&self) -> &str {
36 "Aggregate a list (starting from the left) to a single value using an accumulator closure."
37 }
38
39 fn search_terms(&self) -> Vec<&str> {
40 vec!["map", "fold", "foldl"]
41 }
42
43 fn examples(&self) -> Vec<Example<'_>> {
44 vec![
45 Example {
46 example: "[ 1 2 3 4 ] | reduce {|it, acc| $it + $acc }",
47 description: "Sum values of a list (same as 'math sum').",
48 result: Some(Value::test_int(10)),
49 },
50 Example {
51 example: "[ 1 2 3 4 ] | reduce {|it, acc| $acc - $it }",
52 description: "`reduce` accumulates value from left to right, equivalent to (((1 - 2) - 3) - 4).",
53 result: Some(Value::test_int(-8)),
54 },
55 Example {
56 example: "[ 8 7 6 ] | enumerate | reduce --fold 0 {|it, acc| $acc + $it.item + $it.index }",
57 description: "Sum values of a list, plus their indexes.",
58 result: Some(Value::test_int(24)),
59 },
60 Example {
61 example: "[ 1 2 3 4 ] | reduce --fold 10 {|it, acc| $acc + $it }",
62 description: "Sum values with a starting value (fold).",
63 result: Some(Value::test_int(20)),
64 },
65 Example {
66 example: r#"[[foo baz] [baz quux]] | reduce --fold "foobar" {|it, acc| $acc | str replace $it.0 $it.1}"#,
67 description: "Iteratively perform string replace (from left to right): 'foobar' -> 'bazbar' -> 'quuxbar'.",
68 result: Some(Value::test_string("quuxbar")),
69 },
70 Example {
71 example: r#"[ i o t ] | reduce --fold "Arthur, King of the Britons" {|it, acc| $acc | str replace --all $it "X" }"#,
72 description: "Replace selected characters in a string with 'X'.",
73 result: Some(Value::test_string("ArXhur, KXng Xf Xhe BrXXXns")),
74 },
75 Example {
76 example: r#"['foo.gz', 'bar.gz', 'baz.gz'] | enumerate | reduce --fold '' {|str all| $"($all)(if $str.index != 0 {'; '})($str.index + 1)-($str.item)" }"#,
77 description: "Add ascending numbers to each of the filenames, and join with semicolons.",
78 result: Some(Value::test_string("1-foo.gz; 2-bar.gz; 3-baz.gz")),
79 },
80 Example {
81 example: r#"let s = "Str"; 0..2 | reduce --fold '' {|it, acc| $acc + $s}"#,
82 description: "Concatenate a string with itself, using a range to determine the number of times.",
83 result: Some(Value::test_string("StrStrStr")),
84 },
85 Example {
86 example: "[{a: 1} {b: 2} {c: 3}] | reduce {|it| merge $it}",
87 description: "Merge multiple records together, making use of the fact that the accumulated value is also supplied as pipeline input to the closure.",
88 result: Some(Value::test_record(record!(
89 "a" => Value::test_int(1),
90 "b" => Value::test_int(2),
91 "c" => Value::test_int(3),
92 ))),
93 },
94 ]
95 }
96
97 fn run(
98 &self,
99 engine_state: &EngineState,
100 stack: &mut Stack,
101 call: &Call,
102 input: PipelineData,
103 ) -> Result<PipelineData, ShellError> {
104 let head = call.head;
105 let fold: Option<Value> = call.get_flag(engine_state, stack, "fold")?;
106 let closure: Closure = call.req(engine_state, stack, 0)?;
107
108 let mut iter = input.into_iter();
109
110 let mut acc = fold.or_else(|| iter.next()).ok_or_else(|| {
111 ShellError::Generic(GenericError::new("Expected input", "needs input", head))
112 })?;
113
114 let mut closure = ClosureEval::new(engine_state, stack, closure);
115
116 for value in iter {
117 engine_state.signals().check(&head)?;
118 acc = closure
119 .add_arg(value)
120 .add_arg(acc.clone())
121 .run_with_input(PipelineData::value(acc, None))?
122 .into_value(head)?;
123 }
124
125 Ok(acc.with_span(head).into_pipeline_data())
126 }
127}
128
129#[cfg(test)]
130mod test {
131 use super::*;
132
133 #[test]
134 fn test_examples() -> nu_test_support::Result {
135 nu_test_support::test().examples(Reduce)
136 }
137}