nu_command/conversions/into/
record.rs1use chrono::{DateTime, Datelike, FixedOffset, Timelike};
2use nu_engine::command_prelude::*;
3use nu_protocol::format_duration_as_timeperiod;
4
5#[derive(Clone)]
6pub struct IntoRecord;
7
8impl Command for IntoRecord {
9 fn name(&self) -> &str {
10 "into record"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("into record")
15 .input_output_types(vec![
16 (Type::Date, Type::record()),
17 (Type::Duration, Type::record()),
18 (Type::List(Box::new(Type::Any)), Type::record()),
19 (Type::record(), Type::record()),
20 ])
21 .category(Category::Conversions)
22 }
23
24 fn description(&self) -> &str {
25 "Convert value to record."
26 }
27
28 fn search_terms(&self) -> Vec<&str> {
29 vec!["convert"]
30 }
31
32 fn run(
33 &self,
34 _engine_state: &EngineState,
35 _stack: &mut Stack,
36 call: &Call,
37 input: PipelineData,
38 ) -> Result<PipelineData, ShellError> {
39 into_record(call, input)
40 }
41
42 fn examples(&self) -> Vec<Example> {
43 vec![
44 Example {
45 description: "Convert from one row table to record",
46 example: "[[value]; [false]] | into record",
47 result: Some(Value::test_record(record! {
48 "value" => Value::test_bool(false),
49 })),
50 },
51 Example {
52 description: "Convert from list of records to record",
53 example: "[{foo: bar} {baz: quux}] | into record",
54 result: Some(Value::test_record(record! {
55 "foo" => Value::test_string("bar"),
56 "baz" => Value::test_string("quux"),
57 })),
58 },
59 Example {
60 description: "Convert from list of pairs into record",
61 example: "[[foo bar] [baz quux]] | into record",
62 result: Some(Value::test_record(record! {
63 "foo" => Value::test_string("bar"),
64 "baz" => Value::test_string("quux"),
65 })),
66 },
67 Example {
68 description: "convert duration to record (weeks max)",
69 example: "(-500day - 4hr - 5sec) | into record",
70 result: Some(Value::test_record(record! {
71 "week" => Value::test_int(71),
72 "day" => Value::test_int(3),
73 "hour" => Value::test_int(4),
74 "second" => Value::test_int(5),
75 "sign" => Value::test_string("-"),
76 })),
77 },
78 Example {
79 description: "convert record to record",
80 example: "{a: 1, b: 2} | into record",
81 result: Some(Value::test_record(record! {
82 "a" => Value::test_int(1),
83 "b" => Value::test_int(2),
84 })),
85 },
86 Example {
87 description: "convert date to record",
88 example: "2020-04-12T22:10:57+02:00 | into record",
89 result: Some(Value::test_record(record! {
90 "year" => Value::test_int(2020),
91 "month" => Value::test_int(4),
92 "day" => Value::test_int(12),
93 "hour" => Value::test_int(22),
94 "minute" => Value::test_int(10),
95 "second" => Value::test_int(57),
96 "millisecond" => Value::test_int(0),
97 "microsecond" => Value::test_int(0),
98 "nanosecond" => Value::test_int(0),
99 "timezone" => Value::test_string("+02:00"),
100 })),
101 },
102 Example {
103 description: "convert date components to table columns",
104 example: "2020-04-12T22:10:57+02:00 | into record | transpose | transpose -r",
105 result: None,
106 },
107 ]
108 }
109}
110
111fn into_record(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
112 let span = input.span().unwrap_or(call.head);
113 match input {
114 PipelineData::Value(Value::Date { val, .. }, _) => {
115 Ok(parse_date_into_record(val, span).into_pipeline_data())
116 }
117 PipelineData::Value(Value::Duration { val, .. }, _) => {
118 Ok(parse_duration_into_record(val, span).into_pipeline_data())
119 }
120 PipelineData::Value(Value::List { .. }, _) | PipelineData::ListStream(..) => {
121 let mut record = Record::new();
122 let metadata = input.metadata();
123
124 enum ExpectedType {
125 Record,
126 Pair,
127 }
128 let mut expected_type = None;
129
130 for item in input.into_iter() {
131 let span = item.span();
132 match item {
133 Value::Record { val, .. }
134 if matches!(expected_type, None | Some(ExpectedType::Record)) =>
135 {
136 for (key, val) in val.into_owned() {
138 record.insert(key, val);
139 }
140 expected_type = Some(ExpectedType::Record);
141 }
142 Value::List { mut vals, .. }
143 if matches!(expected_type, None | Some(ExpectedType::Pair)) =>
144 {
145 if vals.len() == 2 {
146 let (val, key) = vals.pop().zip(vals.pop()).expect("length is < 2");
147 record.insert(key.coerce_into_string()?, val);
148 } else {
149 return Err(ShellError::IncorrectValue {
150 msg: format!(
151 "expected inner list with two elements, but found {} element(s)",
152 vals.len()
153 ),
154 val_span: span,
155 call_span: call.head,
156 });
157 }
158 expected_type = Some(ExpectedType::Pair);
159 }
160 Value::Nothing { .. } => {}
161 Value::Error { error, .. } => return Err(*error),
162 _ => {
163 return Err(ShellError::TypeMismatch {
164 err_message: format!(
165 "expected {}, found {} (while building record from list)",
166 match expected_type {
167 Some(ExpectedType::Record) => "record",
168 Some(ExpectedType::Pair) => "list with two elements",
169 None => "record or list with two elements",
170 },
171 item.get_type(),
172 ),
173 span,
174 })
175 }
176 }
177 }
178 Ok(Value::record(record, span).into_pipeline_data_with_metadata(metadata))
179 }
180 PipelineData::Value(Value::Record { .. }, _) => Ok(input),
181 PipelineData::Value(Value::Error { error, .. }, _) => Err(*error),
182 other => Err(ShellError::TypeMismatch {
183 err_message: format!("Can't convert {} to record", other.get_type()),
184 span,
185 }),
186 }
187}
188
189fn parse_date_into_record(date: DateTime<FixedOffset>, span: Span) -> Value {
190 Value::record(
191 record! {
192 "year" => Value::int(date.year() as i64, span),
193 "month" => Value::int(date.month() as i64, span),
194 "day" => Value::int(date.day() as i64, span),
195 "hour" => Value::int(date.hour() as i64, span),
196 "minute" => Value::int(date.minute() as i64, span),
197 "second" => Value::int(date.second() as i64, span),
198 "millisecond" => Value::int(date.timestamp_subsec_millis() as i64, span),
199 "microsecond" => Value::int((date.nanosecond() / 1_000 % 1_000) as i64, span),
200 "nanosecond" => Value::int((date.nanosecond() % 1_000) as i64, span),
201 "timezone" => Value::string(date.offset().to_string(), span),
202 },
203 span,
204 )
205}
206
207fn parse_duration_into_record(duration: i64, span: Span) -> Value {
208 let (sign, periods) = format_duration_as_timeperiod(duration);
209
210 let mut record = Record::new();
211 for p in periods {
212 let num_with_unit = p.to_text().to_string();
213 let split = num_with_unit.split(' ').collect::<Vec<&str>>();
214 record.push(
215 match split[1] {
216 "ns" => "nanosecond",
217 "µs" => "microsecond",
218 "ms" => "millisecond",
219 "sec" => "second",
220 "min" => "minute",
221 "hr" => "hour",
222 "day" => "day",
223 "wk" => "week",
224 _ => "unknown",
225 },
226 Value::int(split[0].parse().unwrap_or(0), span),
227 );
228 }
229
230 record.push(
231 "sign",
232 Value::string(if sign == -1 { "-" } else { "+" }, span),
233 );
234
235 Value::record(record, span)
236}
237
238#[cfg(test)]
239mod test {
240 use super::*;
241
242 #[test]
243 fn test_examples() {
244 use crate::test_examples;
245
246 test_examples(IntoRecord {})
247 }
248}