1brc.nika

// 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}"
}

The file itself · all the examples

This site is open source. Improve this page.