Skip to main content

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 a table by extracting nested values."
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    mut input: PipelineData,
121) -> Result<PipelineData, ShellError> {
122    let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
123    let metadata = input.take_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 val = val.into_owned();
156            let retained_outer_columns: Vec<String> = if columns.is_empty() {
157                vec![]
158            } else {
159                val.iter()
160                    .filter_map(|(column, value)| {
161                        let column_requested =
162                            columns.iter().find(|c| c.to_column_name() == *column);
163                        let need_flatten = column_requested.is_some();
164
165                        let will_flatten = match value {
166                            Value::Record { .. } => need_flatten,
167                            Value::List { vals, .. } => {
168                                if all && vals.iter().all(|value| value.as_record().is_ok()) {
169                                    need_flatten
170                                } else {
171                                    matches!(
172                                        column_requested
173                                            .and_then(|cell_path| cell_path.members.first()),
174                                        Some(PathMember::String { .. })
175                                    )
176                                }
177                            }
178                            _ => false,
179                        };
180
181                        (!will_flatten).then_some(column.clone())
182                    })
183                    .collect()
184            };
185            let mut out = IndexMap::<String, Value>::new();
186            let mut inner_table = None;
187
188            for (column_index, (column, value)) in val.into_iter().enumerate() {
189                let column_requested = columns.iter().find(|c| c.to_column_name() == column);
190                let need_flatten = { columns.is_empty() || column_requested.is_some() };
191                let span = value.span();
192
193                match value {
194                    Value::Record { ref val, .. } => {
195                        if need_flatten {
196                            for (col, val) in val.clone().into_owned() {
197                                if out.contains_key(&col)
198                                    || retained_outer_columns.iter().any(|column| column == &col)
199                                {
200                                    out.insert(format!("{column}_{col}"), val);
201                                } else {
202                                    out.insert(col, val);
203                                }
204                            }
205                        } else if out.contains_key(&column) {
206                            out.insert(format!("{column}_{column}"), value);
207                        } else {
208                            out.insert(column, value);
209                        }
210                    }
211                    Value::List { vals, .. } => {
212                        if need_flatten && inner_table.is_some() {
213                            return vec![Value::error(
214                                ShellError::UnsupportedInput {
215                                    msg: "can only flatten one inner list at a time. tried flattening more than one column with inner lists... but is flattened already".into(),
216                                    input: "value originates from here".into(),
217                                    msg_span: tag,
218                                    input_span: span
219                                },
220                                span,
221                            )];
222                        }
223
224                        if all && vals.iter().all(|f| f.as_record().is_ok()) {
225                            // it's a table (a list of record, we can flatten inner record)
226                            if need_flatten {
227                                let records = vals
228                                    .into_iter()
229                                    .filter_map(|v| v.into_record().ok())
230                                    .collect();
231
232                                inner_table = Some(TableInside::FlattenedRows {
233                                    records,
234                                    parent_column_name: column,
235                                    parent_column_index: column_index,
236                                });
237                            } else if out.contains_key(&column) {
238                                out.insert(format!("{column}_{column}"), Value::list(vals, span));
239                            } else {
240                                out.insert(column, Value::list(vals, span));
241                            }
242                        } else if !columns.is_empty() {
243                            let cell_path =
244                                column_requested.and_then(|x| match x.members.first() {
245                                    Some(PathMember::String { val, .. }) => Some(val),
246                                    _ => None,
247                                });
248
249                            if let Some(r) = cell_path {
250                                inner_table =
251                                    Some(TableInside::Entries(r.clone(), vals, column_index));
252                            } else {
253                                out.insert(column, Value::list(vals, span));
254                            }
255                        } else {
256                            inner_table = Some(TableInside::Entries(column, vals, column_index));
257                        }
258                    }
259                    _ => {
260                        out.insert(column, value);
261                    }
262                }
263            }
264
265            let mut expanded = vec![];
266            match inner_table {
267                Some(TableInside::Entries(column, entries, parent_column_index)) => {
268                    for entry in entries {
269                        let base = out.clone();
270                        let mut record = Record::new();
271                        let mut index = 0;
272                        for (col, val) in base.into_iter() {
273                            // meet the flattened column, push them to result record first
274                            // this can avoid output column order changed.
275                            if index == parent_column_index {
276                                record.push(column.clone(), entry.clone());
277                            }
278                            record.push(col, val);
279                            index += 1;
280                        }
281                        // the flattened column may be the last column in the original table.
282                        if index == parent_column_index {
283                            record.push(column.clone(), entry);
284                        }
285                        expanded.push(Value::record(record, tag));
286                    }
287                }
288                Some(TableInside::FlattenedRows {
289                    records,
290                    parent_column_name,
291                    parent_column_index,
292                }) => {
293                    for inner_record in records {
294                        let base = out.clone();
295                        let mut record = Record::new();
296                        let mut index = 0;
297
298                        for (base_col, base_val) in base {
299                            // meet the flattened column, push them to result record first
300                            // this can avoid output column order changed.
301                            if index == parent_column_index {
302                                for (col, val) in &inner_record {
303                                    if record.contains(col)
304                                        || (!columns.is_empty() && out.contains_key(col))
305                                    {
306                                        record.push(
307                                            format!("{parent_column_name}_{col}"),
308                                            val.clone(),
309                                        );
310                                    } else {
311                                        record.push(col, val.clone());
312                                    };
313                                }
314                            }
315
316                            record.push(base_col, base_val);
317                            index += 1;
318                        }
319
320                        // the flattened column may be the last column in the original table.
321                        if index == parent_column_index {
322                            for (col, val) in inner_record {
323                                if record.contains(&col)
324                                    || (!columns.is_empty() && out.contains_key(&col))
325                                {
326                                    record.push(format!("{parent_column_name}_{col}"), val);
327                                } else {
328                                    record.push(col, val);
329                                }
330                            }
331                        }
332                        expanded.push(Value::record(record, tag));
333                    }
334                }
335                None => {
336                    expanded.push(Value::record(out.into_iter().collect(), tag));
337                }
338            }
339            expanded
340        }
341        Value::List { vals, .. } => vals,
342        item => vec![item],
343    }
344}
345
346#[cfg(test)]
347mod test {
348    use super::*;
349    #[test]
350    fn test_examples() -> nu_test_support::Result {
351        nu_test_support::test().examples(Flatten)
352    }
353}