Текст и командная строка
Частоты слов#
Задача. Посчитать, сколько раз встречается каждое слово, и вывести самые частые.
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 возвращает u64?, ?? 0 — если слова ещё не было
}
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}" // топ-3: по убыванию частоты, затем по алфавиту
for line in top { print(line) }
print("distinct words: {counts.len()}")
}Вывод
the: 3 fox: 2 brown: 1 distinct words: 8
counts[w.lower()] = (counts.get(w.lower()) ?? 0) + 1— классический счётчик:getвозвращаетu64?,?? 0задаёт начальное значение.- Запрос
from (w, n) in counts order by n desc, w take 3 select …сортирует пары словаря и берёт первые три. Второй ключ сортировки (w) делает порядок при равных частотах детерминированным. - Подводный камень:
Mapитерируется в порядке вставки, а не по алфавиту — если нужен алфавитный порядок, сортируйте явно.
Разбор CSV с ошибками#
Задача. Прочитать табличные данные, найти плохие строки и агрегировать хорошие.
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) { // пропускаем заголовок
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}")
}
}Вывод
north: 2 sales south: 2 sales revenue: 96 line 2: qty is not a number
parseRowвозвращаетSale | BadRow: ошибка знает номер строки и причину.fail BadRow{ … }— ранний выход.match s.parseInt()разбираетi64 | ParseError; ошибку преобразуем в свою, более осмысленную.rows.push(parseRow(i + 1, line)?)—?останавливает разбор на первой плохой строке. Чтобы собрать все ошибки, накопите их вList<BadRow>вместо?.from s in rows group by s.region into g select (g.key, g.count())— группировка;rows.iter().map(…).sum()— агрегат.- Для настоящих CSV с кавычками используйте библиотеку формата;
split(",")годится для простых данных.
Аргументы командной строки#
Задача. Разобрать --name Ann --times 3 --loud без внешних библиотек.
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] — имя программы
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}")
}
}Вывод
Hello, world! true usage error: unknown option --bogus
std.os.args()возвращаетList<str>; нулевой элемент — имя программы, поэтомуskip(1). Запуск:oxl run prog.oxl -- --name Ann --loud.- Разбор вынесен в чистую функцию
parseArgs(argv): её удобно тестировать без запуска программы (в примере она вызывается с готовыми списками). argv.get(i + 1) ?? fail Usage{…}— «значение или ошибка использования»:fail— выражение типаnever, поэтому его можно написать справа от??.fn main() -> () | Usage io— необработаннаяUsageзавершит процесс с кодом 1 и сообщением.
Выровненная таблица#
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 выравнивают по ширине
}
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}")
}Вывод
item | price | qty ------------------------- pen | 3 | 10 notebook | 120 | 2 ink | 15 | 1 total: 285
padEnd и padStart дополняют строку до ширины; "-".repeat(25) рисует линию. Ширины колонок передаются заимствованием (&widths): список
остаётся у вызывающего и используется многократно (без & второй вызов дал бы use-after-move).
Файлы#
Задача. Записать файл, дописать в него, прочитать, посмотреть метаданные и каталог, аккуратно обработать отсутствие файла.
use std.io { File, fs }
fn main() -> () | IoError io {
let dir = fs.tempDir()? // временный каталог, удалим в конце
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") { // ошибка ввода-вывода — значение
str _ => print("unexpected")
IoError e => print("cannot read: {e.kind}")
}
fs.removeAll(dir)?
print("cleaned: {!File.exists(path)}")
}Вывод
3 lines, last: third size 19 bytes, is file: true ["notes.txt", "other.txt"] cannot read: NotFound cleaned: true
- Все операции
std.io.Fileвозвращают… | IoErrorи требуют эффектаio.fn main() -> () | IoError ioпозволяет писать?везде. fs.tempDir()создаёт временный каталог — безопасно для примеров и тестов;fs.removeAllудаляет его.IoError— значение с полемkind(NotFound,PermissionDenied, …); так можно реагировать по-разному: создать файл, если его нет, но остановиться при отказе в доступе.- Для больших файлов читайте по частям потоками (
std.stream), а неreadTextцеликом.
Вернуться: кулинарная книга.