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