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:

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.

← All articles