nu_command/conversions/into/
record.rs

1use 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                        // Don't use .extend() unless that gets changed to check for duplicate keys
137                        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}