From 53ba07a7ba8223c4346c1c1019ad236eedac79b1 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:26:49 -0400 Subject: [PATCH 01/19] kitchener_utilities.ts: parse statement date --- finances/documents.ts | 2 + finances/documents/kitchener_utilities.ts | 72 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 finances/documents/kitchener_utilities.ts diff --git a/finances/documents.ts b/finances/documents.ts index 8625d7b..c7b5937 100644 --- a/finances/documents.ts +++ b/finances/documents.ts @@ -1,5 +1,6 @@ import { rogersBill } from "./documents/rogers.ts"; import { publicMobileStatement } from "./documents/public_mobile.ts"; +import { kitchenerUtilitiesBill } from "./documents/kitchener_utilities.ts"; import { questradeMarginStatement, questradeRESPStatement, @@ -12,6 +13,7 @@ export const allDocuments = Object.freeze([ questradeMarginStatement, questradeRESPStatement, questradeRRSPStatement, + kitchenerUtilitiesBill, ]); export type Documents = Exclude<(typeof allDocuments)[number], undefined>; diff --git a/finances/documents/kitchener_utilities.ts b/finances/documents/kitchener_utilities.ts new file mode 100644 index 0000000..e1e6bc4 --- /dev/null +++ b/finances/documents/kitchener_utilities.ts @@ -0,0 +1,72 @@ +import { + isDocumentParseError, + type Document, + type DocumentParseError, +} from "../document.ts"; +import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; + +export interface ParsedKitchenerUtilitiesBill { + type: "KitchenerUtilitiesBill"; + statementDate: string; + amountDue: string; +} + +class KitchenerUtilitiesBill implements Document< + ParsedKitchenerUtilitiesBill, + "KitchenerUtilitiesBill" +> { + readonly type = "KitchenerUtilitiesBill" as const; + + identify(lines: readonly string[]): boolean { + return lines.some((line) => line.includes("UTILITIES@KITCHENER.CA")); + } + + calculateFileName(pdf: Readonly): string { + const { statementDate, amountDue } = pdf; + return `${statementDate} Kitchener Utilities Bill ${amountDue}.pdf`; + } + + parse( + lines: readonly string[], + ): ParsedKitchenerUtilitiesBill | DocumentParseError { + const statementDate = this.#parseStatementDate(lines); + if (isDocumentParseError(statementDate)) { + return statementDate; + } + + const amountDue = "$12.34"; + + return { type: "KitchenerUtilitiesBill", statementDate, amountDue }; + } + + #parseStatementDate(lines: readonly string[]): string | DocumentParseError { + const regex = /Statement Date:\s*(\w+\s+\d+\s+\d+)/i; + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: "Statement Date line not found", + }; + } + + const dateStr = line.match(regex)?.[1]; + if (!dateStr) { + throw new Error("internal error vngd3pn5ry: regex should have matched"); + } + + const date = parseDateToYYYYMMDD("MMM D YYYY", dateStr); + if (isParseDateError(date)) { + const { message } = date; + return { + type: "DocumentParseError", + message: `unable to parse statement date: ${dateStr} (${message})`, + }; + } + + return date; + } +} + +export const kitchenerUtilitiesBill: Readonly< + Document +> = Object.freeze(new KitchenerUtilitiesBill()); From 3db7577f2b85ff41ee6a6aecf670ceaa0bf93c50 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:27:28 -0400 Subject: [PATCH 02/19] gasbill.ts: deleted --- finances/gasbill.ts | 173 -------------------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 finances/gasbill.ts diff --git a/finances/gasbill.ts b/finances/gasbill.ts deleted file mode 100644 index 30083a7..0000000 --- a/finances/gasbill.ts +++ /dev/null @@ -1,173 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { Command } from "commander"; -import { PDFParse } from "pdf-parse"; - -const program = new Command(); - -async function parsePdfAndPrint(filePath: string): Promise { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const dataBuffer = fs.readFileSync(filePath); - const parser = new PDFParse({ data: dataBuffer }); - const result = await parser.getText(); - console.log(result.text); - await parser.destroy(); -} - -const MONTH_MAP: Record = { - jan: "01", - feb: "02", - mar: "03", - apr: "04", - may: "05", - jun: "06", - jul: "07", - aug: "08", - sep: "09", - oct: "10", - nov: "11", - dec: "12", -}; - -function formatDate( - monthStrRaw: string, - dayStr: string, - yearStr: string, -): string { - const day = dayStr.padStart(2, "0"); - const monthKey = monthStrRaw.toLowerCase().substring(0, 3); - const month = MONTH_MAP[monthKey]; - if (!month) { - throw new Error(`Invalid month name in date: "${monthStrRaw}"`); - } - return `${yearStr}-${month}-${day}`; -} - -interface ParsedGasBill { - preAuthorizedWithdrawal: string; - issueDate: string; -} - -async function extractInfoFromPdf(filePath: string): Promise { - const dataBuffer = fs.readFileSync(filePath); - const parser = new PDFParse({ data: dataBuffer }); - const result = await parser.getText(); - await parser.destroy(); - - const withdrawalMatch = result.text.match( - /Pre-authorized Withdrawal:\s*([\d.]+)/i, - ); - const issueDateMatch = result.text.match( - /Issue Date:\s*([A-Za-z]+)\s*(\d{1,2})\s*(\d{4})/i, - ); - - if (withdrawalMatch && issueDateMatch) { - const formattedDate = formatDate( - issueDateMatch[1]!, - issueDateMatch[2]!, - issueDateMatch[3]!, - ); - return { - preAuthorizedWithdrawal: `$${parseFloat(withdrawalMatch[1]!).toFixed(2)}`, - issueDate: formattedDate, - }; - } else { - const missingFields: string[] = []; - if (!withdrawalMatch) missingFields.push("Pre-authorized Withdrawal"); - if (!issueDateMatch) missingFields.push("Issue Date"); - throw new Error( - `Failed to parse PDF. Missing fields: ${missingFields.join(", ")}`, - ); - } -} - -async function parsePdfAndExtractInfo(filePath: string): Promise { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - - console.log(`Pre-authorized Withdrawal: ${parsed.preAuthorizedWithdrawal}`); - console.log(`Issue Date: ${parsed.issueDate}`); -} - -function getNewFilename(parsed: ParsedGasBill): string { - return `${parsed.issueDate} Kitchener Utilities Bill ${parsed.preAuthorizedWithdrawal}`; -} - -async function generateFilenames(filePaths: string[]): Promise { - for (const filePath of filePaths) { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - const filename = getNewFilename(parsed); - console.log(filename); - } -} - -async function renameFiles(filePaths: string[]): Promise { - for (const filePath of filePaths) { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - const baseNewName = getNewFilename(parsed); - - const dir = path.dirname(filePath); - const ext = path.extname(filePath); - const newFilename = `${baseNewName}${ext}`; - const newPath = path.join(dir, newFilename); - - console.log(`Renaming ${filePath} to ${newFilename}`); - fs.renameSync(filePath, newPath); - } -} - -program - .name("gasbill") - .description( - "Reads a Kitchener Utilities gas bill PDF and extracts " + - "information from it relevant for financial record keeping", - ) - .version("1.0.0"); - -program - .command("print") - .description("load a PDF and print its text to stdout") - .argument("", "path to the PDF file") - .action(parsePdfAndPrint); - -program - .command("parse") - .description( - "extract financial record information from the PDF file and print it", - ) - .argument("", "path to the PDF file") - .action(parsePdfAndExtractInfo); - -program - .command("filename") - .description("generate standardized filenames from the PDF data") - .argument("", "paths to the PDF files") - .action(generateFilenames); - -program - .command("rename") - .description( - "rename the PDF files to standardized filenames based on their content", - ) - .argument("", "paths to the PDF files") - .action(renameFiles); - -program.parse(process.argv); From 2110a29e279d038a29018953f987da26b79e4e5d Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:32:37 -0400 Subject: [PATCH 03/19] kitchener_utilities.ts: done --- finances/documents/kitchener_utilities.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/finances/documents/kitchener_utilities.ts b/finances/documents/kitchener_utilities.ts index e1e6bc4..1331dc4 100644 --- a/finances/documents/kitchener_utilities.ts +++ b/finances/documents/kitchener_utilities.ts @@ -34,7 +34,10 @@ class KitchenerUtilitiesBill implements Document< return statementDate; } - const amountDue = "$12.34"; + const amountDue = this.#parseAmountDue(lines); + if (isDocumentParseError(amountDue)) { + return amountDue; + } return { type: "KitchenerUtilitiesBill", statementDate, amountDue }; } @@ -65,6 +68,24 @@ class KitchenerUtilitiesBill implements Document< return date; } + + #parseAmountDue(lines: readonly string[]): string | DocumentParseError { + const regex = /Pre-authorized Withdrawal:\s*(\d+\.\d+)/i; + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: "Pre-authorized Withdrawal line not found", + }; + } + + const amountDueStr = line.match(regex)?.[1]; + if (!amountDueStr) { + throw new Error("internal error ms3atxxre5: regex should have matched"); + } + + return `$${amountDueStr}`; + } } export const kitchenerUtilitiesBill: Readonly< From eb5ab35f693dd32f8f27b7ee49f5e2b6749f2283 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:42:27 -0400 Subject: [PATCH 04/19] morgan_stanley.ts: added with identify() and calculateFileName() implemented --- finances/documents.ts | 6 +- finances/documents/morgan_stanley.ts | 109 +++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 finances/documents/morgan_stanley.ts diff --git a/finances/documents.ts b/finances/documents.ts index c7b5937..8fc0b18 100644 --- a/finances/documents.ts +++ b/finances/documents.ts @@ -1,6 +1,7 @@ import { rogersBill } from "./documents/rogers.ts"; import { publicMobileStatement } from "./documents/public_mobile.ts"; import { kitchenerUtilitiesBill } from "./documents/kitchener_utilities.ts"; +import { morganStanleyRelease } from "./documents/morgan_stanley.ts"; import { questradeMarginStatement, questradeRESPStatement, @@ -8,12 +9,13 @@ import { } from "./documents/questrade.ts"; export const allDocuments = Object.freeze([ + kitchenerUtilitiesBill, + morganStanleyRelease, publicMobileStatement, - rogersBill, questradeMarginStatement, questradeRESPStatement, questradeRRSPStatement, - kitchenerUtilitiesBill, + rogersBill, ]); export type Documents = Exclude<(typeof allDocuments)[number], undefined>; diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts new file mode 100644 index 0000000..d27d38d --- /dev/null +++ b/finances/documents/morgan_stanley.ts @@ -0,0 +1,109 @@ +import { + isDocumentParseError, + type Document, + type DocumentParseError, +} from "../document.ts"; +import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; + +export interface ParsedMorganStanleyRelease { + type: "MorganStanleyRelease"; + awardId: string; + settlementDate: string; + vestedValue: string; + saleAmount: string; + sharesSold: string; + salePrice: string; +} + +class MorganStanleyRelease implements Document< + ParsedMorganStanleyRelease, + "MorganStanleyRelease" +> { + readonly type = "MorganStanleyRelease" as const; + + identify(lines: readonly string[]): boolean { + return lines.some((line) => line.toLowerCase() === "release confirmation"); + } + + calculateFileName(pdf: Readonly): string { + const { + awardId, + settlementDate, + vestedValue, + saleAmount, + sharesSold, + salePrice, + } = pdf; + return ( + `${settlementDate} Morgan Stanley Release Confirmation ` + + `${awardId} ${sharesSold} shares vested for ` + + `${vestedValue} sold for ${saleAmount} ` + + `(${salePrice} per share)` + ); + } + + parse( + lines: readonly string[], + ): ParsedMorganStanleyRelease | DocumentParseError { + const statementDate = this.#parseStatementDate(lines); + if (isDocumentParseError(statementDate)) { + return statementDate; + } + + const amountDue = this.#parseAmountDue(lines); + if (isDocumentParseError(amountDue)) { + return amountDue; + } + + return { type: "MorganStanleyRelease", statementDate, amountDue }; + } + + #parseStatementDate(lines: readonly string[]): string | DocumentParseError { + const regex = /Statement Date:\s*(\w+\s+\d+\s+\d+)/i; + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: "Statement Date line not found", + }; + } + + const dateStr = line.match(regex)?.[1]; + if (!dateStr) { + throw new Error("internal error vngd3pn5ry: regex should have matched"); + } + + const date = parseDateToYYYYMMDD("MMM D YYYY", dateStr); + if (isParseDateError(date)) { + const { message } = date; + return { + type: "DocumentParseError", + message: `unable to parse statement date: ${dateStr} (${message})`, + }; + } + + return date; + } + + #parseAmountDue(lines: readonly string[]): string | DocumentParseError { + const regex = /Pre-authorized Withdrawal:\s*(\d+\.\d+)/i; + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: "Pre-authorized Withdrawal line not found", + }; + } + + const amountDueStr = line.match(regex)?.[1]; + if (!amountDueStr) { + throw new Error("internal error ms3atxxre5: regex should have matched"); + } + + return `$${amountDueStr}`; + } +} + +export const morganStanleyRelease: Readonly< + Document +> = Object.freeze(new MorganStanleyRelease()); From 54100a57fa35ca1055a44a2aaa3b8b5665a8ca29 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:47:39 -0400 Subject: [PATCH 05/19] morgan_stanley.ts: awardId parse --- finances/documents/morgan_stanley.ts | 66 ++++++++++------------------ 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index d27d38d..c863415 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -45,62 +45,44 @@ class MorganStanleyRelease implements Document< parse( lines: readonly string[], ): ParsedMorganStanleyRelease | DocumentParseError { - const statementDate = this.#parseStatementDate(lines); - if (isDocumentParseError(statementDate)) { - return statementDate; + const awardId = this.#parseAwardId(lines); + if (isDocumentParseError(awardId)) { + return awardId; } - const amountDue = this.#parseAmountDue(lines); - if (isDocumentParseError(amountDue)) { - return amountDue; - } - - return { type: "MorganStanleyRelease", statementDate, amountDue }; - } - - #parseStatementDate(lines: readonly string[]): string | DocumentParseError { - const regex = /Statement Date:\s*(\w+\s+\d+\s+\d+)/i; - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: "Statement Date line not found", - }; - } - - const dateStr = line.match(regex)?.[1]; - if (!dateStr) { - throw new Error("internal error vngd3pn5ry: regex should have matched"); - } + const settlementDate = "zzyzx"; + const vestedValue = "zzyzx"; + const saleAmount = "zzyzx"; + const sharesSold = "zzyzx"; + const salePrice = "zzyzx"; - const date = parseDateToYYYYMMDD("MMM D YYYY", dateStr); - if (isParseDateError(date)) { - const { message } = date; - return { - type: "DocumentParseError", - message: `unable to parse statement date: ${dateStr} (${message})`, - }; - } - - return date; + return { + type: "MorganStanleyRelease", + awardId, + settlementDate, + vestedValue, + saleAmount, + sharesSold, + salePrice, + }; } - #parseAmountDue(lines: readonly string[]): string | DocumentParseError { - const regex = /Pre-authorized Withdrawal:\s*(\d+\.\d+)/i; + #parseAwardId(lines: readonly string[]): string | DocumentParseError { + const regex = /^Award ID:\s+([\w\d]+)/i; const line = lines.find((line) => regex.test(line)); if (!line) { return { type: "DocumentParseError", - message: "Pre-authorized Withdrawal line not found", + message: "Award ID line not found", }; } - const amountDueStr = line.match(regex)?.[1]; - if (!amountDueStr) { - throw new Error("internal error ms3atxxre5: regex should have matched"); + const awardId = line.match(regex)?.[1]; + if (!awardId) { + throw new Error("internal error stdp7xw6rb: regex should have matched"); } - return `$${amountDueStr}`; + return awardId; } } From bd33ec0578c035d6522143deb7025fcec8994ab5 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 12:52:56 -0400 Subject: [PATCH 06/19] morgan_stanley.ts: settlement date parse --- finances/documents/morgan_stanley.ts | 35 ++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index c863415..94cb147 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -50,7 +50,11 @@ class MorganStanleyRelease implements Document< return awardId; } - const settlementDate = "zzyzx"; + const settlementDate = this.#parseSettlementDate(lines); + if (isDocumentParseError(settlementDate)) { + return settlementDate; + } + const vestedValue = "zzyzx"; const saleAmount = "zzyzx"; const sharesSold = "zzyzx"; @@ -68,7 +72,7 @@ class MorganStanleyRelease implements Document< } #parseAwardId(lines: readonly string[]): string | DocumentParseError { - const regex = /^Award ID:\s+([\w\d]+)/i; + const regex = /^Award ID:\s+([\w\d]+)$/i; const line = lines.find((line) => regex.test(line)); if (!line) { return { @@ -84,6 +88,33 @@ class MorganStanleyRelease implements Document< return awardId; } + + #parseSettlementDate(lines: readonly string[]): string | DocumentParseError { + const regex = /^Settlement Date:\s*(\d+-\w+-\d+)$/i; + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: "Settlement Date line not found", + }; + } + + const dateStr = line.match(regex)?.[1]; + if (!dateStr) { + throw new Error("internal error tccydgchk6: regex should have matched"); + } + + const date = parseDateToYYYYMMDD("DD-MMM-YYYY", dateStr); + if (isParseDateError(date)) { + const { message } = date; + return { + type: "DocumentParseError", + message: `unable to parse settlement date: ${dateStr} (${message})`, + }; + } + + return date; + } } export const morganStanleyRelease: Readonly< From eb47ec238374dbfe4c9f505c84a1aca14cedd8fd Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:14:04 -0400 Subject: [PATCH 07/19] document_utils.ts: created with repeated parsing logic --- finances/document_utils.ts | 49 +++++++++++++++++++++++ finances/documents/morgan_stanley.ts | 58 +++++----------------------- finances/error.ts | 4 ++ 3 files changed, 63 insertions(+), 48 deletions(-) create mode 100644 finances/document_utils.ts diff --git a/finances/document_utils.ts b/finances/document_utils.ts new file mode 100644 index 0000000..12fe908 --- /dev/null +++ b/finances/document_utils.ts @@ -0,0 +1,49 @@ +import { isDocumentParseError, type DocumentParseError } from "./document.ts"; +import { parseDateToYYYYMMDD, isParseDateError } from "./date.ts"; + +export function stringFromLines( + lines: readonly string[], + regex: RegExp, +): string | DocumentParseError { + const line = lines.find((line) => regex.test(line)); + if (!line) { + return { + type: "DocumentParseError", + message: `line not found matching regex: ${regex.source}`, + }; + } + + const matchingString = line.match(regex)?.[1]; + if (!matchingString) { + throw new Error( + `internal error stdp7xw6rb: regex should have matched line; ` + + `regex=${regex.source}, line=${line}`, + ); + } + + return matchingString; +} + +export function yyyymmddDateFromLines( + lines: readonly string[], + regex: RegExp, + dateFormat: string, +): string | DocumentParseError { + const dateStr = stringFromLines(lines, regex); + if (isDocumentParseError(dateStr)) { + return dateStr; + } + + const date = parseDateToYYYYMMDD(dateFormat, dateStr); + if (isParseDateError(date)) { + const { message } = date; + return { + type: "DocumentParseError", + message: + `unable to parse date "${dateStr}" ` + + `using format "${dateFormat}": ${message}`, + }; + } + + return date; +} diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 94cb147..0012e35 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -3,7 +3,8 @@ import { type Document, type DocumentParseError, } from "../document.ts"; -import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; +import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; +import { prefixMessage } from "../error.ts"; export interface ParsedMorganStanleyRelease { type: "MorganStanleyRelease"; @@ -45,13 +46,19 @@ class MorganStanleyRelease implements Document< parse( lines: readonly string[], ): ParsedMorganStanleyRelease | DocumentParseError { - const awardId = this.#parseAwardId(lines); + const awardId = stringFromLines(lines, /^Award ID:\s+([\w\d]+)$/i); if (isDocumentParseError(awardId)) { + prefixMessage(awardId, "Award ID not found: "); return awardId; } - const settlementDate = this.#parseSettlementDate(lines); + const settlementDate = yyyymmddDateFromLines( + lines, + /^Settlement Date:\s*(\d+-\w+-\d+)$/i, + "DD-MMM-YYYY", + ); if (isDocumentParseError(settlementDate)) { + prefixMessage(settlementDate, "Settlement Date not found: "); return settlementDate; } @@ -70,51 +77,6 @@ class MorganStanleyRelease implements Document< salePrice, }; } - - #parseAwardId(lines: readonly string[]): string | DocumentParseError { - const regex = /^Award ID:\s+([\w\d]+)$/i; - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: "Award ID line not found", - }; - } - - const awardId = line.match(regex)?.[1]; - if (!awardId) { - throw new Error("internal error stdp7xw6rb: regex should have matched"); - } - - return awardId; - } - - #parseSettlementDate(lines: readonly string[]): string | DocumentParseError { - const regex = /^Settlement Date:\s*(\d+-\w+-\d+)$/i; - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: "Settlement Date line not found", - }; - } - - const dateStr = line.match(regex)?.[1]; - if (!dateStr) { - throw new Error("internal error tccydgchk6: regex should have matched"); - } - - const date = parseDateToYYYYMMDD("DD-MMM-YYYY", dateStr); - if (isParseDateError(date)) { - const { message } = date; - return { - type: "DocumentParseError", - message: `unable to parse settlement date: ${dateStr} (${message})`, - }; - } - - return date; - } } export const morganStanleyRelease: Readonly< diff --git a/finances/error.ts b/finances/error.ts index cee984f..0ce658d 100644 --- a/finances/error.ts +++ b/finances/error.ts @@ -17,3 +17,7 @@ function propertyFromUnknown(obj: unknown, propertyName: string): unknown { ? (obj as Record)[propertyName] : undefined; } + +export function prefixMessage(obj: { message: string }, prefix: string) { + obj.message = `${prefix}${obj.message}`; +} From 7cd4b87c838bd43dae82062a2a7399e7ac6de09f Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:17:22 -0400 Subject: [PATCH 08/19] morgan_stanley.ts: vested value --- finances/documents/morgan_stanley.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 0012e35..a112e51 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -62,7 +62,15 @@ class MorganStanleyRelease implements Document< return settlementDate; } - const vestedValue = "zzyzx"; + const vestedValue = stringFromLines( + lines, + /^Total Gain.*:\s*(\$[\d,.]+)$/i, + ); + if (isDocumentParseError(vestedValue)) { + prefixMessage(vestedValue, "Vested Value not found: "); + return vestedValue; + } + const saleAmount = "zzyzx"; const sharesSold = "zzyzx"; const salePrice = "zzyzx"; From 4837c31f07534f5340b06333f229e47c4d8f8ecf Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:19:50 -0400 Subject: [PATCH 09/19] morgan_stanley.ts: sale amount --- finances/documents/morgan_stanley.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index a112e51..870e885 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -71,7 +71,15 @@ class MorganStanleyRelease implements Document< return vestedValue; } - const saleAmount = "zzyzx"; + const saleAmount = stringFromLines( + lines, + /^Sale PricexQuantity Sold:\s*\((\$[\d,.]+)\)$/i, + ); + if (isDocumentParseError(saleAmount)) { + prefixMessage(saleAmount, "Sale Amount not found: "); + return saleAmount; + } + const sharesSold = "zzyzx"; const salePrice = "zzyzx"; From 1a0f29d7f9d63363a04648bcc4c4e680e3cd1fe8 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:22:55 -0400 Subject: [PATCH 10/19] morgan_stanley.ts: shares sold --- finances/documents/morgan_stanley.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 870e885..66a67b7 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -80,7 +80,15 @@ class MorganStanleyRelease implements Document< return saleAmount; } - const sharesSold = "zzyzx"; + const sharesSold = stringFromLines( + lines, + /^Quantity Sold:\s*\(([\d.]+)\)$/i, + ); + if (isDocumentParseError(sharesSold)) { + prefixMessage(sharesSold, "Shares Sold not found: "); + return sharesSold; + } + const salePrice = "zzyzx"; return { From 33171f595c9c36e4ebf1ef269143ca9626fa7cb7 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:26:34 -0400 Subject: [PATCH 11/19] morgan_stanley.ts: sale price --- finances/documents/morgan_stanley.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 66a67b7..adcee9c 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -82,14 +82,21 @@ class MorganStanleyRelease implements Document< const sharesSold = stringFromLines( lines, - /^Quantity Sold:\s*\(([\d.]+)\)$/i, + /^Quantity Sold:\s*\((\d+\.\d{3})0*\)$/i, ); if (isDocumentParseError(sharesSold)) { prefixMessage(sharesSold, "Shares Sold not found: "); return sharesSold; } - const salePrice = "zzyzx"; + const salePrice = stringFromLines( + lines, + /shares at (\$\d+\.\d{4})0* per share/i, + ); + if (isDocumentParseError(salePrice)) { + prefixMessage(salePrice, "Sale Price not found: "); + return salePrice; + } return { type: "MorganStanleyRelease", From f2e0a08929f6dac08f530fd1fad7f5b0cded0336 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:28:44 -0400 Subject: [PATCH 12/19] morgan_stanley.ts: forgot pdf suffix --- finances/documents/morgan_stanley.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index adcee9c..3228e50 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -39,7 +39,7 @@ class MorganStanleyRelease implements Document< `${settlementDate} Morgan Stanley Release Confirmation ` + `${awardId} ${sharesSold} shares vested for ` + `${vestedValue} sold for ${saleAmount} ` + - `(${salePrice} per share)` + `(${salePrice} per share).pdf` ); } From 8e44de159d27e3b3363901a44c71bc8912175fe1 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:29:03 -0400 Subject: [PATCH 13/19] mspdf.ts: deleted --- finances/mspdf.ts | 210 ---------------------------------------------- 1 file changed, 210 deletions(-) delete mode 100644 finances/mspdf.ts diff --git a/finances/mspdf.ts b/finances/mspdf.ts deleted file mode 100644 index 2b8910f..0000000 --- a/finances/mspdf.ts +++ /dev/null @@ -1,210 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { Command } from "commander"; -import { PDFParse } from "pdf-parse"; - -const program = new Command(); - -async function parsePdfAndPrint(filePath: string): Promise { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const dataBuffer = fs.readFileSync(filePath); - const parser = new PDFParse({ data: dataBuffer }); - const result = await parser.getText(); - console.log(result.text); - await parser.destroy(); -} - -const MONTH_MAP: Record = { - jan: "01", - feb: "02", - mar: "03", - apr: "04", - may: "05", - jun: "06", - jul: "07", - aug: "08", - sep: "09", - oct: "10", - nov: "11", - dec: "12", -}; - -function formatDate(dateStr: string): string { - const parts = dateStr.split("-"); - if (parts.length !== 3) { - throw new Error(`Invalid date format (expected DD-MMM-YYYY): "${dateStr}"`); - } - const [dayStr, monthStrRaw, yearStr] = parts as [string, string, string]; - const day = dayStr.padStart(2, "0"); - const monthStr = monthStrRaw.toLowerCase(); - const year = yearStr; - - const month = MONTH_MAP[monthStr]; - if (!month) { - throw new Error(`Invalid month name in date: "${dateStr}"`); - } - - return `${year}-${month}-${day}`; -} - -interface ParsedPdf { - awardId: string; - settlementDate: string; - vestedValue: string; - saleAmount: string; - sharesSold: string; - salePrice: string; -} - -async function extractInfoFromPdf(filePath: string): Promise { - const dataBuffer = fs.readFileSync(filePath); - const parser = new PDFParse({ data: dataBuffer }); - const result = await parser.getText(); - await parser.destroy(); - - const awardIdMatch = result.text.match(/Award ID:\s*([^\r\n]+)/i); - const settlementDateMatch = result.text.match( - /Settlement Date:\s*([^\r\n]+)/i, - ); - const vestedValueMatch = result.text.match( - /Total Gain \(FMV x Quantity Released\):\s*([^\r\n]+)/i, - ); - const saleAmountMatch = result.text.match( - /Sale PricexQuantity Sold:\s*\(?([^)\r\n]+)\)?/i, - ); - const sharesSoldMatch = result.text.match(/Quantity Sold:\s*\(?([\d.]+)\)?/i); - const salePriceMatch = result.text.match(/shares at \$([\d.]+) per share/i); - - if ( - awardIdMatch && - settlementDateMatch && - vestedValueMatch && - saleAmountMatch && - sharesSoldMatch && - salePriceMatch - ) { - const rawSettlementDate = settlementDateMatch[1]!.trim(); - const formattedSettlementDate = formatDate(rawSettlementDate); - const formattedSalePrice = parseFloat(salePriceMatch[1]!).toFixed(4); - - return { - awardId: awardIdMatch[1]!.trim(), - settlementDate: formattedSettlementDate, - vestedValue: vestedValueMatch[1]!.trim(), - saleAmount: saleAmountMatch[1]!.trim(), - sharesSold: sharesSoldMatch[1]!.trim(), - salePrice: `$${formattedSalePrice}`, - }; - } else { - const missingFields: string[] = []; - if (!awardIdMatch) missingFields.push("Award ID"); - if (!settlementDateMatch) missingFields.push("Settlement Date"); - if (!vestedValueMatch) missingFields.push("Vested Value"); - if (!saleAmountMatch) missingFields.push("Sale Amount"); - if (!sharesSoldMatch) missingFields.push("Shares Sold"); - if (!salePriceMatch) missingFields.push("Sale Price"); - throw new Error( - `Failed to parse PDF. Missing fields: ${missingFields.join(", ")}`, - ); - } -} - -async function parsePdfAndExtractInfo(filePath: string): Promise { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - - console.log(`Award ID: ${parsed.awardId}`); - console.log(`Settlement Date: ${parsed.settlementDate}`); - console.log(`Vested Value: ${parsed.vestedValue}`); - console.log(`Sale Amount: ${parsed.saleAmount}`); - console.log(`Shares Sold: ${parsed.sharesSold}`); - console.log(`Sale Price: ${parsed.salePrice}`); -} - -function getNewFilename(parsed: ParsedPdf): string { - return ( - `${parsed.settlementDate} Morgan Stanley Release Confirmation ` + - `${parsed.awardId} ${parsed.sharesSold} shares vested for ` + - `${parsed.vestedValue} sold for ${parsed.saleAmount} ` + - `(${parsed.salePrice} per share)` - ); -} - -async function generateFilenames(filePaths: string[]): Promise { - for (const filePath of filePaths) { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - const filename = getNewFilename(parsed); - console.log(filename); - } -} - -async function renameFiles(filePaths: string[]): Promise { - for (const filePath of filePaths) { - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found at path "${filePath}"`); - process.exit(1); - } - - const parsed = await extractInfoFromPdf(filePath); - const baseNewName = getNewFilename(parsed); - - const dir = path.dirname(filePath); - const ext = path.extname(filePath); - const newFilename = `${baseNewName}${ext}`; - const newPath = path.join(dir, newFilename); - - console.log(`Renaming ${filePath} to ${newFilename}`); - fs.renameSync(filePath, newPath); - } -} - -program - .name("mspdf") - .description( - 'Reads a "Release Confirmation" PDF from Morgan Stanley and extracts ' + - "information from it relevant for income tax reporting", - ) - .version("1.0.0"); - -program - .command("print") - .description("load a PDF and print its text to stdout") - .argument("", "path to the PDF file") - .action(parsePdfAndPrint); - -program - .command("parse") - .description( - "extract tax reporting information from the PDF file and print it", - ) - .argument("", "path to the PDF file") - .action(parsePdfAndExtractInfo); - -program - .command("filename") - .description("generate standardized filenames from the PDF data") - .argument("", "paths to the PDF files") - .action(generateFilenames); - -program - .command("rename") - .description( - "rename the PDF files to standardized filenames based on their content", - ) - .argument("", "paths to the PDF files") - .action(renameFiles); - -program.parse(process.argv); From 2708227674630af28af8bc4dc9d855ab997c3768 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:43:32 -0400 Subject: [PATCH 14/19] kitchener_utilities.ts: updated to use helper functions --- finances/document_utils.ts | 21 +++++++- finances/documents/kitchener_utilities.ts | 62 +++++------------------ 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/finances/document_utils.ts b/finances/document_utils.ts index 12fe908..541f3c4 100644 --- a/finances/document_utils.ts +++ b/finances/document_utils.ts @@ -1,9 +1,14 @@ import { isDocumentParseError, type DocumentParseError } from "./document.ts"; import { parseDateToYYYYMMDD, isParseDateError } from "./date.ts"; +interface StringFromLinesOptions { + resultPrefix: string; +} + export function stringFromLines( lines: readonly string[], regex: RegExp, + options?: Partial, ): string | DocumentParseError { const line = lines.find((line) => regex.test(line)); if (!line) { @@ -21,7 +26,21 @@ export function stringFromLines( ); } - return matchingString; + return matchingStringWithStringFromLinesOptionsApplied( + matchingString, + options, + ); +} + +function matchingStringWithStringFromLinesOptionsApplied( + matchingString: string, + options: Partial | undefined, +): string { + const resultPrefix = options?.resultPrefix; + if (typeof resultPrefix === "undefined") { + return matchingString; + } + return resultPrefix + matchingString; } export function yyyymmddDateFromLines( diff --git a/finances/documents/kitchener_utilities.ts b/finances/documents/kitchener_utilities.ts index 1331dc4..3e4754f 100644 --- a/finances/documents/kitchener_utilities.ts +++ b/finances/documents/kitchener_utilities.ts @@ -3,7 +3,8 @@ import { type Document, type DocumentParseError, } from "../document.ts"; -import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; +import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; +import { prefixMessage } from "../error.ts"; export interface ParsedKitchenerUtilitiesBill { type: "KitchenerUtilitiesBill"; @@ -29,63 +30,28 @@ class KitchenerUtilitiesBill implements Document< parse( lines: readonly string[], ): ParsedKitchenerUtilitiesBill | DocumentParseError { - const statementDate = this.#parseStatementDate(lines); + const statementDate = yyyymmddDateFromLines( + lines, + /^Statement Date:\s*(\w+\s+\d+\s+\d+)\s+/i, + "MMM D YYYY", + ); if (isDocumentParseError(statementDate)) { + prefixMessage(statementDate, "Statement Date not found: "); return statementDate; } - const amountDue = this.#parseAmountDue(lines); + const amountDue = stringFromLines( + lines, + /^Pre-authorized Withdrawal:\s*(\d+\.\d+)$/i, + { resultPrefix: "$" }, + ); if (isDocumentParseError(amountDue)) { + prefixMessage(amountDue, "Amount Due not found: "); return amountDue; } return { type: "KitchenerUtilitiesBill", statementDate, amountDue }; } - - #parseStatementDate(lines: readonly string[]): string | DocumentParseError { - const regex = /Statement Date:\s*(\w+\s+\d+\s+\d+)/i; - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: "Statement Date line not found", - }; - } - - const dateStr = line.match(regex)?.[1]; - if (!dateStr) { - throw new Error("internal error vngd3pn5ry: regex should have matched"); - } - - const date = parseDateToYYYYMMDD("MMM D YYYY", dateStr); - if (isParseDateError(date)) { - const { message } = date; - return { - type: "DocumentParseError", - message: `unable to parse statement date: ${dateStr} (${message})`, - }; - } - - return date; - } - - #parseAmountDue(lines: readonly string[]): string | DocumentParseError { - const regex = /Pre-authorized Withdrawal:\s*(\d+\.\d+)/i; - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: "Pre-authorized Withdrawal line not found", - }; - } - - const amountDueStr = line.match(regex)?.[1]; - if (!amountDueStr) { - throw new Error("internal error ms3atxxre5: regex should have matched"); - } - - return `$${amountDueStr}`; - } } export const kitchenerUtilitiesBill: Readonly< From 32297823015f33711d3316288bd0a66f3283f6ef Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:51:51 -0400 Subject: [PATCH 15/19] finances.yml: allow triggering by workflow dispatch --- .github/workflows/finances.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/finances.yml b/.github/workflows/finances.yml index 7ee86c5..f0e02d3 100644 --- a/.github/workflows/finances.yml +++ b/.github/workflows/finances.yml @@ -1,6 +1,7 @@ name: Finances CI on: + workflow_dispatch: push: branches: [main] paths: From 94c401829a25e607b1d2006c9ce5a86fe94bd8bc Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 13:55:13 -0400 Subject: [PATCH 16/19] finances.yml: rename workflow to "finances" --- .github/workflows/finances.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/finances.yml b/.github/workflows/finances.yml index f0e02d3..aeef16d 100644 --- a/.github/workflows/finances.yml +++ b/.github/workflows/finances.yml @@ -1,4 +1,4 @@ -name: Finances CI +name: finances on: workflow_dispatch: From 4988c418d53067bb16427c47009ccddb00610331 Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 14:01:29 -0400 Subject: [PATCH 17/19] cleanup --- finances/documents/kitchener_utilities.ts | 3 --- finances/documents/morgan_stanley.ts | 7 ------- finances/error.ts | 4 ---- 3 files changed, 14 deletions(-) diff --git a/finances/documents/kitchener_utilities.ts b/finances/documents/kitchener_utilities.ts index 3e4754f..179453e 100644 --- a/finances/documents/kitchener_utilities.ts +++ b/finances/documents/kitchener_utilities.ts @@ -4,7 +4,6 @@ import { type DocumentParseError, } from "../document.ts"; import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; -import { prefixMessage } from "../error.ts"; export interface ParsedKitchenerUtilitiesBill { type: "KitchenerUtilitiesBill"; @@ -36,7 +35,6 @@ class KitchenerUtilitiesBill implements Document< "MMM D YYYY", ); if (isDocumentParseError(statementDate)) { - prefixMessage(statementDate, "Statement Date not found: "); return statementDate; } @@ -46,7 +44,6 @@ class KitchenerUtilitiesBill implements Document< { resultPrefix: "$" }, ); if (isDocumentParseError(amountDue)) { - prefixMessage(amountDue, "Amount Due not found: "); return amountDue; } diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 3228e50..70425c8 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -4,7 +4,6 @@ import { type DocumentParseError, } from "../document.ts"; import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; -import { prefixMessage } from "../error.ts"; export interface ParsedMorganStanleyRelease { type: "MorganStanleyRelease"; @@ -48,7 +47,6 @@ class MorganStanleyRelease implements Document< ): ParsedMorganStanleyRelease | DocumentParseError { const awardId = stringFromLines(lines, /^Award ID:\s+([\w\d]+)$/i); if (isDocumentParseError(awardId)) { - prefixMessage(awardId, "Award ID not found: "); return awardId; } @@ -58,7 +56,6 @@ class MorganStanleyRelease implements Document< "DD-MMM-YYYY", ); if (isDocumentParseError(settlementDate)) { - prefixMessage(settlementDate, "Settlement Date not found: "); return settlementDate; } @@ -67,7 +64,6 @@ class MorganStanleyRelease implements Document< /^Total Gain.*:\s*(\$[\d,.]+)$/i, ); if (isDocumentParseError(vestedValue)) { - prefixMessage(vestedValue, "Vested Value not found: "); return vestedValue; } @@ -76,7 +72,6 @@ class MorganStanleyRelease implements Document< /^Sale PricexQuantity Sold:\s*\((\$[\d,.]+)\)$/i, ); if (isDocumentParseError(saleAmount)) { - prefixMessage(saleAmount, "Sale Amount not found: "); return saleAmount; } @@ -85,7 +80,6 @@ class MorganStanleyRelease implements Document< /^Quantity Sold:\s*\((\d+\.\d{3})0*\)$/i, ); if (isDocumentParseError(sharesSold)) { - prefixMessage(sharesSold, "Shares Sold not found: "); return sharesSold; } @@ -94,7 +88,6 @@ class MorganStanleyRelease implements Document< /shares at (\$\d+\.\d{4})0* per share/i, ); if (isDocumentParseError(salePrice)) { - prefixMessage(salePrice, "Sale Price not found: "); return salePrice; } diff --git a/finances/error.ts b/finances/error.ts index 0ce658d..cee984f 100644 --- a/finances/error.ts +++ b/finances/error.ts @@ -17,7 +17,3 @@ function propertyFromUnknown(obj: unknown, propertyName: string): unknown { ? (obj as Record)[propertyName] : undefined; } - -export function prefixMessage(obj: { message: string }, prefix: string) { - obj.message = `${prefix}${obj.message}`; -} From c142c6008e9cf1cbeeca27110c4863bcd882374a Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 14:07:18 -0400 Subject: [PATCH 18/19] document_utils.ts: rewrite stringFromLines() to be more efficient --- finances/document_utils.ts | 52 ++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/finances/document_utils.ts b/finances/document_utils.ts index 541f3c4..e923d6b 100644 --- a/finances/document_utils.ts +++ b/finances/document_utils.ts @@ -10,37 +10,33 @@ export function stringFromLines( regex: RegExp, options?: Partial, ): string | DocumentParseError { - const line = lines.find((line) => regex.test(line)); - if (!line) { - return { - type: "DocumentParseError", - message: `line not found matching regex: ${regex.source}`, - }; - } + for (const line of lines) { + const trimmedLine = line.trim(); + const match = trimmedLine.match(regex); + if (!match) { + continue; + } - const matchingString = line.match(regex)?.[1]; - if (!matchingString) { - throw new Error( - `internal error stdp7xw6rb: regex should have matched line; ` + - `regex=${regex.source}, line=${line}`, - ); - } - - return matchingStringWithStringFromLinesOptionsApplied( - matchingString, - options, - ); -} + const matchingString = match[1]; + if (!matchingString) { + throw new Error( + `internal error stdp7xw6rb: regex should have matched line; ` + + `regex=${regex.source}, line=${trimmedLine}`, + ); + } -function matchingStringWithStringFromLinesOptionsApplied( - matchingString: string, - options: Partial | undefined, -): string { - const resultPrefix = options?.resultPrefix; - if (typeof resultPrefix === "undefined") { - return matchingString; + const resultPrefix = options?.resultPrefix; + if (typeof resultPrefix === "undefined") { + return matchingString; + } else { + return resultPrefix + matchingString; + } } - return resultPrefix + matchingString; + + return { + type: "DocumentParseError", + message: `line not found matching regex: ${regex.source}`, + }; } export function yyyymmddDateFromLines( From 438d5d07f1f1177c04dfac2a86a3b6bed51aee4f Mon Sep 17 00:00:00 2001 From: denver Date: Sun, 21 Jun 2026 14:11:27 -0400 Subject: [PATCH 19/19] document_utils.ts: improve robustness by not conflating undefined with empty string --- finances/document_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finances/document_utils.ts b/finances/document_utils.ts index e923d6b..9b6a3f7 100644 --- a/finances/document_utils.ts +++ b/finances/document_utils.ts @@ -18,7 +18,7 @@ export function stringFromLines( } const matchingString = match[1]; - if (!matchingString) { + if (typeof matchingString === "undefined") { throw new Error( `internal error stdp7xw6rb: regex should have matched line; ` + `regex=${regex.source}, line=${trimmedLine}`,