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}