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 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 { .. } => {} _ => {
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}