1use super::range_regex::generate_range_regex;
8
9fn format_float(value: f64, precision: i32) -> String {
12 if value == (value as i64) as f64 {
13 return (value as i64).to_string();
14 }
15 let mut result = format!("{value:.*}", precision as usize);
16 if let Some(dot) = result.find('.') {
17 let trimmed_len = result.trim_end_matches('0').len();
18 if trimmed_len > dot {
19 result.truncate(trimmed_len);
20 } else if trimmed_len == dot {
21 result.truncate(dot);
22 }
23 }
24 result
25}
26
27fn escape_dot_for_regex(s: &str) -> String {
31 let mut result = String::with_capacity(s.len() + 2);
32 for c in s.chars() {
33 if c == '.' {
34 result.push_str("\\.");
35 } else {
36 result.push(c);
37 }
38 }
39 result
40}
41
42fn strip_anchors(s: &str) -> String {
45 if s.len() >= 2 {
46 s[1..s.len() - 1].to_string()
47 } else {
48 s.to_string()
49 }
50}
51
52pub fn generate_float_range_regex(start: Option<f64>, end: Option<f64>, precision: i32) -> String {
56 if let (Some(s), Some(e)) = (start, end)
57 && s > e
58 {
59 return "^()$".to_string();
60 }
61 if start.is_none() && end.is_none() {
62 return format!("^-?\\d+(\\.\\d{{1,{precision}}})?$");
63 }
64
65 let mut parts: Vec<String> = Vec::new();
66
67 let mut start_int = 0i64;
68 let mut end_int = 0i64;
69 let mut start_frac = 0.0;
70 let mut end_frac = 0.0;
71 let mut is_start_neg = false;
72 let mut is_end_neg = false;
73
74 if let Some(s) = start {
75 is_start_neg = s < 0.0;
76 start_int = s.floor() as i64;
77 start_frac = s - start_int as f64;
78 }
79 if let Some(e) = end {
80 is_end_neg = e < 0.0;
81 end_int = e.floor() as i64;
82 end_frac = e - end_int as f64;
83 }
84
85 match (start, end) {
86 (Some(s), None) => {
87 let s_str = format_float(s, precision);
88 parts.push(escape_dot_for_regex(&s_str));
91 if start_frac > 0.0 {
92 emit_frac_above(&mut parts, &s_str, is_start_neg, precision);
93 }
94 if start_int < i64::MAX - 1 {
95 let ir = strip_anchors(&generate_range_regex(Some(start_int + 1), None));
96 parts.push(format!("{ir}(\\.\\d{{1,{precision}}})?"));
97 }
98 }
99 (None, Some(e)) => {
100 let e_str = format_float(e, precision);
101 parts.push(escape_dot_for_regex(&e_str));
103 if end_frac > 0.0 {
104 emit_frac_below(&mut parts, &e_str, is_end_neg, precision);
105 }
106 if end_int > i64::MIN + 1 {
107 let ir = strip_anchors(&generate_range_regex(None, Some(end_int - 1)));
108 parts.push(format!("{ir}(\\.\\d{{1,{precision}}})?"));
109 }
110 }
111 (Some(s), Some(e)) => {
112 let s_str = format_float(s, precision);
113 let e_str = format_float(e, precision);
114 if start_int == end_int {
115 if start_frac == 0.0 && end_frac == 0.0 {
116 parts.push(start_int.to_string());
117 } else {
118 parts.push(escape_dot_for_regex(&s_str));
120 if s_str != e_str {
121 parts.push(escape_dot_for_regex(&e_str));
122 }
123 }
124 } else {
125 parts.push(escape_dot_for_regex(&s_str));
127 if s_str != e_str {
128 parts.push(escape_dot_for_regex(&e_str));
129 }
130 if end_int > start_int + 1 {
131 let ir = strip_anchors(&generate_range_regex(
132 Some(start_int + 1),
133 Some(end_int - 1),
134 ));
135 parts.push(format!("{ir}(\\.\\d{{1,{precision}}})?"));
136 }
137 if start_frac > 0.0 {
138 emit_frac_above(&mut parts, &s_str, is_start_neg, precision);
139 } else {
140 parts.push(format!("{start_int}\\.\\d{{1,{precision}}}"));
141 }
142 if end_frac > 0.0 {
143 emit_frac_below(&mut parts, &e_str, is_end_neg, precision);
144 } else {
145 parts.push(format!("{end_int}\\.\\d{{1,{precision}}}"));
146 }
147 }
148 }
149 (None, None) => unreachable!(),
150 }
151
152 format!("^({})$", parts.join("|"))
153}
154
155fn emit_frac_above(parts: &mut Vec<String>, num_str: &str, is_neg: bool, precision: i32) {
158 let dot = match num_str.find('.') {
159 Some(d) => d,
160 None => return,
161 };
162 let int_part = &num_str[..dot];
163 let frac: Vec<char> = num_str[dot + 1..].chars().collect();
164 for (i, &fc) in frac.iter().enumerate() {
165 if i == 0 {
166 if is_neg {
167 for d in '0'..fc {
168 parts.push(format!("{int_part}\\.{d}\\d{{0,{}}}", precision - 1));
169 }
170 } else {
171 let mut d = (fc as u8 + 1) as char;
172 while d <= '9' {
173 parts.push(format!("{int_part}\\.{d}\\d{{0,{}}}", precision - 1));
174 d = (d as u8 + 1) as char;
175 }
176 }
177 } else {
178 let pref: String = frac[..i].iter().collect();
179 if is_neg {
180 if fc > '0' {
181 for d in '0'..fc {
182 parts.push(format!(
183 "{int_part}\\.{pref}{d}\\d{{0,{}}}",
184 precision - i as i32 - 1
185 ));
186 }
187 }
188 } else {
189 let mut d = (fc as u8 + 1) as char;
190 while d <= '9' {
191 parts.push(format!(
192 "{int_part}\\.{pref}{d}\\d{{0,{}}}",
193 precision - i as i32 - 1
194 ));
195 d = (d as u8 + 1) as char;
196 }
197 }
198 }
199 }
200}
201
202fn emit_frac_below(parts: &mut Vec<String>, num_str: &str, is_neg: bool, precision: i32) {
205 let dot = match num_str.find('.') {
206 Some(d) => d,
207 None => return,
208 };
209 let int_part = &num_str[..dot];
210 let frac: Vec<char> = num_str[dot + 1..].chars().collect();
211 for (i, &fc) in frac.iter().enumerate() {
212 if i == 0 {
213 if is_neg {
214 let mut d = (fc as u8 + 1) as char;
215 while d <= '9' {
216 parts.push(format!("{int_part}\\.{d}\\d{{0,{}}}", precision - 1));
217 d = (d as u8 + 1) as char;
218 }
219 } else {
220 for d in '0'..fc {
221 parts.push(format!("{int_part}\\.{d}\\d{{0,{}}}", precision - 1));
222 }
223 }
224 } else if is_neg {
225 let pref: String = frac[..i].iter().collect();
226 let mut d = (fc as u8 + 1) as char;
227 while d <= '9' {
228 parts.push(format!(
229 "{int_part}\\.{pref}{d}\\d{{0,{}}}",
230 precision - i as i32 - 1
231 ));
232 d = (d as u8 + 1) as char;
233 }
234 } else if fc > '0' {
235 let pref: String = frac[..i].iter().collect();
236 for d in '0'..fc {
237 parts.push(format!(
238 "{int_part}\\.{pref}{d}\\d{{0,{}}}",
239 precision - i as i32 - 1
240 ));
241 }
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn unbounded_float() {
252 assert_eq!(
253 generate_float_range_regex(None, None, 6),
254 r"^-?\d+(\.\d{1,6})?$"
255 );
256 }
257
258 #[test]
259 fn inverted_range_is_empty() {
260 assert_eq!(generate_float_range_regex(Some(5.0), Some(1.0), 6), "^()$");
261 }
262
263 #[test]
264 fn whole_number_format() {
265 assert_eq!(format_float(3.0, 6), "3");
266 assert_eq!(format_float(3.5, 6), "3.5");
267 assert_eq!(format_float(3.120000, 6), "3.12");
268 }
269
270 #[test]
271 fn bounded_produces_anchored_regex() {
272 let r = generate_float_range_regex(Some(1.0), Some(10.0), 6);
273 assert!(r.starts_with("^(") && r.ends_with(")$"));
274 }
275
276 #[test]
277 fn escape_dot_helper() {
278 assert_eq!(escape_dot_for_regex("0.5"), r"0\.5");
279 assert_eq!(escape_dot_for_regex("-3.125"), r"-3\.125");
280 assert_eq!(escape_dot_for_regex("42"), "42");
281 }
282
283 #[test]
284 fn float_boundary_dot_is_escaped_not_wildcard() {
285 let r = generate_float_range_regex(Some(0.5), None, 6);
289 assert!(r.contains(r"0\.5"), "expected escaped boundary in {r}");
290 let bytes: Vec<char> = r.chars().collect();
292 for (i, &c) in bytes.iter().enumerate() {
293 if c == '.' {
294 assert!(i > 0 && bytes[i - 1] == '\\', "unescaped dot in {r}");
295 }
296 }
297 }
298
299 #[test]
300 fn float_boundary_dot_escaped_both_bounds() {
301 let r = generate_float_range_regex(Some(0.25), Some(0.75), 6);
302 assert!(r.contains(r"0\.25"), "expected escaped start in {r}");
303 assert!(r.contains(r"0\.75"), "expected escaped end in {r}");
304 }
305}