Locked learning resources

Join us and get access to thousands of tutorials and a community of expert JavaScript developers.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert JavaScript developers.

Unlock This Lesson

Porting the Donation Import

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, unlike float(), doesn’t throw: n/a gives NaN, and a blank amount gives 0. So the port checks for itself, for blank text or a result that isn’t a finite number, with the Number.isFinite() you met when choosing an error, which also turns away "Infinity".
  • Where Python’s float() raised on its own, the port throws its own RangeError, not a ValueError, which JavaScript doesn’t have. The message quotes the text that arrived. The refund check throws a RangeError too, so every bad amount has one type.
  • The loop moves into its own exported function, checkRows(). Python’s enumerate() 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 csv module, 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 else clause, but JavaScript has no else after a catch, 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 await readFile(), 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 :.2f does. Each rejected row prints its line, donor and reason, in Python’s shape, once the exercise fills the list.
  • import.meta.main is true only in the file node starts, not when another file imports it, so the port calls main() when Joan runs it, with process.argv[2], where Python reads sys.argv[1].

Here’s donations.js so far, without rejecting a row and without the missing-file check:

Language: JavaScript Filename: 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]);
}

Locked learning resources

Join us and get access to thousands of tutorials and a community of expert JavaScript developers.

Unlock This Lesson

Already a member? Sign-In

Locked learning resources

The full lesson is for members only. Join us and get access to thousands of tutorials and a community of expert JavaScript developers.

Unlock This Lesson

Already a member? Sign-In

00:02 Joan’s Python script checks every row of the October export, totals the good ones, and lists each rejected row with its reason. The welcome video’s line-by-line port printed this instead: two rows rejected for the wrong reason, and a total that isn’t a number.

00:21 In this lesson, you’ll rebuild the script in JavaScript from the pieces you’ve met since. Unlike Python, a JavaScript module shares only what it exports, so parseAmount() is exported.

00:37 Number() converts the text, but unlike float(), it doesn’t throw: n/a gives NaN, and a blank amount gives zero. So the port checks for itself: blank text, or a result that isn’t a finite number, the check you met when choosing an error, which also turns away infinity.

00:58 Where Python’s float() raised on its own, the port throws its own RangeError, since JavaScript doesn’t have a ValueError.

01:07 The message quotes the text that arrived. The refund check ports as it is, and throws a RangeError too, so every bad amount has one type.

01:18 Python’s loop moves into its own exported function, checkRows(). Python’s enumerate() starts at two, to count the header as line one.

01:29 The port slices off the header, and .entries() pairs each line with its index, from zero. So a rejected row’s line number is the index plus two.

01:41 JavaScript has no csv module, so the port splits each line on the semicolon. Destructuring unpacks the date, donor and amount. That’s enough for Joan’s export, which never quotes a field.

01:56 Real CSV, with quoted fields, needs a package, such as csv-parse.

02:03 Python adds the amount in its else clause. JavaScript has no else after a catch: it’s a SyntaxError, as you saw.

02:13 So the port adds right after the call, and it stays with the call, inside the try, once the exercise wraps it. Rejecting a bad row is the exercise’s job.

02:24 With no tuples, checkRows() returns both in an array.

02:30 main() takes the path as a parameter. It’s async, so it can await readFile(), which reads the whole export at once.

02:39 The text is cut into lines, after trimming the final newline. Python’s check for a missing file is the exercise’s too.

02:49 checkRows() hands back the total and the rejected list. toFixed(2) writes the total as Python’s format does. Each rejected row prints its line, donor and reason, once the exercise fills the list.

03:04 Here, import.meta.main is true only when node starts this file, not when another file imports it. So the port calls main() with process.argv[2], the first argument.

03:20 Run the port on October’s export. Nothing in checkRows() rejects a row yet. Which line of the export stops the run, and what does it print? Your turn: pause the video and predict.

03:38 Line 3, D-102’s n/a. JavaScript stops with a RangeError, and prints no report at all.

03:48 Read this trace from the top, the reverse of Python’s. The first frame is parseAmount()’s throw. The second is checkRows(), at the call that passed n/a in, then main(), and the awaited call at the bottom of the file.

04:04 Python lists that row and keeps going. The port doesn’t, yet.

04:12 That’s most of the import, the JavaScript way: parseAmount() checks for itself and throws a RangeError, checkRows() numbers the lines, and main() awaits the export.

04:24 Next up is the exercise Finish the Donation Import. You’ll make the port list each bad row instead of stopping, and refuse a missing export with a one-line message.

Become a Member to join the conversation.