Source: operator/data-builder.js

import { FieldType, DimensionSubtype } from '../enums';
import { rowDiffsetIterator } from './row-diffset-iterator';
import { mergeSort } from './merge-sort';
import { fieldInSchema } from '../helper';
import { isCallable, isArray, } from '../utils';
/**
 * Generates the sorting functions to sort the data of a DataModel instance
 * according to the input data type.
 *
 * @param {string} dataType - The data type e.g. 'measure', 'datetime' etc.
 * @param {string} sortType - The sorting order i.e. 'asc' or 'desc'.
 * @param {integer} index - The index of the data which will be sorted.
 * @return {Function} Returns the the sorting function.
 */
function getSortFn (dataType, sortType, index) {
    let retFunc;
    switch (dataType) {
    case FieldType.MEASURE:
    case DimensionSubtype.TEMPORAL:
        if (sortType === 'desc') {
            retFunc = (a, b) => b[index] - a[index];
        } else {
            retFunc = (a, b) => a[index] - b[index];
        }
        break;
    default:
        retFunc = (a, b) => {
            const a1 = `${a[index]}`;
            const b1 = `${b[index]}`;
            if (a1 < b1) {
                return sortType === 'desc' ? 1 : -1;
            }
            if (a1 > b1) {
                return sortType === 'desc' ? -1 : 1;
            }
            return 0;
        };
    }
    return retFunc;
}

/**
 * Groups the data according to the specified target field.
 *
 * @param {Array} data - The input data array.
 * @param {number} fieldIndex - The target field index within schema array.
 * @return {Array} Returns an array containing the grouped data.
 */
function groupData(data, fieldIndex) {
    const hashMap = new Map();
    const groupedData = [];

    data.forEach((datum) => {
        const fieldVal = datum[fieldIndex];
        if (hashMap.has(fieldVal)) {
            groupedData[hashMap.get(fieldVal)][1].push(datum);
        } else {
            groupedData.push([fieldVal, [datum]]);
            hashMap.set(fieldVal, groupedData.length - 1);
        }
    });

    return groupedData;
}

/**
 * Creates the argument value used for sorting function when sort is done
 * with another fields.
 *
 * @param {Array} groupedDatum - The grouped datum for a single dimension field value.
 * @param {Array} targetFields - An array of the sorting fields.
 * @param {Array} targetFieldDetails - An array of the sorting field details in schema.
 * @return {Object} Returns an object containing the value of sorting fields and the target field name.
 */
function createSortingFnArg(groupedDatum, targetFields, targetFieldDetails) {
    const arg = {
        label: groupedDatum[0]
    };

    targetFields.reduce((acc, next, idx) => {
        acc[next] = groupedDatum[1].map(datum => datum[targetFieldDetails[idx].index]);
        return acc;
    }, arg);

    return arg;
}

/**
 * Sorts the data before return in dataBuilder.
 *
 * @param {Object} dataObj - An object containing the data and schema.
 * @param {Array} sortingDetails - An array containing the sorting configs.
 */
function sortData(dataObj, sortingDetails) {
    const { data, schema } = dataObj;
    let fieldName;
    let sortMeta;
    let fDetails;
    let i = sortingDetails.length - 1;

    for (; i >= 0; i--) {
        fieldName = sortingDetails[i][0];
        sortMeta = sortingDetails[i][1];
        fDetails = fieldInSchema(schema, fieldName);

        if (!fDetails) {
            // eslint-disable-next-line no-continue
            continue;
        }

        if (isCallable(sortMeta)) {
            // eslint-disable-next-line no-loop-func
            mergeSort(data, (a, b) => sortMeta(a[fDetails.index], b[fDetails.index]));
        } else if (isArray(sortMeta)) {
            const groupedData = groupData(data, fDetails.index);
            const sortingFn = sortMeta[sortMeta.length - 1];
            const targetFields = sortMeta.slice(0, sortMeta.length - 1);
            const targetFieldDetails = targetFields.map(f => fieldInSchema(schema, f));

            groupedData.forEach((groupedDatum) => {
                groupedDatum.push(createSortingFnArg(groupedDatum, targetFields, targetFieldDetails));
            });

            mergeSort(groupedData, (a, b) => {
                const m = a[2];
                const n = b[2];
                return sortingFn(m, n);
            });

            // Empty the array
            data.length = 0;
            groupedData.forEach((datum) => {
                data.push(...datum[1]);
            });
        } else {
            sortMeta = String(sortMeta).toLowerCase() === 'desc' ? 'desc' : 'asc';
            mergeSort(data, getSortFn(fDetails.type, sortMeta, fDetails.index));
        }
    }

    dataObj.uids = [];
    data.forEach((value) => {
        dataObj.uids.push(value.pop());
    });
}


/**
 * Builds the actual data array.
 *
 * @param {Array} fieldStore - An array of field.
 * @param {string} rowDiffset - A string consisting of which rows to be included eg. '0-2,4,6';
 * @param {string} colIdentifier - A string consisting of the details of which column
 * to be included eg 'date,sales,profit';
 * @param {Object} sortingDetails - An object containing the sorting details of the DataModel instance.
 * @param {Object} options - The options required to create the type of the data.
 * @return {Object} Returns an object containing the multidimensional array and the relative schema.
 */
export function dataBuilder (fieldStore, rowDiffset, colIdentifier, sortingDetails, options) {
    const defOptions = {
        addUid: false,
        columnWise: false
    };
    options = Object.assign({}, defOptions, options);

    const retObj = {
        schema: [],
        data: [],
        uids: []
    };
    const addUid = options.addUid;
    const reqSorting = sortingDetails && sortingDetails.length > 0;
    // It stores the fields according to the colIdentifier argument
    const tmpDataArr = [];
    // Stores the fields according to the colIdentifier argument
    const colIArr = colIdentifier.split(',');

    colIArr.forEach((colName) => {
        for (let i = 0; i < fieldStore.length; i += 1) {
            if (fieldStore[i].name === colName) {
                tmpDataArr.push(fieldStore[i]);
                break;
            }
        }
    });

    // Inserts the schema to the schema object
    tmpDataArr.forEach((field) => {
        /** @todo Need to use extend2 here otherwise user can overwrite the schema. */
        retObj.schema.push(field.schema);
    });

    if (addUid) {
        retObj.schema.push({
            name: 'uid',
            type: 'identifier'
        });
    }

    rowDiffsetIterator(rowDiffset, (i) => {
        retObj.data.push([]);
        const insertInd = retObj.data.length - 1;
        let start = 0;
        tmpDataArr.forEach((field, ii) => {
            retObj.data[insertInd][ii + start] = field.data[i];
        });
        if (addUid) {
            retObj.data[insertInd][tmpDataArr.length] = i;
        }
        // Creates an array of unique identifiers for each row
        retObj.uids.push(i);

        // If sorting needed then there is the need to expose the index
        // mapping from the old index to its new index
        if (reqSorting) { retObj.data[insertInd].push(i); }
    });

    // Handles the sort functionality
    if (reqSorting) {
        sortData(retObj, sortingDetails);
    }

    if (options.columnWise) {
        const tmpData = Array(...Array(retObj.schema.length)).map(() => []);
        retObj.data.forEach((tuple) => {
            tuple.forEach((data, i) => {
                tmpData[i].push(data);
            });
        });
        retObj.data = tmpData;
    }

    return retObj;
}