@bitblit/ratchet-common
Version:
Common tools for general use
378 lines • 12.8 kB
JavaScript
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