/**
*
* @class Utility class that provides methods to convert and extract data from Arrays and Maps.
* @hideconstructor
*
* @copyright (c) 2021 TLF Research Ltd.
*
*/
function Convert() { }
/**
* Forces the result of an <code>arrayToMap</code> call so every matching key always points to an array.
*
* @memberof Convert
* @constant {bool} FORCE_ARRAY
*/
Convert.FORCE_ARRAY = true;
/**
* Given an array of objects and the name of a property, returns a map whose keys are the values of the given property or properties.
* Each key points to the matching object element, unless the property value matches more than one element.
* In this case its key will map to an array of all the matching elements.
*
* If <code>forceArray</code> is <code>true</code> then the key will *always* point to an array, even if only one element matches.
*
* The original array remains unchanged.
*
* @example
*
* const test = [
* {"foo": 100, "bar": "One", "baz": null},
* {"foo": 200, "bar": "Two", "baz": "baz2"},
* {"foo": 300, "bar": "Three", "baz": null},
* {"foo": 400, "bar": "Four", "baz": "baz4"},
* {"foo": 500, "bar": "Five", "baz": "baz5"}
* ];
*
* @example
* const m1 = Convert.arrayToMap(test, "bar");
* // m1 is:
* // {
* // Five: {foo: 500, bar: "Five", baz: "baz5"},
* // Four: {foo: 400, bar: "Four", baz: "baz4"},
* // One: {foo: 100, bar: "One", baz: null},
* // Three: {foo: 300, bar: "Three", baz: null},
* // Two: {foo: 200, bar: "Two", baz: "baz2"}
* // }
*
* @example
* const m2 = Convert.arrayToMap(test, "baz");
* // m2 is:
* // {
* // baz2: {foo: 200, bar: "Two", baz: "baz2"},
* // baz4: {foo: 400, bar: "Four", baz: "baz4"},
* // baz5: {foo: 500, bar: "Five", baz: "baz5"},
* // null: [
* // {foo: 100, bar: "One", baz: null}
* // {foo: 300, bar: "Three", baz: null}
* // ]
* // }
*
* @example
* const m3 = Convert.arrayToMap(test, "baz", Convert.FORCE_ARRAY);
* // m3 is:
* // {
* // baz2: [{foo: 200, bar: "Two", baz: "baz2"}],
* // baz4: [{foo: 400, bar: "Four", baz: "baz4"}],
* // baz5: [{foo: 500, bar: "Five", baz: "baz5"}],
* // null: [
* // {foo: 100, bar: "One", baz: null},
* // {foo: 300, bar: "Three", baz: null}
* // ]
* // }
* @param {array} arr Array to inspect
* @param {string} prop Property to extract.
* @param {bool} [forceArray=false] When true the map values are always returned as arrays.
*
* @returns {Object}
*/
Convert.arrayToMap = function (arr, prop, forceArray) {
return arr.reduce(function (acc, obj) {
const clone = JSON.parse(JSON.stringify(obj));
if (typeof acc[obj[prop]] !== "undefined") {
// Multiple-values present so force result to an array (unless it's already an array)
if (!Array.isArray(acc[obj[prop]])) {
acc[obj[prop]] = [acc[obj[prop]]];
}
acc[obj[prop]].push(clone);
} else {
acc[obj[[prop]]] = forceArray ? [clone] : clone;
}
return acc;
}, {});
};
/**
* Given an array of strings, returns a map whose keys are the values of the array, with each key pointing to the same given value.
* <code>val</code> can be a simple scaler, array, map etc.
*
* @example
* const test = ["foo", "bar", "baz"];
* const m = Convert.simpleArrayToMap(test, {val: 1});
* // m is:
* // {
* // "foo": {val: 1},
* // "bar": {val: 1},
* // "baz": {val: 1}
* // }
*
* @param {array} arr Array to inspect
* @param {*} val Value to assign to every map element
*
* @returns {Object}
*/
Convert.simpleArrayToMap = function (arr, val) {
return arr.reduce(function (acc, string) {
acc[string] = val;
return acc;
}, {});
};
/**
* Extracts one or more properties from an array of objects and returns them as an array.
* If only a single property is given the return value is an array of scalars, otherwise it's
* an array of objects containing the extracted properties.
*
* @example
* const test = [
* {"foo": 100, "bar": "One", "baz": null},
* {"foo": 200, "bar": "Two", "baz": "baz2"},
* {"foo": 300, "bar": "Three", "baz": null},
* {"foo": 400, "bar": "Four", "baz": "baz4"},
* {"foo": 500, "bar": "Five", "baz": "baz5"}
* ];
*
* @example
* const val1 = Convert.extractFromArray(test, "foo");
* // val1 is:
* // [ 100, 200, 300, 400, 500 ]
*
* @example
* const val2 = Convert.extractFromArray(test, "foo", el => el.foo < 400);
* // val2 is:
* // [ 100, 200, 300 ]
*
* @example
* const val3 = Convert.extractFromArray(test, "bar", el => el.foo < 400);
* // val3 is:
* // [ "One", "Two", "Three" ]
*
* @example
* const val3 = Convert.extractFromArray(test, "bar", el => el.foo < 400, , val => `Value is ${val}`);
* // val3 is:
* // [ "Value is One", "Value is Two", "Value is Three" ]
*
* @example
* const val4 = Convert.extractFromArray(test, ["foo", "bar"], el => el.foo < 400, , val => `Value is ${val}`);
* // val4 is:
* // [
* // {foo: "Value is 100", bar: "Value is One"},
* // {foo: "Value is 200", bar: "Value is Two"},
* // {foo: "Value is 300", bar: "Value is Three"}
* // ]
*
* @param {array} arr Array to process
* @param {string|array} props One or more properties to extract
* @param {function} [predicate] Filter method that is called with the following parameters: element (object), property_name (string) and results_so_far (array).
* It should return true if the property's value is to be included in the extract.
* @param {function} [transform] Method that will be applied to each returned property to transform the returned value.
* The method is called with the extracted value and its row.
* It should return the new property value.
* @param {function} [rename] Method that will be applied to each returned property to remap each property's name (for objects only).
* The method is called with the name of each property.
* It should return the new property name (or null to keep the existing name).
*
* @returns {Array}
*/
Convert.extractFromArray = function (arr, props, predicate, transform, rename) {
const haveArray = Array.isArray(props);
// By default extract from all objects
predicate = predicate || (() => true);
// By default return results verbatim
transform = transform || ((val) => val);
// By default return object keys verbatim
rename = rename || (() => null);
let ret = [];
if (haveArray) {
arr.forEach(function (obj) {
const extract = {};
props.forEach(function (prop) {
if (predicate(obj, prop, ret)) {
const newVal = transform(obj[prop], obj, prop);
const newProp = rename(prop) || prop;
extract[newProp] = newVal;
}
});
if (Object.keys(extract).length !== 0) {
ret.push(extract);
}
});
} else {
arr.forEach(function (obj) {
if (predicate(obj, props, ret)) {
ret.push(transform(obj[props], obj));
}
});
}
return ret;
};
/**
* Extracts a single numeric property from an array of objects and converts them into an array of proportional values in the interval [0, 1].
* If a predicate function is used the proportions are calculated based only on those values that pass the filter.
*
* @example
* const test = [
* {"foo": 100, "bar": "One", "baz": null},
* {"foo": 200, "bar": "Two", "baz": "baz2"},
* {"foo": 300, "bar": "Three", "baz": null},
* {"foo": 400, "bar": "Four", "baz": "baz4"},
* {"foo": 500, "bar": "Five", "baz": "baz5"}
* ];
*
* @example
* const val1 = Convert.extractFromArrayAsProportion(test, "foo");
* // val1 is:
* // [ 0.06666666666666667, 0.13333333333333333, 0.2, 0.26666666666666666, 0.3333333333333333 ]
* // i.e. each value divided by the total (which is 1500)
*
* @example
* // Convert a subset of the raw values to percentages
* const val2 = Convert.extractFromArrayAsProportion(test, "foo", el => el.foo < 400, , val => val * 100);
* // val2 is:
* // [ 16.666666666666668, 33.333333333333336, 50 ]
* // i.e. each value divided by the total (which is 600) multiplied by 100
*
* @param {array} arr Array to process
* @param {string} prop Numeric property to extract
* @param {function} [predicate] Filter method that should true if the property is to be included in the extract
* @param {function} [transform] Method that will be applied to each proportion value
*
* @returns {Array}
*/
Convert.extractFromArrayAsProportion = function (arr, prop, predicate, transform) {
if (Array.isArray(prop)) {
console.error("Array properties are not supported");
return;
}
// By default return results verbatim
transform = transform || ((val) => val);
const raw = Convert.extractFromArray(arr, prop, predicate);
const total = raw.reduce((acc, x) => (acc += x), 0);
return raw.map((x) => transform(x) / total);
};
/**
* Converts the given numeric property in an array of objects into an array of proportional values in the interval [0, 1].
* The given array is modified in-place.
*
* @example
* const test = [
* {"foo": 100, "bar": "One", "baz": null},
* {"foo": 200, "bar": "Two", "baz": "baz2"},
* {"foo": 300, "bar": "Three", "baz": null},
* {"foo": 400, "bar": "Four", "baz": "baz4"},
* {"foo": 500, "bar": "Five", "baz": "baz5"}
* ];
*
* @example
* Convert.toProportion(test, "foo", "newfoo", v => v * 100);
* // test is:
* // [
* // {foo: 100, bar: "One", baz: null, newfoo: 6.666666666666667}
* // {foo: 200, bar: "Two", baz: "baz2", newfoo: 13.333333333333334}
* // {foo: 300, bar: "Three", baz: null, newfoo: 20}
* // {foo: 400, bar: "Four", baz: "baz4", newfoo: 26.666666666666668}
* // {foo: 500, bar: "Five", baz: "baz5", newfoo: 33.33333333333333}
* // ]
* // i.e. each value divided by the total (which is 1500) * 100
*
* @param {array} arr Array to process
* @param {string} prop Original numeric property to extract
* @param {string} [newprop=prop] Name of the new property to contain the proportional values.
* Defaults to the same as the original which means the original values will be overwritten by the proportions.
* @param {function} [transform] Method that will be applied to each proportion value
*
*/
Convert.toProportion = function (arr, prop, newprop, transform) {
if (Array.isArray(prop)) {
console.error("Array properties are not supported");
return;
}
// Default to replacing property
newprop = newprop || prop;
// By default return results verbatim
transform = transform || ((x) => x);
const raw = Convert.extractFromArray(arr, prop);
const total = raw.reduce((acc, x) => (acc += x), 0);
arr.forEach((val, idx) => {
arr[idx][newprop] = transform(arr[idx][prop] / total);
});
};
/**
* ArrayUnique is a predicate function that can be used in the <code>extractFromArray</code> or <code>extractFromArrayAsProportion</code> methods to return only unique values.
*
* @example
* const test = [
* {"foo": 100, "bar": "One"},
* {"foo": 200, "bar": "Two"},
* {"foo": 300, "bar": "One"},
* {"foo": 400, "bar": "Four"},
* {"foo": 500, "bar": "Two"}
* ];
*
* @example
* const val1 = Convert.extractFromArray(test, "bar", Convert.ArrayUnique);
* // val1 is:
* // [ "One", "Two", "Four" ]
*
* @return {boolean}
*/
Convert.ArrayUnique = function (obj, prop, ret) {
const haveObjectArray = ret && ret.length && typeof ret[0] === "object";
if (!haveObjectArray) {
return ret.indexOf(obj[prop]) === -1;
}
return !ret.some((el) => el[prop] == obj[prop]);
};
/**
* Extracts one or more properties from an map of objects and returns them as an object array.
*
* @example
* const test = {
* obj1: { foo: 1, name: "Name 1"}",
* obj2: { foo: 2, name: "Name 2"}",
* obj3: { foo: 3, name: "Name 3"}",
* obj4: { foo: 4, name: "Name 4"}",
* };
*
* @example
* const r1 = Convert.extractFromMap(test, "name");
* // r1 is ["Name 1", "Name 2", "Name 3", "Name 4"]
*
* @example
* const r2 = Convert.extractFromMap(test, "name");
* // r2 is ["obj1", "obj2", "obj3", "obj4"]
*
* @param {object} map Map to process
* @param {string|array} props One or more properties to extract (the special value <code>__key</code> returns the map key)
* @param {function} [predicate] Filter method that returns true if the map entry is to be included
* @param {function} [transform] Method that will be applied to each returned property
*
* @returns {Array} If props is a string then the returned array is an array of strings, else it's an array of objects.
*/
Convert.extractFromMap = function (map, props, predicate, transform) {
const propsIsArray = Array.isArray(props);
// By default return all map entries
predicate = predicate || (() => true);
// By default return results verbatim
transform = transform || ((val) => val);
let ret = [];
Object.keys(map).forEach(function (key) {
if (predicate(key, map[key])) {
if (propsIsArray) {
const obj = {};
props.forEach(function (prop) {
obj[prop] = prop === "__key" ? key : transform(map[key][prop]);
});
ret.push(obj);
} else {
ret.push(transform(map[key][props]));
}
}
});
return ret;
};
/**
* Returns the ordinal suffix for the given numeric value.
*
* @example
* const ord1 = Convert.toOrdinal(1);
* const ord2 = Convert.toOrdinal(2);
* const ord3 = Convert.toOrdinal(12);
* // ord1 is 1st
* // ord2 is 2nd
* // ord3 is 12th
*
* @param {number} i Numer to be converted
*
* @return {string} Ordinal
*
*/
Convert.toOrdinal = function (i) {
const j = i % 10;
const k = i % 100;
if (j === 1 && k !== 11) {
return i + "st";
}
if (j === 2 && k !== 12) {
return i + "nd";
}
if (j === 3 && k !== 13) {
return i + "rd";
}
return i + "th";
};