index.js

'use strict';

const _ = require('lodash');
const Assert = require('assert');

/**
 * Receives an object containing an array property and substitutes it by a new property with the length of that array
 * @param {Object} object Object containing publications, cites, etc., and other properties
 * @param {String} property Name of the property that needs to be sumed
 * @return {Object} Same object passed in, but without `property` (array) and with `property+Count` (length of the array)
 */
function sumPropertyLength(object, property) {
    Assert(typeof (object) === 'object', "The object parameter must be an object");
    Assert(typeof (property) === 'string', "The property parameter must be a string");
    Assert(property in object, "The property passed must be present in the object passed");

    let changedObject = _.clone(object);
    changedObject[property + 'Count'] = changedObject[property].length;
    delete changedObject[property];
    return changedObject;
}

/**
 * Receives an object containing an array property and substitutes it by a new property with the sum of the elements in the array
 * @param {Object} object Object containing publications, cites, etc., and other properties
 * @param {String} property Name of the property that needs to be sumed
 * @return {Object} Same object passed in, but without `property` (array) and with `property+Count` (sum of the elements in the array)
 */
function sumProperties(object, property) {
    Assert(typeof (object) === 'object', "The object parameter must be an object");
    Assert(typeof (property) === 'string', "The property parameter must be a string");
    Assert(property in object, "The property passed must be present in the object passed");

    let changedObject = _.clone(object);
    changedObject[property + 'Count'] = _.sum(changedObject[property]);
    delete changedObject[property];
    return changedObject;
}

/**
 * Receives an array of objects and a property name and returns the sum of the values of the property for every array element
 * @param {Object[]} array Array of objects with several properties each
 * @param {String} property Name of the property to be sumed
 * @return {Number} Sum of all the values called `property` in the passed array
 */
function sumTotal(array, property) {
    Assert(typeof (array) === 'object', "The array parameter must be an array");
    Assert(typeof (property) === 'string', "The property parameter must be a string");
    array.forEach(element => {
        Assert(property in element, "The property passed must be present in every element of the array passed");
    });

    return _.sumBy(array, property);
}

/**
 * Calculates the average of a given property in an object
 * @param {Object} object Object containing `property` property and others
 * @param {String} property Name of the property to be averaged
 * @return {Object} Same object passed in, but without `property` (array) and with `property+Average` (number)
 */
function averageProperty(object, property) {
    Assert(typeof (object) === 'object', "The object parameter must be an object");
    Assert(typeof (property) === 'string', "The property parameter must be a string");
    Assert(property in object, "The property passed must be present in the object passed");

    let changedObject = _.clone(object);
    changedObject[property + 'Average'] = _.mean(changedObject.cites);
    // If there are no cites, _.mean returns NaN, so we need to convert it to null
    if (isNaN(changedObject[property + 'Average'])) {
        changedObject[property + 'Average'] = null;
    }
    delete changedObject[property];
    return changedObject;
}

/**
 * Given an object, which contains an array, computes the percentage of elements over the total
 * @param {Object} object Object with several properties, including the array 
 * @param {String} property Name of the property where to calculate percentage
 * @param {Number} totalCount Total number of elements, counting all objects in the array passed
 * @return {Object} Same object passed in, but substituting the array by the percentage of its elements over the total
 */
function percentOverTotal(object, property, totalCount) {
    Assert(typeof (object) === 'object', "The object parameter must be an object");
    Assert(typeof (property) === 'string', "The property parameter must be a string");
    Assert(typeof (totalCount) === 'number', "The totalCount parameter must be a number");
    Assert(property in object, "The property passed must be present in the object passed");

    let changedObject = _.clone(object);
    if (totalCount === 0) { // If the author has no publications at all
        changedObject[property + 'PercentOverTotal'] = null;
    } else {
        changedObject[property + 'PercentOverTotal'] = changedObject[property].length / totalCount;
    }
    delete changedObject[property];
    return changedObject;
}

/**
 * Returns the number of publications that are above the required percentile number
 * @param {Object[]} publications Array with all the publications' best percentiles, grouped by year
 * returned by {@link bestPercentiles}
 * @see {@link bestPercentiles}
 * @param {Number} percentile The number over which the publications percentile have to be above
 * @returns {Number} The number of publications that are above `percentile`
 */
function publicationsOverPercentile(publications, percentile) {
    Assert(publications !== undefined, "No publications object received");
    Assert(percentile !== undefined, "No percentile received");
    Assert(publications.constructor === Array, "The publications object must be an array");
    Assert(!isNaN(parseInt(percentile)), "Percentile must be a number");
    percentile = parseInt(percentile);
    Assert(percentile >= 0 && percentile <= 100, "Percentile must be between 0 and 100");

    const publicationsUngrouped = [].concat.apply([], publications.map(group => group.publications));

    const publicationsNumber = publicationsUngrouped.reduce((acc, currPublication) => {
        if (currPublication.percentile >= percentile) {
            acc++;
        }

        return acc;
    }, 0);

    return publicationsNumber;
}

/**
 * 
 * @param {Object[]} publications Array with all the publications' best percentiles, grouped by year
 * returned by {@link bestPercentiles}
 * @param {Number} qNumber The Q you want to know how many publications have. Has to be a number 
 * between 1 and 4
 * @return {Number} The number of publications in the Q you asked for
 */
function publicationsInQn(publications, qNumber) {
    Assert(publications !== undefined, "No publications object received");
    Assert(qNumber !== undefined, "No Q number received");
    Assert(publications.constructor === Array, "The publications object must be an array");
    Assert(!isNaN(parseInt(qNumber)), "Q number must be a number");
    qNumber = parseInt(qNumber);
    Assert(qNumber >= 1 && qNumber <= 4, "Q number must be between 1 and 4");

    const publicationsUngrouped = [].concat.apply([], publications.map(group => group.publications));

    const publicationsNumber = publicationsUngrouped.reduce((acc, currPublication) => {
        const lowestPercentile = 100 - (25 * qNumber);

        // This makes all the publications with percentile 0 not to
        // be counted as Q4
        if (currPublication.percentile > lowestPercentile &&
            currPublication.percentile <= lowestPercentile + 25) {
            acc++;
        }

        return acc;
    }, 0);

    return publicationsNumber;
}

/**
 * Return the best percentile of each publication and the area in which the publication has that percentile.
 * If more than one area is found, returns the one with lowest world average cites. If a publication does
 * not have cite score data for its publication year, or if the area code with best percentile cannot be converted
 * to an area name, `null` is returned in `percentile` and `area`.
 * @param {Array} areasArray Array with the possible areas of the publications. It is used for converting area
 * code to area name
 * @param {Object} publicationsGrouped A JSON with a property year and a property publications with the ones from that year
 * @param {Object} worldAverageCitesByArea JSON object containing all subject areas as keys, each of them with a number of
 * years as keys too, whose values are the average number of cites in that year
 * @returns {Object} Object with the same structure as the one passed as parameter but with an array of each publication's best percentile. 
 * If a publication has no percentile or area for its year, `null` is returned in the corresponding field. Example:
 * ```
 *  {
 *      year: Number,
 *      publications: [...
 *          {
 *              publicationID: string,
 *              percentile: Number?,
 *              area: string?
 *          }
 *      ]
 *  }
 * ```
 */
function bestPercentiles(areasArray, publicationsGrouped, worldAverageCitesByArea) {
    Assert(typeof (publicationsGrouped) === 'object', 'The publicationsGrouped object must be an array');
    Assert('year' in publicationsGrouped, 'Object has not the required format ({year: Number, publications: Array})');
    Assert('publications' in publicationsGrouped, 'Object has not the required format ({year: Number, publications: Array})');
    Assert(!isNaN(parseInt(publicationsGrouped.year)), 'Object has not the required format ({year: Number, publications: Array})');
    Assert(publicationsGrouped.publications.constructor === Array, 'Object has not the required format ({year: Number, publications: Array})');
    Assert(typeof (worldAverageCitesByArea) === 'object', "The worldAverageCitesByArea parameter must be an object");
    Assert(areasArray.constructor === Array, 'areasArray is not an array');

    let bestPercentilesObject = {
        year: parseInt(publicationsGrouped.year),
        publications: []
    };

    // Fixes the year in the range we have world average cites data. If the year
    // lower than 2006, it fixes it to 2006. If it's greater than 2016, fixes 
    // it to 2016. Else, leaves the year untouched.
    const worldAverageCitesYear = __fixYear(bestPercentilesObject.year);

    let areasMapping = {};
    areasArray.forEach((area) => {
        areasMapping[area['@code']] = {
            name: __fixSubjectArea(area['$']),
            abbrev: area['@abbrev']
        };
    });

    publicationsGrouped.publications.forEach((publication) => {

        const publicationID = publication["dc:identifier"];

        let citeScores;

        let result = {
            publicationID: publicationID,
            percentile: null,
            area: null
        };

        if (_.has(publication, 'publisherStats.citeScoreYearInfoList.citeScoreYearInfo')) {
            citeScores = publication['publisherStats']['citeScoreYearInfoList']['citeScoreYearInfo']
                .find(citeScore => {
                    return citeScore['@year'] === String(bestPercentilesObject.year);
                });


            if (citeScores) {
                let bestAreas = [];
                let bestPercentile = 0;

                citeScores['citeScoreInformationList'][0]['citeScoreInfo'][0]['citeScoreSubjectRank'].forEach((area) => {
                    if (parseInt(area.percentile) === bestPercentile) {
                        bestAreas.push(area);
                    } else if (parseInt(area.percentile) > bestPercentile) {
                        bestAreas = [area];
                        bestPercentile = parseInt(area.percentile);
                    }
                });

                // Add the current publication areas to the areas mapping object
                // to complete it. There are cases in which the author doesn't have
                // all the areas in which he or she has published
                publication['publisherStats']['subject-area'].forEach((area) => {
                    if (!(area['@code'] in areasMapping)) {
                        areasMapping[area['@code']] = {
                            name: __fixSubjectArea(area['$']),
                            abbrev: area['@abbrev']
                        };
                    }
                });

                // If we can resolve every area code into an area name, do the normal process
                if (bestAreas.every((area) => {
                    return area['subjectCode'] in areasMapping;
                })) {
                    let bestArea;

                    if (bestAreas.length === 1) {
                        bestArea = bestAreas[0];
                    } else {
                        bestArea = _.minBy(bestAreas, (area) => {
                            const areaName = areasMapping[area['subjectCode']].name;

                            return __checkWorldAverageCites(areaName, worldAverageCitesYear, worldAverageCitesByArea);
                        });
                    }

                    result = {
                        publicationID: publicationID,
                        percentile: bestPercentile,
                        area: areasMapping[bestArea['subjectCode']].name
                    };
                }
                // Else, skip the publication
            }
        }

        bestPercentilesObject.publications.push(result);
    });

    return bestPercentilesObject;
}

/**
 * Given an array of publications, it computes the Q1 publications percent by looking at the property
 * ``percentile${qualitySource}`` placed at first level in every publication object
 * @param {Array} publications Array with all publication objects
 * @param {String} qualitySource Quality source (JCR, SCIE, etc.) used to compute percentile of a publication
 * @returns {Number} Percent of Q1 publications out of the total number of publications
 */
function q1PublicationsPercent(publications, qualitySource) {
    Assert(publications !== undefined, 'publications parameter is mandatory');
    Assert(qualitySource !== undefined, 'qualitySource parameter is mandatory');
    Assert(publications.constructor === Array, 'publications is not an array');
    Assert(typeof (qualitySource) === 'string', 'qualitySource is not a string');
    publications.forEach(publication => {
        Assert(typeof (publication) === 'object');
    });
    return publications.filter(publication => { return Number(publication[`percentile${qualitySource.toUpperCase()}`]) >= 75; }).length / publications.length * 100;
}

/**
 * Receives an object containing a year and a set of publications, and computes the area in which most publications are.
 * If there is more than one mostPublishedArea, the one whose worldAverageCites is the lowest is chosen. In case the
 * year associated to that subject-area is not between 2006 & 2016 (inclusive) these values are respectively taken to
 * choose the mostPublishedArea
 * @param {Object} publicationsOfYear Object containing year and array of publications
 * @param {Object} worldAverageCitesByArea JSON object containing all subject areas as keys, each of them with a number of
 * years as keys too, whose values are the average number of cites in that year
 * @return {Object} Same object passed in, but without publications and with two new properties, `optimalMostPublishedArea` and
 * `mostPublishedAreas`
 */
function mostPublishedArea(publicationsOfYear, worldAverageCitesByArea) {
    Assert(typeof (publicationsOfYear) === 'object', "The publicationsOfYear parameter must be an object");
    Assert('publications' in publicationsOfYear, "The property 'publications' must be present in 'publicationsOfYear'");
    Assert(typeof (publicationsOfYear.publications) === 'object', "The publications property of publicationsOfYear must be an array");
    Assert('year' in publicationsOfYear, "The property 'year' must be present in 'publicationsOfYear'");
    Assert(typeof (publicationsOfYear.year) === 'number', "The year property of publicationsOfYear must be a number");
    Assert(!isNaN(publicationsOfYear.year));
    Assert(typeof (worldAverageCitesByArea) === 'object', "The worldAverageCitesByArea parameter must be an object");
    publicationsOfYear.publications.forEach(publication => {
        Assert('publisherStats' in publication, "The property 'publisherStats' must be present in every element of the array 'publicationsOfYear.publications'");
        // Assert('subject-area' in publication.publisherStats, "The property 'subject-area' must be present in 'publicationsOfYear.publications.publisherStats'");
        // publication.publisherStats["subject-area"].forEach(subjectAreaElement => {
        //     Assert('$' in subjectAreaElement, "The property '$' must be present in 'publicationsOfYear.publications.publisherStats[subject-area]'");
        // });
    });

    // If publication year doesn't exist in Elsevier JSON file, choose the nearest one
    const worldAverageCitesYear = __fixYear(publicationsOfYear.year);

    let areaIndex;
    let areasArray = []; // Contains all areas that come up at least once
    let areasWithHighestCount = []; // Contains the areas that come up more times (same number of times)
    let mostPublishedArea;
    publicationsOfYear.publications.forEach(publication => {
        if (_.has(publication, 'publisherStats[subject-area]')) { // Sometimes, 'publisherStats' is 'No indexed'
            publication.publisherStats["subject-area"].forEach(subject => {
                if (_.has(subject, '$')) {
                    subject["$"] = __fixSubjectArea(subject["$"]); // FIXES ALL SUBJECT-AREAS THAT ARE AREAS, NOT CATEGORIES
                    if ((areaIndex = _.findIndex(areasArray, ['area', subject["$"]])) < 0) {
                        areasArray.push({
                            area: subject["$"],
                            count: 1
                        });
                    } else {
                        areasArray[areaIndex].count++;
                    }
                }
            });
        }
    });
    if (areasArray.length > 0) {
        const highestCount = _.maxBy(areasArray, 'count').count;
        areasArray.forEach(areaElement => {
            if (areaElement.count === highestCount) {
                areasWithHighestCount.push({
                    area: areaElement.area,
                    count: areaElement.count,
                    worldAverageCites: __checkWorldAverageCites(areaElement.area, worldAverageCitesYear, worldAverageCitesByArea)
                });
            }
        });
        if (areasWithHighestCount.length === 1) {
            mostPublishedArea = areasWithHighestCount[0].area;
        } else { // If there are multiple areas with the same count, choose the one whose worldAverageCites is the lowest
            mostPublishedArea = _.minBy(areasWithHighestCount, 'worldAverageCites');
            if (mostPublishedArea) {
                mostPublishedArea = mostPublishedArea.area;
            } else { // This happens when all areasWithHighestCount contain null worldAverageCites
                // Return the first one, it's not possible to make a decision since we have no worldAverageCites figures for any area
                mostPublishedArea = areasWithHighestCount[0].area;
            }
        }
    } else {
        mostPublishedArea = null;
    }

    return {
        year: publicationsOfYear.year,
        optimalMostPublishedArea: mostPublishedArea,
        mostPublishedAreas: areasWithHighestCount.map(area => area.area)
    };
}

/**
 * Computes the normalized impact per year
 * @param {Object} publicationsOfYear Object containing a year and an array of publications
 * @param {Number} averageCites Number of average cites for the year passed in publicationsOfYear
 * @param {String} mostPublishedArea Area in which most publications are
 * @param {Object} worldAverageCitesByArea JSON object containing all subject areas as keys, each of them with a number of 
 * years as keys too, whose values are the average number of cites in that year
 * @return {Object} New object containing the year and the normalized impact 
 */
function normalizedImpact(year, averageCites, mostPublishedArea, worldAverageCitesByArea) {
    Assert(typeof (year) === 'number', "The year parameter must be a number");
    Assert(typeof (averageCites) === 'number' || averageCites === null, "The averageCites parameter must be a number or null");
    Assert(typeof (mostPublishedArea) === 'string' || mostPublishedArea === null, "The mostPublishedArea parameter must be a string or null");
    Assert(typeof (worldAverageCitesByArea) === 'object', "The worldAverageCitesByArea parameter must be an object");
    // Don't assert the following, because this could happen and shouldn't return an error
    // Assert(mostPublishedArea in worldAverageCitesByArea, "The mostPublishedArea passed property must be present in the JSON file worldAverageCitesByArea");

    // If publication year doesn't exist in Elsevier JSON file, choose the nearest one
    const worldAverageCitesYear = __fixYear(year);
    const worldAverageCitesOfYear = __checkWorldAverageCites(mostPublishedArea, worldAverageCitesYear, worldAverageCitesByArea);
    // Compute the value only if the properties passed are not null and year for that subject-area exists in the JSON file
    const normalizedImpact = worldAverageCitesOfYear && __iT(averageCites) && mostPublishedArea ? averageCites / worldAverageCitesOfYear : null;

    return {
        year: year,
        normalizedImpact: normalizedImpact
    };
}

/**
 * Computes an array with the weighted impact per year
 * @param {Object[]} normalizedImpactPerYear Array of objects containing year and normalizedImpact
 * @param {Object[]} publicationsPercentPerYear Array of objects containing year and percentOfPublications per year over the total
 * @return {Object[]} Array containing year and weighted impact per year
 */
function weightedImpact(normalizedImpactPerYear, publicationsPercentPerYear) {
    Assert(normalizedImpactPerYear !== undefined, "The function must be called with two parameters");
    Assert(publicationsPercentPerYear !== undefined, "The function must be called with two parameters");
    Assert(normalizedImpactPerYear.constructor === Array, "The normalizedImpactPerYear parameter must be an array");
    Assert(publicationsPercentPerYear.constructor === Array, "The publicationsPercentPerYear parameter must be an array");
    Assert(normalizedImpactPerYear.length === publicationsPercentPerYear.length, "Both arrays must have the same length");
    const normalizedImpactYears = normalizedImpactPerYear.map(normalizedImpactElement => {
        Assert('year' in normalizedImpactElement, "The property 'year' must be present in every element of the array normalizedImpactPerYear");
        Assert(typeof (normalizedImpactElement.year) === 'number', "The year property of normalizedImpactElement must be a number");
        Assert('normalizedImpact' in normalizedImpactElement, "The property 'normalizedImpact' must be present in every element of the array normalizedImpactPerYear");
        Assert(typeof (normalizedImpactElement.normalizedImpact) === 'number' || normalizedImpactElement.normalizedImpact === null, "The normalizedImpact property of normalizedImpactElement must be a number or null");
        return normalizedImpactElement.year;
    });
    const publicationsPercentYears = publicationsPercentPerYear.map(publicationsPercentElement => {
        Assert('year' in publicationsPercentElement, "The property 'year' must be present in every element of the array publicationsPercentPerYear");
        Assert(typeof (publicationsPercentElement.year) === 'number', "The year property of publicationsPercentElement must be a number");
        Assert('publicationsPercentOverTotal' in publicationsPercentElement, "The property 'publicationsPercentOverTotal' must be present in every element of the array publicationsPercentPerYear");
        Assert(typeof (publicationsPercentElement.publicationsPercentOverTotal) === 'number' || publicationsPercentElement.publicationsPercentOverTotal === null, "The publicationsPercentOverTotal property of publicationsPercentElement must be a number or null");
        return publicationsPercentElement.year;
    });
    Assert(_.difference(normalizedImpactYears, publicationsPercentYears).length === 0, "Both arrays must contain the same years");

    let weightedImpact;
    const weightedImpactPerYear = normalizedImpactPerYear.map(normalizedImpactElement => {
        let publicationsPercentElement = _.find(publicationsPercentPerYear, ['year', normalizedImpactElement.year]);
        weightedImpact = __iT(normalizedImpactElement.normalizedImpact) && __iT(publicationsPercentElement.publicationsPercentOverTotal) ? normalizedImpactElement.normalizedImpact * publicationsPercentElement.publicationsPercentOverTotal : null; // If normalizedImpact or publicationsPercentOverTotal is null, weightedImpact will be null too
        return {
            year: normalizedImpactElement.year,
            weightedImpact: weightedImpact
        };
    });
    return weightedImpactPerYear;
}

/**
 * Returns the world average cites for an area, a year.
 * @param {Object} publicationsGrouped A JSON with a property year and an array of publications from that year
 * @param {Object} worldAverageCitesByArea JSON containing the world average cites of each area 
 * @returns {Object[]} An object with the same year as the passed in, the area with most publications that year in the publications
 * array of `publicationsGrouped` and the world average citation for that area. Example:
 * ```
 * {
 *  year: Number,
 *  area: String,
 *  worldAverageCites: Number
 * }
 * ```
 */
function worldAverageCites(publicationsGrouped, worldAverageCitesByArea) {
    Assert(publicationsGrouped !== undefined, "No publications object received");
    Assert(typeof (publicationsGrouped) === 'object', 'The publicationsGrouped object must be an array');

    Assert(worldAverageCitesByArea !== undefined, "No world average cites object received");
    Assert(typeof (worldAverageCitesByArea) === 'object', 'The worldAverageCitesByArea object must be an array');

    Assert('year' in publicationsGrouped, 'Object has not the required format ({year: Number, publications: Array})');
    Assert('publications' in publicationsGrouped, 'Object has not the required format ({year: Number, publications: Array})');
    Assert(!isNaN(parseInt(publicationsGrouped.year)), 'Object has not the required format ({year: Number, publications: Array})');
    Assert(publicationsGrouped.publications.constructor === Array, 'Object has not the required format ({year: Number, publications: Array})');

    const area = mostPublishedArea(publicationsGrouped, worldAverageCitesByArea).optimalMostPublishedArea;

    const year = publicationsGrouped.year;

    const averageCites = areaWorldAverageCitesAtYear(area, year, worldAverageCitesByArea);

    return {
        year: year,
        area: area,
        worldAverageCites: averageCites
    };

}

/**
 * Returns the world average cites for the requested year and area
 * @param {String} area The area to look for in the world average cites object
 * @param {Number} year The year to look for citations in the world average cites object
 * @param {Object} worldAverageCitesByArea The object with all the world average cite information
 * @returns {Number?} The world average cites or null if the area or year are not in the world
 * average cites object 
 */
function areaWorldAverageCitesAtYear(area, year, worldAverageCitesByArea) {
    Assert(area !== undefined, "No area received");
    Assert(typeof (area) === 'string' || area === null, 'The area must be a string or null');

    Assert(year !== undefined, "No year received");
    Assert(typeof (year) === 'number', 'The year must be a number');

    Assert(worldAverageCitesByArea !== undefined, "No world average cites object received");
    Assert(typeof (worldAverageCitesByArea) === 'object', 'The worldAverageCitesByArea object must be an array');

    // Don't assert the following, because this could happen and shouldn't return an error
    // Assert(area in worldAverageCitesByArea, "The area doesn't exists in the world average cites by area object");

    // If publication year doesn't exist in Elsevier JSON file, choose the nearest one
    const worldAverageCitesYear = __fixYear(year);

    const worldAverageCites = __checkWorldAverageCites(area, worldAverageCitesYear, worldAverageCitesByArea);

    return worldAverageCites;
}

/**
 * Returns an array with the name, abbreviature and code of each area in which the author has published. The result has the
 * following format:
 * <pre>
 * [...{
 *  @abbrev: String,
 *  @code: String,
 *  $: String
 *  }]
 * </pre>
 * @param {Object} authorData Raw data of the author
 * @return {Array?} An array with each area and its name, abbreviature and code or null if there are no subject areas fot that author
 */
function areasPublished(authorData) {
    Assert(typeof (authorData) === 'object', "AuthorData has to be an object");

    if (_.has(authorData, 'subject-areas.subject-area')) {
        return authorData['subject-areas']['subject-area'];
    } else {
        return null;
    }
}

/**
 * This function receives an array of areas and return an array with unique areas
 * @param {Array} areasArray The array with duplicates areas
 * @returns {Array} Array containing only unique areas
 */
function removeDuplicatesSubjectAreas(areasArray) {
    Assert(areasArray !== undefined, "No array of areas received");
    Assert(areasArray.constructor === Array, 'areasArray has to be an array');

    return _.uniqBy(areasArray, '@code');
}

/**
 * Fixes all those areas where the subject-area field is not a category, but a general area, that
 * does not have a corresponding average number of cites per year. The fix is done replacing the areas
 * called XXXX (all) with General XXXX
 * @param {String} subjectArea The area to fix
 * @return {String} The fixed area or the original one if there's no need to fix it
 * @private
 */
function __fixSubjectArea(subjectArea) {
    if (/^.*\(all\)$/.test(subjectArea)) {
        return "General " + subjectArea.split(" (all)")[0];
    } else {
        return subjectArea;
    }
}

/**
 * This function fixes a year that need to be used to find the worldAverageCites of a subject-area. If the year
 * is not in the 2006-2016 range, it needs to be fixed to the nearest year in the range.
 * @param {Number} year The year to fix
 * @return {Number} The fixed year or the original year if it's in the correct range
 * @private
 */
function __fixYear(year) {
    return year > 2016 ? 2016 : year < 2006 ? 2006 : year;
}

// This function is used to check if the subject-area and year passed are present in the Elsevier JSON file. If so,
// returns the associated worldAverageCites; if not, returns null
/**
 * 
 * @param {String} area The subject-area to check
 * @param {Number} year The year to check for the subject-area
 * @param {Object} worldAverageCitesByArea The JSON file with all the subjec-areas in which to do the checking
 * @return {Number?} Returns the worldAverageCite of the subject-area passed in or null if its not present
 * @private
 */
function __checkWorldAverageCites(area, year, worldAverageCitesByArea) {
    return worldAverageCitesByArea[area] ? worldAverageCitesByArea[area][year] : null;
}

/**
 * Short for "isTrue". This functions tests validity of a variable. We need to use it since 0 returns false
 * in tests, which could happen with vars like citesAverage, citesCount, etc. and we need to return true in
 * these cases
 * @param {any} variable Variable to test
 * @return {boolean} `true` if the variable is valid and `false` in other cases
 * @private
 */
function __iT(variable) {
    if (variable === null || variable === undefined) {
        return false;
    } else {
        return true;
    }
}

module.exports = {
    sumProperties,
    sumPropertyLength,
    sumTotal,
    averageProperty,
    percentOverTotal,
    mostPublishedArea,
    worldAverageCites,
    areaWorldAverageCitesAtYear,
    normalizedImpact,
    weightedImpact,
    publicationsOverPercentile,
    bestPercentiles,
    q1PublicationsPercent,
    areasPublished,
    removeDuplicatesSubjectAreas,
    publicationsInQn
};