1use std::str::FromStr;
2
3use nu_cmd_base::input_handler::{CmdArgument, operate};
4use nu_engine::command_prelude::*;
5use nu_parser::{DURATION_UNIT_GROUPS, parse_unit_value};
6use nu_protocol::{SUPPORTED_DURATION_UNITS, Unit, ast::Expr};
7
8const NS_PER_US: i64 = 1_000;
9const NS_PER_MS: i64 = 1_000_000;
10const NS_PER_SEC: i64 = 1_000_000_000;
11const NS_PER_MINUTE: i64 = 60 * NS_PER_SEC;
12const NS_PER_HOUR: i64 = 60 * NS_PER_MINUTE;
13const NS_PER_DAY: i64 = 24 * NS_PER_HOUR;
14const NS_PER_WEEK: i64 = 7 * NS_PER_DAY;
15
16const ALLOWED_COLUMNS: [&str; 9] = [
17 "week",
18 "day",
19 "hour",
20 "minute",
21 "second",
22 "millisecond",
23 "microsecond",
24 "nanosecond",
25 "sign",
26];
27const ALLOWED_SIGNS: [&str; 2] = ["+", "-"];
28
29#[derive(Clone, Debug)]
30struct Arguments {
31 unit: Option<Spanned<Unit>>,
32 cell_paths: Option<Vec<CellPath>>,
33}
34
35impl CmdArgument for Arguments {
36 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
37 self.cell_paths.take()
38 }
39}
40
41#[derive(Clone)]
42pub struct IntoDuration;
43
44impl Command for IntoDuration {
45 fn name(&self) -> &str {
46 "into duration"
47 }
48
49 fn signature(&self) -> Signature {
50 Signature::build("into duration")
51 .input_output_types(vec![
52 (Type::Int, Type::Duration),
53 (Type::Float, Type::Duration),
54 (Type::String, Type::Duration),
55 (Type::Duration, Type::Duration),
56 (Type::record(), Type::Any),
59 (Type::record(), Type::record()),
60 (Type::record(), Type::Duration),
61 (Type::table(), Type::table()),
62 ])
63 .allow_variants_without_examples(true)
64 .named(
65 "unit",
66 SyntaxShape::String,
67 "Unit to convert number into (will have an effect only with integer input)",
68 Some('u'),
69 )
70 .rest(
71 "rest",
72 SyntaxShape::CellPath,
73 "For a data structure input, convert data at the given cell paths.",
74 )
75 .category(Category::Conversions)
76 }
77
78 fn description(&self) -> &str {
79 "Convert value to duration."
80 }
81
82 fn extra_description(&self) -> &str {
83 "Max duration value is i64::MAX nanoseconds; max duration time unit is wk (weeks)."
84 }
85
86 fn search_terms(&self) -> Vec<&str> {
87 vec!["convert", "time", "period"]
88 }
89
90 fn run(
91 &self,
92 engine_state: &EngineState,
93 stack: &mut Stack,
94 call: &Call,
95 input: PipelineData,
96 ) -> Result<PipelineData, ShellError> {
97 let cell_paths = call.rest(engine_state, stack, 0)?;
98 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
99
100 let unit = match call.get_flag::<Spanned<String>>(engine_state, stack, "unit")? {
101 Some(spanned_unit) => match Unit::from_str(&spanned_unit.item) {
102 Ok(u) => match u {
103 Unit::Filesize(_) => {
104 return Err(ShellError::InvalidUnit {
105 span: spanned_unit.span,
106 supported_units: SUPPORTED_DURATION_UNITS.join(", "),
107 });
108 }
109 _ => Some(Spanned {
110 item: u,
111 span: spanned_unit.span,
112 }),
113 },
114 Err(_) => {
115 return Err(ShellError::InvalidUnit {
116 span: spanned_unit.span,
117 supported_units: SUPPORTED_DURATION_UNITS.join(", "),
118 });
119 }
120 },
121 None => None,
122 };
123 let args = Arguments { unit, cell_paths };
124 operate(action, args, input, call.head, engine_state.signals())
125 }
126
127 fn examples(&self) -> Vec<Example> {
128 vec![
129 Example {
130 description: "Convert duration string to duration value",
131 example: "'7min' | into duration",
132 result: Some(Value::test_duration(7 * 60 * NS_PER_SEC)),
133 },
134 Example {
135 description: "Convert compound duration string to duration value",
136 example: "'1day 2hr 3min 4sec' | into duration",
137 result: Some(Value::test_duration(
138 (((((24) + 2) * 60) + 3) * 60 + 4) * NS_PER_SEC,
139 )),
140 },
141 Example {
142 description: "Convert table of duration strings to table of duration values",
143 example: "[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value",
144 result: Some(Value::test_list(vec![
145 Value::test_record(record! {
146 "value" => Value::test_duration(NS_PER_SEC),
147 }),
148 Value::test_record(record! {
149 "value" => Value::test_duration(2 * 60 * NS_PER_SEC),
150 }),
151 Value::test_record(record! {
152 "value" => Value::test_duration(3 * 60 * 60 * NS_PER_SEC),
153 }),
154 Value::test_record(record! {
155 "value" => Value::test_duration(4 * 24 * 60 * 60 * NS_PER_SEC),
156 }),
157 Value::test_record(record! {
158 "value" => Value::test_duration(5 * 7 * 24 * 60 * 60 * NS_PER_SEC),
159 }),
160 ])),
161 },
162 Example {
163 description: "Convert duration to duration",
164 example: "420sec | into duration",
165 result: Some(Value::test_duration(7 * 60 * NS_PER_SEC)),
166 },
167 Example {
168 description: "Convert a number of ns to duration",
169 example: "1_234_567 | into duration",
170 result: Some(Value::test_duration(1_234_567)),
171 },
172 Example {
173 description: "Convert a number of an arbitrary unit to duration",
174 example: "1_234 | into duration --unit ms",
175 result: Some(Value::test_duration(1_234 * 1_000_000)),
176 },
177 Example {
178 description: "Convert a floating point number of an arbitrary unit to duration",
179 example: "1.234 | into duration --unit sec",
180 result: Some(Value::test_duration(1_234 * 1_000_000)),
181 },
182 Example {
183 description: "Convert a record to a duration",
184 example: "{day: 10, hour: 2, minute: 6, second: 50, sign: '+'} | into duration",
185 result: Some(Value::duration(
186 10 * NS_PER_DAY + 2 * NS_PER_HOUR + 6 * NS_PER_MINUTE + 50 * NS_PER_SEC,
187 Span::test_data(),
188 )),
189 },
190 ]
191 }
192}
193
194fn split_whitespace_indices(s: &str, span: Span) -> impl Iterator<Item = (&str, Span)> {
195 s.split_whitespace().map(move |sub| {
196 let start_offset = span
205 .start
206 .wrapping_add(sub.as_ptr() as usize)
207 .wrapping_sub(s.as_ptr() as usize);
208 (sub, Span::new(start_offset, start_offset + sub.len()))
209 })
210}
211
212fn compound_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
213 let mut duration_ns: i64 = 0;
214
215 for (substring, substring_span) in split_whitespace_indices(s, span) {
216 let sub_ns = string_to_duration(substring, substring_span)?;
217 duration_ns += sub_ns;
218 }
219
220 Ok(duration_ns)
221}
222
223fn string_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
224 if let Some(Ok(expression)) = parse_unit_value(
225 s.as_bytes(),
226 span,
227 DURATION_UNIT_GROUPS,
228 Type::Duration,
229 |x| x,
230 ) {
231 if let Expr::ValueWithUnit(value) = expression.expr {
232 if let Expr::Int(x) = value.expr.expr {
233 match value.unit.item {
234 Unit::Nanosecond => return Ok(x),
235 Unit::Microsecond => return Ok(x * 1000),
236 Unit::Millisecond => return Ok(x * 1000 * 1000),
237 Unit::Second => return Ok(x * NS_PER_SEC),
238 Unit::Minute => return Ok(x * 60 * NS_PER_SEC),
239 Unit::Hour => return Ok(x * 60 * 60 * NS_PER_SEC),
240 Unit::Day => return Ok(x * 24 * 60 * 60 * NS_PER_SEC),
241 Unit::Week => return Ok(x * 7 * 24 * 60 * 60 * NS_PER_SEC),
242 _ => {}
243 }
244 }
245 }
246 }
247
248 Err(ShellError::InvalidUnit {
249 span,
250 supported_units: SUPPORTED_DURATION_UNITS.join(", "),
251 })
252}
253
254fn action(input: &Value, args: &Arguments, head: Span) -> Value {
255 let value_span = input.span();
256 let unit_option = &args.unit;
257
258 if let Value::Record { .. } | Value::Duration { .. } = input {
259 if let Some(unit) = unit_option {
260 return Value::error(
261 ShellError::IncompatibleParameters {
262 left_message: "got a record as input".into(),
263 left_span: head,
264 right_message: "the units should be included in the record".into(),
265 right_span: unit.span,
266 },
267 head,
268 );
269 }
270 }
271
272 let unit = match unit_option {
273 Some(unit) => &unit.item,
274 None => &Unit::Nanosecond,
275 };
276
277 match input {
278 Value::Duration { .. } => input.clone(),
279 Value::Record { val, .. } => {
280 merge_record(val, head, value_span).unwrap_or_else(|err| Value::error(err, value_span))
281 }
282 Value::String { val, .. } => {
283 if let Ok(num) = val.parse::<f64>() {
284 let ns = unit_to_ns_factor(unit);
285 return Value::duration((num * (ns as f64)) as i64, head);
286 }
287 match compound_to_duration(val, value_span) {
288 Ok(val) => Value::duration(val, head),
289 Err(error) => Value::error(error, head),
290 }
291 }
292 Value::Float { val, .. } => {
293 let ns = unit_to_ns_factor(unit);
294 Value::duration((*val * (ns as f64)) as i64, head)
295 }
296 Value::Int { val, .. } => {
297 let ns = unit_to_ns_factor(unit);
298 Value::duration(*val * ns, head)
299 }
300 Value::Error { .. } => input.clone(),
302 other => Value::error(
303 ShellError::OnlySupportsThisInputType {
304 exp_input_type: "string or duration".into(),
305 wrong_type: other.get_type().to_string(),
306 dst_span: head,
307 src_span: other.span(),
308 },
309 head,
310 ),
311 }
312}
313
314fn merge_record(record: &Record, head: Span, span: Span) -> Result<Value, ShellError> {
315 if let Some(invalid_col) = record
316 .columns()
317 .find(|key| !ALLOWED_COLUMNS.contains(&key.as_str()))
318 {
319 let allowed_cols = ALLOWED_COLUMNS.join(", ");
320 return Err(ShellError::UnsupportedInput {
321 msg: format!(
322 "Column '{invalid_col}' is not valid for a structured duration. Allowed columns are: {allowed_cols}"
323 ),
324 input: "value originates from here".into(),
325 msg_span: head,
326 input_span: span,
327 });
328 };
329
330 let mut duration: i64 = 0;
331
332 if let Some(col_val) = record.get("week") {
333 let week = parse_number_from_record(col_val, &head)?;
334 duration += week * NS_PER_WEEK;
335 };
336 if let Some(col_val) = record.get("day") {
337 let day = parse_number_from_record(col_val, &head)?;
338 duration += day * NS_PER_DAY;
339 };
340 if let Some(col_val) = record.get("hour") {
341 let hour = parse_number_from_record(col_val, &head)?;
342 duration += hour * NS_PER_HOUR;
343 };
344 if let Some(col_val) = record.get("minute") {
345 let minute = parse_number_from_record(col_val, &head)?;
346 duration += minute * NS_PER_MINUTE;
347 };
348 if let Some(col_val) = record.get("second") {
349 let second = parse_number_from_record(col_val, &head)?;
350 duration += second * NS_PER_SEC;
351 };
352 if let Some(col_val) = record.get("millisecond") {
353 let millisecond = parse_number_from_record(col_val, &head)?;
354 duration += millisecond * NS_PER_MS;
355 };
356 if let Some(col_val) = record.get("microsecond") {
357 let microsecond = parse_number_from_record(col_val, &head)?;
358 duration += microsecond * NS_PER_US;
359 };
360 if let Some(col_val) = record.get("nanosecond") {
361 let nanosecond = parse_number_from_record(col_val, &head)?;
362 duration += nanosecond;
363 };
364
365 if let Some(sign) = record.get("sign") {
366 match sign {
367 Value::String { val, .. } => {
368 if !ALLOWED_SIGNS.contains(&val.as_str()) {
369 let allowed_signs = ALLOWED_SIGNS.join(", ");
370 return Err(ShellError::IncorrectValue {
371 msg: format!("Invalid sign. Allowed signs are {}", allowed_signs)
372 .to_string(),
373 val_span: sign.span(),
374 call_span: head,
375 });
376 }
377 if val == "-" {
378 duration = -duration;
379 }
380 }
381 other => {
382 return Err(ShellError::OnlySupportsThisInputType {
383 exp_input_type: "int".to_string(),
384 wrong_type: other.get_type().to_string(),
385 dst_span: head,
386 src_span: other.span(),
387 });
388 }
389 }
390 };
391
392 Ok(Value::duration(duration, span))
393}
394
395fn parse_number_from_record(col_val: &Value, head: &Span) -> Result<i64, ShellError> {
396 let value = match col_val {
397 Value::Int { val, .. } => {
398 if *val < 0 {
399 return Err(ShellError::IncorrectValue {
400 msg: "number should be positive".to_string(),
401 val_span: col_val.span(),
402 call_span: *head,
403 });
404 }
405 *val
406 }
407 other => {
408 return Err(ShellError::OnlySupportsThisInputType {
409 exp_input_type: "int".to_string(),
410 wrong_type: other.get_type().to_string(),
411 dst_span: *head,
412 src_span: other.span(),
413 });
414 }
415 };
416 Ok(value)
417}
418
419fn unit_to_ns_factor(unit: &Unit) -> i64 {
420 match unit {
421 Unit::Nanosecond => 1,
422 Unit::Microsecond => NS_PER_US,
423 Unit::Millisecond => NS_PER_MS,
424 Unit::Second => NS_PER_SEC,
425 Unit::Minute => NS_PER_MINUTE,
426 Unit::Hour => NS_PER_HOUR,
427 Unit::Day => NS_PER_DAY,
428 Unit::Week => NS_PER_WEEK,
429 _ => 0,
430 }
431}
432
433#[cfg(test)]
434mod test {
435 use super::*;
436 use rstest::rstest;
437
438 #[test]
439 fn test_examples() {
440 use crate::test_examples;
441
442 test_examples(IntoDuration {})
443 }
444
445 const NS_PER_SEC: i64 = 1_000_000_000;
446
447 #[rstest]
448 #[case("3ns", 3)]
449 #[case("4us", 4 * NS_PER_US)]
450 #[case("4\u{00B5}s", 4 * NS_PER_US)] #[case("4\u{03BC}s", 4 * NS_PER_US)] #[case("5ms", 5 * NS_PER_MS)]
453 #[case("1sec", NS_PER_SEC)]
454 #[case("7min", 7 * NS_PER_MINUTE)]
455 #[case("42hr", 42 * NS_PER_HOUR)]
456 #[case("123day", 123 * NS_PER_DAY)]
457 #[case("3wk", 3 * NS_PER_WEEK)]
458 #[case("86hr 26ns", 86 * 3600 * NS_PER_SEC + 26)] #[case("14ns 3hr 17sec", 14 + 3 * NS_PER_HOUR + 17 * NS_PER_SEC)] fn turns_string_to_duration(#[case] phrase: &str, #[case] expected_duration_val: i64) {
462 let args = Arguments {
463 unit: Some(Spanned {
464 item: Unit::Nanosecond,
465 span: Span::test_data(),
466 }),
467 cell_paths: None,
468 };
469 let actual = action(&Value::test_string(phrase), &args, Span::test_data());
470 match actual {
471 Value::Duration {
472 val: observed_val, ..
473 } => {
474 assert_eq!(expected_duration_val, observed_val, "expected != observed")
475 }
476 other => {
477 panic!("Expected Value::Duration, observed {other:?}");
478 }
479 }
480 }
481}