Skip to main content

nu_command/filters/
transpose.rs

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