typestar

csv-report.js in JavaScript

A Node report: read a CSV, derive columns, group, and print a table.

#!/usr/bin/env node
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

const CSV = `day,lang,chars,seconds,errors
2026-07-20,go,1820,60,4
2026-07-20,rust,1510,60,9
2026-07-21,go,1980,60,2
2026-07-21,"sql, standard",1240,60,1
2026-07-22,go,2050,60,3
2026-07-29,rust,1740,60,2
`;

function parseLine(line) {
  const fields = [];
  let current = "";
  let quoted = false;

  for (const char of line) {
    if (char === '"') {
      quoted = !quoted;
    } else if (char === "," && !quoted) {
      fields.push(current);
      current = "";
    } else {
      current += char;
    }
  }
  fields.push(current);
  return fields.map((f) => f.trim());
}

async function readRows(path) {
  const lines = createInterface({ input: createReadStream(path) });
  let header;
  const rows = [];

  for await (const line of lines) {
    if (!line.trim()) continue;
    const fields = parseLine(line);
    if (!header) {
      header = fields;
      continue;
    }
    rows.push(Object.fromEntries(header.map((key, i) => [key, fields[i]])));
  }
  return rows;
}

function derive(row) {
  const chars = Number(row.chars);
  return {
    ...row,
    wpm: chars / 5 / (Number(row.seconds) / 60),
    accuracy: (1 - Number(row.errors) / chars) * 100,
  };
}

function summarize(rows) {
  const groups = new Map();
  for (const row of rows) {
    const group = groups.get(row.lang) ?? { runs: 0, wpm: 0, best: 0 };
    group.runs += 1;
    group.wpm += row.wpm;
    group.best = Math.max(group.best, row.wpm);
    groups.set(row.lang, group);
  }
  return [...groups.entries()]
    .map(([lang, g]) => ({ lang, runs: g.runs, mean: g.wpm / g.runs,
                           best: g.best }))
    .sort((a, b) => b.mean - a.mean);
}

function table(rows) {
  const headers = ["lang", "runs", "mean", "best"];
  const widths = headers.map((key) =>
    Math.max(key.length, ...rows.map((row) => String(row[key]).length)));

  const line = (values) =>
    values.map((value, i) => String(value).padEnd(widths[i])).join("  ");

  const body = rows.map((row) =>
    line([row.lang, row.runs, row.mean.toFixed(1), row.best.toFixed(1)]));
  return [line(headers), ...body].join("\n");
}

const dir = await mkdtemp(join(tmpdir(), "report-"));
const path = join(dir, "runs.csv");
await writeFile(path, CSV);

const rows = (await readRows(path)).map(derive);
console.log(`${rows.length} runs read`);
console.log(table(summarize(rows)));

const mean = rows.reduce((t, row) => t + row.accuracy, 0) / rows.length;
console.log(`mean accuracy ${mean.toFixed(2)}%`);

How it works

  1. The parser handles quoted fields, because a real CSV has them.
  2. Grouping uses a Map, so insertion order is kept.
  3. The table is padded from the widest value in each column.

Keywords and builtins used here

The run, in numbers

Lines
99
Characters to type
2494
Tokens
749
Three-star pace
115 tpm

At the three-star pace of 115 tokens a minute, this run takes about 391 seconds.

Type this snippet

Step 1 of 1 in Encore, step 18 of 18 in Node.js.

← Previous