1use indexmap::IndexMap;
2use nu_engine::command_prelude::*;
3use nu_protocol::ast::PathMember;
4
5#[derive(Clone)]
6pub struct Flatten;
7
8impl Command for Flatten {
9 fn name(&self) -> &str {
10 "flatten"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("flatten")
15 .input_output_types(vec![
16 (
17 Type::List(Box::new(Type::Any)),
18 Type::List(Box::new(Type::Any)),
19 ),
20 (Type::record(), Type::table()),
21 ])
22 .rest(
23 "rest",
24 SyntaxShape::String,
25 "Optionally flatten data by column.",
26 )
27 .switch("all", "flatten inner table one level out", Some('a'))
28 .category(Category::Filters)
29 }
30
31 fn description(&self) -> &str {
32 "Flatten the table."
33 }
34
35 fn run(
36 &self,
37 engine_state: &EngineState,
38 stack: &mut Stack,
39 call: &Call,
40 input: PipelineData,
41 ) -> Result<PipelineData, ShellError> {
42 flatten(engine_state, stack, call, input)
43 }
44
45 fn examples(&self) -> Vec<Example> {
46 vec![
47 Example {
48 description: "flatten a table",
49 example: "[[N, u, s, h, e, l, l]] | flatten ",
50 result: Some(Value::test_list(vec![
51 Value::test_string("N"),
52 Value::test_string("u"),
53 Value::test_string("s"),
54 Value::test_string("h"),
55 Value::test_string("e"),
56 Value::test_string("l"),
57 Value::test_string("l"),
58 ])),
59 },
60 Example {
61 description: "flatten a table, get the first item",
62 example: "[[N, u, s, h, e, l, l]] | flatten | first",
63 result: None, },
65 Example {
66 description: "flatten a column having a nested table",
67 example: "[[origin, people]; [Ecuador, ([[name, meal]; ['Andres', 'arepa']])]] | flatten --all | get meal",
68 result: None, },
70 Example {
71 description: "restrict the flattening by passing column names",
72 example: "[[origin, crate, versions]; [World, ([[name]; ['nu-cli']]), ['0.21', '0.22']]] | flatten versions --all | last | get versions",
73 result: None, },
75 Example {
76 description: "Flatten inner table",
77 example: "{ a: b, d: [ 1 2 3 4 ], e: [ 4 3 ] } | flatten d --all",
78 result: Some(Value::list(
79 vec![
80 Value::test_record(record! {
81 "a" => Value::test_string("b"),
82 "d" => Value::test_int(1),
83 "e" => Value::test_list(
84 vec![Value::test_int(4), Value::test_int(3)],
85 ),
86 }),
87 Value::test_record(record! {
88 "a" => Value::test_string("b"),
89 "d" => Value::test_int(2),
90 "e" => Value::test_list(
91 vec![Value::test_int(4), Value::test_int(3)],
92 ),
93 }),
94 Value::test_record(record! {
95 "a" => Value::test_string("b"),
96 "d" => Value::test_int(3),
97 "e" => Value::test_list(
98 vec![Value::test_int(4), Value::test_int(3)],
99 ),
100 }),
101 Value::test_record(record! {
102 "a" => Value::test_string("b"),
103 "d" => Value::test_int(4),
104 "e" => Value::test_list(
105 vec![Value::test_int(4), Value::test_int(3)],
106 )
107 }),
108 ],
109 Span::test_data(),
110 )),
111 },
112 ]
113 }
114}
115
116fn flatten(
117 engine_state: &EngineState,
118 stack: &mut Stack,
119 call: &Call,
120 input: PipelineData,
121) -> Result<PipelineData, ShellError> {
122 let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
123 let metadata = input.metadata();
124 let flatten_all = call.has_flag(engine_state, stack, "all")?;
125
126 input
127 .flat_map(
128 move |item| flat_value(&columns, item, flatten_all),
129 engine_state.signals(),
130 )
131 .map(|x| x.set_metadata(metadata))
132}
133
134enum TableInside {
135 Entries(String, Vec<Value>, usize),
138 FlattenedRows {
144 records: Vec<Record>,
145 parent_column_name: String,
146 parent_column_index: usize,
147 },
148}
149
150fn flat_value(columns: &[CellPath], item: Value, all: bool) -> Vec<Value> {
151 let tag = item.span();
152
153 match item {
154 Value::Record { val, .. } => {
155 let mut out = IndexMap::<String, Value>::new();
156 let mut inner_table = None;
157
158 for (column_index, (column, value)) in val.into_owned().into_iter().enumerate() {
159 let column_requested = columns.iter().find(|c| c.to_column_name() == column);
160 let need_flatten = { columns.is_empty() || column_requested.is_some() };
161 let span = value.span();
162
163 match value {
164 Value::Record { ref val, .. } => {
165 if need_flatten {
166 for (col, val) in val.clone().into_owned() {
167 if out.contains_key(&col) {
168 out.insert(format!("{column}_{col}"), val);
169 } else {
170 out.insert(col, val);
171 }
172 }
173 } else if out.contains_key(&column) {
174 out.insert(format!("{column}_{column}"), value);
175 } else {
176 out.insert(column, value);
177 }
178 }
179 Value::List { vals, .. } => {
180 if need_flatten && inner_table.is_some() {
181 return vec![Value::error(
182 ShellError::UnsupportedInput {
183 msg: "can only flatten one inner list at a time. tried flattening more than one column with inner lists... but is flattened already".into(),
184 input: "value originates from here".into(),
185 msg_span: tag,
186 input_span: span
187 },
188 span,
189 )];
190 }
191
192 if all && vals.iter().all(|f| f.as_record().is_ok()) {
193 if need_flatten {
195 let records = vals
196 .into_iter()
197 .filter_map(|v| v.into_record().ok())
198 .collect();
199
200 inner_table = Some(TableInside::FlattenedRows {
201 records,
202 parent_column_name: column,
203 parent_column_index: column_index,
204 });
205 } else if out.contains_key(&column) {
206 out.insert(format!("{column}_{column}"), Value::list(vals, span));
207 } else {
208 out.insert(column, Value::list(vals, span));
209 }
210 } else if !columns.is_empty() {
211 let cell_path =
212 column_requested.and_then(|x| match x.members.first() {
213 Some(PathMember::String { val, .. }) => Some(val),
214 _ => None,
215 });
216
217 if let Some(r) = cell_path {
218 inner_table =
219 Some(TableInside::Entries(r.clone(), vals, column_index));
220 } else {
221 out.insert(column, Value::list(vals, span));
222 }
223 } else {
224 inner_table = Some(TableInside::Entries(column, vals, column_index));
225 }
226 }
227 _ => {
228 out.insert(column, value);
229 }
230 }
231 }
232
233 let mut expanded = vec![];
234 match inner_table {
235 Some(TableInside::Entries(column, entries, parent_column_index)) => {
236 for entry in entries {
237 let base = out.clone();
238 let mut record = Record::new();
239 let mut index = 0;
240 for (col, val) in base.into_iter() {
241 if index == parent_column_index {
244 record.push(column.clone(), entry.clone());
245 }
246 record.push(col, val);
247 index += 1;
248 }
249 if index == parent_column_index {
251 record.push(column.clone(), entry);
252 }
253 expanded.push(Value::record(record, tag));
254 }
255 }
256 Some(TableInside::FlattenedRows {
257 records,
258 parent_column_name,
259 parent_column_index,
260 }) => {
261 for inner_record in records {
262 let base = out.clone();
263 let mut record = Record::new();
264 let mut index = 0;
265
266 for (base_col, base_val) in base {
267 if index == parent_column_index {
270 for (col, val) in &inner_record {
271 if record.contains(col) {
272 record.push(
273 format!("{parent_column_name}_{col}"),
274 val.clone(),
275 );
276 } else {
277 record.push(col, val.clone());
278 };
279 }
280 }
281
282 record.push(base_col, base_val);
283 index += 1;
284 }
285
286 if index == parent_column_index {
288 for (col, val) in inner_record {
289 if record.contains(&col) {
290 record.push(format!("{parent_column_name}_{col}"), val);
291 } else {
292 record.push(col, val);
293 }
294 }
295 }
296 expanded.push(Value::record(record, tag));
297 }
298 }
299 None => {
300 expanded.push(Value::record(out.into_iter().collect(), tag));
301 }
302 }
303 expanded
304 }
305 Value::List { vals, .. } => vals,
306 item => vec![item],
307 }
308}
309
310#[cfg(test)]
311mod test {
312 use super::*;
313 #[test]
314 fn test_examples() {
315 use crate::test_examples;
316
317 test_examples(Flatten {})
318 }
319}