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 (
88 Type::List(Box::new(Type::Date)),
89 Type::List(Box::new(Type::Date)),
90 ),
91 (Type::table(), Type::table()),
92 (Type::Nothing, Type::table()),
93 (Type::record(), Type::record()),
94 (Type::record(), Type::Date),
95 (Type::Any, Type::table()),
99 ])
100 .allow_variants_without_examples(true)
101 .param(
102 Flag::new("timezone")
103 .short('z')
104 .arg(SyntaxShape::String)
105 .desc(
106 "Specify timezone to interpret timestamps and formatted datetime input. Valid options: 'UTC' ('u') or 'LOCAL' ('l').",
107 )
108 .completion(Completion::new_list(Zone::OPTIONS)),
109 )
110 .named(
111 "offset",
112 SyntaxShape::Int,
113 "Specify timezone by offset from UTC to interpret timestamps and formatted datetime input, like '+8', '-4'.",
114 Some('o'),
115 )
116 .named(
117 "format",
118 SyntaxShape::String,
119 "Specify expected format of INPUT string to parse to datetime. Use --list to see options.",
120 Some('f'),
121 )
122 .switch(
123 "list",
124 "Show all possible variables for use in --format flag.",
125 Some('l'),
126 )
127 .rest(
128 "rest",
129 SyntaxShape::CellPath,
130 "For a data structure input, convert data at the given cell paths.",
131 )
132 .category(Category::Conversions)
133 }
134
135 fn run(
136 &self,
137 engine_state: &EngineState,
138 stack: &mut Stack,
139 call: &Call,
140 input: PipelineData,
141 ) -> Result<PipelineData, ShellError> {
142 if call.has_flag(engine_state, stack, "list")? {
143 Ok(generate_strftime_list(call.head, true).into_pipeline_data())
144 } else {
145 let cell_paths = call.rest(engine_state, stack, 0)?;
146 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
147
148 let zone_options = {
149 let offset = call.get_flag::<Spanned<i64>>(engine_state, stack, "offset")?;
151 let timezone = call.get_flag::<Spanned<String>>(engine_state, stack, "timezone")?;
152
153 offset
154 .map(|offset| offset.map(Zone::new))
155 .or_else(|| timezone.map(|tz| tz.as_deref().map(Zone::from_string)))
156 };
157
158 let format_options = call
159 .get_flag::<Spanned<String>>(engine_state, stack, "format")?
160 .map(|fmt| fmt.map(DatetimeFormat));
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 \
201 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 \
208 datetime with custom formatting.",
209 example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
210 #[allow(clippy::inconsistent_digit_grouping)]
211 result: Some(Value::date(
212 Local
213 .from_local_datetime(
214 &NaiveDateTime::parse_from_str(
215 "16.11.1984 8:00 am",
216 "%d.%m.%Y %H:%M %P",
217 )
218 .expect("date calculation should not fail in test"),
219 )
220 .unwrap()
221 .with_timezone(Local::now().offset()),
222 Span::test_data(),
223 )),
224 },
225 Example {
226 description: "Convert nanosecond-precision unix timestamp to a datetime with \
227 offset from UTC.",
228 example: "1614434140123456789 | into datetime --offset -5",
229 #[allow(clippy::inconsistent_digit_grouping)]
230 result: example_result_1(1614434140_123456789),
231 },
232 Example {
233 description: "Convert standard (seconds) unix timestamp to a UTC datetime.",
234 example: "1614434140 | into datetime -f '%s'",
235 #[allow(clippy::inconsistent_digit_grouping)]
236 result: example_result_1(1614434140_000000000),
237 },
238 Example {
239 description: "Using a datetime as input simply returns the value.",
240 example: "2021-02-27T13:55:40 | into datetime",
241 #[allow(clippy::inconsistent_digit_grouping)]
242 result: example_result_1(1614434140_000000000),
243 },
244 Example {
245 description: "Using a record as input.",
246 example: "{year: 2025, month: 3, day: 30, hour: 12, minute: 15, second: 59, \
247 timezone: '+02:00'} | into datetime",
248 #[allow(clippy::inconsistent_digit_grouping)]
249 result: example_result_1(1743329759_000000000),
250 },
251 Example {
252 description: "Convert list of timestamps to datetimes.",
253 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"#,
254 result: Some(Value::list(
255 vec![
256 Value::date(
257 DateTime::parse_from_str(
258 "2023-03-30 10:10:07 -05:00",
259 "%Y-%m-%d %H:%M:%S %z",
260 )
261 .expect("date calculation should not fail in test"),
262 Span::test_data(),
263 ),
264 Value::date(
265 DateTime::parse_from_str(
266 "2023-05-05 13:43:49 -05:00",
267 "%Y-%m-%d %H:%M:%S %z",
268 )
269 .expect("date calculation should not fail in test"),
270 Span::test_data(),
271 ),
272 Value::date(
273 DateTime::parse_from_str(
274 "2023-06-05 01:37:42 -05:00",
275 "%Y-%m-%d %H:%M:%S %z",
276 )
277 .expect("date calculation should not fail in test"),
278 Span::test_data(),
279 ),
280 ],
281 Span::test_data(),
282 )),
283 },
284 Example {
285 description: "Passing a list of datetimes through is a no-op.",
286 example: "[2023-03-30T10:10:07-05:00, 2023-05-05T13:43:49-05:00] | into datetime",
287 result: Some(Value::list(
288 vec![
289 Value::date(
290 DateTime::parse_from_str(
291 "2023-03-30 10:10:07 -05:00",
292 "%Y-%m-%d %H:%M:%S %z",
293 )
294 .expect("date calculation should not fail in test"),
295 Span::test_data(),
296 ),
297 Value::date(
298 DateTime::parse_from_str(
299 "2023-05-05 13:43:49 -05:00",
300 "%Y-%m-%d %H:%M:%S %z",
301 )
302 .expect("date calculation should not fail in test"),
303 Span::test_data(),
304 ),
305 ],
306 Span::test_data(),
307 )),
308 },
309 ]
310 }
311}
312
313#[derive(Clone, Debug)]
314struct DatetimeFormat(String);
315
316fn action(input: &Value, args: &Arguments, head: Span) -> Value {
317 let timezone = &args.zone_options;
318 let dateformat = &args.format_options;
319
320 if let Value::Date { .. } = input {
322 return input.clone();
323 }
324
325 if let Value::Record { val: record, .. } = input {
326 if let Some(tz) = timezone {
327 return Value::error(
328 ShellError::IncompatibleParameters {
329 left_message: "got a record as input".into(),
330 left_span: head,
331 right_message: "the timezone should be included in the record".into(),
332 right_span: tz.span,
333 },
334 head,
335 );
336 }
337
338 if let Some(dt) = dateformat {
339 return Value::error(
340 ShellError::IncompatibleParameters {
341 left_message: "got a record as input".into(),
342 left_span: head,
343 right_message: "cannot be used with records".into(),
344 right_span: dt.span,
345 },
346 head,
347 );
348 }
349
350 let span = input.span();
351 return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span));
352 }
353
354 if matches!(input, Value::String { .. }) && dateformat.is_none() {
356 let span = input.span();
357 if let Ok(input_val) = input.coerce_str()
358 && let Ok(date) = parse_date_from_string(&input_val, span)
359 {
360 return Value::date(date, span);
361 }
362 }
363
364 let timestamp = match input {
366 Value::Int { val, .. } => Ok(*val),
367 Value::String { val, .. } => val.parse::<i64>(),
368 Value::Error { .. } => return input.clone(),
370 other => {
371 return Value::error(
372 ShellError::OnlySupportsThisInputType {
373 exp_input_type: "string and int".into(),
374 wrong_type: other.get_type().to_string(),
375 dst_span: head,
376 src_span: other.span(),
377 },
378 head,
379 );
380 }
381 };
382
383 if dateformat.is_none()
384 && let Ok(ts) = timestamp
385 {
386 return match timezone {
387 None => Value::date(Utc.timestamp_nanos(ts).into(), head),
391 Some(Spanned { item, span }) => match item {
392 Zone::Utc => {
393 let dt = Utc.timestamp_nanos(ts);
394 Value::date(dt.into(), *span)
395 }
396 Zone::Local => {
397 let dt = Local.timestamp_nanos(ts);
398 Value::date(dt.into(), *span)
399 }
400 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
401 Some(eastoffset) => {
402 let dt = eastoffset.timestamp_nanos(ts);
403 Value::date(dt, *span)
404 }
405 None => Value::error(
406 ShellError::DatetimeParseError {
407 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
408 span: *span,
409 },
410 *span,
411 ),
412 },
413 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
414 Some(westoffset) => {
415 let dt = westoffset.timestamp_nanos(ts);
416 Value::date(dt, *span)
417 }
418 None => Value::error(
419 ShellError::DatetimeParseError {
420 msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
421 span: *span,
422 },
423 *span,
424 ),
425 },
426 Zone::Error => Value::error(
427 ShellError::TypeMismatch {
429 err_message: "Invalid timezone or offset".to_string(),
430 span: *span,
431 },
432 *span,
433 ),
434 },
435 };
436 };
437
438 let span = input.span();
440
441 let parse_as_string = |val: &str| {
442 match dateformat {
443 Some(dt_format) => {
444 let format_str = dt_format
446 .item
447 .0
448 .replace("%J", "%Y%m%d") .replace("%Q", "%H%M%S"); match DateTime::parse_from_str(val, &format_str) {
451 Ok(dt) => match timezone {
452 None => Value::date(dt, head),
453 Some(Spanned { item, span }) => match item {
454 Zone::Utc => Value::date(dt.with_timezone(&Utc).into(), *span),
455 Zone::Local => Value::date(dt.with_timezone(&Local).into(), *span),
456 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
457 Some(eastoffset) => {
458 Value::date(dt.with_timezone(&eastoffset), *span)
459 }
460 None => Value::error(
461 ShellError::DatetimeParseError {
462 msg: input
463 .to_abbreviated_string(&nu_protocol::Config::default()),
464 span: *span,
465 },
466 *span,
467 ),
468 },
469 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
470 Some(westoffset) => {
471 Value::date(dt.with_timezone(&westoffset), *span)
472 }
473 None => Value::error(
474 ShellError::DatetimeParseError {
475 msg: input
476 .to_abbreviated_string(&nu_protocol::Config::default()),
477 span: *span,
478 },
479 *span,
480 ),
481 },
482 Zone::Error => Value::error(
483 ShellError::TypeMismatch {
485 err_message: "Invalid timezone or offset".to_string(),
486 span: *span,
487 },
488 *span,
489 ),
490 },
491 },
492 Err(reason) => parse_with_format(val, &format_str, head, timezone.as_ref())
493 .unwrap_or_else(|_| {
494 Value::error(
495 ShellError::CantConvert {
496 to_type: format!(
497 "could not parse as datetime using format '{}'",
498 dt_format.item.0
499 ),
500 from_type: reason.to_string(),
501 span: head,
502 help: Some(
503 "you can use `into datetime` without a format string to \
504 enable flexible parsing"
505 .to_string(),
506 ),
507 },
508 head,
509 )
510 }),
511 }
512 }
513
514 None => match parse_date_from_string(val, span) {
518 Ok(date) => Value::date(date, span),
519 Err(err) => err,
520 },
521 }
522 };
523
524 match input {
525 Value::String { val, .. } => parse_as_string(val),
526 Value::Int { val, .. } => parse_as_string(&val.to_string()),
527
528 Value::Error { .. } => input.clone(),
530 other => Value::error(
531 ShellError::OnlySupportsThisInputType {
532 exp_input_type: "string".into(),
533 wrong_type: other.get_type().to_string(),
534 dst_span: head,
535 src_span: other.span(),
536 },
537 head,
538 ),
539 }
540}
541
542fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
543 if let Some(invalid_col) = record
544 .columns()
545 .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
546 {
547 let allowed_cols = ALLOWED_COLUMNS.join(", ");
548 return Err(ShellError::UnsupportedInput {
549 msg: format!(
550 "Column '{invalid_col}' is not valid for a structured datetime. Allowed columns \
551 are: {allowed_cols}"
552 ),
553 input: "value originates from here".into(),
554 msg_span: head,
555 input_span: span,
556 });
557 };
558
559 #[derive(Debug)]
562 enum RecordColumnDefault {
563 Now,
564 Zero,
565 }
566 let mut record_column_default = RecordColumnDefault::Now;
567
568 let now = Local::now();
569 let mut now_nanosecond = now.nanosecond();
570 let now_millisecond = now_nanosecond / 1_000_000;
571 now_nanosecond %= 1_000_000;
572 let now_microsecond = now_nanosecond / 1_000;
573 now_nanosecond %= 1_000;
574
575 let year: i32 = match record.get("year") {
576 Some(val) => {
577 record_column_default = RecordColumnDefault::Zero;
578 match val {
579 Value::Int { val, .. } => *val as i32,
580 other => {
581 return Err(ShellError::OnlySupportsThisInputType {
582 exp_input_type: "int".to_string(),
583 wrong_type: other.get_type().to_string(),
584 dst_span: head,
585 src_span: other.span(),
586 });
587 }
588 }
589 }
590 None => now.year(),
591 };
592 let month = match record.get("month") {
593 Some(col_val) => {
594 record_column_default = RecordColumnDefault::Zero;
595 parse_value_from_record_as_u32("month", col_val, &head, &span)?
596 }
597 None => match record_column_default {
598 RecordColumnDefault::Now => now.month(),
599 RecordColumnDefault::Zero => 1,
600 },
601 };
602 let day = match record.get("day") {
603 Some(col_val) => {
604 record_column_default = RecordColumnDefault::Zero;
605 parse_value_from_record_as_u32("day", col_val, &head, &span)?
606 }
607 None => match record_column_default {
608 RecordColumnDefault::Now => now.day(),
609 RecordColumnDefault::Zero => 1,
610 },
611 };
612 let hour = match record.get("hour") {
613 Some(col_val) => {
614 record_column_default = RecordColumnDefault::Zero;
615 parse_value_from_record_as_u32("hour", col_val, &head, &span)?
616 }
617 None => match record_column_default {
618 RecordColumnDefault::Now => now.hour(),
619 RecordColumnDefault::Zero => 0,
620 },
621 };
622 let minute = match record.get("minute") {
623 Some(col_val) => {
624 record_column_default = RecordColumnDefault::Zero;
625 parse_value_from_record_as_u32("minute", col_val, &head, &span)?
626 }
627 None => match record_column_default {
628 RecordColumnDefault::Now => now.minute(),
629 RecordColumnDefault::Zero => 0,
630 },
631 };
632 let second = match record.get("second") {
633 Some(col_val) => {
634 record_column_default = RecordColumnDefault::Zero;
635 parse_value_from_record_as_u32("second", col_val, &head, &span)?
636 }
637 None => match record_column_default {
638 RecordColumnDefault::Now => now.second(),
639 RecordColumnDefault::Zero => 0,
640 },
641 };
642 let millisecond = match record.get("millisecond") {
643 Some(col_val) => {
644 record_column_default = RecordColumnDefault::Zero;
645 parse_value_from_record_as_u32("millisecond", col_val, &head, &span)?
646 }
647 None => match record_column_default {
648 RecordColumnDefault::Now => now_millisecond,
649 RecordColumnDefault::Zero => 0,
650 },
651 };
652 let microsecond = match record.get("microsecond") {
653 Some(col_val) => {
654 record_column_default = RecordColumnDefault::Zero;
655 parse_value_from_record_as_u32("microsecond", col_val, &head, &span)?
656 }
657 None => match record_column_default {
658 RecordColumnDefault::Now => now_microsecond,
659 RecordColumnDefault::Zero => 0,
660 },
661 };
662
663 let nanosecond = match record.get("nanosecond") {
664 Some(col_val) => parse_value_from_record_as_u32("nanosecond", col_val, &head, &span)?,
665 None => match record_column_default {
666 RecordColumnDefault::Now => now_nanosecond,
667 RecordColumnDefault::Zero => 0,
668 },
669 };
670
671 let offset: FixedOffset = match record.get("timezone") {
672 Some(timezone) => parse_timezone_from_record(timezone, &head, &timezone.span())?,
673 None => now.offset().to_owned(),
674 };
675
676 let total_nanoseconds = nanosecond + microsecond * 1_000 + millisecond * 1_000_000;
677
678 let date = match NaiveDate::from_ymd_opt(year, month, day) {
679 Some(d) => d,
680 None => {
681 return Err(ShellError::IncorrectValue {
682 msg: "one of more values are incorrect and do not represent valid date".to_string(),
683 val_span: head,
684 call_span: span,
685 });
686 }
687 };
688 let time = match NaiveTime::from_hms_nano_opt(hour, minute, second, total_nanoseconds) {
689 Some(t) => t,
690 None => {
691 return Err(ShellError::IncorrectValue {
692 msg: "one of more values are incorrect and do not represent valid time".to_string(),
693 val_span: head,
694 call_span: span,
695 });
696 }
697 };
698 let date_time = NaiveDateTime::new(date, time);
699
700 let date_time_fixed = match offset.from_local_datetime(&date_time).single() {
701 Some(d) => d,
702 None => {
703 return Err(ShellError::IncorrectValue {
704 msg: "Ambiguous or invalid timezone conversion".to_string(),
705 val_span: head,
706 call_span: span,
707 });
708 }
709 };
710 Ok(Value::date(date_time_fixed, span))
711}
712
713fn parse_value_from_record_as_u32(
714 col: &str,
715 col_val: &Value,
716 head: &Span,
717 span: &Span,
718) -> Result<u32, ShellError> {
719 let value: u32 = match col_val {
720 Value::Int { val, .. } => {
721 if *val < 0 || *val > u32::MAX as i64 {
722 return Err(ShellError::IncorrectValue {
723 msg: format!("incorrect value for {col}"),
724 val_span: *head,
725 call_span: *span,
726 });
727 }
728 *val as u32
729 }
730 other => {
731 return Err(ShellError::OnlySupportsThisInputType {
732 exp_input_type: "int".to_string(),
733 wrong_type: other.get_type().to_string(),
734 dst_span: *head,
735 src_span: other.span(),
736 });
737 }
738 };
739 Ok(value)
740}
741
742fn parse_timezone_from_record(
743 timezone: &Value,
744 head: &Span,
745 span: &Span,
746) -> Result<FixedOffset, ShellError> {
747 match timezone {
748 Value::String { val, .. } => {
749 let offset: FixedOffset = match val.parse() {
750 Ok(offset) => offset,
751 Err(_) => {
752 return Err(ShellError::IncorrectValue {
753 msg: "invalid timezone".to_string(),
754 val_span: *span,
755 call_span: *head,
756 });
757 }
758 };
759 Ok(offset)
760 }
761 other => Err(ShellError::OnlySupportsThisInputType {
762 exp_input_type: "string".to_string(),
763 wrong_type: other.get_type().to_string(),
764 dst_span: *head,
765 src_span: other.span(),
766 }),
767 }
768}
769
770fn datetime_parse_error_value(val: &str, span: Span) -> Value {
771 Value::error(
772 ShellError::DatetimeParseError {
773 msg: val.to_string(),
774 span,
775 },
776 span,
777 )
778}
779
780fn invalid_timezone_value(span: Span) -> Value {
781 Value::error(
782 ShellError::TypeMismatch {
784 err_message: "Invalid timezone or offset".to_string(),
785 span,
786 },
787 span,
788 )
789}
790
791fn interpret_wall_clock_datetime(
792 dt: NaiveDateTime,
793 timezone: Option<&Spanned<Zone>>,
794 head: Span,
795 val: &str,
796) -> Value {
797 match timezone {
798 None => match Local.from_local_datetime(&dt).single() {
799 Some(dt_native) => Value::date(dt_native.into(), head),
800 None => datetime_parse_error_value(val, head),
801 },
802 Some(Spanned { item, span }) => match item {
803 Zone::Utc => Value::date(Utc.from_utc_datetime(&dt).into(), *span),
804 Zone::Local => match Local.from_local_datetime(&dt).single() {
805 Some(dt_native) => Value::date(dt_native.into(), *span),
806 None => datetime_parse_error_value(val, *span),
807 },
808 Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
809 Some(eastoffset) => match eastoffset.from_local_datetime(&dt).single() {
810 Some(dt_native) => Value::date(dt_native, *span),
811 None => datetime_parse_error_value(val, *span),
812 },
813 None => datetime_parse_error_value(val, *span),
814 },
815 Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
816 Some(westoffset) => match westoffset.from_local_datetime(&dt).single() {
817 Some(dt_native) => Value::date(dt_native, *span),
818 None => datetime_parse_error_value(val, *span),
819 },
820 None => datetime_parse_error_value(val, *span),
821 },
822 Zone::Error => invalid_timezone_value(*span),
823 },
824 }
825}
826
827fn parse_with_format(
828 val: &str,
829 fmt: &str,
830 head: Span,
831 timezone: Option<&Spanned<Zone>>,
832) -> Result<Value, ()> {
833 if let Ok(dt) = NaiveDateTime::parse_from_str(val, fmt) {
835 return Ok(interpret_wall_clock_datetime(dt, timezone, head, val));
836 }
837
838 if let Ok(date) = NaiveDate::parse_from_str(val, fmt)
840 && let Some(dt) = date.and_hms_opt(0, 0, 0)
841 {
842 return Ok(interpret_wall_clock_datetime(dt, timezone, head, val));
843 }
844
845 if let Ok(time) = NaiveTime::parse_from_str(val, fmt) {
847 let now = Local::now().naive_local().date();
848 return Ok(interpret_wall_clock_datetime(
849 now.and_time(time),
850 timezone,
851 head,
852 val,
853 ));
854 }
855
856 Err(())
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use super::{DatetimeFormat, IntoDatetime, Zone, action};
863 use nu_protocol::Type::Error;
864
865 #[test]
866 fn test_examples() -> nu_test_support::Result {
867 nu_test_support::test().examples(IntoDatetime)
868 }
869
870 #[test]
871 fn takes_a_date_format_with_timezone() {
872 let date_str = Value::test_string("16.11.1984 8:00 am +0000");
873 let fmt_options = Some(Spanned {
874 item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
875 span: Span::test_data(),
876 });
877 let args = Arguments {
878 zone_options: None,
879 format_options: fmt_options,
880 cell_paths: None,
881 };
882 let actual = action(&date_str, &args, Span::test_data());
883 let expected = Value::date(
884 DateTime::parse_from_str("16.11.1984 8:00 am +0000", "%d.%m.%Y %H:%M %P %z").unwrap(),
885 Span::test_data(),
886 );
887 assert_eq!(actual, expected)
888 }
889
890 #[test]
891 fn takes_a_date_format_without_timezone() {
892 let date_str = Value::test_string("16.11.1984 8:00 am");
893 let fmt_options = Some(Spanned {
894 item: DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()),
895 span: Span::test_data(),
896 });
897 let args = Arguments {
898 zone_options: None,
899 format_options: fmt_options,
900 cell_paths: None,
901 };
902 let actual = action(&date_str, &args, Span::test_data());
903 let expected = Value::date(
904 Local
905 .from_local_datetime(
906 &NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
907 .unwrap(),
908 )
909 .unwrap()
910 .with_timezone(Local::now().offset()),
911 Span::test_data(),
912 );
913
914 assert_eq!(actual, expected)
915 }
916
917 #[test]
918 fn takes_a_date_format_without_timezone_with_utc_timezone() {
919 let date_str = Value::test_string("2026-03-21_00:25");
920 let timezone_option = Some(Spanned {
921 item: Zone::Utc,
922 span: Span::test_data(),
923 });
924 let fmt_options = Some(Spanned {
925 item: DatetimeFormat("%F_%R".to_string()),
926 span: Span::test_data(),
927 });
928 let args = Arguments {
929 zone_options: timezone_option,
930 format_options: fmt_options,
931 cell_paths: None,
932 };
933 let actual = action(&date_str, &args, Span::test_data());
934 let expected = Value::date(
935 Utc.from_utc_datetime(
936 &NaiveDateTime::parse_from_str("2026-03-21_00:25", "%F_%R").unwrap(),
937 )
938 .into(),
939 Span::test_data(),
940 );
941
942 assert_eq!(actual, expected)
943 }
944
945 #[test]
946 fn takes_a_date_format_without_timezone_with_offset_timezone() {
947 let date_str = Value::test_string("29 Aug 2025 19:30:07");
948 let timezone_option = Some(Spanned {
949 item: Zone::East(3),
950 span: Span::test_data(),
951 });
952 let fmt_options = Some(Spanned {
953 item: DatetimeFormat("%d %b %Y %H:%M:%S".to_string()),
954 span: Span::test_data(),
955 });
956 let args = Arguments {
957 zone_options: timezone_option,
958 format_options: fmt_options,
959 cell_paths: None,
960 };
961 let actual = action(&date_str, &args, Span::test_data());
962
963 let dt =
964 NaiveDateTime::parse_from_str("29 Aug 2025 19:30:07", "%d %b %Y %H:%M:%S").unwrap();
965 let eastoffset = FixedOffset::east_opt(3 * HOUR).unwrap();
966 let expected = Value::date(
967 eastoffset.from_local_datetime(&dt).single().unwrap(),
968 Span::test_data(),
969 );
970
971 assert_eq!(actual, expected)
972 }
973
974 #[test]
975 fn takes_a_date_format_without_timezone_applies_timezone_and_offset_differently() {
976 let date_str = Value::test_string("2026-03-21_00:25");
977 let fmt_options = Some(Spanned {
978 item: DatetimeFormat("%F_%R".to_string()),
979 span: Span::test_data(),
980 });
981
982 let utc_args = Arguments {
983 zone_options: Some(Spanned {
984 item: Zone::Utc,
985 span: Span::test_data(),
986 }),
987 format_options: fmt_options.clone(),
988 cell_paths: None,
989 };
990 let east_args = Arguments {
991 zone_options: Some(Spanned {
992 item: Zone::East(1),
993 span: Span::test_data(),
994 }),
995 format_options: fmt_options,
996 cell_paths: None,
997 };
998
999 let utc_actual = action(&date_str, &utc_args, Span::test_data());
1000 let east_actual = action(&date_str, &east_args, Span::test_data());
1001
1002 assert_ne!(utc_actual, east_actual)
1003 }
1004
1005 #[test]
1006 fn takes_iso8601_date_format() {
1007 let date_str = Value::test_string("2020-08-04T16:39:18+00:00");
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 let expected = Value::date(
1015 DateTime::parse_from_str("2020-08-04T16:39:18+00:00", "%Y-%m-%dT%H:%M:%S%z").unwrap(),
1016 Span::test_data(),
1017 );
1018 assert_eq!(actual, expected)
1019 }
1020
1021 #[test]
1022 fn takes_timestamp_offset() {
1023 let date_str = Value::test_string("1614434140000000000");
1024 let timezone_option = Some(Spanned {
1025 item: Zone::East(8),
1026 span: Span::test_data(),
1027 });
1028 let args = Arguments {
1029 zone_options: timezone_option,
1030 format_options: None,
1031 cell_paths: None,
1032 };
1033 let actual = action(&date_str, &args, Span::test_data());
1034 let expected = Value::date(
1035 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
1036 Span::test_data(),
1037 );
1038
1039 assert_eq!(actual, expected)
1040 }
1041
1042 #[test]
1043 fn takes_timestamp_offset_as_int() {
1044 let date_int = Value::test_int(1_614_434_140_000_000_000);
1045 let timezone_option = Some(Spanned {
1046 item: Zone::East(8),
1047 span: Span::test_data(),
1048 });
1049 let args = Arguments {
1050 zone_options: timezone_option,
1051 format_options: None,
1052 cell_paths: None,
1053 };
1054 let actual = action(&date_int, &args, Span::test_data());
1055 let expected = Value::date(
1056 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
1057 Span::test_data(),
1058 );
1059
1060 assert_eq!(actual, expected)
1061 }
1062
1063 #[test]
1064 fn takes_int_with_formatstring() {
1065 let date_int = Value::test_int(1_614_434_140);
1066 let fmt_options = Some(Spanned {
1067 item: DatetimeFormat("%s".to_string()),
1068 span: Span::test_data(),
1069 });
1070 let args = Arguments {
1071 zone_options: None,
1072 format_options: fmt_options,
1073 cell_paths: None,
1074 };
1075 let actual = action(&date_int, &args, Span::test_data());
1076 let expected = Value::date(
1077 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
1078 Span::test_data(),
1079 );
1080
1081 assert_eq!(actual, expected)
1082 }
1083
1084 #[test]
1085 fn takes_timestamp_offset_as_int_with_formatting() {
1086 let date_int = Value::test_int(1_614_434_140);
1087 let timezone_option = Some(Spanned {
1088 item: Zone::East(8),
1089 span: Span::test_data(),
1090 });
1091 let fmt_options = Some(Spanned {
1092 item: DatetimeFormat("%s".to_string()),
1093 span: Span::test_data(),
1094 });
1095 let args = Arguments {
1096 zone_options: timezone_option,
1097 format_options: fmt_options,
1098 cell_paths: None,
1099 };
1100 let actual = action(&date_int, &args, Span::test_data());
1101 let expected = Value::date(
1102 DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
1103 Span::test_data(),
1104 );
1105
1106 assert_eq!(actual, expected)
1107 }
1108
1109 #[test]
1110 fn takes_timestamp_offset_as_int_with_local_timezone() {
1111 let date_int = Value::test_int(1_614_434_140);
1112 let timezone_option = Some(Spanned {
1113 item: Zone::Local,
1114 span: Span::test_data(),
1115 });
1116 let fmt_options = Some(Spanned {
1117 item: DatetimeFormat("%s".to_string()),
1118 span: Span::test_data(),
1119 });
1120 let args = Arguments {
1121 zone_options: timezone_option,
1122 format_options: fmt_options,
1123 cell_paths: None,
1124 };
1125 let actual = action(&date_int, &args, Span::test_data());
1126 let expected = Value::date(
1127 Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
1128 Span::test_data(),
1129 );
1130 assert_eq!(actual, expected)
1131 }
1132
1133 #[test]
1134 fn takes_timestamp() {
1135 let date_str = Value::test_string("1614434140000000000");
1136 let timezone_option = Some(Spanned {
1137 item: Zone::Local,
1138 span: Span::test_data(),
1139 });
1140 let args = Arguments {
1141 zone_options: timezone_option,
1142 format_options: None,
1143 cell_paths: None,
1144 };
1145 let actual = action(&date_str, &args, Span::test_data());
1146 let expected = Value::date(
1147 Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
1148 Span::test_data(),
1149 );
1150
1151 assert_eq!(actual, expected)
1152 }
1153
1154 #[test]
1155 fn takes_datetime() {
1156 let timezone_option = Some(Spanned {
1157 item: Zone::Local,
1158 span: Span::test_data(),
1159 });
1160 let args = Arguments {
1161 zone_options: timezone_option,
1162 format_options: None,
1163 cell_paths: None,
1164 };
1165 let expected = Value::date(
1166 Local.timestamp_opt(1_614_434_140, 0).unwrap().into(),
1167 Span::test_data(),
1168 );
1169 let actual = action(&expected, &args, Span::test_data());
1170
1171 assert_eq!(actual, expected)
1172 }
1173
1174 #[test]
1175 fn takes_timestamp_without_timezone() {
1176 let date_str = Value::test_string("1614434140000000000");
1177 let args = Arguments {
1178 zone_options: None,
1179 format_options: None,
1180 cell_paths: None,
1181 };
1182 let actual = action(&date_str, &args, Span::test_data());
1183
1184 let expected = Value::date(
1185 Utc.timestamp_opt(1_614_434_140, 0).unwrap().into(),
1186 Span::test_data(),
1187 );
1188
1189 assert_eq!(actual, expected)
1190 }
1191
1192 #[test]
1193 fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
1194 let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
1195 let fmt_options = Some(Spanned {
1196 item: DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()),
1197 span: Span::test_data(),
1198 });
1199 let args = Arguments {
1200 zone_options: None,
1201 format_options: fmt_options,
1202 cell_paths: None,
1203 };
1204 let actual = action(&date_str, &args, Span::test_data());
1205
1206 assert_eq!(actual.get_type(), Error);
1207 }
1208}