UNPKG

sakti-parser-fa

Version:

Simple TypeScript library for parsing Indonesian government budget Excel files (Sakti FA)

205 lines 7.63 kB
import ExcelJS from 'exceljs'; /** * Universal SAKTI Excel parser that handles files directly */ export class SaktiParser { /** * Parse SAKTI Excel file from various input types */ async parse(input) { const worksheet = await this.loadWorksheet(input); const metadata = this.extractMetadata(worksheet); const data = this.extractData(worksheet); return { metadata, data }; } /** * Load worksheet from various input types */ async loadWorksheet(input) { const workbook = new ExcelJS.Workbook(); if (typeof input === 'string') { // File path (Node.js) await workbook.xlsx.readFile(input); } else if (input instanceof File) { // File object (Browser) const arrayBuffer = await input.arrayBuffer(); await workbook.xlsx.load(arrayBuffer); } else if (input instanceof ArrayBuffer) { // ArrayBuffer (Browser/Node.js) await workbook.xlsx.load(input); } else { throw new Error('Unsupported input type. Use file path (string), File object, or ArrayBuffer.'); } const worksheet = workbook.getWorksheet(1); if (!worksheet) { throw new Error('No worksheet found in the Excel file.'); } return worksheet; } extractMetadata(worksheet) { const getValue = (address) => { const cell = worksheet.getCell(address); return this.getCellValue(cell)?.toString() || ''; }; // Extract metadata from standard SAKTI header positions const judul = getValue('B1'); const periodeText = getValue('B2'); const kementerianInfo = getValue('B3'); const unitInfo = getValue('B4'); const satuanKerjaInfo = getValue('B5'); // Parse periode const { bulan, tahun } = this.parsePeriode(periodeText); // Parse kementerian const { kode: kementerianKode, nama: kementerianNama } = this.parseKodeNama(kementerianInfo); // Parse unit const { kode: unitKode, nama: unitNama } = this.parseKodeNama(unitInfo); // Parse satuan kerja const { kode: satuanKerjaKode, nama: satuanKerjaNama } = this.parseKodeNama(satuanKerjaInfo); return { judul, periode_text: periodeText, periode_bulan: bulan, periode_tahun: tahun, kementerian_kode: kementerianKode, kementerian_nama: kementerianNama, unit_kode: unitKode, unit_nama: unitNama, satuan_kerja_kode: satuanKerjaKode, satuan_kerja_nama: satuanKerjaNama }; } extractData(worksheet) { const data = []; const maxRow = worksheet.rowCount; // Find data start row (after header) let dataStartRow = 8; // Default, adjust based on actual structure for (let rowNumber = dataStartRow; rowNumber <= maxRow; rowNumber++) { const row = worksheet.getRow(rowNumber); // Skip empty rows if (this.isEmptyRow(row)) continue; const parsedRow = this.parseDataRow(row, rowNumber); if (parsedRow) { data.push(parsedRow); } } return data; } parseDataRow(row, rowNumber) { const getValue = (col) => { const cell = row.getCell(col); return this.getCellValue(cell)?.toString() || ''; }; const getNumber = (col) => { const cell = row.getCell(col); const value = this.getCellValue(cell); return typeof value === 'number' ? value : 0; }; // Determine level based on indentation or cell position const level = this.determineLevel(row); // Extract kode and uraian from appropriate columns const kodeCol = this.findKodeColumn(row, level); const uraianCol = this.findUraianColumn(row, level); const kode = getValue(kodeCol); const uraian = getValue(uraianCol); if (!kode && !uraian) return null; // Initialize data structure const parsedData = { _level: level, _parent: this.determineParent(level), mata_anggaran: '', level_1_kode: '', level_1_uraian: '', level_2_kode: '', level_2_uraian: '', level_3_kode: '', level_3_uraian: '', level_4_kode: '', level_4_uraian: '', level_5_kode: '', level_5_uraian: '', level_6_kode: '', level_6_uraian: '', level_7_kode: '', level_7_uraian: '', level_8_kode: '', level_8_uraian: '', level_9_kode: '', level_9_uraian: '', level_10_kode: '', level_10_uraian: '', level_11_kode: '', level_11_uraian: '', nilai_rupiah: getNumber(this.findNilaiColumn(row)), nilai_total: getNumber(this.findNilaiColumn(row)) }; // Set the appropriate level data parsedData[`level_${level}_kode`] = kode; parsedData[`level_${level}_uraian`] = uraian; return parsedData; } getCellValue(cell) { if (!cell || cell.value === null || cell.value === undefined) { return null; } // Handle rich text if (cell.value && typeof cell.value === 'object' && 'richText' in cell.value) { return cell.value.richText.map((rt) => rt.text).join(''); } // Handle formulas if (cell.value && typeof cell.value === 'object' && 'result' in cell.value) { return cell.value.result; } return cell.value; } parsePeriode(periodeText) { // Parse month and year from periode text const match = periodeText.match(/(\\d{1,2}).*?(\\d{4})/); if (match) { return { bulan: parseInt(match[1], 10), tahun: parseInt(match[2], 10) }; } return { bulan: 0, tahun: 0 }; } parseKodeNama(info) { // Parse "KODE - NAMA" format const parts = info.split(' - '); return { kode: parts[0]?.trim() || '', nama: parts[1]?.trim() || '' }; } isEmptyRow(row) { return !row.hasValues; } determineLevel(row) { // Simple level determination based on cell positions // This is a simplified approach - adjust based on actual SAKTI format for (let col = 1; col <= 15; col++) { const cell = row.getCell(col); if (this.getCellValue(cell)) { return Math.min(col, 11); // Max 11 levels } } return 1; } findKodeColumn(row, level) { // Find the column containing the kode for this level return level; // Simplified } findUraianColumn(row, level) { // Find the column containing the uraian for this level return level + 1; // Simplified } findNilaiColumn(row) { // Find the rightmost column with numeric values for (let col = row.cellCount; col >= 1; col--) { const cell = row.getCell(col); const value = this.getCellValue(cell); if (typeof value === 'number') { return col; } } return row.cellCount; // Default to last column } determineParent(level) { // Simplified parent determination return level > 1 ? `level_${level - 1}` : ''; } } //# sourceMappingURL=parser.js.map