epher user guide

Welcome! epher is a programmable, scriptable calculator. You can use it for a quick calculation, or build up your own functions and small programs. Everything is available in eight languages.

This guide is for complete beginners. It starts with the simplest possible calculation and builds up to the full power of the language. Every example shows what you type and what epher answers.

There are five ways to use epher. Pick whichever suits you:

Version What it is Best when
Command line (CLI) Text commands in a terminal You live in a terminal and like scripts
REPL An interactive epher session at the epher> prompt You want quick back-and-forth without leaving the terminal
Terminal UI (TUI) A full-screen program inside the terminal You want a terminal app with graphs and history on screen
Desktop app A normal desktop program with its own window You want a regular application
Web app (PWA) Runs in your browser, installable, works offline You want the fastest start; no installation

The desktop app, the command line, the REPL, and the terminal UI are one program: a single download installs the epher command, which does all four. The web app is the exception: it needs no download at all.

All five versions understand exactly the same language. Learn it once, use it anywhere.

1. The epher language

This chapter teaches the language shared by every version of epher. In the web app or desktop app, type an expression and press Enter (or click the = button). In the CLI, start the session with epher repl and type after the epher> prompt. In the TUI (epher tui), just type and press Enter. In the CLI you can also write epher "expression" to evaluate one expression directly.

1.1 Your first calculation

Type this:

2 + 3 * 4

epher answers:

14

Multiplication is done before addition, exactly like in mathematics. That rule is called operator precedence.

1.2 Order of operations

The full precedence order, from strongest to weakest:

  1. ! factorial and % percent (both postfix)
  2. ^ power
  3. * and / multiplication and division
  4. + and - addition and subtraction

Use parentheses to change the order:

(2 + 3) * 4
20

The ^ operator computes powers, and it works right-to-left:

2 ^ 10
1024
2 ^ 3 ^ 2
512

(2 ^ 3 ^ 2 means 2 ^ (3 ^ 2), which is 2 ^ 9 = 512.)

Powers can be fractional. 2 ^ 0.5 is the square root of 2:

2 ^ 0.5
1.41421356237

Subtraction and division work left-to-right:

10 - 3 - 2
5

The % sign is a postfix operator that means "divided by 100": 5% is 0.05. It never looks at the operators around it, so 200 + 10% is 200.1. To increase 200 by 10%, spell the multiplication:

200 * (1 + 10%)
220

1.3 The special numbers pi, e, tau and phi

The famous constants are built in:

pi
3.14159265359
2 * pi
6.28318530718
e
2.71828182846

Two more: tau is a full turn (2 pi), and phi is the golden ratio:

tau
6.28318530718
phi
1.61803398875

1.4 Comparing and logic

You can compare numbers. The result is either true or false:

Comparison Meaning
a > b a is greater than b
a < b a is less than b
a >= b a is greater than or equal to b
a <= b a is less than or equal to b
a == b a equals b (note the double =)
a != b a does not equal b
3 > 2
true
1 != 2
true

Combine comparisons with and, or and not:

3 > 2 and 2 < 3
true
not 3 > 2
false

1.5 Variables

Give a name to a value with a single =:

x = 5
5

epher repeats the value back to you. From now on, x can be used anywhere:

x ^ 2
25

You can change a variable whenever you like. It keeps its value until you change it:

x = x + 1
6

Names can contain letters and underscores, like radius or my_total. They cannot contain spaces or start with a number.

One name is reserved: i, the imaginary unit (section 1.18).

The special variable ans always holds the previous answer, like the Ans key on a pocket calculator, handy for chained calculations:

2 + 3
ans * 2
5
10

One list can fill several names at once, {a, b} = list takes a list apart, left to right (section 1.11 shows functions that hand back more than one answer this way):

{a, b} = {10, 20}; a + b
{10, 20}
30

A position you do not want is written _:

{x, _} = {7, 8}; x
{7, 8}
7

1.6 Constants: names that never change

A constant is a name for a value that never changes, like the built-in pi, but chosen by you. Define one with const:

const tax = 0.2
0.2

Use it anywhere a number can go:

100 * (1 + tax)
120

The value is fixed: changing it with = is an error,

tax = 0.25
error: cannot assign to constant tax

and so is redefining it with a different value:

const tax = 0.25
error: constant already defined: tax

Constants are different from variables in one more way: like pi, they work inside your own functions.

const g = 9.81
9.81
def weight(m) = m * g
weight(80)
784.8

Save a constant for future sessions with save tax, exactly like a function (chapter 4.4).

A variable and a constant cannot share a name: after const tax = 0.2, tax = ... is always an error. Pick a fresh name or start a new session.

1.7 Strings and print

A string is text in double quotes: "hello". Strings concatenate with +, compare with == and !=, order with < and > (dictionary order), count with len, and index 1-based like lists:

"hello" + " " + "world"
len("hello")
"hello"[1]
"abc" == "abd"
"apple" < "banana"
hello world
5
h
false
true

A backslash inside a string starts an escape: \n is a new line, \t a tab, and \\ and \" are the backslash and the quote themselves, so a string can contain a double quote:

s = "say \"hi\""
len("a\nb")
say "hi"
3

str(x) spells one value the way the answer panel would, and print(a, b, …) joins its arguments with spaces into one line:

print("x =", 42)
x = 42

A small library covers the rest of report writing. upper and lower change case, trim strips spaces from the ends, substr takes a piece (1-based, like every index), find reports where a text appears (0 when it does not), replace swaps every copy of one text for another, split breaks a text into a list at a separator, join glues a list into one text, and fixed spells a number with exactly as many decimal places as you ask, the zero a report wants, kept:

upper("hello")
substr("2026-09-17", 1, 4)
find("hello world", "world")
replace("2026-09-17", "-", "/")
split("a,b,c", ",")
join({1, 2, 3}, "-")
fixed(3.1, 2)
HELLO
2026
7
2026/09/17
{a, b, c}
1-2-3
3.10

1.8 Decisions with if

if chooses between two values:

if 3 > 2 then 10 else 20
10

The shape is always if condition then value_if_true else value_if_false. The else part is required.

A more useful example with a variable:

price = 100
if price > 50 then 2 else 1
100
2

An if can compare strings too (section 1.7): both branches just have to be values of the same kind: numbers with numbers, strings with strings.

1.9 Loops with while

while repeats a statement as long as a condition holds:

x = 0; while x < 5 do x = x + 1; x
0
5

Read that script as: start x at 0; while x is less than 5, add 1 to x; then show x. The result is 5 because the loop ran five times.

Safety net: epher stops any loop after 100,000 steps and shows error: step limit exceeded. That protects you from loops that would never end. If you see it, your condition probably never became false.

A loop can also be left on purpose: the next section adds break and continue, and a function adds return (section 1.11).

1.10 Loops with for

for repeats a statement once per value, collecting the body's values into a list: over a range start to end (inclusive) with an optional step, or over the elements of a list:

for k in 1 to 5 do k^2
for x in {2, 3, 4} do 10*x
for k in 0 to 1 step 0.5 do k
{1, 4, 9, 16, 25}
{20, 30, 40}
{0, 0.5, 1}

The loop variable is scoped to the loop: afterwards the name means what it meant before, while assignments to other names inside the body persist. With print, a loop writes readable lines:

for k in 1 to 3 do print("line", k)
{line 1, line 2, line 3}

The same 100,000-step safety net bounds a for loop as a while.

Leaving a loop early. break stops the loop on the spot; the values collected so far are the loop's answer. continue skips the rest of the pass and moves to the next value. Both are statements, so they sit behind an if:

for k in 1 to 6 do if mod(k, 2) == 1 then k
for k in 1 to 10 do if k == 4 then break else k
total = 0
for k in 1 to 6 do if mod(k, 2) == 0 then continue else total = total + k
total
{1, 3, 5}
{1, 2, 3}
0
{1, 4, 9}
9

Read the middle one as: for k from 1 to 10, if k is 4 stop; otherwise hand back k. The loop's answer is the values it saw before it stopped. The last pair shows continue counting: the for line's collected list is the running total - 1, then 4, then 9 - and the even passes were skipped, so they added nothing. The final total, 9, is the sum of the odd values. An if without an else contributes nothing when its condition is false, which is how the first loop keeps only the odd values, and how a loop can filter a list as well as transform it.

1.11 Your own functions with def

A function is a calculation with a name and parameters:

def f(x) = x ^ 2

Then use it:

f(7)
49

Functions can take several parameters:

def area(w, h) = w * h
area(3, 4)
12

You can also define a function with no parameters:

def answer() = 42
answer()
42

A body with several steps. When one expression is not enough, give the function a do … end body. The statements run one after another in order, and the last one's value is the answer:

def hyp(a, b) do
  c = a ^ 2 + b ^ 2
  sqrt(c)
end
hyp(3, 4)
5

The intermediate name c lives inside the call; it is not visible outside, and two calls do not see each other's c.

return: answer now. return value answers immediately and skips the rest of the body, the natural shape for a choice with an early exit:

def grade(s) do
  if s >= 90 then return "A"
  if s >= 80 then return "B"
  "C"
end
grade(95); grade(85); grade(40)
A
B
C

return also leaves a loop that is searching, the moment it finds:

def firstsq(xs) do
  for x in xs do
    if x ^ 0.5 == floor(x ^ 0.5) then return x
  0
end
firstsq({3, 5, 9, 11}); firstsq({3, 5, 7})
9
0

One rule keeps the blocks readable: the end always closes the function's do, an if, for, or while takes no end. A body that ends without an answer (say, every path returned nothing) is an error, not a silence: epher says so and names the function.

More than one answer? Give back a list, and name it in one move with the destructuring from section 1.5: {mean, sd} = {4, 1.6}.

1.12 Recursion: a function that calls itself

The most famous example is the Fibonacci numbers:

def fib(n) = if n <= 1 then n else fib(n - 1) + fib(n - 2)
fib(10)
55

fib(10) is the 10th Fibonacci number. The function calls itself with smaller arguments until it reaches n <= 1. This works because the if ... then ... else ... form only calculates the branch it needs.

A function's body is one expression after =, or a do … end block of several statements with return for early exits (section 1.11). Combine several calculations in a script instead (next section) when no function is needed.

1.13 Scripts: several statements at once

A script is several statements joined with ; or with newlines, which mean exactly the same thing, executed one after another:

x = 10; y = x + 5; x + y
10
15
25

Scripts are how you build small programs: set up variables, loop, and show a final result.

Newlines and ; are the same separator, and you can mix them freely. The Copy button above a multi-line example copies the whole script, and you can paste it straight into epher: the entry field on the web app and the desktop app, the terminal UI, and epher repl all run each line in order, exactly as if you had typed them one by one. Joining several statements with ; on one line works everywhere too, including the one-shot command line (section 4.1).

Scripts can carry comments - notes for you that epher skips, written the PHP way. // or # comments to the end of the line, and /* ... */ comments out a block. A note about one statement sits at its end, after //, never inside the statement:

// a small script with notes
r = 3 # radius in metres
area = pi * r ^ 2 // pi r squared
area

A block comment may also span several lines. A script file or a pasted program arrives as one whole program, so its block comments may span lines:

/* the area of a circle
of radius 3 */
pi * 3 ^ 2

Input that arrives line by line - typing into the REPL or the terminal UI, or a piped script - takes one line at a time, so there a block comment opens and closes on the same line.

1.14 Exact results: frac, dec and big

Normally epher calculates with decimal numbers like a pocket calculator, and results round to twelve significant digits the way a pocket calculator shows them: 0.1 + 0.2 is 0.3, never 0.30000000000000004. Exact fractions are on by default; a result with a good small-denominator fraction whose decimal keeps repeating shows as one. 1 / 3 displays as 1/3 without asking:

1 / 3
1/3

With exact fractions off in the Results settings (chapter 2.2) the same division shows 0.333333333333. frac(n, d) makes an exact fraction that stays exact through calculations:

frac(1, 3)
1/3

Fractions stay exact through calculations:

frac(1, 3) * 3
1

dec(x) makes an exact decimal. 0.1 + 0.2 shows 0.3 either way, the difference is arithmetic:

0.1 * 3 - 0.3
dec(0.1) * 3 - dec(0.3)
0.0000000000000000555111512313
0.0

The float result carries the tiny rounding error every computer makes with decimal numbers; dec() keeps the arithmetic exact.

big(x) makes an exact whole number, for values too large for a pocket calculator:

big(10 ^ 20)
100000000000000000000

Number bases write integers the way the math community spells them: 0b for binary, 0o for octal, 0x for hex (the prefix changes the spelling, never the value):

0b1010 + 0xFF
265

Convert back with bin(x), oct(x) and hex(x). These give the prefixed spelling of a whole number, ready to feed straight back in:

hex(255)
bin(10)
0xff
0b1010

exact(x) reconstructs the exact fraction behind a decimal result: any value with a good small-denominator fraction shows it. This is the same reconstruction the apps use for their default display, so 1 / 3 usually shows as 1/3 without asking:

exact(0.3333333333333333)
exact(0.30000000000000004)
1/3
3/10

An irrational value like pi has no good fraction, so exact() leaves it alone.

The display verbs spell a number in other notations. scientific(x) uses one digit before the exponent, engineering(x) uses exponents in steps of three (the mantissa stays between 1 and 1000), and grouped(x) inserts thin-space thousands separators:

scientific(12345)
engineering(12345)
engineering(0.5)
grouped(1234567.89)
1.2345e4
12.345e3
500e-3
1 234 567.89

The web app and TUI also offer these as display settings (see chapter 2.2 and 5.2): exact fractions on or off, Auto/Scientific/ Engineering notation, and thousands separators. The settings only change how results are shown; the values stay decimal numbers underneath.

1.15 Built-in functions

epher has the functions of a scientific calculator, grouped by family.

Trigonometry works in radians. Use deg and rad to convert:

Function Meaning Example Result
sin(x), cos(x), tan(x) trigonometric functions sin(pi / 2) 1
asin(x), acos(x), atan(x) inverse trigonometric atan(1) 0.7853981633974483
atan2(y, x) angle of the point (x, y) atan2(1, 1) 0.7853981633974483
deg(x) radians → degrees deg(pi) 180
rad(x) degrees → radians rad(180) 3.14159265359
sinh(x), cosh(x), tanh(x) hyperbolic functions sinh(1) 1.1752011936438014
asinh(x), acosh(x), atanh(x) inverse hyperbolic acosh(1) 0

Powers, roots and logarithms (on a calculator log is base 10):

Function Meaning Example Result
sqrt(x) square root sqrt(16) 4
cbrt(x) cube root cbrt(-27) -3
root(n, x) nth root root(3, 8) 2
exp(x) e to the power x exp(1) 2.71828182846
ln(x) natural logarithm ln(e) 1
log(x) base-10 logarithm log(100) 2
log2(x) base-2 logarithm log2(8) 3
logb(b, x) logarithm in base b logb(2, 8) 3
hypot(a, b) hypotenuse hypot(3, 4) 5
5! (also fact(n)) factorial 5! 120

Rounding, signs and whole numbers:

Function Meaning Example Result
abs(x) absolute value abs(-3) 3
floor(x) / ceil(x) round down / up floor(2.7) 2
round(x) nearest, half away from zero round(2.5) 3
trunc(x) drop the fraction trunc(-2.9) -2
sign(x) -1, 0 or 1 sign(-5) -1
ncr(n, r) combinations ncr(52, 5) 2598960
npr(n, r) permutations npr(5, 2) 20
gcd(a, b) / lcm(a, b) common divisors and multiples gcd(12, 18) 6
mod(a, b) remainder mod(7, 3) 1

Primes and divisors work on whole numbers:

Function Meaning Example Result
isprime(n) true when n is prime isprime(97) true
nextprime(n) / prevprime(n) nearest primes nextprime(10) 11
factors(n) prime factorization factors(360) 2^3 * 3^2 * 5
totient(n) Euler's totient totient(12) 4
ndivisors(n) how many divisors ndivisors(360) 24
modpow(b, e, m) b to the e, mod m, exactly modpow(2, 10, 1000) 24

Statistics take any number of arguments:

Function Meaning Example Result
sum(...) / product(...) totals sum(1, 2, 3) 6
mean(...) average mean(1, 2, 3) 2
median(...) middle value median(1, 2, 3, 4) 2.5
min(...) / max(...) smallest / largest max(4, 1, 3) 4
variance(...) / stdev(...) spread of the values stdev(2, 4) 1

The exact layers from section 1.14 stay:

Function Meaning Example Result
frac(n, d) exact fraction frac(1, 3) 1/3
dec(x) exact decimal dec(0.1) 0.1
big(x) exact whole number big(10 ^ 20) 100000000000000000000
Binary, octal, hex 0b…, 0o…, 0x… 0xFF + 0b1
Base spelling bin(x), oct(x), hex(x) hex(255)
bin(x) / oct(x) / hex(x) prefixed spelling in base 2 / 8 / 16 hex(255) 0xff

They combine like everything else:

min(sqrt(16), 5)
4

The physical constants use SI units, like the astronomy ones in section 1.29:

Name Meaning Value
G Newton's gravitational constant 6.6743e-11
gamma Euler-Mascheroni constant 0.577215664902
q_e elementary charge 1.602176634e-19
ev electronvolt, in joules 1.602176634e-19
eps_0 vacuum permittivity 8.8541878128e-12
mu_0 vacuum permeability 1.25663706212e-6
z_0 impedance of free space 376.730313668
m_e mass of the electron 9.1093837139e-31
m_p mass of the proton 1.67262192595e-27
m_n mass of the neutron 1.67492750056e-27
m_u atomic mass unit 1.66053906892e-27
a_0 Bohr radius 5.29177210544e-11
alpha fine-structure constant 0.0072973525643
r_inf Rydberg constant 10973731.568160
mu_b Bohr magneton 9.2740100783e-24
n_a Avogadro constant 6.02214076e23
faraday Faraday constant, C/mol 96485.33212
r_gas molar gas constant 8.31446261815
atm standard atmosphere, in pascals 101325
wien Wien wavelength constant 0.002897771955
phi_0 magnetic flux quantum 2.067833848e-15
m_P Planck mass 2.176434e-8
l_P Planck length 1.616255e-35
t_P Planck time 5.391247e-44
r_e classical electron radius 2.8179403205e-15
lambda_c Compton wavelength 2.42631023867e-12
mu_n nuclear magneton 5.050783699e-27

1.16 Reading errors

When something goes wrong, epher tells you instead of guessing:

1 / 0
error: division by zero
sqrt(-4)
2i
unknown_name
error: unknown name: unknown_name
foo(1)
error: unknown name: foo

The last example is important: epher tells you exactly which name it does not know, so you can fix your expression.

1.17 Quick reference

What Syntax Example
Add, subtract, multiply, divide + - * / 7 / 2
Power ^ (right-to-left) 2 ^ 10
Factorial ! (postfix) 5!
Percent % (postfix) 200 * (1 + 10%)
Parentheses ( ) (2 + 3) * 4
Constants pi, e, tau, phi 2 * pi
Scientific notation 2.5e-3 6.02e23
Compare > < >= <= == != 3 >= 2
Logic and or not a > 1 and a < 10
Variable name = value x = 5
Constant const name = value const tax = 0.2
Decision if c then a else b if x > 0 then 1 else -1
Choose statements if c then stmt [else stmt] if k == 4 then break
Loop while c do statement while x < 5 do x = x + 1
For loop for k in a to b step s do stmt for k in 1 to 5 do k^2
Leave / skip a loop break, continue if k == 4 then break
Function def name(params) = expr or do … end def f(x) = x ^ 2
Answer now return value if ok then return x
Several names {a, b} = list (_ skips) {m, sd} = stats(d)
Strings "...", + joins, s[k], ==, < "a" + "b"
String library upper lower trim substr split join find replace fixed split("a,b", ",")
Print print(a, b, ...) print("x =", 42)
Script statements joined with ; or newlines x = 1; x + 1
Exact fraction frac(n, d) frac(1, 3)
Exact decimal dec(x) dec(0.1) + dec(0.2)
Exact whole number big(x) big(10 ^ 20)
Reconstruct a fraction exact(x) exact(0.3333333333333333)
Scientific, engineering, grouped scientific(x) engineering(x) grouped(x) engineering(12345)
Imaginary unit i, or a literal 4i sqrt(-1)
Complex parts re(z) im(z) arg(z) conj(z) abs(z) re(3 + 4i)
Solve an equation solve lhs == rhs solve x^2 == 9
Numeric derivative derivative(expr, x) derivative(x^2, 3)
Definite integral integral(expr, a, b) integral(x^2, 0, 3)
Binary, octal, hex 0b…, 0o…, 0x… 0xFF + 0b1
Base spelling bin(x), oct(x), hex(x) hex(255)
Primes isprime(n), factors(n), … factors(360)
List literal {…} {1, 2, 3}
List element list[k] (1-based) {5, 6}[2]
List statistics mean(list), median(list), … stdev(d)
List shape len(s), sort(s), mode(s), range(s), quartile(s, k) quartile(d, 1)
Linear regression linreg(xs, ys) linreg(x, y)
Regression family quadreg expreg powreg logreg quadreg(xs, ys)
Normal family normpdf normcdf invnorm invnorm(0.975)
t family tpdf tcdf invt invt(0.975, 10)
Chi-squared family chi2pdf chi2cdf invchi2 chi2cdf(3.84, 1)
Discrete families binompdf binomcdf poissonpdf poissoncdf binomcdf(2, 10, 0.5)
Tests and intervals ztest ttest zinterval tinterval chisq_gof tinterval(d, 0.95)
ANOVA and paired t anova(lists...), ttestpaired(a, b) anova(g1, g2, g3)
Data plots graph scatter(xs, ys) histogram(data) boxplot(data) graph boxplot(d)
Random numbers random(), random(a, b), randint(a, b), randseed(n) randint(1, 6)
Normal draws randn(mu, sigma) randn(0, 1)
Constants browser Help → Constants: every builtin constant, grouped Help → Constants
Quantity 5 m, 60 mile/hr, 1 km 2 m^2
Convert expr in unit or expr -> unit 72 km/hr in m/s
Prefixes k M G T m µ n p scale any unit 5 km, 3 MPa, 1 GHz
Bitwise and, or a & b, a | b 0xFF & 0x0F
Bitwise xor a xor b 5 xor 3
Bitwise not ~a ~0
Shifts a << n, a >> n 1 << 8
Word size bits(n) for 8, 16, 32, 64 bits(8)
Implicit relation graph lhs == rhs graph x^2 + y^2 == 1
Matrix literal [[1, 2], [3, 4]] [[1, 2], [3, 4]] * [[5, 6], [7, 8]]
Matrix functions det inv transpose trace dim ref rref rref([[2, 1, 5], [1, -1, 1]])
TVM solver tvm_n tvm_i tvm_pv tvm_pmt tvm_fv tvm_pmt(360, 0.08/12, -100000, 0)
NPV and IRR npv(rate, flows) irr(flows) irr({-100, 60, 60})
Amortization amort(p, r, n, k) amort(1000, 0.01, 12, 6)
Interest simple_interest compound_interest compound_interest(1000, 0.05, 2)

1.18 Complex numbers

epher calculates with complex numbers automatically. The imaginary unit is i, exactly like pi:

i ^ 2
sqrt(-1)
-1
i

Unlike every other built-in name, i cannot be reused: i = 5 is refused, so the imaginary unit can never be shadowed - not by an assignment, not by an old saved session.

Write a complex number with the i suffix, no multiplication sign needed: 3 + 4i is one literal, 2.5i works, and so do the based literals (0xFFi). The usual arithmetic extends: add, subtract, multiply, divide, and powers all work, and i follows the normal precedence (i ^ 2 binds like any power).

The real functions extend too. Given a complex argument they compute in the complex plane; given a real argument outside their real domain they return the principal complex result instead of an error:

ln(-1)
asin(2)
exp(i * pi)
3.14159265359i
1.57079632679-1.31695789692i
-1+0.000000000000000122464679915i

(exp(i * pi) is exactly -1; the last digits are the noise of sin(pi) in the computer's arithmetic.)

Four functions read a complex number's parts, and abs() is its magnitude:

re(3 + 4i)
im(3 + 4i)
arg(-1)
conj(3 - 4i)
abs(3 + 4i)
3
4
3.14159265359
3+4i
5

Integer-only functions (fact, gcd, floor, isprime, ...) reject complex arguments with a type error.

1.19 Solving equations

solve finds the roots of an equation in one variable. The equation uses ==:

solve x^2 == 5*x + 6
x = -1, x = 6

Polynomial equations (built from + - * ^ and constants) give every root, real and complex:

solve x^2 == -1
solve x^2 + 2*x + 5 == 0
solve (x - 1)^2 == 0
x = -i, x = i
x = -1-2i, x = -1+2i
x = 1

The variable solved for is x when it appears, otherwise the single other variable. Constants and bound variables act as parameters:

const k = 3
solve k*x == 12
3
x = 4

Any other equation is scanned numerically over -100..100: roots are found by bracketing sign changes, so solve sin(x) == 0.5 lists every root in that range. Two honest limitations: a root where the function only touches zero (like x^2 == 0 through the numeric path) can be missed, and equations in several unbound variables are an error.

1.20 Calculus: derivative and integral

derivative(expr, p) is the numeric derivative of expr at p. The first argument stays an expression, and its free variable is the one differentiated:

derivative(x^2, 3)
derivative(sin(t), 0)
6
1

Because the argument stays an expression, the derivative is graphable: graph derivative(x^3 - x, x) plots the slope curve.

integral(expr, a, b) is the definite integral from a to b, computed by adaptive Simpson quadrature:

integral(x^2, 0, 3)
integral(sin(x), 0, pi)
9
2

integral(x^2, 3, 0) is -9 (the signed integral), and a graphable upper bound works: graph integral(x^2, 0, x).

Both are numeric; the expressions must be real-valued over the range, and an expression in several variables is an error.

1.21 Data: lists, statistics, and regression

A list is a column of numbers in braces: {1, 2, 3}. Elements are expressions, the empty list {} is allowed, and a list binds to a name like any value:

d = {12, 15, 14, 16, 13, 15, 14, 17}
d[2]
len(d)
{12, 15, 14, 16, 13, 15, 14, 17}
15
8

list[k] is the k-th element, 1-based like a calculator expects; an out-of-range index is an error. The bracket binds tighter than ^, so d[2]^2 is (d[2])^2.

Arithmetic over a list is elementwise, with a plain number broadcast to every element:

{1, 2, 3} * 2
{1, 2, 3} + 10
{2, 4, 6}
{11, 12, 13}

Two lists must have the same length for + - * / ^. == and != compare whole lists; ordering comparisons reject them.

The statistics functions take a list as their one argument (they keep their variadic form too, mean(1, 2, 3) still works): sum product mean median mode variance stdev min max range. The new shape functions are len(list), sort(list) (ascending copy), mode(list) (most frequent value, smallest on ties), range(list) (max minus min), and quartile(list, k) for k in 1..3 (TI-style median of halves):

mean(d)
median(d)
quartile(d, 1)
14.5
14.5
13.5

linreg(xs, ys) fits the least-squares line through two same-length lists and reports it with the correlation r:

linreg({1, 2, 3, 4}, {2.1, 4.2, 5.8, 8.1})
y = 1.96*x + 0.15 (r = 0.9979)

The fitted line is a display, like solve's roots; the picture of the fit lives on the scatter plot (section 1.23).

The rest of the regression family fits the models calculators grow into: quadreg fits y = a*x^2 + b*x + c (at least 3 points), expreg fits y = a*e^(b*x) (y > 0), powreg fits y = a*x^b (x and y > 0), and logreg fits y = a + b*ln(x) (x > 0). Each reports the model with its r:

quadreg({1, 2, 3, 4}, {1, 4.1, 8.9, 16.2})
expreg({1, 2, 3}, {2.7, 7.4, 20.1})
y = 1.05*x^2 + -0.21*x + 0.2 (r = 0.9999)
y = 0.9911*e^(1.0037*x) (r = 1)

The r of a transformed fit is the correlation of the linearized pair: the same number TI and NumWorks report. Every model can also draw itself over a scatter: graph scatter(xs, ys, quadreg) (or expreg, powreg, logreg) plots the points with that model's curve.

1.22 Distributions and hypothesis tests

The probability functions cover the standard normal, Student's t, chi-squared, binomial, and Poisson families. The normal family takes one or three arguments, one argument is the standard normal:

normcdf(1.96)
invnorm(0.975)
normcdf(12, 10, 2)
0.975002104852
1.95996398454
0.841344746069

normpdf(x[, mu, sigma]), normcdf(x[, mu, sigma]), invnorm(p[, mu, sigma]); tpdf(x, df), tcdf(x, df), invt(p, df); chi2pdf(x, df), chi2cdf(x, df), invchi2(p, df); binompdf(k, n, p), binomcdf(k, n, p); poissonpdf(k, lambda), poissoncdf(k, lambda). The inv* functions answer the reverse question: invt(0.975, 10) is the t value with 97.5% of the mass below it.

The tests take a data list and report the statistic and the two-sided p-value as a display string; the intervals report (lo, hi) at the level you name:

d = {12, 15, 14, 16, 13, 15, 14, 17}
ttest(d, 14)
tinterval(d, 0.95)
ztest(d, 14, 1.5)
chisq_gof({20, 30, 25, 25}, {25, 25, 25, 25})
{12, 15, 14, 16, 13, 15, 14, 17}
t = 0.8819, p = 0.4071
(13.1594, 15.8406)
z = 0.9428, p = 0.3458
chi2 = 2, p = 0.5724

ttest(data, mu0) and tinterval(data, level) use the sample standard deviation (n−1); ztest(data, mu0, sigma) and zinterval(data, sigma, level) need the known sigma. chisq_gof(observed, expected) is the goodness-of-fit test with k−1 degrees of freedom. The results are display strings, so they are readable and copy-pasteable, but arithmetic cannot touch them.

Two more tests share the same shape. anova(list1, list2, …) is one-way analysis of variance over two or more groups (unequal lengths are fine) and reports F with its p-value:

anova({1, 2, 3}, {4, 5, 6}, {7, 8, 9})
F = 27, p = 0.001

ttestpaired(a, b) is the paired t-test: it takes the differences of two same-length lists and tests them against 0, the classroom "before and after" test:

ttestpaired({80, 85, 90}, {82, 84, 91})
t = -0.7559, p = 0.5286

1.23 Data plots

The graph family takes lists too: a scatter, a histogram, and a box-and-whisker plot. A data plot owns the pane like a solar system does, the newest command wins, and graph clear empties it.

x = {1, 2, 3, 4, 5}
y = {2.1, 4.2, 5.8, 8.1, 9.9}
graph scatter(x, y)
graph histogram({1, 2, 2, 3, 3, 3, 4, 5})
graph boxplot({1, 2, 2, 3, 3, 3, 9})

scatter(xs, ys) plots the points and, with two or more points, draws the least-squares fit line, captioned y = a*x + b (r = …) in the legend. histogram(data[, bins]) draws a frequency histogram; the bin count is optional (Sturges' rule by default) and must be a whole number between 1 and 50. boxplot(data) draws the box-and-whisker: min, Q1, median, Q3, max, with whiskers to the extremes. The plot window opens fitting the data (the from a to b domain keywords still do not apply), and once it is drawn the picture zooms exactly like a curve plot: the mouse wheel, a pinch, and the zoom slider all work, and the export saves what the pane shows.

An optional third word chooses the model: graph scatter(xs, ys, quadreg) (or expreg, powreg, logreg) draws that fit instead of the line.

1.24 Random numbers

random() draws a uniform random number in [0, 1), random(a, b) one in [a, b), and randint(a, b) a whole number from the closed range [a, b], a dice roll:

randseed(7)
randint(1, 6)
7
3

The sequence is reproducible: randseed(n) re-seeds the generator with n and reports it, so the same seed replays the same draws in every session and every frontend.

randn(mu, sigma) draws from the normal distribution with mean mu and standard deviation sigma (Desmos calls it randomNormal, TI calls it randNorm):

randseed(7)
randn(0, 1)
7
1.36499229746

The same seed replays the same draws, exactly like the uniform ones.

1.25 Units and conversion

A number followed by a unit becomes a quantity: the value in SI units plus its dimensions. The unit table covers the SI base and derived units (m, s, kg, A, K, mol, cd, Hz, N, Pa, J, W, C, V, F, ohm, S, Wb, T, H, lm, lx, Bq, Gy, Sv), the everyday units (min, hr, d, yr, L, t, bar, atm, torr, psi, eV, mile, yd, ft, inch, nmi, lb, oz, gal, qt, pt, mph, knot), and the astronomy suffixes from section 1.29. Compound units chain: 60 mile/hr and 5 m/s^2 are single units.

60 mile/hr
60 mile/hr

The SI prefixes scale any of them: k M G T m µ n p are kilo, mega, giga, tera, milli, micro, nano, pico, 5 km, 3 MPa, 1 GHz all work, and 2 kg is the kilogram itself.

The dimensions are checked: adding or comparing quantities with different units errors instead of mixing metres and seconds:

5 m + 3 s
error: dimension error: cannot add 5 m and 3 s

Arithmetic composes the dimensions: 5 m * 3 m is 15 m^2, (3 m)^2 is 9 m^2, sqrt(4 m^2) is 2 m, and a whole expression whose dimensions cancel is an ordinary number again (5 m / 5 m is 1). Results prefer the exact derived name when the dimensions match one, 5 kg * 3 m / 1 s^2 answers 15 N.

Conversion. expr in unit (or expr -> unit) shows a quantity in the named unit; the dimensions must match. in binds loosest of the operators, so 5 m + 3 m in km converts the whole sum:

72 km/hr in m/s
20 m/s
2 m^2 in cm^2
20000 cm^2

Temperature scales (Celsius, Fahrenheit) are not units here, kelvins are, and K works like any other.

1.26 Bitwise operations

The base literals from section 1.15 are made for it: 0b101, 0o17, 0xFF. The bitwise operators work on whole numbers and answer with exact integers:

0xFF & 0x0F
15
Operator Meaning
a & b bitwise and
a | b bitwise or
a xor b bitwise exclusive or
~a bitwise not (two's complement)
a << n shift left (multiply by 2^n)
a >> n shift right, arithmetic (divide by 2^n, rounding down)

The results are exact big integers, so 1 << 60 keeps every digit. The working word size is 64 bits by default: results are read as signed two's complement, so ~0 is -1 and 1 << 100 wraps to 0. bits(n) changes the word size to 8, 16, 32, or 64, and bits() reports it:

bits(8)
~0
8
-1

Shifts by a negative amount reverse the direction (8 << -1 is 4). The boolean and and or keep their meanings; & and | are the bitwise spellings.

1.27 Implicit relations

An equation in two unknowns plots as a curve: the graph family samples the relation with marching squares and draws its zero contour. The circle, the parabola, and the vertical line are all one command each:

graph x^2 + y^2 == 1
graph y == x^2
graph x == 2

The relation is sampled over the square from from a to b (or the default window), so graph x^2 + y^2 == 1 from -2 to 2 fits the circle's window. Everything a curve can do applies: the legend captions the equation, sliders animate its constants, and the picture zooms, pans, and exports like any other plot. The inequality fills (y < …, y > …) stay curves with shading; a relation has no points of interest.

1.28 Matrices

A matrix is a grid of numbers, spelled as rows of lists: [[1, 2], [3, 4]] is the 2×2 matrix. + and - are elementwise (matching shapes), * is the matrix product, a number scales elementwise, and ^ is the whole-number matrix power (A ^ 0 is the identity, so powers need square matrices). M[2][1] is the element at row 2, column 1, rows index like lists, 1-based.

[[1, 2], [3, 4]] * [[5, 6], [7, 8]]
[[19, 22], [43, 50]]

The matrix functions cover the classroom floor: det(M) (square only), inv(M) (singular matrices are an error), transpose(M), trace(M) (square), dim(M) (the {rows, cols} list), and ref(M) with rref(M) for row reduction. Linear systems solve through rref on the augmented matrix:

rref([[2, 1, 5], [1, -1, 1]])
[[1, 0, 2], [0, 1, 1]]

The rows read x = 2, y = 1, the last column of the reduced augmented matrix. Exact fractions display inside matrices like lists, so inv([[1, 2], [3, 4]]) shows [[-2, 1], [3/2, -1/2]].

1.29 Astronomy and the solar system

epher speaks astronomy: unit suffixes, physical constants, calendar and time functions, and a live ephemeris for the Sun, the Moon, the planets and Pluto. Everything works offline.

Units that speak astronomy. Write a number followed by a unit suffix and epher converts it to SI units on the spot:

Suffix Unit Converts to
AU or au astronomical unit metres
pc parsec metres
ly light year metres
deg degree radians
arcmin, arcsec arcminute, arcsecond radians
min, hr, d, yr minute, hour, day, Julian year seconds
Jy jansky W m-2 Hz-1
3.2 AU in m
478713186240 m
sin(30 deg)
0.5

One worked example per suffix, to copy from:

Suffix Try Result
AU, au 1 au in km 149597870.7 km
pc pc / au 206264.806247 (au per parsec)
ly ly / au 63241.0770843 (au per light year)
deg 90 deg in rad 1.57079632679
arcmin sin(1 arcmin) 0.000290888204563
arcsec sin(1 arcsec) 0.00000484813681108
min 1 min in s 60 s
hr 1 hr in min 60 min
d 1 d in hr 24 hr
yr 1 yr in d 365.25 d
Jy mag2jy(0) Jy 3631 Jy

The suffixes are part of the grammar, so no user constant can change what 3.2 AU means, and h stays Planck's constant: hours are written hr. Functions return counts in natural units; a suffix converts a count to SI, so mag2jy(20) is a jansky count and mag2jy(20) Jy is the same flux in watts per square metre hertz.

Astronomy constants. au, pc, ly, c, g, h, h_bar, k_b, sigma_sb, m_sun, r_sun, l_sun, m_earth, r_earth, m_moon, r_moon work like pi, and you can shadow them with your own constants. One example per constant, to copy from:

Constant Quantity Try Result
au astronomical unit 1 au in km 149597870.7 km
pc parsec pc / au 206264.806247
ly light year ly / au 63241.0770843
c speed of light (m/s) c / 1000 299792.458 (km/s)
g standard gravity (m/s²) g 9.80665
h Planck constant (J·s) h 0.000000000000000000000000000000000662607015
h_bar reduced Planck constant h_bar 0.000000000000000000000000000000000105457181765
k_b Boltzmann constant (J/K) k_b 0.00000000000001380649
sigma_sb Stefan-Boltzmann constant sigma_sb * 5772 ^ 4 12902411456/205 (the Sun's surface flux, about 62.9 MW/m²)
m_sun solar mass (kg) m_sun / m_earth 332954.355179
r_sun solar radius (m) r_sun / r_earth 109.197928112
l_sun solar luminosity (W) l_sun / (4 * pi * (1 au) ^ 2) 1361.16646541 (the solar constant)
m_earth Earth mass (kg) m_earth / m_moon 81.342958322
r_earth Earth radius (m) r_earth / r_moon 3.66697363877
m_moon lunar mass (kg) m_moon / m_earth 0.0122936271391
r_moon lunar radius (m) r_earth / r_moon 3.66697363877

Dates and time. jd(y, m, d [, hr]) and mjd(...) turn a calendar date into a Julian Date, now() reads the current instant (the value here is from this writing; yours will differ):

jd(2000, 1, 1, 12)
2451545
mjd(2000, 1, 1, 12)
51544.5

delta_t(jd) is the TT - UT1 correction, and lst(jd, lon) is the local sidereal time in hours for a longitude in degrees east:

delta_t(jd(2026, 1, 1))
69.1
lst(jd(2026, 6, 21), 0)
17.9472349955

Reading a Julian Date back. A Julian Date is just a day count, so it needs translating before it reads as a date. The community's standard conversion (Meeus, "Astronomical Algorithms", ch. 7; the IAU SOFA jd2cal routine) turns a JD into the Gregorian calendar date and the UT clock time; the notation is the ISO 8601 date-time that JPL Horizons and astropy print. epher has all three forms as built-ins: date(jd) gives the {year, month, day}, time(jd) gives the {hour, minute, second} clock in UT, and iso(jd) spells the whole instant as YYYY-MM-DDTHH:MM:SS text:

iso(jd(2000, 1, 1, 12))
2000-01-01T12:00:00
date(jd(2000, 1, 1, 12))
{2000, 1, 1}
time(jd(2000, 1, 1, 12))
{12, 0, 0}

The three are the exact inverse of jd(): jd(date(j) ...) of what date() returns is the original JD again. The calendar switch of 1582-10-15 is handled: JDs from 2299160.5 onward read as Gregorian calendar dates, earlier ones as Julian calendar dates, and iso(0) is the epoch itself, -4712-01-01T12:00:00. time() rounds to the nearest second; iso() lets a half-second carry into the next day so the date-time stays true to the instant. JDs below 0 are outside the calendar's range and raise a domain error.

Hours, minutes and seconds. hms2deg(h, m, s) converts right ascension to degrees, dms2deg(d, m, s) converts a sexagesimal angle, and deg2hms(x) / deg2dms(x) spell an angle back as text:

deg2hms(90)
6h 0m 0s
hms2deg(6, 42, 14)
12067/120
dms2deg(-23, 26, 0)
-703/30
deg2dms(-23.4333)
-23° 26' 0"

The sky, quantified. Give each accessor a body number: Mercury 1, Venus 2, Mars 4, Jupiter 5, Saturn 6, Uranus 7, Neptune 8, Pluto 9, Sun 10, Moon 11 (Earth is 3, the observer, never a target).

Function Meaning
ra(b, jd), decl(b, jd) geocentric right ascension and declination (degrees)
dist(b, jd) distance in AU
alt(b, jd, lat, lon), az(b, jd, lat, lon) topocentric altitude and azimuth (degrees, true)
rise(b, jd, lat, lon), set(...), transit(...) events of that local solar day, as Julian Dates
mag(b, jd) apparent magnitude
phase(b, jd), illum(b, jd) phase angle (degrees) and illuminated fraction
diam(b, jd) angular diameter (degrees)
satx(5, s, jd), saty(5, s, jd), satz(5, s, jd) a moon of Jupiter (s 1-4: Io, Europa, Ganymede, Callisto) or Saturn (s 1-8: Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Hyperion, Iapetus), in the planet's radii (x west, y north, z toward you)
satsep(5, s, jd) the moon's separation from the planet's centre, in arcseconds
satphen(5, s, jd) the moon's state: 0 visible, 1 transit, 2 occulted, 3 in eclipse, 4 shadow transit
decl(10, jd(2000, 6, 21, 1.8))
23.437882351

The remaining accessors at work, one example each (dates use Greenwich, 51.4779 N, 0 E):

ra(10, jd(2000, 1, 1, 12))
281.278205832
dist(4, jd(2026, 6, 1))
2.18405254195
alt(10, jd(2026, 6, 21, 11.97), 51.4779, 0)
61.950168
az(10, jd(2026, 6, 21, 11.97), 51.4779, 0)
178.2365282
mag(11, jd(2026, 6, 21))
-9.66497374724
phase(11, jd(2026, 6, 21))
100.83
illum(11, jd(2026, 6, 21))
0.4061
diam(11, jd(2026, 6, 21))
0.519678138889

The rise, set, and transit functions answer in Julian Dates; read them back with iso() (above):

iso(rise(10, jd(2026, 6, 21), 51.4779, 0))
2026-06-21T03:42:47
iso(transit(10, jd(2026, 6, 21), 51.4779, 0))
2026-06-21T12:01:49
iso(set(10, jd(2026, 6, 21), 51.4779, 0))
2026-06-21T20:20:50

Latitudes and longitudes are degrees, east positive. Positions are geocentric unless an observer is given. Pluto rides an approximate orbit that is honest to about an arcminute, far below the accuracy of the other bodies; eclipses and conjunction searches are not included.

Optics and light. kepler(M, e) solves Kepler's equation, airmass(alt) is the sec(z) airmass, dawes(d) is the resolving power of a d-millimetre aperture in arcseconds, and dist_mod(mu) turns a distance modulus into parsecs; mag2jy(m) and jy2mag(f) convert magnitude and density:

kepler(0.3, 0.1)
0.333333124406
airmass(30)
2
dawes(100)
1.16
dist_mod(10)
1000
mag2jy(0)
3631
jy2mag(363.1)
2.5

Seasons. march_equinox(year), june_solstice(year), september_equinox(year) and december_solstice(year) return the Julian Date of each season boundary:

march_equinox(2000)
1012520636/413
june_solstice(2000)
2451716.57464
september_equinox(2000)
2451810.22761
iso(december_solstice(2000))
2000-12-21T13:37:50

The solar system in 3D. The solar3d command draws the whole system: every orbit as a curve, every body as a labelled dot, with a trail showing where it just was:

solar3d jd(2020, 7, 1)

Give the time as a constant and press the play button to watch the planets move: const t = now(); solar3d t. Drag or use the arrow keys to orbit, clear to empty, and solar3d save file.svg to export.

The ephemeris is computed by the solar-ephemeris crate (github.com/Protonmatter/sol), validated against JPL Horizons; thank you to its author. Accuracy is arcsecond-class for the Sun, Moon and planets over roughly 5000 years around the present.

1.30 Finance

epher speaks money as well as it speaks astronomy: a time-value-of-money solver, loan amortization, interest, and cash-flow analysis, all working offline. Everything in this section returns plain numbers, not currency strings, so the answers are currency-agnostic and flow straight back into arithmetic. Rates are always per period as a fraction: 0.08/12 is an 8% annual rate billed monthly, and 0.01 is 1% (the % postfix from 1.2 works too: 6 * 100% is 0.06).

The sign convention. The solver follows the calculator standard (TI): money you pay out is negative, money you receive is positive. For a loan you took, the payout is negative and the payments are positive; for a savings plan, the deposits are negative and the nest egg you collect is positive. A consistent set of five fields makes the balance zero:

pv*(1+r)^n + pmt*(1+r*begin)*((1+r)^n - 1)/r + fv = 0

Mixing the signs up (both the loan and the payments negative) reads as "the money never balances", and the solver answers with a domain error rather than a nonsense number.

The five fields in plain words. The letters come from the calculator tradition, so they are worth spelling out once. A period is one step of the problem: a month for a monthly loan, a year for a yearly one. The rate is written r throughout this guide, never i - i is the imaginary unit (1.18):

Field Plain words
n the number of periods: how many payments or compounding steps there are. A 30-year loan billed monthly runs n = 360.
r the interest rate per period, as a fraction (0.01 is 1%). An 8% annual rate billed monthly is r = 0.08/12.
pv the present value: what the loan or lump is worth today - the amount borrowed, or the price paid now.
pmt the payment: the fixed amount that changes hands every period.
fv the future value: the balance when the last period is done. 0 for a loan paid off; the savings goal for a plan.
begin 0 when payments fall at the end of each period (the default), 1 at the beginning (rent, most salaries).

The time-value-of-money solver. Five functions answer one field with the other four given. Each name says which field it answers:

Function Answers
tvm_pmt(n, r, pv, fv) the payment
tvm_n(r, pv, pmt, fv) the number of periods
tvm_i(n, pv, pmt, fv) the per-period rate (the one name keeping the old letter; the rate itself is always r)
tvm_pv(n, r, pmt, fv) the present value
tvm_fv(n, r, pv, pmt) the future value

The classic 8% mortgage: 360 monthly payments of 733.76 against a 100,000 loan:

tvm_pmt(360, 0.08/12, -100000, 0)
733.764573879

The lifetime interest is the payments times their count minus the loan:

tvm_pmt(360, 0.08/12, -100000, 0) * 360 - 100000
164155.246597

tvm_i reads the rate back out of a quoted payment (just under 8%/12, because 733.76 is rounded):

tvm_i(360, -100000, 733.76, 0)
0.00666661199068

tvm_n answers "how long". Paying 900 a month instead of the minimum:

tvm_n(0.08/12, -100000, 900, 0)
203.163223431

About 203 months instead of 360. And the bank's own quote checks out: a 100,000 loan at 5% quoting 536.82 a month really is 30 years:

tvm_n(0.05/12, 100000, -536.82, 0)
360.002521488

(Note the signs flipped: here the loan is money received, so it is positive and the payments are negative.)

tvm_fv grows a savings plan: 200 a month for 40 years at 6%:

tvm_fv(480, 0.06/12, 0, -200)
398298.146866

tvm_pv prices a stream of payments: what a fund must hold today to pay 1,500 a month for 20 years at 5%:

tvm_pv(240, 0.05/12, 1500, 0)
-227287.969611

The answer is negative because buying the fund is money out today. The other direction, how much to put aside each month to reach a goal, asks tvm_pmt with the goal as fv: 50,000 in ten years at 5% costs 322 a month:

tvm_pmt(120, 0.05/12, 0, -50000)
321.994242862

Every one of the five takes an optional last argument begin: 0 means payments fall at the end of each period (the default), 1 at the beginning (an annuity due, rent, most salaries). Beginning-of-period payments earn interest one period longer, so the mortgage payment is a little lower:

tvm_pmt(360, 0.08/12, -100000, 0, 1)
728.90520584

The rate search caps at 100% per period and the term search at ten million periods; a problem outside those ranges (or a sign pattern that never balances) reports a domain error naming what it tried.

Amortization. amort(p, r, n, k) is the remaining balance once k of a loan's payments have been made: p is the principal (the amount borrowed), r the per-period rate, n the loan's full number of payments, k how many are done. A fresh loan (k = 0) shows the principal, a paid-off one (k = n) zero:

amort(100000, 0.08/12, 360, 120)
87724.7039064

After ten years of the 8% mortgage, 87,725 still owed. A loop (1.10) turns that into the amortization schedule, one line every five years:

for k in 0 to 360 step 60 do amort(100000, 0.08/12, 360, k)
{100000, 95069.8567174, 87724.7039064, 76781.5595143, 60477.9628062, 36188.1192209, 0}

Interest. simple_interest(p, r, t) is p*r*t and compound_interest(p, r, n) is p*(1+r)^n - p; p is the principal, r the rate (a year's rate in the first, a per-period rate in the second), t the years and n the periods. Both answer the interest earned, not the balance:

simple_interest(1000, 0.05, 2)
100
compound_interest(1000, 0.05, 2)
102.5

The balance itself is plain arithmetic, which is the point of plain numbers:

1000 * 1.05 ^ 2
1102.5

Two everyday rates built the same way. The effective annual rate of a 6% nominal rate compounded monthly, and the rule of 72's doubling time at 6%:

(1 + 0.06/12) ^ 12 - 1
0.0616778118645
72 / (6 * 100%)
12

Cash-flow analysis. npv(r, flows) discounts a cash-flow list at the rate r per period: flows[1] is the outlay today, the rest arrive one period apart. irr(flows) finds the rate where the net present value is zero. Pay 100 today, receive 60 in each of the next two years:

npv(0.1, {-100, 60, 60})
500/121
irr({-100, 60, 60})
0.130662386292

The investment earns 13.07%. (500/121 is epher's exact-fraction display, 1.14, showing the value whose decimal repeats; dec(500/121) spells it as 4.13223140496.) The decision rule: take the project when npv at your hurdle rate is positive, that is, when irr beats the hurdle. At a 15% hurdle this one fails:

npv(0.15, {-100, 60, 60})
-1300/529

A flow list whose entries never change sign has no meaningful rate; irr reports a domain error for those.

Everyday one-liners. The compound annual growth rate of an investment that went 1,000 → 2,400 in five years, and the real return of a 7% nominal return against 2% inflation (the exact-fraction display again; dec(5/102) is 0.0490196078431):

(2400/1000) ^ (1/5) - 1
0.191357898167
1.07 / 1.02 - 1
5/102

The script collection ships 42 ready-made finance scripts building on these ten functions, four folders: interest, investing, loans, and savings, each with a transcript verified against the engine (scripts.html lists them with one-line summaries).

2. The web app (PWA)

2.1 Opening it

The web app lives at:

https://epher.org/pwa/

No installation is needed. It works in any modern browser on a computer, phone, or tablet.

This guide is also built into the app: open Help → In-app user guide in the menu bar (tap ☰ on a phone) to read it inside the app, in the app's current language. Help → Constants opens the constants browser: every builtin constant in groups (Math, Astronomy, Physics, Chemistry), each with its value and a short description; tap one to insert its name into the entry field, and the search box narrows the list. Tap any example in the guide to load it into the entry field.

2.2 Your first calculation

  1. Click the text field (it is already focused when the page loads).
  2. Type an expression, for example 2 + 3 * 4.
  3. Press Enter or click the = button.

The result appears in large text below the field. Everything from chapter 1 works here, including variables, functions, and scripts. A short single answer stays on that line. A longer answer - a script's transcript with several answers, a table, a long number - appears in the result pane, the pane that also shows graphs, with one answer per line; on a phone the pane slides into view on its own. A copy icon sits just left of the answer: press it and the answer's values land on the clipboard, one per line; the result pane's long answers copy the same way.

While you type a name, a suggestion list appears beneath the field: the arrows move the highlight, Enter or Tab accepts, Esc closes, and a click accepts without leaving the keyboard. Each suggestion carries a short description of the function or constant. F1 shows the same description for the word under the cursor in the hint bar above the keypad. If the first thing you type into an empty field is an operator (+ - * / ^ % !), epher inserts ans for you, so the line continues from the previous answer.

The on-screen keypad types for you, exactly as if you had pressed the keys. Its banks cover the whole language: 123 digits and operators, trig, ƒ functions, nΣ numbers and statistics, data matrices, lists, and strings (the bank that types [[1, 2], [3, 4]], {1, 2, 3}, and "text"), 0x conversions and bases, dist regression and distribution families, $ finance, π∇ constants and graph commands, and ☉ astronomy (the one bank that scrolls). Every function in this guide is on a key somewhere. Turn on key hints (the ? button) to caption every key, or rest on a key to read what it does.

The Settings menu (the gear icon, or ☰ → Settings on a phone) holds three groups. Theme and Language do what their names say. Results shapes how answers are shown: exact fractions (on by default, so 1 / 3 displays as 1/3), the notation (Auto, Scientific, or Engineering), and thousands separators. These are display settings only; the values underneath stay ordinary numbers.

2.3 History

Every calculation is added to the history list beneath the result, so you can scroll back and see what you did. Newest entries appear at the top, and the trash icon to the left of the History heading empties it (in the terminal, Ctrl+L, or a click on the same icon). The history is kept while the page is open.

Each entry sits between thin rules: a single-line expression is one row, and a multi-line script is one entry showing all of its lines. Click an entry to load it back into the entry field and run it again.

2.4 Graphing

Type graph followed by an expression and press Enter:

graph x ^ 2

epher draws the curve y = f(x) from x = −10 to x = 10 beneath the input, on a grid with labelled axes. You can graph any expression, including your own functions:

def f(x) = x ^ 3
graph f(x)

Every graph line adds another curve to the same plot, each with its own colour, and a legend naming them. The curves are all solid, so the legend and the captions are what tell them apart without colour. graph clear empties the plot, and a Clear graph button at the top of the graph pane does the same for curves and 3D surfaces together. The TUI keeps the command in its Graph menu.

At the top of the graph pane, beside Clear graph, Copy SVG, and Save PNG, the toolbar can hide the list of points of interest and the highlighted points drawn on the plot itself. Directly above every plot sits a strip of icon-labelled sliders, the words in each one's tooltip: line thickness (0 to 4 in steps of 0.1 for 2D curves, 0 to 0.4 in steps of 0.05 for 3D plots, with 0.2 the default - only the kind in view is shown, and each kind remembers its own value), plus zoom on every plot and, on 3D and solar plots, the horizontal and vertical rotation speeds. Pressing a slider's icon resets that slider to its default. Every legend entry has a checkbox, checked by default: unchecking a curve, a 3D surface, a space curve, or a solar body hides it from the plot, its points of interest, and the SVG export.

graph x ^ 2
graph x ^ 3

Points where the expression has no value (a division by zero, for example) are skipped, leaving a gap in the curve. A jump that is really a vertical asymptote is never drawn as a connecting line.

2.4.1 What you can plot

A domain of your choice:

graph sin(x) from 0 to 2*pi

Parametric curves, with t running from 0 to 2π:

graph param 2*cos(t), 3*sin(t)

Polar curves:

graph polar 1 + cos(theta)

Regions: y < shades the area below the curve, y > shades above:

graph y < x ^ 2

2.4.2 Reading the plot

Trace: move the pointer over the plot, or focus it and press the arrow keys. The nearest point on a curve is marked, with its coordinates announced beneath the plot.

Points of interest: after every graph command epher finds the roots and turning points of each curve and the intersections between curves, marks them on the plot, and lists them beneath it:

root (-1, 0)   minimum (0, 0)   root (1, 0)

Tables: the table command prints a table of values (rows where the expression has no value are blank). An optional derivative <expr> clause adds a third column, the numeric derivative of that expression at each x:

table x ^ 2 from -2 to 2 points 5 derivative x ^ 2
         x           y          y'
        -2           4          -4
        -1           1          -2
         0           0           0
         1           1           2
         2           4           4

Table cells follow the Results settings: with exact fractions on (the default), a value that is a simple fraction shows as one, table x / 3 from 0 to 1 points 4 lists 1/3 instead of 0.333.

table x ^ 2 from -2 to 2 points 5
         x           y
        -2           4
        -1           1
         0           0
         1           1
         2           4

Two more clauses cover the rest of the work. values <list> takes the x column from a data list instead of an even grid: hand it your data and see it transformed:

d = {1, 2, 3, 4, 5}
table x ^ 2 values d
         x           y
         1           1
         2           4
         3           9
         4          16
         5          25

And exact / approx force the cell display for one table: exact shows simple fractions wherever they fit, approx shows decimals; each overrides the Results settings for that one command.

2.4.3 Sliders and export

Define a constant, use it in a graph, and a slider appears beneath the plot. Drag it (or move it with the arrow keys) and every curve redraws:

const a = 1
graph a * x ^ 2

Every plot zooms the same way: scroll the mouse wheel over it (or pinch on a touch screen) to zoom in and out, and the zoom slider above the plot moves with you. Data plots - scatter, histogram, and box-and-whisker - zoom exactly like curves.

Copy SVG copies the current plot as a self-contained SVG image for pasting into documents. The colours are baked in and the background is transparent, so it sits on any page, slide, or document. Save PNG saves the same picture as a bitmap at twice its size, so curves stay crisp, with the same transparent background; the desktop app asks where to put it, the browser app saves it to your downloads (or asks, where the browser offers to). The slider rows and any animated constants sit directly beneath the plot, above the points-of-interest list. An animated slider is a real control: grab its indicator and drag it to a new value (grabbing stops the animation first), or nudge it with the arrow keys while it is focused.

2.4.4 3D surfaces

graph3d plots a surface z = f(x, y) over a square domain (−5 to 5, or your from a to b):

graph3d x ^ 2 - y ^ 2

Mesh lines nearer to you are drawn stronger, so the shape reads in depth. Several graph3d lines overlay, like curves, and graph3d clear empties the plot. The pane shows one kind at a time: drawing a surface clears the 2D curves, and drawing a curve clears the surfaces. Each plot keeps its full size. Rotate the view by dragging, or focus the plot and use the arrow keys. The terminal UI draws the same surface as an ASCII wireframe, with the arrow keys rotating it.

Space curves plot with param: graph3d param cos(t), sin(t), t draws a helix over the t range you give (from 0 to 6.28318), or 0 to 2pi by default. Same pane, same rotation, zoom, and animation.

2.4.5 Animation

Every slider has a play button. It steps its constant through the slider's range and loops around: the indicator travels the full track and returns to the start when it reaches the end. This is the standard way calculators animate: you animate a parameter, and everything that uses it moves. Press the button again to pause.

A "time" variable is just a constant you animate:

const t = 0
graph sin(x - t)

Playing t's slider makes the wave travel. 3D surfaces animate the same way. Define a constant first, then play its slider:

const a = 1
graph3d sin(a * (x ^ 2 + y ^ 2)) from -3 to 3

In the terminal UI, the space bar starts and stops the animation.

2.5 Installing it and using it offline

The web app is a progressive web app: after one visit it works fully offline, and you can install it like a normal app.

Once installed, launch it from your home screen or app list. It opens instantly, even with no internet connection.

2.6 What the web app does not do

The web app keeps your work to the current session: it evaluates expressions, graphs them (section 2.4), and keeps a history. The save, save script, and language commands work in the desktop, command line, and terminal versions (chapters 3, 4, and 5). In the web app they answer with a note that saving works there. Your recent activity is remembered - the history, your variables and functions, and the last answer survive closing the app, kept in this browser only.

3. The desktop app

The desktop app is a normal window around the same web app. Everything in chapter 2 applies; the difference is only how you install and start it.

3.1 Installing

Download one installer for your system from the epher website:

sudo apt install ./epher-linux-x86_64.deb
sudo dnf install ./epher-linux-x86_64.rpm
chmod +x epher-linux-x86_64.AppImage
./epher-linux-x86_64.AppImage

Or install from a repository or store: updates then arrive with your package manager. On Debian/Ubuntu:

curl -fsSL https://epher.org/apt/KEY.gpg | sudo tee /etc/apt/keyrings/epher.gpg >/dev/null
curl -fsSL https://epher.org/epher.sources | sudo tee /etc/apt/sources.list.d/epher.sources >/dev/null
sudo apt update && sudo apt install epher

On Fedora:

curl -fsSL https://epher.org/epher.repo | sudo tee /etc/yum.repos.d/epher.repo >/dev/null
sudo dnf install epher

ARM64 builds of the .deb, .rpm, and AppImage are on the download page.

Every installer contains the whole epher: the desktop app, the command line (chapter 4), and the terminal UI (chapter 5), as the single epher command. On Linux, the package puts epher in /usr/bin.

3.2 Using it

Launch epher like any other application. You get a window with the same interface as the web app: type an expression, press Enter or click =, and read the result. Graphing works here too. graph x ^ 2 draws in the window (chapter 2.4). The window can be window. The menu bar includes Help → In-app user guide, the same guide as this page, with tap-to-load examples.

You can also open it from a terminal: a bare epher (or epher gui) starts the desktop app. On macOS, use the Install the epher command button inside the app to put epher on your terminal PATH.

3.3 Storage: one store with the CLI and TUI

The desktop app shares its storage with the command line and terminal versions. Functions, constants, scripts, history, and the language preference live in one place, ~/.epher on your computer (or EPHER_STORE_DIR, chapter 4.6), and everything saved in one version is available in the others:

def area(w, h) = w * h
save area

Define area in the desktop app, save it, close the window. Then open the CLI and area(3, 4) just works. It works the other way too: functions and scripts you saved in the CLI or TUI are already there when the desktop window opens, including variables set by saved scripts. The save, save script, and language commands from chapter 4 work exactly the same here.

Commands you type in the CLI, the REPL, the TUI, or the desktop app all join the same history, and the session travels too: variables you assign and the ans value follow you from one version to the next. The shared store is live: while two versions are open at once, a change in one is reflected in the other immediately (the desktop app and the TUI watch the store and refresh in place).

The web app in the browser is the one version that does not use this storage. It keeps each session to itself (chapter 2.6).

4. The command line (CLI)

The CLI is the text side of the same epher program as the desktop app. It has three modes: one-shot evaluation, piped scripts, and an interactive session for longer work.

For help at any time, run epher --help (all commands, with examples) or epher help (the full manual; on Linux packages this is the man epher page).

Every installer carries the whole script collection with it - the same scripts the website's Scripts page browses - and installs it beside the program. The terminal commands you can copy on the website, in the scripts' README, and below all point at that installed folder, so they work as soon as epher is installed:

# Debian, Ubuntu, Fedora (deb, rpm)
epher /usr/lib/epher/scripts/astronomy/moon/full-moons.epher

# Windows (PowerShell)
epher "$env:LOCALAPPDATA\epher\scripts\astronomy\moon\full-moons.epher"

# macOS
epher /Applications/epher.app/Contents/Resources/scripts/astronomy/moon/full-moons.epher

4.1 One-shot calculations

Give the expression as an argument:

epher "2 + 3 * 4"
14

You can do anything from chapter 1 that is a single expression:

epher "if 3 > 2 then 10 else 20"
10

An expression that starts with a minus sign works directly:

epher "-2 + 5"
3

One-shot mode is for scripts, from a single expression up to a whole program. Each statement's value prints on its own line:

epher "x = 10; x + 5"
10
15

Statements joined with newlines work the same way inside the argument. Anything from chapter 1 is available: variables, functions, loops, everything. The lines share one session, like a piped script (section 4.2).

4.2 Piped scripts

epher - reads expressions from standard input, one line at a time, the way scripting languages are used in pipelines:

# Linux and macOS (bash, zsh)
printf "x = 3\nx * 10\n" | epher -

# Windows PowerShell
"x = 3`nx * 10" | epher -
= 3
= 30

Everything from chapter 1 works, and the lines share one session: a function defined on an early line is available later, and save writes to the same store as always. Errors print and the script keeps going. A line may join several statements with ;. Newlines and ; mean the same thing everywhere in epher.

A file works the same way: epher plots/sine.es runs every line of the file in order and prints each result. The argument is treated as a file when it names an existing file and contains a ., / or \ - so epher x still evaluates the name x.

4.3 The interactive session (REPL)

Start it with epher repl:

epher repl

A bare epher with no arguments opens the desktop app (chapter 3).

epher prints its prompt and waits:

epher>

Now type anything from chapter 1, one line at a time. Variables keep their values between lines:

epher> x = 5
= 5
epher> x ^ 2
= 25

The table command (section 2.4.2) prints a table of values here too:

epher> table x ^ 2 from -2 to 2 points 5
         x           y
        -2           4
        -1           1
         0           0
`graph` lines plot here too: the curves build up across lines, and
`graph save plot.svg` writes the same SVG image the web app's
**Copy SVG** button yields. `graph3d save file.svg` saves a 3D surface
the same way. The same graph lines work in one-shot and piped scripts:
`epher "graph sin(x); graph save plot.svg"` is a complete plot in one
command.

         1           1
         2           4

Each answer is shown as = result. To leave, type quit (or exit):

epher> quit

Your history is remembered: the next time you run epher repl, the previous session's lines are still there.

The load command runs a script - a file path, or the name of a script you saved with save script - line by line, exactly as if you had typed it:

epher> load plots/sine.es
epher> load my_setup

4.4 Saving functions, constants and scripts

Define a function, then save it:

epher> def fib(n) = if n <= 1 then n else fib(n - 1) + fib(n - 2)
epher> save fib
saved fib

The save fib command stores the function on disk. Next time you start the session, fib is already defined:

epher> fib(10)
= 55

Constants save the same way. save on the constant's name:

epher> const tax = 0.2
= 0.2
epher> save tax
saved tax

To save a whole script (the last line you typed) use save script:

epher> x = 0; while x < 5 do x = x + 1; x
= 5
epher> save script count_to_five
saved script count_to_five

Saved scripts run automatically when epher starts, so anything they define is ready for you.

You can also load a saved script on demand with load count_to_five, or keep it as a plain file and run load count_to_five.es; epher count_to_five.es runs it straight from the command line (section 4.2).

4.5 Changing the interface language

The interface language is chosen from the languages you set on your device. To override it, type language followed by one of: en, zh-CN, hi, es, fr, ar, de, pt:

epher> language fr
language set to fr

The choice is remembered for next time. Note: the language you type, the expression language, is always the same, in any interface language.

4.6 Where your data lives

Functions, scripts, history, and your language choice are stored in one folder on your computer:

~/.epher

Delete that folder to start completely fresh. To use a different location, set the environment variable EPHER_STORE_DIR before starting epher:

EPHER_STORE_DIR=/tmp/my-epher epher repl

5. The terminal UI (TUI)

The TUI is a full-screen version of the interactive session, inside your terminal. It is part of the same epher program. Start it with:

epher tui

5.1 The screen

The screen is divided into panels:

5.2 Keys

Key Action
Type add to the expression at the cursor
Enter evaluate the whole script (a multi-line entry runs as one history item)
Shift+Enter start a new line
← → ↑ ↓ move the cursor (with empty input: rotate the 3D graph)
Esc clear the input line
F1 describe the function under the cursor (in the answer line)
Ctrl+C quit
q quit (when the input is empty)
Arrow keys rotate the 3D view (when the input is empty)
Space start/stop the animation (when the input is empty)
F9 open the menus (File, Edit, Graph, Settings, Help)
Tab focus the always-visible keypad (or history, from the keypad); switch its banks (Esc returns focus to typing)
Mouse click menus and popup items, keypad cells and bank tabs, and history lines (loads the expression); drag the graph panel to orbit (3D) or pan (2D), the wheel zooms, a double-click resets the view
Ctrl+L clear the history

The Help menu opens the in-app guide, the keypad key help, and a constants browser: the builtin constants in groups, arrows choose a row, Enter inserts its name into the expression at the cursor, and Esc closes.

The keypad's banks hold every function, constant, and command the language supports, in the web keypad's tab order: trig, fn, num, data, dist, fin, 0x, var, and astro (the digits bank is 123). The 0x bank holds the exactness and base conversions (frac, dec, big, bin, oct, hex) and the factorial !. Arrow keys move the highlight, Enter inserts the token, and Tab cycles the banks. An operator typed into an empty line (or inserted from the keypad) adds ans first, so the line continues from the previous answer.

The Settings menu offers the same result display choices as the web app (exact fractions, notation, thousands separators), next to the theme and language rows.

5.3 Graphing

Type graph followed by an expression, and press Enter:

graph x ^ 2

epher samples the curve from x = −10 to x = 10 and draws it as an ASCII plot in the Graph panel; the legend above the plot names what is plotted.

graph clear empties the plot, and the Graph menu does the same; the Help menu opens this guide inside the TUI (arrow keys scroll, Esc closes). The Settings menu can hide the points of interest listed under the plot.

You can graph any expression, including your own functions. First define one, then graph it:

def f(x) = x ^ 3
graph f(x)

Every graph line adds a curve to the plot, drawn with its own symbol (o, x, +, *); graph clear empties the plot. The same grammar as the web app applies: a domain (graph sin(x) from 0 to 2*pi), parametric curves (graph param 2*cos(t), 3*sin(t)), polar curves (graph polar 1 + cos(theta)), and regions (graph y < x ^ 2 shades the area below the curve).

Points where the expression has no value (for example division by zero) are simply skipped, leaving a gap in the plot. After every graph command the TUI lists the points of interest (roots, turning points, and intersections) under the plot. The table command (section 2.4.2) works here too.

graph3d x ^ 2 - y ^ 2 plots a 3D surface as an ASCII wireframe. Rotate it with the arrow keys while the input is empty, and press the space bar to animate a slider constant (section 2.4.5). The bottom hint line shows the arrow-key and space hints only while a 3D surface or an animatable curve is displayed.

graph save plot.svg writes the current plot as the same SVG image the web app's Copy SVG button yields; graph3d save file.svg saves the 3D wireframe from the angle you are looking at.

5.4 Saving and persistence

The TUI shares its storage with the CLI: everything saved in one is available in the other. Functions, scripts, history, and the language preference live in ~/.epher (chapter 4.6), and the same save, save script, and language commands work here.

6. Your data and privacy

All five versions run the calculation entirely on your device. Nothing is sent anywhere.