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. calc rounds to a float64 only to print a fraction.

Usage

Start the REPL:

$ calc
> 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.

Pass an expression as arguments or stdin to print results and exit:

calc '7 / 2'
printf '2 + 2\n_ * 10\n' | calc

Expressions

An expression is a Go constant expression: literals (1_000, 0xff, 1e-3), parentheses, and operators + - * / % & | ^ &^ << >>. %, the bitwise operators, and shifts require integers.

/ is the one operator where 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, even for integers. go/constant holds integers of arbitrary size, so calc limits shift counts to 10,000 to prevent memory exhaustion.

One step function evaluates lines for both piped input and the REPL.

Line editing

calc uses peterh/liner for line editing and history navigation. History persists across sessions in ~/.calc_history.

GitHub repo is a mirror

I develop calc on sockeye and mirror main to GitHub, which go install resolves.

← All articles