hurl/runner/
number.rs

1/*
2 * Hurl (https://hurl.dev)
3 * Copyright (C) 2025 Orange
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *          http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18use std::cmp::Ordering;
19use std::fmt;
20
21/// System types used in Hurl.
22///
23/// Values are used by queries, captures, asserts and predicates.
24#[derive(Clone, Debug)]
25pub enum Number {
26    Float(f64),
27    Integer(i64),
28    BigInteger(String),
29}
30
31// You must implement it yourself because of the Float
32impl PartialEq for Number {
33    fn eq(&self, other: &Self) -> bool {
34        match (self, other) {
35            (Number::Float(v1), Number::Float(v2)) => (v1 - v2).abs() < f64::EPSILON,
36            (Number::Integer(v1), Number::Integer(v2)) => v1 == v2,
37            (Number::BigInteger(v1), Number::BigInteger(v2)) => v1 == v2,
38            _ => false,
39        }
40    }
41}
42
43impl Eq for Number {}
44
45impl fmt::Display for Number {
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        let value = match self {
48            Number::Float(f) => format_float(*f),
49            Number::Integer(x) => x.to_string(),
50            Number::BigInteger(s) => s.to_string(),
51        };
52        write!(f, "{value}")
53    }
54}
55
56fn format_float(value: f64) -> String {
57    if value.fract() < f64::EPSILON {
58        format!("{value}.0")
59    } else {
60        value.to_string()
61    }
62}
63
64impl From<f64> for Number {
65    fn from(value: f64) -> Self {
66        Number::Float(value)
67    }
68}
69
70impl From<i64> for Number {
71    fn from(value: i64) -> Self {
72        Number::Integer(value)
73    }
74}
75
76impl Number {
77    pub fn cmp_value(&self, other: &Number) -> Ordering {
78        match (self, other) {
79            (Number::Integer(i1), Number::Integer(i2)) => i1.cmp(i2),
80            (Number::Float(f1), Number::Float(f2)) => compare_float(*f1, *f2),
81            (Number::Integer(i1), Number::Float(f2)) => compare_float(*i1 as f64, *f2),
82            (Number::Float(f1), Number::Integer(i2)) => compare_float(*f1, *i2 as f64),
83            (n1, n2) => compare_number_string(&n1.to_string(), &n2.to_string()),
84        }
85    }
86}
87
88fn compare_float(f1: f64, f2: f64) -> Ordering {
89    if f1 > f2 {
90        Ordering::Greater
91    } else if f1 < f2 {
92        Ordering::Less
93    } else {
94        Ordering::Equal
95    }
96}
97
98fn compare_number_string(n1: &str, n2: &str) -> Ordering {
99    let (neg1, i1, d1) = number_components(n1);
100    let (neg2, i2, d2) = number_components(n2);
101    if neg1 == neg2 {
102        match i1.cmp(i2) {
103            Ordering::Less => Ordering::Less,
104            Ordering::Greater => Ordering::Greater,
105            Ordering::Equal => d1.cmp(d2),
106        }
107    } else if neg1 {
108        Ordering::Less
109    } else {
110        Ordering::Greater
111    }
112}
113
114// return triple (negative, integer, decimals)
115fn number_components(s: &str) -> (bool, &str, &str) {
116    match s.strip_prefix('-') {
117        None => match s.find('.') {
118            None => (false, s.trim_start_matches('0'), ""),
119            Some(index) => (
120                false,
121                (s[..index].trim_start_matches('0')),
122                (s[(index + 1)..].trim_end_matches('0')),
123            ),
124        },
125        Some(s) => {
126            let (_, integer, decimal) = number_components(s);
127            (true, integer, decimal)
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_to_string() {
138        assert_eq!(Number::Float(1.0).to_string(), "1.0".to_string());
139        assert_eq!(Number::Float(1.1).to_string(), "1.1".to_string());
140        assert_eq!(Number::from(1.0).to_string(), "1.0".to_string());
141        assert_eq!(Number::from(1.1).to_string(), "1.1".to_string());
142        assert_eq!(
143            Number::BigInteger("1.1".to_string()).to_string(),
144            "1.1".to_string()
145        );
146        assert_eq!(
147            Number::BigInteger("1".to_string()).to_string(),
148            "1".to_string()
149        );
150    }
151
152    #[test]
153    fn test_cmp_value() {
154        let integer_zero = Number::from(0);
155        let integer_minus_one = Number::from(-1);
156        let integer_one = Number::from(1);
157        let integer_two = Number::from(2);
158        let integer_max = Number::from(i64::MAX);
159        let integer_min = Number::from(i64::MIN);
160
161        let float_zero = Number::from(0.0);
162        let float_one = Number::from(1.0);
163        let float_one_plus_epsilon = Number::from(1.000_000_000_000_000_100);
164        let float_one_plus_plus_epsilon = Number::from(1.000_000_000_000_001);
165        let float_two = Number::from(2.0);
166        let float_min = Number::from(f64::MIN);
167        let float_max = Number::from(f64::MAX);
168
169        let number_one = Number::BigInteger("1".to_string());
170        let number_two = Number::BigInteger("2".to_string());
171        let number_two_with_decimal = Number::BigInteger("2.0".to_string());
172
173        assert_eq!(integer_minus_one.cmp_value(&integer_zero), Ordering::Less);
174
175        assert_eq!(integer_one.cmp_value(&integer_one), Ordering::Equal);
176        assert_eq!(integer_one.cmp_value(&number_one), Ordering::Equal);
177        assert_eq!(integer_one.cmp_value(&float_one), Ordering::Equal);
178        assert_eq!(integer_one.cmp_value(&integer_zero), Ordering::Greater);
179        assert_eq!(integer_one.cmp_value(&float_zero), Ordering::Greater);
180        assert_eq!(integer_one.cmp_value(&integer_two), Ordering::Less);
181        assert_eq!(integer_one.cmp_value(&float_two), Ordering::Less);
182        assert_eq!(integer_one.cmp_value(&number_two), Ordering::Less);
183        assert_eq!(
184            integer_one.cmp_value(&number_two_with_decimal),
185            Ordering::Less
186        );
187
188        assert_eq!(integer_min.cmp_value(&float_min), Ordering::Greater);
189        assert_eq!(integer_max.cmp_value(&float_max), Ordering::Less);
190
191        assert_eq!(float_one.cmp_value(&float_one), Ordering::Equal);
192        assert_eq!(
193            float_one.cmp_value(&float_one_plus_epsilon),
194            Ordering::Equal
195        );
196        assert_eq!(
197            float_one.cmp_value(&float_one_plus_plus_epsilon),
198            Ordering::Less
199        );
200
201        // edge cases
202        // the integer 9_007_199_254_740_993 can not be represented by f64
203        // it will be casted to 9_007_199_254_740_992 for comparison
204        assert_eq!(
205            Number::from(9_007_199_254_740_992.0).cmp_value(&Number::from(9_007_199_254_740_993)),
206            Ordering::Equal
207        );
208    }
209
210    #[test]
211    fn test_cmp_number_string() {
212        assert_eq!(compare_number_string("1", "1"), Ordering::Equal);
213        assert_eq!(compare_number_string("1", "1.0"), Ordering::Equal);
214        assert_eq!(compare_number_string("1.000", "1.0"), Ordering::Equal);
215        assert_eq!(compare_number_string("1", "2"), Ordering::Less);
216        assert_eq!(compare_number_string("1.1", "2"), Ordering::Less);
217        assert_eq!(compare_number_string("-001.1000", "-1.1"), Ordering::Equal);
218    }
219
220    #[test]
221    fn test_number_components() {
222        assert_eq!(number_components("1"), (false, "1", ""));
223        assert_eq!(number_components("1.0"), (false, "1", ""));
224        assert_eq!(number_components("01"), (false, "1", ""));
225
226        assert_eq!(number_components("1.1"), (false, "1", "1"));
227        assert_eq!(number_components("1.100"), (false, "1", "1"));
228
229        assert_eq!(number_components("-1.1"), (true, "1", "1"));
230        assert_eq!(number_components("-01.100"), (true, "1", "1"));
231    }
232}