When a tool needs to answer "given this data, what's the total, the
breakdown, the top few?", reach for a frame — a small in-language data
table of named columns you total, sort, group, and join, the way you
would a spreadsheet. And because SCUA has an exact decimal
number, a column of money totals to the penny instead of drifting the
way a float-based tool does.
This walkthrough takes a batch of in-app purchases and answers the
questions a live-ops dashboard asks: total revenue, a per-row line
total, revenue by region, the best sellers, and the local tax rate
joined in. The full program is examples/frames.scua;
run it with scua examples/frames.scua.
#Build the table
Pass a table of { column-name = array }, every column
the same length. Money goes in as a decimal (the
d suffix), so it stays exact:
let sales = frame({
region = ["EU", "US", "EU", "US", "EU"],
item = ["sword", "sword", "shield", "potion", "potion"],
net = [10.50d, 20.00d, 5.25d, 3.00d, 3.00d], -- exact decimal money
qty = [1, 2, 4, 5, 3],
})
#Total a column, exactly
Operations read on the frame and chain. total sums a
column; a decimal column sums exactly, an int
column sums to an int:
print(`total revenue = {sales.total("net")}`) -- total revenue = 41.75
print(`units sold = {sales.total("qty")}`) -- units sold = 15
#Add a computed column
with builds a new column from an expression evaluated
per row. Column names are written bare — net and
qty here are the row's columns:
print(sales.with("line_total", net * qty).pick(["item", "net", "qty", "line_total"]).to_table())
┌────────┬───────┬─────┬────────────┐
│ item │ net │ qty │ line_total │
├────────┼───────┼─────┼────────────┤
│ sword │ 10.50 │ 1 │ 10.50 │
│ sword │ 20.00 │ 2 │ 40.00 │
│ shield │ 5.25 │ 4 │ 21.00 │
│ potion │ 3.00 │ 5 │ 15.00 │
│ potion │ 3.00 │ 3 │ 9.00 │
└────────┴───────┴─────┴────────────┘
pick keeps only the named columns, in that order, and
to_table() renders the frame as a box-drawing string you
can print, drop in a report, or paste into a chat.
#Break it down by group
group_sum is the everyday analytical question: group by
one column, sum another. The money stays exact:
print(sales.group_sum("region", "net").to_table())
┌────────┬───────┐
│ region │ total │
├────────┼───────┤
│ EU │ 18.75 │
│ US │ 23.00 │
└────────┴───────┘
#Rank the top rows
Sort descending, then keep the first few and the columns you care about:
print(sales.sort_by("net", true).head(3).pick(["item", "region", "net"]).to_table())
┌────────┬────────┬───────┐
│ item │ region │ net │
├────────┼────────┼───────┤
│ sword │ US │ 20.00 │
│ sword │ EU │ 10.50 │
│ shield │ EU │ 5.25 │
└────────┴────────┴───────┘
#Join in another table
join combines two frames on a shared key column —
"inner" by default, or "left" to keep every
left row. Here it attaches each region's tax rate:
let tax = frame({ region = ["EU", "US"], rate = [0.20d, 0.08d] })
print(sales.join(tax, "region").pick(["item", "region", "net", "rate"]).head(3).to_table())
┌────────┬────────┬───────┬──────┐
│ item │ region │ net │ rate │
├────────┼────────┼───────┼──────┤
│ sword │ EU │ 10.50 │ 0.20 │
│ sword │ US │ 20.00 │ 0.08 │
│ shield │ EU │ 5.25 │ 0.20 │
└────────┴────────┴───────┴──────┘
#Load it from a CSV instead
Real data usually arrives as text. csv(text) parses it
straight into a frame: the first line names the columns, and each cell
is typed per value — a whole number becomes an int, a
decimal-point number an exact decimal, anything else a
string. So a money column from a CSV is still exact:
let imported = csv("city,revenue
Paris,1200.50
Tokyo,980.00
Paris,300.25")
print(imported.group_sum("city", "revenue").to_table())
┌───────┬─────────┐
│ city │ total │
├───────┼─────────┤
│ Paris │ 1500.75 │
│ Tokyo │ 980.00 │
└───────┴─────────┘
#A typo is caught before it runs
When you build a frame from a frame({ … }) literal, its
columns are known at compile time, so a mistyped column name — or
summing a text column — is an error before the program runs, with a
suggestion:
sales.total("amont") -- error: frame has no column 'amont' — did you mean 'net'?
sales.total("region") -- error: can't `total` column 'region' — it holds strings, not numbers
That check follows the chain, so
sales.filter(…).total("amont") is caught too. A frame whose
shape is genuinely dynamic — one from csv,
join, or a function argument — is checked at runtime
instead.
#Where to go next
- Free-text columns:
f.filter(note.contains("…")),f.filter(note.matches(re)), andf.search(needle, opts)— see Search a text column. - The full set of operations (
mean,pivot,unpivot, computed columns, the filter escape hatch) is in the data tables guide. - To validate loose data into a trusted shape before you tabulate it, see Validate and transform incoming data.
- For more on exact money, see Handle money.