3

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.
4
  • What about using zcalc (autoload zcalc; zcalc, info -f zsh --index-search=zcal)? Commented Mar 2, 2013 at 8:49
  • @StephaneChazelas: With zcalc, I get 0 for 1/3. Also, do you know a way to send the termination :q (or Ret keystroke) from the command line? I'd like to use it in the same way as the bc example above: e.g., calc "2*3" gets you 6 and the program terminates. Commented Mar 3, 2013 at 20:49
  • You can do 1./3 to force floating point arithmetics calc() echo $(($*)); alias 'calc=noglob calc'. Then: calc 2. / 3 * 5 Commented Mar 3, 2013 at 20:59
  • @StephaneChazelas: Wow! Made an edit. Commented Mar 3, 2013 at 21:22

5 Answers 5

5

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.

5
  • No, this is exactly what I want! It doesn't matter that it is complicated as I only have to put it in .zshrc once. And it is appealing to use the shell's calculator, rather than bc, although I never thought about that possibility when I first posted. Also, I have to say I'm surprised it took such a "hackish" solution --- one would think, presenting numerical data in a way pleasant for humans, that would be a tool almost from the infancy of computing! Anyway, this is great! Commented Mar 4, 2013 at 0:59
  • For any potential future users of this great hack/tool: multiplication isn't implicit on parenthesis, and, instead of ^, use **. When you know that, I guess it is not really a big thing, but if Mr. Chazelas finds joy in perfecting his creation, that would be a way to make it even more intuitive. For example, then you could get the utilization bound for 14 multi-CPUs with calc 14(2^(1/14) - 1). Am I disturbed, or wouldn't that be so cool? :) Commented Aug 7, 2013 at 3:02
  • @EmanuelBerg, well I'm quite used to ^ being XOR and ** being pow, but if you prefer ^ being pow, you can always add a ${ //\^/**} in there. Commented Aug 7, 2013 at 5:37
  • @EmanuelBerg, implicit multiplication conflicts with math function calls where variables may be use (cos(x) vs cos*(x)). Commented Aug 7, 2013 at 6:05
  • Aha! Get it. Well, most important, it is documented. Commented Aug 8, 2013 at 21:00
2

If I understood correctly, you want to delete trailing zeroes and a trailing dot. In that case, if EXTENDED_GLOB is set, you can use

${result//%.#0##/}

That is: at the end of the string (%) match zero or more dots (.#) followed by one or more zeroes (0##).

But this will return "" if result is 0. You can do another substitution around the first one, to restore the return value to 0:

${${result//%.#0##/}:-0}
4
  • Cool, will check this out bit by bit, might get back to you tomorrow. If you enjoyed this exercise, does it work for .3333 (etc.) to .33 as well? (And, .6666, etc.) :) But, if you feel you're done, removing zeroes and the dot is fine; I'll accept it as soon as I learned how you did it, and tested it. Commented Mar 1, 2013 at 23:47
  • Also (don't miss my first comment to your answer), you indicated it wasn't 100% clear what I intended, but now once you've got it, do you know how to put it in "math"? I actually studied math at a high level but I suppose I was a poor student because I don't know how to put it to precisely tell what I'm after, that's why I included all the examples. Commented Mar 1, 2013 at 23:51
  • I don't really get what you want to do with .3333. Just delete all decimal digits after the second?
    – angus
    Commented Mar 1, 2013 at 23:55
  • Yes, that would be great! Commented Mar 2, 2013 at 0:50
2

bc can either print results as long integers or as decimals. Here's a script that joins long integers split into multiple lines together, and removes trailing zeroes after the decimal points in decimals.

calc () {
  emulate -L zsh; setopt extended_glob
  local line
  bc <<EOF |
scale=3
$@
EOF
    while read line; do
      if [[ $line = *.* ]]; then
        print -r -- ${${${line%%0##}/#%0#./0}%.}
      else
        print -r -- $line
      fi
    done
}

The way it's written is more of an exercise in text manipulation with parameter substitutions than a really clear way of pretty-printing decimals.

  • ${…%%0##} removes the longest suffix matching 0##, i.e. trailing zeroes.
  • ${…/#%0#./0} sets the string to 0 if it consists solely (#% prefix to the pattern in ${VAR/PATTERN/REPLACEMENT}) of optional leading zeroes (0#) and .
  • ${…%.} strips off a trailing . if any.

I think splitting the steps is clearer.

if [[ $line = *.* ]]; then line=${line%%0##}; fi
if [[ $line = . ]]; then line=0; else line=${line%.}
print -r -- $line
1

Try zcalc in zsh; if you're not already autoloading all the functions that come with zsh, you'll need to autoload zcalc first. Does away with bc, has prompting, $output back-references, command-history, scientific functions, the ability to define more functions, etc.

Documented in zshcontrib(1).

Downside: still has the "ints by default" issue, thus 3./5 != 3/5

1
  • Yes, check out this answer above. Commented Mar 4, 2013 at 23:44
1

We can use the same technique as asm.js to do this without string manipulation or subprocesses:

echo $((123.45|0))  # prints 123

It takes advantage of the fact that certain operators, such as bitwise-or, can only be performed on integers.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .