Source: operator/group-by.js

import { extend2 } from '../utils';
import { rowDiffsetIterator } from './row-diffset-iterator';
import DataModel from '../export';
import reducerStore from '../utils/reducer-store';

/**
 * This function sanitize the user given field and return a common Array structure field
 * list
 * @param  {DataModel} dataModel the dataModel operating on
 * @param  {Array} fieldArr  user input of field Array
 * @return {Array}           arrays of field name
 */
function getFieldArr (dataModel, fieldArr) {
    const retArr = [];
    const fieldStore = dataModel.getPartialFieldspace();
    const dimensions = fieldStore.getDimension();
    const measures = fieldStore.getMeasure();

    Object.entries(dimensions).forEach(([key]) => {
        if (fieldArr && fieldArr.length) {
            if (fieldArr.indexOf(key) !== -1) {
                retArr.push(key);
            }
        } else {
            retArr.push(key);
        }
    });

    Object.entries(measures).forEach(([key]) => {
        if (measures[key].subType() === 'discrete') {
            if (fieldArr && fieldArr.length) {
                if (fieldArr.indexOf(key) !== -1) {
                    retArr.push(key);
                }
            } else {
                retArr.push(key);
            }
        }
    });
    return retArr;
}

/**
 * This sanitize the reducer provide by the user and create a common type of object.
 * user can give function Also
 * @param  {DataModel} dataModel     dataModel to worked on
 * @param  {Object|function} [reducers={}] reducer provided by the users
 * @return {Object}               object containing reducer function for every measure
 */
function getReducerObj (dataModel, reducers = {}) {
    const retObj = {};
    const pReducers = reducers;
    const fieldStore = dataModel.getPartialFieldspace();
    const measures = fieldStore.getMeasure();
    let reducer = reducerStore.defaultReducer();
    if (typeof reducers === 'function') {
        reducer = reducers;
    }
    Object.entries(measures).forEach(([key]) => {
        if (typeof reducers[key] === 'string') {
            pReducers[key] = reducerStore.resolve(pReducers[key]) ? reducerStore.resolve(pReducers[key]) : reducer;
        }
        if (typeof reducers[key] !== 'function') {
            pReducers[key] = undefined;
        }
        retObj[key] = pReducers[key] || reducerStore.resolve(measures[key].defAggFn()) || reducer;
    });
    return retObj;
}

/**
 * main function which perform the group-by operations which reduce the measures value is the
 * fields are common according to the reducer function provided
 * @param  {DataModel} dataModel the dataModel to worked
 * @param  {Array} fieldArr  fields according to which the groupby should be worked
 * @param  {Object|Function} reducers  reducers function
 * @param {DataModel} existingDataModel Existing datamodel instance
 * @return {DataModel} new dataModel with the group by
 */
function groupBy (dataModel, fieldArr, reducers, existingDataModel) {
    const sFieldArr = getFieldArr(dataModel, fieldArr);
    const reducerObj = getReducerObj(dataModel, reducers);
    const fieldStore = dataModel.getPartialFieldspace();
    const fieldStoreObj = fieldStore.fieldsObj();
    const dbName = fieldStore.name;
    const dimensionArr = [];
    const measureArr = [];
    const schema = [];
    const hashMap = {};
    const data = [];
    let newDataModel;
    // Prepare the schema
    Object.entries(fieldStoreObj).forEach(([key, value]) => {
        if (sFieldArr.indexOf(key) !== -1 || reducerObj[key]) {
            schema.push(extend2({}, value.schema));
            if (value.schema.type === 'measure' && value.schema.subtype !== 'discrete') {
                measureArr.push(key);
            } else if (value.schema.type === 'dimension' || value.schema.subtype === 'discrete') {
                dimensionArr.push(key);
            }
        }
    });
    // Prepare the data
    let rowCount = 0;
    rowDiffsetIterator(dataModel._rowDiffset, (i) => {
        let hash = '';
        dimensionArr.forEach((_) => {
            hash = `${hash}-${fieldStoreObj[_].data[i]}`;
        });
        if (hashMap[hash] === undefined) {
            hashMap[hash] = rowCount;
            data.push({});
            dimensionArr.forEach((_) => {
                data[rowCount][_] = fieldStoreObj[_].data[i];
            });
            measureArr.forEach((_) => {
                data[rowCount][_] = [fieldStoreObj[_].data[i]];
            });
            rowCount += 1;
        } else {
            measureArr.forEach((_) => {
                data[hashMap[hash]][_].push(fieldStoreObj[_].data[i]);
            });
        }
    });
    // reduction
    data.forEach((row) => {
        const tuple = row;
        measureArr.forEach((_) => {
            tuple[_] = reducerObj[_](row[_]);
        });
    });
    if (existingDataModel) {
        existingDataModel.__calculateFieldspace();
        newDataModel = existingDataModel;
    }
    else {
        newDataModel = new DataModel(data, schema, { name: dbName });
    }
    return newDataModel;
}

export { groupBy, getFieldArr, getReducerObj };