// Counts operating-budget transfers into and out of Washington's general fund from wa_fund_transfers_to_gf.csv. // Run: node millionaires-tax/data/build_transfer_counts.js -> wa-fund-transfer-counts.csv, transfer-counts.json const fs = require("fs"), path = require("path"); const text = fs.readFileSync(path.join(__dirname, "wa_fund_transfers_to_gf.csv"), "utf8").trim().split(/\r?\n/); const head = text[0].split(","); const rows = text.slice(1).map(l => { const v = l.match(/("([^"]|"")*"|[^,]*)(,|$)/g).map(s => s.replace(/,$/, "").replace(/^"|"$/g, "").replace(/""/g, '"')); return Object.fromEntries(head.map((k, i) => [k, v[i]])); }); const vetoed = r => /veto/i.test(r.note || ""); const loanOrReserve = r => /loan repayment|GF-funded reserve/i.test(r.note || ""); const lists = ["2021-23", "2023-25", "2025-27"]; const m = x => Math.round(x / 100) / 10; // $K -> $M, one decimal const byBiennium = lists.map(b => { const to = rows.filter(r => r.biennium === b && r.direction === "toGF" && !vetoed(r)); const ded = to.filter(r => r.category === "dedicated"); const strict = ded.filter(r => !loanOrReserve(r)); const from = rows.filter(r => r.biennium === b && r.direction === "fromGF"); const sum = a => a.reduce((s, r) => s + Number(r.totalThousands || 0), 0); return { biennium: b, toGfItems: to.length, toGfMillions: m(sum(to)), routineItems: to.filter(r => r.category === "routine").length, bsaItems: to.filter(r => r.category === "bsa").length, dedicatedItems: ded.length, dedicatedMillions: m(sum(ded)), dedicatedExLoansItems: strict.length, dedicatedExLoansMillions: m(sum(strict)), distinctSourceAccounts: new Set(to.map(r => r.sourceAccount)).size, fromGfItems: from.length, fromGfMillions: m(sum(from)), }; }); // 2019-21 list: only items newly inserted by the 2021 law fall in the 2021-2026 window (see method notes). const new2021 = rows.filter(r => r.biennium === "2019-21" && r.direction === "toGF" && /new in 2021|inserted/i.test(r.note || "")); const tot = k => byBiennium.reduce((s, r) => s + r[k], 0); // "Rent and Lease" (2023-25) and "Rental and Lease" (2025-27) are the same Military Department account. const acct = r => r.sourceAccount.replace("Rental and Lease", "Rent and Lease"); const allAccounts = new Set(rows.filter(r => lists.includes(r.biennium) && r.direction === "toGF" && !vetoed(r)).map(acct)); const fiveYear = [ { definition: "All transfer lines into the general fund, 2021-23 to 2025-27 lists", items: tot("toGfItems") }, { definition: "Plus 2019-21 lines first enacted in 2021 (BSA, School Employees' Insurance loan repayment)", items: tot("toGfItems") + 2 }, { definition: "Excluding cannabis, tobacco-settlement and rainy-day (BSA) transfers", items: tot("dedicatedItems") + 1 }, { definition: "Also excluding repayments of general-fund loans and the general fund's own reserve account", items: tot("dedicatedExLoansItems") }, { definition: "Distinct source accounts, 2021-23 to 2025-27 lists", items: allAccounts.size }, ]; const stacked = byBiennium.flatMap(r => [ { biennium: r.biennium, kind: "Other dedicated accounts", items: r.dedicatedItems }, { biennium: r.biennium, kind: "Cannabis and tobacco settlement", items: r.routineItems }, { biennium: r.biennium, kind: "Rainy-day fund (BSA)", items: r.bsaItems }, ]); const keys = Object.keys(byBiennium[0]); fs.writeFileSync(path.join(__dirname, "wa-fund-transfer-counts.csv"), [keys.join(","), ...byBiennium.map(r => keys.map(k => r[k]).join(","))].join("\n") + "\n"); fs.writeFileSync(path.join(__dirname, "transfer-counts.json"), JSON.stringify({ byBiennium, fiveYear, stacked, new2021Found: new2021.length }, null, 1)); console.log(JSON.stringify({ byBiennium, fiveYear, stacked, new2021Found: new2021.length }, null, 1));