fish-shell/src/builtins/math.rs
Fabian Boehm 0a92d03498 Remove L! from sprintf calls
Remove unnecessary L!
2024-01-13 08:52:54 +01:00

249 lines
8.1 KiB
Rust

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;
/// 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;
struct Options {
print_help: bool,
scale: usize,
base: usize,
}
fn parse_cmd_opts(
args: &mut [&wstr],
parser: &Parser,
streams: &mut IoStreams,
) -> Result<(Options, usize), Option<c_int>> {
const cmd: &wstr = L!("math");
let print_hints = true;
// 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 LONG_OPTS: &[woption] = &[
wopt(L!("scale"), woption_argument_t::required_argument, 's'),
wopt(L!("base"), woption_argument_t::required_argument, 'b'),
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
];
let mut opts = Options {
print_help: false,
scale: DEFAULT_SCALE,
base: 10,
};
let mut have_scale = false;
let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.wgetopt_long() {
match c {
's' => {
let optarg = w.woptarg.unwrap();
have_scale = true;
// "max" is the special value that tells us to pick the maximum scale.
if optarg == "max" {
opts.scale = 15;
} else {
let scale = fish_wcstoi(optarg);
if scale.is_err() || scale.unwrap() < 0 || scale.unwrap() > 15 {
streams.err.append(wgettext_fmt!(
"%ls: %ls: invalid base value\n",
cmd,
optarg
));
return Err(STATUS_INVALID_ARGS);
}
// We know the value is in the range [0, 15]
opts.scale = scale.unwrap() as usize;
}
}
'b' => {
let optarg = w.woptarg.unwrap();
if optarg == "hex" {
opts.base = 16;
} else if optarg == "octal" {
opts.base = 8;
} else {
let base = fish_wcstoi(optarg);
if base.is_err() || (base.unwrap() != 8 && base.unwrap() != 16) {
streams.err.append(wgettext_fmt!(
"%ls: %ls: invalid base value\n",
cmd,
optarg
));
return Err(STATUS_INVALID_ARGS);
}
// We know the value is 8 or 16.
opts.base = base.unwrap() as usize;
}
}
'h' => {
opts.print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], print_hints);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
// For most commands this is an error. We ignore it because a math expression
// can begin with a minus sign.
return Ok((opts, w.woptind - 1));
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
if have_scale && opts.scale != 0 && opts.base != 10 {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
"non-zero scale value only valid
for base 10"
));
return Err(STATUS_INVALID_ARGS);
}
Ok((opts, w.woptind))
}
/// Return a formatted version of the value `v` respecting the given `opts`.
fn format_double(mut v: f64, opts: &Options) -> WString {
if opts.base == 16 {
v = v.trunc();
let mneg = if v.is_sign_negative() { "-" } else { "" };
return sprintf!("%s0x%lx", mneg, v.abs() as u64);
} else if opts.base == 8 {
v = v.trunc();
if v == 0.0 {
// not 00
return WString::from_str("0");
}
let mneg = if v.is_sign_negative() { "-" } else { "" };
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();
return sprintf!("%.*f", opts.scale, v);
}
let mut ret = sprintf!("%.*f", opts.scale, v);
// If we contain a decimal separator, trim trailing zeros after it, and then the separator
// itself if there's nothing after it. Detect a decimal separator as a non-digit.
if ret.chars().any(|c| !c.is_ascii_digit()) {
let trailing_zeroes = ret.chars().rev().take_while(|&c| c == '0').count();
let mut to_keep = ret.len() - trailing_zeroes;
if ret.as_char_slice()[to_keep - 1] == '.' {
to_keep -= 1;
}
ret.truncate(to_keep);
}
// If we trimmed everything it must have just been zero.
// TODO: can this ever happen?
if ret.is_empty() {
ret.push('0');
}
ret
}
fn evaluate_expression(
cmd: &wstr,
streams: &mut IoStreams,
opts: &Options,
expression: &wstr,
) -> Option<c_int> {
let ret = te_interp(expression);
match ret {
Ok(n) => {
// Check some runtime errors after the fact.
// TODO: Really, this should be done in tinyexpr
// (e.g. infinite is the result of "x / 0"),
// but that's much more work.
let error_message = if n.is_infinite() {
L!("Result is infinite")
} else if n.is_nan() {
L!("Result is not a number")
} else if n.abs() >= MAX_CONTIGUOUS_INTEGER {
L!("Result magnitude is too large")
} else {
let mut s = format_double(n, opts);
s.push('\n');
streams.out.append(s);
return STATUS_CMD_OK;
};
streams
.err
.append(sprintf!("%ls: Error: %ls\n", cmd, error_message));
streams.err.append(sprintf!("'%ls'\n", expression));
STATUS_CMD_ERROR
}
Err(err) => {
streams.err.append(sprintf!(
L!("%ls: Error: %ls\n"),
cmd,
err.kind.describe_wstr()
));
streams.err.append(sprintf!("'%ls'\n", expression));
let padding = WString::from_chars(vec![' '; err.position + 1]);
if err.len >= 2 {
let tildes = WString::from_chars(vec!['~'; err.len - 2]);
streams.err.append(sprintf!("%ls^%ls^\n", padding, tildes));
} else {
streams.err.append(sprintf!("%ls^\n", padding));
}
STATUS_CMD_ERROR
}
}
}
/// How much math reads at one. We don't expect very long input.
const MATH_CHUNK_SIZE: usize = 1024;
/// The math builtin evaluates math expressions.
pub fn math(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option<c_int> {
let cmd = argv[0];
let (opts, mut optind) = match parse_cmd_opts(argv, parser, streams) {
Ok(x) => x,
Err(e) => return e,
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let mut expression = WString::new();
for (arg, _) in Arguments::new(argv, &mut optind, streams, MATH_CHUNK_SIZE) {
if !expression.is_empty() {
expression.push(' ')
}
expression.push_utfstr(&arg);
}
if expression.is_empty() {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, 0));
return STATUS_CMD_ERROR;
}
evaluate_expression(cmd, streams, &opts, &expression)
}