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