Under the Hood: The Architecture of Bank Statement to CSV Reformatter
Cloud bank-statement converters typically ingest a PDF or photo, persist it on object storage, run OCR on a GPU queue, and cache the extracted ledger for “faster re-opens.” That pipeline is a financial data privacy failure: account numbers, merchant names, and balances become telemetry on someone else’s disk, often retained for model training or support replay. This bank statement to CSV converter rejects that architecture. It is a bank statement CSV converter that never leaves the tab. The only inputs are a string you paste or a .txt/.csv export decoded with TextDecoder after File.arrayBuffer(). There is no OCR, no upload endpoint, and no server-side cache of your ledger.
Transaction CSV formatter intent is structural, not visual. Online banks already render rows as text. Users copy those lines or export a text dump. The reformatter then treats the clipboard as the source of truth and emits a bank statement to Excel-ready CSV. That distinction matters for AdSense-quality documentation and for security: a scanned JPEG of a statement is out of scope by design, because image OCR would require a model and usually a remote worker. Client-side execution here is a finite-state parser: split lines, match dates, match amounts, emit rows. Corporate telemetry cannot attach to a function that never calls fetch.
Memory Allocation & Browser Threading Mechanics
Absorption starts as a UTF-16 JavaScript string in V8/JavaScriptCore heap, or as an ArrayBuffer from the File API that TextDecoder transcodes once into a string. The parser does not build a DOM of the statement. It uses String.prototype.split(/\r?\n/) to produce an array of line strings, then walks that array with a for…of loop. Each iteration allocates a small match result from two regular expressions—one date family and one money family—then pushes a four-cell row into a result array. Peak memory is roughly O(n) in statement length plus a constant for the CSV header.
The work runs on the main thread because typical pasted statements are tens of kilobytes, not gigabytes. Lock-up risk is dominated by regex backtracking, not I/O. The date and money patterns are linear: they use bounded character classes and optional groups without nested unbounded quantifiers, so they stay closer to linear time than catastrophic exponential matchers. If you drop a file, decoding happens in one await file.arrayBuffer() turn; the UI remains interactive because the File API does not stream the bytes to a server. Garbage from intermediate replace calls is collected after the function returns; React state holds only the input string and the output CSV string.
Algorithmic Deep-Dive: Code Snippet & Logical Analysis
Core logic is a line-oriented tokenizer. A row is accepted only when both a date and at least one money token exist. The first money token is treated as the transaction amount; if a second exists it is treated as running balance—an edge case common in printed statements where debit and balance share a line.
const DATE_RE = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{1,2}\s+[A-Za-z]{3,9}\s+\d{2,4})\b/;
const MONEY_RE = /-?\$?\(?\d{1,3}(?:,\d{3})*(?:\.\d{2})\)?/;
function statementToRows(text) {
const rows = [["Date", "Description", "Amount", "Balance"]];
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
const dateMatch = line.match(DATE_RE);
if (!dateMatch) continue;
const amounts = [...line.matchAll(new RegExp(MONEY_RE, "g"))]
.map((m) => m[0].replace(/[()$,]/g, "").trim());
if (!amounts.length) continue;
const description = line
.replace(dateMatch[1], " ")
.replace(new RegExp(MONEY_RE, "g"), " ")
.replace(/\s+/g, " ")
.trim();
rows.push([dateMatch[1], description, amounts[0], amounts[1] ?? ""]);
}
return rows.map((cells) => cells.map(csvEscape).join(",")).join("\n");
}DATE_RE covers ISO dates, slash/dash numeric dates, and “12 Mar 2026” forms. Lines without a date—headers, “Beginning balance” prose, page footers—are skipped, which is the correct behavior for a transaction CSV formatter rather than a dump of every glyph. csvEscape quotes cells that contain commas or quotes so Excel does not split merchant names. Duplicate whitespace is collapsed so copy-paste from proportional fonts does not pollute columns. If no line matches, the output is a header-only CSV rather than a thrown exception: empty result is a valid, auditable artifact.
The Local Storage Sandbox & Volatile Data Pipelines
Nothing is written to localStorage or IndexedDB. The paste buffer lives in React state (volatile RAM). Download constructs a Blob with MIME text/csv, then URL.createObjectURL to mint a short-lived object URL. An ephemeral <a download> click streams those bytes to the user’s disk. Immediately after click, URL.revokeObjectURL drops the mapping so the blob can be garbage-collected. Clearing the textarea or leaving the page drops the last JS string references; the engine’s scavenger collects them. There is no “recent statements” cache, which is intentional for financial data privacy.
Verifying True Privacy: How to Audit Your Browser's Network Tab
Open DevTools with F12 (or Option-Command-I on macOS). Select the Network panel and click the clear-log icon so prior document loads do not confuse the experiment. Filter to Fetch/XHR if you want to hide static JS/CSS from the first page paint. Paste a dummy line such as 03/12/2026 TEST 1.00 10.00, convert, and download. The Network log should show zero new POSTs, no WebSocket frames, and no multipart uploads of your paste. Subsequent requests, if any, belong to ads or analytics on the shell page—not to a statement API—because this reformatter has no backend route. That is the operational definition of a no-upload bank statement to CSV converter.