Skip to main content

nu_command/filters/
uniq_by.rs

1pub use super::uniq;
2use nu_engine::{column::nonexistent_column, command_prelude::*};
3use nu_protocol::shell_error::generic::GenericError;
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    let first = vec.first();
135    if let Some(v) = first {
136        let val_span = v.span();
137        if let Value::Record { val: record, .. } = &v {
138            if columns.is_empty() {
139                return Err(ShellError::Generic(GenericError::new(
140                    "expected name",
141                    "requires a column name to filter table data",
142                    span,
143                )));
144            }
145
146            if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
147                return Err(ShellError::CantFindColumn {
148                    col_name: nonexistent,
149                    span: Some(span),
150                    src_span: val_span,
151                });
152            }
153        }
154    }
155
156    Ok(())
157}
158
159fn get_data_by_columns(columns: &[String], item: &Value) -> Vec<Value> {
160    columns
161        .iter()
162        .filter_map(|col| item.get_data_by_key(col))
163        .collect::<Vec<_>>()
164}
165
166fn item_mapper_by_col(cols: Vec<String>) -> impl Fn(crate::ItemMapperState) -> crate::ValueCounter {
167    let columns = cols;
168
169    Box::new(move |ms: crate::ItemMapperState| -> crate::ValueCounter {
170        let item_column_values = get_data_by_columns(&columns, &ms.item);
171
172        let col_vals = Value::list(item_column_values, ms.head);
173
174        crate::ValueCounter::new_vals_to_compare(
175            ms.item,
176            ms.flag_ignore_case,
177            col_vals,
178            ms.index,
179            ms.head,
180        )
181    })
182}
183
184#[cfg(test)]
185mod test {
186    use super::*;
187
188    #[test]
189    fn test_examples() -> nu_test_support::Result {
190        nu_test_support::test().examples(UniqBy)
191    }
192}