Skip to main content

nu_command/strings/format/
duration.rs

1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4pub const SUPPORTED_UNITS: &[&str] = &[
5    "ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk", "month", "yr", "dec",
6];
7
8struct Arguments {
9    format_value: Spanned<String>,
10    float_precision: usize,
11    cell_paths: Option<Vec<CellPath>>,
12}
13
14impl CmdArgument for Arguments {
15    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
16        self.cell_paths.take()
17    }
18}
19
20#[derive(Clone)]
21pub struct FormatDuration;
22
23impl Command for FormatDuration {
24    fn name(&self) -> &str {
25        "format duration"
26    }
27
28    fn signature(&self) -> Signature {
29        Signature::build("format duration")
30            .input_output_types(vec![
31                (Type::Duration, Type::String),
32                (
33                    Type::List(Box::new(Type::Duration)),
34                    Type::List(Box::new(Type::String)),
35                ),
36                (Type::table(), Type::table()),
37            ])
38            .allow_variants_without_examples(true)
39            .param(Parameter::Required(
40                PositionalArg::new("format value", SyntaxShape::String)
41                    .desc("The unit in which to display the duration.")
42                    .completion(Completion::new_list(SUPPORTED_UNITS)),
43            ))
44            .rest(
45                "rest",
46                SyntaxShape::CellPath,
47                "For a data structure input, format duration at the given cell paths.",
48            )
49            .category(Category::Strings)
50    }
51
52    fn description(&self) -> &str {
53        "Outputs duration with a specified unit of time."
54    }
55
56    fn search_terms(&self) -> Vec<&str> {
57        vec!["convert", "display", "pattern", "human readable"]
58    }
59
60    fn is_const(&self) -> bool {
61        true
62    }
63
64    fn run(
65        &self,
66        engine_state: &EngineState,
67        stack: &mut Stack,
68        call: &Call,
69        input: PipelineData,
70    ) -> Result<PipelineData, ShellError> {
71        let format_value = call.req::<Value>(engine_state, stack, 0)?;
72        let format_value_span = format_value.span();
73        let format_value = Spanned {
74            item: format_value.coerce_into_string()?.to_ascii_lowercase(),
75            span: format_value_span,
76        };
77        let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
78        let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
79        let float_precision = engine_state.config.float_precision as usize;
80        let arg = Arguments {
81            format_value,
82            float_precision,
83            cell_paths,
84        };
85        operate(
86            format_value_impl,
87            arg,
88            input,
89            call.head,
90            engine_state.signals(),
91        )
92    }
93
94    fn run_const(
95        &self,
96        working_set: &StateWorkingSet,
97        call: &Call,
98        input: PipelineData,
99    ) -> Result<PipelineData, ShellError> {
100        let format_value = call.req_const::<Value>(working_set, 0)?;
101        let format_value_span = format_value.span();
102        let format_value = Spanned {
103            item: format_value.coerce_into_string()?.to_ascii_lowercase(),
104            span: format_value_span,
105        };
106        let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
107        let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
108        let float_precision = working_set.permanent().config.float_precision as usize;
109        let arg = Arguments {
110            format_value,
111            float_precision,
112            cell_paths,
113        };
114        operate(
115            format_value_impl,
116            arg,
117            input,
118            call.head,
119            working_set.permanent().signals(),
120        )
121    }
122
123    fn examples(&self) -> Vec<Example<'_>> {
124        vec![
125            Example {
126                description: "Convert µs duration to the requested second duration as a string.",
127                example: "1000000µs | format duration sec",
128                result: Some(Value::test_string("1 sec")),
129            },
130            Example {
131                description: "Convert durations to µs duration as strings.",
132                example: "[1sec 2sec] | format duration µs",
133                result: Some(Value::test_list(vec![
134                    Value::test_string("1000000 µs"),
135                    Value::test_string("2000000 µs"),
136                ])),
137            },
138            Example {
139                description: "Convert duration to µs as a string if unit asked for was us.",
140                example: "1sec | format duration us",
141                result: Some(Value::test_string("1000000 µs")),
142            },
143        ]
144    }
145}
146
147fn format_value_impl(val: &Value, arg: &Arguments, span: Span) -> Value {
148    let inner_span = val.span();
149    match val {
150        Value::Duration { val: inner, .. } => {
151            let duration = *inner;
152            let float_precision = arg.float_precision;
153            match convert_inner_to_unit(duration, &arg.format_value.item, arg.format_value.span) {
154                Ok(d) => {
155                    let unit = if &arg.format_value.item == "us" {
156                        "µs"
157                    } else {
158                        &arg.format_value.item
159                    };
160                    if d.fract() == 0.0 {
161                        Value::string(format!("{d} {unit}"), inner_span)
162                    } else {
163                        Value::string(format!("{d:.float_precision$} {unit}"), inner_span)
164                    }
165                }
166                Err(e) => Value::error(e, inner_span),
167            }
168        }
169        Value::Error { .. } => val.clone(),
170        _ => Value::error(
171            ShellError::OnlySupportsThisInputType {
172                exp_input_type: "filesize".into(),
173                wrong_type: val.get_type().to_string(),
174                dst_span: span,
175                src_span: val.span(),
176            },
177            span,
178        ),
179    }
180}
181
182fn convert_inner_to_unit(val: i64, to_unit: &str, span: Span) -> Result<f64, ShellError> {
183    match to_unit {
184        "ns" => Ok(val as f64),
185        "us" => Ok(val as f64 / 1000.0),
186        "µs" => Ok(val as f64 / 1000.0), // Micro sign
187        "μs" => Ok(val as f64 / 1000.0), // Greek small letter
188        "ms" => Ok(val as f64 / 1000.0 / 1000.0),
189        "sec" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0),
190        "min" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0),
191        "hr" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0),
192        "day" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0 / 24.0),
193        "wk" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0 / 24.0 / 7.0),
194        "month" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0 / 24.0 / 30.0),
195        "yr" => Ok(val as f64 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0 / 24.0 / 365.0),
196        "dec" => Ok(val as f64 / 10.0 / 1000.0 / 1000.0 / 1000.0 / 60.0 / 60.0 / 24.0 / 365.0),
197
198        _ => Err(ShellError::InvalidUnit {
199            span,
200            supported_units: SUPPORTED_UNITS.join(", "),
201        }),
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_examples() -> nu_test_support::Result {
211        nu_test_support::test().examples(FormatDuration)
212    }
213}