107 lines
2.7 KiB
TypeScript
107 lines
2.7 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { parse } from "csv-parse/sync";
|
|
|
|
const FILE_A = process.env.COMPARE_FILE_A ?? path.resolve(process.cwd(), "a.csv");
|
|
const FILE_B = process.env.COMPARE_FILE_B ?? path.resolve(process.cwd(), "b.csv");
|
|
const PHONE_COL_A = process.env.COMPARE_PHONE_COL_A ?? "phone";
|
|
const PHONE_COL_B = process.env.COMPARE_PHONE_COL_B ?? "phone";
|
|
const MODE = process.env.COMPARE_MODE ?? "intersection";
|
|
const OUTPUT_FILE =
|
|
process.env.COMPARE_OUTPUT_FILE ?? path.resolve(process.cwd(), `compare-${MODE}.csv`);
|
|
|
|
type CsvRow = Record<string, string>;
|
|
|
|
function normalizePhone(value: string): string {
|
|
return value.replace(/\D+/g, "");
|
|
}
|
|
|
|
function toCsv(rows: CsvRow[]): string {
|
|
if (rows.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const headers = Array.from(
|
|
rows.reduce((set, row) => {
|
|
for (const key of Object.keys(row)) {
|
|
set.add(key);
|
|
}
|
|
return set;
|
|
}, new Set<string>()),
|
|
);
|
|
|
|
const escape = (value: string): string => {
|
|
if (/[",\n\r]/.test(value)) {
|
|
return `"${value.replace(/"/g, "\"\"")}"`;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const lines = [headers.join(",")];
|
|
for (const row of rows) {
|
|
lines.push(headers.map((header) => escape(row[header] ?? "")).join(","));
|
|
}
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
async function readCsv(filePath: string): Promise<CsvRow[]> {
|
|
const content = await readFile(filePath, "utf8");
|
|
return parse(content, {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
}) as CsvRow[];
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const [rowsA, rowsB] = await Promise.all([readCsv(FILE_A), readCsv(FILE_B)]);
|
|
|
|
const phonesB = new Set(
|
|
rowsB.map((row) => normalizePhone(row[PHONE_COL_B] ?? "")).filter(Boolean),
|
|
);
|
|
|
|
const matches: CsvRow[] = [];
|
|
const onlyA: CsvRow[] = [];
|
|
|
|
for (const row of rowsA) {
|
|
const normalized = normalizePhone(row[PHONE_COL_A] ?? "");
|
|
if (!normalized) {
|
|
continue;
|
|
}
|
|
|
|
if (phonesB.has(normalized)) {
|
|
matches.push({
|
|
source: "a",
|
|
normalized_phone: normalized,
|
|
...row,
|
|
});
|
|
} else {
|
|
onlyA.push({
|
|
source: "a",
|
|
normalized_phone: normalized,
|
|
...row,
|
|
});
|
|
}
|
|
}
|
|
|
|
let outputRows: CsvRow[];
|
|
switch (MODE) {
|
|
case "intersection":
|
|
outputRows = matches;
|
|
break;
|
|
case "only_a":
|
|
outputRows = onlyA;
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported COMPARE_MODE: ${MODE}. Use intersection or only_a.`);
|
|
}
|
|
|
|
await writeFile(OUTPUT_FILE, toCsv(outputRows), "utf8");
|
|
process.stdout.write(`${OUTPUT_FILE}\n`);
|
|
}
|
|
|
|
main().catch((error: unknown) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
process.exitCode = 1;
|
|
});
|