typestar

csv-report.js en JavaScript

Un reporte en Node: leer un CSV, derivar columnas, agrupar e imprimir una tabla.

#!/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)}%`);

Cómo funciona

  1. El parser maneja campos entrecomillados, porque un CSV real los tiene.
  2. La agrupación usa un Map, así se conserva el orden de inserción.
  3. La tabla se rellena según el valor más ancho de cada columna.

Palabras clave y builtins usados aquí

El intento, en números

Líneas
99
Caracteres a escribir
2494
Tokens
749
Ritmo de tres estrellas
115 tpm

Al ritmo de tres estrellas de 115 tokens por minuto, este intento toma unos 391 segundos.

Escribe este fragmento

Paso 1 de 1 en Bis; paso 18 de 18 en Node.js.

← Anterior