// The One Billion Row Challenge: the lowest, mean and highest temperature // of each weather station, from one reading per line. // // printf 'Hamburg;12.0\nBulawayo;8.9\nHamburg;-3.4\n' > measurements.txt // nikaia run -- measurements.txt // {Bulawayo=8.9/8.9/8.9, Hamburg=-3.4/4.3/12.0} // // To parse on every core, add `user-parallelism = "yes"` under `[build]` in // nikaia.toml; for timing, also `opt-level = 3` under `[build.x86_64-linux]`. // Name the project `brc`, not `1brc`: a package name cannot start with a digit. // // Shows: a `grammar` that describes the whole file, a parallel `par_fold`, // names that are slices of a memory-mapped file rather than copies, and a // format spec inside an f-string. // // To run it, copy this file to `src/main.nika` of a project (guide/getting-started.md). use std::fs use std::cli use std::text use std::collections // --------------------------------------------------------------------------- // 1. A grammar for the file // --------------------------------------------------------------------------- // A grammar for the whole file, not for one line: it is called once and walks // the bytes itself. There is no tokenizer, so station names may contain blanks, // dots and non-ASCII text. grammar Measurements { // An uppercase rule is *lexical*: nothing is skipped between its parts, so // "Hamburg " with a trailing blank is a station of its own. // // The `String` is a slice of the input, never a copy. // // The scan stops at the `;` or at `frame_end`, the line end that // MEASUREMENT below declares. A plain `until(";")` could run into the next // line, and the compiler rejects it here. rule NAME -> String = s:until(";" | frame_end) -> { s } // Temperatures always carry exactly one decimal, so they are parsed as // tenths and all the arithmetic stays in integers until printing. // `digit{1,2}` is the width the format allows (-99.9 to 99.9), so // "123.0" is a parse error. `dec[i32](…)` hands over the number those // digits write rather than their text, so the action has nothing left to // decode. rule TENTHS -> i32 = neg:"-"? whole:dec[i32](digit{1,2}) "." frac:digit -> { let value = whole * 10 + text::digit_value(frac) if neg != null { -value } else { value } } // `=>` is a commit point: once the `;` is there, the line is a measurement, // and a bad temperature after it is reported, not retried as something else. // // `@frame` says a measurement can be found from any offset by scanning to // the next "\n", so the file can be cut into pieces and parsed in parallel. // The compiler checks that no scan inside the frame can run past it (see NAME). @frame(boundary: "\n") rule MEASUREMENT -> Reading = name:NAME ";" => temp:TENTHS frame_end -> { Reading { name, temp } } // `MEASUREMENT*` would collect a billion readings into memory. A fold // instead passes each one to an accumulator, so the parse runs in // constant space. `par_fold` also says how two partial results merge; // with that and the frame, the compiler writes the splitting, one // accumulator per core and the final merge. The merge is exact because // everything is summed in integer tenths. entry rule file -> Summary = par_fold(MEASUREMENT, Summary, fn(acc, m) { acc.record(m) }, Summary::merge) } struct Reading { name: String, temp: i32, } // --------------------------------------------------------------------------- // 2. Aggregation // --------------------------------------------------------------------------- struct Stats { min: i32, max: i32, sum: i64, count: i64, } impl Stats { fn(first: i32) -> Stats { return Stats { min: first, max: first, sum: first as i64, count: 1 } } fn add(ref mut self, temp: i32) { if temp < self.min { self.min = temp } if temp > self.max { self.max = temp } self.sum += temp as i64 self.count += 1 } fn merge(ref mut self, other: ref Stats) { if other.min < self.min { self.min = other.min } if other.max > self.max { self.max = other.max } self.sum += other.sum self.count += other.count } // The mean in tenths, rounded as the challenge rounds: a tie goes up, // toward +infinity (`Math.round` in the official Java baseline). Exact // integer arithmetic, no float: `div_euclid` divides rounding down, also // when the sum is negative. fn mean(ref self) -> i64 { return (2 * self.sum + self.count).div_euclid(2 * self.count) } } // The stations seen so far and their statistics. The keys are slices of the // input too. struct Summary { stations: collections::HashMap[String, Stats], } impl Summary { fn() -> Summary { return Summary { stations: collections::HashMap() } } fn record(ref mut self, m: Reading) { self.stations .entry(m.name) .and_modify fn (stats) { stats.add(m.temp) } .or_insert_with fn { Stats(m.temp) } } fn merge(ref mut self, other: Summary) { for (name, stats) in other.stations.drain() { self.stations .entry(name) .and_modify fn (into) { into.merge(ref stats) } .or_insert(stats) } } } // --------------------------------------------------------------------------- // 3. Main: the I/O half // --------------------------------------------------------------------------- fn main() throws { let path = cli::args().nth(1) ?? "measurements.txt" // `fs::map` maps the file into memory instead of reading it, so a file of // many gigabytes is never copied, and every station name points into it. // No `async` or `await` is written: I/O does not block, and the code reads // straight through. `catch` runs its block when the call fails, with the // failure named `error`. let data = fs::map(ref path, fs::Root::Anywhere) catch { eprintln(f"cannot open {path}: {error}") return } // The whole parse. With no `catch`, a bad line leaves `main` through // `throws` and ends the program with an error that gives its line number // in the whole file, even when the file was parsed in pieces. let totals = Measurements::file(data) let mut names = totals.stations.keys().collect() names.sort() let body = names .map fn(name) { // A map lookup gives a `Stats?` that may be absent. `??` supplies // the other case; the key came from this map, so it cannot happen. let s = totals.stations[name] ?? panic(f"{name} was a key a moment ago") f"{name}={tenths(s.min as i64)}/{tenths(s.mean())}/{tenths(s.max as i64)}" } .join(", ") // In an f-string, `{{` and `}}` are literal braces. println(f"{{{body}}}") } // A value in tenths as the challenge prints it: one decimal, never `-0.0`. fn tenths(t: i64) -> String { if t < 0 { let a = -t return f"-{a / 10}.{a % 10}" } return f"{t / 10}.{t % 10}" }