Recipes#
Short, runnable answers to the problems that come up in most apps: dates,
money, CSV exports, and a few API helpers. Every snippet here was executed with
bialet -r or against a running app.
Read Common Pitfalls as well: it covers the API traps these
recipes work around (toString is a getter, a missing return returns null,
iterating null crashes, a literal % is \x25, a Query takes at most three
arguments).
Dates: add days, add months, week start, due dates#
Date has no +/- and cannot be built from a Unix timestamp. Keep the civil
calendar arithmetic in one helper and call it everywhere:
class Dates {
static leap(y) { (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) }
static daysInMonth(y, m) {
if (m == 2) return Dates.leap(y) ? 29 : 28
return [4, 6, 9, 11].contains(m) ? 30 : 31
}
// Adds calendar months, clamping the day: 2024-01-31 + 1 -> 2024-02-29.
static addMonths(date, n) {
var total = date.year * 12 + (date.month - 1) + n
var y = (total / 12).floor
var m = total % 12 + 1
var d = date.day
var last = Dates.daysInMonth(y, m)
if (d > last) d = last
return Date.new(y, m, d, date.hours, date.minutes, date.seconds)
}
// Days -> civil date (Howard Hinnant's civil_from_days, see datetime.md).
static fromUnix(secs) {
var days = (secs / 86400).floor
var z = days + 719468
var era = ((z >= 0 ? z : z - 146096) / 146097).floor
var doe = z - era * 146097
var yoe = ((doe - (doe / 1460).floor + (doe / 36524).floor - (doe / 146096).floor) / 365).floor
var y = yoe + era * 400
var doy = doe - (365 * yoe + (yoe / 4).floor - (yoe / 100).floor)
var mp = ((5 * doy + 2) / 153).floor
var d = doy - ((153 * mp + 2) / 5).floor + 1
var m = mp < 10 ? mp + 3 : mp - 9
y = m <= 2 ? y + 1 : y
return [y, m, d]
}
static dateFromUnix(secs) {
var ymd = Dates.fromUnix(secs)
return Date.new(ymd[0], ymd[1], ymd[2])
}
// 0 = Sunday, so shift by six to make Monday the first day.
static startOfWeek(date) {
var dow = Num.fromString(date.format("#w"))
return Dates.dateFromUnix(date.unix - ((dow + 6) % 7) * 86400)
}
static addDays(date, n) { Dates.dateFromUnix(date.unix + n * 86400) }
}
Dates.addMonths(Date.new(2024, 1, 31), 1).format("#Y-#m-#d") // 2024-02-29
Dates.startOfWeek(Date.new(2026, 10, 18)).format("#Y-#m-#d") // 2026-10-12
Dates.addDays(Date.new(2026, 10, 18), 60).format("#Y-#m-#d") // 2026-12-17
Format tokens are strftime-style with # instead of %: #Y #m #d #H #M #S,
#w (0 = Sunday), #u (1 = Monday), #V (ISO week), #F (ISO date). The
full table is in Date and Time Functions.
Comparing dates is safe with <, >, ==. Comparing date strings is not:
"2026-01-01" < "2026-02-01" raises String does not implement '<(_)'. Parse
to Date.new(text) first, and guard the null that .first/.val return when
there is no row.
Money: store cents, print two decimals#
Wren has no Num.format. Store amounts as integer cents and format them with a
helper; never do float arithmetic on pesos/dollars across many rows.
class Money {
// Accepts "1234.5", "1.234,50", "12,5", "19.99" and null.
static parse(text) {
if (text == null) return 0
var t = "%(text)".trim().replace(" ", "")
if (t == "") return 0
if (t.contains(",")) {
// Spanish format: dots are thousands, comma is the decimal separator.
t = t.replace(".", "").replace(",", ".")
} else if (t.contains(".")) {
var parts = t.split(".")
var last = parts[parts.count - 1]
// "1.234" is thousands; "19.99" is decimal.
if (parts.count == 2 && last.count == 3) t = t.replace(".", "")
}
return Num.fromString(t) || 0
}
static cents(text) { (Money.parse(text) * 100).round }
static fmt(cents) {
var negative = cents < 0
var abs = negative ? -cents : cents
var whole = (abs / 100).floor
var frac = abs - whole * 100
return "%(negative ? "-" : "")%(whole).%(frac < 10 ? "0" : "")%(frac)"
}
}
Money.fmt(Money.cents("1234.567")) // 1234.57
Money.fmt(Money.cents("1.234,50")) // 1234.50
Money.fmt(Money.cents("12,5")) // 12.50
Money.cents("19.99") // 1999 (store this INTEGER)
Money.cents(null) // 0
In SQL, store INTEGER cents and sum with SUM(cents); convert only when
rendering. Keep the arithmetic in integers: summing REAL amounts across
hundreds of rows accumulates rounding error.
CSV download#
Response.header(name, value) sets the response header; there is no
Response.csv/download helper. Build the body, set both headers, and
return it:
var rows = `SELECT name, amount FROM expenses ORDER BY id`.fetch
var lines = ["concepto,importe"]
for (row in rows) {
var name = "%(row["name"])".replace("\"", "\"\"")
// Quote every field, including numbers: "12.500,50" contains a comma and
// would shift the columns if it were left bare.
var amount = "%(row["amount"])".replace("\"", "\"\"")
lines.add("\"%(name)\",\"%(amount)\"")
}
Response.header("Content-Type", "text/csv; charset=utf-8")
Response.header("Content-Disposition", "attachment; filename=\"gastos.csv\"")
return lines.join("\n") + "\n"
Quote a field whenever it contains a comma, a quote or a newline; double every
inner quote. Link to the route with a normal <a href="/exportar">Exportar CSV</a>.
In tests, assert the header instead of the whole file:
Test.get("/exportar").status(200).header("Content-Type", "text/csv; charset=utf-8").
bialet -r is not a request#
bialet -r executes a bare Wren script. It does not create the BIALET_*
tables or a request/session, so these fail there:
Session.new()→Query Error no such table: BIALET_SESSIONConfig.get(...)→Query Error no such table: BIALET_CONFIGApp queries (
SELECT ... FROM my_table) →no such table
Use it only for pure language questions (Date, Num, String, List,
Map). For framework behaviour, write a test in _tests/ and run bialet -T,
or start the server and try the page.
Util and Config#
Stable Util helpers:
Call |
Returns |
|---|---|
|
random URL-safe string of length |
|
salted PBKDF2 hash (store this in the DB) |
|
|
|
|
|
escaped HTML text |
|
|
|
constant-time string comparison |
Config is backed by BIALET_CONFIG:
Config.get("title") // String or null
Config.set("title", "Mi app")
Config.bool("beta_features") // true only for "1"/"true"
Config.num("page_size") // Num
Config.enable("beta_features") // same as Config.set("beta_features", "1")
Config.disable("maintenance") // same as Config.set("maintenance", "0")
Config.json("theme") // parsed JSON value
Config.bool(key) is an allow-list: true for "1" and "true"
(case-insensitive), false for everything else including a missing key. For
an opt-in flag (a test mode, a feature gate) that is the behavior you want, but
write Config.get(key) == "1" when the default should be obvious at the call
site. See Configuration.
Queries with more than three parameters#
A Query method takes at most three positional parameters. With four or more it
raises Query does not implement 'fetch(_,_,_,_)' (or fetch(_,_,_,_,_) with
five). Pass one array instead:
var rows = `SELECT * FROM items WHERE a = ? AND b = ? AND c = ? AND d = ?`
.fetch(["1", "2", "3", "4"])
The same rule applies to .first, .val, .toNum, .query, .toBool and
.order: three arguments maximum, then an array. See Database.
Tests: one database, no noise#
Every file in
_tests/runs in the same process against one temporary database for that run. A row inserted bya.wrenis visible tob.wren; do not assume each file starts clean.Seed shared rows in
_tests/_init.wren, which runs once before the files.System.printoutput becomes part of the test log. Never dump a whole table or a list of sessions; print one scalar when you need to debug, and delete the debug test afterwards. Hundreds ofdebugN.wrenfiles make every later run slower and bury the real failure.Testhas no cookie jar. For session-scoped behaviour, test the domain class directly, or assert the redirect and the row it wrote. When the app accepts an explicit header, use.setHeader("Cookie", "session=...")before the assertion.Never
import "bialet/test" for Test: the runner preloadsTest, and the import fails withModule variable is already defined.Assertions are lazy: the request fires on the first
.status/.contains. Chain them on the sameTest.get(...)call..containsmatches the raw HTML. Interpolation HTML-escapes strings, so a title with accents/quotes may appear as entities; assert on text that is stable, or read the stored row instead.
See Testing in Bialet for the full runner reference.