packages/base/src/util.js
import merge from 'deepmerge'; // default
/**
* Make a recursive function that will only run to a given depth
* and switches to an alternative function at that depth. \
* No limitation if `n` is `undefined` (Just wraps `f` in that case).
*
* @param { number | undefined } n Allowed depth of recursion. `undefined` for no limitation.
* @param { Function } f Function that accepts recursive callback as the first argument.
* @param { Function } [g] Function to run instead, when maximum depth was reached. Do nothing by default.
* @returns { Function }
*/
function limitedDepthRecursive (n, f, g = () => undefined) {
if (n === undefined) {
const f1 = function (...args) { return f(f1, ...args); };
return f1;
}
if (n >= 0) {
return function (...args) { return f(limitedDepthRecursive(n - 1, f, g), ...args); };
}
return g;
}
/**
* Return the same string or a substring with
* the given character occurrences removed from each side.
*
* @param { string } str A string to trim.
* @param { string } char A character to be trimmed.
* @returns { string }
*/
function trimCharacter (str, char) {
let start = 0;
let end = str.length;
while (start < end && str[start] === char) { ++start; }
while (end > start && str[end - 1] === char) { --end; }
return (start > 0 || end < str.length)
? str.substring(start, end)
: str;
}
/**
* Return the same string or a substring with
* the given character occurrences removed from the end only.
*
* @param { string } str A string to trim.
* @param { string } char A character to be trimmed.
* @returns { string }
*/
function trimCharacterEnd (str, char) {
let end = str.length;
while (end > 0 && str[end - 1] === char) { --end; }
return (end < str.length)
? str.substring(0, end)
: str;
}
/**
* Return a new string will all characters replaced with unicode escape sequences.
* This extreme kind of escaping can used to be safely compose regular expressions.
*
* @param { string } str A string to escape.
* @returns { string } A string of unicode escape sequences.
*/
function unicodeEscape (str) {
return str.replace(/[\s\S]/g, c => '\\u' + c.charCodeAt().toString(16).padStart(4, '0'));
}
/**
* Deduplicate an array by a given key callback.
* Item properties are merged recursively and with the preference for last defined values.
* Of items with the same key, merged item takes the place of the last item,
* others are omitted.
*
* @param { any[] } items An array to deduplicate.
* @param { (x: any) => string } getKey Callback to get a value that distinguishes unique items.
* @returns { any[] }
*/
function mergeDuplicatesPreferLast (items, getKey) {
const map = new Map();
for (let i = items.length; i-- > 0;) {
const item = items[i];
const key = getKey(item);
map.set(
key,
(map.has(key))
? merge(item, map.get(key), { arrayMerge: overwriteMerge })
: item
);
}
return [...map.values()].reverse();
}
const overwriteMerge = (acc, src, options) => [...src];
/**
* Get a nested property from an object.
*
* @param { object } obj The object to query for the value.
* @param { string[] } path The path to the property.
* @returns { any }
*/
function get (obj, path) {
for (const key of path) {
if (!obj) { return undefined; }
obj = obj[key];
}
return obj;
}
/**
* Convert a number into alphabetic sequence representation (Sequence without zeroes).
*
* For example: `a, ..., z, aa, ..., zz, aaa, ...`.
*
* @param { number } num Number to convert. Must be >= 1.
* @param { string } [baseChar = 'a'] Character for 1 in the sequence.
* @param { number } [base = 26] Number of characters in the sequence.
* @returns { string }
*/
function numberToLetterSequence (num, baseChar = 'a', base = 26) {
const digits = [];
do {
num -= 1;
digits.push(num % base);
num = (num / base) >> 0; // quick `floor`
} while (num > 0);
const baseCode = baseChar.charCodeAt(0);
return digits
.reverse()
.map(n => String.fromCharCode(baseCode + n))
.join('');
}
const I = ['I', 'X', 'C', 'M'];
const V = ['V', 'L', 'D'];
/**
* Convert a number to it's Roman representation. No large numbers extension.
*
* @param { number } num Number to convert. `0 < num <= 3999`.
* @returns { string }
*/
function numberToRoman (num) {
return [...(num) + '']
.map(n => +n)
.reverse()
.map((v, i) => ((v % 5 < 4)
? (v < 5 ? '' : V[i]) + I[i].repeat(v % 5)
: I[i] + (v < 5 ? V[i] : I[i + 1])))
.reverse()
.join('');
}
export {
get,
limitedDepthRecursive,
mergeDuplicatesPreferLast,
numberToLetterSequence,
numberToRoman,
trimCharacter,
trimCharacterEnd,
unicodeEscape
};