cmd / calc
calc is a REPL for arithmetic. It reads Go expressions and computes
them with exact arithmetic.
It is open source.
Install:
go install github.com/croaky/calc@latest
Why
A calculator that uses float64 prints 0.1 + 0.2 as
0.30000000000000004. Go truncates integer division, so 7 / 2 is
3. I wanted 0.3 and 3.5.
calc holds each value as an exact integer or rational, as the Go
compiler holds a constant. An integer keeps every digit. calc rounds
to a float64 only to print a fraction.
Usage
Start the REPL:
$ calc
> 2 + 2
4
> (12 + 4) * 3 / 2.5
19.2
> 7 / 2
3.5
> 0.1 + 0.2
0.3
> _ * 10
3
> 1 << 70
1180591620717411303424
> quit
_ is the result of the last line that gave one, as in irb.
Give an expression as arguments to print one result and exit:
calc '7 / 2'
Pipe expressions on stdin, one per line, to print one result per line:
printf '2 + 2\n_ * 10\n' | calc
Expressions
An expression is a Go constant expression: integer and floating-point
literals (1_000, 0xff, 1e-3), parentheses, and the operators
+ - * / % & | ^ &^ << >>. %, the bitwise operators, and the shifts
need integers.
/ is the one place calc differs from Go. It gives the exact
quotient.
How it works
calc uses two standard library packages. go/parser parses a line
as a Go expression. go/constant computes it.
The evalExpr function walks the tree. It accepts literals, _,
parentheses, and unary and binary operators. It refuses every other
node by name:
func evalExpr(e ast.Expr, prev constant.Value) (constant.Value, error) {
switch e := e.(type) {
case *ast.BasicLit:
if e.Kind != token.INT && e.Kind != token.FLOAT {
return nil, fmt.Errorf("%s is not a number", e.Value)
}
return constant.MakeFromLiteral(e.Value, e.Kind, 0), nil
case *ast.Ident:
if e.Name != "_" {
return nil, fmt.Errorf("unknown name %s", e.Name)
}
if prev == nil {
return nil, errors.New("_ has no value yet")
}
return prev, nil
constant.BinaryOp with token.QUO gives the exact quotient, also
for two integers. token.QUO_ASSIGN is the token that asks it for
Go's truncated integer division. So the exact / costs no code.
go/constant holds an integer of any size. A typo such as 1 << 1e9
builds a value of a billion bits and holds the prompt for minutes. So
calc limits a shift count to 10,000.
One step function computes a line for both the pipe and the REPL, so
the two cannot give different results.
Line editing
calc uses peterh/liner for line
editing:
- Up and Down: walk the history.
- Ctrl+A and Ctrl+E: move to the start and the end of the line.
- Ctrl+R: search the history.
- Ctrl+C: discard the line.
- Ctrl+D on an empty line,
exit, orquit: exit.
calc saves the history in ~/.calc_history, so the up arrow reaches
lines from an earlier session.
My first choice was golang.org/x/term. It fails on Ctrl+C:
Terminal.ReadLine returns io.EOF before it consumes the key, so
the next call reads the same bytes again.
GitHub repo is a mirror
I develop calc on sockeye and mirror main to
GitHub, which go install resolves.