nu_command/filters/
flatten.rs

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,//Some(Value::test_string("N")),
65            },
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,//Some(Value::test_string("arepa")),
70            },
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, //Some(Value::test_string("0.22")),
75            },
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    // handle for a column which contains a single list(but not list of records)
137    // it contains (column, span, values in the column, column index).
138    Entries(String, Vec<Value>, usize),
139    // handle for a column which contains a table, we can flatten the inner column to outer level
140    // `records` is the nested/inner table to flatten to the outer level
141    // `parent_column_name` is handled for conflicting column name, the nested table may contains columns which has the same name
142    // to outer level, for that case, the output column name should be f"{parent_column_name}_{inner_column_name}".
143    // `parent_column_index` is the column index in original table.
144    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                            // it's a table (a list of record, we can flatten inner record)
195                            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                            // meet the flattened column, push them to result record first
243                            // this can avoid output column order changed.
244                            if index == parent_column_index {
245                                record.push(column.clone(), entry.clone());
246                            }
247                            record.push(col, val);
248                            index += 1;
249                        }
250                        // the flattened column may be the last column in the original table.
251                        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                            // meet the flattened column, push them to result record first
269                            // this can avoid output column order changed.
270                            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                        // the flattened column may be the last column in the original table.
288                        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}