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                "repeated",
29                "Return the input values that occur more than once",
30                Some('d'),
31            )
32            .switch(
33                "ignore-case",
34                "Ignore differences in case when comparing input values",
35                Some('i'),
36            )
37            .switch(
38                "unique",
39                "Return the input values that occur once only",
40                Some('u'),
41            )
42            .allow_variants_without_examples(true)
43            .category(Category::Filters)
44    }
45
46    fn description(&self) -> &str {
47        "Return the distinct values in the input by the given column(s)."
48    }
49
50    fn search_terms(&self) -> Vec<&str> {
51        vec!["distinct", "deduplicate"]
52    }
53
54    fn run(
55        &self,
56        engine_state: &EngineState,
57        stack: &mut Stack,
58        call: &Call,
59        input: PipelineData,
60    ) -> Result<PipelineData, ShellError> {
61        let columns: Vec<String> = call.rest(engine_state, stack, 0)?;
62
63        if columns.is_empty() {
64            return Err(ShellError::MissingParameter {
65                param_name: "columns".into(),
66                span: call.head,
67            });
68        }
69
70        let metadata = input.metadata();
71
72        let vec: Vec<_> = input.into_iter().collect();
73        match validate(&vec, &columns, call.head) {
74            Ok(_) => {}
75            Err(err) => {
76                return Err(err);
77            }
78        }
79
80        let mapper = Box::new(item_mapper_by_col(columns));
81
82        uniq(engine_state, stack, call, vec, mapper, metadata)
83    }
84
85    fn examples(&self) -> Vec<Example<'_>> {
86        vec![Example {
87            description: "Get rows from table filtered by column uniqueness ",
88            example: "[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 7]] | uniq-by fruit",
89            result: Some(Value::test_list(vec![
90                Value::test_record(record! {
91                    "fruit" => Value::test_string("apple"),
92                    "count" => Value::test_int(9),
93                }),
94                Value::test_record(record! {
95                    "fruit" => Value::test_string("pear"),
96                    "count" => Value::test_int(3),
97                }),
98                Value::test_record(record! {
99                    "fruit" => Value::test_string("orange"),
100                    "count" => Value::test_int(7),
101                }),
102            ])),
103        }]
104    }
105}
106
107fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellError> {
108    let first = vec.first();
109    if let Some(v) = first {
110        let val_span = v.span();
111        if let Value::Record { val: record, .. } = &v {
112            if columns.is_empty() {
113                return Err(ShellError::GenericError {
114                    error: "expected name".into(),
115                    msg: "requires a column name to filter table data".into(),
116                    span: Some(span),
117                    help: None,
118                    inner: vec![],
119                });
120            }
121
122            if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
123                return Err(ShellError::CantFindColumn {
124                    col_name: nonexistent,
125                    span: Some(span),
126                    src_span: val_span,
127                });
128            }
129        }
130    }
131
132    Ok(())
133}
134
135fn get_data_by_columns(columns: &[String], item: &Value) -> Vec<Value> {
136    columns
137        .iter()
138        .filter_map(|col| item.get_data_by_key(col))
139        .collect::<Vec<_>>()
140}
141
142fn item_mapper_by_col(cols: Vec<String>) -> impl Fn(crate::ItemMapperState) -> crate::ValueCounter {
143    let columns = cols;
144
145    Box::new(move |ms: crate::ItemMapperState| -> crate::ValueCounter {
146        let item_column_values = get_data_by_columns(&columns, &ms.item);
147
148        let col_vals = Value::list(item_column_values, Span::unknown());
149
150        crate::ValueCounter::new_vals_to_compare(ms.item, ms.flag_ignore_case, col_vals, ms.index)
151    })
152}
153
154#[cfg(test)]
155mod test {
156    use super::*;
157
158    #[test]
159    fn test_examples() {
160        use crate::test_examples;
161
162        test_examples(UniqBy {})
163    }
164}