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