diff --git a/src/utils/comparison/string-equality-comparer.ts b/src/utils/comparison/string-equality-comparer.ts new file mode 100644 index 0000000..1484c80 --- /dev/null +++ b/src/utils/comparison/string-equality-comparer.ts @@ -0,0 +1,78 @@ +import { IS_LETTER_OR_DIGIT_REGEX } from "@/utils/string-utils"; +import { createDefaultEqualityComparer, createEqualityComparer } from "./equality-comparer"; +import { IGNORE_CASE_COMPARER } from "./string-comparer"; + +/** + * A string comparer that performs a case-sensitive ordinal string comparison. + */ +export const ORDINAL_EQUALITY_COMPARER = createDefaultEqualityComparer(); + +/** + * A string comparer that ignores case differences. + */ +export const IGNORE_CASE_EQUALITY_COMPARER = IGNORE_CASE_COMPARER.asEqualityComparer(); + +/** + * An equality comparer that compares two strings ignoring non-word characters (e.g. spaces, punctuation). + */ +export const IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER = createEqualityComparer( + (x, y) => compareStringsIgnoreNonWordCharacters(x, y, false) +); + +/** + * Creates an equality comparer that compares two strings ignoring non-word characters (e.g. spaces, punctuation) and case sensitivity. + */ +export const IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER = createEqualityComparer( + (x, y) => compareStringsIgnoreNonWordCharacters(x, y, true) +); + +/** + * A comparer function that compares two strings ignoring non-word characters (e.g. spaces, punctuation). + * + * @param x - The first string to compare. + * @param y - The second string to compare. + * @param ignoreCase - A flag indicating whether to ignore case during comparison. + * + * @returns `true` if the two strings are equal; otherwise, `false`. + */ +function compareStringsIgnoreNonWordCharacters(x: string, y: string, ignoreCase?: boolean): boolean { + if (x === null || x === undefined || y === null || y === undefined) { + return x === y; + } + + const sensitivity = ignoreCase ? "accent" : "variant"; + + let xI = 0; + let yI = 0; + while (xI < x.length && yI < y.length) { + let xChar = x.charAt(xI); + let yChar = y.charAt(yI); + if (xChar === yChar) { + ++xI; + ++yI; + continue; + } + + while (xI < x.length && !IS_LETTER_OR_DIGIT_REGEX.test(xChar)) { + xChar = x.charAt(++xI); + } + while (yI < y.length && !IS_LETTER_OR_DIGIT_REGEX.test(yChar)) { + yChar = y.charAt(++yI); + } + + if (xChar.localeCompare(yChar, undefined, { sensitivity }) !== 0) { + return false; + } + ++xI; + ++yI; + } + + while (xI < x.length && !IS_LETTER_OR_DIGIT_REGEX.test(x.charAt(xI))) { + ++xI; + } + while (yI < y.length && !IS_LETTER_OR_DIGIT_REGEX.test(y.charAt(yI))) { + ++yI; + } + + return xI >= x.length && yI >= y.length; +} diff --git a/tests/unit/utils/comparison/string-equality-comparer.spec.ts b/tests/unit/utils/comparison/string-equality-comparer.spec.ts new file mode 100644 index 0000000..749b022 --- /dev/null +++ b/tests/unit/utils/comparison/string-equality-comparer.spec.ts @@ -0,0 +1,114 @@ +import { + ORDINAL_EQUALITY_COMPARER, + IGNORE_CASE_EQUALITY_COMPARER, + IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER, + IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER, +} from "@/utils/comparison/string-equality-comparer"; + +describe("ORDINAL_EQUALITY_COMPARER", () => { + test("returns true for identical strings", () => { + expect(ORDINAL_EQUALITY_COMPARER("Test", "Test")).toBe(true); + }); + + test("returns false for different strings", () => { + expect(ORDINAL_EQUALITY_COMPARER("Test", "Other")).toBe(false); + }); + + test("returns false for strings differing only by case", () => { + expect(ORDINAL_EQUALITY_COMPARER("Test", "test")).toBe(false); + }); + + test("returns false when one string is null or undefined", () => { + expect(ORDINAL_EQUALITY_COMPARER(null, "Test")).toBe(false); + expect(ORDINAL_EQUALITY_COMPARER("Test", undefined)).toBe(false); + expect(ORDINAL_EQUALITY_COMPARER(null, undefined)).toBe(false); + }); + + test("returns true when both strings are null, or both strings are undefined", () => { + expect(ORDINAL_EQUALITY_COMPARER(null, null)).toBe(true); + expect(ORDINAL_EQUALITY_COMPARER(undefined, undefined)).toBe(true); + }); +}); + +describe("IGNORE_CASE_EQUALITY_COMPARER", () => { + test("returns true for identical strings", () => { + expect(IGNORE_CASE_EQUALITY_COMPARER("Test", "Test")).toBe(true); + }); + + test("returns false for different strings", () => { + expect(IGNORE_CASE_EQUALITY_COMPARER("Test", "Other")).toBe(false); + }); + + test("returns true for strings differing only by case", () => { + expect(IGNORE_CASE_EQUALITY_COMPARER("Test", "test")).toBe(true); + }); + + test("returns false when one string is null or undefined", () => { + expect(IGNORE_CASE_EQUALITY_COMPARER(null, "Test")).toBe(false); + expect(IGNORE_CASE_EQUALITY_COMPARER("Test", undefined)).toBe(false); + expect(IGNORE_CASE_EQUALITY_COMPARER(null, undefined)).toBe(false); + }); + + test("returns true when both strings are null, or both strings are undefined", () => { + expect(IGNORE_CASE_EQUALITY_COMPARER(null, null)).toBe(true); + expect(IGNORE_CASE_EQUALITY_COMPARER(undefined, undefined)).toBe(true); + }); +}); + +describe("IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER", () => { + test("returns true for identical strings", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "Test")).toBe(true); + }); + + test("returns false for different strings", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "Other")).toBe(false); + }); + + test("returns false for strings differing only by case", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "test")).toBe(false); + }); + + test("returns true for strings differing only be non-word characters", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "T.e_s-t")).toBe(true); + }); + + test("returns false when one string is null or undefined", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, "Test")).toBe(false); + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", undefined)).toBe(false); + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, undefined)).toBe(false); + }); + + test("returns true when both strings are null, or both strings undefined", () => { + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, null)).toBe(true); + expect(IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER(undefined, undefined)).toBe(true); + }); +}); + +describe("IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER", () => { + test("returns true for identical strings", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "Test")).toBe(true); + }); + + test("returns false for different strings", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "Other")).toBe(false); + }); + + test("returns true for strings differing only by case", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "test")).toBe(true); + }); + + test("returns true for strings differing only by non-word characters and cases", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", "t.e_s-t")).toBe(true); + }); + + test("returns false when one string is null or undefined", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, "Test")).toBe(false); + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER("Test", undefined)).toBe(false); + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, undefined)).toBe(false); + }); + + test("returns true when both strings are null, or both strings are undefined", () => { + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(null, null)).toBe(true); + expect(IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(undefined, undefined)).toBe(true); + }); +});