From 5b0abaa5f2bed4274648d6e153ca742eab0e404f Mon Sep 17 00:00:00 2001 From: denver Date: Mon, 22 Jun 2026 23:12:35 -0400 Subject: [PATCH 1/2] pdf_read.ts: return pdf as just a string --- finances/calculate_file_names.ts | 34 ++++++++++++++++---------------- finances/commands/parse.ts | 10 +++++----- finances/commands/print.ts | 8 ++++---- finances/document.ts | 9 ++------- finances/identify.ts | 7 ++----- finances/pdf_read.ts | 15 ++------------ 6 files changed, 32 insertions(+), 51 deletions(-) diff --git a/finances/calculate_file_names.ts b/finances/calculate_file_names.ts index 74bb9b7..faee005 100644 --- a/finances/calculate_file_names.ts +++ b/finances/calculate_file_names.ts @@ -28,7 +28,7 @@ export async function calculateFileNames( ): Promise | CalculateFileNamesError> { const infoByFileName = new Map< string, - Array<{ filePath: string; hash: string }> + Array<{ filePath: string; pdf: string }> >(); const fileNameByFilePath = new Map(); @@ -37,16 +37,16 @@ export async function calculateFileNames( continue; } - const readPdfResult = await readPdf(filePath); - if (isReadPdfError(readPdfResult)) { + const pdf = await readPdf(filePath); + if (isReadPdfError(pdf)) { return { type: "CalculateFileNamesError", - message: readPdfResult.message, + message: pdf.message, filePath, }; } - const document = identify(readPdfResult); + const document = identify(pdf); if (typeof document === "undefined") { return { type: "CalculateFileNamesError", @@ -62,7 +62,7 @@ export async function calculateFileNames( }; } - const parseResult = document.parse(readPdfResult); + const parseResult = document.parse(pdf); if (isDocumentParseError(parseResult)) { return { type: "CalculateFileNamesError", @@ -75,7 +75,7 @@ export async function calculateFileNames( const fileName = document.calculateFileName(parseResult as unknown as any); fileNameByFilePath.set(filePath, fileName); - const newFileNameInfo = { filePath, hash: readPdfResult.hash }; + const newFileNameInfo = { filePath, pdf }; const info = infoByFileName.get(fileName); if (info) { info.push(newFileNameInfo); @@ -85,22 +85,22 @@ export async function calculateFileNames( } for (const [fileName, infoList] of infoByFileName.entries()) { - const hashes = new Set(); + const pdfs = new Set(); for (const info of infoList) { - hashes.add(info.hash); + pdfs.add(info.pdf); } - if (hashes.size < 2) { + if (pdfs.size < 2) { continue; } - const numberByHash = new Map(); - for (const hash of hashes) { - numberByHash.set(hash, numberByHash.size + 1); + const numberByPdf = new Map(); + for (const pdf of pdfs) { + numberByPdf.set(pdf, numberByPdf.size + 1); } - const fileNameByHash = new Map(); - for (const [hash, fileNumber] of numberByHash) { + const fileNameByPdf = new Map(); + for (const [pdf, fileNumber] of numberByPdf) { const lastPeriodIndex = fileName.lastIndexOf("."); let numberedFileName: string; if (lastPeriodIndex < 0) { @@ -111,11 +111,11 @@ export async function calculateFileNames( ` ${fileNumber}` + fileName.substring(lastPeriodIndex); } - fileNameByHash.set(hash, numberedFileName); + fileNameByPdf.set(pdf, numberedFileName); } for (const info of infoList) { - const newFileName = fileNameByHash.get(info.hash); + const newFileName = fileNameByPdf.get(info.pdf); if (!newFileName) { throw new Error( `internal error kwteqzzx79: ` + diff --git a/finances/commands/parse.ts b/finances/commands/parse.ts index 5d95b5d..5d19a57 100644 --- a/finances/commands/parse.ts +++ b/finances/commands/parse.ts @@ -17,13 +17,13 @@ export async function parseCommand( return; } - const readPdfResult = await readPdf(filePath); - if (isReadPdfError(readPdfResult)) { - console.error(`ERROR: ${readPdfResult.message}: ${filePath}`); + const pdf = await readPdf(filePath); + if (isReadPdfError(pdf)) { + console.error(`ERROR: ${pdf.message}: ${filePath}`); process.exit(1); } - const document = identify(readPdfResult); + const document = identify(pdf); if (typeof document === "undefined") { console.error(`ERROR: unable to identify pdf contents: ${filePath}`); process.exit(1); @@ -34,7 +34,7 @@ export async function parseCommand( process.exit(1); } - const parseResult = document.parse(readPdfResult); + const parseResult = document.parse(pdf); if (isDocumentParseError(parseResult)) { const { message } = parseResult; console.error(`ERROR: ${message}: ${filePath}`); diff --git a/finances/commands/print.ts b/finances/commands/print.ts index 50a3796..da4d8f9 100644 --- a/finances/commands/print.ts +++ b/finances/commands/print.ts @@ -15,14 +15,14 @@ export async function printCommand( return; } - const readPdfResult = await readPdf(filePath); - if (isReadPdfError(readPdfResult)) { - console.error(`ERROR: ${readPdfResult.message}: ${filePath}`); + const pdf = await readPdf(filePath); + if (isReadPdfError(pdf)) { + console.error(`ERROR: ${pdf.message}: ${filePath}`); process.exit(1); } if (options?.v) { console.log(filePath); } - console.log(readPdfResult.text); + console.log(pdf); } diff --git a/finances/document.ts b/finances/document.ts index c7a9a7a..691e5b1 100644 --- a/finances/document.ts +++ b/finances/document.ts @@ -3,11 +3,6 @@ export interface DocumentParseError { message: string; } -export interface DocumentSource { - text: string; - lines: readonly string[]; -} - export function isDocumentParseError(e: unknown): e is DocumentParseError { return ( e !== null && @@ -29,9 +24,9 @@ export interface Document< > { readonly type: TypeName; - identify(source: Readonly): boolean; + identify(pdf: string): boolean; - parse(source: Readonly): ParsedDocumentT | DocumentParseError; + parse(pdf: string): ParsedDocumentT | DocumentParseError; calculateFileName(pdf: Readonly): string; } diff --git a/finances/identify.ts b/finances/identify.ts index 0da0509..4caa1e5 100644 --- a/finances/identify.ts +++ b/finances/identify.ts @@ -1,4 +1,3 @@ -import { type DocumentSource } from "./document.ts"; import { allDocuments, type Documents } from "./documents.ts"; export interface IdentifyError { @@ -17,11 +16,9 @@ export function isIdentifyError(e: unknown): e is IdentifyError { ); } -export function identify( - source: Readonly, -): Documents | IdentifyError | undefined { +export function identify(pdf: string): Documents | IdentifyError | undefined { const filteredDocuments = allDocuments.filter((document) => - document.identify(source), + document.identify(pdf), ); if (filteredDocuments.length > 1) { diff --git a/finances/pdf_read.ts b/finances/pdf_read.ts index 0a8d327..c0aa084 100644 --- a/finances/pdf_read.ts +++ b/finances/pdf_read.ts @@ -19,15 +19,9 @@ export function isReadPdfError(e: unknown): e is ReadPdfError { ); } -export interface ReadPdfResult { - text: string; - lines: string[]; - hash: string; -} - export async function readPdf( filePath: string, -): Promise { +): Promise { let fileContents: Buffer; try { fileContents = await fs.readFile(filePath); @@ -37,10 +31,9 @@ export async function readPdf( const parser = new PDFParse({ data: fileContents }); - let text: string; try { const textContents = await parser.getText(); - text = textContents.text; + return textContents.text; } catch (e: unknown) { const errorMessage = messageForError(e); const message = `parsing pdf file contents failed (${errorMessage})`; @@ -48,8 +41,4 @@ export async function readPdf( } finally { await parser.destroy(); } - - const lines = text.split("\n").map((line) => line.trim()); - const hash = Bun.CryptoHasher.hash("sha512-256", fileContents, "hex"); - return { text, lines, hash }; } From e40c9019befeb2dbe64a5e86ea261b2540f8faba Mon Sep 17 00:00:00 2001 From: denver Date: Tue, 23 Jun 2026 00:26:51 -0400 Subject: [PATCH 2/2] fix documents --- finances/document_utils.ts | 53 ++++++------ finances/documents/kitchener_utilities.ts | 29 +++---- finances/documents/morgan_stanley.ts | 50 +++++------- finances/documents/paystub.ts | 37 ++++----- finances/documents/public_mobile.ts | 57 +++++-------- finances/documents/questrade.ts | 98 +++++++---------------- finances/documents/rogers.ts | 67 +++++----------- 7 files changed, 135 insertions(+), 256 deletions(-) diff --git a/finances/document_utils.ts b/finances/document_utils.ts index 9b6a3f7..3e58e65 100644 --- a/finances/document_utils.ts +++ b/finances/document_utils.ts @@ -5,46 +5,41 @@ interface StringFromLinesOptions { resultPrefix: string; } -export function stringFromLines( - lines: readonly string[], +export function stringFromPdf( + pdf: string, regex: RegExp, options?: Partial, ): string | DocumentParseError { - for (const line of lines) { - const trimmedLine = line.trim(); - const match = trimmedLine.match(regex); - if (!match) { - continue; - } - - const matchingString = match[1]; - if (typeof matchingString === "undefined") { - throw new Error( - `internal error stdp7xw6rb: regex should have matched line; ` + - `regex=${regex.source}, line=${trimmedLine}`, - ); - } + const match = pdf.match(regex); + if (!match) { + return { + type: "DocumentParseError", + message: `regular expression was not matched: ${regex.source}`, + }; + } - const resultPrefix = options?.resultPrefix; - if (typeof resultPrefix === "undefined") { - return matchingString; - } else { - return resultPrefix + matchingString; - } + const matchingString = match[1]; + if (typeof matchingString === "undefined") { + throw new Error( + `internal error stdp7xw6rb: regex should have had a matching group: ` + + regex.source, + ); } - return { - type: "DocumentParseError", - message: `line not found matching regex: ${regex.source}`, - }; + const resultPrefix = options?.resultPrefix; + if (typeof resultPrefix !== "undefined") { + return resultPrefix + matchingString; + } else { + return matchingString; + } } -export function yyyymmddDateFromLines( - lines: readonly string[], +export function yyyymmddDateFromPdf( + pdf: string, regex: RegExp, dateFormat: string, ): string | DocumentParseError { - const dateStr = stringFromLines(lines, regex); + const dateStr = stringFromPdf(pdf, regex); if (isDocumentParseError(dateStr)) { return dateStr; } diff --git a/finances/documents/kitchener_utilities.ts b/finances/documents/kitchener_utilities.ts index a6c2251..aa0e22e 100644 --- a/finances/documents/kitchener_utilities.ts +++ b/finances/documents/kitchener_utilities.ts @@ -1,10 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; +import type { Document, DocumentParseError } from "../document.ts"; import { isDocumentParseError } from "../document.ts"; -import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; +import { stringFromPdf, yyyymmddDateFromPdf } from "../document_utils.ts"; export interface ParsedKitchenerUtilitiesBill { type: "KitchenerUtilitiesBill"; @@ -18,8 +14,9 @@ class KitchenerUtilitiesBill implements Document< > { readonly type = "KitchenerUtilitiesBill" as const; - identify(source: Readonly): boolean { - return source.lines.some((line) => line.includes("UTILITIES@KITCHENER.CA")); + identify(pdf: string): boolean { + const regex = /\s519-741-2626.*utilities@kitchener.ca$/im; + return regex.test(pdf); } calculateFileName(pdf: Readonly): string { @@ -27,21 +24,19 @@ class KitchenerUtilitiesBill implements Document< return `${statementDate} Kitchener Utilities Bill ${amountDue}.pdf`; } - parse( - source: Readonly, - ): ParsedKitchenerUtilitiesBill | DocumentParseError { - const statementDate = yyyymmddDateFromLines( - source.lines, - /^Statement Date:\s*(\w+\s+\d+\s+\d+)\s+/i, + parse(pdf: string): ParsedKitchenerUtilitiesBill | DocumentParseError { + const statementDate = yyyymmddDateFromPdf( + pdf, + /^Statement Date:\s*(\w+\s+\d+\s+\d+)\s/im, "MMM D YYYY", ); if (isDocumentParseError(statementDate)) { return statementDate; } - const amountDue = stringFromLines( - source.lines, - /^Pre-authorized Withdrawal:\s*(\d+\.\d+)$/i, + const amountDue = stringFromPdf( + pdf, + /^Pre-authorized Withdrawal:\s*(\d+\.\d+)$/im, { resultPrefix: "$" }, ); if (isDocumentParseError(amountDue)) { diff --git a/finances/documents/morgan_stanley.ts b/finances/documents/morgan_stanley.ts index 723a8ce..e662c71 100644 --- a/finances/documents/morgan_stanley.ts +++ b/finances/documents/morgan_stanley.ts @@ -1,10 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; +import type { Document, DocumentParseError } from "../document.ts"; import { isDocumentParseError } from "../document.ts"; -import { stringFromLines, yyyymmddDateFromLines } from "../document_utils.ts"; +import { stringFromPdf, yyyymmddDateFromPdf } from "../document_utils.ts"; export interface ParsedMorganStanleyRelease { type: "MorganStanleyRelease"; @@ -22,10 +18,9 @@ class MorganStanleyRelease implements Document< > { readonly type = "MorganStanleyRelease" as const; - identify(source: Readonly): boolean { - return source.lines.some( - (line) => line.toLowerCase() === "release confirmation", - ); + identify(pdf: string): boolean { + const regex = /^release confirmation$/im; + return regex.test(pdf); } calculateFileName(pdf: Readonly): string { @@ -45,50 +40,45 @@ class MorganStanleyRelease implements Document< ); } - parse( - source: Readonly, - ): ParsedMorganStanleyRelease | DocumentParseError { - const awardId = stringFromLines(source.lines, /^Award ID:\s+([\w\d]+)$/i); + parse(pdf: string): ParsedMorganStanleyRelease | DocumentParseError { + const awardId = stringFromPdf(pdf, /^Award ID:\s+([\w\d]+)$/im); if (isDocumentParseError(awardId)) { return awardId; } - const settlementDate = yyyymmddDateFromLines( - source.lines, - /^Settlement Date:\s*(\d+-\w+-\d+)$/i, + const settlementDate = yyyymmddDateFromPdf( + pdf, + /^Settlement Date:\s*(\d+-\w+-\d+)$/im, "DD-MMM-YYYY", ); if (isDocumentParseError(settlementDate)) { return settlementDate; } - const vestedValue = stringFromLines( - source.lines, - /^Total Gain.*:\s*(\$[\d,.]+)$/i, - ); + const vestedValue = stringFromPdf(pdf, /^Total Gain.*:\s*(\$[\d,.]+)$/im); if (isDocumentParseError(vestedValue)) { return vestedValue; } - const saleAmount = stringFromLines( - source.lines, - /^Sale PricexQuantity Sold:\s*\((\$[\d,.]+)\)$/i, + const saleAmount = stringFromPdf( + pdf, + /^Sale PricexQuantity Sold:\s*\((\$[\d,.]+)\)$/im, ); if (isDocumentParseError(saleAmount)) { return saleAmount; } - const sharesSold = stringFromLines( - source.lines, - /^Quantity Sold:\s*\((\d+\.\d{3})0*\)$/i, + const sharesSold = stringFromPdf( + pdf, + /^Quantity Sold:\s*\((\d+\.\d{3})0*\)$/im, ); if (isDocumentParseError(sharesSold)) { return sharesSold; } - const salePrice = stringFromLines( - source.lines, - /shares at (\$\d+\.\d{4})0* per share/i, + const salePrice = stringFromPdf( + pdf, + /\sshares at (\$\d+\.\d{4})0* per share\s/im, ); if (isDocumentParseError(salePrice)) { return salePrice; diff --git a/finances/documents/paystub.ts b/finances/documents/paystub.ts index 9998f2b..f6ec418 100644 --- a/finances/documents/paystub.ts +++ b/finances/documents/paystub.ts @@ -1,10 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; +import type { Document, DocumentParseError } from "../document.ts"; import { isDocumentParseError } from "../document.ts"; -import { yyyymmddDateFromLines } from "../document_utils.ts"; +import { yyyymmddDateFromPdf } from "../document_utils.ts"; export type PayStubType = | "PayStubAnnualBonus" @@ -51,12 +47,13 @@ class PayStub implements Document< this.#filenameTemplate = filenameTemplate; } - identify(source: Readonly): boolean { - if (!source.lines.some((line) => line === "Pay Summary")) { + identify(pdf: string): boolean { + const identifyRegex = /^Pay Summary$/im; + if (!identifyRegex.test(pdf)) { return false; } - const match = source.text.match(this.#regex); + const match = pdf.match(this.#regex); if (!match) { return false; } @@ -80,19 +77,17 @@ class PayStub implements Document< return `${pdf.payDate} Google Pay Stub (${parentheticalText}).pdf`; } - parse( - source: Readonly, - ): ParsedPayStub | DocumentParseError { - const payDate = yyyymmddDateFromLines( - source.lines, - /^Pay\s+Date\s+(\d+\s+\d+\s+\d+)$/i, + parse(pdf: string): ParsedPayStub | DocumentParseError { + const payDate = yyyymmddDateFromPdf( + pdf, + /^Pay\s+Date\s+(\d+\s+\d+\s+\d+)$/im, "YYYY MM DD", ); if (isDocumentParseError(payDate)) { return payDate; } - const match = source.text.match(this.#regex); + const match = pdf.match(this.#regex); if (!match) { return { type: "DocumentParseError", @@ -116,7 +111,7 @@ class PayStubAnnualBonus extends PayStub<"PayStubAnnualBonus"> { constructor() { super( "PayStubAnnualBonus", - /\sAnnual\s+Bonus\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+\s/i, + /^Annual\s+Bonus\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+$/im, "Annual Bonus", ); } @@ -126,7 +121,7 @@ class PayStubRegularPay extends PayStub<"PayStubRegularPay"> { constructor() { super( "PayStubRegularPay", - /\sRegular\s+Pay\s+\d+\.\d+\s+\$\d+\.\d+\s+(\$[\d,]+\.\d+)\s/i, + /^Regular\s+Pay\s+\d+\.\d+\s+\$\d+\.\d+\s+(\$[\d,]+\.\d+)\s/im, "Regular Pay", ); } @@ -136,7 +131,7 @@ class PayStubGSU extends PayStub<"PayStubGSU"> { constructor() { super( "PayStubGSU", - /\sGoogle\s+Stock\s+Un\s+0.0*\s+\$0.0*\s+(\$[\d,]+\.\d+)\s/i, + /^Google\s+Stock\s+Un\s+0.0*\s+\$0.0*\s+(\$[\d,]+\.\d+)\s/im, "GSU Vest __AMOUNT__ CAD", ); } @@ -146,7 +141,7 @@ class PayStubShuttle extends PayStub<"PayStubShuttle"> { constructor() { super( "PayStubShuttle", - /\sCA\s+Shuttle\s+Bus\s+\$[\d,]+\.\d+\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s/, + /^CA\s+Shuttle\s+Bus\s+\$[\d,]+\.\d+\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+$/im, "Shuttle Bus", ); } @@ -156,7 +151,7 @@ class PayStubMeal extends PayStub<"PayStubMeal"> { constructor() { super( "PayStubMeal", - /\sMeal\s+Benefit\s+\$[\d,]+\.\d+\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s/, + /^Meal\s+Benefit\s+\$[\d,]+\.\d+\s+(\$[\d,]+\.\d+)\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+\s+\$[\d,]+\.\d+$/im, "Meal Benefit", ); } diff --git a/finances/documents/public_mobile.ts b/finances/documents/public_mobile.ts index 803d62d..934315c 100644 --- a/finances/documents/public_mobile.ts +++ b/finances/documents/public_mobile.ts @@ -1,9 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; -import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; +import type { Document, DocumentParseError } from "../document.ts"; +import { yyyymmddDateFromPdf, stringFromPdf } from "../document_utils.ts"; +import { isDocumentParseError } from "../document.ts"; export interface ParsedPublicMobileStatement { type: "PublicMobileStatement"; @@ -17,8 +14,9 @@ class PublicMobileStatement implements Document< > { readonly type = "PublicMobileStatement" as const; - identify(source: Readonly): boolean { - return source.lines.includes("Public Mobile Account"); + identify(pdf: string): boolean { + const regex = /^Public Mobile Account$/im; + return regex.test(pdf); } calculateFileName(pdf: Readonly): string { @@ -26,43 +24,24 @@ class PublicMobileStatement implements Document< return `${invoiceDate} Public Mobile Payment ${totalAmountPaid}.pdf`; } - parse( - source: Readonly, - ): ParsedPublicMobileStatement | DocumentParseError { - const invoiceIndex = source.lines.findIndex( - (line) => line.toLowerCase() === "invoice", + parse(pdf: string): ParsedPublicMobileStatement | DocumentParseError { + const invoiceDate = yyyymmddDateFromPdf( + pdf, + /^invoice\s+(\w+\s+\d+,\s+\d+)$/im, + "MMM D, YYYY", ); - if (invoiceIndex < 0) { - return { type: "DocumentParseError", message: "INVOICE line not found" }; - } - const invoiceDateStr = source.lines[invoiceIndex + 1]?.trim(); - if (!invoiceDateStr) { - return { - type: "DocumentParseError", - message: "expected line after INVOICE line", - }; - } - const invoiceDate = parseDateToYYYYMMDD("MMM D, YYYY", invoiceDateStr); - if (isParseDateError(invoiceDate)) { - const { message } = invoiceDate; - return { - type: "DocumentParseError", - message: `unable to parse invoice date: ${invoiceDateStr} (${message})`, - }; + if (isDocumentParseError(invoiceDate)) { + return invoiceDate; } - const totalAmountPaidLine = source.lines.find((line) => - line.toLowerCase().startsWith("total amount paid"), + const totalAmountPaid = stringFromPdf( + pdf, + /^total amount paid\s+(\$\d+\.\d+)$/im, ); - if (!totalAmountPaidLine) { - return { - type: "DocumentParseError", - message: "Total Amount Paid line not found", - }; + if (isDocumentParseError(totalAmountPaid)) { + return totalAmountPaid; } - const totalAmountPaid = totalAmountPaidLine.substring(17).trim(); - return { type: "PublicMobileStatement", invoiceDate, totalAmountPaid }; } } diff --git a/finances/documents/questrade.ts b/finances/documents/questrade.ts index 490994c..ab6a005 100644 --- a/finances/documents/questrade.ts +++ b/finances/documents/questrade.ts @@ -1,9 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; -import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; +import type { Document, DocumentParseError } from "../document.ts"; +import { isDocumentParseError } from "../document.ts"; +import { yyyymmddDateFromPdf, stringFromPdf } from "../document_utils.ts"; export type StatementType = | "QuestradeRESPStatement" @@ -46,24 +43,17 @@ class QuestradeStatement implements Document< this.#fileNameType = fileNameType; } - identify(source: Readonly): boolean { - if ( - !source.lines.some((line) => - line.toLowerCase().startsWith("questrade wealth management inc."), - ) - ) { + identify(pdf: string): boolean { + const identifyRegex = /^questrade wealth management inc\.\s/im; + if (!identifyRegex.test(pdf)) { return false; } - if ( - source.lines.some((line) => - line.toLowerCase().includes(this.#identifyingLine), - ) - ) { - return true; - } - - return false; + const lineRegex = RegExp( + "\\s" + RegExp.escape(this.#identifyingLine) + "\\s", + "i", + ); + return lineRegex.test(pdf); } calculateFileName(pdf: Readonly>): string { @@ -74,61 +64,27 @@ class QuestradeStatement implements Document< ); } - parse( - source: Readonly, - ): ParsedQuestradeStatement | DocumentParseError { - const accountNumberRegex = /Account\s*#:\s*(\d+)/i; - const accountNumberLine = source.lines.find((line) => - accountNumberRegex.test(line), - ); - if (!accountNumberLine) { - return { - type: "DocumentParseError", - message: "Account number line not found", - }; - } - const accountNumber = accountNumberLine.match(accountNumberRegex)?.[1]; - if (!accountNumber) { - throw new Error( - "internal error rhtan4myg2: accountNumberRegex should have matched", - ); + parse(pdf: string): ParsedQuestradeStatement | DocumentParseError { + const accountNumber = stringFromPdf(pdf, /^Account\s*#\s*:\s*(\d+)\s/im); + if (isDocumentParseError(accountNumber)) { + return accountNumber; } - const currentMonthRegex = /Current month\s*:\s*(\w+\s+\d+,\s*\d+)/i; - const currentMonthLine = source.lines.find((line) => - line.match(currentMonthRegex), + const statementDate = yyyymmddDateFromPdf( + pdf, + /\sCurrent month\s*:\s*(\w+\s+\d+,\s*\d+)$/im, + "MMMM D, YYYY", ); - if (!currentMonthLine) { - return { - type: "DocumentParseError", - message: "Current month line not found", - }; - } - const statementDateStr = currentMonthLine.match(currentMonthRegex)?.[1]; - if (!statementDateStr) { - throw new Error( - "internal error ydjbyakqr8: currentMonthRegex should have matched", - ); - } - const statementDate = parseDateToYYYYMMDD("MMMM D, YYYY", statementDateStr); - if (isParseDateError(statementDate)) { - const { message } = statementDate; - return { - type: "DocumentParseError", - message: `unable to parse statement date: ${statementDateStr} (${message})`, - }; + if (isDocumentParseError(statementDate)) { + return statementDate; } - const balanceRegex = /Current month balance:\s*(\$[\d,.]+)/i; - const balanceLine = source.lines.find((line) => line.match(balanceRegex)); - if (!balanceLine) { - return { type: "DocumentParseError", message: "Balance line not found" }; - } - const balance = balanceLine.match(balanceRegex)?.[1]; - if (!balance) { - throw new Error( - "internal error ky4fmdxh8b: balanceRegex should have matched", - ); + const balance = stringFromPdf( + pdf, + /^Current month balance:\s*(\$[\d,]+\.\d+)$/im, + ); + if (isDocumentParseError(balance)) { + return balance; } return { type: this.type, statementDate, accountNumber, balance }; diff --git a/finances/documents/rogers.ts b/finances/documents/rogers.ts index d21962b..6e06a3f 100644 --- a/finances/documents/rogers.ts +++ b/finances/documents/rogers.ts @@ -1,9 +1,6 @@ -import type { - Document, - DocumentSource, - DocumentParseError, -} from "../document.ts"; -import { parseDateToYYYYMMDD, isParseDateError } from "../date.ts"; +import type { Document, DocumentParseError } from "../document.ts"; +import { isDocumentParseError } from "../document.ts"; +import { yyyymmddDateFromPdf, stringFromPdf } from "../document_utils.ts"; export interface ParsedRogersBill { type: "RogersBill"; @@ -14,10 +11,9 @@ export interface ParsedRogersBill { class RogersBill implements Document { readonly type = "RogersBill" as const; - identify(source: Readonly): boolean { - return source.lines.some((line) => - line.toUpperCase().includes("1-888-ROGERS-1"), - ); + identify(pdf: string): boolean { + const regex = /^Call 1-888-ROGERS-1$/im; + return regex.test(pdf); } calculateFileName(pdf: Readonly): string { @@ -25,49 +21,22 @@ class RogersBill implements Document { return `${billDate} Rogers Bill ${amountDue}.pdf`; } - parse( - source: Readonly, - ): ParsedRogersBill | DocumentParseError { - const amountDueIndex = source.lines.findIndex( - (line) => line.toLowerCase() === "what is the total due?", + parse(pdf: string): ParsedRogersBill | DocumentParseError { + const amountDue = stringFromPdf( + pdf, + /^what is the total due\?\s+(\$\d+\.\d+)$/im, ); - if (amountDueIndex < 0) { - return { - type: "DocumentParseError", - message: "amount due line not found", - }; - } - const amountDue = source.lines[amountDueIndex + 1]?.trim(); - if (!amountDue) { - return { - type: "DocumentParseError", - message: "expected line after amount due line", - }; + if (isDocumentParseError(amountDue)) { + return amountDue; } - const billDateIndex = source.lines.findIndex( - (line) => line.toLowerCase() === "bill date", + const billDate = yyyymmddDateFromPdf( + pdf, + /^bill date\s+(\w+\s+\d+,\s+\d+)$/im, + "MMM D, YYYY", ); - if (billDateIndex < 0) { - return { - type: "DocumentParseError", - message: "bill date line not found", - }; - } - const billDateStr = source.lines[billDateIndex + 1]?.trim(); - if (!billDateStr) { - return { - type: "DocumentParseError", - message: "expected line after bill date line", - }; - } - const billDate = parseDateToYYYYMMDD("MMM D, YYYY", billDateStr); - if (isParseDateError(billDate)) { - const { message } = billDate; - return { - type: "DocumentParseError", - message: `unable to parse invoice date: ${billDateStr} (${message})`, - }; + if (isDocumentParseError(billDate)) { + return billDate; } return { type: "RogersBill", billDate, amountDue };