Source: arffDataSet.js

/*
 * @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;
    }
}