feat(math): add round options (#9117)

Add round options, but I think can also add floor, ceiling, etc. And
the default mode is trunc.

Closes #9117

Co-authored-by: Mahmoud Al-Qudsi <mqudsi@neosmart.net>
This commit is contained in:
Looouiiis 2024-05-30 12:15:02 +08:00 committed by Mahmoud Al-Qudsi
parent f1ae170155
commit 480d48351c
4 changed files with 101 additions and 9 deletions

View file

@ -126,6 +126,7 @@ Scripting improvements
- Commas in command substitution output are no longer used as separators in brace expansion, preventing a surprising expansion in rare cases (:issue:`5048`).
- Universal variables can now store strings containing invalid Unicode codepoints (:issue:`10313`).
- ``path basename`` now takes a ``-E`` option that causes it to return the basename (i.e. "filename" with the directory prefix removed) with the final extension (if any) also removed. This takes the place of ``path change-extension "" (path basename $foo)`` (:issue:`10521`).
- ``math`` now adds ``--scale-mode`` parameter. You can choose between ``truncate``, ``round``, ``floor``, ``ceiling`` as you wish (default value is ``truncate``). (:issue:`9117`).
Interactive improvements
------------------------

View file

@ -8,7 +8,7 @@ Synopsis
.. synopsis::
math [(-s | --scale) N] [(-b | --base) BASE] EXPRESSION ...
math [(-s | --scale) N] [(-b | --base) BASE] [(-m | --scale-mode) MODE] EXPRESSION ...
Description
@ -19,7 +19,6 @@ It supports simple operations such as addition, subtraction, and so on, as well
By default, the output shows up to 6 decimal places.
To change the number of decimal places, use the ``--scale`` option, including ``--scale=0`` for integer output.
Trailing zeroes will always be trimmed.
Keep in mind that parameter expansion happens before expressions are evaluated.
This can be very useful in order to perform calculations involving shell variables or the output of command substitutions, but it also means that parenthesis (``()``) and the asterisk (``*``) glob character have to be escaped or quoted.
@ -37,8 +36,8 @@ The following options are available:
**-s** *N* or **--scale** *N*
Sets the scale of the result.
``N`` must be an integer or the word "max" for the maximum scale.
A scale of zero causes results to be truncated, not rounded. Any non-integer component is thrown away.
So ``3/2`` returns ``1`` rather than ``2`` which ``1.5`` would normally round to.
A scale of zero causes results to be truncated by default. Any non-integer component is thrown away.
So ``3/2`` returns ``1`` by default, rather than ``2`` which ``1.5`` would normally round to.
This is for compatibility with ``bc`` which was the basis for this command prior to fish 3.0.0.
Scale values greater than zero causes the result to be rounded using the usual rules to the specified number of decimal places.
@ -49,6 +48,11 @@ The following options are available:
Hex numbers will be printed with a ``0x`` prefix.
Octal numbers will have a prefix of ``0`` but aren't understood by ``math`` as input.
**-m** *MODE* or **--scale-mode** *MODE*
Sets scale behavior.
The ``MODE`` can be ``truncate``, ``round``, ``floor``, ``ceiling``.
The default value of scale mode is ``round`` with non zero scale and ``truncate`` with zero scale.
**-h** or **--help**
Displays help about using this command.

View file

@ -1,17 +1,31 @@
use num_traits::pow;
use widestring::utf32str;
use super::prelude::*;
use crate::tinyexpr::te_interp;
/// The maximum number of points after the decimal that we'll print.
const DEFAULT_SCALE: usize = 6;
const DEFAULT_ZERO_SCALE_MODE: ZeroScaleMode = ZeroScaleMode::Default;
/// The end of the range such that every integer is representable as a double.
/// i.e. this is the first value such that x + 1 == x (or == x + 2, depending on rounding mode).
const MAX_CONTIGUOUS_INTEGER: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
enum ZeroScaleMode {
Truncate,
Round,
Floor,
Ceiling,
Default,
}
struct Options {
print_help: bool,
scale: usize,
base: usize,
zero_scale_mode: ZeroScaleMode,
}
fn parse_cmd_opts(
@ -24,17 +38,19 @@ fn parse_cmd_opts(
// This command is atypical in using the "+" (REQUIRE_ORDER) option for flag parsing.
// This is needed because of the minus, `-`, operator in math expressions.
const SHORT_OPTS: &wstr = L!("+:hs:b:");
const SHORT_OPTS: &wstr = L!("+:hs:b:m:");
const LONG_OPTS: &[WOption] = &[
wopt(L!("scale"), ArgType::RequiredArgument, 's'),
wopt(L!("base"), ArgType::RequiredArgument, 'b'),
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("scale-mode"), ArgType::RequiredArgument, 'm'),
];
let mut opts = Options {
print_help: false,
scale: DEFAULT_SCALE,
base: 10,
zero_scale_mode: DEFAULT_ZERO_SCALE_MODE,
};
let mut have_scale = false;
@ -62,6 +78,23 @@ fn parse_cmd_opts(
opts.scale = scale as usize;
}
}
'm' => {
let optarg = w.woptarg.unwrap();
if optarg.eq(utf32str!("truncate")) {
opts.zero_scale_mode = ZeroScaleMode::Truncate;
} else if optarg.eq(utf32str!("round")) {
opts.zero_scale_mode = ZeroScaleMode::Round;
} else if optarg.eq(utf32str!("floor")) {
opts.zero_scale_mode = ZeroScaleMode::Floor;
} else if optarg.eq(utf32str!("ceiling")) {
opts.zero_scale_mode = ZeroScaleMode::Ceiling;
} else {
streams
.err
.append(wgettext_fmt!("%ls: %ls: invalid mode\n", cmd, optarg));
return Err(STATUS_INVALID_ARGS);
}
}
'b' => {
let optarg = w.woptarg.unwrap();
if optarg == "hex" {
@ -129,10 +162,26 @@ fn format_double(mut v: f64, opts: &Options) -> WString {
return sprintf!("%s0%lo", mneg, v.abs() as u64);
}
// As a special-case, a scale of 0 means to truncate to an integer
// instead of rounding.
if opts.scale == 0 {
v = v.trunc();
v *= pow(10f64, opts.scale);
v = match opts.zero_scale_mode {
ZeroScaleMode::Truncate => v.trunc(),
ZeroScaleMode::Round => v.round(),
ZeroScaleMode::Floor => v.floor(),
ZeroScaleMode::Ceiling => v.ceil(),
ZeroScaleMode::Default => {
if opts.scale == 0 {
v.trunc()
} else {
v
}
}
};
// if we don't add check here, the result of 'math -s 0 "22 / 5 - 5"' will be '0', not '-0'
if opts.scale != 0 {
v /= pow(10f64, opts.scale);
} else {
return sprintf!("%.*f", opts.scale, v);
}

View file

@ -379,3 +379,41 @@ math 0x0_2.0P-f
# CHECKERR: math: Error: Unexpected token
# CHECKERR: '0x0_2.0P-f'
# CHECKERR: ^
math "22 / 5 - 5"
# CHECK: -0.6
math -s 0 --scale-mode=truncate "22 / 5 - 5"
# CHECK: -0
math --scale=0 -m truncate "22 / 5 - 5"
# CHECK: -0
math -s 0 --scale-mode=floor "22 / 5 - 5"
# CHECK: -1
math -s 0 --scale-mode=round "22 / 5 - 5"
# CHECK: -1
math -s 0 --scale-mode=ceiling "22 / 5 - 5"
# CHECK: -0
math "1 / 3 - 1"
# CHECK: -0.666667
math --scale-mode=truncate "1 / 3 - 1"
# CHECK: -0.666666
math --scale-mode=floor "1 / 3 - 1"
# CHECK: {{-0.666667|-0.666668}}
math --scale-mode=floor "2 / 3 - 1"
# CHECK: {{-0.333334|-0.333335}}
math --scale-mode=round "1 / 3 - 1"
# CHECK: {{-0.666667|-0.666668}}
math --scale-mode=ceiling "1 / 3 - 1"
# CHECK: -0.666666
math --scale-mode=ceiling "2 / 3 - 1"
# CHECK: -0.333333
math -s 6 --scale-mode=truncate "1 / 3 - 1"
# CHECK: -0.666666
math -s 6 --scale-mode=floor "1 / 3 - 1"
# CHECK: {{-0.666667|-0.666668}}
math -s 6 --scale-mode=floor "2 / 3 - 1"
# CHECK: {{-0.333334|-0.333335}}
math -s 6 --scale-mode=round "1 / 3 - 1"
# CHECK: {{-0.666667|-0.666668}}
math -s 6 --scale-mode=ceiling "1 / 3 - 1"
# CHECK: -0.666666
math -s 6 --scale-mode=ceiling "2 / 3 - 1"
# CHECK: -0.333333