@etsoo/shared
Version:
TypeScript shared utilities and functions
772 lines (771 loc) • 25.1 kB
JavaScript
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 = {}));