/*
* @fileoverview Clase para leer, interpretar y desglosar archivos de tipo ARFF (Attribute Relation File Format).
* Generado como parte del proyecto CAIM+BAYES+CV K-FOLDS.
*
* https://waikato.github.io/weka-wiki/formats_and_processing/arff/
*
* @author Fernando MM
* @version 1.0
* @date 2024-04-10
*
* Historial de cambios:
* - 1.0 (2024-04-10): Creación de la librería.
*/
class ARFFDataSet {
/**
* @description La clase ARFFDataSet está encargada de leer, interpretar y desglosar archivos de tipo ARFF (Attribute Relation File Format).
*/
constructor() {
this.attributes = [];
this.data = [];
this.labels = [];
}
/**
* @description Lee el archivo ARFF y lo interpreta como texto
* @param {blob} file Archivo ARFF a procesar
*/
readARFF(file, callback) {
const reader = new FileReader();
reader.onload = (e) => {
const lines = e.target.result.split('\n');
let readingData = false; // Bandera para iniciar a leer datos
for (const line of lines) {
const trimmedLine = line.trim();
// Comentarios y lineas vacias se ignoran
if (trimmedLine.startsWith('%') || trimmedLine === '')
continue;
if (trimmedLine.toUpperCase() === '@DATA') {
readingData = true; // Explicitamente iniciar a leer datos
continue;
}
if (readingData) {
// Los datos nominales pueden venir entre comillas simples y deben quitarse los espacios laterales de cada dato si existen
//const row = trimmedLine.replace(/'/g, '').split(',');
const row = trimmedLine.split(',').map(item => item.replace(/'/g, '').trim());
this.data.push(row.slice(0, -1));
this.labels.push(row.slice(-1)[0]);
} else if (trimmedLine.toUpperCase().startsWith('@ATTRIBUTE')) {
const regex = /@ATTRIBUTE\s+('[^']+'|\S+)\s+(INTEGER|REAL|NUMERIC|\{[^\}]+\})\s*(\[[^\]]+\])?/i;
const parts = trimmedLine.match(regex);
if (parts) {
const attributeName = parts[1].replace(/'/g, ''); // Eliminar comillas simples en los nombres de los atributos
const attributeType = parts[2].replace(/\{|\}/g, ''); // Eliminar los corchetes si son parte del 'tipo' (se asume que es una lista de valores nominales)
const rangeOrValues = parts[3];
if (attributeType.toUpperCase() === 'INTEGER' && rangeOrValues && rangeOrValues.startsWith('[')) {
// Los casos que están definidos como INTEGER pero presentan un rango de valores posibles (p.ej. INTEGER[i,j]), se tomarán para este algoritmo
// como atributos NOMINALES, con los valores del rango desglosados como valores. NO SON TOMADOS COMO NÚMEROS.
const range = rangeOrValues.match(/\[(\d+),(\d+)\]/);
if (range) {
const start = parseInt(range[1], 10);
const end = parseInt(range[2], 10);
const values = Array.from({length: end - start + 1}, (_, i) => start + i);
this.attributes.push({name: attributeName, type: 'NOMINAL', values: values.map(String)});
} else {
this.attributes.push({name: attributeName, type: 'INTEGER'});
}
} else if (attributeType.match(/^(INTEGER|NUMERIC|REAL)$/i)) {
this.attributes.push({name: attributeName, type: attributeType.toUpperCase()});
} else {
// Procesar valores nominales (se asume que venían entre {}), pudieran estar entre comillas simples (eliminar)
const values = attributeType.split(',').map(value => value.trim().replace(/'/g, ''));
this.attributes.push({name: attributeName, type: 'NOMINAL', values: values});
}
}
} else {
// Si hay contenido y no empieza con una etiqueta identificable, pero ya hay al menos un atributo reconocido previamente, se asume que
// debe iniciar a leer datos, aún si no hay una etiqueta @DATA explicita.
if (this.attributes.length > 0) {
readingData = true;
// Los datos nominales pueden venir entre comillas simples y deben quitarse los espacios laterales de cada dato si existen
const row = trimmedLine.split(',').map(item => item.replace(/'/g, '').trim());
this.data.push(row.slice(0, -1));
this.labels.push(row.slice(-1)[0]);
}
}
}
callback(this.attributes, this.data, this.labels);
};
reader.readAsText(file);
}
/**
* @description Obtiene los índices de aquellos datos que ya están discretizados para guardarlos y no discrtetizarlos de nuevo.
* @returns {Array}
*/
getDiscretizedAttributesInfo() {
const attributes = this.attributes;
let discretizedAttributes = [];
// Iterar sobre todos los atributos
for (let i = 0; i < attributes.length; i++) {
// Verificar si el atributo tiene la propiedad 'cuts'
if (attributes[i].cuts) {
// Si el atributo ha sido discretizado, agregar su índice y sus cortes a la lista
discretizedAttributes.push({
index: i,
name: attributes[i].name,
cuts: attributes[i].cuts
});
}
}
return discretizedAttributes;
}
}