Skip to main content

nu_command/date/
to_timezone.rs

1use std::sync::LazyLock;
2
3use super::parser::datetime_in_timezone;
4use crate::date::utils::parse_date_from_string;
5use chrono::{DateTime, FixedOffset, Local, LocalResult, TimeZone};
6use nu_engine::command_prelude::*;
7
8static TIMEZONES: LazyLock<Vec<&'static str>> =
9    LazyLock::new(|| chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect());
10
11#[derive(Clone)]
12pub struct DateToTimezone;
13
14impl Command for DateToTimezone {
15    fn name(&self) -> &str {
16        "date to-timezone"
17    }
18
19    fn signature(&self) -> Signature {
20        Signature::build("date to-timezone")
21            .input_output_types(vec![
22                (Type::Date, Type::Date),
23                (Type::String, Type::Date),
24                (
25                    Type::List(Box::new(Type::Date)),
26                    Type::List(Box::new(Type::Date)),
27                ),
28                (
29                    Type::List(Box::new(Type::String)),
30                    Type::List(Box::new(Type::Date)),
31                ),
32            ])
33            .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
34            .param(
35                PositionalArg::new("time zone", SyntaxShape::String)
36                    .desc("Time zone description.")
37                    .completion(Completion::new_list(TIMEZONES.as_slice()))
38                    .required(),
39            )
40            .category(Category::Date)
41    }
42
43    fn description(&self) -> &str {
44        "Convert a date to a given time zone."
45    }
46
47    fn extra_description(&self) -> &str {
48        "Use 'date list-timezone' to list all supported time zones."
49    }
50
51    fn search_terms(&self) -> Vec<&str> {
52        vec![
53            "tz",
54            "transform",
55            "convert",
56            "UTC",
57            "GMT",
58            "list",
59            "list-timezone",
60        ]
61    }
62
63    fn run(
64        &self,
65        engine_state: &EngineState,
66        stack: &mut Stack,
67        call: &Call,
68        input: PipelineData,
69    ) -> Result<PipelineData, ShellError> {
70        let head = call.head;
71        let timezone: Spanned<String> = call.req(engine_state, stack, 0)?;
72
73        // This doesn't match explicit nulls
74        if let PipelineData::Empty = input {
75            return Err(ShellError::PipelineEmpty { dst_span: head });
76        }
77        input.map(
78            move |value| helper(value, head, &timezone),
79            engine_state.signals(),
80        )
81    }
82
83    fn examples(&self) -> Vec<Example<'_>> {
84        let example_result_1 = || match FixedOffset::east_opt(5 * 3600)
85            .expect("to timezone: help example is invalid")
86            .with_ymd_and_hms(2020, 10, 10, 13, 00, 00)
87        {
88            LocalResult::Single(dt) => Some(Value::date(dt, Span::test_data())),
89            _ => panic!("to timezone: help example is invalid"),
90        };
91
92        let example_result_list_datetime = || {
93            let tz = FixedOffset::east_opt(5 * 3600).expect("to timezone: help example is invalid");
94            Some(Value::list(
95                vec![
96                    Value::date(
97                        tz.with_ymd_and_hms(2021, 10, 22, 13, 00, 00)
98                            .single()
99                            .expect("to timezone: help example is invalid"),
100                        Span::test_data(),
101                    ),
102                    Value::date(
103                        tz.with_ymd_and_hms(2021, 10, 23, 13, 00, 00)
104                            .single()
105                            .expect("to timezone: help example is invalid"),
106                        Span::test_data(),
107                    ),
108                ],
109                Span::test_data(),
110            ))
111        };
112
113        let example_result_list_string = || {
114            let tz = FixedOffset::east_opt(5 * 3600).expect("to timezone: help example is invalid");
115            Some(Value::list(
116                vec![
117                    Value::date(
118                        tz.with_ymd_and_hms(2020, 10, 10, 13, 00, 00)
119                            .single()
120                            .expect("to timezone: help example is invalid"),
121                        Span::test_data(),
122                    ),
123                    Value::date(
124                        tz.with_ymd_and_hms(2020, 10, 10, 15, 00, 00)
125                            .single()
126                            .expect("to timezone: help example is invalid"),
127                        Span::test_data(),
128                    ),
129                ],
130                Span::test_data(),
131            ))
132        };
133
134        vec![
135            Example {
136                description: "Get the current date in UTC+05:00.",
137                example: "date now | date to-timezone '+0500'",
138                result: None,
139            },
140            Example {
141                description: "Get the current date in the local time zone.",
142                example: "date now | date to-timezone local",
143                result: None,
144            },
145            Example {
146                description: "Get the current date in Hawaii.",
147                example: "date now | date to-timezone US/Hawaii",
148                result: None,
149            },
150            Example {
151                description: "Get a date in a different time zone, from a string.",
152                example: r#""2020-10-10 10:00:00 +02:00" | date to-timezone "+0500""#,
153                result: example_result_1(),
154            },
155            Example {
156                description: "Get a date in a different time zone, from a datetime.",
157                example: r#""2020-10-10 10:00:00 +02:00" | into datetime | date to-timezone "+0500""#,
158                result: example_result_1(),
159            },
160            Example {
161                description: "Convert a list of datetimes to a given time zone.",
162                example: "[2021-10-22T10:00:00+02:00, 2021-10-23T10:00:00+02:00] | date to-timezone \"+0500\"",
163                result: example_result_list_datetime(),
164            },
165            Example {
166                description: "Convert a list of date strings to a given time zone.",
167                example: r#"["2020-10-10 10:00:00 +02:00", "2020-10-10 12:00:00 +02:00"] | date to-timezone "+0500""#,
168                result: example_result_list_string(),
169            },
170        ]
171    }
172}
173
174fn helper(value: Value, head: Span, timezone: &Spanned<String>) -> Value {
175    let val_span = value.span();
176    match value {
177        Value::Date { val, .. } => _to_timezone(val, timezone, head),
178        Value::String { val, .. } => {
179            let time = parse_date_from_string(&val, val_span);
180            match time {
181                Ok(dt) => _to_timezone(dt, timezone, head),
182                Err(e) => e,
183            }
184        }
185
186        Value::Nothing { .. } => {
187            let dt = Local::now();
188            _to_timezone(dt.with_timezone(dt.offset()), timezone, head)
189        }
190        _ => Value::error(
191            ShellError::OnlySupportsThisInputType {
192                exp_input_type: "date, string (that represents datetime), or nothing".into(),
193                wrong_type: value.get_type().to_string(),
194                dst_span: head,
195                src_span: val_span,
196            },
197            head,
198        ),
199    }
200}
201
202fn _to_timezone(dt: DateTime<FixedOffset>, timezone: &Spanned<String>, span: Span) -> Value {
203    match datetime_in_timezone(&dt, timezone.item.as_str()) {
204        Ok(dt) => Value::date(dt, span),
205        Err(_) => Value::error(
206            ShellError::TypeMismatch {
207                err_message: String::from("invalid time zone"),
208                span: timezone.span,
209            },
210            timezone.span,
211        ),
212    }
213}
214
215#[cfg(test)]
216mod test {
217    use super::*;
218
219    #[test]
220    fn test_examples() -> nu_test_support::Result {
221        nu_test_support::test().examples(DateToTimezone)
222    }
223}