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`). - 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`). - 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`). - ``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 Interactive improvements
------------------------ ------------------------

View File

@ -8,7 +8,7 @@ Synopsis
.. synopsis:: .. synopsis::
math [(-s | --scale) N] [(-b | --base) BASE] EXPRESSION ... math [(-s | --scale) N] [(-b | --base) BASE] [(-m | --scale-mode) MODE] EXPRESSION ...
Description 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. 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. 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. 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. 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* **-s** *N* or **--scale** *N*
Sets the scale of the result. Sets the scale of the result.
``N`` must be an integer or the word "max" for the maximum scale. ``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. A scale of zero causes results to be truncated by default. Any non-integer component is thrown away.
So ``3/2`` returns ``1`` rather than ``2`` which ``1.5`` would normally round to. 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. 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. 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. 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. 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** **-h** or **--help**
Displays help about using this command. Displays help about using this command.

View File

@ -1,17 +1,31 @@
use num_traits::pow;
use widestring::utf32str;
use super::prelude::*; use super::prelude::*;
use crate::tinyexpr::te_interp; use crate::tinyexpr::te_interp;
/// The maximum number of points after the decimal that we'll print. /// The maximum number of points after the decimal that we'll print.
const DEFAULT_SCALE: usize = 6; 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. /// 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). /// 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; const MAX_CONTIGUOUS_INTEGER: f64 = (1_u64 << f64::MANTISSA_DIGITS) as f64;
enum ZeroScaleMode {
Truncate,
Round,
Floor,
Ceiling,
Default,
}
struct Options { struct Options {
print_help: bool, print_help: bool,
scale: usize, scale: usize,
base: usize, base: usize,
zero_scale_mode: ZeroScaleMode,
} }
fn parse_cmd_opts( 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 command is atypical in using the "+" (REQUIRE_ORDER) option for flag parsing.
// This is needed because of the minus, `-`, operator in math expressions. // 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] = &[ const LONG_OPTS: &[WOption] = &[
wopt(L!("scale"), ArgType::RequiredArgument, 's'), wopt(L!("scale"), ArgType::RequiredArgument, 's'),
wopt(L!("base"), ArgType::RequiredArgument, 'b'), wopt(L!("base"), ArgType::RequiredArgument, 'b'),
wopt(L!("help"), ArgType::NoArgument, 'h'), wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("scale-mode"), ArgType::RequiredArgument, 'm'),
]; ];
let mut opts = Options { let mut opts = Options {
print_help: false, print_help: false,
scale: DEFAULT_SCALE, scale: DEFAULT_SCALE,
base: 10, base: 10,
zero_scale_mode: DEFAULT_ZERO_SCALE_MODE,
}; };
let mut have_scale = false; let mut have_scale = false;
@ -62,6 +78,23 @@ fn parse_cmd_opts(
opts.scale = scale as usize; 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' => { 'b' => {
let optarg = w.woptarg.unwrap(); let optarg = w.woptarg.unwrap();
if optarg == "hex" { 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); return sprintf!("%s0%lo", mneg, v.abs() as u64);
} }
// As a special-case, a scale of 0 means to truncate to an integer v *= pow(10f64, opts.scale);
// instead of rounding.
if opts.scale == 0 { v = match opts.zero_scale_mode {
v = v.trunc(); 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); return sprintf!("%.*f", opts.scale, v);
} }

View File

@ -379,3 +379,41 @@ math 0x0_2.0P-f
# CHECKERR: math: Error: Unexpected token # CHECKERR: math: Error: Unexpected token
# CHECKERR: '0x0_2.0P-f' # CHECKERR: '0x0_2.0P-f'
# CHECKERR: ^ # 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