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![(Type::Date, Type::Date), (Type::String, Type::Date)])
22            .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
23            .param(
24                PositionalArg::new("time zone", SyntaxShape::String)
25                    .desc("Time zone description.")
26                    .completion(Completion::new_list(TIMEZONES.as_slice()))
27                    .required(),
28            )
29            .category(Category::Date)
30    }
31
32    fn description(&self) -> &str {
33        "Convert a date to a given time zone."
34    }
35
36    fn extra_description(&self) -> &str {
37        "Use 'date list-timezone' to list all supported time zones."
38    }
39
40    fn search_terms(&self) -> Vec<&str> {
41        vec![
42            "tz",
43            "transform",
44            "convert",
45            "UTC",
46            "GMT",
47            "list",
48            "list-timezone",
49        ]
50    }
51
52    fn run(
53        &self,
54        engine_state: &EngineState,
55        stack: &mut Stack,
56        call: &Call,
57        input: PipelineData,
58    ) -> Result<PipelineData, ShellError> {
59        let head = call.head;
60        let timezone: Spanned<String> = call.req(engine_state, stack, 0)?;
61
62        // This doesn't match explicit nulls
63        if let PipelineData::Empty = input {
64            return Err(ShellError::PipelineEmpty { dst_span: head });
65        }
66        input.map(
67            move |value| helper(value, head, &timezone),
68            engine_state.signals(),
69        )
70    }
71
72    fn examples(&self) -> Vec<Example<'_>> {
73        let example_result_1 = || match FixedOffset::east_opt(5 * 3600)
74            .expect("to timezone: help example is invalid")
75            .with_ymd_and_hms(2020, 10, 10, 13, 00, 00)
76        {
77            LocalResult::Single(dt) => Some(Value::date(dt, Span::test_data())),
78            _ => panic!("to timezone: help example is invalid"),
79        };
80
81        vec![
82            Example {
83                description: "Get the current date in UTC+05:00.",
84                example: "date now | date to-timezone '+0500'",
85                result: None,
86            },
87            Example {
88                description: "Get the current date in the local time zone.",
89                example: "date now | date to-timezone local",
90                result: None,
91            },
92            Example {
93                description: "Get the current date in Hawaii.",
94                example: "date now | date to-timezone US/Hawaii",
95                result: None,
96            },
97            Example {
98                description: "Get a date in a different time zone, from a string.",
99                example: r#""2020-10-10 10:00:00 +02:00" | date to-timezone "+0500""#,
100                result: example_result_1(),
101            },
102            Example {
103                description: "Get a date in a different time zone, from a datetime.",
104                example: r#""2020-10-10 10:00:00 +02:00" | into datetime | date to-timezone "+0500""#,
105                result: example_result_1(),
106            },
107        ]
108    }
109}
110
111fn helper(value: Value, head: Span, timezone: &Spanned<String>) -> Value {
112    let val_span = value.span();
113    match value {
114        Value::Date { val, .. } => _to_timezone(val, timezone, head),
115        Value::String { val, .. } => {
116            let time = parse_date_from_string(&val, val_span);
117            match time {
118                Ok(dt) => _to_timezone(dt, timezone, head),
119                Err(e) => e,
120            }
121        }
122
123        Value::Nothing { .. } => {
124            let dt = Local::now();
125            _to_timezone(dt.with_timezone(dt.offset()), timezone, head)
126        }
127        _ => Value::error(
128            ShellError::OnlySupportsThisInputType {
129                exp_input_type: "date, string (that represents datetime), or nothing".into(),
130                wrong_type: value.get_type().to_string(),
131                dst_span: head,
132                src_span: val_span,
133            },
134            head,
135        ),
136    }
137}
138
139fn _to_timezone(dt: DateTime<FixedOffset>, timezone: &Spanned<String>, span: Span) -> Value {
140    match datetime_in_timezone(&dt, timezone.item.as_str()) {
141        Ok(dt) => Value::date(dt, span),
142        Err(_) => Value::error(
143            ShellError::TypeMismatch {
144                err_message: String::from("invalid time zone"),
145                span: timezone.span,
146            },
147            timezone.span,
148        ),
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155
156    #[test]
157    fn test_examples() {
158        use crate::test_examples;
159
160        test_examples(DateToTimezone {})
161    }
162}