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