diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 0eff739d5f..c692e30aa4 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -317,7 +317,8 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { ); } - return merge_record(record, head, input.span()); + let span = input.span(); + return merge_record(record, head, span).unwrap_or_else(|err| Value::error(err, span)); } // Let's try dtparse first @@ -458,15 +459,10 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { } }, Err(reason) => { - match parse_with_format(val, &dt_format.item.0, head) { - Ok(parsed) => parsed, - Err(_) => { - Value::error ( + parse_with_format(val, &dt_format.item.0, head).unwrap_or_else(|_| Value::error ( 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()) }, head, - ) - } - } + )) } }, @@ -501,21 +497,20 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { } } -fn merge_record(record: &Record, head: Span, span: Span) -> Value { +fn merge_record(record: &Record, head: Span, span: Span) -> Result { if let Some(invalid_col) = record .columns() .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str())) { let allowed_cols = ALLOWED_COLUMNS.join(", "); - return Value::error(ShellError::UnsupportedInput { + return Err(ShellError::UnsupportedInput { msg: format!( "Column '{invalid_col}' is not valid for a structured datetime. Allowed columns are: {allowed_cols}" ), input: "value originates from here".into(), msg_span: head, input_span: span - }, - span, + } ); }; @@ -541,15 +536,12 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { match val { Value::Int { val, .. } => *val as i32, other => { - return Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".to_string(), - wrong_type: other.get_type().to_string(), - dst_span: head, - src_span: other.span(), - }, - span, - ); + return Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "int".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }); } } } @@ -558,12 +550,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let month = match record.get("month") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("month", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("month", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now.month(), @@ -573,12 +560,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let day = match record.get("day") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("day", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("day", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now.day(), @@ -588,12 +570,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let hour = match record.get("hour") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("hour", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("hour", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now.hour(), @@ -603,12 +580,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let minute = match record.get("minute") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("minute", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("minute", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now.minute(), @@ -618,12 +590,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let second = match record.get("second") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("second", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("second", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now.second(), @@ -633,12 +600,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let millisecond = match record.get("millisecond") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("millisecond", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("millisecond", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now_millisecond, @@ -648,12 +610,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let microsecond = match record.get("microsecond") { Some(col_val) => { record_column_default = RecordColumnDefault::Zero; - match parse_value_from_record_as_u32("microsecond", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } + parse_value_from_record_as_u32("microsecond", col_val, &head, &span)? } None => match record_column_default { RecordColumnDefault::Now => now_microsecond, @@ -662,14 +619,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { }; let nanosecond = match record.get("nanosecond") { - Some(col_val) => { - match parse_value_from_record_as_u32("nanosecond", col_val, &head, &span) { - Ok(value) => value, - Err(err) => { - return err; - } - } - } + Some(col_val) => parse_value_from_record_as_u32("nanosecond", col_val, &head, &span)?, None => match record_column_default { RecordColumnDefault::Now => now_nanosecond, RecordColumnDefault::Zero => 0, @@ -677,12 +627,7 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { }; let offset: FixedOffset = match record.get("timezone") { - Some(timezone) => match parse_timezone_from_record(timezone, &head, &timezone.span()) { - Ok(value) => value, - Err(err) => { - return err; - } - }, + Some(timezone) => parse_timezone_from_record(timezone, &head, &timezone.span())?, None => now.offset().to_owned(), }; @@ -691,29 +636,21 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let date = match NaiveDate::from_ymd_opt(year, month, day) { Some(d) => d, None => { - return Value::error( - ShellError::IncorrectValue { - msg: "one of more values are incorrect and do not represent valid date" - .to_string(), - val_span: head, - call_span: span, - }, - span, - ) + return Err(ShellError::IncorrectValue { + msg: "one of more values are incorrect and do not represent valid date".to_string(), + val_span: head, + call_span: span, + }) } }; let time = match NaiveTime::from_hms_nano_opt(hour, minute, second, total_nanoseconds) { Some(t) => t, None => { - return Value::error( - ShellError::IncorrectValue { - msg: "one of more values are incorrect and do not represent valid time" - .to_string(), - val_span: head, - call_span: span, - }, - span, - ) + return Err(ShellError::IncorrectValue { + msg: "one of more values are incorrect and do not represent valid time".to_string(), + val_span: head, + call_span: span, + }) } }; let date_time = NaiveDateTime::new(date, time); @@ -721,17 +658,14 @@ fn merge_record(record: &Record, head: Span, span: Span) -> Value { let date_time_fixed = match offset.from_local_datetime(&date_time).single() { Some(d) => d, None => { - return Value::error( - ShellError::IncorrectValue { - msg: "Ambiguous or invalid timezone conversion".to_string(), - val_span: head, - call_span: span, - }, - span, - ) + return Err(ShellError::IncorrectValue { + msg: "Ambiguous or invalid timezone conversion".to_string(), + val_span: head, + call_span: span, + }) } }; - Value::date(date_time_fixed, span) + Ok(Value::date(date_time_fixed, span)) } fn parse_value_from_record_as_u32( @@ -739,31 +673,25 @@ fn parse_value_from_record_as_u32( col_val: &Value, head: &Span, span: &Span, -) -> Result { +) -> Result { let value: u32 = match col_val { Value::Int { val, .. } => { if *val < 0 || *val > u32::MAX as i64 { - return Err(Value::error( - ShellError::IncorrectValue { - msg: format!("incorrect value for {}", col), - val_span: *head, - call_span: *span, - }, - *span, - )); + return Err(ShellError::IncorrectValue { + msg: format!("incorrect value for {}", col), + val_span: *head, + call_span: *span, + }); } *val as u32 } other => { - return Err(Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".to_string(), - wrong_type: other.get_type().to_string(), - dst_span: *head, - src_span: other.span(), - }, - *span, - )); + return Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "int".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: *head, + src_span: other.span(), + }); } }; Ok(value) @@ -773,33 +701,27 @@ fn parse_timezone_from_record( timezone: &Value, head: &Span, span: &Span, -) -> Result { +) -> Result { match timezone { Value::String { val, .. } => { let offset: FixedOffset = match val.parse() { Ok(offset) => offset, Err(_) => { - return Err(Value::error( - ShellError::IncorrectValue { - msg: "invalid timezone".to_string(), - val_span: *span, - call_span: *head, - }, - *span, - )) + return Err(ShellError::IncorrectValue { + msg: "invalid timezone".to_string(), + val_span: *span, + call_span: *head, + }) } }; Ok(offset) } - other => Err(Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "string".to_string(), - wrong_type: other.get_type().to_string(), - dst_span: *head, - src_span: other.span(), - }, - *span, - )), + other => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "string".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: *head, + src_span: other.span(), + }), } } diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 8c92c22841..c826d60551 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -1,8 +1,41 @@ +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS}; use nu_protocol::{ast::Expr, Unit}; +const NS_PER_US: i64 = 1_000; +const NS_PER_MS: i64 = 1_000_000; const NS_PER_SEC: i64 = 1_000_000_000; +const NS_PER_MINUTE: i64 = 60 * NS_PER_SEC; +const NS_PER_HOUR: i64 = 60 * NS_PER_MINUTE; +const NS_PER_DAY: i64 = 24 * NS_PER_HOUR; +const NS_PER_WEEK: i64 = 7 * NS_PER_DAY; + +const ALLOWED_COLUMNS: [&str; 9] = [ + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + "sign", +]; +const ALLOWED_SIGNS: [&str; 2] = ["+", "-"]; + +#[derive(Clone, Debug)] +struct Arguments { + unit: Option>, + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + #[derive(Clone)] pub struct IntoDuration; @@ -18,11 +51,14 @@ impl Command for IntoDuration { (Type::Float, Type::Duration), (Type::String, Type::Duration), (Type::Duration, Type::Duration), + // FIXME: https://github.com/nushell/nushell/issues/15485 + // 'record -> any' was added as a temporary workaround to avoid type inference issues. The Any arm needs to be appear first. + (Type::record(), Type::Any), + (Type::record(), Type::record()), + (Type::record(), Type::Duration), (Type::table(), Type::table()), - //todo: record | into duration -> Duration - //(Type::record(), Type::record()), ]) - //.allow_variants_without_examples(true) + .allow_variants_without_examples(true) .named( "unit", SyntaxShape::String, @@ -56,7 +92,35 @@ impl Command for IntoDuration { call: &Call, input: PipelineData, ) -> Result { - into_duration(engine_state, stack, call, input) + let cell_paths = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + + let span = match input.span() { + Some(t) => t, + None => call.head, + }; + let unit = match call.get_flag::>(engine_state, stack, "unit")? { + Some(spanned_unit) => { + if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"] + .contains(&spanned_unit.item.as_str()) + { + Some(spanned_unit) + } else { + return Err(ShellError::CantConvertToDuration { + details: spanned_unit.item, + dst_span: span, + src_span: span, + help: Some( + "supported units are ns, us/µs, ms, sec, min, hr, day, and wk" + .to_string(), + ), + }); + } + } + None => None, + }; + let args = Arguments { unit, cell_paths }; + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { @@ -115,67 +179,18 @@ impl Command for IntoDuration { example: "1.234 | into duration --unit sec", result: Some(Value::test_duration(1_234 * 1_000_000)), }, + Example { + description: "Convert a record to a duration", + example: "{day: 10, hour: 2, minute: 6, second: 50, sign: '+'} | into duration", + result: Some(Value::duration( + 10 * NS_PER_DAY + 2 * NS_PER_HOUR + 6 * NS_PER_MINUTE + 50 * NS_PER_SEC, + Span::test_data(), + )), + }, ] } } -fn into_duration( - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, -) -> Result { - let span = match input.span() { - Some(t) => t, - None => call.head, - }; - let column_paths: Vec = call.rest(engine_state, stack, 0)?; - - let unit = match call.get_flag::(engine_state, stack, "unit")? { - Some(sep) => { - if ["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"] - .iter() - .any(|d| d == &sep) - { - sep - } else { - return Err(ShellError::CantConvertToDuration { - details: sep, - dst_span: span, - src_span: span, - help: Some( - "supported units are ns, us/µs, ms, sec, min, hr, day, and wk".to_string(), - ), - }); - } - } - None => "ns".to_string(), - }; - - input.map( - move |v| { - if column_paths.is_empty() { - action(&v, &unit.clone(), span) - } else { - let unitclone = &unit.clone(); - let mut ret = v; - for path in &column_paths { - let r = ret.update_cell_path( - &path.members, - Box::new(move |old| action(old, unitclone, span)), - ); - if let Err(error) = r { - return Value::error(error, span); - } - } - - ret - } - }, - engine_state.signals(), - ) -} - fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator { s.split_whitespace().map(move |sub| { // Gets the offset of the `sub` substring inside the string `s`. @@ -238,27 +253,51 @@ fn string_to_duration(s: &str, span: Span) -> Result { }) } -fn action(input: &Value, unit: &str, span: Span) -> Value { +fn action(input: &Value, args: &Arguments, head: Span) -> Value { let value_span = input.span(); + let unit_option = &args.unit; + + if let Value::Record { .. } | Value::Duration { .. } = input { + if let Some(unit) = unit_option { + return Value::error( + ShellError::IncompatibleParameters { + left_message: "got a record as input".into(), + left_span: head, + right_message: "the units should be included in the record".into(), + right_span: unit.span, + }, + head, + ); + } + } + + let unit: &str = match unit_option { + Some(unit) => &unit.item, + None => "ns", + }; + match input { Value::Duration { .. } => input.clone(), + Value::Record { val, .. } => { + merge_record(val, head, value_span).unwrap_or_else(|err| Value::error(err, value_span)) + } Value::String { val, .. } => { if let Ok(num) = val.parse::() { let ns = unit_to_ns_factor(unit); - return Value::duration((num * (ns as f64)) as i64, span); + return Value::duration((num * (ns as f64)) as i64, head); } match compound_to_duration(val, value_span) { - Ok(val) => Value::duration(val, span), - Err(error) => Value::error(error, span), + Ok(val) => Value::duration(val, head), + Err(error) => Value::error(error, head), } } Value::Float { val, .. } => { let ns = unit_to_ns_factor(unit); - Value::duration((*val * (ns as f64)) as i64, span) + Value::duration((*val * (ns as f64)) as i64, head) } Value::Int { val, .. } => { let ns = unit_to_ns_factor(unit); - Value::duration(*val * ns, span) + Value::duration(*val * ns, head) } // Propagate errors by explicitly matching them before the final case. Value::Error { .. } => input.clone(), @@ -266,24 +305,130 @@ fn action(input: &Value, unit: &str, span: Span) -> Value { ShellError::OnlySupportsThisInputType { exp_input_type: "string or duration".into(), wrong_type: other.get_type().to_string(), - dst_span: span, + dst_span: head, src_span: other.span(), }, - span, + head, ), } } +fn merge_record(record: &Record, head: Span, span: Span) -> Result { + if let Some(invalid_col) = record + .columns() + .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str())) + { + let allowed_cols = ALLOWED_COLUMNS.join(", "); + return Err(ShellError::UnsupportedInput { + msg: format!( + "Column '{invalid_col}' is not valid for a structured duration. Allowed columns are: {allowed_cols}" + ), + input: "value originates from here".into(), + msg_span: head, + input_span: span + } + ); + }; + + let mut duration: i64 = 0; + + if let Some(col_val) = record.get("week") { + let week = parse_number_from_record(col_val, &head)?; + duration += week * NS_PER_WEEK; + }; + if let Some(col_val) = record.get("day") { + let day = parse_number_from_record(col_val, &head)?; + duration += day * NS_PER_DAY; + }; + if let Some(col_val) = record.get("hour") { + let hour = parse_number_from_record(col_val, &head)?; + duration += hour * NS_PER_HOUR; + }; + if let Some(col_val) = record.get("minute") { + let minute = parse_number_from_record(col_val, &head)?; + duration += minute * NS_PER_MINUTE; + }; + if let Some(col_val) = record.get("second") { + let second = parse_number_from_record(col_val, &head)?; + duration += second * NS_PER_SEC; + }; + if let Some(col_val) = record.get("millisecond") { + let millisecond = parse_number_from_record(col_val, &head)?; + duration += millisecond * NS_PER_MS; + }; + if let Some(col_val) = record.get("microsecond") { + let microsecond = parse_number_from_record(col_val, &head)?; + duration += microsecond * NS_PER_US; + }; + if let Some(col_val) = record.get("nanosecond") { + let nanosecond = parse_number_from_record(col_val, &head)?; + duration += nanosecond; + }; + + if let Some(sign) = record.get("sign") { + match sign { + Value::String { val, .. } => { + if !ALLOWED_SIGNS.contains(&val.as_str()) { + let allowed_signs = ALLOWED_SIGNS.join(", "); + return Err(ShellError::IncorrectValue { + msg: format!("Invalid sign. Allowed signs are {}", allowed_signs) + .to_string(), + val_span: sign.span(), + call_span: head, + }); + } + if val == "-" { + duration = -duration; + } + } + other => { + return Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "int".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }); + } + } + }; + + Ok(Value::duration(duration, span)) +} + +fn parse_number_from_record(col_val: &Value, head: &Span) -> Result { + let value = match col_val { + Value::Int { val, .. } => { + if *val < 0 { + return Err(ShellError::IncorrectValue { + msg: "number should be positive".to_string(), + val_span: col_val.span(), + call_span: *head, + }); + } + *val + } + other => { + return Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "int".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: *head, + src_span: other.span(), + }); + } + }; + Ok(value) +} + fn unit_to_ns_factor(unit: &str) -> i64 { match unit { "ns" => 1, - "us" | "µs" => 1_000, - "ms" => 1_000_000, + "us" | "µs" => NS_PER_US, + "ms" => NS_PER_MS, "sec" => NS_PER_SEC, - "min" => NS_PER_SEC * 60, - "hr" => NS_PER_SEC * 60 * 60, - "day" => NS_PER_SEC * 60 * 60 * 24, - "wk" => NS_PER_SEC * 60 * 60 * 24 * 7, + "min" => NS_PER_MINUTE, + "hr" => NS_PER_HOUR, + "day" => NS_PER_DAY, + "wk" => NS_PER_WEEK, _ => 0, } } @@ -304,24 +449,27 @@ mod test { #[rstest] #[case("3ns", 3)] - #[case("4us", 4*1000)] - #[case("4\u{00B5}s", 4*1000)] // micro sign - #[case("4\u{03BC}s", 4*1000)] // mu symbol - #[case("5ms", 5 * 1000 * 1000)] + #[case("4us", 4 * NS_PER_US)] + #[case("4\u{00B5}s", 4 * NS_PER_US)] // micro sign + #[case("4\u{03BC}s", 4 * NS_PER_US)] // mu symbol + #[case("5ms", 5 * NS_PER_MS)] #[case("1sec", NS_PER_SEC)] - #[case("7min", 7 * 60 * NS_PER_SEC)] - #[case("42hr", 42 * 60 * 60 * NS_PER_SEC)] - #[case("123day", 123 * 24 * 60 * 60 * NS_PER_SEC)] - #[case("3wk", 3 * 7 * 24 * 60 * 60 * NS_PER_SEC)] + #[case("7min", 7 * NS_PER_MINUTE)] + #[case("42hr", 42 * NS_PER_HOUR)] + #[case("123day", 123 * NS_PER_DAY)] + #[case("3wk", 3 * NS_PER_WEEK)] #[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] // compound duration string - #[case("14ns 3hr 17sec", 14 + 3 * 3600 * NS_PER_SEC + 17 * NS_PER_SEC)] // compound string with units in random order + #[case("14ns 3hr 17sec", 14 + 3 * NS_PER_HOUR + 17 * NS_PER_SEC)] // compound string with units in random order fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) { - let actual = action( - &Value::test_string(phrase), - "ns", - Span::new(0, phrase.len()), - ); + let args = Arguments { + unit: Some(Spanned { + item: "ns".to_string(), + span: Span::test_data(), + }), + cell_paths: None, + }; + let actual = action(&Value::test_string(phrase), &args, Span::test_data()); match actual { Value::Duration { val: observed_val, .. diff --git a/crates/nu-command/tests/commands/into_datetime.rs b/crates/nu-command/tests/commands/into_datetime.rs index 8ad096ef0a..7dc17497a2 100644 --- a/crates/nu-command/tests/commands/into_datetime.rs +++ b/crates/nu-command/tests/commands/into_datetime.rs @@ -2,6 +2,13 @@ use nu_test_support::nu; // Tests happy paths +#[test] +fn into_datetime_from_record_cell_path() { + let actual = nu!(r#"{d: '2021'} | into datetime d"#); + + assert!(actual.out.contains("years ago")); +} + #[test] fn into_datetime_from_record() { let actual = nu!( diff --git a/crates/nu-command/tests/commands/into_duration.rs b/crates/nu-command/tests/commands/into_duration.rs index a39ea8e12d..7d5ef91ae9 100644 --- a/crates/nu-command/tests/commands/into_duration.rs +++ b/crates/nu-command/tests/commands/into_duration.rs @@ -8,3 +8,103 @@ fn into_duration_float() { assert_eq!("1min 4sec 200ms", actual.out); } + +#[test] +fn into_duration_from_record_cell_path() { + let actual = nu!(r#"{d: '1hr'} | into duration d"#); + let expected = nu!(r#"{d: 1hr}"#); + + assert_eq!(expected.out, actual.out); +} + +#[test] +fn into_duration_from_record() { + let actual = nu!( + r#"{week: 10, day: 1, hour: 2, minute: 3, second: 4, millisecond: 5, microsecond: 6, nanosecond: 7, sign: '+'} | into duration | into record"# + ); + let expected = nu!( + r#"{week: 10, day: 1, hour: 2, minute: 3, second: 4, millisecond: 5, microsecond: 6, nanosecond: 7, sign: '+'}"# + ); + + assert_eq!(expected.out, actual.out); +} + +#[test] +fn into_duration_from_record_negative() { + let actual = nu!( + r#"{week: 10, day: 1, hour: 2, minute: 3, second: 4, millisecond: 5, microsecond: 6, nanosecond: 7, sign: '-'} | into duration | into record"# + ); + let expected = nu!( + r#"{week: 10, day: 1, hour: 2, minute: 3, second: 4, millisecond: 5, microsecond: 6, nanosecond: 7, sign: '-'}"# + ); + + assert_eq!(expected.out, actual.out); +} + +#[test] +fn into_duration_from_record_defaults() { + let actual = nu!(r#"{} | into duration | into int"#); + + assert_eq!("0".to_string(), actual.out); +} + +#[test] +fn into_duration_from_record_round_trip() { + let actual = nu!( + r#"('10wk 1day 2hr 3min 4sec 5ms 6µs 7ns' | into duration | into record | into duration | into string) == '10wk 1day 2hr 3min 4sec 5ms 6µs 7ns'"# + ); + + assert!(actual.out.contains("true")); +} + +#[test] +fn into_duration_table_column() { + let actual = + nu!(r#"[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value"#); + let expected = nu!(r#"[[value]; [1sec] [2min] [3hr] [4day] [5wk]]"#); + + assert_eq!(actual.out, expected.out); +} + +// Tests error paths + +#[test] +fn into_duration_from_record_fails_with_wrong_type() { + let actual = nu!(r#"{week: '10'} | into duration"#); + + assert!(actual + .err + .contains("nu::shell::only_supports_this_input_type")); +} + +#[test] +fn into_duration_from_record_fails_with_invalid_date_time_values() { + let actual = nu!(r#"{week: -10} | into duration"#); + + assert!(actual.err.contains("nu::shell::incorrect_value")); +} + +#[test] +fn into_duration_from_record_fails_with_invalid_sign() { + let actual = nu!(r#"{week: 10, sign: 'x'} | into duration"#); + + assert!(actual.err.contains("nu::shell::incorrect_value")); +} + +// Tests invalid usage + +#[test] +fn into_duration_from_record_fails_with_unknown_key() { + let actual = nu!(r#"{week: 10, unknown: 1} | into duration"#); + + assert!(actual.err.contains("nu::shell::unsupported_input")); +} + +#[test] +fn into_duration_from_record_incompatible_with_unit_flag() { + let actual = nu!( + r#"{week: 10, day: 1, hour: 2, minute: 3, second: 4, sign: '-'} | into duration --unit sec"# + ); + + assert!(actual.err.contains("nu::shell::incompatible_parameters")); +}