1#!/bin/zsh -i
2#
3# Zsh calculator.  Understands most ordinary arithmetic expressions.
4# Line editing and history are available. A blank line or `q' quits.
5#
6# Runs as a script or a function.  If used as a function, the history
7# is remembered for reuse in a later call (and also currently in the
8# shell's own history).  There are various problems using this as a
9# script, so a function is recommended.
10#
11# The prompt shows a number for the current line.  The corresponding
12# result can be referred to with $<line-no>, e.g.
13#   1> 32 + 10
14#   42
15#   2> $1 ** 2
16#   1764
17# The set of remembered numbers is primed with anything given on the
18# command line.  For example,
19#   zcalc '2 * 16'
20#   1> 32                     # printed by function
21#   2> $1 + 2                 # typed by user
22#   34
23#   3> 
24# Here, 32 is stored as $1.  This works in the obvious way for any
25# number of arguments.
26#
27# If the mathfunc library is available, probably understands most system
28# mathematical functions.  The left parenthesis must be adjacent to the
29# end of the function name, to distinguish from shell parameters
30# (translation: to prevent the maintainers from having to write proper
31# lookahead parsing).  For example,
32#   1> sqrt(2)
33#   1.4142135623730951
34# is right, but `sqrt (2)' will give you an error.
35#
36# You can do things with parameters like
37#   1> pi = 4.0 * atan(1)
38# too.  These go into global parameters, so be careful.  You can declare
39# local variables, however:
40#   1> local pi
41# but note this can't appear on the same line as a calculation.  Don't
42# use the variables listed in the `local' and `integer' lines below
43# (translation: I can't be bothered to provide a sandbox).
44#
45# You can declare or delete math functions (implemented via zmathfuncdef):
46#   1> function cube $1 * $1 * $1
47# This has a single compulsory argument.  Note the function takes care of
48# the punctuation.  To delete the function, put nothing (at all) after
49# the function name:
50#   1> function cube
51#
52# Some constants are already available: (case sensitive as always):
53#   PI     pi, i.e. 3.1415926545897931
54#   E      e, i.e. 2.7182818284590455
55#
56# You can also change the output base.
57#   1> [#16]
58#   1>
59# Changes the default output to hexadecimal with numbers preceded by `16#'.
60# Note the line isn't remembered.
61#   2> [##16]
62#   2>
63# Change the default output base to hexadecimal with no prefix.
64#   3> [#]
65# Reset the default output base.
66#
67# This is based on the builtin feature that you can change the output base
68# of a given expression.  For example,
69#   1> [##16]  32 + 20 / 2
70#   2A
71#   2> 
72# prints the result of the calculation in hexadecimal.
73#
74# You can't change the default input base, but the shell allows any small
75# integer as a base:
76#   1> 2#1111
77#   15
78#   2> [##13] 13#6 * 13#9
79#   42
80# and the standard C-like notation with a leading 0x for hexadecimal is
81# also understood.  However, leading 0 for octal is not understood --- it's
82# too confusing in a calculator.  Use 8#777 etc.
83#
84# Options: -#<base> is the same as a line containing just `[#<base>],
85# similarly -##<base>; they set the default output base, with and without
86# a base discriminator in front, respectively.
87#
88# With the option -e, the arguments are evaluated as if entered
89# interactively.  So, for example:
90#   zcalc -e -\#16 -e 1055
91# prints
92#   0x41f
93# Any number of expressions may be given and they are evaluated
94# sequentially just as if read automatically.
95
96emulate -L zsh
97setopt extendedglob
98
99# TODO: make local variables that shouldn't be visible in expressions
100# begin with _.
101local line ans base defbase forms match mbegin mend psvar optlist opt arg
102local compcontext="-zcalc-line-"
103integer num outdigits outform=1 expression_mode
104local -a expressions
105
106# We use our own history file with an automatic pop on exit.
107history -ap "${ZDOTDIR:-$HOME}/.zcalc_history"
108
109forms=( '%2$g' '%.*g' '%.*f' '%.*E' '')
110
111zmodload -i zsh/mathfunc 2>/dev/null
112autoload -Uz zmathfuncdef
113
114: ${ZCALCPROMPT="%1v> "}
115
116# Supply some constants.
117float PI E
118(( PI = 4 * atan(1), E = exp(1) ))
119
120# Process command line
121while [[ -n $1 && $1 = -(|[#-]*|f|e) ]]; do
122  optlist=${1[2,-1]}
123  shift
124  [[ $optlist = (|-) ]] && break
125  while [[ -n $optlist ]]; do
126    opt=${optlist[1]}
127    optlist=${optlist[2,-1]}
128    case $opt in
129      ('#') # Default base
130            if [[ -n $optlist ]]; then
131	       arg=$optlist
132	       optlist=
133	    elif [[ -n $1 ]]; then
134	       arg=$1
135	       shift
136	    else
137	       print -- "-# requires an argument" >&2
138	       return 1
139	    fi
140	    if [[ $arg != (|\#)[[:digit:]]## ]]; then
141	      print -- "-# requires a decimal number as an argument" >&2
142	      return 1
143	    fi
144            defbase="[#${arg}]"
145	    ;;
146	(f) # Force floating point operation
147	    setopt forcefloat
148	    ;;
149        (e) # Arguments are expressions
150	    (( expression_mode = 1 ));
151	    ;;
152    esac
153  done
154done
155
156if (( expression_mode )); then
157  expressions=("$@")
158  argv=()
159fi
160
161for (( num = 1; num <= $#; num++ )); do
162  # Make sure all arguments have been evaluated.
163  # The `$' before the second argv forces string rather than numeric
164  # substitution.
165  (( argv[$num] = $argv[$num] ))
166  print "$num> $argv[$num]"
167done
168
169psvar[1]=$num
170local prev_line cont_prompt
171while (( expression_mode )) ||
172  vared -cehp "${cont_prompt}${ZCALCPROMPT}" line; do
173  if (( expression_mode )); then
174    (( ${#expressions} )) || break
175    line=$expressions[1]
176    shift expressions
177  fi
178  if [[ $line = (|*[^\\])('\\')#'\' ]]; then
179    prev_line+=$line[1,-2]
180    cont_prompt="..."
181    line=
182    continue
183  fi
184  line="$prev_line$line"
185  prev_line=
186  cont_prompt=
187  # Test whether there are as many open as close
188  # parentheses in the line so far.
189  if [[ ${#line//[^\(]} -gt ${#line//[^\)]} ]]; then
190      prev_line+=$line
191      cont_prompt="..."
192      line=
193      continue
194  fi
195  [[ -z $line ]] && break
196  # special cases
197  # Set default base if `[#16]' or `[##16]' etc. on its own.
198  # Unset it if `[#]' or `[##]'.
199  if [[ $line = (#b)[[:blank:]]#('[#'(\#|)(<->|)']')[[:blank:]]#(*) ]]; then
200    if [[ -z $match[4] ]]; then
201      if [[ -z $match[3] ]]; then
202	defbase=
203      else
204	defbase=$match[1]
205      fi
206      print -s -- $line
207      print -- $(( ${defbase} ans ))
208      line=
209      continue
210    else
211      base=$match[1]
212    fi
213  else
214    base=$defbase
215  fi
216
217  print -s -- $line
218
219  line="${${line##[[:blank:]]#}%%[[:blank:]]#}"
220  case "$line" in
221    # Escapes begin with a colon
222    (:(\\|)\!*)
223    # shell escape: handle completion's habit of quoting the !
224    eval ${line##:(\\|)\![[:blank:]]#}
225    line=
226    continue
227    ;;
228
229    ((:|)q)
230    # Exit
231    return 0
232    ;;
233
234    ((:|)norm) # restore output format to default
235      outform=1
236    ;;
237
238    ((:|)sci[[:blank:]]#(#b)(<->)(#B))
239      outdigits=$match[1]
240      outform=2
241    ;;
242
243    ((:|)fix[[:blank:]]#(#b)(<->)(#B))
244      outdigits=$match[1]
245      outform=3
246    ;;
247
248    ((:|)eng[[:blank:]]#(#b)(<->)(#B))
249      outdigits=$match[1]
250      outform=4
251    ;;
252
253    (:raw)
254    outform=5
255    ;;
256
257    ((:|)local([[:blank:]]##*|))
258      eval $line
259      line=
260      continue
261    ;;
262
263    ((function|:f(unc(tion|)|))[[:blank:]]##(#b)([^[:blank:]]##)(|[[:blank:]]##([^[:blank:]]*)))
264      zmathfuncdef $match[1] $match[3]
265      line=
266      continue
267    ;;
268
269    (:*)
270    print "Unrecognised escape"
271    line=
272    continue
273    ;;
274
275    (*)
276      # Latest value is stored as a string, because it might be floating
277      # point or integer --- we don't know till after the evaluation, and
278      # arrays always store scalars anyway.
279      #
280      # Since it's a string, we'd better make sure we know which
281      # base it's in, so don't change that until we actually print it.
282      eval "ans=\$(( $line ))"
283      # on error $ans is not set; let user re-edit line
284      [[ -n $ans ]] || continue
285      argv[num++]=$ans
286      psvar[1]=$num
287    ;;
288  esac
289  if [[ -n $base ]]; then
290    print -- $(( $base $ans ))
291  elif [[ $ans = *.* ]] || (( outdigits )); then
292    if [[ -z $forms[outform] ]]; then
293      print -- $(( $ans ))
294    else
295      printf "$forms[outform]\n" $outdigits $ans
296    fi
297  else
298    printf "%d\n" $ans
299  fi
300  line=
301done
302
303return 0
304