nu_command/filters/
transpose.rs

1use nu_engine::{column::get_columns, command_prelude::*};
2
3#[derive(Clone)]
4pub struct Transpose;
5
6pub struct TransposeArgs {
7    rest: Vec<Spanned<String>>,
8    header_row: bool,
9    ignore_titles: bool,
10    as_record: bool,
11    keep_last: bool,
12    keep_all: bool,
13}
14
15impl Command for Transpose {
16    fn name(&self) -> &str {
17        "transpose"
18    }
19
20    fn signature(&self) -> Signature {
21        Signature::build("transpose")
22            .input_output_types(vec![
23                (Type::table(), Type::Any),
24                (Type::record(), Type::table()),
25            ])
26            .switch(
27                "header-row",
28                "use the first input column as the table header-row (or keynames when combined with --as-record)",
29                Some('r'),
30            )
31            .switch(
32                "ignore-titles",
33                "don't transpose the column names into values",
34                Some('i'),
35            )
36            .switch(
37                "as-record",
38                "transfer to record if the result is a table and contains only one row",
39                Some('d'),
40            )
41            .switch(
42                "keep-last",
43                "on repetition of record fields due to `header-row`, keep the last value obtained",
44                Some('l'),
45            )
46            .switch(
47                "keep-all",
48                "on repetition of record fields due to `header-row`, keep all the values obtained",
49                Some('a'),
50            )
51            .allow_variants_without_examples(true)
52            .rest(
53                "rest",
54                SyntaxShape::String,
55                "The names to give columns once transposed.",
56            )
57            .category(Category::Filters)
58    }
59
60    fn description(&self) -> &str {
61        "Transposes the table contents so rows become columns and columns become rows."
62    }
63
64    fn search_terms(&self) -> Vec<&str> {
65        vec!["pivot"]
66    }
67
68    fn run(
69        &self,
70        engine_state: &EngineState,
71        stack: &mut Stack,
72        call: &Call,
73        input: PipelineData,
74    ) -> Result<PipelineData, ShellError> {
75        transpose(engine_state, stack, call, input)
76    }
77
78    fn examples(&self) -> Vec<Example> {
79        vec![
80            Example {
81                description: "Transposes the table contents with default column names",
82                example: "[[c1 c2]; [1 2]] | transpose",
83                result: Some(Value::test_list(vec![
84                    Value::test_record(record! {
85                        "column0" => Value::test_string("c1"),
86                        "column1" => Value::test_int(1),
87                    }),
88                    Value::test_record(record! {
89                        "column0" =>  Value::test_string("c2"),
90                        "column1" =>  Value::test_int(2),
91                    }),
92                ])),
93            },
94            Example {
95                description: "Transposes the table contents with specified column names",
96                example: "[[c1 c2]; [1 2]] | transpose key val",
97                result: Some(Value::test_list(vec![
98                    Value::test_record(record! {
99                        "key" =>  Value::test_string("c1"),
100                        "val" =>  Value::test_int(1),
101                    }),
102                    Value::test_record(record! {
103                        "key" =>  Value::test_string("c2"),
104                        "val" =>  Value::test_int(2),
105                    }),
106                ])),
107            },
108            Example {
109                description: "Transposes the table without column names and specify a new column name",
110                example: "[[c1 c2]; [1 2]] | transpose --ignore-titles val",
111                result: Some(Value::test_list(vec![
112                    Value::test_record(record! {
113                        "val" => Value::test_int(1),
114                    }),
115                    Value::test_record(record! {
116                        "val" => Value::test_int(2),
117                    }),
118                ])),
119            },
120            Example {
121                description: "Transfer back to record with -d flag",
122                example: "{c1: 1, c2: 2} | transpose | transpose --ignore-titles -r -d",
123                result: Some(Value::test_record(record! {
124                    "c1" =>  Value::test_int(1),
125                    "c2" =>  Value::test_int(2),
126                })),
127            },
128        ]
129    }
130}
131
132pub fn transpose(
133    engine_state: &EngineState,
134    stack: &mut Stack,
135    call: &Call,
136    input: PipelineData,
137) -> Result<PipelineData, ShellError> {
138    let name = call.head;
139    let args = TransposeArgs {
140        header_row: call.has_flag(engine_state, stack, "header-row")?,
141        ignore_titles: call.has_flag(engine_state, stack, "ignore-titles")?,
142        as_record: call.has_flag(engine_state, stack, "as-record")?,
143        keep_last: call.has_flag(engine_state, stack, "keep-last")?,
144        keep_all: call.has_flag(engine_state, stack, "keep-all")?,
145        rest: call.rest(engine_state, stack, 0)?,
146    };
147
148    if !args.rest.is_empty() && args.header_row {
149        return Err(ShellError::IncompatibleParametersSingle {
150            msg: "Can not provide header names and use `--header-row`".into(),
151            span: call.get_flag_span(stack, "header-row").expect("has flag"),
152        });
153    }
154    if !args.header_row && args.keep_all {
155        return Err(ShellError::IncompatibleParametersSingle {
156            msg: "Can only be used with `--header-row`(`-r`)".into(),
157            span: call.get_flag_span(stack, "keep-all").expect("has flag"),
158        });
159    }
160    if !args.header_row && args.keep_last {
161        return Err(ShellError::IncompatibleParametersSingle {
162            msg: "Can only be used with `--header-row`(`-r`)".into(),
163            span: call.get_flag_span(stack, "keep-last").expect("has flag"),
164        });
165    }
166    if args.keep_all && args.keep_last {
167        return Err(ShellError::IncompatibleParameters {
168            left_message: "can't use `--keep-last` at the same time".into(),
169            left_span: call.get_flag_span(stack, "keep-last").expect("has flag"),
170            right_message: "because of `--keep-all`".into(),
171            right_span: call.get_flag_span(stack, "keep-all").expect("has flag"),
172        });
173    }
174
175    let metadata = input.metadata();
176    let input: Vec<_> = input.into_iter().collect();
177
178    // Ensure error values are propagated and non-record values are rejected
179    for value in input.iter() {
180        match value {
181            Value::Error { .. } => {
182                return Ok(value.clone().into_pipeline_data_with_metadata(metadata));
183            }
184            Value::Record { .. } => {} // go on, this is what we're looking for
185            _ => {
186                return Err(ShellError::OnlySupportsThisInputType {
187                    exp_input_type: "table or record".into(),
188                    wrong_type: "list<any>".into(),
189                    dst_span: call.head,
190                    src_span: value.span(),
191                });
192            }
193        }
194    }
195
196    let descs = get_columns(&input);
197
198    let mut headers: Vec<String> = Vec::with_capacity(input.len());
199
200    if args.header_row {
201        for i in input.iter() {
202            if let Some(desc) = descs.first() {
203                match &i.get_data_by_key(desc) {
204                    Some(x) => {
205                        if let Ok(s) = x.coerce_string() {
206                            headers.push(s);
207                        } else {
208                            return Err(ShellError::GenericError {
209                                error: "Header row needs string headers".into(),
210                                msg: "used non-string headers".into(),
211                                span: Some(name),
212                                help: None,
213                                inner: vec![],
214                            });
215                        }
216                    }
217                    _ => {
218                        return Err(ShellError::GenericError {
219                            error: "Header row is incomplete and can't be used".into(),
220                            msg: "using incomplete header row".into(),
221                            span: Some(name),
222                            help: None,
223                            inner: vec![],
224                        });
225                    }
226                }
227            } else {
228                return Err(ShellError::GenericError {
229                    error: "Header row is incomplete and can't be used".into(),
230                    msg: "using incomplete header row".into(),
231                    span: Some(name),
232                    help: None,
233                    inner: vec![],
234                });
235            }
236        }
237    } else {
238        for i in 0..=input.len() {
239            if let Some(name) = args.rest.get(i) {
240                headers.push(name.item.clone())
241            } else {
242                headers.push(format!("column{i}"));
243            }
244        }
245    }
246
247    let mut descs = descs.into_iter();
248    if args.header_row {
249        descs.next();
250    }
251    let mut result_data = descs
252        .map(|desc| {
253            let mut column_num: usize = 0;
254            let mut record = Record::new();
255
256            if !args.ignore_titles && !args.header_row {
257                record.push(
258                    headers[column_num].clone(),
259                    Value::string(desc.clone(), name),
260                );
261                column_num += 1
262            }
263
264            for i in input.iter() {
265                let x = i
266                    .get_data_by_key(&desc)
267                    .unwrap_or_else(|| Value::nothing(name));
268                match record.get_mut(&headers[column_num]) {
269                    None => {
270                        record.push(headers[column_num].clone(), x);
271                    }
272                    Some(val) => {
273                        if args.keep_all {
274                            let current_span = val.span();
275                            match val {
276                                Value::List { vals, .. } => {
277                                    vals.push(x);
278                                }
279                                v => {
280                                    *v = Value::list(vec![std::mem::take(v), x], current_span);
281                                }
282                            };
283                        } else if args.keep_last {
284                            *val = x;
285                        }
286                    }
287                }
288
289                column_num += 1;
290            }
291
292            Value::record(record, name)
293        })
294        .collect::<Vec<Value>>();
295    if result_data.len() == 1 && args.as_record {
296        Ok(PipelineData::value(
297            result_data
298                .pop()
299                .expect("already check result only contains one item"),
300            metadata,
301        ))
302    } else {
303        Ok(result_data.into_pipeline_data_with_metadata(
304            name,
305            engine_state.signals().clone(),
306            metadata,
307        ))
308    }
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314
315    #[test]
316    fn test_examples() {
317        use crate::test_examples;
318
319        test_examples(Transpose {})
320    }
321}