0.4.0 — 2026-09-09
- Offer export (Pro): File → Export offer turns a draft into a client-ready quote PDF. A labeled line like Tiling: 8 h * 45 €/h becomes a Position/Amount row, a plain total line becomes the bold sum, and ordinary prose (a greeting, a closing line) prints as paragraphs around it — unlabeled scratch math, comments and charts are left out, since they're not meant for the client. Exporting asks first if the draft still has error lines, so a typo can't silently drop an item from the printed quote.
- Offer settings (File → Offer: settings, Pro): company name, address, email, phone and tax ID for the quote's letterhead — or your own letterhead image/PDF as the page background instead, with an adjustable offset so printed content doesn't overlap it. A PDF letterhead is rastered at 300 dpi for a crisp result.
- OfferDate, OfferNumber, ValidUntil, CustomerName and CustomerAddress are recognized by name in the draft, the same way total is: set anywhere, they show up on the printed offer (the date on its own line, number/validity together below it, the name/address as a recipient block further down); left unset, OfferDate defaults to today and the rest simply don't show.
- File → Offer (Pro) groups New offer, Export offer and Offer: settings in one place — New offer starts a fresh draft already shaped like a quote, with OfferDate pre-filled as today, four more fields as empty placeholders, a sample item line and a total.
- @N referencing a total line reported “Undefined variable” instead of the summed value — total results were never registered for line references, unlike every other line type.
- total now converts units instead of adding the bare numbers. A column of 1 h and 30 min totalled “31”, and 1000 m above 2 km totalled “1,002”. Within one quantity everything is converted to the unit of the first line, exactly as sum([1 h, 30 min]) has done since 0.3.0.
- total no longer merges quantities that do not belong together. 5 km above 1 h came out as “6”, and with a 1 s in front of it as “3,606” — no unit, but no meaning either. Each quantity is summed on its own now and the sums are shown as a vector, in the order the quantities first appear: [5 km, 1.5 h]. It stays an ordinary value, so @4[0] picks the distance out of it, and a column of a single quantity is unchanged — still a plain 30 kg, not a vector.
- In a total, where a plain number belongs now depends on where it stands. Read downwards, a number after a unit is read in that unit (5 km above 1 is 6 km), but a number before the first unit has nothing to be measured in yet and keeps its own place: 1 above 5 km is [1, 5 km] rather than 6 km. It only adopts a unit when the group has exactly one — with two quantities side by side there is nothing to decide between, so it stays a figure of its own.
- A total shows at most one figure without a unit. Plain numbers written before and after the first unit are tracked apart to decide whether they can adopt it, but when they cannot — because two quantities stand side by side — they belong back together: 25 … 100 above seconds and kilometres now reads [125, 3,631 s, 5.007 km] instead of listing 25 and 100 as two separate unitless entries.
- total no longer stops at the continuation line of a multi-line expression. A wrapped expression like y = (1 + / 2) anywhere in the column made the total silently fall to 0, taking every value above it with it — only a genuinely blank line separates a group, which is what the rule always said.
- total keeps the unit you asked for: a column of convert(1, "m", "km") lines showing 0.001 km no longer totals to “3 m”.
- total counts truth values like sum() does. A column of true/false totalled 0, although sum([true, false, true]) is 2 and true + 1 is 2.
- total no longer counts a defunit() line as a measurement. A unit declaration echoes its own unit (1 Ohm) so the line shows something, and that echo was added to the sum: defunit("Widget") above 10 and 20 totalled "31 Widget". Its two siblings, a function definition and an import, were already skipped.
- A plain number in a list is now read in the base unit of the quantity beside it, the same reading 5 km + 7 already used. The list literal used to throw the unit away as soon as one element lacked it — [5 km, 7] became [5, 7] — and everything built on it inherited the mistake: max([5 km, 7]) answered 7, claiming 7 is larger than 5 km, and sum([5 km, 7]) came to 12 m, less than one of its own terms. Now [5 km, 7] is [5 km, 0.007 km], max is 5 km and sum agrees with 5 km + 7. Quantities that do not belong together ([1 m, 2 kg]) stay an error.
- A variable no longer inherits a unit from its own name. After s = 10 the line s showed “10 s” (seconds), g = 10 showed “10 kg” and h = 10 showed “10 s” — short names like h for height, m for months or s for a sum are everyday, and so was the phantom unit. Standing on its own, the name is your variable; right after a number the unit still wins.
0.3.0 — 2026-08-30
- Units: a power now belongs to the unit, not to the whole quantity — 54 m² is 54 square metres again instead of 2,916 m². Put the quantity in brackets, (54 m)^2, to square it as a whole.
- Collapsing a function now really closes it: the hidden body leaves a single thin gap instead of reserving space per hidden line, so long functions no longer take several blank lines.
- min, max, clamp and abs keep the unit — and min/max now compare in SI, so min(5 km, 300 m) is 300 m instead of 5. Mixing quantities that cannot be compared (min(5 km, 3 kg)) reports an error instead of a silent number; a plain number still works as a bound, as in max(cost, 0).
- Energy units now add up: W·h and kWh are the same thing, so 1500 W 4 h 0.30 €/kWh is 1.80 € instead of “1.8 W·h·€/kWh”, and 1500 W * 4 h reads 6 kWh. convert() accepts them too. The electricity-costs sample calculates with real units again.
- A C-style conditional (a ? b : c) is now reported as an error naming the supported form (b if a else c). It used to be swallowed silently and evaluated to just a.
- Importing a library without Pro now says so on the import line, instead of loading nothing and then failing with “Unknown function” at every call.
- A variable no longer shadows a unit of the same name: with km = 15000 km, a later 6.5 L/100km stays a consumption instead of being silently divided by 15,000. Right after a number the unit always wins; on its own the name is still your variable.
- convert() now shows the unit you asked for: convert(1, "m", "km") is 0.001 km instead of “1 m”, and convert(5, "km", "m") is 5000 m instead of “5 km”. The automatic display prefix no longer undoes the conversion.
- for v in d: now works on a named list too and walks its values, units and all — it used to be an error even though a named list is a list. Use for k in keys(d): to walk the keys instead.
- for c in text: now walks a string character by character, with c being the character. It used to be an error (“range must be start..end or a list”) and you had to know the detour via ord(text). Together with ord(c) this is the plain way through a string; + joins the characters back together.
- A fraction of units is no longer renamed into another quantity: 16 kWh/100km reads as a consumption again instead of “576 N”. Energy per distance does have the dimension of a force, but the reinterpretation carried on through the sheet — the EV charging sample now shows “Cost per km: 0.048 €/km” and a real break-even price of 0.731 €/kWh instead of “172.8 €·N/kWh” and “0 €/km·N”. Fractions you named yourself (defunit) and ones spelled out of base quantities (2 kg * 3 m / 4 s^2 → 1.5 N) still get their name.
- Each quantity now uses one unit per sheet. Picking the prefix value by value used to put “Annual CO2 (EV): 750 kg” right above “Annual CO2 (gas car): 2.252 t” — both correct, unreadable side by side. They now read 0.75 t and 2.252 t. Nothing changes where the units already agree, and when no single unit suits every value (5 mm next to 300 km) each keeps its own.
- Lists keep their units: [5 €, 3 €] stays euros, and min, max, sum, mean, median and std return them — as do sort, an element like list[1], and for x in list. Different units of one quantity are converted instead of mixed, so [1 m, 2 km] is [1 m, 2000 m] and sum([1 m, 200 cm]) is 3 m rather than 201. Mixing quantities that do not belong together ([1 m, 2 kg]) now reports an error.
- sort() on a list that mixes text and numbers now says so. It reported “sort: list expected” and pointed at the argument — but the list is fine; its contents cannot be put into one order.
- Variable names may contain accented letters: Größe, año, Wohnfläche. Only a–z counted before, so Größe = 5 failed with “Unknown character: ö” and the only way out was Groesse. The same letters now count in prose and labels, in autocompletion and in the form editor. × and ÷ stay operators rather than letters.
- Text can contain a double quote: write \" inside the string, and \\ for a backslash itself (\n and \t already worked). A quote used to need char(34). An unknown escape keeps its backslash, so "C:\Users" still reads as written.
- Complex numbers: the imaginary part is a number with an i attached (3i, 1i, 0.5i). sqrt(-1) is 1i instead of a silent NaN, and + - / ^ and == work with them, as do abs (magnitude), re, im, conj and arg. A result whose imaginary part cancels out is a plain number again — 1i 1i is -1. A plain i stays an ordinary variable name, so for i in 0..10 is unaffected. The ordering operators stay out, and not for want of work: complex numbers have no order, so 1i > 2 is reported rather than answered, with a pointer to abs(z1) > abs(z2). The same rule text and two different quantities already follow.
- Form view: an input can now be a text field. A variable holding text (loc1 = "JN48HU" in the QTH locator) needed its quotes typed into the form field, and forgetting them turned the sheet into “Undefined variable”. The form now shows JN48HU and writes the quotes itself; the " button in the form editor marks such a field, and linking a variable that already holds text marks it for you.
- Form view: the text is selectable now — title, description, labels and above all the output values. A result could only be read off and typed out again; it can now be selected with the mouse or a long press and copied, exactly like the result column of the editor.
- Form editor: the automatic mode now recognizes text as an input. A sheet whose input is a locator, a name or a city (loc1 = "JN48HU") came back with an empty form — no input meant the outputs depending on it were dropped too. Such an assignment now becomes a text field.
- Line references move with the line. @3 pointed at the third line — and at the new, empty one as soon as you inserted a line above it, so a sheet with ten references was easier to retype than to repair. Inserting, deleting and pasting now rewrite the numbers; a reference to a line you delete says so instead of quietly pointing somewhere else.
- Truth values are their own type: done = 5 > 3 now reads true instead of 1, so a line shows at a glance that it answers a question rather than counting something. They still work as numbers wherever that was the point — sum([true, false, true]) is 2, true + 1 is 2, and a form chip writing 1 or 0 is unaffected.
- Comparisons with units now convert first: 2 m > 100 cm was false, because the operators compared the bare numbers 2 and 100. min, max and clamp had already been fixed; the comparison operators had been missed.
- +, -, % and the ordering comparisons now check that the quantities belong together. 2 m + 3 kg gave 5 m and 5 km > 3 kg gave true — both carried on in SI without asking whether the question makes sense. Multiplication and division are unaffected (they compose quantities), == and != still answer false rather than an error, and a plain number on one side (2 m + 3) stays allowed.
- A square root now halves the unit: sqrt(4 m²) is 2 m instead of 2 without a unit, and (4 m²)^0.5 is 2 m instead of 2 m². sqrt(R² + X²) gives ohms, sqrt(9 m²/s²) gives m/s, (8 m³)^(1/3) gives m. A root that would not come out even (sqrt(4 m) needs m^0.5) now says so instead of silently dropping the unit.
- A unit may be a symbol: after defunit("Ω", "V/A") the sheet can say 50 Ω, 50Ω or 30 + 40i Ω. Only €£¥₿$ were built in, so a unit whose name starts with a Greek letter was read as unused input. kΩ now reads as one unit as well, not as a variable k.
- Complex numbers carry a unit: Z = 50 + 30i Ohm is an impedance and keeps calculating as one — Z * 2 A is 100 + 60i V, U / I gives ohms back. Both parts are scaled by one factor, so 50000 + 30000i Ohm reads 50 + 30i kOhm.
- Complex numbers in lists and statistics: sum([1i, 2]) is 2 + 1i, [1i, 2] 2 is [2i, 4], and [50 + 30i Ohm, 20 Ohm] 2 A is [100 + 60i V, 40 V]. Each of these used to report “list expected”.
- exp, log, log10, log2, sin, cos, tan, asin, acos and atan take complex arguments — exp(1i * pi) is -1, log(exp(2 + 3i)) comes back to 2 + 3i. The real path gains the most: log(-1) is 3.1415926536i, asin(2) is 1.5707963268 - 1.3169578969i and (-8)^0.5 is 2.8284271247i, where each used to be a silent NaN.
- Complex matrices: [1i, 2 ;; 3, 4] is a matrix now, not an error. Meant for mesh analysis — det, inv and the matrix product work, and the units come along: det of an impedance matrix is in Ohm², its inverse in 1/Ohm.
- solve writes complex roots down: solve(x^2 + 1, x) gives x = -1i, x = 1i, and solve(x^2 + 2*x + 5, x) gives x = -1 - 2i, x = -1 + 2i. A negative discriminant used to end at “No real solution” — true, but half the answer.
- Undo takes deletions one at a time. Backspace never recorded a step, so Ctrl+Z jumped back to the last Enter and took every deletion on the way with it. Holding the key down still counts as one move.
- The debug trace now sits at the line it came from. With two nested loops, the inner passes were listed under the outer for line, where nobody looks for them.
- The menu bar drops its button labels as soon as they no longer fit, instead of at a fixed window width — the text moves into the tooltip. The width is measured in the font the labels are actually drawn in, including the system text scale, so a long translation (“Formulário”) no longer sits outside the visible area.
- 3D charts: surface(sin(x) * cos(y)) samples a formula in x and y and draws the height as a shaded surface, scatter3(xs, ys, zs) plots three lists as a point cloud. Drag inside the chart to turn it, double-click to go back to the default view; SVG and PNG export the angle currently on screen, CSV writes x,y,z. Singularities leave a hole in the surface instead of a jump.
- Samples: twelve sheets wrote units into the document that are not units — a field marked {years} stored term = 25 years, and the notebook fell apart into “unused input” and a chain of undefined variables. The unit now sits in the label. The QTH locator also showed its distance in miles labelled km.
- Tab now indents a multi-line selection instead of replacing it with two spaces; Shift+Tab outdents.
- Unary minus now binds weaker than ^: -2^2 is -(2²) = -4, matching normal math notation and spreadsheets. (-2)^2 is still 4.
- A label followed by a duration like "1 h / 30 min" no longer confuses the parser: test: 1 h / 30 min is 2 instead of “2s”.
- The sidebar no longer flashes a render-overflow warning while it slides open or closed.
- total now recalculates correctly when a blank line between two numbers is added or removed.
- A loaded CSV table now belongs to its draft and disappears with it instead of staying loaded after closing and reopening — and its labels now survive saving to a .dply file.
- solve, simplify and factor no longer substitute the variable you're solving for (or asking to simplify): solve(x^2 - 4, 0, x) works again even if x was assigned earlier in the sheet, and simplify(x + x) stays 2x instead of collapsing to a number once x had a value.
- Form view: inputs and outputs can be grouped into collapsible sections, each shown as its own card. Write #Title on its own line where a new section should start; add |collapsed to start it folded.
- Form view: fields sit side by side, in as many columns as the window allows (up to three).
- Form view: chart size is a S/M/L/XL choice instead of a fixed height.
- 25k now means 25000 and 1.5M means 1500000 — but only when no unit letter follows, so 25kg is still kilograms.
- A parenthesised expression or a function call's argument list can now span multiple lines too, not just a list literal — and, like a list, it works inside a function body as well as at the top level of a sheet. The editor also indents automatically after an open ( the same way it already did after [.
- Form editor: automatic-mode suggestions can now be reviewed before they're added. Applying used to add every detected field in one go, mixed in with what you'd already entered, with no way to tell which ones were new. The banner now starts collapsed with a single “Apply all”; expanding it splits the suggestions into Inputs and Outputs with a checkbox per field, so you can add only the ones you want.
- Form view: clearing a number field no longer breaks the sheet. It used to write the empty assignment straight into the document (kapital = ), turning every line that used the variable into a silent error the form never pointed to. The last value now reappears once you leave the field.
- Font size is now three independent settings — Editor, Interface and Form — instead of one slider that only ever resized the editor. Settings, the form editor, Help & Reference and the file-info dialog previously stayed a fixed size regardless of it, and a filled-in form had no font-size control of its own at all; each now has its own slider in Settings.
- csvwrite() can now export text, not just numbers — a plain list of strings, a mix of text and numbers, or a table built row by row (append(t, ["Name", "Amount"]), append(t, ["Rent", 500])). It used to silently write an empty (0 KB) file for anything but a pure number matrix.
- csv() can now read a text column back. A column written with real text came back as nothing but NaN, because every cell was forced into a number regardless of content; a text (or mixed) column now comes back as a list of the actual values instead.
- A long text-interpolation line ("…{expr}…") now wraps like any other line instead of being clamped to a single row — the rest of it used to be cut off entirely (not just visually truncated), while the gutter and result column had already made room for the extra rows, throwing off the alignment below it.
- A line with a large number could still lose its wrapped tail, most noticeably when indented. The thousands separator (1,234,567) only ever appeared in the drawn line, not in the width used to decide whether it needed to wrap, so a line just barely fitting without the separator sometimes wasn't given enough room once it was actually drawn with one.
- A long note (plain text, not a formula) in the result column now wraps across the lines its own line already reserves there, instead of being clamped to a single one and cut off without any visible sign of it; only once it runs past that reserved space does it end in “…”, with the full text still available in a tooltip.
- A list or named-list result that heads a multi-line literal — ausgabe = list(…) written across several lines — now uses the blank continuation lines already reserved for it in the result column, instead of stopping after one line while the rest of that space sits empty.
- log2() returned the wrong number for almost every input — log2(1024) was 4.8045 instead of 10 — because it divided by log2(e) instead of ln(2). Only log2(1) happened to still read 0.
- variance() of a list with a unit, like variance([1 m, 2 m, 3 m]), showed a stray "m" next to the number even though variance deliberately drops the unit (a variance is m², not m — the code already said so, the display just wasn't listening). It's a plain number again.
- A total line with a trailing comment (total # groceries) silently stopped summing and was read as plain text instead. total now works the same with or without a comment after it.
- A list or a function call typed without a space after the comma silently corrupted its numbers: [1,2,3,4] read as [1.2, 3.4], and contains, median, mode and percentile inherited the wrong data from it. Three or more comma-separated numbers in a row are unambiguous — a plain decimal like 1,5 * 2 still reads as 1.5, but 1,2,3,4 now reads as four numbers again, not two.
- pie(values) draws a pie chart, pie(values, labels) adds slice labels — and a named list turns straight into one in a single argument: pie(["Rent": 900, "Groceries": 320, "Transport": 140]) takes the keys as labels and the values as slice sizes. A unit shared by every entry is shown on the chart and in its legend; SVG, PNG and CSV export all work the same as for the other chart types.
- Closing the last remaining sheet while it was pinned left the fresh, empty sheet that replaced it pinned too. Closing the very last sheet resets it in place instead of deleting it (there must always be at least one), and that reset cleared the name, content and file path but forgot the pin.
0.2.0 — 2026-07-27
- Numbers copied from the web with thousands separators (e.g. 35,987.34 or 35.987,34) are now evaluated, displayed and saved correctly.
- Closing a file with unsaved changes now asks whether to save.
- Form view: input fields show the correct values again after switching documents.
- Form editor: a new automatic mode reads your sheet and suggests inputs and outputs — plain numbers become inputs, dependent variables become outputs. Apply it with one tap, then keep only what you need.
- Form editor: linking a variable now fills in its unit automatically.
- “Restore purchase” no longer hangs and now shows feedback.
0.1.0 — 2026-07-12