Round/truncate digit in string in zsh (or with external tool)

bcmathzsh

I'm trying to do an interface to bc so it can be used intuitively and without the annoyance of getting "stuck" in it. I haven't got around to test it that much, because I got stuck on another detail, namely how to present the result (which is, I think, a string).

Rounding or truncating does not matter, either one is fine. Take a look below, and you'll understand immediately. I use zsh but an external tool will be just fine as I won't use this in any time or otherwise critical context, it's just a desktop tool.

calc () {
    result=`bc <<EOF
    scale=3;
    $@
    EOF`
    echo ${result//%0/} # doesn't work; will only remove one zero
                        # also, if there are only zeroes, should
                        # remove dot as well - what about .333, etc.?
}

Edit

I'm very impressed by the below solution, especially how the noglob gets away with the quotes!

But, the use of a dot to force floating point calculation is something I'll never remember (you don't use a normal calculator like that). And it is even a bit risky, especially for calculations when it's not obvious that floating point would yield an altogether different result (most likely the one you wanted).

Also, the calculations below show some un-pretty output (the too long real, and the trailing dot).

Perhaps I should combine this (some of it) with the output formatting of @Gille's answer below? When I get it to work perfectly, I'll post the result here. (Edit: The accepted answer works great. Be sure to read the comments to that answer, as well.)

calc () {
  echo $(($*));
}
alias calc='noglob calc'

calc 1./3
0.33333333333333331
calc 7.5 - 2.5
5.

Best Answer

Using zsh's own arithmetic, you could do:

calc() printf '%.6g\n' $(($*))
alias 'calc=noglob calc'

But that would mean you'd need to enter numbers as 123. for them to be taken as floating point and trigger a floating point calculation.

You could work around that by appending . to any sequence of decimal digits that is not otherwise part of a hex number (or number in another base) or of a variable name or 12e-20 type numbers like:

setopt extendedglob
calc() printf '%.6g\n' $((${*//(#bm)(([0-9.]##[eE][-+][0-9]##|[[:alnum:]_#]#[.#_[:alpha:]][[:alnum:]_#]#)|([0-9]##))/$MATCH${match[3]:+.}}))
alias 'calc=noglob calc'

By which time you may think it easier to use bc and trim the trailing 0s.

See also awk:

calc() awk "BEGIN{print $*}"

which supports fewer operators and math functions but might be enough for you.

Related Question