tera/builtins/filters/
number.rs

1/// Filters operating on numbers
2use std::collections::HashMap;
3
4#[cfg(feature = "builtins")]
5use humansize::format_size;
6use serde_json::value::{to_value, Value};
7
8use crate::errors::{Error, Result};
9
10/// Returns the absolute value of the argument.
11pub fn abs(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
12    if value.as_u64().is_some() {
13        Ok(value.clone())
14    } else if let Some(num) = value.as_i64() {
15        Ok(to_value(num.abs()).unwrap())
16    } else if let Some(num) = value.as_f64() {
17        Ok(to_value(num.abs()).unwrap())
18    } else {
19        Err(Error::msg("Filter `abs` was used on a value that isn't a number."))
20    }
21}
22
23/// Returns a plural suffix if the value is not equal to ±1, or a singular
24/// suffix otherwise. The plural suffix defaults to `s` and the singular suffix
25/// defaults to the empty string (i.e nothing).
26pub fn pluralize(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
27    let num = try_get_value!("pluralize", "value", f64, value);
28
29    let plural = match args.get("plural") {
30        Some(val) => try_get_value!("pluralize", "plural", String, val),
31        None => "s".to_string(),
32    };
33
34    let singular = match args.get("singular") {
35        Some(val) => try_get_value!("pluralize", "singular", String, val),
36        None => "".to_string(),
37    };
38
39    // English uses plural when it isn't one
40    if (num.abs() - 1.).abs() > ::std::f64::EPSILON {
41        Ok(to_value(plural).unwrap())
42    } else {
43        Ok(to_value(singular).unwrap())
44    }
45}
46
47/// Returns a rounded number using the `method` arg and `precision` given.
48/// `method` defaults to `common` which will round to the nearest number.
49/// `ceil` and `floor` are also available as method.
50/// `precision` defaults to `0`, meaning it will round to an integer
51pub fn round(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
52    let num = try_get_value!("round", "value", f64, value);
53    let method = match args.get("method") {
54        Some(val) => try_get_value!("round", "method", String, val),
55        None => "common".to_string(),
56    };
57    let precision = match args.get("precision") {
58        Some(val) => try_get_value!("round", "precision", i32, val),
59        None => 0,
60    };
61    let multiplier = if precision == 0 { 1.0 } else { 10.0_f64.powi(precision) };
62
63    match method.as_ref() {
64        "common" => Ok(to_value((multiplier * num).round() / multiplier).unwrap()),
65        "ceil" => Ok(to_value((multiplier * num).ceil() / multiplier).unwrap()),
66        "floor" => Ok(to_value((multiplier * num).floor() / multiplier).unwrap()),
67        _ => Err(Error::msg(format!(
68            "Filter `round` received an incorrect value for arg `method`: got `{:?}`, \
69             only common, ceil and floor are allowed",
70            method
71        ))),
72    }
73}
74
75/// Returns a human-readable file size (i.e. '110 MB') from an integer
76#[cfg(feature = "builtins")]
77pub fn filesizeformat(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
78    let num = try_get_value!("filesizeformat", "value", usize, value);
79    let binary = match args.get("binary") {
80        Some(binary) => try_get_value!("filesizeformat", "binary", bool, binary),
81        None => false,
82    };
83    let format = if binary { humansize::BINARY } else { humansize::WINDOWS };
84    Ok(to_value(format_size(num, format))
85        .expect("json serializing should always be possible for a string"))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use serde_json::value::to_value;
92    use std::collections::HashMap;
93
94    #[test]
95    fn test_abs_unsigend() {
96        let result = abs(&to_value(1).unwrap(), &HashMap::new());
97        assert!(result.is_ok());
98        assert_eq!(result.unwrap(), to_value(1).unwrap());
99    }
100
101    #[test]
102    fn test_abs_negative_integer() {
103        let result = abs(&to_value(-1).unwrap(), &HashMap::new());
104        assert!(result.is_ok());
105        assert_eq!(result.unwrap(), to_value(1).unwrap());
106    }
107
108    #[test]
109    fn test_abs_negative_float() {
110        let result = abs(&to_value(-1.0).unwrap(), &HashMap::new());
111        assert!(result.is_ok());
112        assert_eq!(result.unwrap(), to_value(1.0).unwrap());
113    }
114
115    #[test]
116    fn test_abs_non_number() {
117        let result = abs(&to_value("nan").unwrap(), &HashMap::new());
118        assert!(result.is_err());
119        assert_eq!(
120            result.unwrap_err().to_string(),
121            "Filter `abs` was used on a value that isn't a number."
122        );
123    }
124
125    #[test]
126    fn test_pluralize_single() {
127        let result = pluralize(&to_value(1).unwrap(), &HashMap::new());
128        assert!(result.is_ok());
129        assert_eq!(result.unwrap(), to_value("").unwrap());
130    }
131
132    #[test]
133    fn test_pluralize_multiple() {
134        let result = pluralize(&to_value(2).unwrap(), &HashMap::new());
135        assert!(result.is_ok());
136        assert_eq!(result.unwrap(), to_value("s").unwrap());
137    }
138
139    #[test]
140    fn test_pluralize_zero() {
141        let result = pluralize(&to_value(0).unwrap(), &HashMap::new());
142        assert!(result.is_ok());
143        assert_eq!(result.unwrap(), to_value("s").unwrap());
144    }
145
146    #[test]
147    fn test_pluralize_multiple_custom_plural() {
148        let mut args = HashMap::new();
149        args.insert("plural".to_string(), to_value("es").unwrap());
150        let result = pluralize(&to_value(2).unwrap(), &args);
151        assert!(result.is_ok());
152        assert_eq!(result.unwrap(), to_value("es").unwrap());
153    }
154
155    #[test]
156    fn test_pluralize_multiple_custom_singular() {
157        let mut args = HashMap::new();
158        args.insert("singular".to_string(), to_value("y").unwrap());
159        let result = pluralize(&to_value(1).unwrap(), &args);
160        assert!(result.is_ok());
161        assert_eq!(result.unwrap(), to_value("y").unwrap());
162    }
163
164    #[test]
165    fn test_round_default() {
166        let result = round(&to_value(2.1).unwrap(), &HashMap::new());
167        assert!(result.is_ok());
168        assert_eq!(result.unwrap(), to_value(2.0).unwrap());
169    }
170
171    #[test]
172    fn test_round_default_precision() {
173        let mut args = HashMap::new();
174        args.insert("precision".to_string(), to_value(2).unwrap());
175        let result = round(&to_value(3.15159265359).unwrap(), &args);
176        assert!(result.is_ok());
177        assert_eq!(result.unwrap(), to_value(3.15).unwrap());
178    }
179
180    #[test]
181    fn test_round_ceil() {
182        let mut args = HashMap::new();
183        args.insert("method".to_string(), to_value("ceil").unwrap());
184        let result = round(&to_value(2.1).unwrap(), &args);
185        assert!(result.is_ok());
186        assert_eq!(result.unwrap(), to_value(3.0).unwrap());
187    }
188
189    #[test]
190    fn test_round_ceil_precision() {
191        let mut args = HashMap::new();
192        args.insert("method".to_string(), to_value("ceil").unwrap());
193        args.insert("precision".to_string(), to_value(1).unwrap());
194        let result = round(&to_value(2.11).unwrap(), &args);
195        assert!(result.is_ok());
196        assert_eq!(result.unwrap(), to_value(2.2).unwrap());
197    }
198
199    #[test]
200    fn test_round_floor() {
201        let mut args = HashMap::new();
202        args.insert("method".to_string(), to_value("floor").unwrap());
203        let result = round(&to_value(2.1).unwrap(), &args);
204        assert!(result.is_ok());
205        assert_eq!(result.unwrap(), to_value(2.0).unwrap());
206    }
207
208    #[test]
209    fn test_round_floor_precision() {
210        let mut args = HashMap::new();
211        args.insert("method".to_string(), to_value("floor").unwrap());
212        args.insert("precision".to_string(), to_value(1).unwrap());
213        let result = round(&to_value(2.91).unwrap(), &args);
214        assert!(result.is_ok());
215        assert_eq!(result.unwrap(), to_value(2.9).unwrap());
216    }
217
218    #[cfg(feature = "builtins")]
219    #[test]
220    fn test_filesizeformat() {
221        let args = HashMap::new();
222        let result = filesizeformat(&to_value(123456789).unwrap(), &args);
223        assert!(result.is_ok());
224        assert_eq!(result.unwrap(), to_value("117.74 MB").unwrap());
225    }
226
227    #[cfg(feature = "builtins")]
228    #[test]
229    fn test_filesizeformat_binary() {
230        let mut args = HashMap::new();
231        args.insert("binary".to_string(), to_value(true).unwrap());
232        let result = filesizeformat(&to_value(123456789).unwrap(), &args);
233        assert!(result.is_ok());
234        assert_eq!(result.unwrap(), to_value("117.74 MiB").unwrap());
235    }
236}