@slck/utils
Version:
utils library - Utility functions for common development.
589 lines • 20.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isAnyRecordWithEmptyValues = exports.hasValidDateFn = exports.trimObjectValues = exports.isEndpointConfig = exports.addSpacesToCamelCase = exports.constructTreeRecursively = exports.convertFirstLetterToUpper = exports.convertMinutesToTimeText = exports.checkObjectPropValueExistsInCollection = exports.shiftToFristWith = exports.compareObjectArraysWithTypeSafe = exports.genericObjectTypeFn = exports.objectDifferenceByProps = exports.camelCaseKeysHelper = exports.toCamelCaseKeys = exports.isEmptyInDepth = exports.isNullOrUndefinedEmpty = exports.hasValidLength = exports.isEmpty = exports.remainingDaysHoursFormSeconds = exports.remainingDaysHoursFormTwoDates = exports.daysTimeFromSeconds = exports.objectNonShadowCopy = exports.leadZeroForMonthOrDay = exports.isObject = exports.isDate = exports.isNullOrUndefined = void 0;
exports.numberToText = numberToText;
exports.extractCommonAndDifferentValues = extractCommonAndDifferentValues;
const lodash_1 = require("lodash");
const generics_1 = require("./generics");
/**
* verifies object is null or undefined, if 'yes' return true.
* @param value type any
* @returns boolean
*/
const isNullOrUndefined = (value) => {
return value === null || value === undefined;
};
exports.isNullOrUndefined = isNullOrUndefined;
/**
*
* @param value
* @returns
*/
const isDate = (value) => {
try {
return !isNaN(new Date(value).getTime());
}
catch (e) {
return false;
}
};
exports.isDate = isDate;
/**
*
* @param value
* @returns
*/
const isObject = (value) => {
return !(0, exports.isNullOrUndefined)(value) ? typeof value === 'object' : false;
};
exports.isObject = isObject;
/**
* Add leading '0' to the either to month or day
* @param value type number
* @returns number or string
*/
const leadZeroForMonthOrDay = (value) => {
return value < 10 ? `0${value}` : value;
};
exports.leadZeroForMonthOrDay = leadZeroForMonthOrDay;
/**
*
* @param value
* @returns
*/
const objectNonShadowCopy = (value) => {
return JSON.parse(JSON.stringify(value));
};
exports.objectNonShadowCopy = objectNonShadowCopy;
/**
* Calculate number of days, hours, minutes and seconds remaining for given seconds
* @param value type 'number'
* @returns an object consists number of days, hours, minutes and seconds remaining
*/
const daysTimeFromSeconds = (seconds) => {
return {
days: Math.floor(seconds / 86400),
hours: Math.floor(seconds / 3600) % 24,
minutes: Math.floor(seconds / 60) % 60,
seconds: seconds % 60,
};
};
exports.daysTimeFromSeconds = daysTimeFromSeconds;
/**
*
* @param value
* @returns
*/
const remainingDaysHoursFormTwoDates = (startDate, finish) => {
try {
if (finish.getTime() > startDate.getTime()) {
return (0, exports.daysTimeFromSeconds)((finish.getTime() - startDate.getTime()) / 1000);
}
else {
return null;
}
}
catch (error) {
return null;
}
};
exports.remainingDaysHoursFormTwoDates = remainingDaysHoursFormTwoDates;
/**
*
* @param value
* @returns
*/
const remainingDaysHoursFormSeconds = (seconds) => {
try {
return (0, exports.daysTimeFromSeconds)(seconds);
}
catch (error) {
return null;
}
};
exports.remainingDaysHoursFormSeconds = remainingDaysHoursFormSeconds;
/**
* verifies object length equals to 0, if 'yes' return true.
* @param value type any
* @returns boolean
*/
const isEmpty = (value) => {
// we don't check for string here so it also works with arrays
return value == null || value.length === 0;
};
exports.isEmpty = isEmpty;
const hasValidLength = (value) => {
// non-strict comparison is intentional, to check for both `null` and `undefined` values
return value != null && typeof value.length === 'number';
};
exports.hasValidLength = hasValidLength;
/**
* verifies object is null or undefined and length equals to 0, if 'yes' return true.
* @param value type any
* @returns boolean
*/
const isNullOrUndefinedEmpty = (value) => {
return (0, exports.isNullOrUndefined)(value) || (0, exports.isEmpty)(value);
};
exports.isNullOrUndefinedEmpty = isNullOrUndefinedEmpty;
/**
* verifies object is empty & it's props, if 'yes' return true.
* @param value type any
* @returns boolean
*/
const isEmptyInDepth = (value) => {
if ((0, exports.isNullOrUndefined)(value)) {
return true;
}
else {
let emptyValues = 0;
Object.entries(value).forEach(([key, v]) => {
switch (typeof v) {
case 'boolean':
emptyValues += v === false ? 1 : 0;
break;
case 'string':
emptyValues += v.length <= 0 ? 1 : 0;
break;
case 'object':
if ((0, exports.isNullOrUndefined)(v)) {
emptyValues += 1;
}
else {
emptyValues += Object.entries(value).length <= 0 ? 1 : 0;
}
break;
}
});
return Object.entries(value).length === emptyValues;
}
};
exports.isEmptyInDepth = isEmptyInDepth;
/**
*
* @param obj
* @returns camelCase notation object
*/
const toCamelCaseKeys = (obj) => {
return (0, lodash_1.isArray)(obj)
? obj.map((o) => (0, exports.toCamelCaseKeys)(o))
: (0, exports.camelCaseKeysHelper)(obj);
};
exports.toCamelCaseKeys = toCamelCaseKeys;
const camelCaseKeysHelper = (obj) => {
const entries = Object.entries(obj);
const mappedEntries = entries.map(([k, v]) => [
`${k.slice(0, 1).toLowerCase()}${k.slice(1)}`,
(0, exports.isObject)(v) ? (0, exports.toCamelCaseKeys)(v) : v,
]);
return Object.fromEntries(mappedEntries);
};
exports.camelCaseKeysHelper = camelCaseKeysHelper;
/** difference between two objects
* return an array object with differed property its source object
* and its destination object values respectively
*/
const objectDifferenceByProps = (sourceObject, destinationObject) => {
const diffProps = [];
if ((0, exports.isNullOrUndefinedEmpty)(sourceObject) &&
(0, exports.isNullOrUndefinedEmpty)(destinationObject)) {
return diffProps;
}
for (const prop in sourceObject) {
if (Object.prototype.hasOwnProperty.call(sourceObject, prop) &&
Object.prototype.hasOwnProperty.call(destinationObject, prop)) {
switch (typeof sourceObject[prop]) {
case 'object':
(0, exports.objectDifferenceByProps)(sourceObject[prop], destinationObject[prop]);
break;
default:
if (sourceObject[prop] !== destinationObject[prop]) {
diffProps.push({
property: prop,
sourceValue: sourceObject[prop],
destinationValue: destinationObject[prop],
});
}
break;
}
}
}
return diffProps;
};
exports.objectDifferenceByProps = objectDifferenceByProps;
const genericObjectTypeFn = (key, rValue) => ({ [key]: rValue });
exports.genericObjectTypeFn = genericObjectTypeFn;
/**
*
* @param arr1
* @param arr2
* @returns { result: boolean; error: ErrorType }
*/
const compareObjectArraysWithTypeSafe = (arr1, arr2) => {
if (arr1.length !== arr2.length) {
return {
result: false,
error: `Array lengths do not match`,
};
}
for (let i = 0; i < arr1.length; i++) {
const obj1 = arr1[i];
const obj2 = arr2[i];
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return {
result: false,
error: `Object at index ${i} has a different number of keys.`,
};
}
for (const key in obj1) {
if (!(key in obj2)) {
return {
result: false,
error: `Key "${key}" in object at index ${i} does not exist in the other object.`,
};
}
if (typeof obj1[key] !== typeof obj2[key]) {
return {
result: false,
error: `Key "${key}" in object at index ${i} has a type mismatch.`,
};
}
}
}
return { result: true, error: null };
};
exports.compareObjectArraysWithTypeSafe = compareObjectArraysWithTypeSafe;
/**
* @description bring the element to the first by searchWith
* @param items Type of array
* @param key key of the object inside the array
* @param searchWith search key either string, number or boolean
* @param isConvertStringToLowerCase by default is true
* @returns modified object array
*/
const shiftToFristWith = (items, key, searchWith, isConvertStringToLowerCase = true) => {
items.forEach((value, index) => {
switch (typeof value[key]) {
case 'string':
if (isConvertStringToLowerCase
? value[key].toLowerCase() ===
searchWith.toLowerCase()
: value[key] === searchWith) {
items.splice(index, 1);
items.unshift(value);
}
break;
case 'boolean':
if (value[key] === searchWith) {
items.splice(index, 1);
items.unshift(value);
}
break;
case 'number':
if (value[key] === searchWith) {
items.splice(index, 1);
items.unshift(value);
}
break;
default:
break;
}
});
return items;
};
exports.shiftToFristWith = shiftToFristWith;
/**
* @description Return `true` if the property values match in the collection of objects.
* @param object T
* @param collection T[]
* @param prop key of T
* @returns boolean
*/
const checkObjectPropValueExistsInCollection = (object, collection, prop) => collection.some((c) => c[prop] === object[prop]);
exports.checkObjectPropValueExistsInCollection = checkObjectPropValueExistsInCollection;
/**
*
* @param minutes number of minutes
* @param unitOfHours like 'h', 'hrs'
* @param unitOfminutes like 'min', 'm'
* @returns
*/
const convertMinutesToTimeText = (minutes, unitOfHours, unitOfminutes) => {
return `${Math.floor(minutes / 60)} ${unitOfHours} ${Math.floor(minutes % 60)} ${unitOfminutes}`;
};
exports.convertMinutesToTimeText = convertMinutesToTimeText;
/**
*
* @param text
* @param seperator
* @returns
*/
const convertFirstLetterToUpper = (text, seperator = ' ') => {
return text
? text
.split(`${seperator}`)
.map((s) => (s ? s[0].toUpperCase() : ''))
.join('')
: ``;
};
exports.convertFirstLetterToUpper = convertFirstLetterToUpper;
/**
*
* @param data is a collection of T
* @param childrenKey children key property in K
* @param valueKey value key which used for split or run logic
* @param valueKeyForTree value to holds the conversion value from valueKey
* @param delimiter string spearator
* @returns a collection of K
*/
const constructTreeRecursively = (data, childrenKey, valueKey, valueKeyForTree, delimiter = '.') => {
const root = [];
data.forEach((obj) => {
const parts = obj[valueKey].split(delimiter);
addPathToTreeRecursively(parts, obj, root, childrenKey, valueKeyForTree);
});
return root;
};
exports.constructTreeRecursively = constructTreeRecursively;
const addPathToTreeRecursively = (parts, rootobjectReference, nodeList, childrenKey, valueKey, delimiter = '.') => {
if (parts.length === 0) {
return;
}
const [current, ...rest] = parts;
// Find the current node in the existing tree
let node = nodeList.find((n) => n[valueKey] === current);
if (!node) {
// If the node doesn't exist, create it
node = Object.assign(Object.assign({}, rootobjectReference), { [valueKey]: current, [childrenKey]: [] });
nodeList.push(node);
}
const children = node[childrenKey];
// Recurse for the rest of the parts
addPathToTreeRecursively(rest, rootobjectReference, children, childrenKey, valueKey, delimiter);
};
const addSpacesToCamelCase = (input) => {
return input.replace(/([A-Z])/g, ' $1').trim();
};
exports.addSpacesToCamelCase = addSpacesToCamelCase;
const isEndpointConfig = (value) => {
return (typeof value === 'object' &&
typeof value.uri === 'string' &&
['GET', 'POST', 'DELETE', 'PUT', 'PATCH'].includes(value.verb) &&
(value.pathParams === undefined ||
(Array.isArray(value.pathParams) &&
value.pathParams.every((param) => typeof param.key === 'string' &&
Object.prototype.hasOwnProperty.call(param, 'value')))) &&
(value.queryParams === undefined ||
(Array.isArray(value.queryParams) &&
value.queryParams.every((param) => typeof param.key === 'string' &&
Object.prototype.hasOwnProperty.call(param, 'value')))) &&
(value.body === undefined ||
typeof value.body === 'object' ||
typeof value.body === 'string' ||
value.body === null));
};
exports.isEndpointConfig = isEndpointConfig;
const trimObjectValues = (obj, seen = new WeakSet()) => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (seen.has(obj)) {
return obj;
}
seen.add(obj);
const result = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
if (typeof value === 'string') {
result[key] = value.trim();
}
else if (typeof value === 'object' && value !== null) {
result[key] = (0, exports.trimObjectValues)(value, seen);
}
else {
result[key] = value;
}
}
}
return result;
};
exports.trimObjectValues = trimObjectValues;
/**
*
* @param date
* @returns
*/
const hasValidDateFn = (date) => {
return date !== '0001-01-01T00:00:00';
};
exports.hasValidDateFn = hasValidDateFn;
/**
* Converts a number to its English text representation
* @param num The number to convert (must be an integer between -1e18 and 1e18)
* @returns The textual representation of the number
* @throws {Error} If the number is too large, not finite, or not a safe integer
*/
function numberToText(num) {
// Input validation
if (!Number.isFinite(num)) {
throw new Error('Input must be a finite number');
}
if (!Number.isSafeInteger(num)) {
throw new Error('Input must be a safe integer');
}
if (Math.abs(num) > 1e18) {
throw new Error('Number too large - maximum supported absolute value is 1e18');
}
if (num === 0)
return 'zero';
if (num < 0)
return `minus ${numberToText(-num)}`;
let remaining = num;
let result = '';
// Handle large scales (thousands, millions, etc.)
for (const scale of generics_1.SCALES) {
if (remaining >= scale.value) {
const scaleAmount = Math.floor(remaining / scale.value);
result += `${convertLessThanThousand(scaleAmount)} ${scale.name}`;
remaining %= scale.value;
if (remaining > 0) {
result += remaining < 100 ? ' and ' : ', ';
}
}
}
// Handle the remaining part (less than 1000)
if (remaining > 0) {
result += convertLessThanThousand(remaining);
}
return result.trim();
}
/**
* Converts a number less than 1000 to text
* @param num The number to convert (0 < num < 1000)
* @returns The textual representation
*/
function convertLessThanThousand(num) {
let result = '';
const hundreds = Math.floor(num / 100);
const remainder = num % 100;
if (hundreds > 0) {
result += `${generics_1.UNITS[hundreds]} hundred`;
if (remainder > 0) {
result += ` and `;
}
}
if (remainder > 0) {
result += convertLessThanHundred(remainder);
}
return result;
}
/**
* Converts a number less than 100 to text
* @param num The number to convert (0 < num < 100)
* @returns The textual representation
*/
function convertLessThanHundred(num) {
if (num < 10) {
return generics_1.UNITS[num];
}
if (num < 20) {
return generics_1.TEENS[num - 10];
}
const tens = Math.floor(num / 10);
const units = num % 10;
return units === 0 ? generics_1.TENS[tens] : `${generics_1.TENS[tens]}-${generics_1.UNITS[units]}`;
}
/**
* Compares a list of objects and returns their deeply nested differences and similarities.
*
* This function supports:
* - Recursive comparison of nested objects
* - Array value comparison
* - Optional key exclusion via `options.skipKeys`
*
* @param objects - An array of objects to be compared (minimum two recommended).
* @param options - Configuration options for comparison.
* - skipKeys: Keys to ignore during comparison (e.g. timestamps, metadata, etc.)
*
* @returns An object with two keys:
* - `same`: Keys and values that are identical across all input objects.
* - `diff`: Keys with values that differ between objects.
*
* @example
* compareDeepDifferencesAcrossObjects(
* [
* { name: "Alice", tags: ["admin"], info: { age: 30 } },
* { name: "Alice", tags: ["admin"], info: { age: 31 } }
* ],
* { skipKeys: ["tags"] }
* )
* // => {
* // same: { name: "Alice" },
* // diff: { info: { age: [30, 31] } }
* // }
*/
function extractCommonAndDifferentValues(objects, options = {}) {
if (!objects || objects.length < 2)
return { same: {}, diff: {} };
const { skipKeys = [], compareKeys, ignoreArrayOrder = false } = options;
const same = {};
const diff = {};
const allKeys = Array.from(new Set(objects.flatMap((obj) => extractKeys(obj)))).filter((key) => {
if (compareKeys)
return compareKeys.includes(key);
return !skipKeys.includes(key);
});
for (const key of allKeys) {
const values = objects.map((obj) => getByPath(obj, key));
const allEqual = values.every((val) => isEqualWithArrays(val, values[0], ignoreArrayOrder));
setByPath(allEqual ? same : diff, key, values[0]);
}
return { same, diff };
}
function extractKeys(obj, prefix = '') {
if (typeof obj !== 'object' || obj === null)
return [];
return Object.entries(obj).flatMap(([key, val]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
return extractKeys(val, fullKey);
}
return fullKey;
});
}
function getByPath(obj, path) {
return path.split('.').reduce((o, k) => (o ? o[k] : undefined), obj);
}
function setByPath(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
current[k] = current[k] || {};
current = current[k];
}
current[keys[keys.length - 1]] = value;
}
function isEqualWithArrays(a, b, ignoreOrder) {
if (Array.isArray(a) && Array.isArray(b)) {
if (ignoreOrder) {
return (a.length === b.length &&
[...a].sort().toString() === [...b].sort().toString());
}
return JSON.stringify(a) === JSON.stringify(b);
}
if (typeof a === 'object' && typeof b === 'object') {
return JSON.stringify(a) === JSON.stringify(b);
}
return a === b;
}
const isAnyRecordWithEmptyValues = (records, compareKeys, skipKeys = []) => {
return records.some((record) => {
return compareKeys
.filter((key) => !skipKeys.includes(key))
.some((key) => (0, exports.isEmptyInDepth)(record[key]));
});
};
exports.isAnyRecordWithEmptyValues = isAnyRecordWithEmptyValues;
//# sourceMappingURL=utils.js.map