Fix range contains (#14011)

# Description

This PR changes the range contains logic to take the step into account. 

```nushell
# before
2 in 1..3.. # true

# now
2 in 1..3.. # false
```

---

I encountered another issue while adding tests. Due to floating point
precision, `2.1 in 1..1.1..3` will return `false`. The floating point
error is even bigger than `f64::EPSILON` (`0.09999999999999876` vs
`2.220446049250313e-16`). This issue disappears with bigger numbers.

I tried a different algorithm (checking if the estimated number of steps
is close enough to any integer) but the results are still pretty bad:

```rust
let n_steps = (value - self.start) / self.step; // 14.999999999999988
(n_steps - n_steps.round()).abs() < f64::EPSILON // returns false
```

Maybe it can be shipped like this, the REPL already has floating point
errors (`1.1 - 1` returns `0.10000000000000009`). Or maybe there's a way
to fix this that I didn't think of. I'm open to ideas! But in any case
performing this kind of checks on a range of floats seems more niche
than doing it on a range of ints.

# User-Facing Changes

Code that depended on this behavior to check if a number is between
`start` and `end` will potentially return a different value.

# Tests + Formatting

# After Submitting
This commit is contained in:
Joaquín Triñanes 2024-10-22 17:34:41 +02:00 committed by GitHub
parent 4968b6b9d0
commit f738932bbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 25 deletions

View File

@ -95,20 +95,27 @@ mod int_range {
pub fn contains(&self, value: i64) -> bool { pub fn contains(&self, value: i64) -> bool {
if self.step < 0 { if self.step < 0 {
value <= self.start // Decreasing range
&& match self.end { if value > self.start {
Bound::Included(end) => value >= end, return false;
Bound::Excluded(end) => value > end, }
Bound::Unbounded => true, match self.end {
} Bound::Included(end) if value < end => return false,
Bound::Excluded(end) if value <= end => return false,
_ => {}
};
} else { } else {
self.start <= value // Increasing range
&& match self.end { if value < self.start {
Bound::Included(end) => value <= end, return false;
Bound::Excluded(end) => value < end, }
Bound::Unbounded => true, match self.end {
} Bound::Included(end) if value > end => return false,
Bound::Excluded(end) if value >= end => return false,
_ => {}
};
} }
(value - self.start) % self.step == 0
} }
pub fn into_range_iter(self, signals: Signals) -> Iter { pub fn into_range_iter(self, signals: Signals) -> Iter {
@ -330,20 +337,27 @@ mod float_range {
pub fn contains(&self, value: f64) -> bool { pub fn contains(&self, value: f64) -> bool {
if self.step < 0.0 { if self.step < 0.0 {
value <= self.start // Decreasing range
&& match self.end { if value > self.start {
Bound::Included(end) => value >= end, return false;
Bound::Excluded(end) => value > end, }
Bound::Unbounded => true, match self.end {
} Bound::Included(end) if value <= end => return false,
Bound::Excluded(end) if value < end => return false,
_ => {}
};
} else { } else {
self.start <= value // Increasing range
&& match self.end { if value < self.start {
Bound::Included(end) => value <= end, return false;
Bound::Excluded(end) => value < end, }
Bound::Unbounded => true, match self.end {
} Bound::Included(end) if value >= end => return false,
Bound::Excluded(end) if value > end => return false,
_ => {}
};
} }
((value - self.start) % self.step).abs() < f64::EPSILON
} }
pub fn into_range_iter(self, signals: Signals) -> Iter { pub fn into_range_iter(self, signals: Signals) -> Iter {

View File

@ -7,7 +7,7 @@ fn int_in_inc_range() -> TestResult {
#[test] #[test]
fn int_in_dec_range() -> TestResult { fn int_in_dec_range() -> TestResult {
run_test(r#"1 in 9.42..-4"#, "true") run_test(r#"1 in 9..-4.42"#, "true")
} }
#[test] #[test]
@ -15,6 +15,16 @@ fn int_in_exclusive_range() -> TestResult {
run_test(r#"3 in 0..<3"#, "false") run_test(r#"3 in 0..<3"#, "false")
} }
#[test]
fn float_in_inc_range() -> TestResult {
run_test(r#"1.58 in -4.42..9"#, "true")
}
#[test]
fn float_in_dec_range() -> TestResult {
run_test(r#"1.42 in 9.42..-4.42"#, "true")
}
#[test] #[test]
fn non_number_in_range() -> TestResult { fn non_number_in_range() -> TestResult {
fail_test(r#"'a' in 1..3"#, "subset comparison is not supported") fail_test(r#"'a' in 1..3"#, "subset comparison is not supported")
@ -34,3 +44,33 @@ fn range_and_reduction() -> TestResult {
fn zip_ranges() -> TestResult { fn zip_ranges() -> TestResult {
run_test(r#"1..3 | zip 4..6 | get 2.1"#, "6") run_test(r#"1..3 | zip 4..6 | get 2.1"#, "6")
} }
#[test]
fn int_in_stepped_range() -> TestResult {
run_test(r#"7 in 1..3..15"#, "true")
}
#[test]
fn int_in_unbounded_stepped_range() -> TestResult {
run_test(r#"1000001 in 1..3.."#, "true")
}
#[test]
fn int_not_in_unbounded_stepped_range() -> TestResult {
run_test(r#"2 in 1..3.."#, "false")
}
#[test]
fn float_in_stepped_range() -> TestResult {
run_test(r#"5.5 in 1..1.5..10"#, "true")
}
#[test]
fn float_in_unbounded_stepped_range() -> TestResult {
run_test(r#"100.5 in 1..1.5.."#, "true")
}
#[test]
fn float_not_in_unbounded_stepped_range() -> TestResult {
run_test(r#"2.1 in 1.2..3.."#, "false")
}