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::{operate, CmdArgument};
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:
201 "Convert non-standard timestamp string, with timezone offset, to datetime using a custom format",
202 example: "'20210227_135540+0000' | into datetime --format '%Y%m%d_%H%M%S%z'",
203 #[allow(clippy::inconsistent_digit_grouping)]
204 result: example_result_1(1614434140_000000000),
205 },
206 Example {
207 description: "Convert non-standard timestamp string, without timezone offset, to datetime with custom formatting",
208 example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
209 #[allow(clippy::inconsistent_digit_grouping)]
210 result: Some(Value::date(
211 Local
212 .from_local_datetime(
213 &NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
214 .expect("date calculation should not fail in test"),
215 )
216 .unwrap()
217 .with_timezone(Local::now().offset()),
218 Span::test_data(),
219 )),
220 },
221 Example {
222 description:
223 "Convert nanosecond-precision unix timestamp to a datetime with offset from UTC",
224 example: "1614434140123456789 | into datetime --offset -5",
225 #[allow(clippy::inconsistent_digit_grouping)]
226 result: example_result_1(1614434140_123456789),
227 },
228 Example {
229 description: "Convert standard (seconds) unix timestamp to a UTC datetime",
230 example: "1614434140 | into datetime -f '%s'",
231 #[allow(clippy::inconsistent_digit_grouping)]
232 result: example_result_1(1614434140_000000000),
233 },
234 Example {
235 description: "Using a datetime as input simply returns the value",
236 example: "2021-02-27T13:55:40 | into datetime",
237 #[allow(clippy::inconsistent_digit_grouping)]
238 result: example_result_1(1614434140_000000000),
239 },
240 Example {
241 description: "Using a record as input",
242 example: "{year: 2025, month: 3, day: 30, hour: 12, minute: 15, second: 59, timezone: '+02:00'} | into datetime",
243 #[allow(clippy::inconsistent_digit_grouping)]
244 result: example_result_1(1743329759_000000000),
245 },
246 Example {
247 description: "Convert list of timestamps to datetimes",
248 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"#,
249 result: Some(Value::list(
250 vec![
251 Value::date(
252 DateTime::parse_from_str(
253 "2023-03-30 10:10:07 -05:00",
254 "%Y-%m-%d %H:%M:%S %z",
255 )
256 .expect("date calculation should not fail in test"),
257 Span::test_data(),
258 ),
259 Value::date(
260 DateTime::parse_from_str(
261 "2023-05-05 13:43:49 -05:00",
262 "%Y-%m-%d %H:%M:%S %z",
263 )
264 .expect("date calculation should not fail in test"),
265 Span::test_data(),
266 ),
267 Value::date(
268 DateTime::parse_from_str(
269 "2023-06-05 01:37:42 -05:00",
270 "%Y-%m-%d %H:%M:%S %z",
271 )
272 .expect("date calculation should not fail in test"),
273 Span::test_data(),
274 ),
275 ],
276 Span::test_data(),
277 )),
278 },
279 ]
280 }
281}
282
283#[derive(Clone, Debug)]
284struct DatetimeFormat(String);
285
286fn action(input: &Value, args: &Arguments, head: Span) -> Value {
287 let timezone = &args.zone_options;
288 let dateformat = &args.format_options;
289
290 if matches!(input, Value::Date { .. }) {
292 return input.clone();
293 }
294
295 if let Value::Record { val: record, .. } = input {
296 if let Some(tz) = timezone {
297 return Value::error(
298 ShellError::IncompatibleParameters {
299 left_message: "got a record as input".into(),
300 left_span: head,
301 right_message: "the timezone should be included in the record".into(),
302 right_span: tz.span,
303 },
304 head,
305 );
306 }
307
308 if let Some(dt) = dateformat {
309 return Value::error(
310 ShellError::IncompatibleParameters {
311 left_message: "got a record as input".into(),
312 left_span: head,
313 right_message: "cannot be used with records".into(),
314 right_span: dt.span,
315 },
316 head,
317 );
318 }
319
320 let span = input.span();
321 return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span));
322 }
323
324 if matches!(input, Value::String { .. }) && dateformat.is_none() {
326 let span = input.span();
327 if let Ok(input_val) = input.coerce_str() {
328 if let Ok(date) = parse_date_from_string(&input_val, span) {
329 return Value::date(date, span);
330 }
331 }
332 }
333
334 let timestamp = match input {
336 Value::Int { val, .. } => Ok(*val),
337 Value::String { val, .. } => val.parse::<i64>(),
338 Value::Error { .. } => return input.clone(),
340 other => {
341 return Value::error(
342 ShellError::OnlySupportsThisInputType {
343 exp_input_type: "string and int".into(),
344 wrong_type: other.get_type().to_string(),
345 dst_span: head,
346 src_span: other.span(),
347 },
348 head,
349 );
350 }
351 };
352
353 if dateformat.is_none() {
354 if let Ok(ts) = timestamp {
355 return match timezone {
356 None => Value::date(Utc.timestamp_nanos(ts).into(), head),
360 Some(Spanned { item, span }) => match item {
361 Zone::Utc => {
362 let dt = Utc.timestamp_nanos(ts);
363 Value::date(dt.into(), *span)
364 }
365 Zone::Local => {
366 let dt = Local.timestamp_nanos(ts);
367 Value::date(dt.into(), *span)
368 }
369 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
370 Some(eastoffset) => {
371 let dt = eastoffset.timestamp_nanos(ts);
372 Value::date(dt, *span)
373 }
374 None => Value::error(
375 ShellError::DatetimeParseError {
376 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
377 span: *span,
378 },
379 *span,
380 ),
381 },
382 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
383 Some(westoffset) => {
384 let dt = westoffset.timestamp_nanos(ts);
385 Value::date(dt, *span)
386 }
387 None => Value::error(
388 ShellError::DatetimeParseError {
389 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
390 span: *span,
391 },
392 *span,
393 ),
394 },
395 Zone::Error => Value::error(
396 ShellError::TypeMismatch {
398 err_message: "Invalid timezone or offset".to_string(),
399 span: *span,
400 },
401 *span,
402 ),
403 },
404 };
405 };
406 }
407
408 let span = input.span();
410
411 let parse_as_string = |val: &str| {
412 match dateformat {
413 Some(dt_format) => match DateTime::parse_from_str(val, &dt_format.item.0) {
414 Ok(dt) => {
415 match timezone {
416 None => {
417 Value::date ( dt, head )
418 },
419 Some(Spanned { item, span }) => match item {
420 Zone::Utc => {
421 Value::date ( dt, head )
422 }
423 Zone::Local => {
424 Value::date(dt.with_timezone(&Local).into(), *span)
425 }
426 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
427 Some(eastoffset) => {
428 Value::date(dt.with_timezone(&eastoffset), *span)
429 }
430 None => Value::error(
431 ShellError::DatetimeParseError {
432 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
433 span: *span,
434 },
435 *span,
436 ),
437 },
438 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
439 Some(westoffset) => {
440 Value::date(dt.with_timezone(&westoffset), *span)
441 }
442 None => Value::error(
443 ShellError::DatetimeParseError {
444 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
445 span: *span,
446 },
447 *span,
448 ),
449 },
450 Zone::Error => Value::error(
451 ShellError::TypeMismatch {
453 err_message: "Invalid timezone or offset".to_string(),
454 span: *span,
455 },
456 *span,
457 ),
458 },
459 }
460 },
461 Err(reason) => {
462 parse_with_format(val, &dt_format.item.0, head).unwrap_or_else(|_| Value::error (
463 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()) },
464 head,
465 ))
466 }
467 },
468
469 None => match parse_date_from_string(val, span) {
473 Ok(date) => Value::date (
474 date,
475 span,
476 ),
477 Err(err) => err,
478 },
479 }
480 };
481
482 match input {
483 Value::String { val, .. } => parse_as_string(val),
484 Value::Int { val, .. } => parse_as_string(&val.to_string()),
485
486 Value::Error { .. } => input.clone(),
488 other => Value::error(
489 ShellError::OnlySupportsThisInputType {
490 exp_input_type: "string".into(),
491 wrong_type: other.get_type().to_string(),
492 dst_span: head,
493 src_span: other.span(),
494 },
495 head,
496 ),
497 }
498}
499
500fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
501 if let Some(invalid_col) = record
502 .columns()
503 .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
504 {
505 let allowed_cols = ALLOWED_COLUMNS.join(", ");
506 return Err(ShellError::UnsupportedInput {
507 msg: format!(
508 "Column '{invalid_col}' is not valid for a structured datetime. Allowed columns are: {allowed_cols}"
509 ),
510 input: "value originates from here".into(),
511 msg_span: head,
512 input_span: span
513 }
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::{action, DatetimeFormat, IntoDatetime, Zone};
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}