draftply Download

Function reference

A complete overview of every built-in function, operator and constant.

Free vs. Pro

Free

CategoryFunctions / features
Constantspi, e, inf, true, false, today, now
Operators+ - * / ^ %, < > <= >= == !=, and or not, ..
Trigonometrysin, cos, tan, asin, acos, atan, atan2
Mathsqrt, abs, exp, log, log10, log2, floor, ceil, round, min, max, pow, sign, clamp, lerp, mod
Number systems0x/0b/0o literals, hex, bin, oct, dec, band, bor, bxor, bnot, shl, shr
Complex numbers3i literals, re/im/conj/arg, units, exp/log/sin, lists, matrices
Stringsstr, ord, char, print, s[i], == / !=, +
Date & timedate, datetime, day, month, year, hour, minute, weekday, days, addmonths, addyears, monthstart, monthend, dformat
Proportionproportion
CSVcsv, csvwrite (manual load/save)
LanguageVariables, labels, line references (@n), comments, units
Files & sharing.dply / .dplylib / .dplybundle, share, open-with
ShortcutsDesktop keyboard shortcuts

Pro

CategoryFunctions / features
Matricesdet, inv, transpose, trace, norm, eye, zeros, ones, size
Listslen, range, push, pop, first, last, sort, reverse, contains, list, append, keys, values, map, filterlen, sort and contains count as list functions even when applied to text
Statisticssum, mean, median, std, variance, mode, percentile, quantile, corr, cov
Financepv, fv, pmt, nper, rate, npv, irr
Control flowfor / while loops with break / continue, if / elif / else
Chartsplot, scatter, bar, hist, pie, surface, scatter3
Symbolic algebrasolve, simplify, factor
Differentiationdiff (symbolic and numeric)
Integrationintegrate (symbolic and numeric)
User-defined functionsf(x) = …, def name(…): (including recursion)
Classes & objectsclass Name(…):, methods, instances
Form viewTurn a sheet into a fillable form (inputs, outputs, charts, CSV)

Constants

NameValue / meaning
piπ ≈ 3.14159265358979…
eEuler's number ≈ 2.71828182845904…
infInfinity (∞)
trueTruth value
falseTruth value
todayToday's date (no time component)
nowCurrent date and time

Truth values are their own kind of value, so a line that answers a question shows the answer and not a 1:

done = false               → false
done = 5 > 3               → true
not done                   → false
"Status: {done}"           → Status: true
[true, false, true]        → [true, false, true]

They still count as 1 and 0 wherever a number is wanted. Nothing that used 1/0 for truth stops working:

sum([true, false, true])   → 2      (counts the true ones)
true + 1                   → 2
true == 1                  → true
if 1:                      → runs   (any number ≠ 0 is true)

Text is the exception, on purpose: "true" == true is false rather than sneaking through as 1 == 1.

Like pi, they are constants and not reserved words — true = 5 overwrites the name for the rest of the document.

Operators

Arithmetic

OperatorMeaningExample
+Addition3 + 47
-Subtraction10 - 37
*Multiplication2 * 510
/Division7 / 23.5
^Power2^8256
%Modulo10 % 31

A number or parenthesised expression directly in front of a parenthesis is an implicit multiplication: 3(x + 5) = 3 * (x + 5), (1 + 2)(3 + 4)21.

A parenthesised expression or a function call's argument list can span several lines — draftply keeps reading until the brackets close:

total = (
  100 +
  50
)

sum(
  100,
  50,
  25
)

A leading minus binds looser than ^, as in mathematics and in spreadsheets: -2^2 is -(2^2)-4. Write (-2)^24 for the square of the negative number.

Magnitudes. A k or M directly behind a number is the thousandfold or the millionfold, the way prices and key figures are written:

25k          → 25000
1.5M         → 1500000
25k€ + 5k€   → 30000 €

Only when no letter follows, so units keep their meaning: 25kg is 25 kilograms and 2km is 2 kilometres, not 2000 anything. A lower-case m is the metre and never a magnitude.

With units, +, - and % need the same quantity on both sides. Different prefixes convert (5 km + 3 m5.003 km), different quantities are reported (2 m + 3 kg, 5 € + 3 $). A plain number on one side stays fine — 2 m + 3 is 5 m. and / are exempt: combining quantities is what they do, so 2 m 3 kg is 6 m·kg.

Compound assignment

+=, -=, *= and /= combine an assignment with an arithmetic operator — x += 1 is short for x = x + 1:

x = 5
x += 3      → 8
x -= 2      → 6
x *= 4      → 24
x /= 3      → 8

They work on list entries and object fields too:

depot = list(0)
depot[-1] += 100        → depot is now [100]

c = Circle(5)
c.r *= 2                → c.r is now 10

The variable must already have a value — y += 1 on an undefined y is an error, same as plain y + 1 would be.

Percentages

A % written after a value is a percentage: 19% is 0.19. It binds tighter than * and /, so the percentage stays in one piece — 238 / 119% is 238 / 1.19200, not (238 / 119) / 100.

With + and - a percentage is taken of the left-hand value — that is how you add or remove a markup:

ExpressionResultReads as
100 + 10%110100 plus 10 % of 100
4 + 20%4.84 plus 20 % of 4
250 - 15%212.5250 minus 15 % of 250
100 + 10% + 10%121two markups in a row

This applies to a percentage written directly to the right of + or -, or one further divided by a plain number — useful for taking only part of a written percentage: 10% / 10 is still "1 %", so 100 + 10% / 10 is 101. Multiplying a percentage, or using one as a divisor, drops back to its plain decimal value instead:

1200 * 19%          → 228     // * and a percentage as divisor are unaffected
4 + 20% * 2         → 4.4     // the right side is a product, not a percentage
100 + 10% / 10      → 101     // 10% / 10 is still "1 %"
p = 20%
4 + p               → 4.2     // p is 0.2; there is no % in the expression
4 + 5/100           → 4.05    // a real division, not a percentage

A % between two values is the modulo operator (10 % 31).

Comparison

OperatorMeaning
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal
==Equal
!=Not equal

Result: a truth valuetrue or false.

Values with units are converted before comparing, so 2 m > 100 cm is true and 1 m == 100 cm holds. Currencies are compared as written (no exchange rates).

Different quantities have no order, so 5 km > 3 kg is reported rather than answered — the same rule min and max follow. == and != do answer: a length is never equal to a mass, so 5 km == 3 kg is false.

Logic

OperatorMeaningExample
andLogical andx > 0 and x < 10
orLogical orx < 0 or x > 10
notNegationnot (x == 0)

and, or and not return a truth value and accept any number as their input — 0 is false, everything else true. So they combine comparisons and the constants true/false alike: not truefalse, flag or x > 10. A form chip that writes 1 or 0 into a variable still drives them.

Conditional expression

value_if_true if condition else value_if_false — an inline conditional (Python-style). It makes one-liner functions and recursive definitions practical:

discount(n) = 0.1 if n > 100 else 0
fact(n)     = 1 if n <= 1 else n * fact(n - 1)
grade(p)    = "A" if p >= 90 else "B" if p >= 80 else "C"

It chains right-associatively, so a if c1 else b if c2 else c reads as a if c1 else (b if c2 else c).

There is no C-style a ? b : c. Writing one is reported as an error that names the supported form — it used to be accepted silently and evaluated to a.

Range

SyntaxMeaning
a..bInteger range from a to b (inclusive)

Used in for loops and plot.

Trigonometry

All angles are in radians.

FunctionSyntaxResult
sinsin(x)Sine of x
coscos(x)Cosine of x
tantan(x)Tangent of x
asinasin(x)Arcsine, result in [−π/2, π/2]
acosacos(x)Arccosine, result in [0, π]
atanatan(x)Arctangent, result in (−π/2, π/2)
atan2atan2(y, x)Arctangent of y/x (preserves the quadrant)
sin(pi / 2)      → 1
cos(0)           → 1
atan2(1, 1)      → 0.7853… (= π/4)

Basic math functions

FunctionSyntaxResult
sqrtsqrt(x)Square root of x
absabs(x)Absolute value
expexp(x)
loglog(x)Natural logarithm (base e)
log10log10(x)Logarithm base 10
log2log2(x)Logarithm base 2
floorfloor(x) / floor(x, digits)Round down; to digits decimals if given
ceilceil(x) / ceil(x, digits)Round up; to digits decimals if given
roundround(x) / round(x, digits)Round half away from zero; to digits decimals if given
convertconvert(x, "from", "to")Convert between units, incl. temperatures (see below)
defunitdefunit("name") / defunit("name", "base") / defunit("name", factor, "base")Define a custom unit (see below)
minmin(a, b) / min(list)The smaller of two values, or the smallest element of a list
maxmax(a, b) / max(list)The larger of two values, or the largest element of a list
powpow(x, n)x to the power of n (same as x^n)
signsign(x)Sign: −1, 0 or 1
clampclamp(x, min, max)Clamp x to the range [min, max]
lerplerp(a, b, t)Linear interpolation: a + t*(b−a); t=0 → a, t=1 → b
modmod(a, b)Remainder of a÷b (same as a % b, but as a function)
sqrt(144)          → 12
log(e)             → 1
log10(1000)        → 3
floor(3.7)         → 3
ceil(3.2)          → 4
round(3.14159, 2)  → 3.14
round(3.14159 m, 2)→ 3.14 m      (keeps the unit)
sign(-5)           → -1
min(3, 5)          → 3
max([3, 1, 2])     → 3
clamp(15, 0, 10)   → 10
min(5 km, 300 m)   → 300 m       (compared in SI, keeps its unit)
max(2 kWh, 500 Wh) → 2 kWh
abs(-3 m)          → 3 m
max(cost, 0)                     (a plain number still works as a bound)
min(5 km, 3 kg)    → error       (different quantities)
clamp(-3, 0, 10)   → 0
lerp(0, 100, 0.25) → 25
mod(17, 5)         → 2

Unit conversion — convert

convert(value, "from", "to") converts a value between units. The units are given as text. Temperatures (C/°C, F/°F, K) are converted with their offset; all other units multiplicatively. Only units of the same physical kind convert into each other (length↔length, not length↔mass), and currencies are rejected (no fixed rate).

convert(20, "C", "F")      → 68
convert(0, "C", "K")       → 273.15
convert(5, "km", "m")      → 5000 m
convert(1000, "nm", "µm")  → 1 µm
convert(5, "mm", "µm")     → 5000 µm
convert(2, "h", "min")     → 120 min
convert(36, "km/h", "m/s") → 10 m/s
convert(5, "km", "cm")     → 500000 cm   (exact — no rounding artifacts)

Length supports the full metric range: nm, µm (or um), mm, cm, dm, m, km. Both spellings work inline as well: 5 µm + 3 µm is the same as 5um + 3um.

Conversions across many powers of ten are computed exactly: the decimal shift is applied to numerator/denominator instead of multiplying by an inexact factor, so integer results such as convert(5, "km", "cm") → 500000 stay clean.

Imperial units. Common imperial units are built in and mix freely with metric ones via convert:

Length: in, ft, yd, mi Mass: lb, oz * Volume (US measures): gal, qt, pt, floz

convert(1, "mi", "km")     → 1.609344 km
convert(1, "mi", "ft")     → 5280 ft
convert(1, "lb", "oz")     → 16 oz
convert(1, "gal", "L")     → 3.785411784 L
3ft + 2ft                  → 5 ft

The result carries the target unit (except temperatures, which are plain numbers), so it keeps calculating: convert(5, "km", "m") + 100 m5100 m.

More physical quantities. Beyond length, mass and time these units are built in and convert within their own kind:

Volume (metric): ml, dl, L (or l) Energy: J, kJ, Wh, kWh Power: W, kW, MW Force: N, kN Pressure: Pa, bar Frequency: Hz, kHz, MHz

convert(1, "kWh", "J")     → 3600000 J
2kWh + 500Wh               → 2.5 kWh
convert(1, "bar", "Pa")    → 100000 Pa
1.5L + 3dl                 → 1.8 L

Custom units — defunit

Define your own units, e.g. for an electrical-engineering library. Once defined, a unit works both inline (12 V) and in convert.

FormMeaning
defunit("V")New base unit with its own dimension
defunit("Ohm", "V/A")Derived unit (factor 1) from existing units
defunit("kOhm", 1000, "Ohm")Scaled unit: 1 kOhm = 1000 Ohm
defunit("V")
defunit("A")
defunit("Ohm", "V/A")
defunit("kOhm", 1000, "Ohm")
convert(1, "kOhm", "Ohm")  → 1000 Ohm
U = 12 V                   → 12 V

Custom units are document-local (they reset per document). Put defunit(...) calls at the top of a .dplylib library and import it to share a unit set across documents. Only units of the same dimension convert into each other.

A unit may be a symbol. The name does not have to be a word — anything that is not a digit or an operator works, so the unit can be spelled the way it is printed:

defunit("V")
defunit("A")
defunit("Ω", "V/A")
defunit("kΩ", 1000, "Ω")
R = 2 kΩ + 500 Ω           → 2.5 kΩ
Z = 30 + 40i Ω             → 30 + 40i Ω

Multiplying and dividing units reduces the result to its base unit whenever one has the same dimension, converting the value accordingly. Because Ohm is defined as V/A, the compound units cancel to the expected named result:

470 Ohm * 0.01 A → 4.7 V        (Ohm·A cancels to voltage)
5 V / 0.025 A    → 200 Ohm      (reduced to base Ohm, not 0.2 kOhm)
12 V / 4 A       → 3 Ohm
1 / Ohm          → 1 1/Ohm      (no built-in name for a reciprocal; defunit("S", "1/Ohm") to give it one)

Note: a magnitude prefix like k/m/M (25k, 5 mm) only works for the metric length/mass/time units built into draftply. It does not compose with a unit you defined yourself — 10 mA after defunit("A") is not recognised and reports an error, so write out the number instead (0.01 A).

Two units of the same dimension combine into a power (the value is converted): 5 mm 10 m → 0.05 m², 2 Ohm 3 kOhm → 6000 Ohm².

Units are reduced to SI base dimensions, so a product lands on the named unit of the dimension it actually has — and cancels against any other unit of that dimension:

1500 W * 4 h                 → 6 kWh      (power × time is energy)
5 N * 3 m                    → 15 J       (force × distance is energy)
1500 W * 4 h * 0.30 €/kWh    → 1.80 €     (the kWh cancels)

If the dimension has no named unit, the compound form is kept (100 km / 2 h → 50 km/h).

A fraction is not renamed into another quantity. Energy per distance happens to have the dimension of a force, but nobody calls fuel consumption a force — so a fraction keeps its written form whenever a named derived unit appears in it:

16 kWh/100km                 → 0.16 kWh/km   (a consumption, not 576 N)
100 J / 2 s                  → 50 J/s
10 N / 2 m^2                 → 5 N/m²

Two kinds of fraction do get a name. One where you defined the unit yourself — defunit("Ohm", "V/A") says that volts per ampere are called ohms, so 12 V / 4 A → 3 Ohm. And one where you spell a quantity out of base quantities alone, in which case the name is the answer you were after:

2 kg * 3 m / 4 s^2           → 1.5 N
1 / 2 s                      → 0.5 Hz

A power belongs to the unit, not to the quantity: 54 m^2 and 54 m² are both 54 square metres. Put the quantity in brackets to square it as a whole ((54 m)^2 → 2916 m²).

A root lowers the unit exponent — as long as it stays whole:

a = 3 m
b = 4 m
sqrt(a^2 + b^2)              → 5 m
sqrt(9 m^2 / s^2)            → 3 m/s
(8 m^3)^(1/3)                → 2 m
sqrt(4 m)                    → sqrt: m would need a fractional exponent here

m^0.5 is not something unit notation can write, so sqrt(4 m) says so instead of quietly answering 2 with no unit. A negative quantity under a root that does work out is complex, as everywhere else: sqrt(-4 m^2)2i m.

After a number, a unit name is always the unit — even if you have a variable of the same name. You can call a variable km and still write 100 km:

km = 15000 km
consumption = 6.5 L/100km
fuel_year = km * consumption   → 975 L

On its own (km * 2) the name is the variable, as usual. Only the position right after a number is reserved for the unit.

Automatic prefix for display. When a result falls outside the range [0.1, 1000), it is shown in the unit that brings it into a nice range — chosen among your own units and the metric 1000-step prefixes:

1500 m       → 1.5 km
0.008 m      → 8 mm

This is display-only: the stored value is unchanged, so @n references and further math keep full precision. Values already in range (200 Ohm, 0.5 m), compounds (50 km/h), powers (20 m²) and currencies are left as-is, and it never switches between metric and imperial.

The target unit of convert is exempt: if you ask for km, you get km (convert(1, "m", "km") → 0.001 km). Anything you calculate from that result is auto-scaled again as usual.

One unit per quantity, per sheet. Deciding the prefix value by value puts 750 kg right above 2.252 t — both correct, unreadable side by side. So when the same quantity would end up in different units, the whole sheet switches to the one that reads best for all of them:

750 kg                     → 0.75 t
2252.25 kg                 → 2.252 t
1502.25 kg                 → 1.502 t

This holds across the whole document, not just within a block of adjacent lines. It only kicks in when there is a disagreement — if every value of a quantity already shows the same unit, nothing changes. And if no single unit works for all of them, each keeps its own scaling:

5 mm                       → 5 mm      (no unit suits both, so both keep theirs)
300 km                     → 300 km

The unit you wrote can win even when it is not a 1000-step prefix: a sheet written in cm stays in cm. Results of convert and the confirmation line of a defunit do not take part — there you named the unit yourself.

Number systems (binary, octal, hex)

Numbers can be entered in binary, octal or hexadecimal using the prefixes 0b, 0o and 0x. They are ordinary numbers and mix freely with decimal values and all other functions. Conversion functions turn a value back into a string in a given base, and dec parses such a string into a number.

FunctionSyntaxResult
0x…0xFFHexadecimal literal (0xFF → 255)
0b…0b1010Binary literal (0b1010 → 10)
0o…0o100Octal literal (0o100 → 64)
hexhex(x)x as a hexadecimal string (0x…)
binbin(x)x as a binary string (0b…)
octoct(x)x as an octal string (0o…)
decdec("0xFF")Parse a 0x/0b/0o/decimal string to a number
0xFF               → 255
0b1010             → 10
0o100              → 64
0xFF + 1           → 256
hex(255)           → 0xFF
bin(10)            → 0b1010
oct(64)            → 0o100
hex(-255)          → -0xFF
dec("0xFF")        → 255
dec(hex(1234))     → 1234

Bitwise operations

Bitwise functions operate on integers (fractional parts are truncated). ^ is reserved for exponentiation, so XOR is the bxor function rather than an operator.

FunctionSyntaxResult
bandband(a, b)Bitwise AND
borbor(a, b)Bitwise OR
bxorbxor(a, b)Bitwise XOR
bnotbnot(a)Bitwise NOT (complement)
shlshl(a, n)Shift a left by n bits
shrshr(a, n)Shift a right by n bits
band(12, 10)       → 8
bor(12, 10)        → 14
bxor(12, 10)       → 6
bnot(0)            → -1
shl(1, 4)          → 16
shr(256, 2)        → 64
hex(band(0xF0, 0x3C)) → 0x30

Complex numbers

The imaginary part is a number with an i attached — 3i, 1i, 0.5i. A plain i stays an ordinary variable name, so for i in 0..10 keeps working.

sqrt(-1)              → 1i          (used to be a silent NaN)
2 + 3i                → 2 + 3i
(2 + 3i) * (1 - 1i)   → 5 + 1i
(2 + 3i) / (1 - 1i)   → -0.5 + 2.5i
(1 + 1i)^8            → 16

A result whose imaginary part cancels out is a plain number again: 1i * 1i is -1, not -1 + 0i.

FunctionSyntaxResult
rere(z)Real part
imim(z)Imaginary part
conjconj(z)Conjugate: 2 + 3i2 - 3i
argarg(z)Angle in the plane, in radians
absabs(z)Magnitude: abs(3 + 4i)5
sqrtsqrt(z)Root, also of a negative number

With a unit

A complex number carries a unit like any other quantity — that is what makes it useful for AC circuits, where an impedance is 50 + 30i Ohm. The unit goes after the whole number, not after each part, and the same unit algebra applies as for real quantities, so Ohm's law works out:

defunit("V")
defunit("A")
defunit("Ohm", "V/A")

Z = 50 + 30i Ohm      → 50 + 30i Ohm
I = 2 A               → 2 A
Z * I                 → 100 + 60i V
abs(Z)                → 58.3095189485 Ohm
re(Z)                 → 50 Ohm
100 V / 50i Ohm       → -2i A

Both parts are always scaled together for display, so a single number never shows two orders of magnitude: 50000 + 30000i Ohm reads as 50 + 30i kOhm. arg(Z) is an angle in radians and therefore stays a plain number.

Squaring raises the unit exponent just as it does for real quantities ((50 + 30i Ohm)^21,600 + 3,000i Ohm²), and a root lowers it again as long as the exponent stays whole (sqrt(Z^2)50 + 30i Ohm). Where it would not, the line is reported instead: sqrt(50 + 30i Ohm) would need Ohm^0.5, which unit notation cannot write.

Exponential, logarithm and angle functions

exp, log, log10, log2, sin, cos, tan, asin, acos and atan take complex arguments:

exp(1i * pi)          → -1              (Euler's identity)
exp(2 + 3i)           → -7.3151100949 + 1.0427436562i
log(exp(2 + 3i))      → 2 + 3i
sin(1i)               → 1.1752011936i   (= sinh(1))
cos(1i)               → 1.5430806348    (= cosh(1))

They also answer where the real versions used to return a silent NaN — the result was never undefined, just not a real number:

log(-1)               → 3.1415926536i   (πi)
log10(-100)           → 2 + 1.3643763538i
asin(2)               → 1.5707963268 - 1.3169578969i
(-8)^0.5              → 2.8284271247i   (same as sqrt(-8))
pow(-8, 0.5)          → 2.8284271247i

Each of these returns the principal value, the same branch Python, NumPy and Octave use. Where several roots exist that is not always the real one: (-8)^(1/3) is 1 + 1.7320508076i, not -2. A real argument that has a real answer keeps taking the real path unchanged, so log(0) is still -∞ and (-8)^2 is still 64.

In lists

A list may hold complex numbers and computes elementwise, the same way a list of real numbers does:

[1i, 2] * 2           → [2i, 4]
[1i, 2] + [1, 1]      → [1 + 1i, 3]
sum([1i, 2])          → 2 + 1i
mean([1i, 2])         → 1 + 0.5i

sum and mean need nothing but addition, so they work — including the unit rule of +, which makes a series circuit fall out on its own:

Zs = [50 + 30i Ohm, 20 Ohm]
sum(Zs)               → 70 + 30i Ohm
Zs * 2 A              → [100 + 60i V, 40 V]

Anything that needs an order cannot: median, sort, min, max, percentile and quantile report that complex numbers have none and point to abs(z). std, variance, mode, corr, cov and linreg are defined on real numbers and say so rather than inventing a reading.

In matrices

A matrix with one complex entry is a complex matrix, and det, inv, transpose, trace, the matrix product and integer powers all work on it — see complex entries for the mesh-analysis example.

As roots

solve returns complex roots for a quadratic with a negative discriminant — see symbolic algebra.

What complex numbers do not do yet:

  • No order. 1i < 2 is reported — compare magnitudes instead: abs(z1) < abs(z2). == and != do work.
  • Not as CAS input. solve, simplify, factor, diff and integrate compute with real coefficients, so simplify(1i x) reports. Complex results* (the roots above) do work.

Calculus Pro

Symbolic (2 arguments)

FunctionSyntaxResult
diffdiff(expr, var)Symbolic derivative with respect to var
integrateintegrate(expr, var)Symbolic indefinite integral
diff(x^3, x)          → 3x²
diff(sin(x), x)        → cos(x)
integrate(x^2, x)     → 0.3333333333x³ + C
integrate(sin(x), x)  → −cos(x) + C

Numeric (3 or 4 arguments)

FunctionSyntaxResult
diffdiff(expr, var, point)Numeric derivative at a point (central difference)
integrateintegrate(expr, var, a, b)Numeric integral from a to b (Simpson's rule)
diff(x^2, x, 3)              → 6
integrate(x^2, x, 0, 3)     → 9

Symbolic algebra (CAS) Pro

FunctionSyntaxResult
solvesolve(expr, var)Roots / solutions
solvesolve(eq1, eq2, …, var1, var2, …)Solve a system of equations
simplifysimplify(expr)Expand & combine a polynomial
factorfactor(expr)Factor an expression
solve(x^2 - 4, x)                       → x = -2,  x = 2
solve(x^2 + 2*x + 1 == 0, x)           → x = -1
solve(x + y == 3, x - y == 1, x, y)    → x = 2,  y = 1
simplify(2*x + 3*x)                     → 5x
simplify((x + 1)^2)                     → x² + 2x + 1
factor(x^2 - 4)                         → (x − 2)(x + 2)
factor(x^2 + 2*x + 1)                  → (x + 1)²

A quadratic with a negative discriminant has complex roots, and solve writes them out rather than reporting that there is no real one:

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

factor stays in the reals, where x² + 1 has no factors — factor(x^2 + 1) says so as "irreducible over the reals". Complex numbers in the input of a CAS function (simplify(1i * x)) are not supported; those report.

simplify expands and combines polynomials (powers with non-negative integer exponents). It does not cancel divisions by a variable (x^2 / x) or apply trigonometric identities (sin(x)^2 + cos(x)^2) — those report an error.

Variables that already have a value

A sheet is a notepad, so x is often taken further up. That never changes what a CAS call means: the unknown stays the unknown, even when a line above assigns it.

x = 3
solve(x^2 - 4, x)                       → x = -2,  x = 2
diff(x^2, x)                            → 2x
integrate(6*x, x)                       → 3x² + C

Every other variable is filled in from the sheet — that is what makes solve work with values coming from a form:

a = 2
solve(a*x - 8, x)                       → x = 4

simplify and factor name no unknown, so they keep every variable you set yourself symbolic; only the built-in constants (pi, e, tau) are filled in. Writing the expression without simplify is the way to get the number.

x = 3
simplify(x + x)                         → 2x
x + x                                   → 6

Matrices Pro

Inside […], , and ; separate elements and ;; separates rows:

v = [1, 2, 3]        // row vector
v = [1; 2; 3]        // row vector (semicolon also separates elements)
A = [1, 2 ;; 3, 4]   // 2×2 matrix (;; = new row)
c = [1 ;; 2 ;; 3]    // column vector (3×1)

Comma-decimal mode: when the number format uses a comma as the decimal point, , is the decimal sign, so use ; to separate values unambiguously — [1,5; 2,5] is the vector [1.5, 2.5], and ;; still starts a new row. The same applies to function arguments: sum(1,5; 2,5).

FunctionSyntaxResult
detdet(A)Determinant (square matrix)
invinv(A)Inverse (square matrix)
transposetranspose(A)Transpose
tracetrace(A)Trace (sum of diagonal elements)
normnorm(A)Frobenius norm; for a scalar: abs(x)
eyeeye(n)n×n identity matrix (up to 1000×1000)
zeroszeros(n) / zeros(r, c)Matrix of zeros
onesones(n) / ones(r, c)Matrix of ones
sizesize(A) / size(A, dim)[rows, cols]; dim=1 → rows, dim=2 → cols
det([1, 2 ;; 3, 4])        → -2
inv([1, 2 ;; 3, 4])        → [[-2, 1], [1.5, -0.5]]
transpose([1, 2 ;; 3, 4])  → [[1, 3], [2, 4]]
trace([1, 2 ;; 3, 4])      → 5
eye(3)                   → [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
zeros(2, 3)              → [[0, 0, 0], [0, 0, 0]]
size([1, 2 ;; 3, 4])       → [2, 2]
size([1, 2 ;; 3, 4], 1)    → 2

A matrix keeps the unit of its entries, and ^ needs a whole-number exponent (A^0.5 is reported — a square root of a matrix is not the matrix).

Complex entries

As soon as one entry is complex, the whole matrix is, and the same functions apply — see complex numbers:

A = [1i, 2 ;; 3, 4]
det(A)                     → -6 + 4i
A * inv(A)                 → [[1, 0], [0, 1]]

That covers mesh analysis, where the impedance matrix carries the reactances and the unit algebra follows along on its own:

defunit("V")
defunit("A")
defunit("Ohm", "V/A")

Z = [50 + 30i Ohm, -20 Ohm ;; -20 Ohm, 40 + 10i Ohm]
det(Z)                     → 1,300 + 1,700i Ohm²
inv(Z)                     → entries in 1/Ohm

Collapsing tall outputs

Multi-row matrix results can be collapsed to a one-line summary ([42×1] 1…) — click the chevron next to the line number, or tap the summary to expand again. On small screens (phones in portrait) tall matrix outputs start collapsed, like plots.

Lists & arrays Pro

Lists are internally 1×n matrices: [1, 2, 3]. They can carry a unit — see Lists with units.

FunctionSyntaxResult
lenlen(list)Number of elements
lenlen(text)Number of characters (see Strings)
rangerange(end)Integers 0 to end−1
rangerange(start, end)Integers start to end−1 (up to 100,000)
rangerange(start, end, step)With a custom step
pushpush(list, value)Append an element (returns a new list)
poppop(list)Remove the last element (returns a new list)
firstfirst(list)First element
lastlast(list)Last element
sortsort(list)Sorted ascending (returns a new list)
reversereverse(list)Reversed order (returns a new list)
containscontains(list, value)1 if present, otherwise 0
containscontains(text, part)1 if part occurs in text, otherwise 0
listlist(a, b, …)Generic list of arbitrary values (see below)
appendappend(list, value)Append to a generic list (returns a new list)
keyskeys(named list)The keys as a list (see Named lists)
valuesvalues(named list)The values as a list (see Named lists)
mapmap(f, list)Apply function f to each element
filterfilter(f, list)Keep elements where f(element) is true (≠ 0)
len([10, 20, 30])          → 3
range(5)                   → [0, 1, 2, 3, 4]
range(2, 6)                → [2, 3, 4, 5]
range(0, 10, 2)            → [0, 2, 4, 6, 8]
push([1, 2], 3)            → [1, 2, 3]
pop([1, 2, 3])             → [1, 2]
first([5, 6, 7])           → 5
last([5, 6, 7])            → 7
sort([3, 1, 2])            → [1, 2, 3]
reverse([1, 2, 3])         → [3, 2, 1]
contains([1, 2, 3], 2)     → true

Lists with units

A list can carry a unit. If every element has one, the list keeps it: it is shown on each element, list[i] gives it back, and aggregations return it.

[5 €, 3 €]                 → [5 €, 3 €]
min([5 €, 3 €])            → 3 €
sum([1 m, 2 m])            → 3 m
mean([2 m, 4 m])           → 3 m
sort([2 m, 1 m])           → [1 m, 2 m]
[1 m, 2 m] * 2             → [2 m, 4 m]

All elements share one unit — that of the first element — and different units of the same quantity are converted into it. So [1 m, 2 km] is [1 m, 2000 m], and sum([1 m, 200 cm]) is 3 m rather than 201. A list is never shown with mixed prefixes; whatever the magnitudes, the unit stays the same down the list.

A plain number among unit-carrying values is read in the base unit of that quantity — the same reading 5 km + 7 already uses:

[5 km, 7]                  → [5 km, 0.007 km]   (the 7 is 7 m)
sum([5 km, 7])             → 5.007 km           (same as 5 km + 7)
max([5 km, 7])             → 5 km
[900 €, 50]                → [900 €, 50 €]      (currency has no base scale)

Quantities that are not one quantity stay an error — a list is the wrong shape for them:

[1 m, 2 kg]                → error       (m and kg are not one quantity)

Multi-argument forms behave the same: sum(1 m, 2 m) → 3 m. Statistics that change the quantity do not claim a unit — variance([1 m, 2 m, 3 m]) is a plain number, because the variance of metres is m².

Generic lists

A generic list holds arbitrary values — whole vectors, named lists, strings or numbers — without flattening them. It is the tool for collecting results in a loop and reading them back one by one.

Create one with list(...) (or list() for an empty list) and grow it with append. A bracket literal whose elements are not plain numbers — such as [v1, v2] with vectors — also forms a generic list.

Results = list()
for i in 1..3:
  Results = append(Results, [i, i*2, i*3])

Results                    → [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
Results[1]                 → [2, 4, 6]     (the whole second vector)
sum(Results[1])            → 12
len(Results)               → 3

Access is 0-based; negative indices count from the end (Results[-1] is the last element). Results[0] = [9, 9, 9] replaces an element, for v in Results: iterates over the elements, and first/last/reverse/push/pop work element-wise. Aggregations such as sum and mean flatten all numeric values inside the list, including the elements of contained vectors.

map & filter

map(f, list) applies a function to each element; filter(f, list) keeps the elements for which the function is true. The first argument is a function name — a reference, not a call — and may be a builtin or one of your own functions. They replace many explicit for loops.

square(x) = x^2
map(square, [1, 2, 3])     → [1, 4, 9]
map(sqrt, [1, 4, 9])       → [1, 2, 3]

even(n) = n % 2 == 0
filter(even, [1, 2, 3, 4, 5, 6]) → [2, 4, 6]

sum(map(square, [1, 2, 3]))      → 14

Named lists

A named list labels each value with a key, so you can read it back by name instead of by position. Write the entries as key: value inside the brackets. Keys may be quoted ("Strom") or, if they are a single word, bare (Strom). The list can span several lines.

Expenses = [
  "Electricity": 120,
  "Water":        80,
  "Rent":        900
]

Expenses["Electricity"]    → 120
sum(Expenses)              → 1100
len(Expenses)              → 3
mean(Expenses)             → 366.67

Access and update entries by key; assigning a new key adds it:

Expenses["Internet"] = 40   // adds a new entry
sum(Expenses)               → 1140

Entries can also be read by position (0-based, like a list), which keeps the unit too:

Expenses[0]                 → 120   (first entry)
Expenses[-1]                → 40    (last entry)

List and statistics functions (sum, mean, median, std, min, max, sort, len, …) operate on the values, in the order the entries were written.

Entries may carry units, and they are kept — both when reading a single entry and when aggregating, as long as all entries share the same unit:

Costs = ["Electricity": 120 €, "Water": 80 €]
Costs["Electricity"]       → 120 €
sum(Costs)                 → 200 €
max(Costs)                 → 120 €

Values may also be anything — vectors, lists, strings — not just numbers:

Series = ["Jan": [10, 12, 9], "Feb": [11, 8, 14]]
Series["Feb"]              → [11, 8, 14]     (the whole vector)
keys(Series)               → [Jan, Feb]
values(Series)             → [[10, 12, 9], [11, 8, 14]]

keys(d) and values(d) return the keys and values as a list. Aggregations still work over the numeric entries and skip non-numeric ones.

for v in d: walks the values in the same order, keeping their units; write for k in keys(d): to walk the keys instead.

Costs = ["Electricity": 120 €, "Water": 80 €]
total = 0 €
for v in Costs:
  total = total + v

total                      → 200 €

Statistics Pro

All statistics functions accept either a list or comma-separated values.

FunctionSyntaxResult
sumsum(list) / sum(a, b, …)Sum of all values
meanmean(list) / mean(a, b, …)Arithmetic mean
medianmedian(list) / median(a, b, …)Median (middle value)
stdstd(list) / std(a, b, …)Sample standard deviation (÷ n−1)
variancevariance(list) / variance(a, b, …)Sample variance (÷ n−1)
modemode(list) / mode(a, b, …)Most frequent value
percentilepercentile(list, p)p-th percentile (p: 0–100), linear interpolation
quantilequantile(list, q)q-th quantile (q: 0–1), linear interpolation
corrcorr(xs, ys)Pearson correlation coefficient
covcov(xs, ys)Sample covariance (÷ n−1)
linreglinreg(xs, ys)Linear regression → [slope, intercept, r2]
sum([1, 2, 3, 4])            → 10
mean(2, 4, 6)                → 4
median([1, 3, 5, 7])         → 4
std(2, 4, 4, 4, 5, 5, 7, 9) → 2.1381
variance([2, 4, 4, 4, 5, 5, 7, 9]) → 4.5714
mode([1, 2, 2, 3])           → 2
percentile([1, 2, 3, 4, 5], 75) → 4
quantile([1, 2, 3, 4, 5], 0.5)  → 3
xs = [1, 2, 3, 4, 5]
ys = [2, 4, 5, 4, 5]
corr(xs, ys)                 → 0.7746
cov(xs, ys)                  → 1.5

linreg fits a straight line y = slope·x + intercept and returns a named list with slope, intercept and the coefficient of determination r2:

r = linreg([1, 2, 3, 4], [2.1, 3.9, 6.2, 7.8])
r["slope"]                   → 1.9…
r["intercept"]               → 0.1…
r["r2"]                      → 0.99…

Finance Pro

All finance functions use standard Excel-compatible sign conventions: inflows positive, outflows negative. rate as a decimal (5 % = 0.05).

FunctionSyntaxResult
pvpv(rate, nper, pmt [, fv])Present value of an annuity; fv defaults to 0
fvfv(rate, nper, pmt [, pv])Future value of an annuity; pv defaults to 0
pmtpmt(rate, nper, pv [, fv])Periodic payment; fv defaults to 0
npernper(rate, pmt, pv [, fv])Number of periods; fv defaults to 0
raterate(nper, pmt, pv [, fv])Interest rate per period (Newton–Raphson, 300 iter.)
npvnpv(rate, cashflows)Net present value of future cashflows (list)
irrirr(cashflows)Internal rate of return (Newton–Raphson, 300 iter.)
// Monthly payment on a 200 000 € loan, 5 % p.a. over 20 years
rate_m = 0.05 / 12
pmt(rate_m, 240, 200000)     → -1319.91

// Future value of 200 €/month over 10 years at 4 % p.a.
fv(0.04 / 12, 120, -200)     → 29449.96

// Present value of receiving 1 000 €/year for 5 years at 6 %
pv(0.06, 5, 1000)            → -4212.36  // negative: this is what you'd pay now for the income stream

// IRR of a project: invest 1 000 €, receive 400 € each of 3 years
irr([-1000, 400, 400, 400])  → 0.0970  // ≈ 9.70 %

Strings

FunctionSyntaxResult
strstr(value)Convert a number, matrix or date to text
ordord(text)Unicode code point(s): a single character → a number, a longer string → the list of its code points
charchar(code)The character(s) for a code point or a list of code points — the inverse of ord
printprint(value)Print value or string in the result column
str(42)             → "42"
str([1, 2, 3])      → "[[1, 2, 3]]"
ord("A")            → 65
ord("Hi")           → [72, 105]
char(65)            → "A"
char([72, 105])     → "Hi"
char(ord("draft"))  → "draft"      (round-trips)

ord/char bridge text and numbers, so a string can be processed byte-wise (hashing, XOR/Caesar ciphers, checksums) and turned back into text.

Writing, joining, comparing and indexing text is free. len, sort and contains accept text too, but they belong to the list functions and therefore need Pro — see Length, sorting and searching.

Text is always written in double quotes. Single quotes are not string delimiters — x = 'hello' is reported, not read as text. Any other character the language does not know is reported by name instead of being skipped (Unknown character: "&"). Prose lines are unaffected — a line without operators stays a note.

Escape sequences

A backslash gives the four characters that cannot be written directly:

SequenceResult
\"A double quote
\\A backslash
\nA line break
\tA tab
"He said \"Hi\""      → He said "Hi"
"back\\slash"         → back\slash
len("\"")             → 1

Anything else keeps its backslash, so a Windows path stays readable: "C:\Users" is C:\Users. Only \t would be read as a tab there — write C:\\temp if you mean the folder. char(34) still works and is the older way to write a quote.

Comparing text

== and != compare text:

"a" == "a"          → true
"a" == "A"          → false    (case matters)
"ha" + "llo" == "hallo"  → true

The result is 1 for true and 0 for false, like every other comparison. Text and a number are never equal — "5" == 5 is 0, not an error. The ordering operators (<, >, <=, >=) do not apply to text and say so.

A single character: s[i]

s[i] reads the i-th character, counting from 0; negative indices count from the end. It counts in Unicode characters, so umlauts and accents are one character each — the same counting len and for c in s use (both Pro):

s = "hallo"
s[0]                → "h"
s[-1]               → "o"
s[len(s)-1]         → "o"        (`len` needs Pro, `s[-1]` does not)
"Käse"[1]           → "ä"
s[5]                → Index 5 out of bounds (length 5)

A character is itself text, so it compares and joins like any other:

word = "hallo"
first = word[0]     → "h"
first == "h"        → true
ord(word[1])        → 97

Text cannot be changed in place — s[0] = "H" is reported. Build a new text with + instead: "H" + s[1] + s[2] + s[3] + s[4].

Length, sorting and searching Pro

len, sort and contains also accept text, but they are list functions and stay Pro wherever they are used — len on a string counts as a list function just as much as len on a list:

len("Hello")                           → 5
sort(["banane", "Apfel", "citrone"])   → [Apfel, banane, citrone]
contains(["a", "b"], "b")              → true
contains("hallo", "all")               → true
contains("hallo", "H")                 → false    (searching is case-sensitive)

len counts Unicode characters, so len("Käse") is 4, not 5.

Sorting ignores upper and lower case — otherwise "Zebra" would come before "apfel", because sorting would follow the character codes. Words with the same letters keep a stable order among themselves. Umlauts and accents sort after z; there is no dictionary collation.

A list must be all text or all numbers to be sorted — sort(["a", 1]) is reported, because text and numbers have no common order.

for c in text: walks a string character by character (see For loop). It is control flow, so it needs Pro for its own reason — not because of the string. Joining the characters back together with + is free.

Date & time

Constructors

FunctionSyntaxResult
datedate(year, month, day)Date value
datetimedatetime(year, month, day, hour, minute)Date with time
datetimedatetime(year, month, day, hour, minute, second)Date with seconds

Extractors

FunctionSyntaxResult
dayday(d)Day (1–31)
monthmonth(d)Month (1–12)
yearyear(d)Year (e.g. 2025)
hourhour(d)Hour (0–23)
minuteminute(d)Minute (0–59)
weekdayweekday(d)Weekday: 1 = Monday … 7 = Sunday (ISO 8601)

Arithmetic & formatting

FunctionSyntaxResult
daysdays(d1, d2)Difference in days (d1 − d2)
addmonthsaddmonths(d, n)Add n months (day clamped to month end if needed)
addyearsaddyears(d, n)Add n years (Feb 29 → Feb 28 in non-leap years)
monthstartmonthstart(d)First day of the month
monthendmonthend(d)Last day of the month
dformatdformat(d)Default format (e.g. "13.06.2026")
dformatdformat(d, "pattern")Custom format (see placeholders below)
-d1 - d2Difference in days (shorthand)

Placeholders for dformat:

PlaceholderMeaning
yyyyFour-digit year
MMTwo-digit month (01–12)
ddTwo-digit day (01–31)
HHTwo-digit hour (00–23)
mmTwo-digit minute (00–59)
ssTwo-digit second (00–59)
d = date(2026, 6, 13)
day(d)                         → 13
weekday(d)                     → 6   // Saturday
days(date(2026, 12, 31), d)    → 201
dformat(d, "dd.MM.yyyy")       → "13.06.2026"
addmonths(d, 3)                → 2026-09-13
addmonths(date(2026, 1, 31), 1) → 2026-02-28   // clamped to Feb end
addyears(d, 1)                 → 2027-06-13
monthstart(d)                  → 2026-06-01
monthend(d)                    → 2026-06-30
datetime(2026, 6, 13, 14, 30)
today                          → current date
now                            → current date and time

Charts Pro

plot — line chart

plot(expr)
plot(expr, from..to)
plot(expr, from..to, yFrom..yTo)
plot(expr, from..to, yFrom..yTo, steps)
plot(expr1, expr2, …, from..to)
plot(expr, ..., "title", "x axis", "y axis")
plot(expr, ..., "logy")
ParameterMeaningDefault
exprExpression in x
from..tox range−2π .. 2π
yFrom..yToFix the y axisautomatic
stepsNumber of sample points (2 – 10,000)300
"…" stringsChart title, x-axis and y-axis label (in order)none
"logx", "logy", "loglog"Logarithmic scaling of the x, y, or both axeslinear

String arguments may appear in any position; the scaling keywords "logx", "logy" and "loglog" are recognized as options, all other strings are assigned in order as title, x-axis label and y-axis label.

On a logarithmic axis only positive values are shown (non-positive points are skipped) and the gridlines snap to full decades. Tooltips and CSV/SVG export keep the real (un-transformed) values.

Several curves in one plot: pass more than one expression before the range. Each gets its own colour and an automatic legend (labelled with the expression), and the CSV export gets one column per curve:

plot(sin(x), cos(x), 0..2*pi)
plot(x, 2*x, 3*x, 0..5, "Comparison")

Custom legend labels: pass a named list instead of bare expressions to label each curve explicitly — handy when the expression itself makes a poor legend entry, e.g. pension[x]:

plot(["Pension": pension[x], "Savings": savings[x]], 0..30)

A named list can combine with plain expressions (those keep their expression-text label) and appear anywhere in the argument list, not just first.

plot(sin(x))
plot(x^2, -5..5)
plot(x^3, -10..10)
plot(sin(x), -pi..pi, -1..1, 500)
plot(sin(x), "Oscillation", "time t", "amplitude")
plot(exp(x), 1..20, "logy")
plot(x^3, 1..1000, "loglog")

scatter — point cloud

scatter(xs, ys)
scatter(xs, ys, "title", "x axis", "y axis")

Both data arguments must be lists or vectors of equal length. String arguments are assigned in order as title, x-axis and y-axis label.

scatter([1, 2, 3, 4], [1, 4, 9, 16])
scatter(xs, ys, "Measurements", "time", "value")

bar — bar chart

bar(values)
bar(values, xs)
bar(values, ..., "title", "x axis", "y axis")

Draws a bar chart. values is a list of bar heights. The optional xs argument provides explicit x positions (must have the same length as values); without it the bars are numbered 0, 1, 2, …

ParameterMeaningDefault
valuesList of bar heights
xsExplicit x positions for the bars0, 1, 2, …
"…" stringsChart title, x-axis and y-axis label (in order)none
bar([12, 7, 19, 5])
sales = [4200, 5100, 3800, 6300]
bar(sales)
bar([10, 20, 30], [2020, 2021, 2022])
bar([12, 19, 7], "Revenue", "month", "kEUR")

pie — pie chart

pie(values)
pie(values, labels)
pie(namedList)
pie(..., "title")

Draws a pie chart. values is a list of non-negative slice sizes; without labels the slices are numbered 1, 2, 3, …

A named list is the most direct way to chart a breakdown — its keys become the slice labels and its values the slice sizes, in one argument:

pie([120, 80, 60])
pie([120, 80, 60], ["Rent", "Groceries", "Transport"], "Monthly budget")
pie(["Rent": 900, "Groceries": 320, "Transport": 140])

If every entry of a named list — or every element of values — carries the same unit, the chart and its legend show it (pie(["Rent": 900 €, "Groceries": 320 €]) labels each slice in €). A negative value, or a labels list of a different length than values, is reported instead of drawn.

hist — histogram

hist(data)
hist(data, bins)
hist(data, ..., "title", "x axis", "y axis")

Bins the data into a frequency histogram and draws it. Bars touch to emphasise the continuous distribution.

ParameterMeaningDefault
dataList of values to bin
binsNumber of bins (2 – 200)10
"…" stringsChart title, x-axis and y-axis label (in order)none
data = [2, 5, 3, 8, 5, 7, 5, 3, 4, 6, 7, 5]
hist(data)
hist(data, 5)
hist(data, 5, "Distribution", "value", "count")

surface — 3D surface

surface(expr)
surface(expr, from..to)
surface(expr, xFrom..xTo, yFrom..yTo)
surface(expr, ..., steps)
surface(expr, ..., "title", "x axis", "y axis", "z axis")

Samples an expression in x and y over a grid and draws the height z as a shaded surface. A single range applies to both axes.

ParameterMeaningDefault
exprExpression in x and y
xFrom..xTox range−2π .. 2π
yFrom..yToy rangesame as x
stepsSample points per axis (2 – 200)30
"…" stringsTitle, x-, y- and z-axis label (in order)none

Points where the expression has no finite value (singularities) leave a hole in the surface instead of a jump. Colour runs from blue (low) to peach (high).

surface(sin(x) * cos(y))
surface(x^2 - y^2, -2..2)
surface(sin(sqrt(x^2 + y^2)), -8..8, -8..8, 60)
surface(x*y, 0..1, "Saddle", "width", "depth", "height")

scatter3 — 3D point cloud

scatter3(xs, ys, zs)
scatter3(xs, ys, zs, "title", "x axis", "y axis", "z axis")

Three lists or vectors of equal length. Points nearer the viewer are drawn larger and last; the colour follows the z value.

scatter3([1, 2, 3], [4, 5, 6], [7, 8, 9])
scatter3(xs, ys, zs, "Measurements", "length", "width", "height")

Rotating: drag inside a 3D chart to turn it, double-click to return to the default view. The angle belongs to the view, not to the document — SVG and PNG export the angle currently on screen.

Labeled chart lines: a label in front of any chart call becomes the chart title if no explicit title string is given — Revenue: bar([12, 19, 7]) draws the chart with the title "Revenue".

Export: every chart has overlay buttons for SVG, PNG and CSV. Title and axis labels are included in the SVG export. The CSV of a 3D chart has the columns x,y,z — for a surface one row per grid point. The CSV of a pie chart has the columns label,value — one row per slice.

CSV import / export

CSV files are read and written manually — never automatically on every keystroke. A load/save button appears in the result row of any line that uses csv(…) or csvwrite(…).

total = csv("sales")          // column 1 of the picked file
q2    = csv("sales", 2)       // column 2 (1-based)
rev   = csv("sales", "revenue") // column by header name
csvwrite([1, 2, 3], "out")    // shows a save button
FunctionMeaning
csv("name")First column as a list
csv("name", n)Column n (1-based)
csv("name", "header")Column by header name
csvwrite(data, "name")Export a list/matrix as CSV (manual save button)

csvwrite accepts a number, a matrix, or a generic list — numbers, text or a mix, including a list built row by row (append(t, ["Name", "Amount"]), append(t, ["Rent", 500])) for a table with a text column.

A column read back with csv(…) is a number list only if every cell in it parses as a number; a column with any text comes back as a generic list instead, text and numbers as written. A cell that's simply missing (a row shorter than the table) still reads back as NaN.

What the name is for

The string in csv("name") is a display name / key, not a file path. Clicking the load button opens a native file dialog; the name only labels the import. It does three things:

  1. Tells imports apart. Several independent sources can live in one document, each with its own load button and its own file:
    revenue = csv("revenue")
    costs   = csv("costs")
    
  2. Load once, use many columns. Every call with the same name shares the one loaded table, so you pick the file only once:
    month = csv("sales", 1)
    units = csv("sales", 2)
    price = csv("sales", "price")
    
  3. Drives the load indicator. The button shows, per name, whether data is already in memory (a check mark) or still needs loading.

Loaded data lives in memory only and belongs to the draft it was loaded in — after restarting the app, or after closing and reopening the draft, you load it again, and the name is the stable label that reconnects a formula to its source. The delimiter (;, tab or ,) and a header row are detected automatically; with ;/tab a decimal comma is accepted.

Proportion

FunctionSyntaxResult
proportionproportion(a, b, c)a · c / b

Solves: a : b = ? : c

proportion(3, 4, 8)    → 6    // 3:4 = 6:8

Control flow (user functions) Pro

For loop

for i in 1..n:
  …

for i in list:
  …

for c in text:
  …

The loop runs over a range, a list, a named list or a string. Over a named list it walks the values, in the order the entries were written — for the keys use for k in keys(d):.

Over a string the loop runs once per character, and c is that character — together with ord(c) this is the plain way to walk through text:

total = 0
for c in "AB":
  total = total + ord(c)

total                      → 131          (65 + 66)

Maximum 10,000 iterations. Integer range: at most 10,000 steps.

Use break to exit the loop immediately, continue to skip ahead to the next iteration:

for i in 1..10:
  if i == 5:
    break
  if i % 2 == 0:
    continue
  …

While loop

while condition:
  …

break and continue work the same way as in for.

If / elif / else

if condition:
  …
elif another_condition:
  …
elif yet_another:
  …
else:
  …

Any number of elif branches are allowed; else is optional.

Return

return value

Recursion

Maximum 100 levels of call depth.

User-defined functions Pro

Short form (single line)

f(x) = x^2 + 2*x

Long form with def

def name(param1, param2, optParam = defaultValue):
  …
  return result
def factorial(n):
  s = 1
  for i in 1..n:
    s = s * i
  return s

factorial(6)    → 720

Parameters may be numbers, lists/vectors or strings — a function can take a whole vector and index into it:

def dot(a, b):
  s = 0
  for i in 0..len(a) - 1:
    s = s + a[i] * b[i]
  return s

dot([1, 2, 3], [4, 5, 6])   → 32

Classes Pro

class Name(param1, param2, …):
  def method():
    return …

The parameters in the class header become fields of the instance; inside a method you read them as self.param1.

class Circle(r):
  def area():
    return pi * self.r^2
  def circumference():
    return 2 * pi * self.r

c = Circle(5)
c.area()           → 78.5398…
c.circumference()  → 31.4159…

Form view Pro

The form view turns a sheet into a clean, fillable form: labelled input fields at the top, computed results below, plus any charts and CSV load/save buttons. The underlying document is unchanged — the form only reads and writes the variables you point it at, so switching back to the editor shows the same sheet.

Open the Form menu:

  • Edit form… opens the form editor.
  • Form view (appears once a form is defined) toggles between the code editor and the filled-in form.

Inputs

Each input links a label to a variable in the sheet. In the form, an input is one of three kinds:

  • A number field. Typing a value writes it back to that variable's assignment (kapital = 1000). An optional unit is shown as a suffix and appended automatically, so kapital = 1000 € stays intact while you only edit the number. Clearing the field doesn't erase the assignment — an empty value would break every line that uses the variable — so the last value reappears once you leave the field.
  • A text field for a variable holding text (loc1 = "JN48HU"). The quotes belong to the language, not to your input: the form shows JN48HU and writes "JN48HU" back. Turn it on with the " button in the form editor — linking a variable that already holds text turns it on for you.
  • A choice of chips. Give the input a set of label : value options and it becomes a row of tap-to-select chips — handy for switching an assignment between a few fixed values (e.g. Reinvest? → Yes = 1 / No = 0).

Outputs

An output links a label to a variable whose computed result is shown read-only, formatted with the sheet's precision and its unit. Outputs update as soon as an input changes.

Read-only does not mean untouchable: the text in a form can be selected and copied — title, description, labels and the output values — the same way as in the result column of the editor.

Sections

A long form reads better in groups. Section (next to Add input / Add output) inserts a heading into the list; every field below it belongs to that section until the next heading. Inputs and outputs are grouped independently.

  • The arrows on the right of each row move it up or down — that is how a heading gets in front of the fields it is meant to group, and how a field changes sections.
  • The heading is a fold: tapping it in the form hides or shows its fields. The button in the editor decides whether a section starts folded, which is useful for advanced settings you rarely touch. Folding in the form is only a view state; it never rewrites the sheet.

Layout

Fields are laid out in as many columns as the window is wide enough for (up to three), one grid per section — a wide window shows a row of inputs side by side instead of one long column. Narrow the window and they stack again. Output rows stay near their labels rather than stretching the value to the far right edge.

Charts and CSV

  • Charts — pick which chart lines (plot, scatter, bar, hist, pie, surface, scatter3) appear in the form. S / M / L / XL below the list sets how large they are drawn; in the editor a chart stays the height of its line, but in a form it is often the result itself.
  • CSV — each csv(…) load and csvwrite(…) save in the sheet gets a row in the form; you can give it a friendlier label or hide it.

Automatic mode

The editor can read the sheet and propose a starting point instead of adding every field by hand. When it finds candidates, a banner shows how many:

  • Inputs — every variable set to a plain value without a formula (kapital = 1000, weg = 12 km); its unit is filled in automatically. Text values count too (loc1 = "JN48HU") and become a text field.
  • Outputs — every variable that (directly or indirectly) depends on one of those inputs (zinsen = kapital * satz / 100).

The banner starts collapsed with a single Apply all. Expanding it splits the suggestions into Inputs and Outputs, each with a checkbox per field, so you can add only the ones you want instead of all-or-nothing.

It only proposes fields that aren't already present, and it never runs on its own — apply it, then remove what you don't need and add the labels. Variables defined inside for/while/if/def blocks and function definitions are ignored.

How it is stored

A form lives in the sheet's frontmatter as form_* fields, so it travels with the .dply file:

---
form_title: Compound interest
form_description: Quick estimate
form_inputs: #Basics, Capital|kapital{€}, #Assumptions|collapsed, Reinvest?|reinvest[Yes:1,No:0], Locator|loc1"
form_outputs: Net gain (€)|net_gain
form_plots: 1
form_plot_size: l
---

| separates the label from the variable, {unit} adds a unit, " marks a text field, and [label:value,…] defines chip options. An entry starting with # is a section heading for the fields that follow it; |collapsed makes it start folded. form_plot_size is s, m (the default), l or xl.

Miscellaneous

Variable names

A name starts with a letter or _, followed by letters, digits or _. Letters include the accented ones, so a sheet can be written in the language it is about — Groesse is no longer the only way to say Größe:

Größe = 5
año = 12
Wohnfläche = 80 m
prêt = 100
prêt / 4            → 25

Upper and lower case are distinct, so Größe and größe are two variables. Not letters: × and ÷ (they are reported as unknown characters instead of quietly joining a name) and the Greek alphabet (µ and Ω are unit symbols there). Prose lines are unaffected either way — a line without operators stays a note, accents or not.

Line references

@1, @2, … — refer to the result of that line.

5 * 3          → 15
@1 + 10        → 25

They follow the line. Insert or delete lines above a reference and the number is rewritten so it keeps pointing at the same calculation — the same way a spreadsheet fixes up its formulas. Undo takes the edit and the renumbering back together, and a reference in a comment (# see @3) is carried along too.

If the line a reference points at is deleted, the reference becomes @0 and the line reports "reference to a deleted line". That is deliberate: a silently repointed @3 would quietly compute the wrong thing, while @0 shows you exactly where to look. If a form selects charts by line (Form view), that selection moves along as well.

Comments

// single-line comment
# also a comment

A note behind a value has to be a comment. draftply reads a line as one expression and reports whatever is left over, so a trailing word that is not a unit is an error:

years = 20 years    → error: unused input “years”
years = 20          # investment period (years)

That also catches real slips: T = 20 °C used to evaluate to a bare 20 (there is no inline °C — temperatures go through convert), and x = 5 foo bar to a bare 5.

Labels

VAT: 0.19 * 100    → VAT:  19

The label is the text before the :. It may contain letters, digits, spaces and hyphens. If it needs other characters — parentheses, /, a currency sign — wrap it in quotes; the quotes are dropped from the output:

"Area (m²)": 4.5 * 12    → Area (m²):  54

Text with a calculation in it

A line of prose may carry {…} — the expression is evaluated and its result takes the place of the braces. Any value fits in there, not just a plain number: a quantity keeps its unit, a list stays a list.

price = 1.85 €/L
liters = 40 L
Refuelling costs {price * liters} at {price}.
                   → Refuelling costs 74 € at 1.85 €/L.

Write {{ and }} for literal braces. A label (see above) is a different thing: its text is shown as written, so {…} inside a label is not replaced.

Running total

A line containing just total sums the results of the lines above it, back to the last blank line, heading/comment or a previous total. Ideal for shopping lists and budgets:

# Groceries
12.50
8.00
3.20
total              → 23.70

It keeps a shared unit and converts within one quantity, so 1 h above 30 min is 1.5 h, not 31. The first line of the group picks the unit. An optional label works too (Total: total).

Quantities that do not belong together are not merged into one number — each gets its own sum, shown as a vector in the order the quantities first appear:

5 km
1 h
30 min
total              → [5 km, 1.5 h]

The vector is an ordinary value: @4[0] picks the distance out of it.

A plain number after a unit has appeared is read in that unit, so a 50 in a column of euros still counts as 50 €. A plain number before the first unit has no such context yet and stays a figure of its own — reading downwards, nothing has established what it would be measured in:

1              1
5 km           5 km
total          → [1, 5 km]
5 km           5 km
1              1
total          → 6 km

The same applies when two quantities meet: there is no single unit to adopt, so the plain number keeps its own place (5 km, 1 h, 7[5 km, 1 h, 7]).

Offer export Pro

A draft with labeled lines and a running total is already a quote — File → Offer → Export offer turns it into a client-ready PDF with your company letterhead, without any new syntax. File → Offer → New offer starts a fresh draft already shaped like one, with the recognized fields below as empty placeholders:

Dear Mrs Meier,

please find our quote for the bathroom renovation below:

Tiling: 8 h * 45 €/h
Material: 320 €
Total: total

Best regards,
Max Mustermann

Plain prose lines (the greeting, the closing) become paragraphs; labeled value lines become rows in a Position / Amount table; a total line becomes a sum row. Everything else — an unlabeled scratch calculation, a comment, a chart — is left out of the printed document, since it is not meant for the client. A typo that turns an item line into an error would otherwise vanish from the quote unnoticed, so exporting asks for confirmation first if the draft still has any error lines.

A bare item label can be several words (Tile removal and subfloor prep: 3 h * LaborRate works as written) as long as it starts with a letter. A line starting with a digit (3D-Druck) looks like the start of an expression instead (3 minus something) and produces an error rather than falling back to text — quoting the whole line as a string ("3D-Druck der Funktionsteile:") fixes that and prints as a normal paragraph, not as a discarded calculation. The same quoting works for an item label with the same problem ("3D-Druck der Funktionsteile": 45 €) — the quotes themselves never show up in the offer, same as they don't in the live editor.

A total line does not end its table by itself — a blank line (or a prose line) does. This lets you chain further named lines onto a total via @N line references, e.g. a discount off the subtotal followed by a manually computed grand total, and have them all print as one continuous table:

Tiling: 8 h * 45 €/h
Material: 320 €
Subtotal: total
Discount: -1* @3 * 10%
Grand total: @3 + @4

Whichever row is last in a table gets the bold sum styling — here that is Grand total, not Subtotal — but only if the table contains a total line somewhere; a plain item list with no total at all never gets a bold last row just because it happens to be last.

File → Offer → Offer: settings holds your company details (name, address, email, phone, tax ID) shown as a plain-text header, or an optional letterhead image/PDF used as the page background instead (with an adjustable offset so the content does not overlap your logo).

A few variable names are recognized by name, the same way total is — set them anywhere in the draft and they appear on the printed offer; leave them out and nothing changes:

DocumentTitle = "Estimate"
OfferDate = today
OfferNumber = "2026-014"
ValidUntil = "2026-10-15"
CustomerName = "Jane Doe"
CustomerAddress = "123 Example Street\n90210 Springfield"
VatRate = 19%

Not every one of these documents is an "Offer" — a cost estimate, an order confirmation or a price quote may be the right word depending on your country or industry, and some of those carry a different legal meaning than an offer does. DocumentTitle replaces the label everywhere it shows up: the PDF's own title, the print dialog's document name, the heading printed on the page itself, and the default filename when exporting as CSV. Leave it out and it falls back to the translated default ("Offer", "Angebot", "Devis", …, matching the app's current language).

OfferDate gets its own line above the quote text, with OfferNumber/ ValidUntil together on the line below it; CustomerName/ CustomerAddress print as an address block below that. A multi-line CustomerAddress (\n) prints one line per address line, same as the company address above. Leave OfferDate out and it defaults to today's date, so most quotes never need to set it — set it only to backdate a quote or pin the date on a draft you finish printing later. OfferDate/ValidUntil both take a real date value too (ValidUntil = date(2026, 10, 15)), formatted the same way it would be anywhere else in the document.

VatRate, if set, turns the last row of any table that contains a total into three: the net amount (the value that row already had), a VAT row at that rate, and a bold gross total below it. It applies to the last row of the table, not necessarily the total line itself — so a discount chained onto a total via @N (as in the example above) gets taxed after the discount, not before it: Grand total above would fan out into net/VAT/ gross, while Subtotal stays a plain, untaxed row. Write the rate either as a percent literal (VatRate = 19%) or a bare number (VatRate = 19); both mean the same 19 %. Leave it out entirely for a quote without VAT (e.g. reverse charge or the German small-business exemption) — the total row then stays exactly as it was before this field existed.

Money amounts always print with exactly 2 decimal places in the offer, whether that's the PDF or the CSV — 100 € prints as 100.00 € (or 100,00 €, 100.00 in the CSV, depending on locale/output), and a rate that lands on a third decimal (a VAT amount, say) is rounded to the cent rather than shown with more digits. This is specific to the offer export; elsewhere in draftply a whole number still prints without a decimal part.

File → Offer → Export offer as CSV writes the same item rows — including any VAT/gross breakdown — as a plain ;-separated CSV file instead of a PDF, for anyone who wants to keep working with the numbers in a spreadsheet rather than send the finished document. Amounts are written as plain numbers without a unit (100.00, not 100.00 €) since a spreadsheet needs a real value to calculate with, not a display string. Only the item table(s) are included — the letterhead, prose lines and customer details are PDF-only.

Library import

import "file.dplylib"

Debug / print

print(x)        // prints x in the result column

Inside a loop, print runs on every pass and its own line collects all of them, separated like a list — not the for line, however far apart the two are:

total = 0
for i in 1..3:
  total = total + i
  print(total)      // ↳ 1, 3, 6      (here, next to the print)

The result column carries one row per document line, so a long run is capped at 50 outputs and ends in . Turn the output on under View → Debug output.

With the output on, a loop also writes one trace line per pass next to its for (or while), naming the loop variable and every value that changed. Passes that change nothing are left out, and a while loop counts its passes as #1, #2:

total = 0
for i in 1..3:        // i=1: total=1
  total = total + i   // i=2: total=3
                      // i=3: total=6

Every trace line stands next to the line it belongs to. In nested loops the inner passes are listed at the inner for, and what a called function traces appears at the call, not somewhere inside the definition:

for i in 1..2:        // i=1: t=2, j=2
  for j in 1..2:      // j=1: t=1
    t = t + 1         // j=2: t=2
                      // …

Bracket matching

The editor helps you keep parentheses () and brackets [] balanced:

  • Unbalanced brackets are marked red. If a bracket has no partner — an opening ( that is never closed, or a stray closing ) — it is highlighted in red so you can see exactly where the problem is.
  • Matching pair highlight. Place the cursor next to a bracket and its matching partner is highlighted (like a spreadsheet), so nested expressions are easy to follow.
  • Clear error message. A line with a missing closing bracket now reports e.g. "Unclosed bracket: 1× ) missing" instead of a generic "Unknown input".
cos((1+2)*3        → Unclosed bracket: 1× ) missing   (first ( shown red)

Number format

Settings → Number format controls how results and numbers in the editor are displayed:

  • Decimal separatorAutomatic (follows your system language), Period (1,234.56) or Comma (1.234,56). In comma mode use ; to separate list and function-argument values (see Lists & arrays).
  • Thousands separator — a toggle to turn digit grouping on or off. With it off, numbers are shown without grouping (1234567 instead of 1,234,567); the decimal separator is unaffected. Applies to both results and the editor.

Toolbar in narrow windows

The toolbar adapts to the space it has instead of scrolling sideways:

  • The four menus drop their labels (File, Library, View, Form) as soon as they no longer fit, leaving icon and arrow. The label moves into the tooltip — hover, or long-press on a touch screen. Where the switch happens depends on your language and system font size, not on a fixed window width: Visualização needs more room than View.
  • Below ~600 px the bar is always icon-only. The file name is hidden, the Pro badge shrinks to its star, and the drafts sidebar slides over the editor instead of pushing it aside.

Files, sharing & open-with

draftply files are plain text with a small header, so they travel well.

ExtensionContents
.dplyA notebook (document).
.dplylibA function/unit library you import into a document.
.dplybundleA notebook packaged together with its libraries in one file.

Sharing. File → Share sends the current draft through your operating system's share sheet. If the document uses imported libraries, it is shared as a self-contained .dplybundle (so it still works for the recipient); otherwise as a plain .dply. Library → Share exports the document's functions as a .dplylib. Sharing appears where the OS provides a share sheet (iOS, Android, macOS, Windows).

Open with draftply. Opening a .dply, .dplylib or .dplybundle from another app or your file manager opens it in draftply as a new draft (the content is copied in — the original file is not linked). For safety, a document that arrives from another app is not evaluated automatically: a banner lets you review the content first and press Evaluate to run it.

Keyboard shortcuts

Desktop shortcuts. On macOS use ⌘ (Cmd) instead of Ctrl.

ShortcutAction
Ctrl + NNew draft
Ctrl + OOpen file
Ctrl + SSave
Ctrl + Shift + SSave as…
Ctrl + PPrint / Save as PDF
Ctrl + ,Settings
Ctrl + FSearch in the document
Ctrl + Z · Ctrl + Shift + Z / Ctrl + YUndo · Redo
Ctrl + C · Ctrl + X · Ctrl + V · Ctrl + ACopy · Cut · Paste · Select all
Tab or EnterAccept autocomplete
· Navigate the autocomplete list
Tab · Shift + TabIndent · outdent (without autocomplete open; works on every selected line)