/** * CSV helpers for the Data explorer export (pure, unit-tested). RFC 4180: fields containing a comma, * a double quote, CR or LF are quoted and inner quotes doubled. Attribution rows are prefixed with `#` * so spreadsheet users see them and parsers with a comment option can skip them. */ export function csvCell(v: unknown): string { if (v == null) return ''; const s = v instanceof Date ? v.toISOString() : String(v); return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } export function csvLine(values: readonly unknown[]): string { return values.map(csvCell).join(','); } /** Comment row: one line, newlines collapsed so the header stays line-oriented. */ export function csvComment(text: string): string { return `# ${text.replace(/[\r\n]+/g, ' ').trim()}`; } export const EPI_CSV_COLUMNS = [ 'cancer_id', 'cancer_slug', 'cancer_name', 'geography_id', 'geography_slug', 'geography_name', 'iso3', 'year', 'year_end', 'sex', 'age_group', 'metric', 'value', 'unit', 'lower_ci', 'upper_ci', 'standard_population', 'estimate_type', 'site_definition', 'source_slug', 'source_name', 'provenance_id', 'dataset', 'dataset_version', 'source_url', 'retrieved_at', ] as const; export type EpiCsvColumn = (typeof EPI_CSV_COLUMNS)[number]; /** File name: cancerindex-epidemiology----.csv, safe characters only. */ export function csvFileName(parts: Array): string { const clean = parts .filter((p) => p != null && p !== '') .map((p) => String(p).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) .filter(Boolean); return `cancerindex-epidemiology-${clean.join('-') || 'export'}.csv`; }