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