carsum: a 60-line CSV → price/mileage summary for used-car listings
Zero-dependency Node script. Reads a CSV (file or stdin), parses "$12,500" / "84,000 mi", skips junk rows, groups by make + model: count, median price, median mileage, $ per 10k mi (least-squares slope, 3+ rows).
Run: node carsum.mjs listings.csv
Output on made-up sample data (first 6 lines):
make/model count median price median miles $ per 10k mi
Toyota Corolla 5 $14,500 62,000 -1,027
Honda Civic 4 $15,350 56,500 -1,145
Ford F-150 3 $22,500 98,000 -1,713
Mazda 3 2 $17,300 42,500 -
Subaru Outback 1 $18,900 81,000 -
Limits: no multi-line quoted fields; the slope ignores year and trim.
#!/usr/bin/env node
// carsum: summarize price/mileage of used-car listings from a CSV file or stdin.
import { readFileSync } from 'node:fs';
function parseLine(line) {
const fields = [];
let cur = '', inQuotes = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (inQuotes && c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
else if (c === '"') inQuotes = !inQuotes;
else if (c === ',' && !inQuotes) { fields.push(cur); cur = ''; }
else cur += c;
}
fields.push(cur);
return fields.map((f) => f.trim());
}
const toNumber = (s) => (/\d/.test(s ?? '') ? Number(s.replace(/[^0-9.\-]/g, '')) : NaN);
const median = (xs) => {
const s = [...xs].sort((a, b) => a - b), m = Math.floor(s.length / 2);
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
};
function slopePer10k(rows) {
if (rows.length < 3) return '-';
const mx = rows.reduce((a, r) => a + r.mileage, 0) / rows.length;
const my = rows.reduce((a, r) => a + r.price, 0) / rows.length;
let num = 0, den = 0;
for (const r of rows) { num += (r.mileage - mx) * (r.price - my); den += (r.mileage - mx) ** 2; }
return den === 0 ? '-' : Math.round((num / den) * 10000).toLocaleString('en-US');
}
const text = readFileSync(process.argv[2] ?? 0, 'utf8');
const [header, ...lines] = text.split(/\r?\n/).filter((l) => l.trim() !== '');
const cols = parseLine(header).map((h) => h.toLowerCase());
const [pi, mi, mki, mdi] = ['price', 'mileage', 'make', 'model'].map((n) => cols.indexOf(n));
if (pi < 0 || mi < 0) { console.error('CSV needs "price" and "mileage" columns'); process.exit(1); }
const rows = [];
let skipped = 0;
for (const line of lines) {
const f = parseLine(line);
const price = toNumber(f[pi]), mileage = toNumber(f[mi]);
if (!Number.isFinite(price) || !Number.isFinite(mileage)) { skipped++; continue; }
const group = [mki >= 0 ? f[mki] : '', mdi >= 0 ? f[mdi] : ''].join(' ').trim() || '(ungrouped)';
rows.push({ group, price, mileage });
}
const byGroup = new Map();
for (const r of rows) byGroup.set(r.group, [...(byGroup.get(r.group) ?? []), r]);
const groups = [...byGroup].sort((a, b) => b[1].length - a[1].length);
const fmt = (name, rs) => [name.padEnd(22), String(rs.length).padStart(5),
('$' + Math.round(median(rs.map((r) => r.price))).toLocaleString('en-US')).padStart(12),
Math.round(median(rs.map((r) => r.mileage))).toLocaleString('en-US').padStart(12),
slopePer10k(rs).padStart(14)].join(' ');
console.log(['make/model'.padEnd(22), 'count'.padStart(5), 'median price'.padStart(12),
'median miles'.padStart(12), '$ per 10k mi'.padStart(14)].join(' '));
for (const [name, rs] of groups) console.log(fmt(name, rs));
if (rows.length) console.log(fmt('all listings', rows));
console.log(`${rows.length} rows used, ${skipped} skipped (non-numeric price or mileage)`);