Skip to main content

nu_command/math/
round.rs

1use crate::math::utils::run_with_elementwise;
2use nu_engine::command_prelude::*;
3
4#[derive(Clone)]
5pub struct MathRound;
6
7impl Command for MathRound {
8    fn name(&self) -> &str {
9        "math round"
10    }
11
12    fn signature(&self) -> Signature {
13        Signature::build("math round")
14            .input_output_types(vec![
15                (Type::Number, Type::Number),
16                (Type::Duration, Type::Duration),
17                (Type::Filesize, Type::Filesize),
18                (
19                    Type::List(Box::new(Type::Number)),
20                    Type::List(Box::new(Type::Number)),
21                ),
22                (
23                    Type::List(Box::new(Type::Duration)),
24                    Type::List(Box::new(Type::Duration)),
25                ),
26                (
27                    Type::List(Box::new(Type::Filesize)),
28                    Type::List(Box::new(Type::Filesize)),
29                ),
30                (Type::Range, Type::List(Box::new(Type::Number))),
31                (Type::record(), Type::record()),
32            ])
33            .rest(
34                "columns",
35                SyntaxShape::CellPath,
36                "The cell-paths/columns to operate on.",
37            )
38            .allow_variants_without_examples(true)
39            .named(
40                "precision",
41                SyntaxShape::Number,
42                "Digits of precision.",
43                Some('p'),
44            )
45            .category(Category::Math)
46    }
47
48    fn description(&self) -> &str {
49        "Returns the input number rounded to the specified precision."
50    }
51
52    fn extra_description(&self) -> &str {
53        "Filesize and duration values are stored as integers in base units \
54         (bytes and nanoseconds). With no display unit to round against, \
55         `math round` is the identity function for those types. `--precision` is not supported \
56         for filesize or duration."
57    }
58
59    fn search_terms(&self) -> Vec<&str> {
60        vec!["approx", "closest", "nearest"]
61    }
62
63    fn is_const(&self) -> bool {
64        true
65    }
66
67    fn run(
68        &self,
69        engine_state: &EngineState,
70        stack: &mut Stack,
71        call: &Call,
72        input: PipelineData,
73    ) -> Result<PipelineData, ShellError> {
74        let precision_param: Option<i64> = call.get_flag(engine_state, stack, "precision")?;
75        let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
76        let head = call.head;
77        run_with_elementwise(
78            input,
79            cell_paths,
80            head,
81            engine_state.signals(),
82            true,
83            move |value| operate(value, head, precision_param),
84        )
85    }
86
87    fn run_const(
88        &self,
89        working_set: &StateWorkingSet,
90        call: &Call,
91        input: PipelineData,
92    ) -> Result<PipelineData, ShellError> {
93        let precision_param: Option<i64> = call.get_flag_const(working_set, "precision")?;
94        let cell_paths: Vec<CellPath> = call.rest_const(working_set, 0)?;
95        let head = call.head;
96        run_with_elementwise(
97            input,
98            cell_paths,
99            head,
100            working_set.permanent().signals(),
101            true,
102            move |value| operate(value, head, precision_param),
103        )
104    }
105
106    fn examples(&self) -> Vec<Example<'_>> {
107        vec![
108            Example {
109                description: "Apply the round function to a list of numbers.",
110                example: "[1.5 2.3 -3.1] | math round",
111                result: Some(Value::list(
112                    vec![Value::test_int(2), Value::test_int(2), Value::test_int(-3)],
113                    Span::test_data(),
114                )),
115            },
116            Example {
117                description: "Apply the round function with precision specified.",
118                example: "[1.555 2.333 -3.111] | math round --precision 2",
119                result: Some(Value::list(
120                    vec![
121                        Value::test_float(1.56),
122                        Value::test_float(2.33),
123                        Value::test_float(-3.11),
124                    ],
125                    Span::test_data(),
126                )),
127            },
128            Example {
129                description: "Apply negative precision to a list of numbers.",
130                example: "[123, 123.3, -123.4] | math round --precision -1",
131                result: Some(Value::list(
132                    vec![
133                        Value::test_int(120),
134                        Value::test_int(120),
135                        Value::test_int(-120),
136                    ],
137                    Span::test_data(),
138                )),
139            },
140            Example {
141                description: "Apply the round function to list-valued columns in a record.",
142                example: "{alice: [1.2 2.7 3.5], bob: [4.1 5.9]} | math round",
143                result: Some(Value::test_record(record! {
144                    "alice" => Value::list(
145                        vec![Value::test_int(1), Value::test_int(3), Value::test_int(4)],
146                        Span::test_data(),
147                    ),
148                    "bob" => Value::list(
149                        vec![Value::test_int(4), Value::test_int(6)],
150                        Span::test_data(),
151                    ),
152                })),
153            },
154            Example {
155                description: "Apply the round function to a single column using a cell path.",
156                example: "{alice: [1.2 2.7 3.5], bob: [4.1 5.9]} | math round alice",
157                result: Some(Value::test_record(record! {
158                    "alice" => Value::list(
159                        vec![Value::test_int(1), Value::test_int(3), Value::test_int(4)],
160                        Span::test_data(),
161                    ),
162                    "bob" => Value::list(
163                        vec![Value::test_float(4.1), Value::test_float(5.9)],
164                        Span::test_data(),
165                    ),
166                })),
167            },
168            Example {
169                // Filesize is already whole bytes; rounding cannot use the display unit (KB).
170                description: "Filesize values are already whole bytes, so rounding is a no-op.",
171                example: "2.1KB | math round",
172                result: Some(Value::test_filesize(2100)),
173            },
174        ]
175    }
176}
177
178fn operate(value: Value, head: Span, precision: Option<i64>) -> Value {
179    let span = value.span();
180
181    // Duration and filesize are already integer units (ns / bytes). Identity is
182    // correct without --precision; decimal precision is not meaningful for units.
183    if matches!(value, Value::Duration { .. } | Value::Filesize { .. }) {
184        if precision.is_some() {
185            return Value::error(
186                ShellError::UnsupportedInput {
187                    msg: "'math round --precision' is not supported for duration or filesize"
188                        .into(),
189                    input: "value originates from here".into(),
190                    msg_span: head,
191                    input_span: span,
192                },
193                span,
194            );
195        }
196        return value;
197    }
198
199    // We treat int values as float values to share the rounding path.
200    let float_val = match &value {
201        Value::Int { val, .. } => *val as f64,
202        Value::Float { val, .. } => *val,
203        Value::Error { .. } => return value,
204        other => {
205            return Value::error(
206                ShellError::OnlySupportsThisInputType {
207                    exp_input_type: crate::math::utils::NUMERIC_INPUT_TYPES.into(),
208                    wrong_type: other.get_type().to_string(),
209                    dst_span: head,
210                    src_span: other.span(),
211                },
212                head,
213            );
214        }
215    };
216
217    if !float_val.is_finite() {
218        return Value::error(
219            ShellError::UnsupportedInput {
220                msg: "cannot round non-finite number".into(),
221                input: "value originates from here".into(),
222                msg_span: span,
223                input_span: span,
224            },
225            span,
226        );
227    }
228
229    match precision {
230        Some(precision_number) => Value::float(
231            (float_val * ((10_f64).powf(precision_number as f64))).round()
232                / (10_f64).powf(precision_number as f64),
233            span,
234        ),
235        None => Value::int(float_val.round() as i64, span),
236    }
237}
238
239#[cfg(test)]
240mod test {
241    use super::*;
242
243    #[test]
244    fn test_examples() -> nu_test_support::Result {
245        nu_test_support::test().examples(MathRound)
246    }
247}