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 columns = columns
79            .into_iter()
80            .map(|col| PathMember::string(col, false, Casing::Sensitive, call.head))
81            .collect();
82        let mapper = Box::new(item_mapper_by_col(columns));
83
84        let vec: Vec<_> = input.into_iter().collect();
85        uniq(engine_state, stack, call, vec, mapper, metadata)
86    }
87
88    fn examples(&self) -> Vec<Example<'_>> {
89        vec![
90            Example {
91                description: "Get rows from table filtered by column uniqueness.",
92                example: "[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 7]] | uniq-by fruit",
93                result: Some(Value::test_list(vec![
94                    Value::test_record(record! {
95                        "fruit" => Value::test_string("apple"),
96                        "count" => Value::test_int(9),
97                    }),
98                    Value::test_record(record! {
99                        "fruit" => Value::test_string("pear"),
100                        "count" => Value::test_int(3),
101                    }),
102                    Value::test_record(record! {
103                        "fruit" => Value::test_string("orange"),
104                        "count" => Value::test_int(7),
105                    }),
106                ])),
107            },
108            Example {
109                description: "Get rows from table filtered by column uniqueness, keeping the last occurrence of each duplicate.",
110                example: "[[fruit count]; [apple 9] [apple 2] [pear 3] [orange 7]] | uniq-by fruit --keep-last",
111                result: Some(Value::test_list(vec![
112                    Value::test_record(record! {
113                        "fruit" => Value::test_string("apple"),
114                        "count" => Value::test_int(2),
115                    }),
116                    Value::test_record(record! {
117                        "fruit" => Value::test_string("pear"),
118                        "count" => Value::test_int(3),
119                    }),
120                    Value::test_record(record! {
121                        "fruit" => Value::test_string("orange"),
122                        "count" => Value::test_int(7),
123                    }),
124                ])),
125            },
126        ]
127    }
128}
129
130fn item_mapper_by_col(
131    columns: Vec<PathMember>,
132) -> impl Fn(crate::ItemMapperState) -> Result<crate::ValueCounter, ShellError> {
133    move |ms: crate::ItemMapperState| -> Result<crate::ValueCounter, ShellError> {
134        // Resolve each requested column while building the comparison value.
135        // Validation and extraction share the same access semantics.
136        let item_column_values = columns
137            .iter()
138            .map(|column| {
139                ms.item
140                    .follow_cell_path(std::slice::from_ref(column))
141                    .map(|value| value.into_owned())
142            })
143            .collect::<Result<Vec<_>, _>>()?;
144
145        let col_vals = Value::list(item_column_values, ms.head);
146
147        Ok(crate::ValueCounter::new_vals_to_compare(
148            ms.item,
149            ms.flag_ignore_case,
150            col_vals,
151            ms.index,
152            ms.head,
153        ))
154    }
155}
156
157#[cfg(test)]
158mod test {
159    use super::*;
160
161    #[test]
162    fn test_examples() -> nu_test_support::Result {
163        nu_test_support::test().examples(UniqBy)
164    }
165}