UNPKG

@bitblit/ratchet-common

Version:

Common tools for general use

378 lines 12.8 kB
import { RequireRatchet } from './require-ratchet.js'; export class StringRatchet { static RFC_3986_RESERVED = [ '!', '*', "'", '(', ')', ';', ':', '@', '&', '=', '+', '$', ',', '/', '?', '#', '[', ']', '%', ]; static WHITESPACE = ' \n\t'; static DIGITS = '0123456789'; static HEXITS = StringRatchet.DIGITS + 'ABCDEF'; static UPPER_CASE_LATIN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; static LOWER_CASE_LATIN = 'abcdefghijklmnopqrstuvwxyz'; static CASE_INSENSITIVE_LATIN = StringRatchet.UPPER_CASE_LATIN + StringRatchet.LOWER_CASE_LATIN; static stringIsInGivenAlphabet(input, alphabet) { let rval = false; if (input && alphabet) { for (let i = 0; i < input.length && !rval; i++) { rval = alphabet.includes(input.charAt(i)); } } return rval; } static stringToUint8Array(val) { return val ? new TextEncoder().encode(val) : null; } static uint8ArrayToString(val) { return val ? new TextDecoder().decode(val) : null; } static attemptJsonParse(val) { let rval = null; if (StringRatchet.trimToNull(val)) { try { rval = JSON.parse(val); } catch { rval = null; } } return rval; } static canParseAsJson(val) { return !!StringRatchet.attemptJsonParse(val); } static allUnique(input) { let rval = true; if (input) { const check = new Set(); for (let i = 0; i < input.length && rval; i++) { const test = input.charAt(i); rval = !check.has(test); check.add(test); } } return rval; } static allPermutationsOfLength(len, alphabet) { const rval = []; if (len > 0 && alphabet && alphabet.length > 0) { RequireRatchet.true(StringRatchet.allUnique(alphabet), 'Alphabet must be unique'); const step = len === 1 ? [''] : StringRatchet.allPermutationsOfLength(len - 1, alphabet); for (let i = 0; i < alphabet.length; i++) { step.forEach((s) => rval.push(alphabet.charAt(i) + s)); } } return rval; } static breakIntoBlocks(input, blockSize, separator) { let out = ''; while (input.length > blockSize) { out = separator + input.substring(input.length - blockSize) + out; input = input.substring(0, input.length - blockSize); } if (input.length > 0) { out = input + out; } else { out = out.substring(1); } return out; } static createShortUid(blockSize = 0, uniquesPerSecond = 1000, radix = 36) { const currentEpoch = Math.floor(Date.now() / 1000); const asDecimal = parseInt(String(Math.floor(Math.random() * uniquesPerSecond)) + String(currentEpoch)); const asHex = asDecimal.toString(radix); const out = blockSize > 0 ? StringRatchet.breakIntoBlocks(asHex, blockSize, '-') : asHex; return out; } static createType4Guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } static createRandomStringFromAlphabet(alphabet, len = 10) { RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(alphabet, 'Alphabet may not be empty'); let rval = ''; while (rval.length < len) { rval += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); } return rval; } static createRandomHexString(len = 10) { let r = ''; for (let i = 0; i < len; i++) { r += Math.floor(Math.random() * 16).toString(16); } return r; } static canonicalize(value) { let rval = value ? value.toLowerCase() : ''; rval = rval.replace(' ', '-'); StringRatchet.RFC_3986_RESERVED.forEach((s) => { rval = rval.replace(s, ''); }); return rval; } static formatBytes(bytes, decimals = 2) { if (bytes == 0) return '0 Bytes'; const k = 1024, dm = decimals || 2, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } static safeString(input) { let rval = null; if (input != null) { const type = typeof input; if (type == 'string') { rval = input; } else { rval = String(input); } } return rval; } static stringContainsOnlyWhitespace(input) { return StringRatchet.stringContainsOnly(input, StringRatchet.WHITESPACE); } static stringContainsOnlyNumbers(input) { const rval = /^[0-9]+$/.test(input); return rval; } static stringContainsOnlyAlphanumeric(input) { const rval = /^[0-9a-zA-Z]+$/.test(input); return rval; } static stringContainsOnlyHex(input) { const rval = /^[0-9a-fA-F]+$/.test(input); return rval; } static stringContainsOnly(inVal, validCharsIn) { const input = !inVal ? '' : inVal; const validChars = !validCharsIn ? '' : validCharsIn; let rval = true; for (let i = 0; i < input.length && rval; i++) { rval = validChars.indexOf(input.charAt(i)) >= 0; } return rval; } static obscure(input, prefixLength = 2, suffixLength = 2) { if (!input) { return input; } const len = input.length; let pl = prefixLength; let sl = suffixLength; while (len > 0 && len < pl + sl + 1) { pl = Math.max(0, pl - 1); sl = Math.max(0, sl - 1); } const rem = len - (pl + sl); let rval = ''; rval += input.substring(0, pl); for (let i = 0; i < rem; i++) { rval += '*'; } rval += input.substring(len - sl); return rval; } static leadingZeros(inVal, size) { const pad = '00000000000000000000000000000000000000000000000000'; let negative = false; let sVal = String(inVal); if (sVal.startsWith('-')) { negative = true; sVal = sVal.substring(1); } if (size > pad.length) { throw new Error('Cannot format number that large'); } let rval = (pad + sVal).slice(-1 * size); if (negative) { rval = '-' + rval; } return rval; } static trimToEmpty(input) { const t = input ? StringRatchet.safeString(input) : ''; return t.trim(); } static trimToNull(input) { const x = StringRatchet.trimToEmpty(input); return x.length > 0 ? x : null; } static trimAllStringPropertiesToNullInPlace(input) { return StringRatchet.trimAllStringPropertiesInPlace(input, false); } static trimAllStringPropertiesToEmptyInPlace(input) { return StringRatchet.trimAllStringPropertiesInPlace(input, true); } static trimAllStringPropertiesInPlace(input, toEmpty) { const dealKeys = Object.keys(input); dealKeys.forEach((key) => { const val = input[key]; if (val != null && typeof val === 'string') { input[key] = toEmpty ? StringRatchet.trimToEmpty(input[key]) : StringRatchet.trimToNull(input[key]); } }); return input; } static stripNonNumeric(input) { let rval = input; if (input != null && !StringRatchet.stringContainsOnlyNumbers(input)) { rval = ''; for (let i = 0; i < input.length; i++) { const c = input.charAt(i); if (StringRatchet.stringContainsOnlyNumbers(c) || (i === 0 && c === '-')) { rval += c; } } } return rval; } static csvSafe(input) { let rval = StringRatchet.trimToEmpty(StringRatchet.safeString(input)); rval.split('"').join('\\"'); if (rval.indexOf(',') !== -1 || rval.indexOf('"') !== -1 || rval.indexOf("'") !== -1) { rval = '"' + rval + '"'; } return rval; } static stripNonAscii(value) { const reduced = [...value].reduce((previousValue, currentValue) => { const charCode = currentValue.charCodeAt(0); if (charCode > 127) { return previousValue; } return previousValue + currentValue; }); return reduced; } static replaceAll(value, src, dst) { let rval = value; if (rval?.length && src?.length && dst?.length) { rval = rval.split(src).join(dst); } return rval; } static simpleTemplateFill(template, fillers, errorOnMissingFiller = false, opener = '${', closer = '}') { let rval = template; if (rval && fillers) { Object.keys(fillers).forEach((key) => { rval = rval.split(opener + key + closer).join(fillers[key]); }); } if (errorOnMissingFiller && rval?.indexOf(opener) >= 0) { throw new Error('Template has unfilled variables:' + rval); } return rval; } static circSafeJsonStringify(input) { let rval = null; try { rval = JSON.stringify(input); } catch (err) { if (err instanceof TypeError) { let lines = err.message .split('\n') .map((s) => StringRatchet.trimToNull(s)) .filter((s) => !!s); lines = lines.filter((s) => s.startsWith('-->') || s.startsWith('---')); rval = 'Cannot stringify - object contains circular reference : ' + lines.join(', '); } else { throw err; } } return rval; } static format(fmt, ...args) { const re = /(%?)(%([ojds]))/g; if (args.length) { fmt = fmt.replace(re, function (match, escaped, ptn, flag) { let arg = args.shift(); switch (flag) { case 'o': if (Array.isArray(arg)) { arg = StringRatchet.circSafeJsonStringify(arg); break; } else { throw new Error('Cannot use o placeholder for argument of type ' + typeof arg); } case 's': arg = '' + arg; break; case 'd': arg = Number(arg); break; case 'j': arg = StringRatchet.circSafeJsonStringify(arg); break; } if (!escaped) { return arg; } args.unshift(arg); return match; }); } if (args.length) { fmt += ' ' + args.join(' '); } fmt = fmt.replace(/%{2,2}/g, '%'); return '' + fmt; } static findSuffix(i, j, s, memo) { if (j === s.length) return 0; if (memo[i][j] !== -1) return memo[i][j]; if (s[i] === s[j]) { memo[i][j] = 1 + Math.min(StringRatchet.findSuffix(i + 1, j + 1, s, memo), j - i - 1); } else { memo[i][j] = 0; } return memo[i][j]; } static longestNonOverlappingRepeatingSubstring(s) { const n = s.length; const memo = Array.from({ length: n }, () => Array(n).fill(-1)); for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { StringRatchet.findSuffix(i, j, s, memo); } } let ans = ''; let ansLen = 0; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (memo[i][j] > ansLen) { ansLen = memo[i][j]; ans = s.substring(i, i + ansLen); } } } return ansLen > 0 ? ans : null; } } //# sourceMappingURL=string-ratchet.js.map