nushell/crates/nu-command/tests/commands/math/mod.rs
Ian Manske 9f09930834
Div, mod, and floor div overhaul (#14157)
# Description
Dividing two ints can currently return either an int or a float. Not
having a single return type for an operation between two types seems
problematic. Additionally, the type signature for division says that
dividing two ints returns only an int which does not match the current
implementation (it can also return a float). This PR changes division
between almost all types to return a float (except for `filesize /
number` or `duration / number`, since there are no float representations
for these types).

Currently, floor division between certain types is not implemented even
though the type signature allows it. Also, the current implementation of
floor division uses a combination of clamping and flooring rather than
simply performing floor division which this PR fixes. Additionally, the
signature was changed so that `int // float`, `float // int`, and `float
// float` now return float instead of int. This matches the automatic
float promotion in the rest of the operators (as well as how Python does
floor division which I think is the original inspiration).

Since regular division has always returned fractional values (and now
returns a float to reflect that), `mod` is now defined in terms of floor
division. That is, `D // d = q`, `D mod d = r`, and `D = d * q + r `.
This is just like the `%` operator in Python, which is also based off
floor division (at least for ints and floats). Additionally,
implementations missing from `mod`'s current type signature have been
added (`duration mod int` and `duration mod float`).

This PR also overhauls the overflow checking and errors for div, mod,
and floor div. If an operation overflows, it will now cause an error.

# User-Facing Changes
- Div now returns a float in most cases.
- Floor division now actually does floor division.
- Floor division now does automatic float promotion, returning a float
in more instances.
- Floor division now actually allows division with filesize and
durations as its type signature claimed.
- Mod is now defined and implemented in terms of floor division rather
than truncating division.
- Mod now actually allows filesize and durations as its type signature
claimed.
- Div, mod, and floor div now all have proper overflow checks.

## Examples

When the divisor and the dividend have the same sign, the quotient and
remainder will be the same as before. (Except that this PR will give
more accurate results, since it does not do an intermediate float
conversion). If the signs of the divisor and dividend are different,
then the results will be different, or rather actually correct.

Before:

```nu
let q = 8 // -3 # -3
let r = 8 mod -3 # 2
8 == $q * -3 + $r # false
```

After:

```nu
let q = 8 // -3 # -3
let r = 8 mod -3 # -1
8 == $q * -3 + $r # true
```


Before:

```nu
let q = -8 // 3 # -3
let r = -8 mod 3 # -2
-8 == $q * 3 + $r # false
```

After:

```nu
let q = -8 // 3 # -3
let r = -8 mod 3 # 1
-8 == $q * 3 + $r # true
```

# Tests + Formatting
Added a few tests.

# After Submitting
Probably update the docs.
2024-11-04 18:03:48 +01:00

547 lines
9.0 KiB
Rust

mod abs;
mod avg;
mod ceil;
mod floor;
mod log;
mod max;
mod median;
mod min;
mod mode;
mod product;
mod round;
mod sqrt;
mod stddev;
mod sum;
mod variance;
use nu_test_support::{nu, pipeline};
#[test]
fn one_arg() {
let actual = nu!(pipeline(
r#"
1
"#
));
assert_eq!(actual.out, "1");
}
#[test]
fn add() {
let actual = nu!(pipeline(
r#"
1 + 1
"#
));
assert_eq!(actual.out, "2");
}
#[test]
fn add_compound() {
let actual = nu!(pipeline(
r#"
1 + 2 + 2
"#
));
assert_eq!(actual.out, "5");
}
#[test]
fn precedence_of_operators() {
let actual = nu!(pipeline(
r#"
1 + 2 * 2
"#
));
assert_eq!(actual.out, "5");
}
#[test]
fn precedence_of_operators2() {
let actual = nu!(pipeline(
r#"
1 + 2 * 2 + 1
"#
));
assert_eq!(actual.out, "6");
}
#[test]
fn precedence_of_operators3() {
let actual = nu!(pipeline(
r#"
5 - 5 * 10 + 5
"#
));
assert_eq!(actual.out, "-40");
}
#[test]
fn precedence_of_operators4() {
let actual = nu!(pipeline(
r#"
5 - (5 * 10) + 5
"#
));
assert_eq!(actual.out, "-40");
}
#[test]
fn division_of_ints() {
let actual = nu!(pipeline(
r#"
4 / 2
"#
));
assert_eq!(actual.out, "2");
}
#[test]
fn division_of_ints2() {
let actual = nu!(pipeline(
r#"
1 / 4
"#
));
assert_eq!(actual.out, "0.25");
}
#[test]
fn error_zero_division_int_int() {
let actual = nu!(pipeline(
r#"
1 / 0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_division_float_int() {
let actual = nu!(pipeline(
r#"
1.0 / 0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_division_int_float() {
let actual = nu!(pipeline(
r#"
1 / 0.0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_division_float_float() {
let actual = nu!(pipeline(
r#"
1.0 / 0.0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn floor_division_of_ints() {
let actual = nu!(pipeline(
r#"
5 // 2
"#
));
assert_eq!(actual.out, "2");
}
#[test]
fn floor_division_of_ints2() {
let actual = nu!(pipeline(
r#"
-3 // 2
"#
));
assert_eq!(actual.out, "-2");
}
#[test]
fn floor_division_of_floats() {
let actual = nu!(pipeline(
r#"
-3.0 // 2.0
"#
));
assert_eq!(actual.out, "-2");
}
#[test]
fn error_zero_floor_division_int_int() {
let actual = nu!(pipeline(
r#"
1 // 0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_floor_division_float_int() {
let actual = nu!(pipeline(
r#"
1.0 // 0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_floor_division_int_float() {
let actual = nu!(pipeline(
r#"
1 // 0.0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn error_zero_floor_division_float_float() {
let actual = nu!(pipeline(
r#"
1.0 // 0.0
"#
));
assert!(actual.err.contains("division by zero"));
}
#[test]
fn proper_precedence_history() {
let actual = nu!(pipeline(
r#"
2 / 2 / 2 + 1
"#
));
assert_eq!(actual.out, "1.5");
}
#[test]
fn parens_precedence() {
let actual = nu!(pipeline(
r#"
4 * (6 - 3)
"#
));
assert_eq!(actual.out, "12");
}
#[test]
fn modulo() {
let actual = nu!(pipeline(
r#"
9 mod 2
"#
));
assert_eq!(actual.out, "1");
}
#[test]
fn floor_div_mod() {
let actual = nu!("let q = 8 // -3; let r = 8 mod -3; 8 == $q * -3 + $r");
assert_eq!(actual.out, "true");
let actual = nu!("let q = -8 // 3; let r = -8 mod 3; -8 == $q * 3 + $r");
assert_eq!(actual.out, "true");
}
#[test]
fn floor_div_mod_overflow() {
let actual = nu!(format!("{} // -1", i64::MIN));
assert!(actual.err.contains("overflow"));
let actual = nu!(format!("{} mod -1", i64::MIN));
assert!(actual.err.contains("overflow"));
}
#[test]
fn floor_div_mod_zero() {
let actual = nu!("1 // 0");
assert!(actual.err.contains("zero"));
let actual = nu!("1 mod 0");
assert!(actual.err.contains("zero"));
}
#[test]
fn floor_div_mod_large_num() {
let actual = nu!(format!("{} // {}", i64::MAX, i64::MAX / 2));
assert_eq!(actual.out, "2");
let actual = nu!(format!("{} mod {}", i64::MAX, i64::MAX / 2));
assert_eq!(actual.out, "1");
}
#[test]
fn unit_multiplication_math() {
let actual = nu!(pipeline(
r#"
1mb * 2
"#
));
assert_eq!(actual.out, "1.9 MiB");
}
#[test]
fn unit_multiplication_float_math() {
let actual = nu!(pipeline(
r#"
1mb * 1.2
"#
));
assert_eq!(actual.out, "1.1 MiB");
}
#[test]
fn unit_float_floor_division_math() {
let actual = nu!(pipeline(
r#"
1mb // 3.0
"#
));
assert_eq!(actual.out, "325.5 KiB");
}
#[test]
fn unit_division_math() {
let actual = nu!(pipeline(
r#"
1mb / 4
"#
));
assert_eq!(actual.out, "244.1 KiB");
}
#[test]
fn unit_float_division_math() {
let actual = nu!(pipeline(
r#"
1mb / 3.1
"#
));
assert_eq!(actual.out, "315.0 KiB");
}
#[test]
fn duration_math() {
let actual = nu!(pipeline(
r#"
1wk + 1day
"#
));
assert_eq!(actual.out, "1wk 1day");
}
#[test]
fn duration_decimal_math() {
let actual = nu!(pipeline(
r#"
5.5day + 0.5day
"#
));
assert_eq!(actual.out, "6day");
}
#[test]
fn duration_math_with_nanoseconds() {
let actual = nu!(pipeline(
r#"
1wk + 10ns
"#
));
assert_eq!(actual.out, "1wk 10ns");
}
#[test]
fn duration_decimal_math_with_nanoseconds() {
let actual = nu!(pipeline(
r#"
1.5wk + 10ns
"#
));
assert_eq!(actual.out, "1wk 3day 10ns");
}
#[test]
fn duration_decimal_math_with_all_units() {
let actual = nu!(pipeline(
r#"
1wk + 3day + 8hr + 10min + 16sec + 121ms + 11us + 12ns
"#
));
assert_eq!(actual.out, "1wk 3day 8hr 10min 16sec 121ms 11µs 12ns");
}
#[test]
fn duration_decimal_dans_test() {
let actual = nu!(pipeline(
r#"
3.14sec
"#
));
assert_eq!(actual.out, "3sec 140ms");
}
#[test]
fn duration_math_with_negative() {
let actual = nu!(pipeline(
r#"
1day - 1wk
"#
));
assert_eq!(actual.out, "-6day");
}
#[test]
fn compound_comparison() {
let actual = nu!(pipeline(
r#"
4 > 3 and 2 > 1
"#
));
assert_eq!(actual.out, "true");
}
#[test]
fn compound_comparison2() {
let actual = nu!(pipeline(
r#"
4 < 3 or 2 > 1
"#
));
assert_eq!(actual.out, "true");
}
#[test]
fn compound_where() {
let actual = nu!(pipeline(
r#"
echo '[{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 2, "b": 2}]' | from json | where a == 2 and b == 1 | to json -r
"#
));
assert_eq!(actual.out, r#"[{"a":2,"b":1}]"#);
}
#[test]
fn compound_where_paren() {
let actual = nu!(pipeline(
r#"
echo '[{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 2, "b": 2}]' | from json | where ($it.a == 2 and $it.b == 1) or $it.b == 2 | to json -r
"#
));
assert_eq!(actual.out, r#"[{"a":2,"b":1},{"a":2,"b":2}]"#);
}
// TODO: these ++ tests are not really testing *math* functionality, maybe find another place for them
#[test]
fn adding_lists() {
let actual = nu!(pipeline(
r#"
[1 3] ++ [5 6] | to nuon
"#
));
assert_eq!(actual.out, "[1, 3, 5, 6]");
}
#[test]
fn adding_list_and_value() {
let actual = nu!(pipeline(
r#"
[1 3] ++ 5 | to nuon
"#
));
assert_eq!(actual.out, "[1, 3, 5]");
}
#[test]
fn adding_value_and_list() {
let actual = nu!(pipeline(
r#"
1 ++ [3 5] | to nuon
"#
));
assert_eq!(actual.out, "[1, 3, 5]");
}
#[test]
fn adding_tables() {
let actual = nu!(pipeline(
r#"
[[a b]; [1 2]] ++ [[c d]; [10 11]] | to nuon
"#
));
assert_eq!(actual.out, "[{a: 1, b: 2}, {c: 10, d: 11}]");
}
#[test]
fn append_strings() {
let actual = nu!(pipeline(
r#"
"foo" ++ "bar"
"#
));
assert_eq!(actual.out, "foobar");
}
#[test]
fn append_binary_values() {
let actual = nu!(pipeline(
r#"
0x[01 02] ++ 0x[03 04] | to nuon
"#
));
assert_eq!(actual.out, "0x[01020304]");
}