Skip to main content

nu_command/filters/
uniq_by.rs

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