Skip to main content

nu_command/filters/
reduce.rs

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}