From 9bafefa32c39c0ed7c410c7799ad27a8d5ef9adf Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sun, 24 Nov 2024 18:08:47 -0500 Subject: [PATCH] Add facility for caching and prepopulating a cache --- src/applyInflections.ts | 32 ++++++++------------ src/cache.ts | 21 +++++++++++++ src/camelize.ts | 58 ++++++++++++++++++----------------- src/classify.ts | 5 ++- src/constantify.ts | 9 +++--- src/humanize.ts | 67 +++++++++++++++++++++-------------------- src/parameterize.ts | 48 +++++++++++++++-------------- src/pluralize.ts | 8 +++-- src/singularize.ts | 8 +++-- src/underscore.ts | 5 +-- 10 files changed, 145 insertions(+), 116 deletions(-) create mode 100644 src/cache.ts diff --git a/src/applyInflections.ts b/src/applyInflections.ts index 7a39b9b..bb2bb18 100644 --- a/src/applyInflections.ts +++ b/src/applyInflections.ts @@ -1,33 +1,27 @@ import { inflections } from "./Inflector"; export function applyInflections(word: string, rules: [RegExp | string, string][]) { - let result = "" + word, - rule, - regex, - replacement; + let result = word; const inflector = inflections(); if (result.length === 0) { return result; - } else { - const match = result.toLowerCase().match(/\b\w+$/); + } - if (match && inflector.uncountables.indexOf(match[0]) > -1) { - return result; - } else { - for (let i = 0, ii = rules.length; i < ii; i++) { - rule = rules[i]; + const match = result.toLowerCase().match(/\b\w+$/); - regex = rule[0]; - replacement = rule[1]; + if (match && inflector.uncountables.indexOf(match[0]) > -1) { + return result; + } else { + for (const rule of rules) { + const [regex, replacement] = rule; - if (result.match(regex)) { - result = result.replace(regex, replacement); - break; - } + if (result.match(regex)) { + result = result.replace(regex, replacement); + break; } - - return result; } + + return result; } } diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..580538e --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,21 @@ +/** Wrap a given function in a cache that is off by default */ +export const cacheable = any>( + fn: T, + getCacheKey: (...args: Parameters) => string = ((str: string) => str) as unknown as (...args: Parameters) => string +) => { + const cache = new Map>(); + + const cachedFn = Object.assign( + function (this: unknown, ...args: Parameters): ReturnType { + return cache.get(getCacheKey(...args)) ?? fn.call(this, ...args); + }, + { + cache, + populate: (...args: Parameters) => { + cache.set(getCacheKey(...args), fn(...args)); + }, + } + ); + + return cachedFn as T & { cache: Map>; populate: (...args: Parameters) => void }; +}; diff --git a/src/camelize.ts b/src/camelize.ts index 4c736d1..a487fb9 100644 --- a/src/camelize.ts +++ b/src/camelize.ts @@ -1,41 +1,45 @@ import { inflections } from "./Inflector"; import type { AhoCorasick } from "./ahoCorasick"; +import { cacheable } from "./cache"; import { capitalize } from "./capitalize"; const separators = /(?:_|(\/))([a-z\d]*)/gi; -export function camelize(term: string, uppercaseFirstLetter = true) { - const inflector = inflections(); +export const camelize = cacheable( + (term: string, uppercaseFirstLetter = true) => { + const inflector = inflections(); - let result: string = term; + let result: string = term; - if (uppercaseFirstLetter) { - const startAcronym = findLongestStartAcronym(inflector.lowerAcronymMatcher, term); - if (startAcronym) { - result = inflector.lowerToAcronyms[startAcronym] + result.slice(startAcronym.length); + if (uppercaseFirstLetter) { + const startAcronym = findLongestStartAcronym(inflector.lowerAcronymMatcher, term); + if (startAcronym) { + result = inflector.lowerToAcronyms[startAcronym] + result.slice(startAcronym.length); + } else { + result = term.charAt(0).toUpperCase() + term.slice(1); + } } else { - result = term.charAt(0).toUpperCase() + term.slice(1); + const startAcronym = findLongestStartAcronym(inflector.casedAcronymMatcher, term); + if (startAcronym) { + result = startAcronym.toLowerCase() + result.slice(startAcronym.length); + } else { + result = term.charAt(0).toLowerCase() + term.slice(1); + } } - } else { - const startAcronym = findLongestStartAcronym(inflector.casedAcronymMatcher, term); - if (startAcronym) { - result = startAcronym.toLowerCase() + result.slice(startAcronym.length); - } else { - result = term.charAt(0).toLowerCase() + term.slice(1); - } - } - - result = result.replace(separators, (_match, separator, word) => { - word = inflector.lowerToAcronyms[word] ?? capitalize(word); - if (separator) { - return separator + word; - } else { - return word; - } - }); - return result; -} + result = result.replace(separators, (_match, separator, word) => { + word = inflector.lowerToAcronyms[word] ?? capitalize(word); + if (separator) { + return separator + word; + } else { + return word; + } + }); + + return result; + }, + (term, uppercaseFirstLetter) => `${term}-${uppercaseFirstLetter}` +); const findLongestStartAcronym = (matcher: AhoCorasick | null, word: string) => { if (!matcher) return null; diff --git a/src/classify.ts b/src/classify.ts index 11840d5..80a0730 100644 --- a/src/classify.ts +++ b/src/classify.ts @@ -1,6 +1,5 @@ +import { cacheable } from "./cache"; import { camelize } from "./camelize"; import { singularize } from "./singularize"; -export function classify(tableName: string) { - return camelize(singularize(tableName.replace(/.*\./g, ""))); -} +export const classify = cacheable((tableName: string) => camelize(singularize(tableName.replace(/.*\./g, "")))); diff --git a/src/constantify.ts b/src/constantify.ts index 897c26c..79cbbed 100644 --- a/src/constantify.ts +++ b/src/constantify.ts @@ -1,7 +1,6 @@ +import { cacheable } from "./cache"; import { underscore } from "./underscore"; -export function constantify(word: string) { - return underscore(word) - .toUpperCase() - .replace(/\s+/g, "_"); -} +export const constantify = cacheable((word: string) => { + return underscore(word).toUpperCase().replace(/\s+/g, "_"); +}); diff --git a/src/humanize.ts b/src/humanize.ts index ed31dc8..1ebd066 100644 --- a/src/humanize.ts +++ b/src/humanize.ts @@ -1,40 +1,43 @@ import { inflections } from "./Inflector"; +import { cacheable } from "./cache"; +export const humanize = cacheable( + (lowerCaseAndUnderscoredWord: string, options?: { capitalize?: boolean }) => { + let result = "" + lowerCaseAndUnderscoredWord; + const inflector = inflections(); + const humans = inflector.humans; + let human, rule, replacement; + + options = options || {}; + + if (options.capitalize === null || options.capitalize === undefined) { + options.capitalize = true; + } -export function humanize(lowerCaseAndUnderscoredWord: string, options?: { capitalize?: boolean }) { - let result = "" + lowerCaseAndUnderscoredWord; - const inflector = inflections(); - const humans = inflector.humans; - let human, rule, replacement; - - options = options || {}; - - if (options.capitalize === null || options.capitalize === undefined) { - options.capitalize = true; - } - - for (let i = 0, ii = humans.length; i < ii; i++) { - human = humans[i]; - rule = human[0]; - replacement = human[1]; + for (let i = 0, ii = humans.length; i < ii; i++) { + human = humans[i]; + rule = human[0]; + replacement = human[1]; - if (rule instanceof RegExp ? rule.test(result) : result.indexOf(rule) > -1) { - result = result.replace(rule, replacement); - break; + if (rule instanceof RegExp ? rule.test(result) : result.indexOf(rule) > -1) { + result = result.replace(rule, replacement); + break; + } } - } - result = result.replace(/_id$/, ""); - result = result.replace(/_/g, " "); + result = result.replace(/_id$/, ""); + result = result.replace(/_/g, " "); - result = result.replace(/([a-z\d]*)/gi, function (match) { - return inflector.lowerToAcronyms[match] || match.toLowerCase(); - }); - - if (options.capitalize) { - result = result.replace(/^\w/, function (match) { - return match.toUpperCase(); + result = result.replace(/([a-z\d]*)/gi, function (match) { + return inflector.lowerToAcronyms[match] || match.toLowerCase(); }); - } - return result; -} + if (options.capitalize) { + result = result.replace(/^\w/, function (match) { + return match.toUpperCase(); + }); + } + + return result; + }, + (lowerCaseAndUnderscoredWord, options) => `${lowerCaseAndUnderscoredWord}-${options?.capitalize}` +); diff --git a/src/parameterize.ts b/src/parameterize.ts index 114ed75..b0c52d1 100644 --- a/src/parameterize.ts +++ b/src/parameterize.ts @@ -1,32 +1,36 @@ +import { cacheable } from "./cache"; import { transliterate } from "./Transliterator"; -export function parameterize(string: string, options: { locale?: string; separator?: string | null; preserveCase?: boolean } = {}) { - if (options.separator === undefined) { - options.separator = "-"; - } +export const parameterize = cacheable( + (string: string, options: { locale?: string; separator?: string | null; preserveCase?: boolean } = {}) => { + if (options.separator === undefined) { + options.separator = "-"; + } - if (options.separator === null) { - options.separator = ""; - } + if (options.separator === null) { + options.separator = ""; + } - // replace accented chars with their ascii equivalents - let result = transliterate(string, { locale: options.locale }); + // replace accented chars with their ascii equivalents + let result = transliterate(string, { locale: options.locale }); - result = result.replace(/[^a-z0-9\-_]+/gi, options.separator); + result = result.replace(/[^a-z0-9\-_]+/gi, options.separator); - if (options.separator.length) { - const separatorRegex = new RegExp(options.separator); + if (options.separator.length) { + const separatorRegex = new RegExp(options.separator); - // no more than one of the separator in a row - result = result.replace(new RegExp(separatorRegex.source + "{2,}"), options.separator); + // no more than one of the separator in a row + result = result.replace(new RegExp(separatorRegex.source + "{2,}"), options.separator); - // remove leading/trailing separator - result = result.replace(new RegExp("^" + separatorRegex.source + "|" + separatorRegex.source + "$", "i"), ""); - } + // remove leading/trailing separator + result = result.replace(new RegExp("^" + separatorRegex.source + "|" + separatorRegex.source + "$", "i"), ""); + } - if (options.preserveCase) { - return result; - } + if (options.preserveCase) { + return result; + } - return result.toLowerCase(); -} + return result.toLowerCase(); + }, + (string, options) => `${string}-${options?.locale}-${options?.separator}-${options?.preserveCase}` +); diff --git a/src/pluralize.ts b/src/pluralize.ts index 5838077..f96d3ae 100644 --- a/src/pluralize.ts +++ b/src/pluralize.ts @@ -1,6 +1,8 @@ import { applyInflections } from "./applyInflections"; +import { cacheable } from "./cache"; import { inflections } from "./Inflector"; -export function pluralize(word: string, locale = "en") { - return applyInflections(word, inflections(locale).plurals); -} +export const pluralize = cacheable( + (word: string, locale = "en") => applyInflections(word, inflections(locale).plurals), + (word, locale) => `${word}-${locale}` +); diff --git a/src/singularize.ts b/src/singularize.ts index 6265e17..ae36380 100644 --- a/src/singularize.ts +++ b/src/singularize.ts @@ -1,6 +1,8 @@ import { applyInflections } from "./applyInflections"; +import { cacheable } from "./cache"; import { inflections } from "./Inflector"; -export function singularize(word: string, locale = "en") { - return applyInflections(word, inflections(locale).singulars); -} +export const singularize = cacheable( + (word: string, locale = "en") => applyInflections(word, inflections(locale).singulars), + (word, locale) => `${word}-${locale}` +); diff --git a/src/underscore.ts b/src/underscore.ts index 4f73e5f..2d96a28 100644 --- a/src/underscore.ts +++ b/src/underscore.ts @@ -1,10 +1,11 @@ +import { cacheable } from "./cache"; import { inflections } from "./Inflector"; const letterOrDigit = /[A-Za-z\d]/; const wordBoundaryOrNonLetter = /\b|[^a-z]/; const boundaryMatcher = /([A-Z\d]+)([A-Z][a-z])|([a-z\d])([A-Z])|(-)/g; -export function underscore(camelCasedWord: string) { +export const underscore = cacheable((camelCasedWord: string) => { let result = camelCasedWord; const acronymMatches = inflections().casedAcronymMatcher?.search(camelCasedWord, isWordBoundary); if (acronymMatches) { @@ -32,7 +33,7 @@ export function underscore(camelCasedWord: string) { return `${p3}_${p4}`; }) .toLowerCase(); -} +}); function isWordBoundary(char: string): boolean { const charCode = char.charCodeAt(0);