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 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 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 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}