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 a 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 input = input;
122 let mut record = Record::new();
123 let metadata = input.take_metadata();
124
125 enum ExpectedType {
126 Record,
127 Pair,
128 }
129 let mut expected_type = None;
130
131 for item in input.into_iter() {
132 let span = item.span();
133 match item {
134 Value::Record { val, .. }
135 if matches!(expected_type, None | Some(ExpectedType::Record)) =>
136 {
137 for (key, val) in val.into_owned() {
139 record.insert(key, val);
140 }
141 expected_type = Some(ExpectedType::Record);
142 }
143 Value::List { mut vals, .. }
144 if matches!(expected_type, None | Some(ExpectedType::Pair)) =>
145 {
146 if vals.len() == 2 {
147 let (val, key) = vals.pop().zip(vals.pop()).expect("length is < 2");
148 record.insert(key.coerce_into_string()?, val);
149 } else {
150 return Err(ShellError::IncorrectValue {
151 msg: format!(
152 "expected inner list with two elements, but found {} element(s)",
153 vals.len()
154 ),
155 val_span: span,
156 call_span: call.head,
157 });
158 }
159 expected_type = Some(ExpectedType::Pair);
160 }
161 Value::Nothing { .. } => {}
162 Value::Error { error, .. } => return Err(*error),
163 _ => {
164 return Err(ShellError::TypeMismatch {
165 err_message: format!(
166 "expected {}, found {} (while building record from list)",
167 match expected_type {
168 Some(ExpectedType::Record) => "record",
169 Some(ExpectedType::Pair) => "list with two elements",
170 None => "record or list with two elements",
171 },
172 item.get_type(),
173 ),
174 span,
175 });
176 }
177 }
178 }
179 Ok(Value::record(record, span).into_pipeline_data_with_metadata(metadata))
180 }
181 PipelineData::Value(Value::Record { .. }, _) => Ok(input),
182 PipelineData::Value(Value::Error { error, .. }, _) => Err(*error),
183 other => Err(ShellError::TypeMismatch {
184 err_message: format!("Can't convert {} to record", other.get_type()),
185 span,
186 }),
187 }
188}
189
190fn parse_date_into_record(date: DateTime<FixedOffset>, span: Span) -> Value {
191 Value::record(
192 record! {
193 "year" => Value::int(date.year() as i64, span),
194 "month" => Value::int(date.month() as i64, span),
195 "day" => Value::int(date.day() as i64, span),
196 "hour" => Value::int(date.hour() as i64, span),
197 "minute" => Value::int(date.minute() as i64, span),
198 "second" => Value::int(date.second() as i64, span),
199 "millisecond" => Value::int(date.timestamp_subsec_millis() as i64, span),
200 "microsecond" => Value::int((date.nanosecond() / 1_000 % 1_000) as i64, span),
201 "nanosecond" => Value::int((date.nanosecond() % 1_000) as i64, span),
202 "timezone" => Value::string(date.offset().to_string(), span),
203 },
204 span,
205 )
206}
207
208fn parse_duration_into_record(duration: i64, span: Span) -> Value {
209 let (sign, periods) = format_duration_as_timeperiod(duration);
210
211 let mut record = Record::new();
212 for p in periods {
213 let num_with_unit = p.to_text().to_string();
214 let split = num_with_unit.split(' ').collect::<Vec<&str>>();
215 record.push(
216 match split[1] {
217 "ns" => "nanosecond",
218 "µs" => "microsecond",
219 "ms" => "millisecond",
220 "sec" => "second",
221 "min" => "minute",
222 "hr" => "hour",
223 "day" => "day",
224 "wk" => "week",
225 _ => "unknown",
226 },
227 Value::int(split[0].parse().unwrap_or(0), span),
228 );
229 }
230
231 record.push(
232 "sign",
233 Value::string(if sign == -1 { "-" } else { "+" }, span),
234 );
235
236 Value::record(record, span)
237}
238
239#[cfg(test)]
240mod test {
241 use super::*;
242
243 #[test]
244 fn test_examples() -> nu_test_support::Result {
245 nu_test_support::test().examples(IntoRecord)
246 }
247}