diff --git a/studio/src/lib/analysis.js b/studio/src/lib/analysis.js index 6280729..79527ac 100644 --- a/studio/src/lib/analysis.js +++ b/studio/src/lib/analysis.js @@ -21,10 +21,26 @@ export function tokenize(text) { .filter(Boolean); } +// Compiled emoji regexes are cached per emoji-list array so countEmoji does not +// recompile ~70 RegExps on every call (it runs once per tweet, several times). +// String.prototype.match with a /g regex is stateless (it ignores and resets +// lastIndex), so reusing the same compiled regexes across calls is safe. +const _emojiRegexCache = new WeakMap(); + +function emojiRegexes(list) { + let regexes = _emojiRegexCache.get(list); + if (!regexes) { + regexes = list.map( + (e) => new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"), + ); + _emojiRegexCache.set(list, regexes); + } + return regexes; +} + export function countEmoji(text, list) { let count = 0; - for (const e of list) { - const re = new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"); + for (const re of emojiRegexes(list)) { const m = text.match(re); if (m) count += m.length; } diff --git a/studio/src/lib/analysis.test.js b/studio/src/lib/analysis.test.js index 111d8f7..b21a0ca 100644 --- a/studio/src/lib/analysis.test.js +++ b/studio/src/lib/analysis.test.js @@ -35,6 +35,13 @@ describe("countEmoji", () => { it("returns 0 when no listed emoji are present", () => { expect(countEmoji("plain text", POSITIVE_EMOJI)).toBe(0); }); + + it("stays correct across repeated calls with the same list (cached regexes are stateless)", () => { + expect(countEmoji("🔥🔥", POSITIVE_EMOJI)).toBe(2); + expect(countEmoji("🔥", POSITIVE_EMOJI)).toBe(1); + expect(countEmoji("no emoji here", POSITIVE_EMOJI)).toBe(0); + expect(countEmoji("🔥🔥🔥", POSITIVE_EMOJI)).toBe(3); + }); }); describe("analyzeSentiment", () => {