No matches — try another term.
A complete overview of every built-in function, operator and constant.
Free vs. Pro
Free
| Category | Functions / features |
|---|---|
| Constants | pi, e, inf, true, false, today, now |
| Operators | + - * / ^ %, < > <= >= == !=, and or not, .. |
| Trigonometry | sin, cos, tan, asin, acos, atan, atan2 |
| Math | sqrt, abs, exp, log, log10, log2, floor, ceil, round, min, max, pow, sign, clamp, lerp, mod |
| Number systems | 0x/0b/0o literals, hex, bin, oct, dec, band, bor, bxor, bnot, shl, shr |
| Complex numbers | 3i literals, re/im/conj/arg, units, exp/log/sin, lists, matrices |
| Strings | str, ord, char, print, s[i], == / !=, + |
| Date & time | date, datetime, day, month, year, hour, minute, weekday, days, addmonths, addyears, monthstart, monthend, dformat |
| Proportion | proportion |
| CSV | csv, csvwrite (manual load/save) |
| Language | Variables, labels, line references (@n), comments, units |
| Files & sharing | .dply / .dplylib / .dplybundle, share, open-with |
| Shortcuts | Desktop keyboard shortcuts |
Pro
| Category | Functions / features |
|---|---|
| Matrices | det, inv, transpose, trace, norm, eye, zeros, ones, size |
| Lists | len, range, push, pop, first, last, sort, reverse, contains, list, append, keys, values, map, filter — len, sort and contains count as list functions even when applied to text |
| Statistics | sum, mean, median, std, variance, mode, percentile, quantile, corr, cov |
| Finance | pv, fv, pmt, nper, rate, npv, irr |
| Control flow | for / while loops with break / continue, if / elif / else |
| Charts | plot, scatter, bar, hist, pie, surface, scatter3 |
| Symbolic algebra | solve, simplify, factor |
| Differentiation | diff (symbolic and numeric) |
| Integration | integrate (symbolic and numeric) |
| User-defined functions | f(x) = …, def name(…): (including recursion) |
| Classes & objects | class Name(…):, methods, instances |
| Form view | Turn a sheet into a fillable form (inputs, outputs, charts, CSV) |
Constants
| Name | Value / meaning |
|---|---|
pi | π ≈ 3.14159265358979… |
e | Euler's number ≈ 2.71828182845904… |
inf | Infinity (∞) |
true | Truth value |
false | Truth value |
today | Today's date (no time component) |
now | Current 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
| Operator | Meaning | Example |
|---|---|---|
+ | Addition | 3 + 4 → 7 |
- | Subtraction | 10 - 3 → 7 |
* | Multiplication | 2 * 5 → 10 |
/ | Division | 7 / 2 → 3.5 |
^ | Power | 2^8 → 256 |
% | Modulo | 10 % 3 → 1 |
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)^2 → 4 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 m → 5.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.19 → 200, not (238 / 119) / 100.
With + and - a percentage is taken of the left-hand value — that is how you add or remove a markup:
| Expression | Result | Reads as |
|---|---|---|
100 + 10% | 110 | 100 plus 10 % of 100 |
4 + 20% | 4.8 | 4 plus 20 % of 4 |
250 - 15% | 212.5 | 250 minus 15 % of 250 |
100 + 10% + 10% | 121 | two 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 % 3 → 1).
Comparison
| Operator | Meaning |
|---|---|
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
== | Equal |
!= | Not equal |
Result: a truth value — true 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
| Operator | Meaning | Example |
|---|---|---|
and | Logical and | x > 0 and x < 10 |
or | Logical or | x < 0 or x > 10 |
not | Negation | not (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 true → false, 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
| Syntax | Meaning |
|---|---|
a..b | Integer range from a to b (inclusive) |
Used in for loops and plot.
Trigonometry
All angles are in radians.
| Function | Syntax | Result |
|---|---|---|
sin | sin(x) | Sine of x |
cos | cos(x) | Cosine of x |
tan | tan(x) | Tangent of x |
asin | asin(x) | Arcsine, result in [−π/2, π/2] |
acos | acos(x) | Arccosine, result in [0, π] |
atan | atan(x) | Arctangent, result in (−π/2, π/2) |
atan2 | atan2(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
| Function | Syntax | Result |
|---|---|---|
sqrt | sqrt(x) | Square root of x |
abs | abs(x) | Absolute value |
exp | exp(x) | eˣ |
log | log(x) | Natural logarithm (base e) |
log10 | log10(x) | Logarithm base 10 |
log2 | log2(x) | Logarithm base 2 |
floor | floor(x) / floor(x, digits) | Round down; to digits decimals if given |
ceil | ceil(x) / ceil(x, digits) | Round up; to digits decimals if given |
round | round(x) / round(x, digits) | Round half away from zero; to digits decimals if given |
convert | convert(x, "from", "to") | Convert between units, incl. temperatures (see below) |
defunit | defunit("name") / defunit("name", "base") / defunit("name", factor, "base") | Define a custom unit (see below) |
min | min(a, b) / min(list) | The smaller of two values, or the smallest element of a list |
max | max(a, b) / max(list) | The larger of two values, or the largest element of a list |
pow | pow(x, n) | x to the power of n (same as x^n) |
sign | sign(x) | Sign: −1, 0 or 1 |
clamp | clamp(x, min, max) | Clamp x to the range [min, max] |
lerp | lerp(a, b, t) | Linear interpolation: a + t*(b−a); t=0 → a, t=1 → b |
mod | mod(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 m → 5100 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.
| Form | Meaning |
|---|---|
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.
| Function | Syntax | Result |
|---|---|---|
0x… | 0xFF | Hexadecimal literal (0xFF → 255) |
0b… | 0b1010 | Binary literal (0b1010 → 10) |
0o… | 0o100 | Octal literal (0o100 → 64) |
hex | hex(x) | x as a hexadecimal string (0x…) |
bin | bin(x) | x as a binary string (0b…) |
oct | oct(x) | x as an octal string (0o…) |
dec | dec("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.
| Function | Syntax | Result |
|---|---|---|
band | band(a, b) | Bitwise AND |
bor | bor(a, b) | Bitwise OR |
bxor | bxor(a, b) | Bitwise XOR |
bnot | bnot(a) | Bitwise NOT (complement) |
shl | shl(a, n) | Shift a left by n bits |
shr | shr(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.
| Function | Syntax | Result |
|---|---|---|
re | re(z) | Real part |
im | im(z) | Imaginary part |
conj | conj(z) | Conjugate: 2 + 3i → 2 - 3i |
arg | arg(z) | Angle in the plane, in radians |
abs | abs(z) | Magnitude: abs(3 + 4i) → 5 |
sqrt | sqrt(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)^2 → 1,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 < 2is reported — compare magnitudes instead:abs(z1) < abs(z2).==and!=do work. - Not as CAS input.
solve,simplify,factor,diffandintegratecompute with real coefficients, sosimplify(1i x)reports. Complex results* (the roots above) do work.
Calculus Pro
Symbolic (2 arguments)
| Function | Syntax | Result |
|---|---|---|
diff | diff(expr, var) | Symbolic derivative with respect to var |
integrate | integrate(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)
| Function | Syntax | Result |
|---|---|---|
diff | diff(expr, var, point) | Numeric derivative at a point (central difference) |
integrate | integrate(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
| Function | Syntax | Result |
|---|---|---|
solve | solve(expr, var) | Roots / solutions |
solve | solve(eq1, eq2, …, var1, var2, …) | Solve a system of equations |
simplify | simplify(expr) | Expand & combine a polynomial |
factor | factor(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).
| Function | Syntax | Result |
|---|---|---|
det | det(A) | Determinant (square matrix) |
inv | inv(A) | Inverse (square matrix) |
transpose | transpose(A) | Transpose |
trace | trace(A) | Trace (sum of diagonal elements) |
norm | norm(A) | Frobenius norm; for a scalar: abs(x) |
eye | eye(n) | n×n identity matrix (up to 1000×1000) |
zeros | zeros(n) / zeros(r, c) | Matrix of zeros |
ones | ones(n) / ones(r, c) | Matrix of ones |
size | size(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.
| Function | Syntax | Result |
|---|---|---|
len | len(list) | Number of elements |
len | len(text) | Number of characters (see Strings) |
range | range(end) | Integers 0 to end−1 |
range | range(start, end) | Integers start to end−1 (up to 100,000) |
range | range(start, end, step) | With a custom step |
push | push(list, value) | Append an element (returns a new list) |
pop | pop(list) | Remove the last element (returns a new list) |
first | first(list) | First element |
last | last(list) | Last element |
sort | sort(list) | Sorted ascending (returns a new list) |
reverse | reverse(list) | Reversed order (returns a new list) |
contains | contains(list, value) | 1 if present, otherwise 0 |
contains | contains(text, part) | 1 if part occurs in text, otherwise 0 |
list | list(a, b, …) | Generic list of arbitrary values (see below) |
append | append(list, value) | Append to a generic list (returns a new list) |
keys | keys(named list) | The keys as a list (see Named lists) |
values | values(named list) | The values as a list (see Named lists) |
map | map(f, list) | Apply function f to each element |
filter | filter(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.
| Function | Syntax | Result |
|---|---|---|
sum | sum(list) / sum(a, b, …) | Sum of all values |
mean | mean(list) / mean(a, b, …) | Arithmetic mean |
median | median(list) / median(a, b, …) | Median (middle value) |
std | std(list) / std(a, b, …) | Sample standard deviation (÷ n−1) |
variance | variance(list) / variance(a, b, …) | Sample variance (÷ n−1) |
mode | mode(list) / mode(a, b, …) | Most frequent value |
percentile | percentile(list, p) | p-th percentile (p: 0–100), linear interpolation |
quantile | quantile(list, q) | q-th quantile (q: 0–1), linear interpolation |
corr | corr(xs, ys) | Pearson correlation coefficient |
cov | cov(xs, ys) | Sample covariance (÷ n−1) |
linreg | linreg(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).
| Function | Syntax | Result |
|---|---|---|
pv | pv(rate, nper, pmt [, fv]) | Present value of an annuity; fv defaults to 0 |
fv | fv(rate, nper, pmt [, pv]) | Future value of an annuity; pv defaults to 0 |
pmt | pmt(rate, nper, pv [, fv]) | Periodic payment; fv defaults to 0 |
nper | nper(rate, pmt, pv [, fv]) | Number of periods; fv defaults to 0 |
rate | rate(nper, pmt, pv [, fv]) | Interest rate per period (Newton–Raphson, 300 iter.) |
npv | npv(rate, cashflows) | Net present value of future cashflows (list) |
irr | irr(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
| Function | Syntax | Result |
|---|---|---|
str | str(value) | Convert a number, matrix or date to text |
ord | ord(text) | Unicode code point(s): a single character → a number, a longer string → the list of its code points |
char | char(code) | The character(s) for a code point or a list of code points — the inverse of ord |
print | print(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:
| Sequence | Result |
|---|---|
\" | A double quote |
\\ | A backslash |
\n | A line break |
\t | A 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
| Function | Syntax | Result |
|---|---|---|
date | date(year, month, day) | Date value |
datetime | datetime(year, month, day, hour, minute) | Date with time |
datetime | datetime(year, month, day, hour, minute, second) | Date with seconds |
Extractors
| Function | Syntax | Result |
|---|---|---|
day | day(d) | Day (1–31) |
month | month(d) | Month (1–12) |
year | year(d) | Year (e.g. 2025) |
hour | hour(d) | Hour (0–23) |
minute | minute(d) | Minute (0–59) |
weekday | weekday(d) | Weekday: 1 = Monday … 7 = Sunday (ISO 8601) |
Arithmetic & formatting
| Function | Syntax | Result |
|---|---|---|
days | days(d1, d2) | Difference in days (d1 − d2) |
addmonths | addmonths(d, n) | Add n months (day clamped to month end if needed) |
addyears | addyears(d, n) | Add n years (Feb 29 → Feb 28 in non-leap years) |
monthstart | monthstart(d) | First day of the month |
monthend | monthend(d) | Last day of the month |
dformat | dformat(d) | Default format (e.g. "13.06.2026") |
dformat | dformat(d, "pattern") | Custom format (see placeholders below) |
- | d1 - d2 | Difference in days (shorthand) |
Placeholders for dformat:
| Placeholder | Meaning |
|---|---|
yyyy | Four-digit year |
MM | Two-digit month (01–12) |
dd | Two-digit day (01–31) |
HH | Two-digit hour (00–23) |
mm | Two-digit minute (00–59) |
ss | Two-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")
| Parameter | Meaning | Default |
|---|---|---|
expr | Expression in x | — |
from..to | x range | −2π .. 2π |
yFrom..yTo | Fix the y axis | automatic |
steps | Number of sample points (2 – 10,000) | 300 |
"…" strings | Chart title, x-axis and y-axis label (in order) | none |
"logx", "logy", "loglog" | Logarithmic scaling of the x, y, or both axes | linear |
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, …
| Parameter | Meaning | Default |
|---|---|---|
values | List of bar heights | — |
xs | Explicit x positions for the bars | 0, 1, 2, … |
"…" strings | Chart 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.
| Parameter | Meaning | Default |
|---|---|---|
data | List of values to bin | — |
bins | Number of bins (2 – 200) | 10 |
"…" strings | Chart 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.
| Parameter | Meaning | Default |
|---|---|---|
expr | Expression in x and y | — |
xFrom..xTo | x range | −2π .. 2π |
yFrom..yTo | y range | same as x |
steps | Sample points per axis (2 – 200) | 30 |
"…" strings | Title, 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
| Function | Meaning |
|---|---|
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:
- 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") - 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") - 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
| Function | Syntax | Result |
|---|---|---|
proportion | proportion(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, sokapital = 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 showsJN48HUand 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 : valueoptions 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 andcsvwrite(…)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 separator — Automatic (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 (
1234567instead of1,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.
| Extension | Contents |
|---|---|
.dply | A notebook (document). |
.dplylib | A function/unit library you import into a document. |
.dplybundle | A 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.
| Shortcut | Action |
|---|---|
Ctrl + N | New draft |
Ctrl + O | Open file |
Ctrl + S | Save |
Ctrl + Shift + S | Save as… |
Ctrl + P | Print / Save as PDF |
Ctrl + , | Settings |
Ctrl + F | Search in the document |
Ctrl + Z · Ctrl + Shift + Z / Ctrl + Y | Undo · Redo |
Ctrl + C · Ctrl + X · Ctrl + V · Ctrl + A | Copy · Cut · Paste · Select all |
Tab or Enter | Accept autocomplete |
↑ · ↓ | Navigate the autocomplete list |
Tab · Shift + Tab | Indent · outdent (without autocomplete open; works on every selected line) |