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) .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 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}