On Your Own: Reading Progress

The Course
•
5 min read

Close the planner solution and solve a new brief. A reading-progress tool imports JSON for one book, then reports how many pages remain. A valid book has a nonblank title, total pages and read pages. Both counts must be finite nonnegative integers, and read cannot exceed pages.

Return a Result union: an ok variant contains a clean Book; an error variant contains a message. Syntax failure uses “Invalid JSON”; shape or domain failure uses “Invalid book”. Trim the title in the accepted record. Do not coerce numeric strings or use assertions.

Keep the trusted calculation separate from parsing. remaining(book: Book) returns pages minus read. It should not need to re-parse JSON. Readonly properties document that the calculation does not edit the book.

Write a test plan before coding: a partial book, a fully read book, zero total pages, read greater than total, fractional counts, blank title, text counts, missing fields, null and malformed JSON. Make at least one assertion catch a deliberately broken condition, then restore your parser.

Budget twenty minutes for an initial attempt and return later if needed. The exercise is valuable even if you need more time. Try the idea hint before the first-step hint, and attempt the program before opening the complete worked answer.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
const pages = 120;
const read = 45;

console.log(pages - read);

Enable JavaScript to edit and type-check this example.

Check your prediction
75

Your turn

Exercise

On Your Own: Reading Progress: practise

Implement Book, Result, parseBook and remaining. Display Types: 75 pages left for {title:” Types “,pages:120,read:45}, Invalid book for read greater than pages, and Invalid JSON for ”{”. Also include runtime assertions for a fully read book and a zero-page book.

Your workspaceTypeScript
// Design the shapes, validate the boundary, calculate from trusted data, and test.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Separate syntax failure, invalid records and trusted arithmetic.

Hint 2: a first step

First narrow to a non-null object and verify title, pages and read. Then compare the two counts.

Show one solution
One possible solutionTypeScript
type Book = {
  readonly title: string;

  readonly pages: number;

  readonly read: number;
};

type Result =
  | {
      kind: "ok";

      book: Book;
    }
  | {
      kind: "error";

      message: string;
    };

function parseBook(json: string): Result {
  let value: unknown;

  try {
    value = JSON.parse(json);
  } catch {
    return { kind: "error", message: "Invalid JSON" };
  }

  if (
    typeof value !== "object" ||
    value === null ||
    !("title" in value) ||
    typeof value.title !== "string" ||
    value.title.trim() === "" ||
    !("pages" in value) ||
    typeof value.pages !== "number" ||
    !Number.isFinite(value.pages) ||
    !Number.isInteger(value.pages) ||
    value.pages < 0 ||
    !("read" in value) ||
    typeof value.read !== "number" ||
    !Number.isFinite(value.read) ||
    !Number.isInteger(value.read) ||
    value.read < 0 ||
    value.read > value.pages
  ) {
    return { kind: "error", message: "Invalid book" };
  }

  return {
    kind: "ok",
    book: { title: value.title.trim(), pages: value.pages, read: value.read }
  };
}

function remaining(book: Book): number {
  return book.pages - book.read;
}

function display(result: Result): string {
  if (result.kind === "error") {
    return result.message;
  }

  return `${result.book.title}: ${remaining(result.book)} pages left`;
}

function assertRemaining(json: string, expected: number): void {
  const result = parseBook(json);

  if (result.kind !== "ok" || remaining(result.book) !== expected) {
    throw new Error("Remaining-pages test failed");
  }
}

assertRemaining('{"title":"Done","pages":10,"read":10}', 0);
assertRemaining('{"title":"Empty","pages":0,"read":0}', 0);

function assertInvalid(json: string, message: string): void {
  const result = parseBook(json);

  if (result.kind !== "error" || result.message !== message) {
    throw new Error("Invalid-book test failed");
  }
}

for (const value of [
  null,
  {},
  { pages: 10, read: 0 },
  { title: false, pages: 10, read: 0 },
  { title: " ", pages: 10, read: 0 },
  { title: "Types", pages: "10", read: 0 },
  { title: "Types", pages: 10, read: "0" },
  { title: "Types", pages: -1, read: 0 },
  { title: "Types", pages: 10.5, read: 0 },
  { title: "Types", pages: 10, read: 0.5 },
  { title: "Types", pages: 10, read: -1 },
  { title: "Types", pages: 10, read: 11 },
  { title: "Types", read: 0 },
  { title: "Types", pages: 10 }
]) {
  assertInvalid(JSON.stringify(value), "Invalid book");
}

assertInvalid("{", "Invalid JSON");
assertInvalid('{"title":"Types","pages":1e400,"read":0}', "Invalid book");
assertInvalid('{"title":"Types","pages":10,"read":1e400}', "Invalid book");
console.log(display(parseBook('{"title":" Types ","pages":120,"read":45}')));
console.log(display(parseBook('{"title":"Types","pages":10,"read":11}')));
console.log(display(parseBook("{")));

Enable JavaScript to edit and type-check this example.

Types: 75 pages left
Invalid book
Invalid JSON

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Expand your runtime assertions to cover every invalid-input case in the brief. Temporarily remove read <= pages validation and verify a test fails. Explain why the Book type alone cannot express your whole domain rule. Tomorrow, reproduce the parser structure without copying it.

Was this article helpful?

Your response is saved on this device.