UNPKG

@etsoo/shared

Version:

TypeScript shared utilities and functions

772 lines (771 loc) 25.1 kB
import { DataTypes } from "./DataTypes"; import isEqual from "lodash.isequal"; import { DateUtils } from "./DateUtils"; String.prototype.addUrlParam = function (name, value, arrayFormat) { return this.addUrlParams({ [name]: value }, arrayFormat); }; String.prototype.addUrlParams = function (data, arrayFormat) { if (typeof data === "string") { let url = this; if (url.includes("?")) { url += "&"; } else { if (!url.endsWith("/")) url = url + "/"; url += "?"; } return url + data; } // Simple check if (typeof URL === "undefined" || !this.includes("://")) { const params = Object.entries(data) .map(([key, value]) => { let v; if (Array.isArray(value)) { if (arrayFormat == null || arrayFormat === false) { return value .map((item) => `${key}=${encodeURIComponent(`${item}`)}`) .join("&"); } else { v = value.join(arrayFormat ? "," : arrayFormat); } } else if (value instanceof Date) { v = value.toJSON(); } else { v = value == null ? "" : `${value}`; } return `${key}=${encodeURIComponent(v)}`; }) .join("&"); return this.addUrlParams(params); } else { const urlObj = new URL(this); Object.entries(data).forEach(([key, value]) => { if (Array.isArray(value)) { if (arrayFormat == null || arrayFormat === false) { value.forEach((item) => { urlObj.searchParams.append(key, `${item}`); }); } else { urlObj.searchParams.append(key, value.join(arrayFormat ? "," : arrayFormat)); } } else if (value instanceof Date) { urlObj.searchParams.append(key, value.toJSON()); } else { urlObj.searchParams.append(key, `${value == null ? "" : value}`); } }); return urlObj.toString(); } }; String.prototype.containChinese = function () { const regExp = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]/g; return regExp.test(this); }; String.prototype.containKorean = function () { const regExp = /[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f\ua960-\ua97f\ud7b0-\ud7ff\u3400-\u4dbf]/g; return regExp.test(this); }; String.prototype.containJapanese = function () { const regExp = /[\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf]/g; return regExp.test(this); }; String.prototype.format = function (...parameters) { let template = this; parameters.forEach((value, index) => { template = template.replace(new RegExp(`\\{${index}\\}`, "g"), value); }); return template; }; String.prototype.formatInitial = function (upperCase = false) { const initial = this.charAt(0); return ((upperCase ? initial.toUpperCase() : initial.toLowerCase()) + this.slice(1)); }; String.prototype.hideData = function (endChar) { if (this.length === 0) return this; if (endChar != null) { const index = this.indexOf(endChar); if (index === -1) return this.hideData(); return this.substring(0, index).hideData() + this.substring(index); } var len = this.length; if (len < 4) return this.substring(0, 1) + "***"; if (len < 6) return this.substring(0, 2) + "***"; if (len < 8) return this.substring(0, 2) + "***" + this.slice(-2); if (len < 12) return this.substring(0, 3) + "***" + this.slice(-3); return this.substring(0, 4) + "***" + this.slice(-4); }; String.prototype.hideEmail = function () { return this.hideData("@"); }; String.prototype.isDigits = function (minLength) { return this.length >= (minLength ?? 0) && /^\d+$/.test(this); }; String.prototype.isEmail = function () { const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(this.toLowerCase()); }; String.prototype.removeNonLetters = function () { return this.replace(/[^a-zA-Z0-9]/g, ""); }; /** * Utilities */ export var Utils; (function (Utils) { const IgnoredProperties = ["changedFields", "id"]; /** * Add blank item to collection * @param options Options * @param idField Id field, default is id * @param labelField Label field, default is label * @param blankLabel Blank label, default is --- */ function addBlankItem(options, idField, labelField, blankLabel) { // Avoid duplicate blank items idField ?? (idField = "id"); if (options.length === 0 || Reflect.get(options[0], idField) !== "") { const blankItem = { [idField]: "", [typeof labelField === "string" ? labelField : "label"]: blankLabel ?? "---" }; options.unshift(blankItem); } return options; } Utils.addBlankItem = addBlankItem; /** * Base64 chars to number * @param base64Chars Base64 chars * @returns Number */ function charsToNumber(base64Chars) { const chars = typeof Buffer === "undefined" ? [...atob(base64Chars)].map((char) => char.charCodeAt(0)) : [...Buffer.from(base64Chars, "base64")]; return chars.reduce((previousValue, currentValue, currentIndex) => { return previousValue + currentValue * Math.pow(128, currentIndex); }, 0); } Utils.charsToNumber = charsToNumber; /** * Correct object's property value type * @param input Input object * @param fields Fields to correct */ function correctTypes(input, fields) { for (const field in fields) { const type = fields[field]; if (type == null) continue; const value = Reflect.get(input, field); const newValue = DataTypes.convertByType(value, type); if (newValue !== value) { Reflect.set(input, field, newValue); } } } Utils.correctTypes = correctTypes; /** * Two values equal * @param v1 Value 1 * @param v2 Value 2 * @param strict Strict level, 0 with ==, 1 === but null equal undefined, 2 === */ function equals(v1, v2, strict = 1) { // Null and undefined case if (v1 == null || v2 == null) { if (strict <= 1 && v1 == v2) return true; return v1 === v2; } // For date, array and object if (typeof v1 === "object") return isEqual(v1, v2); // 1 and '1' case if (strict === 0) return v1 == v2; // Strict equal return v1 === v2; } Utils.equals = equals; /** * Exclude specific items * @param items Items * @param field Filter field * @param excludedValues Excluded values * @returns Result */ function exclude(items, field, ...excludedValues) { return items.filter((item) => !excludedValues.includes(item[field])); } Utils.exclude = exclude; /** * Async exclude specific items * @param items Items * @param field Filter field * @param excludedValues Excluded values * @returns Result */ async function excludeAsync(items, field, ...excludedValues) { const result = await items; if (result == null) return result; return exclude(result, field, ...excludedValues); } Utils.excludeAsync = excludeAsync; /** * Format inital character to lower case or upper case * @param input Input string * @param upperCase To upper case or lower case */ function formatInitial(input, upperCase = false) { return input.formatInitial(upperCase); } Utils.formatInitial = formatInitial; /** * Format string with parameters * @param template Template with {0}, {1}, ... * @param parameters Parameters to fill the template * @returns Result */ function formatString(template, ...parameters) { return template.format(...parameters); } Utils.formatString = formatString; /** * Get data changed fields (ignored changedFields) with input data updated * @param input Input data * @param initData Initial data * @param ignoreFields Ignore fields, default is ['changedFields', 'id'] * @returns */ function getDataChanges(input, initData, ignoreFields) { // Default ignore fields const fields = ignoreFields ?? IgnoredProperties; // Changed fields const changes = []; Object.entries(input).forEach(([key, value]) => { // Ignore fields, no process if (fields.includes(key)) return; // Compare with init value const initValue = Reflect.get(initData, key); if (value == null && initValue == null) { // Both are null, it's equal Reflect.deleteProperty(input, key); return; } if (initValue != null) { // Date when meets string if (value instanceof Date) { if (value.valueOf() === DateUtils.parse(initValue)?.valueOf()) { Reflect.deleteProperty(input, key); return; } changes.push(key); return; } const newValue = DataTypes.convert(value, initValue); if (Utils.equals(newValue, initValue)) { Reflect.deleteProperty(input, key); return; } // Update Reflect.set(input, key, newValue); } // Remove empty property if (value == null || value === "") { Reflect.deleteProperty(input, key); } // Hold the key changes.push(key); }); return changes; } Utils.getDataChanges = getDataChanges; /** * Get nested value from object * @param data Data * @param name Field name, support property chain like 'jsonData.logSize' * @returns Result */ function getNestedValue(data, name) { const properties = name.split("."); const len = properties.length; if (len === 1) { return Reflect.get(data, name); } else { let curr = data; for (let i = 0; i < len; i++) { const property = properties[i]; if (i + 1 === len) { return Reflect.get(curr, property); } else { let p = Reflect.get(curr, property); if (p == null) { return undefined; } curr = p; } } } } Utils.getNestedValue = getNestedValue; /** * Get input function or value result * @param input Input function or value * @param args Arguments * @returns Result */ Utils.getResult = (input, ...args) => { return typeof input === "function" ? input(...args) : input; }; /** * Get time zone * @param tz Default timezone, default is UTC * @returns Timezone */ Utils.getTimeZone = (tz) => { // If Intl supported if (typeof Intl === "object" && typeof Intl.DateTimeFormat === "function") return Intl.DateTimeFormat().resolvedOptions().timeZone; // Default timezone return tz ?? "UTC"; }; /** * Check the input string contains HTML entity or not * @param input Input string * @returns Result */ function hasHtmlEntity(input) { return /&(lt|gt|nbsp|60|62|160|#x3C|#x3E|#xA0);/i.test(input); } Utils.hasHtmlEntity = hasHtmlEntity; /** * Check the input string contains HTML tag or not * @param input Input string * @returns Result */ function hasHtmlTag(input) { return /<\/?[a-z]+[^<>]*>/i.test(input); } Utils.hasHtmlTag = hasHtmlTag; /** * Is digits string * @param input Input string * @param minLength Minimum length * @returns Result */ Utils.isDigits = (input, minLength) => { if (input == null) return false; return input.isDigits(minLength); }; /** * Is email string * @param input Input string * @returns Result */ Utils.isEmail = (input) => { if (input == null) return false; return input.isEmail(); }; /** * Join items as a string * @param items Items * @param joinPart Join string */ Utils.joinItems = (items, joinPart = ", ") => items .reduce((items, item) => { if (item) { const newItem = item.trim(); if (newItem) items.push(newItem); } return items; }, []) .join(joinPart); /** * Merge class names * @param classNames Class names */ Utils.mergeClasses = (...classNames) => Utils.joinItems(classNames, " "); /** * Create a GUID */ function newGUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } Utils.newGUID = newGUID; /** * Number to base64 chars * @param num Input number * @returns Result */ function numberToChars(num) { const codes = []; while (num > 0) { const code = num % 128; codes.push(code); num = (num - code) / 128; } if (typeof Buffer === "undefined") { return btoa(String.fromCharCode(...codes)); } else { const buffer = Buffer.from(codes); return buffer.toString("base64"); } } Utils.numberToChars = numberToChars; /** * Test two objects are equal or not * @param obj1 Object 1 * @param obj2 Object 2 * @param ignoreFields Ignored fields * @param strict Strict level, 0 with ==, 1 === but null equal undefined, 2 === * @returns Result */ function objectEqual(obj1, obj2, ignoreFields = [], strict = 1) { // Unique keys const keys = Utils.objectKeys(obj1, obj2, ignoreFields); for (const key of keys) { // Values const v1 = Reflect.get(obj1, key); const v2 = Reflect.get(obj2, key); if (!Utils.equals(v1, v2, strict)) return false; } return true; } Utils.objectEqual = objectEqual; /** * Get two object's unqiue properties * @param obj1 Object 1 * @param obj2 Object 2 * @param ignoreFields Ignored fields * @returns Unique properties */ function objectKeys(obj1, obj2, ignoreFields = []) { // All keys const allKeys = [...Object.keys(obj1), ...Object.keys(obj2)].filter((item) => !ignoreFields.includes(item)); // Unique keys return new Set(allKeys); } Utils.objectKeys = objectKeys; /** * Get the new object's updated fields contrast to the previous object * @param objNew New object * @param objPre Previous object * @param ignoreFields Ignored fields * @param strict Strict level, 0 with ==, 1 === but null equal undefined, 2 === * @returns Updated fields */ function objectUpdated(objNew, objPrev, ignoreFields = [], strict = 1) { // Fields const fields = []; // Unique keys const keys = Utils.objectKeys(objNew, objPrev, ignoreFields); for (const key of keys) { // Values const vNew = Reflect.get(objNew, key); const vPrev = Reflect.get(objPrev, key); if (!Utils.equals(vNew, vPrev, strict)) { fields.push(key); } } return fields; } Utils.objectUpdated = objectUpdated; /** * Try to parse JSON input to array * @param input JSON input * @param checkValue Type check value * @returns Result */ function parseJsonArray(input, checkValue) { try { if (!input.startsWith("[")) input = `[${input}]`; const array = JSON.parse(input); const type = typeof checkValue; if (Array.isArray(array) && (checkValue == null || !array.some((item) => typeof item !== type))) { return array; } } catch (e) { console.error(`Utils.parseJsonArray ${input} with error`, e); } return; } Utils.parseJsonArray = parseJsonArray; /** * Parse string (JSON) to specific type, no type conversion * When return type depends on parameter value, uses function overloading, otherwise uses conditional type * For type conversion, please use DataTypes.convert * @param input Input string * @param defaultValue Default value * @returns Parsed value */ function parseString(input, defaultValue) { // Undefined and empty case, return default value if (input == null || input === "") return defaultValue; // String if (typeof defaultValue === "string") return input; try { // Date if (defaultValue instanceof Date) { const date = new Date(input); if (date == null) return defaultValue; return date; } // JSON const json = JSON.parse(input); // Return return json; } catch { if (defaultValue == null) return input; return defaultValue; } } Utils.parseString = parseString; /** * Remove empty values (null, undefined, '') from the input object * @param input Input object */ function removeEmptyValues(input) { Object.keys(input).forEach((key) => { const value = Reflect.get(input, key); if (value == null || value === "") { Reflect.deleteProperty(input, key); } }); } Utils.removeEmptyValues = removeEmptyValues; /** * Remove non letters * @param input Input string * @returns Result */ Utils.removeNonLetters = (input) => { return input?.removeNonLetters(); }; /** * Replace null or empty with default value * @param input Input string * @param defaultValue Default value * @returns Result */ Utils.replaceNullOrEmpty = (input, defaultValue) => { if (input == null || input.trim() === "") return defaultValue; return input; }; /** * Set source with new labels * @param source Source * @param labels Labels * @param reference Key reference dictionary */ Utils.setLabels = (source, labels, reference) => { Object.keys(source).forEach((key) => { // Reference key const labelKey = reference == null ? key : reference[key] ?? key; // Label const label = labels[labelKey]; if (label != null) { // If found, update Reflect.set(source, key, label); } }); }; /** * Snake name to works, 'snake_name' to 'Snake Name' * @param name Name text * @param firstOnly Only convert the first word to upper case */ Utils.snakeNameToWord = (name, firstOnly = false) => { const items = name.split("_"); if (firstOnly) { items[0] = items[0].formatInitial(true); return items.join(" "); } return items.map((part) => part.formatInitial(true)).join(" "); }; function getSortValue(n1, n2) { if (n1 === n2) return 0; if (n1 === -1) return 1; if (n2 === -1) return -1; return n1 - n2; } /** * Set nested value to object * @param data Data * @param name Field name, support property chain like 'jsonData.logSize' * @param value Value * @param keepNull Keep null value or not */ function setNestedValue(data, name, value, keepNull) { const properties = name.split("."); const len = properties.length; if (len === 1) { if (value == null && keepNull !== true) { Reflect.deleteProperty(data, name); } else { Reflect.set(data, name, value); } } else { let curr = data; for (let i = 0; i < len; i++) { const property = properties[i]; if (i + 1 === len) { setNestedValue(curr, property, value, keepNull); // Reflect.set(curr, property, value); } else { let p = Reflect.get(curr, property); if (p == null) { p = {}; Reflect.set(curr, property, p); } curr = p; } } } } Utils.setNestedValue = setNestedValue; /** * Parse path similar with node.js path.parse * @param path Input path */ Utils.parsePath = (path) => { // Two formats or mixed // /home/user/dir/file.txt // C:\\path\\dir\\file.txt const lastIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); let root = "", dir = "", base, ext, name; if (lastIndex === -1) { base = path; } else { base = path.substring(lastIndex + 1); const index1 = path.indexOf("/"); const index2 = path.indexOf("\\"); const index = index1 === -1 ? index2 : index2 === -1 ? index1 : Math.min(index1, index2); root = path.substring(0, index + 1); dir = path.substring(0, lastIndex); if (dir === "") dir = root; } const extIndex = base.lastIndexOf("."); if (extIndex === -1) { name = base; ext = ""; } else { name = base.substring(0, extIndex); ext = base.substring(extIndex); } return { root, dir, base, ext, name }; }; /** * Sort array by favored values * @param items Items * @param favored Favored values * @returns Sorted array */ Utils.sortByFavor = (items, favored) => { return items.sort((r1, r2) => { const n1 = favored.indexOf(r1); const n2 = favored.indexOf(r2); return getSortValue(n1, n2); }); }; /** * Sort array by favored field values * @param items Items * @param field Field to sort * @param favored Favored field values * @returns Sorted array */ Utils.sortByFieldFavor = (items, field, favored) => { return items.sort((r1, r2) => { const n1 = favored.indexOf(r1[field]); const n2 = favored.indexOf(r2[field]); return getSortValue(n1, n2); }); }; /** * Trim chars * @param input Input string * @param chars Trim chars * @returns Result */ Utils.trim = (input, ...chars) => { return Utils.trimEnd(Utils.trimStart(input, ...chars), ...chars); }; /** * Trim end chars * @param input Input string * @param chars Trim chars * @returns Result */ Utils.trimEnd = (input, ...chars) => { let char; while ((char = chars.find((char) => input.endsWith(char))) != null) { input = input.substring(0, input.length - char.length); } return input; }; /** * Trim start chars * @param input Input string * @param chars Trim chars * @returns Result */ Utils.trimStart = (input, ...chars) => { let char; while ((char = chars.find((char) => input.startsWith(char))) != null) { input = input.substring(char.length); } return input; }; })(Utils || (Utils = {}));