Text and the command line
Word frequencies#
Task. Count how often each word occurs and print the most frequent ones.
const TEXT = "the quick brown fox jumps over the lazy dog the fox"
fn countWords(text: str) -> Map<str, u64> pure {
var counts: Map<str, u64> = {:}
for w in text.splitWhitespace() {
counts[w.lower()] = (counts.get(w.lower()) ?? 0) + 1 // get returns u64?, ?? 0 when the word has not been seen
}
counts
}
fn main() io {
let counts = countWords(TEXT)
let top = from (w, n) in counts order by n desc, w take 3 select "{w}: {n}" // top 3: by descending count, then alphabetically
for line in top { print(line) }
print("distinct words: {counts.len()}")
}Output
the: 3 fox: 2 brown: 1 distinct words: 8
counts[w.lower()] = (counts.get(w.lower()) ?? 0) + 1is the classic counter:getreturnsu64?, and?? 0provides the initial value.- The query
from (w, n) in counts order by n desc, w take 3 select …sorts the pairs of the map and takes the first three. The second sort key (w) makes the order deterministic for equal counts. - Pitfall: a
Mapiterates in insertion order, not alphabetically - sort explicitly if you need alphabetical order.
Parsing CSV with errors#
Task. Read tabular data, find bad rows and aggregate the good ones.
data Sale { region: str; product: str; qty: i64; price: i64 }
@error data BadRow { line: usize; why: str }
const CSV = "region,product,qty,price\nnorth,pen,10,3\nsouth,ink,2,15\nnorth,ink,1,15\nsouth,pen,7,3"
fn parseRow(n: usize, line: str) -> Sale | BadRow {
let cols = line.split(",")
if cols.len() != 4 { fail BadRow{ line: n, why: "expected 4 columns, got {cols.len()}" } }
let qty = match cols[2].trim().parseInt() {
i64 v => v
ParseError => fail BadRow{ line: n, why: "qty is not a number" }
}
let price = match cols[3].trim().parseInt() {
i64 v => v
ParseError => fail BadRow{ line: n, why: "price is not a number" }
}
Sale{ region: cols[0], product: cols[1], qty, price }
}
fn parseAll(text: str) -> List<Sale> | BadRow {
var rows: List<Sale> = []
for (i, line) in text.lines().iter().enumerate().skip(1) { // skip the header
rows.push(parseRow(i + 1, line)?)
}
rows
}
fn main() io {
match parseAll(CSV) {
List<Sale> rows => {
let byRegion = from s in rows group by s.region into g select (g.key, g.count())
for (region, n) in byRegion { print("{region}: {n} sales") }
let revenue = rows.iter().map(s => s.qty * s.price).sum()
print("revenue: {revenue}")
}
BadRow e => print("line {e.line}: {e.why}")
}
match parseAll("h\nnorth,pen,x,3") {
List<Sale> rows => print("{rows.len()} rows")
BadRow e => print("line {e.line}: {e.why}")
}
}Output
north: 2 sales south: 2 sales revenue: 96 line 2: qty is not a number
parseRowreturnsSale | BadRow: the error knows the line number and the reason.fail BadRow{ … }is an early exit.match s.parseInt()takesi64 | ParseErrorapart; we convert the error into our own, more meaningful one.rows.push(parseRow(i + 1, line)?)-?stops parsing at the first bad row. To collect all errors, accumulate them in aList<BadRow>instead of using?.from s in rows group by s.region into g select (g.key, g.count())is a grouping;rows.iter().map(…).sum()is an aggregate.- For real CSV with quotes use a format library;
split(",")is fine for simple data.
Command-line arguments#
Task. Parse --name Ann --times 3 --loud without external libraries.
use std.os { args }
@error data Usage { msg: str }
data Options { name: str; times: i64; loud: bool }
fn parseArgs(argv: List<str>) -> Options | Usage {
var opts = Options{ name: "world", times: 1, loud: false }
var i = 0
while i < argv.len() {
match argv[i] {
"--name" => {
opts.name = argv.get(i + 1) ?? fail Usage{ msg: "--name needs a value" }
i += 1
}
"--times" => {
let raw = argv.get(i + 1) ?? fail Usage{ msg: "--times needs a value" }
match raw.parseInt() {
i64 n => opts.times = n
ParseError => fail Usage{ msg: "--times: not a number: {raw}" }
}
i += 1
}
"--loud" => opts.loud = true
other => fail Usage{ msg: "unknown option {other}" }
}
i += 1
}
opts
}
fn main() -> () | Usage io {
let opts = parseArgs(args().iter().skip(1).toList())? // args()[0] is the program name
for _ in 0..opts.times {
let text = "Hello, {opts.name}!"
print(if opts.loud { text.upper() } else { text })
}
print("{parseArgs(["--times", "2", "--loud", "--name", "Ann"]) is Options}")
match parseArgs(["--bogus"]) {
Options _ => print("ok")
Usage e => print("usage error: {e.msg}")
}
}Output
Hello, world! true usage error: unknown option --bogus
std.os.args()returnsList<str>; element zero is the program name, henceskip(1). Run:oxl run prog.oxl -- --name Ann --loud.- Parsing is a pure function
parseArgs(argv): it is easy to test without launching the program (in the example it is called with ready-made lists). argv.get(i + 1) ?? fail Usage{…}is "a value or a usage error":failis an expression of typenever, so it can stand to the right of??.fn main() -> () | Usage io- an unhandledUsageends the process with exit code 1 and a message.
An aligned table#
fn row(cols: List<str>, widths: &List<usize>) -> str pure {
var parts: List<str> = []
for (i, c) in cols.iter().enumerate() {
parts.push(c.padEnd(widths[i], ' ')) // padEnd/padStart align to a width
}
parts.iter().fold("", (a, p) => if a.isEmpty() { p } else { "{a} | {p}" })
}
fn main() io {
let items = [("pen", 3, 10), ("notebook", 120, 2), ("ink", 15, 1)]
let widths: List<usize> = [10, 6, 5]
print(row(["item", "price", "qty"], &widths))
print("-".repeat(25))
var total = 0
for (name, price, qty) in items {
print(row([name, "{price}".padStart(5, ' '), "{qty}".padStart(5, ' ')], &widths))
total += price * qty
}
print("total: {total}")
}Output
item | price | qty ------------------------- pen | 3 | 10 notebook | 120 | 2 ink | 15 | 1 total: 285
padEnd and padStart pad a string to a width; "-".repeat(25) draws a line. The column widths are passed by borrow (&widths): the list
stays with the caller and is used many times (without & the second call would give use-after-move).
Files#
Task. Write a file, append to it, read it, look at the metadata and the directory, and handle a missing file gracefully.
use std.io { File, fs }
fn main() -> () | IoError io {
let dir = fs.tempDir()? // a temp directory, removed at the end
let path = "{dir}/notes.txt"
File.writeText(path, "first\nsecond\n")?
File.appendText(path, "third\n")?
let text = File.readText(path)?
let lines = text.lines()
print("{lines.len()} lines, last: {lines.last() ?? "-"}")
let meta = File.metadata(path)?
print("size {meta.size} bytes, is file: {meta.isFile()}")
File.writeText("{dir}/other.txt", "x")?
let entries = fs.list(dir)?
let names = entries.iter().map(e => e.name).sortBy(n => n).toList()
print("{names}")
match File.readText("{dir}/missing.txt") { // an I/O error is a value
str _ => print("unexpected")
IoError e => print("cannot read: {e.kind}")
}
fs.removeAll(dir)?
print("cleaned: {!File.exists(path)}")
}Output
3 lines, last: third size 19 bytes, is file: true ["notes.txt", "other.txt"] cannot read: NotFound cleaned: true
- All
std.io.Fileoperations return… | IoErrorand need theioeffect.fn main() -> () | IoError iolets you write?everywhere. fs.tempDir()creates a temporary directory - safe for examples and tests;fs.removeAlldeletes it.IoErroris a value with akindfield (NotFound,PermissionDenied, …); this lets you react differently: create the file if it is missing but stop on a permission error.- For large files read in parts with streams (
std.stream) rather than the wholereadText.
Back: cookbook.