Perbaiki pemisah CSV
Perbaiki pemisah CSV
Section titled “Perbaiki pemisah CSV”Ubah pemisah pipa menjadi koma, validasi UUID dan baris kembar.
Berkas: alat/hari-3/Praktik 6/tools/perbaiki-csv.mjs
#!/usr/bin/env node// =====================================================================// Praktik 6 - perbaiki-csv.mjs//// Masalah pada berkas latihan:// "katalog_data_2d.csv" memakai tanda PIPAI (|) sebagai pemisah kolom,// padahal namanya .csv dan DBeaver/Excel memakai koma. Import lewat// "Import Data" di DBeaver akan menghasilkan satu kolom raksasa, bukan// tujuh kolom.//// Skrip ini menulis ulang berkas ke CSV yang benar (RFC 4180), sekaligus:// - memvalidasi setiap data_2d_id berupa UUID// - menandai baris duplikat// - melaporkan kolom mana saja yang kosong//// Pakai:// node perbaiki-csv.mjs <input.csv> [output.csv]// node perbaiki-csv.mjs --cek <input.csv> # hanya lapor, tidak menulis// =====================================================================
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';import { basename, dirname } from 'node:path';
// Kolom wajib berbeda antara katalog 2D (layer_name) dan 3D (nama).// Keduanya wajib punya kolom akses dan sebuah kolom identitas.// Ditentukan setelah header terbaca, lihat bagian "pemeriksaan".const NILAI_AKSES_VALID = ['public', 'private'];const POLA_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// --- argumen ---------------------------------------------------------const argv = process.argv.slice(2);const hanyaCek = argv.includes('--cek');const posisi = argv.filter((a) => !a.startsWith('--'));const masukan = posisi[0];const keluaran = posisi[1] ?? (masukan ? masukan.replace(/\.csv$/i, '') + '.bersih.csv' : null);
if (!masukan) { console.error('Pakai: node perbaiki-csv.mjs <input.csv> [output.csv] [--cek]'); process.exit(2);}
// --- deteksi pemisah -------------------------------------------------// Menghitung pemisah di luar tanda kutip pada baris pertama.function hitungPemisah(baris, pemisah) { let jumlah = 0; let dalamKutip = false; for (let i = 0; i < baris.length; i++) { const c = baris[i]; if (c === '"') { if (dalamKutip && baris[i + 1] === '"') i++; else dalamKutip = !dalamKutip; } else if (c === pemisah && !dalamKutip) { jumlah++; } } return jumlah;}
const mentah = readFileSync(masukan, 'utf8').replace(/^\uFEFF/, '');const barisPertama = mentah.split(/\r?\n/).find((b) => b.trim() !== '') ?? '';
const kandidat = ['|', ',', ';', '\t'];const skor = kandidat.map((p) => ({ pemisah: p, jumlah: hitungPemisah(barisPertama, p) }));skor.sort((a, b) => b.jumlah - a.jumlah);const pemisah = skor[0].jumlah > 0 ? skor[0].pemisah : ',';
console.log(`Berkas : ${masukan}`);console.log( `Pemisah : ${JSON.stringify(pemisah)} (${skor[0].jumlah + 1} kolom terdeteksi` + `${pemisah !== ',' ? '; CSV seharusnya memakai koma -> akan dikonversi' : ''})`,);
// --- parser CSV (mendukung tanda kutip dan newline di dalam sel) -----function parseCsv(teks, pemisah) { const baris = []; let sel = []; let nilai = ''; let dalamKutip = false;
for (let i = 0; i < teks.length; i++) { const c = teks[i]; if (dalamKutip) { if (c === '"') { if (teks[i + 1] === '"') { nilai += '"'; i++; } else dalamKutip = false; } else nilai += c; continue; } if (c === '"') { dalamKutip = true; continue; } if (c === pemisah) { sel.push(nilai); nilai = ''; continue; } if (c === '\r') continue; if (c === '\n') { sel.push(nilai); baris.push(sel); sel = []; nilai = ''; continue; } nilai += c; } if (nilai !== '' || sel.length > 0) { sel.push(nilai); baris.push(sel); } return baris.filter((b) => b.some((s) => s.trim() !== ''));}
const baris = parseCsv(mentah, pemisah);if (baris.length === 0) { console.error('Berkas kosong atau tidak bisa dibaca.'); process.exit(1);}
const header = baris[0].map((h) => h.trim());const data = baris.slice(1);
// --- pemeriksaan -----------------------------------------------------const masalah = [];const catatan = [];
const KOLOM_WAJIB = header.includes('layer_name') ? ['layer_name', 'akses'] : ['nama', 'akses'];const hilang = KOLOM_WAJIB.filter((k) => !header.includes(k));if (hilang.length > 0) masalah.push(`kolom wajib tidak ada di header: ${hilang.join(', ')}`);
const idx = Object.fromEntries(header.map((h, i) => [h, i]));
// Kolom primary key dideteksi, bukan dipatok: berkas 2D memakai// data_2d_id, berkas 3D memakai data_3d_id.const kolomId = header.find((h) => h === 'data_2d_id' || h === 'data_3d_id') ?? header.find((h) => h.endsWith('_id'));if (kolomId) console.log(`Kunci utama : ${kolomId}`);else masalah.push('tidak menemukan kolom primary key berakhiran _id');
const terlihat = new Set();
data.forEach((row, n) => { const nomorBaris = n + 2; // +1 header, +1 karena manusia mulai dari 1 if (row.length !== header.length) { masalah.push(`baris ${nomorBaris}: ${row.length} sel, header punya ${header.length} kolom`); }
const id = kolomId ? (row[idx[kolomId]] ?? '').trim() : ''; if (kolomId) { if (!id) masalah.push(`baris ${nomorBaris}: ${kolomId} kosong`); else if (!POLA_UUID.test(id)) masalah.push(`baris ${nomorBaris}: ${kolomId} bukan UUID -> ${id}`); else if (terlihat.has(id)) masalah.push(`baris ${nomorBaris}: ${kolomId} duplikat -> ${id}`); else terlihat.add(id); }
const akses = (row[idx.akses] ?? '').trim(); if (idx.akses !== undefined && akses && !NILAI_AKSES_VALID.includes(akses)) { masalah.push(`baris ${nomorBaris}: akses "${akses}" tidak dikenal (harus public/private)`); }
const author = (row[idx.author] ?? '').trim(); if (idx.author !== undefined && author && !POLA_UUID.test(author)) { masalah.push(`baris ${nomorBaris}: author bukan UUID -> ${author}`); }});
if (idx.author !== undefined) { const authors = [...new Set(data.map((r) => (r[idx.author] ?? '').trim()).filter(Boolean))]; catatan.push( `author yang dipakai CSV: ${authors.length} nilai (${authors.map((a) => a.slice(0, 8)).join(', ')}…)`, ); catatan.push( 'author ini TIDAK ada di tabel users. Import akan gagal saat foreign key dibuat — ' + 'inilah alasan modul menyuruh menghapus row hasil import lebih dahulu.', );}
console.log(`Header : ${header.join(', ')}`);console.log(`Baris data : ${data.length}`);for (const c of catatan) console.log(`Catatan : ${c}`);
if (masalah.length > 0) { console.log(`\nHASIL: ${masalah.length} masalah`); for (const m of masalah) console.log(` - ${m}`);} else { console.log('\nHASIL: isi baris valid');}
// --- tulis CSV yang benar -------------------------------------------function selCsv(nilai) { const s = String(nilai ?? ''); return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;}
if (hanyaCek) { console.log('\n(--cek: tidak ada berkas yang ditulis)'); process.exit(masalah.length > 0 ? 1 : 0);}
const keluaranCsv = [header.map(selCsv).join(','), ...data.map((r) => header.map((_, i) => selCsv(r[i])).join(','))].join( '\n', ) + '\n';
mkdirSync(dirname(keluaran), { recursive: true });writeFileSync(keluaran, keluaranCsv, 'utf8');console.log(`\nDitulis : ${keluaran} (${basename(keluaran)})`);console.log('Import berkas ini di DBeaver dengan delimiter koma, header di baris pertama.');process.exit(masalah.length > 0 ? 1 : 0);