Porting the Donation Import
Joan’s Python script checks every row of the October export, totals the good ones, and lists each
rejected row with its line number and reason. The welcome video’s line-by-line port printed 6
rows, 2 rejected, total £NaN, with ValueError is not defined as the reason for lines 4 and 6. In
this lesson, you rebuilt the script in JavaScript from the pieces you’ve met since.
- Unlike a Python module, a JavaScript module shares only what it exports, so
parseAmount()is exported, like every function in the port. Number()converts the text but, unlikefloat(), doesn’t throw:n/agivesNaN, and a blank amount gives0. So the port checks for itself, for blank text or a result that isn’t a finite number, with theNumber.isFinite()you met when choosing an error, which also turns away"Infinity".- Where Python’s
float()raised on its own, the port throws its ownRangeError, not aValueError, which JavaScript doesn’t have. The message quotes the text that arrived. The refund check throws aRangeErrortoo, so every bad amount has one type. - The loop moves into its own exported function,
checkRows(). Python’senumerate()starts at 2 to count the header as line 1; the port slices off the header, and.entries()pairs each line with its index from 0, so the line number a rejected row needs is the index plus 2. - JavaScript has no
csvmodule, so the port splits each line on";"and destructures the date, donor and amount. That’s enough for Joan’s export, which never quotes a field. Real CSV, with quoted fields, needs a package, such as csv-parse. - Python adds the amount in its
elseclause, but JavaScript has noelseafter acatch, so the port adds the amount right after the call. With no tuples,checkRows()returns[total, rejected]. main()takes the path as a parameter. It’s an async function, so it can awaitreadFile(), which reads the whole export in one call.text.trim().split("\n")cuts it into lines, after trimming the final newline..toFixed(2)writes the total with two decimals, as Python’s:.2fdoes. Each rejected row prints its line, donor and reason, in Python’s shape, once the exercise fills the list.import.meta.mainistrueonly in the filenodestarts, not when another file imports it, so the port callsmain()when Joan runs it, withprocess.argv[2], where Python readssys.argv[1].
Here’s donations.js so far, without rejecting a row and without the missing-file check:
donations.js
import { readFile } from "node:fs/promises";
export function parseAmount(text) {
const amount = Number(text);
if (text.trim() === "" || !Number.isFinite(amount)) {
throw new RangeError(`amount is not a number, got "${text}"`);
}
if (amount <= 0) {
throw new RangeError(`amount must be positive, got ${amount}`);
}
return amount;
}
export function checkRows(lines) {
let total = 0;
const rejected = [];
for (const [index, line] of lines.slice(1).entries()) {
const [date, donor, amountText] = line.split(";");
total += parseAmount(amountText);
}
return [total, rejected];
}
export async function main(path) {
const text = await readFile(path, "utf8");
const lines = text.trim().split("\n");
const [total, rejected] = checkRows(lines);
const count = lines.length - 1;
const summary = `${count} rows, ${rejected.length} rejected`;
console.log(`${summary}, total £${total.toFixed(2)}`);
for (const entry of rejected) {
const where = `line ${entry.line} (${entry.donor})`;
console.log(` ${where}: ${entry.reason}`);
}
}
if (import.meta.main) {
await main(process.argv[2]);
}