SCUA

How-to

Summarize and query a table of data

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