1use crate::{generate_strftime_list, parse_date_from_string};
2use chrono::{
3 DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone,
4 Timelike, Utc,
5};
6use nu_cmd_base::input_handler::{CmdArgument, operate};
7use nu_engine::command_prelude::*;
8
9const HOUR: i32 = 60 * 60;
10const ALLOWED_COLUMNS: [&str; 10] = [
11 "year",
12 "month",
13 "day",
14 "hour",
15 "minute",
16 "second",
17 "millisecond",
18 "microsecond",
19 "nanosecond",
20 "timezone",
21];
22
23#[derive(Clone, Debug)]
24struct Arguments {
25 zone_options: Option<Spanned<Zone>>,
26 format_options: Option<Spanned<DatetimeFormat>>,
27 cell_paths: Option<Vec<CellPath>>,
28}
29
30impl CmdArgument for Arguments {
31 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
32 self.cell_paths.take()
33 }
34}
35
36#[derive(Clone, Debug)]
38enum Zone {
39 Utc,
40 Local,
41 East(u8),
42 West(u8),
43 Error, }
45
46impl Zone {
47 const OPTIONS: &[&str] = &["utc", "local"];
48 fn new(i: i64) -> Self {
49 if i.abs() <= 12 {
50 if i >= 0 {
52 Self::East(i as u8) } else {
54 Self::West(-i as u8) }
56 } else {
57 Self::Error }
59 }
60 fn from_string(s: &str) -> Self {
61 match s.to_ascii_lowercase().as_str() {
62 "utc" | "u" => Self::Utc,
63 "local" | "l" => Self::Local,
64 _ => Self::Error,
65 }
66 }
67}
68
69#[derive(Clone)]
70pub struct IntoDatetime;
71
72impl Command for IntoDatetime {
73 fn name(&self) -> &str {
74 "into datetime"
75 }
76
77 fn signature(&self) -> Signature {
78 Signature::build("into datetime")
79 .input_output_types(vec![
80 (Type::Date, Type::Date),
81 (Type::Int, Type::Date),
82 (Type::String, Type::Date),
83 (
84 Type::List(Box::new(Type::String)),
85 Type::List(Box::new(Type::Date)),
86 ),
87 (Type::table(), Type::table()),
88 (Type::Nothing, Type::table()),
89 (Type::record(), Type::record()),
90 (Type::record(), Type::Date),
91 (Type::Any, Type::table()),
95 ])
96 .allow_variants_without_examples(true)
97 .param(
98 Flag::new("timezone")
99 .short('z')
100 .arg(SyntaxShape::String)
101 .desc(
102 "Specify timezone if the input is a Unix timestamp. Valid options: 'UTC' ('u') or 'LOCAL' ('l').",
103 )
104 .completion(Completion::new_list(Zone::OPTIONS)),
105 )
106 .named(
107 "offset",
108 SyntaxShape::Int,
109 "Specify timezone by offset from UTC if the input is a Unix timestamp, like '+8', '-4'.",
110 Some('o'),
111 )
112 .named(
113 "format",
114 SyntaxShape::String,
115 "Specify expected format of INPUT string to parse to datetime. Use --list to see options.",
116 Some('f'),
117 )
118 .switch(
119 "list",
120 "Show all possible variables for use in --format flag.",
121 Some('l'),
122 )
123 .rest(
124 "rest",
125 SyntaxShape::CellPath,
126 "For a data structure input, convert data at the given cell paths.",
127 )
128 .category(Category::Conversions)
129 }
130
131 fn run(
132 &self,
133 engine_state: &EngineState,
134 stack: &mut Stack,
135 call: &Call,
136 input: PipelineData,
137 ) -> Result<PipelineData, ShellError> {
138 if call.has_flag(engine_state, stack, "list")? {
139 Ok(generate_strftime_list(call.head, true).into_pipeline_data())
140 } else {
141 let cell_paths = call.rest(engine_state, stack, 0)?;
142 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
143
144 let timezone = call.get_flag::<Spanned<String>>(engine_state, stack, "timezone")?;
146 let zone_options =
147 match &call.get_flag::<Spanned<i64>>(engine_state, stack, "offset")? {
148 Some(zone_offset) => Some(Spanned {
149 item: Zone::new(zone_offset.item),
150 span: zone_offset.span,
151 }),
152 None => timezone.as_ref().map(|zone| Spanned {
153 item: Zone::from_string(&zone.item),
154 span: zone.span,
155 }),
156 };
157
158 let format_options = call
159 .get_flag::<Spanned<String>>(engine_state, stack, "format")?
160 .as_ref()
161 .map(|fmt| Spanned {
162 item: DatetimeFormat(fmt.item.to_string()),
163 span: fmt.span,
164 });
165
166 let args = Arguments {
167 zone_options,
168 format_options,
169 cell_paths,
170 };
171 operate(action, args, input, call.head, engine_state.signals())
172 }
173 }
174
175 fn description(&self) -> &str {
176 "Convert text or timestamp into a datetime."
177 }
178
179 fn search_terms(&self) -> Vec<&str> {
180 vec!["convert", "timezone", "UTC"]
181 }
182
183 fn examples(&self) -> Vec<Example<'_>> {
184 let example_result_1 = |nanos: i64| {
185 Some(Value::date(
186 Utc.timestamp_nanos(nanos).into(),
187 Span::test_data(),
188 ))
189 };
190 vec![
191 Example {
192 description: "Convert timestamp string to datetime with timezone offset.",
193 example: "'27.02.2021 1:55 pm +0000' | into datetime",
194 #[allow(clippy::inconsistent_digit_grouping)]
195 result: example_result_1(1614434100_000000000),
196 },
197 Example {
198 description: "Convert standard timestamp string to datetime with timezone offset.",
199 example: "'2021-02-27T13:55:40.2246+00:00' | into datetime",
200 #[allow(clippy::inconsistent_digit_grouping)]
201 result: example_result_1(1614434140_224600000),
202 },
203 Example {
204 description: "Convert non-standard timestamp string, with timezone offset, to \
205 datetime using a custom format.",
206 example: "'20210227_135540+0000' | into datetime --format '%Y%m%d_%H%M%S%z'",
207 #[allow(clippy::inconsistent_digit_grouping)]
208 result: example_result_1(1614434140_000000000),
209 },
210 Example {
211 description: "Convert non-standard timestamp string, without timezone offset, to \
212 datetime with custom formatting.",
213 example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
214 #[allow(clippy::inconsistent_digit_grouping)]
215 result: Some(Value::date(
216 Local
217 .from_local_datetime(
218 &NaiveDateTime::parse_from_str(
219 "16.11.1984 8:00 am",
220 "%d.%m.%Y %H:%M %P",
221 )
222 .expect("date calculation should not fail in test"),
223 )
224 .unwrap()
225 .with_timezone(Local::now().offset()),
226 Span::test_data(),
227 )),
228 },
229 Example {
230 description: "Convert nanosecond-precision unix timestamp to a datetime with \
231 offset from UTC.",
232 example: "1614434140123456789 | into datetime --offset -5",
233 #[allow(clippy::inconsistent_digit_grouping)]
234 result: example_result_1(1614434140_123456789),
235 },
236 Example {
237 description: "Convert standard (seconds) unix timestamp to a UTC datetime.",
238 example: "1614434140 | into datetime -f '%s'",
239 #[allow(clippy::inconsistent_digit_grouping)]
240 result: example_result_1(1614434140_000000000),
241 },
242 Example {
243 description: "Using a datetime as input simply returns the value.",
244 example: "2021-02-27T13:55:40 | into datetime",
245 #[allow(clippy::inconsistent_digit_grouping)]
246 result: example_result_1(1614434140_000000000),
247 },
248 Example {
249 description: "Using a record as input.",
250 example: "{year: 2025, month: 3, day: 30, hour: 12, minute: 15, second: 59, \
251 timezone: '+02:00'} | into datetime",
252 #[allow(clippy::inconsistent_digit_grouping)]
253 result: example_result_1(1743329759_000000000),
254 },
255 Example {
256 description: "Convert list of timestamps to datetimes.",
257 example: r#"["2023-03-30 10:10:07 -05:00", "2023-05-05 13:43:49 -05:00", "2023-06-05 01:37:42 -05:00"] | into datetime"#,
258 result: Some(Value::list(
259 vec![
260 Value::date(
261 DateTime::parse_from_str(
262 "2023-03-30 10:10:07 -05:00",
263 "%Y-%m-%d %H:%M:%S %z",
264 )
265 .expect("date calculation should not fail in test"),
266 Span::test_data(),
267 ),
268 Value::date(
269 DateTime::parse_from_str(
270 "2023-05-05 13:43:49 -05:00",
271 "%Y-%m-%d %H:%M:%S %z",
272 )
273 .expect("date calculation should not fail in test"),
274 Span::test_data(),
275 ),
276 Value::date(
277 DateTime::parse_from_str(
278 "2023-06-05 01:37:42 -05:00",
279 "%Y-%m-%d %H:%M:%S %z",
280 )
281 .expect("date calculation should not fail in test"),
282 Span::test_data(),
283 ),
284 ],
285 Span::test_data(),
286 )),
287 },
288 ]
289 }
290}
291
292#[derive(Clone, Debug)]
293struct DatetimeFormat(String);
294
295fn action(input: &Value, args: &Arguments, head: Span) -> Value {
296 let timezone = &args.zone_options;
297 let dateformat = &args.format_options;
298
299 if let Value::Date { .. } = input {
301 return input.clone();
302 }
303
304 if let Value::Record { val: record, .. } = input {
305 if let Some(tz) = timezone {
306 return Value::error(
307 ShellError::IncompatibleParameters {
308 left_message: "got a record as input".into(),
309 left_span: head,
310 right_message: "the timezone should be included in the record".into(),
311 right_span: tz.span,
312 },
313 head,
314 );
315 }
316
317 if let Some(dt) = dateformat {
318 return Value::error(
319 ShellError::IncompatibleParameters {
320 left_message: "got a record as input".into(),
321 left_span: head,
322 right_message: "cannot be used with records".into(),
323 right_span: dt.span,
324 },
325 head,
326 );
327 }
328
329 let span = input.span();
330 return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span));
331 }
332
333 if matches!(input, Value::String { .. }) && dateformat.is_none() {
335 let span = input.span();
336 if let Ok(input_val) = input.coerce_str()
337 && let Ok(date) = parse_date_from_string(&input_val, span)
338 {
339 return Value::date(date, span);
340 }
341 }
342
343 let timestamp = match input {
345 Value::Int { val, .. } => Ok(*val),
346 Value::String { val, .. } => val.parse::<i64>(),
347 Value::Error { .. } => return input.clone(),
349 other => {
350 return Value::error(
351 ShellError::OnlySupportsThisInputType {
352 exp_input_type: "string and int".into(),
353 wrong_type: other.get_type().to_string(),
354 dst_span: head,
355 src_span: other.span(),
356 },
357 head,
358 );
359 }
360 };
361
362 if dateformat.is_none()
363 && let Ok(ts) = timestamp
364 {
365 return match timezone {
366 None => Value::date(Utc.timestamp_nanos(ts).into(), head),
370 Some(Spanned { item, span }) => match item {
371 Zone::Utc => {
372 let dt = Utc.timestamp_nanos(ts);
373 Value::date(dt.into(), *span)
374 }
375 Zone::Local => {
376 let dt = Local.timestamp_nanos(ts);
377 Value::date(dt.into(), *span)
378 }
379 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
380 Some(eastoffset) => {
381 let dt = eastoffset.timestamp_nanos(ts);
382 Value::date(dt, *span)
383 }
384 None => Value::error(
385 ShellError::DatetimeParseError {
386 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
387 span: *span,
388 },
389 *span,
390 ),
391 },
392 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
393 Some(westoffset) => {
394 let dt = westoffset.timestamp_nanos(ts);
395 Value::date(dt, *span)
396 }
397 None => Value::error(
398 ShellError::DatetimeParseError {
399 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
400 span: *span,
401 },
402 *span,
403 ),
404 },
405 Zone::Error => Value::error(
406 ShellError::TypeMismatch {
408 err_message: "Invalid timezone or offset".to_string(),
409 span: *span,
410 },
411 *span,
412 ),
413 },
414 };
415 };
416
417 let span = input.span();
419
420 let parse_as_string = |val: &str| {
421 match dateformat {
422 Some(dt_format) => {
423 let format_str = dt_format
425 .item
426 .0
427 .replace("%J", "%Y%m%d") .replace("%Q", "%H%M%S"); match DateTime::parse_from_str(val, &format_str) {
430 Ok(dt) => match timezone {
431 None => Value::date(dt, head),
432 Some(Spanned { item, span }) => match item {
433 Zone::Utc => Value::date(dt, head),
434 Zone::Local => Value::date(dt.with_timezone(&Local).into(), *span),
435 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
436 Some(eastoffset) => {
437 Value::date(dt.with_timezone(&eastoffset), *span)
438 }
439 None => Value::error(
440 ShellError::DatetimeParseError {
441 msg: input
442 .to_abbreviated_string(&nu_protocol::Config::default()),
443 span: *span,
444 },
445 *span,
446 ),
447 },
448 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
449 Some(westoffset) => {
450 Value::date(dt.with_timezone(&westoffset), *span)
451 }
452 None => Value::error(
453 ShellError::DatetimeParseError {
454 msg: input
455 .to_abbreviated_string(&nu_protocol::Config::default()),
456 span: *span,
457 },
458 *span,
459 ),
460 },
461 Zone::Error => Value::error(
462 ShellError::TypeMismatch {
464 err_message: "Invalid timezone or offset".to_string(),
465 span: *span,
466 },
467 *span,
468 ),
469 },
470 },
471 Err(reason) => parse_with_format(val, &format_str, head).unwrap_or_else(|_| {
472 Value::error(
473 ShellError::CantConvert {
474 to_type: format!(
475 "could not parse as datetime using format '{}'",
476 dt_format.item.0
477 ),
478 from_type: reason.to_string(),
479 span: head,
480 help: Some(
481 "you can use `into datetime` without a format string to \
482 enable flexible parsing"
483 .to_string(),
484 ),
485 },
486 head,
487 )
488 }),
489 }
490 }
491
492 None => match parse_date_from_string(val, span) {
496 Ok(date) => Value::date(date, span),
497 Err(err) => err,
498 },
499 }
500 };
501
502 match input {
503 Value::String { val, .. } => parse_as_string(val),
504 Value::Int { val, .. } => parse_as_string(&val.to_string()),
505
506 Value::Error { .. } => input.clone(),
508 other => Value::error(
509 ShellError::OnlySupportsThisInputType {
510 exp_input_type: "string".into(),
511 wrong_type: other.get_type().to_string(),
512 dst_span: head,
513 src_span: other.span(),
514 },
515 head,
516 ),
517 }
518}
519
520fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
521 if let Some(invalid_col) = record
522 .columns()
523 .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
524 {
525 let allowed_cols = ALLOWED_COLUMNS.join(", ");
526 return Err(ShellError::UnsupportedInput {
527 msg: format!(
528 "Column '{invalid_col}' is not valid for a structured datetime. Allowed columns \
529 are: {allowed_cols}"
530 ),
531 input: "value originates from here".into(),
532 msg_span: head,
533 input_span: span,
534 });
535 };
536
537 #[derive(Debug)]
540 enum RecordColumnDefault {
541 Now,
542 Zero,
543 }
544 let mut record_column_default = RecordColumnDefault::Now;
545
546 let now = Local::now();
547 let mut now_nanosecond = now.nanosecond();
548 let now_millisecond = now_nanosecond / 1_000_000;
549 now_nanosecond %= 1_000_000;
550 let now_microsecond = now_nanosecond / 1_000;
551 now_nanosecond %= 1_000;
552
553 let year: i32 = match record.get("year") {
554 Some(val) => {
555 record_column_default = RecordColumnDefault::Zero;
556 match val {
557 Value::Int { val, .. } => *val as i32,
558 other => {
559 return Err(ShellError::OnlySupportsThisInputType {
560 exp_input_type: "int".to_string(),
561 wrong_type: other.get_type().to_string(),
562 dst_span: head,
563 src_span: other.span(),
564 });
565 }
566 }
567 }
568 None => now.year(),
569 };
570 let month = match record.get("month") {
571 Some(col_val) => {
572 record_column_default = RecordColumnDefault::Zero;
573 parse_value_from_record_as_u32("month", col_val, &head, &span)?
574 }
575 None => match record_column_default {
576 RecordColumnDefault::Now => now.month(),
577 RecordColumnDefault::Zero => 1,
578 },
579 };
580 let day = match record.get("day") {
581 Some(col_val) => {
582 record_column_default = RecordColumnDefault::Zero;
583 parse_value_from_record_as_u32("day", col_val, &head, &span)?
584 }
585 None => match record_column_default {
586 RecordColumnDefault::Now => now.day(),
587 RecordColumnDefault::Zero => 1,
588 },
589 };
590 let hour = match record.get("hour") {
591 Some(col_val) => {
592 record_column_default = RecordColumnDefault::Zero;
593 parse_value_from_record_as_u32("hour", col_val, &head, &span)?
594 }
595 None => match record_column_default {
596 RecordColumnDefault::Now => now.hour(),
597 RecordColumnDefault::Zero => 0,
598 },
599 };
600 let minute = match record.get("minute") {
601 Some(col_val) => {
602 record_column_default = RecordColumnDefault::Zero;
603 parse_value_from_record_as_u32("minute", col_val, &head, &span)?
604 }
605 None => match record_column_default {
606 RecordColumnDefault::Now => now.minute(),
607 RecordColumnDefault::Zero => 0,
608 },
609 };
610 let second = match record.get("second") {
611 Some(col_val) => {
612 record_column_default = RecordColumnDefault::Zero;
613 parse_value_from_record_as_u32("second", col_val, &head, &span)?
614 }
615 None => match record_column_default {
616 RecordColumnDefault::Now => now.second(),
617 RecordColumnDefault::Zero => 0,
618 },
619 };
620 let millisecond = match record.get("millisecond") {
621 Some(col_val) => {
622 record_column_default = RecordColumnDefault::Zero;
623 parse_value_from_record_as_u32("millisecond", col_val, &head, &span)?
624 }
625 None => match record_column_default {
626 RecordColumnDefault::Now => now_millisecond,
627 RecordColumnDefault::Zero => 0,
628 },
629 };
630 let microsecond = match record.get("microsecond") {
631 Some(col_val) => {
632 record_column_default = RecordColumnDefault::Zero;
633 parse_value_from_record_as_u32("microsecond", col_val, &head, &span)?
634 }
635 None => match record_column_default {
636 RecordColumnDefault::Now => now_microsecond,
637 RecordColumnDefault::Zero => 0,
638 },
639 };
640
641 let nanosecond = match record.get("nanosecond") {
642 Some(col_val) => parse_value_from_record_as_u32("nanosecond", col_val, &head, &span)?,
643 None => match record_column_default {
644 RecordColumnDefault::Now => now_nanosecond,
645 RecordColumnDefault::Zero => 0,
646 },
647 };
648
649 let offset: FixedOffset = match record.get("timezone") {
650 Some(timezone) => parse_timezone_from_record(timezone, &head, &timezone.span())?,
651 None => now.offset().to_owned(),
652 };
653
654 let total_nanoseconds = nanosecond + microsecond * 1_000 + millisecond * 1_000_000;
655
656 let date = match NaiveDate::from_ymd_opt(year, month, day) {
657 Some(d) => d,
658 None => {
659 return Err(ShellError::IncorrectValue {
660 msg: "one of more values are incorrect and do not represent valid date".to_string(),
661 val_span: head,
662 call_span: span,
663 });
664 }
665 };
666 let time = match NaiveTime::from_hms_nano_opt(hour, minute, second, total_nanoseconds) {
667 Some(t) => t,
668 None => {
669 return Err(ShellError::IncorrectValue {
670 msg: "one of more values are incorrect and do not represent valid time".to_string(),
671 val_span: head,
672 call_span: span,
673 });
674 }
675 };
676 let date_time = NaiveDateTime::new(date, time);
677
678 let date_time_fixed = match offset.from_local_datetime(&date_time).single() {
679 Some(d) => d,
680 None => {
681 return Err(ShellError::IncorrectValue {
682 msg: "Ambiguous or invalid timezone conversion".to_string(),
683 val_span: head,
684 call_span: span,
685 });
686 }
687 };
688 Ok(Value::date(date_time_fixed, span))
689}
690
691fn parse_value_from_record_as_u32(
692 col: &str,
693 col_val: &Value,
694 head: &Span,
695 span: &Span,
696) -> Result<u32, ShellError> {
697 let value: u32 = match col_val {
698 Value::Int { val, .. } => {
699 if *val < 0 || *val > u32::MAX as i64 {
700 return Err(ShellError::IncorrectValue {
701 msg: format!("incorrect value for {col}"),
702 val_span: *head,
703 call_span: *span,
704 });
705 }
706 *val as u32
707 }
708 other => {
709 return Err(ShellError::OnlySupportsThisInputType {
710 exp_input_type: "int".to_string(),
711 wrong_type: other.get_type().to_string(),
712 dst_span: *head,
713 src_span: other.span(),
714 });
715 }
716 };
717 Ok(value)
718}
719
720fn parse_timezone_from_record(
721 timezone: &Value,
722 head: &Span,
723 span: &Span,
724) -> Result<FixedOffset, ShellError> {
725 match timezone {
726 Value::String { val, .. } => {
727 let offset: FixedOffset = match val.parse() {
728 Ok(offset) => offset,
729 Err(_) => {
730 return Err(ShellError::IncorrectValue {
731 msg: "invalid timezone".to_string(),
732 val_span: *span,
733 call_span: *head,
734 });
735 }
736 };
737 Ok(offset)
738 }
739 other => Err(ShellError::OnlySupportsThisInputType {
740 exp_input_type: "string".to_string(),
741 wrong_type: other.get_type().to_string(),
742 dst_span: *head,
743 src_span: other.span(),
744 }),
745 }
746}
747
748fn parse_with_format(val: &str, fmt: &str, head: Span) -> Result<Value, ()> {
749 if let Ok(dt) = NaiveDateTime::parse_from_str(val, fmt) {
751 let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
752 return Ok(Value::date(dt_native.into(), head));
753 }
754
755 if let Ok(date) = NaiveDate::parse_from_str(val, fmt)
757 && let Some(dt) = date.and_hms_opt(0, 0, 0)
758 {
759 let dt_native = Local.from_local_datetime(&dt).single().unwrap_or_default();
760 return Ok(Value::date(dt_native.into(), head));
761 }
762
763 if let Ok(time) = NaiveTime::parse_from_str(val, fmt) {
765 let now = Local::now().naive_local().date();
766 let dt_native = Local
767 .from_local_datetime(&now.and_time(time))
768 .single()
769 .unwrap_or_default();
770 return Ok(Value::date(dt_native.into(), head));
771 }
772
773 Err(())
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use super::{DatetimeFormat, IntoDatetime, Zone, action};
780 use nu_protocol::Type::Error;
781
782 #[test]
783 fn test_examples() {
784 use crate::test_examples;
785
786 test_examples(IntoDatetime {})
787 }
788
789 #[test]
790 fn takes_a_date_format_with_timezone() {
791 let date_str = Value::test_string("16.11.1984 8:00 am +0000");
792 let fmt_options = Some(Spanned {
793 item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
794 span: Span::test_data(),
795 });
796 let args = Arguments {
797 zone_options: None,
798 format_options: fmt_options,
799 cell_paths: None,
800 };
801 let actual = action(&date_str, &args, Span::test_data());
802 let expected = Value::date(
803 DateTime::parse_from_str("16.11.1984 8:00 am +0000", "%d.%m.%Y %H:%M %P %z").unwrap(),
804 Span::test_data(),
805 );
806 assert_eq!(actual, expected)
807 }
808
809 #[test]
810 fn takes_a_date_format_without_timezone() {
811 let date_str = Value::test_string("16.11.1984 8:00 am");
812 let fmt_options = Some(Spanned {
813 item: DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()),
814 span: Span::test_data(),
815 });
816 let args = Arguments {
817 zone_options: None,
818 format_options: fmt_options,
819 cell_paths: None,
820 };
821 let actual = action(&date_str, &args, Span::test_data());
822 let expected = Value::date(
823 Local
824 .from_local_datetime(
825 &NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
826 .unwrap(),
827 )
828 .unwrap()
829 .with_timezone(Local::now().offset()),
830 Span::test_data(),
831 );
832
833 assert_eq!(actual, expected)
834 }
835
836 #[test]
837 fn takes_iso8601_date_format() {
838 let date_str = Value::test_string("2020-08-04T16:39:18+00:00");
839 let args = Arguments {
840 zone_options: None,
841 format_options: None,
842 cell_paths: None,
843 };
844 let actual = action(&date_str, &args, Span::test_data());
845 let expected = Value::date(
846 DateTime::parse_from_str("2020-08-04T16:39:18+00:00", "%Y-%m-%dT%H:%M:%S%z").unwrap(),
847 Span::test_data(),
848 );
849 assert_eq!(actual, expected)
850 }
851
852 #[test]
853 fn takes_timestamp_offset() {
854 let date_str = Value::test_string("1614434140000000000");
855 let timezone_option = Some(Spanned {
856 item: Zone::East(8),
857 span: Span::test_data(),
858 });
859 let args = Arguments {
860 zone_options: timezone_option,
861 format_options: None,
862 cell_paths: None,
863 };
864 let actual = action(&date_str, &args, Span::test_data());
865 let expected = Value::date(
866 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
867 Span::test_data(),
868 );
869
870 assert_eq!(actual, expected)
871 }
872
873 #[test]
874 fn takes_timestamp_offset_as_int() {
875 let date_int = Value::test_int(1_614_434_140_000_000_000);
876 let timezone_option = Some(Spanned {
877 item: Zone::East(8),
878 span: Span::test_data(),
879 });
880 let args = Arguments {
881 zone_options: timezone_option,
882 format_options: None,
883 cell_paths: None,
884 };
885 let actual = action(&date_int, &args, Span::test_data());
886 let expected = Value::date(
887 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
888 Span::test_data(),
889 );
890
891 assert_eq!(actual, expected)
892 }
893
894 #[test]
895 fn takes_int_with_formatstring() {
896 let date_int = Value::test_int(1_614_434_140);
897 let fmt_options = Some(Spanned {
898 item: DatetimeFormat("%s".to_string()),
899 span: Span::test_data(),
900 });
901 let args = Arguments {
902 zone_options: None,
903 format_options: fmt_options,
904 cell_paths: None,
905 };
906 let actual = action(&date_int, &args, Span::test_data());
907 let expected = Value::date(
908 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
909 Span::test_data(),
910 );
911
912 assert_eq!(actual, expected)
913 }
914
915 #[test]
916 fn takes_timestamp_offset_as_int_with_formatting() {
917 let date_int = Value::test_int(1_614_434_140);
918 let timezone_option = Some(Spanned {
919 item: Zone::East(8),
920 span: Span::test_data(),
921 });
922 let fmt_options = Some(Spanned {
923 item: DatetimeFormat("%s".to_string()),
924 span: Span::test_data(),
925 });
926 let args = Arguments {
927 zone_options: timezone_option,
928 format_options: fmt_options,
929 cell_paths: None,
930 };
931 let actual = action(&date_int, &args, Span::test_data());
932 let expected = Value::date(
933 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
934 Span::test_data(),
935 );
936
937 assert_eq!(actual, expected)
938 }
939
940 #[test]
941 fn takes_timestamp_offset_as_int_with_local_timezone() {
942 let date_int = Value::test_int(1_614_434_140);
943 let timezone_option = Some(Spanned {
944 item: Zone::Local,
945 span: Span::test_data(),
946 });
947 let fmt_options = Some(Spanned {
948 item: DatetimeFormat("%s".to_string()),
949 span: Span::test_data(),
950 });
951 let args = Arguments {
952 zone_options: timezone_option,
953 format_options: fmt_options,
954 cell_paths: None,
955 };
956 let actual = action(&date_int, &args, Span::test_data());
957 let expected = Value::date(
958 Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
959 Span::test_data(),
960 );
961 assert_eq!(actual, expected)
962 }
963
964 #[test]
965 fn takes_timestamp() {
966 let date_str = Value::test_string("1614434140000000000");
967 let timezone_option = Some(Spanned {
968 item: Zone::Local,
969 span: Span::test_data(),
970 });
971 let args = Arguments {
972 zone_options: timezone_option,
973 format_options: None,
974 cell_paths: None,
975 };
976 let actual = action(&date_str, &args, Span::test_data());
977 let expected = Value::date(
978 Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
979 Span::test_data(),
980 );
981
982 assert_eq!(actual, expected)
983 }
984
985 #[test]
986 fn takes_datetime() {
987 let timezone_option = Some(Spanned {
988 item: Zone::Local,
989 span: Span::test_data(),
990 });
991 let args = Arguments {
992 zone_options: timezone_option,
993 format_options: None,
994 cell_paths: None,
995 };
996 let expected = Value::date(
997 Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
998 Span::test_data(),
999 );
1000 let actual = action(&expected, &args, Span::test_data());
1001
1002 assert_eq!(actual, expected)
1003 }
1004
1005 #[test]
1006 fn takes_timestamp_without_timezone() {
1007 let date_str = Value::test_string("1614434140000000000");
1008 let args = Arguments {
1009 zone_options: None,
1010 format_options: None,
1011 cell_paths: None,
1012 };
1013 let actual = action(&date_str, &args, Span::test_data());
1014
1015 let expected = Value::date(
1016 Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
1017 Span::test_data(),
1018 );
1019
1020 assert_eq!(actual, expected)
1021 }
1022
1023 #[test]
1024 fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
1025 let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
1026 let fmt_options = Some(Spanned {
1027 item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
1028 span: Span::test_data(),
1029 });
1030 let args = Arguments {
1031 zone_options: None,
1032 format_options: fmt_options,
1033 cell_paths: None,
1034 };
1035 let actual = action(&date_str, &args, Span::test_data());
1036
1037 assert_eq!(actual.get_type(), Error);
1038 }
1039}