UNPKG

@rzl-zone/utils-js

Version:

A modern, lightweight set of JavaScript utility functions with TypeScript support for everyday development, crafted to enhance code readability and maintainability.

588 lines (575 loc) 25.5 kB
/*! * ==================================================== * Rzl Utils-JS. * ---------------------------------------------------- * Version: 3.11.0. * Author: Rizalvin Dwiky. * Repository: https://github.com/rzl-zone/utils-js. * ==================================================== */ import { normalizeSpaces } from './chunk-ZVWZEGQP.js'; import { parseCurrencyString } from './chunk-2XSZ2ANI.js'; import { isFinite } from './chunk-CCJ2MSN7.js'; import { isInteger } from './chunk-WVSPXFTY.js'; import { isEmptyString } from './chunk-ULQPCIA2.js'; import { assertIsString } from './chunk-3T6VSWYX.js'; import { safeStableStringify, isDate } from './chunk-AXDYWO67.js'; import { isString, getPreciseType, assertIsPlainObject, hasOwnProp, isBoolean, isPlainObject, isNaN, isUndefined, isFunction, assertIsBoolean, isNil, isNumber, isNonEmptyString } from './chunk-MSUW5VHZ.js'; import { isSupportedCountry, AsYouType } from 'libphonenumber-js/max'; import { id, enUS } from 'date-fns/locale'; import { parse, format } from 'date-fns'; var formatIndianNumber = (numStr, separator) => { const lastThree = numStr.slice(-3); const rest = numStr.slice(0, -3); if (!rest) return lastThree; return rest.replace(/\B(?=(\d{2})+(?!\d))/g, separator) + separator + lastThree; }; var formatCurrency = (value, options = {}) => { if (!isString(value) && !isFinite(value)) { throw new TypeError( `First parameter (\`value\`) must be of type \`string\` or \`primitive-number\`, but received: \`${getPreciseType( value )}\`, with value: \`${safeStableStringify(value, { keepUndefined: true })}\`.` ); } assertIsPlainObject(options, { message: ({ currentType, validType }) => `Second parameter (\`options\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); const decimal = hasOwnProp(options, "decimal") ? options.decimal : false; const totalDecimal = hasOwnProp(options, "totalDecimal") ? options.totalDecimal : 2; const endDecimal = hasOwnProp(options, "endDecimal") ? options.endDecimal : true; const indianFormat = hasOwnProp(options, "indianFormat") ? options.indianFormat : false; const suffixCurrency = hasOwnProp(options, "suffixCurrency") ? options.suffixCurrency : ""; const suffixDecimal = hasOwnProp(options, "suffixDecimal") ? options.suffixDecimal : ""; const roundedDecimal = hasOwnProp(options, "roundedDecimal") ? options.roundedDecimal : "round"; const negativeFormat = hasOwnProp(options, "negativeFormat") ? options.negativeFormat : "dash"; let separatorDecimals = hasOwnProp(options, "separatorDecimals") ? options.separatorDecimals : ","; let separator = hasOwnProp(options, "separator") ? options.separator : "."; if (!isString(separator) || !isString(separatorDecimals) || !isString(suffixCurrency) || !isString(suffixDecimal)) { throw new TypeError( `Parameter \`separator\`, \`separatorDecimals\`, \`suffixCurrency\` and \`suffixDecimal\` property of the \`options\` (second parameter) must be of type \`string\`, but received: ['separator': \`${getPreciseType( separator )}\`, 'separatorDecimals': \`${getPreciseType( separatorDecimals )}\`, 'suffixCurrency': \`${getPreciseType( suffixCurrency )}\`, 'suffixDecimal': \`${getPreciseType(suffixDecimal)}\`].` ); } if (!isBoolean(decimal) || !isBoolean(endDecimal) || !isBoolean(indianFormat)) { throw new TypeError( `Parameter \`decimal\`, \`endDecimal\` and \`indianFormat\` property of the \`options\` (second parameter) must be of type \`boolean\`, but received: ['decimal': \`${getPreciseType( decimal )}\`, 'endDecimal': \`${getPreciseType( endDecimal )}\`, 'indianFormat': \`${getPreciseType(indianFormat)}\`].` ); } if (!isInteger(totalDecimal)) { throw new TypeError( `Parameter \`totalDecimal\` property of the \`options\` (second parameter) must be of type \`integer-number\`, but received: \`${getPreciseType( totalDecimal )}\`, with value: \`${safeStableStringify(length, { keepUndefined: true })}\`.` ); } if (!(roundedDecimal === false || roundedDecimal === "round" || roundedDecimal === "ceil" || roundedDecimal === "floor")) { throw new TypeError( `Parameter \`roundedDecimal\` property of the \`options\` (second parameter) must be of type \`false\` or \`string\` must be one of "round" | "ceil" | "floor", but received: \`${getPreciseType( roundedDecimal )}\`, with value: \`${safeStableStringify(roundedDecimal, { keepUndefined: true })}\`.` ); } if (!(negativeFormat === "abs" || negativeFormat === "brackets" || negativeFormat === "dash" || isPlainObject(negativeFormat))) { throw new TypeError( `Parameter \`negativeFormat\` property of the \`options\` (second parameter) must be of type \`string\` must be one of "abs" | "brackets" | "dash" or \`plain-object\` type, but received: \`${getPreciseType( negativeFormat )}\`, with value: \`${safeStableStringify(negativeFormat, { keepUndefined: true })}\`.` ); } const rawNum = isString(value) ? parseCurrencyString(value) : value; if (isNaN(rawNum)) { throw new TypeError( `First parameter (\`value\`) could not be parsed into a valid \`number\`.` ); } let integerPart = ""; let decimalPartRaw = ""; let num = Math.abs(rawNum); const factor = Math.pow(10, totalDecimal); if (roundedDecimal) { const scaled = num * factor; switch (roundedDecimal) { case "round": num = Math.round(scaled) / factor; break; case "ceil": num = Math.ceil(scaled) / factor; break; case "floor": num = Math.floor(scaled) / factor; break; } } if (roundedDecimal) { [integerPart, decimalPartRaw] = num.toFixed(totalDecimal).split("."); decimalPartRaw = decimalPartRaw ?? "".padEnd(totalDecimal, "0"); } else { const split = String(num).split("."); integerPart = split[0]; decimalPartRaw = (split[1] || "").slice(0, totalDecimal).padEnd(totalDecimal, "0"); } let formattedInteger; if (indianFormat) { separator = ","; separatorDecimals = "."; formattedInteger = (suffixCurrency.trim().length ? suffixCurrency : "") + formatIndianNumber(integerPart, separator); } else { formattedInteger = (suffixCurrency.trim().length ? suffixCurrency : "") + integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator); } if (decimal && !isUndefined(decimalPartRaw) && totalDecimal > 0) { let formattedDecimal = separatorDecimals + decimalPartRaw; if (endDecimal) formattedDecimal += suffixDecimal; formattedInteger += formattedDecimal; } if (rawNum < 0) { if (negativeFormat === "dash") { formattedInteger = "-" + formattedInteger; } else if (negativeFormat === "brackets") { formattedInteger = "(" + formattedInteger + ")"; } else if (negativeFormat === "abs") ; else if (isPlainObject(negativeFormat)) { if (hasOwnProp(negativeFormat, "custom")) { const formatCustomNegative = negativeFormat.custom; if (!isFunction(formatCustomNegative)) { throw new TypeError( `Parameter \`negativeFormat.custom\` property of the \`options\` (second parameter) must be of type function: \`(formatted: string) => string\`, but received: \`${getPreciseType( formatCustomNegative )}\`.` ); } const result = formatCustomNegative(formattedInteger); assertIsString(result, { message: ({ currentType, validType }) => `Parameter \`negativeFormat.custom\` property of the \`options\` (second parameter) expected return a \`${validType}\` type value, but received: \`${currentType}\`.` }); formattedInteger = result; } else { const formatStyleNegative = negativeFormat.style || "dash"; const formatSpaceNegative = !isUndefined(negativeFormat.space) ? negativeFormat.space : false; assertIsBoolean(formatSpaceNegative, { message: ({ currentType, validType }) => `Parameter \`negativeFormat.space\` property of the \`options\` (second parameter) must be of type \`${validType} or undefined\`, but received: \`${currentType}\`.` }); if (!(formatStyleNegative === "abs" || formatStyleNegative === "brackets" || formatStyleNegative === "dash")) { throw new TypeError( `Parameter \`negativeFormat.style\` property of the \`options\` (second parameter) must be of type \`string\` must be of type "abs" | "brackets" | "dash", but received: \`${getPreciseType( formatStyleNegative )}\`, with value: \`${safeStableStringify(formatStyleNegative, { keepUndefined: true })}\`.` ); } switch (formatStyleNegative) { case "dash": formattedInteger = "-" + (formatSpaceNegative ? " " : "") + formattedInteger; break; case "brackets": formattedInteger = formatSpaceNegative ? `( ${formattedInteger} )` : `(${formattedInteger})`; break; } } } } return formattedInteger; }; var formatNumber = (value, separator = ",") => { if (!isString(value) && !isFinite(value)) { throw new TypeError( `First parameter (\`value\`) must be of type \`string\` or \`primitive number\`, but received: \`${getPreciseType( value )}\`.` ); } if (!isString(separator)) { throw new TypeError( `Second parameter (\`separator\`) must be of type \`string\` or empty as \`undefined\`, but received: \`${getPreciseType( separator )}\`.` ); } separator = isString(separator) ? separator : ","; const decimalSeparator = separator === "." ? "," : separator === "," ? "." : "."; const stringValue = value.toString().trim(); const lastDot = stringValue.lastIndexOf("."); const lastComma = stringValue.lastIndexOf(","); let actualDecimal = ""; if (lastDot > lastComma) { actualDecimal = "."; } else if (lastComma > lastDot) { actualDecimal = ","; } let integerPart = stringValue; let decimalPart = ""; if (actualDecimal) { const parts = stringValue.split(actualDecimal); integerPart = parts.slice(0, -1).join(actualDecimal); decimalPart = parts.slice(-1)[0]; } integerPart = integerPart.replace(/[^\d]/g, ""); const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator); return decimalPart ? `${formattedInteger}${decimalSeparator}${decimalPart}` : formattedInteger; }; var parsingAsYouType = (value, defaultCountry) => { let parsed; try { parsed = new AsYouType(defaultCountry); parsed.input(value); return parsed; } catch { parsed?.reset(); return void 0; } }; var isValidParseAsYouType = (parsedAsYouType) => { const parsed = !!parsedAsYouType?.isValid() && !!parsedAsYouType.getNumber(); return parsed; }; function formatPhoneNumber(value, options = {}) { if (isNil(value)) return ""; if (!isString(value) && !isNumber(value)) { throw new TypeError( `First parameter (\`value\`) must be of type \`string\`, \`number\`, \`null\` or \`undefined\`, but received: \`${getPreciseType( value )}\`.` ); } assertIsPlainObject(options, { message: ({ currentType, validType }) => `Second parameter (\`options\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); const takeNumberOnly = hasOwnProp(options, "takeNumberOnly") ? options.takeNumberOnly : false; const checkValidOnly = hasOwnProp(options, "checkValidOnly") ? options.checkValidOnly : false; const defaultCountry = hasOwnProp( options, "defaultCountry" ) ? options.defaultCountry : void 0; const separator = hasOwnProp(options, "separator") ? options.separator : " "; const prependPlusCountryCode = hasOwnProp(options, "prependPlusCountryCode") ? options.prependPlusCountryCode : true; const outputFormat = hasOwnProp(options, "outputFormat") ? options.outputFormat : "INTERNATIONAL"; const openingNumberCountry = hasOwnProp(options, "openingNumberCountry") ? options.openingNumberCountry : ""; const closingNumberCountry = hasOwnProp(options, "closingNumberCountry") ? options.closingNumberCountry : ""; if (!isBoolean(takeNumberOnly) || !isBoolean(checkValidOnly) || !isBoolean(prependPlusCountryCode)) { throw new TypeError( `Parameter \`takeNumberOnly\`, \`checkValidOnly\` and \`prependPlusCountryCode\` property of the \`options\` (second parameter) must be of type \`boolean\` or unset as \`undefined\` value, but received: ['takeNumberOnly': \`${getPreciseType( takeNumberOnly )}\`, 'checkValidOnly': \`${getPreciseType( checkValidOnly )}\`, 'prependPlusCountryCode': \`${getPreciseType(prependPlusCountryCode)}\`].` ); } if (!isUndefined(defaultCountry) && !isSupportedCountry(defaultCountry)) { throw new TypeError( `Parameter \`defaultCountry\` property of the \`options\` (second parameter) must be of type \`string\` as \`CountryCode\` (ISO-3166-1 alpha-2) or unset as \`undefined\` value, but received: \`${getPreciseType( defaultCountry )}\`, with value: \`${safeStableStringify(defaultCountry, { keepUndefined: true })}\`. See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements, for all ISO 3166-1 alpha-2 code.` ); } if (!["INTERNATIONAL", "NATIONAL", "RFC3966", "E.164"].includes(outputFormat)) { throw new TypeError( `Parameter \`outputFormat\` property of the \`options\` (second parameter) must be of type \`string\` as \`OutputFormat\` ("NATIONAL" | "INTERNATIONAL" | "E.164" | "RFC3966") or unset as \`undefined\` (default value to: \`INTERNATIONAL\`) value, but received: \`${getPreciseType( outputFormat )}\`, with value: ${safeStableStringify(outputFormat, { keepUndefined: true })}.` ); } if (!isString(separator) || !isString(openingNumberCountry) || !isString(closingNumberCountry)) { throw new TypeError( `Parameter \`separator\`, \`plusNumberCountry\`, \`openingNumberCountry\` and \`closingNumberCountry\` property of the \`options\` (second parameter) must be of type \`string\` or unset as \`undefined\` value, but received: ['separator': \`${getPreciseType( separator )}\`,'openingNumberCountry': \`${getPreciseType( openingNumberCountry )}\`, 'closingNumberCountry': \`${getPreciseType(closingNumberCountry)}\`].` ); } if (!isString(value)) value = String(value); const parsedPhoneNumber = parsingAsYouType(value, defaultCountry); const validPhoneNumber = isValidParseAsYouType(parsedPhoneNumber); if (checkValidOnly) return validPhoneNumber; if (!validPhoneNumber) return ""; if (takeNumberOnly) { return parsedPhoneNumber.getNumber().formatNational().replace(/\D/g, ""); } const num = parsedPhoneNumber.getNumber(); const intlNumb = num.format(outputFormat); if (outputFormat === "INTERNATIONAL") { const [cc, ...rest] = intlNumb.split(" "); const countryCode = prependPlusCountryCode ? cc : cc.replace(/^\++/, ""); const restWithSeparator = rest.join(separator); if (isNonEmptyString(openingNumberCountry) && isNonEmptyString(closingNumberCountry)) { return `${openingNumberCountry}${countryCode}${closingNumberCountry} ${restWithSeparator}`; } return `${countryCode} ${restWithSeparator}`; } if (outputFormat === "NATIONAL") { const restWithSeparator = intlNumb.split(" ").join(separator); return `${restWithSeparator}`; } return intlNumb; } var hashSeedGenerate = (mode, email) => { const generateSeed = () => { let hash = 0; for (let i = 0; i < email.length; i++) { hash = (hash << 5) - hash + email.charCodeAt(i); hash |= 0; } return Math.abs(hash); }; return mode === "fixed" ? generateSeed() : void 0; }; var _censor = (str, minCensor, maxPercentage, hashSeed) => { if (str.length <= minCensor) return "*".repeat(str.length); const strArr = str.split(""); const totalCensor = Math.max(minCensor, Math.ceil(str.length * maxPercentage)); const indexes = /* @__PURE__ */ new Set(); let i = 0; while (indexes.size < totalCensor) { const idx = !isUndefined(hashSeed) ? (hashSeed + str.length + i * 31) % str.length : Math.floor(Math.random() * str.length); indexes.add(idx); i++; } for (const index of indexes) { strArr[index] = "*"; } return strArr.join(""); }; var censorEmail = (email, options = {}) => { if (!isNonEmptyString(email)) return ""; assertIsPlainObject(options, { message: ({ currentType, validType }) => `Second parameter (\`options\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); const mode = hasOwnProp(options, "mode") ? options.mode : "fixed"; if (mode !== "random" && mode !== "fixed") { throw new TypeError( `Parameter \`mode\` property of the \`options\` (second parameter) must be one of "fixed" or "random", but received: \`${getPreciseType( mode )}\`, with value: \`${safeStableStringify(mode, { keepUndefined: true })}\`.` ); } const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(email)) return ""; const [local, domain] = email.split("@"); const domainParts = domain.split("."); if (domainParts.length < 2) return ""; const [domainName, ...tldParts] = domainParts; const tld = tldParts.join("."); const hashSeed = hashSeedGenerate(mode, email); const localMinCensor = local.length < 4 ? 1 : 2; const domainMinCensor = domainName.length < 4 ? 1 : 2; const censoredLocal = _censor(local, localMinCensor, 0.6, hashSeed); const censoredDomain = _censor(domainName, domainMinCensor, 0.5, hashSeed); const censoredTLD = tld.length <= 2 ? tld : _censor(tld, 1, 0.4, hashSeed); return `${censoredLocal}@${censoredDomain}.${censoredTLD}`; }; function chunkString(subject, limiter, options = {}) { if (isNil(subject) || limiter <= 0) return subject; assertIsString(subject, { message: ({ currentType, validType }) => `First parameter (\`subject\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); if (!isInteger(limiter)) { throw new TypeError( `Second parameter (\`limiter\`) must be of type \`integer-number\`, but received: \`${getPreciseType( limiter )}\`, with value: \`${safeStableStringify(limiter, { keepUndefined: true })}\`.` ); } assertIsPlainObject(options, { message: ({ currentType, validType }) => `Third parameter (\`options\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); const separator = hasOwnProp(options, "separator") ? options.separator : " "; const reCountAfterSpace = hasOwnProp(options, "reCountAfterSpace") ? options.reCountAfterSpace : false; assertIsString(separator, { message: ({ currentType, validType }) => `Parameter \`separator\` property of the \`options\` (third parameter) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); assertIsBoolean(reCountAfterSpace, { message: ({ currentType, validType }) => `Parameter \`reCountAfterSpace\` property of the \`options\` (third parameter) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); subject = normalizeSpaces(subject); if (!reCountAfterSpace) { let result = ""; let currentCount = 0; for (let i = 0; i < subject.length; i++) { const char = subject[i]; if (currentCount === limiter) { result += separator; currentCount = 0; } result += char; currentCount++; } return result; } const words = subject.split(" "); const finalSegments = []; let currentGroup = []; let wordsInCurrentGroupCount = 0; for (let i = 0; i < words.length; i++) { const word = words[i]; let processedWord = ""; let charCountInWord = 0; for (let j = 0; j < word.length; j++) { processedWord += word[j]; charCountInWord++; if (charCountInWord === limiter && j < word.length - 1) { processedWord += separator; charCountInWord = 0; } } currentGroup.push(processedWord); wordsInCurrentGroupCount++; if (wordsInCurrentGroupCount === limiter || i === words.length - 1) { finalSegments.push(currentGroup.join(separator)); currentGroup = []; wordsInCurrentGroupCount = 0; } } return finalSegments.join(" "); } var truncateString = (text, options = {}) => { if (!isNonEmptyString(text)) return ""; assertIsPlainObject(options, { message: ({ currentType, validType }) => `Seconds parameter (\`options\`) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); const trim = hasOwnProp(options, "trim") ? options.trim : true; const length2 = hasOwnProp(options, "length") ? options.length : 10; let ending = hasOwnProp(options, "ending") ? options.ending : "..."; if (!isInteger(length2)) { throw new TypeError( `Parameter \`length\` property of the \`options\` (second parameter) must be of type \`integer-number\`, but received: \`${getPreciseType( length2 )}\`, with value: \`${safeStableStringify(length2, { keepUndefined: true })}\`.` ); } if (length2 < 1) return ""; assertIsString(ending, { message: ({ currentType, validType }) => `Parameter \`ending\` property of the \`options\` (second parameter) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); assertIsBoolean(trim, { message: ({ currentType, validType }) => `Parameter \`trim\` property of the \`options\` (second parameter) must be of type \`${validType}\`, but received: \`${currentType}\`.` }); if (isEmptyString(ending)) { ending = "..."; } else { ending = ending.trim(); } const original = trim ? text.trim() : text; const originalTrimmedLength = original.length; if (originalTrimmedLength <= length2) return original; const sliced = original.slice(0, length2); const cleanSliced = trim ? sliced : sliced.trimEnd(); return cleanSliced + ending; }; var formatDateFns = (date, options = {}) => { if (isNil(date) || !(isDate(date) || isNonEmptyString(date))) return null; if (!isPlainObject(options)) { options = {}; } const { inputFormat, locale = "en", inputLocale = "en", ...restOptions } = options; const format$1 = hasOwnProp(options, "format") && isNonEmptyString(options.format) ? options.format : "dd MMM yyyy - HH:mm:ss"; let parsedDate; try { if (isNonEmptyString(date) && inputFormat && inputLocale) { const valueOfInputLocale = isNonEmptyString(inputLocale) ? inputLocale === "id" ? id : enUS : inputLocale; parsedDate = parse(date, inputFormat, /* @__PURE__ */ new Date(), { locale: valueOfInputLocale }); } else { parsedDate = new Date(date); } if (isNaN(parsedDate.getTime())) return null; const valueOfLocale = isNonEmptyString(locale) ? locale === "id" ? id : enUS : locale; return format(parsedDate, format$1, { ...restOptions, locale: valueOfLocale }); } catch { return null; } }; var formatDateIntl = (date, options) => { if (isNil(date) || !(isDate(date) || isNonEmptyString(date))) return null; const parsedDate = new Date(date); if (isNaN(parsedDate.getTime())) return null; if (!isPlainObject(options)) { options = {}; } const { locale = "en-US", ...restProps } = options; try { return new Intl.DateTimeFormat( isNonEmptyString(locale) ? locale.trim() : "en-US", restProps ).format(parsedDate); } catch { return null; } }; var formatDateTime = (date, format) => { if (isNil(format)) { format = "YYYY-MM-DD hh:mm:ss"; } if (!isString(format)) return null; if (isNil(date) || !(isDate(date) || isNonEmptyString(date))) { return null; } try { const parsedDate = new Date(date); if (isNaN(parsedDate.getTime())) return null; const pad2 = (n) => n.toString().padStart(2, "0"); const map = { YYYY: parsedDate.getFullYear().toString(), MM: pad2(parsedDate.getMonth() + 1), DD: pad2(parsedDate.getDate()), hh: pad2(parsedDate.getHours()), mm: pad2(parsedDate.getMinutes()), ss: pad2(parsedDate.getSeconds()) }; const result = Object.entries(map).reduce( (prev, [key, value]) => prev.split(key).join(value), format ); return !result.includes("NaN") ? result : null; } catch { return null; } }; var getGMTOffset = (date) => { try { if (isNil(date) || isString(date) && isEmptyString(date)) { date = /* @__PURE__ */ new Date(); } else if (!(isDate(date) || isNonEmptyString(date))) { return "0"; } const parsedDate = new Date(date); if (isNaN(parsedDate.getTime())) return "0"; const padZero = (num) => num.toString().padStart(2, "0"); let offset = parsedDate.getTimezoneOffset(); const sign = offset < 0 ? "+" : "-"; offset = Math.abs(offset); return `${sign}${padZero(Math.floor(offset / 60))}${padZero(offset % 60)}`; } catch { return "0"; } }; export { censorEmail, chunkString, formatCurrency, formatDateFns, formatDateIntl, formatDateTime, formatNumber, formatPhoneNumber, getGMTOffset, truncateString };