sakti-parser-fa
Version:
Simple TypeScript library for parsing Indonesian government budget Excel files (Sakti FA)
205 lines • 7.63 kB
JavaScript
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