Skip to main content

nu_command/filters/
uniq_by.rs

1pub use super::uniq;
2use nu_engine::{column::nonexistent_column, command_prelude::*};
3
4#[derive(Clone)]
5pub struct UniqBy;
6
7impl Command for UniqBy {
8    fn name(&self) -> &str {
9        "uniq-by"
10    }
11
12    fn signature(&self) -> Signature {
13        Signature::build("uniq-by")
14            .input_output_types(vec![
15                (Type::table(), Type::table()),
16                (
17                    Type::List(Box::new(Type::Any)),
18                    Type::List(Box::new(Type::Any)),
19                ),
20            ])
21            .rest("columns", SyntaxShape::Any, "The column(s) to filter by.")
22            .switch(
23                "count",
24                "Return a table containing the distinct input values together with their counts.",
25                Some('c'),
26            )
27            .switch(
28                "keep-last",
29                "Return the last occurrence of each unique value instead of the first.",
30                Some('l'),
31            )
32            .switch(
33                "repeated",
34                "Return the input values that occur more than once.",
35                Some('d'),
36            )
37            .switch(
38                "ignore-case",
39                "Ignore differences in case when comparing input values.",
40                Some('i'),
41            )
42            .switch(
43                "unique",
44                "Return the input values that occur once only.",
45                Some('u'),
46            )
47            .allow_variants_without_examples(true)
48            .category(Category::Filters)
49    }
50
51    fn description(&self) -> &str {
52        "Return the distinct values in the input by the given column(s)."
53    }
54
55    fn search_terms(&self) -> Vec<&str> {
56        vec!["distinct", "deduplicate"]
57    }
58
59    fn run(
60        &self,
61        engine_state: &EngineState,
62        stack: &mut Stack,
63        call: &Call,
64        input: PipelineData,
65    ) -> Result<PipelineData, ShellError> {
66        let columns: Vec<String> = call.rest(engine_state, stack, 0)?;
67
68        if columns.is_empty() {
69            return Err(ShellError::MissingParameter {
70                param_name: "columns".into(),
71                span: call.head,
72            });
73        }
74
75        let metadata = input.metadata();
76
77        let vec: Vec<_> = input.into_iter().collect();
78        match validate(&vec, &columns, call.head) {
79            Ok(_) => {}
80            Err(err) => {
81                return Err(err);
82            }
83        }
84
85        let mapper = Box::new(item_mapper_by_col(columns));
86
87        uniq(engine_state, stack, call, vec, mapper, metadata)
88    }
89
90    fn examples(&self) -> Vec<Example<'_>> {
91        vec![
92            Example {
93                description: "Get rows from table filtered by column uniqueness.",
94                example: "[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 7]] | uniq-by fruit",
95                result: Some(Value::test_list(vec![
96                    Value::test_record(record! {
97                        "fruit" => Value::test_string("apple"),
98                        "count" => Value::test_int(9),
99                    }),
100                    Value::test_record(record! {
101                        "fruit" => Value::test_string("pear"),
102                        "count" => Value::test_int(3),
103                    }),
104                    Value::test_record(record! {
105                        "fruit" => Value::test_string("orange"),
106                        "count" => Value::test_int(7),
107                    }),
108                ])),
109            },
110            Example {
111                description: "Get rows from table filtered by column uniqueness, keeping the last occurrence of each duplicate.",
112                example: "[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 7]] | uniq-by fruit --keep-last",
113                result: Some(Value::test_list(vec![
114                    Value::test_record(record! {
115                        "fruit" => Value::test_string("apple"),
116                        "count" => Value::test_int(2),
117                    }),
118                    Value::test_record(record! {
119                        "fruit" => Value::test_string("pear"),
120                        "count" => Value::test_int(3),
121                    }),
122                    Value::test_record(record! {
123                        "fruit" => Value::test_string("orange"),
124                        "count" => Value::test_int(7),
125                    }),
126                ])),
127            },
128        ]
129    }
130}
131
132fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellError> {
133    let first = vec.first();
134    if let Some(v) = first {
135        let val_span = v.span();
136        if let Value::Record { val: record, .. } = &v {
137            if columns.is_empty() {
138                return Err(ShellError::GenericError {
139                    error: "expected name".into(),
140                    msg: "requires a column name to filter table data".into(),
141                    span: Some(span),
142                    help: None,
143                    inner: vec![],
144                });
145            }
146
147            if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
148                return Err(ShellError::CantFindColumn {
149                    col_name: nonexistent,
150                    span: Some(span),
151                    src_span: val_span,
152                });
153            }
154        }
155    }
156
157    Ok(())
158}
159
160fn get_data_by_columns(columns: &[String], item: &Value) -> Vec<Value> {
161    columns
162        .iter()
163        .filter_map(|col| item.get_data_by_key(col))
164        .collect::<Vec<_>>()
165}
166
167fn item_mapper_by_col(cols: Vec<String>) -> impl Fn(crate::ItemMapperState) -> crate::ValueCounter {
168    let columns = cols;
169
170    Box::new(move |ms: crate::ItemMapperState| -> crate::ValueCounter {
171        let item_column_values = get_data_by_columns(&columns, &ms.item);
172
173        let col_vals = Value::list(item_column_values, Span::unknown());
174
175        crate::ValueCounter::new_vals_to_compare(ms.item, ms.flag_ignore_case, col_vals, ms.index)
176    })
177}
178
179#[cfg(test)]
180mod test {
181    use super::*;
182
183    #[test]
184    fn test_examples() {
185        use crate::test_examples;
186
187        test_examples(UniqBy {})
188    }
189}