Skip to content

Commit

Permalink
Add tests for numeric regular expressions; some bug fixes (#61)
Browse files Browse the repository at this point in the history
* integer tests and fixes
* float tests and fixes
  • Loading branch information
hudson-ai authored Nov 21, 2024
1 parent 8a4d88a commit 5c0705a
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 4 deletions.
13 changes: 13 additions & 0 deletions parser/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ crate-type = ["staticlib", "rlib", "cdylib"]

[build-dependencies]
cbindgen = "0.27.0"

[dev-dependencies]
regex = "1.11.1"
211 changes: 207 additions & 4 deletions parser/src/json/numeric.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use regex_syntax::escape;

fn mk_or(parts: Vec<String>) -> String {
if parts.len() == 1 {
Expand Down Expand Up @@ -74,11 +75,11 @@ pub fn rx_int_range(left: Option<i64>, right: Option<i64>) -> Result<String> {
let rx = &r[r.len() - 1..];

if lpref == rpref {
return Ok(format!("({}{}[{}-{}])", lpref, "", lx, rx));
return Ok(format!("({}[{}-{}])", lpref, lx, rx));
}

let left_rec = lpref.parse::<i64>().unwrap_or(0);
let right_rec = rpref.parse::<i64>().unwrap_or(0);
let mut left_rec = lpref.parse::<i64>().unwrap_or(0);
let mut right_rec = rpref.parse::<i64>().unwrap_or(0);
if left_rec >= right_rec {
return Err(anyhow!(
"Invalid recursive range: left_rec ({}) must be less than right_rec ({})",
Expand All @@ -90,10 +91,12 @@ pub fn rx_int_range(left: Option<i64>, right: Option<i64>) -> Result<String> {
let mut parts = Vec::new();

if lx != "0" {
left_rec += 1;
parts.push(format!("{}[{}-9]", lpref, lx));
}

if rx != "9" {
right_rec -= 1;
parts.push(format!("{}[0-{}]", rpref, rx));
}

Expand Down Expand Up @@ -335,7 +338,7 @@ pub fn rx_float_range(
}
if left == right {
if left_inclusive && right_inclusive {
Ok(format!("({})", float_to_str(left)))
Ok(format!("({})", escape(&float_to_str(left))))
} else {
Err(anyhow!(
"Empty range when left equals right and not both inclusive"
Expand Down Expand Up @@ -435,3 +438,203 @@ pub fn rx_float_range(
}
}
}

#[cfg(test)]
mod test {
use super::{rx_float_range, rx_int_range};
use regex::Regex;

fn do_test_int_range(rx: &str, left: Option<i64>, right: Option<i64>) {
let re = Regex::new(&format!("^{}$", rx)).unwrap();
for n in (left.unwrap_or(0) - 1000)..=(right.unwrap_or(0) + 1000) {
let matches = re.is_match(&n.to_string());
let expected = (left.is_none() || left.unwrap() <= n) && (right.is_none() || n <= right.unwrap());
if expected != matches {
let range_str = match (left, right) {
(Some(l), Some(r)) => format!("[{}, {}]", l, r),
(Some(l), None) => format!("[{}, ∞)", l),
(None, Some(r)) => format!("(-∞, {}]", r),
(None, None) => "(-∞, ∞)".to_string(),
};
if matches {
panic!("{} not in range {} but matches {:?}", n, range_str, rx);
} else {
panic!("{} in range {} but does not match {:?}", n, range_str, rx);
}
}
}
}

#[test]
fn test_int_range() {
let cases = vec![
(Some(0), Some(9)),
(Some(1), Some(7)),
(Some(0), Some(99)),
(Some(13), Some(170)),
(Some(13), Some(17)),
(Some(13), Some(27)),
(Some(13), Some(57)),
(Some(72), Some(91)),
(Some(723), Some(915)),
(Some(23), Some(915)),
(Some(-1), Some(915)),
(Some(-9), Some(9)),
(Some(-3), Some(3)),
(Some(-3), Some(0)),
(Some(-72), Some(13)),
(None, Some(0)),
(None, Some(7)),
(None, Some(23)),
(None, Some(725)),
(None, Some(-1)),
(None, Some(-17)),
(None, Some(-283)),
(Some(0), None),
(Some(2), None),
(Some(33), None),
(Some(234), None),
(Some(-1), None),
(Some(-87), None),
(Some(-329), None),
(None, None),
(Some(-13), Some(-13)),
(Some(-1), Some(-1)),
(Some(0), Some(0)),
(Some(1), Some(1)),
(Some(13), Some(13)),
];

for (left, right) in cases {
let rx = rx_int_range(left, right).unwrap();
do_test_int_range(&rx, left, right);
}
}

fn do_test_float_range(
rx: &str,
left: Option<f64>,
right: Option<f64>,
left_inclusive: bool,
right_inclusive: bool,
) {
let re = Regex::new(&format!("^{}$", rx)).unwrap();
let left_int = left.map(|x| {
let left_int = x.ceil() as i64;
if !left_inclusive && x == left_int as f64 {
left_int + 1
} else {
left_int
}
});
let right_int = right.map(|x| {
let right_int = x.floor() as i64;
if !right_inclusive && x == right_int as f64 {
right_int - 1
} else {
right_int
}
});
do_test_int_range(rx, left_int, right_int);

let eps1 = 0.0000001;
let eps2 = 0.01;
let test_cases = vec![
left.unwrap_or(-1000.0),
right.unwrap_or(1000.0),
0.0,
left_int.unwrap_or(-1000) as f64,
right_int.unwrap_or(1000) as f64,
];
for x in test_cases {
for offset in [0.0, -eps1, eps1, -eps2, eps2, 1.0, -1.0].iter() {
let n = x + offset;
let matches = re.is_match(&n.to_string());
let left_cond = left.is_none() || left.unwrap() < n || (left.unwrap() == n && left_inclusive);
let right_cond = right.is_none() || right.unwrap() > n || (right.unwrap() == n && right_inclusive);
let expected = left_cond && right_cond;
if expected != matches {
let lket = if left_inclusive {"["} else {"("};
let rket = if right_inclusive {"]"} else {")"};
let range_str = match (left, right) {
(Some(l), Some(r)) => format!("{}{}, {}{}", lket, l, r, rket),
(Some(l), None) => format!("{}{}, ∞)", lket, l),
(None, Some(r)) => format!("(-∞, {}{}", r, rket),
(None, None) => "(-∞, ∞)".to_string(),
};
if matches {
panic!("{} not in range {} but matches {:?}", n, range_str, rx);
} else {
panic!("{} in range {} but does not match {:?}", n, range_str, rx);
}
}
}
}
}

#[test]
fn test_float_range() {
let cases = vec![
(Some(0.0), Some(10.0)),
(Some(-10.0), Some(0.0)),
(Some(0.5), Some(0.72)),
(Some(0.5), Some(1.72)),
(Some(0.5), Some(1.32)),
(Some(0.45), Some(0.5)),
(Some(0.3245), Some(0.325)),
(Some(0.443245), Some(0.44325)),
(Some(1.0), Some(2.34)),
(Some(1.33), Some(2.0)),
(Some(1.0), Some(10.34)),
(Some(1.33), Some(10.0)),
(Some(-1.33), Some(10.0)),
(Some(-17.23), Some(-1.33)),
(Some(-1.23), Some(-1.221)),
(Some(-10.2), Some(45293.9)),
(None, Some(0.0)),
(None, Some(1.0)),
(None, Some(1.5)),
(None, Some(1.55)),
(None, Some(-17.23)),
(None, Some(-1.33)),
(None, Some(-1.23)),
(None, Some(103.74)),
(None, Some(100.0)),
(Some(0.0), None),
(Some(1.0), None),
(Some(1.5), None),
(Some(1.55), None),
(Some(-17.23), None),
(Some(-1.33), None),
(Some(-1.23), None),
(Some(103.74), None),
(Some(100.0), None),
(None, None),
(Some(-103.4), Some(-103.4)),
(Some(-27.0), Some(-27.0)),
(Some(-1.5), Some(-1.5)),
(Some(-1.0), Some(-1.0)),
(Some(0.0), Some(0.0)),
(Some(1.0), Some(1.0)),
(Some(1.5), Some(1.5)),
(Some(27.0), Some(27.0)),
(Some(103.4), Some(103.4)),
];

for (left, right) in cases {
for left_inclusive in [true, false].iter() {
for right_inclusive in [true, false].iter() {
match (left, right) {
(Some(left), Some(right)) if left == right && !(*left_inclusive && *right_inclusive) => {
assert!(rx_float_range(Some(left), Some(right), *left_inclusive, *right_inclusive).is_err());
}
_ => {
let rx = rx_float_range(left, right, *left_inclusive, *right_inclusive).unwrap();
do_test_float_range(&rx, left, right, *left_inclusive, *right_inclusive);
}
}
}
}
}
}
}

0 comments on commit 5c0705a

Please sign in to comment.