2017-08-22 19:57:30 -07:00
// Implementation of the math builtin.
# include "config.h" // IWYU pragma: keep
# include <errno.h>
# include <stddef.h>
# include <algorithm>
2018-03-07 18:01:12 +01:00
# include <cmath>
2018-07-24 00:00:06 -07:00
# include <limits>
2017-08-22 19:57:30 -07:00
# include <string>
2018-02-10 19:46:34 +01:00
# include "tinyexpr.h"
2017-08-22 19:57:30 -07:00
# include "builtin.h"
# include "builtin_math.h"
# include "common.h"
# include "fallback.h" // IWYU pragma: keep
# include "io.h"
# include "wgetopt.h"
# include "wutil.h" // IWYU pragma: keep
2018-07-24 00:00:06 -07:00
// The maximum number of points after the decimal that we'll print.
static constexpr int kDefaultScale = 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).
static constexpr double kMaximumContiguousInteger =
double ( 1LLU < < std : : numeric_limits < double > : : digits ) ;
2017-08-22 19:57:30 -07:00
struct math_cmd_opts_t {
bool print_help = false ;
2018-07-24 00:00:06 -07:00
int scale = kDefaultScale ;
2017-08-22 19:57:30 -07:00
} ;
// This command is atypical in using the "+" (REQUIRE_ORDER) option for flag parsing.
// This is needed because of the minus, `-`, operator in math expressions.
static const wchar_t * short_options = L " +:hs: " ;
static const struct woption long_options [ ] = { { L " scale " , required_argument , NULL , ' s ' } ,
{ L " help " , no_argument , NULL , ' h ' } ,
{ NULL , 0 , NULL , 0 } } ;
static int parse_cmd_opts ( math_cmd_opts_t & opts , int * optind , //!OCLINT(high ncss method)
int argc , wchar_t * * argv , parser_t & parser , io_streams_t & streams ) {
const wchar_t * cmd = L " math " ;
int opt ;
wgetopter_t w ;
while ( ( opt = w . wgetopt_long ( argc , argv , short_options , long_options , NULL ) ) ! = - 1 ) {
switch ( opt ) {
case ' s ' : {
opts . scale = fish_wcstoi ( w . woptarg ) ;
if ( errno | | opts . scale < 0 | | opts . scale > 15 ) {
streams . err . append_format ( _ ( L " %ls: '%ls' is not a valid scale value \n " ) , cmd ,
w . woptarg ) ;
return STATUS_INVALID_ARGS ;
}
break ;
}
case ' h ' : {
opts . print_help = true ;
break ;
}
case ' : ' : {
builtin_missing_argument ( parser , streams , cmd , argv [ w . woptind - 1 ] ) ;
return STATUS_INVALID_ARGS ;
}
case ' ? ' : {
// For most commands this is an error. We ignore it because a math expression
// can begin with a minus sign.
* optind = w . woptind - 1 ;
return STATUS_CMD_OK ;
}
default : {
DIE ( " unexpected retval from wgetopt_long " ) ;
break ;
}
}
}
* optind = w . woptind ;
return STATUS_CMD_OK ;
}
// We read from stdin if we are the second or later process in a pipeline.
static bool math_args_from_stdin ( const io_streams_t & streams ) {
return streams . stdin_is_directly_redirected ;
}
/// Get the arguments from stdin.
static const wchar_t * math_get_arg_stdin ( wcstring * storage , const io_streams_t & streams ) {
std : : string arg ;
for ( ; ; ) {
char ch = ' \0 ' ;
long rc = read_blocked ( streams . stdin_fd , & ch , 1 ) ;
if ( rc < 0 ) return NULL ; // failure
if ( rc = = 0 ) { // EOF
if ( arg . empty ( ) ) return NULL ;
break ;
}
if ( ch = = ' \n ' ) break ; // we're done
arg + = ch ;
}
* storage = str2wcstring ( arg ) ;
return storage - > c_str ( ) ;
}
/// Return the next argument from argv.
static const wchar_t * math_get_arg_argv ( int * argidx , wchar_t * * argv ) {
return argv & & argv [ * argidx ] ? argv [ ( * argidx ) + + ] : NULL ;
}
/// Get the arguments from argv or stdin based on the execution context. This mimics how builtin
/// `string` does it.
static const wchar_t * math_get_arg ( int * argidx , wchar_t * * argv , wcstring * storage ,
2017-08-23 20:38:40 -07:00
const io_streams_t & streams ) {
2017-08-22 19:57:30 -07:00
if ( math_args_from_stdin ( streams ) ) {
return math_get_arg_stdin ( storage , streams ) ;
}
return math_get_arg_argv ( argidx , argv ) ;
}
2018-03-01 11:24:16 -08:00
static wcstring math_describe_error ( te_error_t & error ) {
2018-02-26 14:23:21 +01:00
if ( error . position = = 0 ) return L " NO ERROR?!? " ;
2018-02-12 00:02:47 +01:00
assert ( error . type ! = TE_ERROR_NONE & & L " Error has no position " ) ;
switch ( error . type ) {
case TE_ERROR_UNKNOWN_VARIABLE : return _ ( L " Unknown variable " ) ;
case TE_ERROR_MISSING_CLOSING_PAREN : return _ ( L " Missing closing parenthesis " ) ;
case TE_ERROR_MISSING_OPENING_PAREN : return _ ( L " Missing opening parenthesis " ) ;
case TE_ERROR_TOO_FEW_ARGS : return _ ( L " Too few arguments " ) ;
case TE_ERROR_TOO_MANY_ARGS : return _ ( L " Too many arguments " ) ;
case TE_ERROR_MISSING_OPERATOR : return _ ( L " Missing operator " ) ;
2018-02-25 21:41:38 +01:00
case TE_ERROR_UNKNOWN : return _ ( L " Expression is bogus " ) ;
2018-02-26 14:23:21 +01:00
default : return L " Unknown error " ;
2018-02-12 00:02:47 +01:00
}
}
2018-07-24 00:00:06 -07:00
/// Return a formatted version of the value \p v respecting the given \p opts.
static wcstring format_double ( double v , const math_cmd_opts_t & opts ) {
wcstring ret = format_string ( L " %.*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.
const wchar_t * const digits = L " 0123456789 " ;
if ( ret . find_first_not_of ( digits ) ! = wcstring : : npos ) {
while ( ret . back ( ) = = L ' 0 ' ) {
ret . pop_back ( ) ;
}
if ( ! wcschr ( digits , ret . back ( ) ) ) {
ret . pop_back ( ) ;
}
}
// If we trimmed everything it must have just been zero.
if ( ret . empty ( ) ) {
ret . push_back ( L ' 0 ' ) ;
}
return ret ;
}
2017-08-23 20:38:40 -07:00
/// Evaluate math expressions.
2017-12-17 14:41:55 -08:00
static int evaluate_expression ( const wchar_t * cmd , parser_t & parser , io_streams_t & streams ,
2017-08-22 19:57:30 -07:00
math_cmd_opts_t & opts , wcstring & expression ) {
UNUSED ( parser ) ;
2018-02-26 11:25:07 +01:00
int retval = STATUS_CMD_OK ;
2018-02-12 00:02:47 +01:00
te_error_t error ;
2018-03-01 11:24:16 -08:00
std : : string narrow_str = wcs2string ( expression ) ;
2018-02-11 15:44:35 +01:00
// Switch locale while computing stuff.
// This means that the "." is always the radix character,
// so numbers work the same across locales.
char * saved_locale = strdup ( setlocale ( LC_NUMERIC , NULL ) ) ;
setlocale ( LC_NUMERIC , " C " ) ;
2018-03-01 11:24:16 -08:00
double v = te_interp ( narrow_str . c_str ( ) , & error ) ;
2018-02-11 15:44:35 +01:00
2018-02-12 00:02:47 +01:00
if ( error . position = = 0 ) {
2018-03-07 18:01:12 +01:00
// 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.
2018-07-24 00:00:06 -07:00
const char * error_message = NULL ;
2018-03-07 18:13:26 +01:00
if ( std : : isinf ( v ) ) {
2018-07-24 00:00:06 -07:00
error_message = " Result is infinite " ;
2018-03-07 18:01:12 +01:00
} else if ( std : : isnan ( v ) ) {
2018-07-24 00:00:06 -07:00
error_message = " Result is not a number " ;
} else if ( std : : abs ( v ) > = kMaximumContiguousInteger ) {
error_message = " Result magnitude is too large " ;
}
if ( error_message ) {
streams . err . append_format ( L " %ls: Error: %s \n " , cmd , error_message ) ;
2018-03-07 18:01:12 +01:00
streams . err . append_format ( L " '%ls' \n " , expression . c_str ( ) ) ;
retval = STATUS_CMD_ERROR ;
2017-12-17 14:41:55 -08:00
} else {
2018-07-24 00:00:06 -07:00
streams . out . append ( format_double ( v , opts ) ) ;
streams . out . push_back ( L ' \n ' ) ;
2017-12-17 14:41:55 -08:00
}
2018-02-10 19:46:34 +01:00
} else {
2018-02-15 15:00:11 +01:00
streams . err . append_format ( L " %ls: Error: %ls \n " , cmd , math_describe_error ( error ) . c_str ( ) ) ;
2018-02-12 00:02:47 +01:00
streams . err . append_format ( L " '%ls' \n " , expression . c_str ( ) ) ;
streams . err . append_format ( L " %*lc^ \n " , error . position - 1 , L ' ' ) ;
2018-02-26 11:25:07 +01:00
retval = STATUS_CMD_ERROR ;
2017-12-17 14:41:55 -08:00
}
2018-02-26 21:23:41 +01:00
setlocale ( LC_NUMERIC , saved_locale ) ;
2018-02-11 15:44:35 +01:00
free ( saved_locale ) ;
2018-02-26 11:25:07 +01:00
return retval ;
2017-08-22 19:57:30 -07:00
}
/// The math builtin evaluates math expressions.
int builtin_math ( parser_t & parser , io_streams_t & streams , wchar_t * * argv ) {
wchar_t * cmd = argv [ 0 ] ;
int argc = builtin_count_args ( argv ) ;
math_cmd_opts_t opts ;
int optind ;
// Is this really the right way to handle no expression present?
// if (argc == 0) return STATUS_CMD_OK;
int retval = parse_cmd_opts ( opts , & optind , argc , argv , parser , streams ) ;
if ( retval ! = STATUS_CMD_OK ) return retval ;
if ( opts . print_help ) {
builtin_print_help ( parser , streams , cmd , streams . out ) ;
return STATUS_CMD_OK ;
}
wcstring expression ;
wcstring storage ;
while ( const wchar_t * arg = math_get_arg ( & optind , argv , & storage , streams ) ) {
if ( ! expression . empty ( ) ) expression . push_back ( L ' ' ) ;
expression . append ( arg ) ;
}
2018-03-01 22:27:24 +01:00
if ( expression . empty ( ) ) {
streams . err . append_format ( BUILTIN_ERR_MIN_ARG_COUNT1 , L " math " , 1 , 0 ) ;
return STATUS_CMD_ERROR ;
}
2017-08-22 19:57:30 -07:00
return evaluate_expression ( cmd , parser , streams , opts , expression ) ;
}