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