diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0e8a36b..195b2bf 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,10 +1,13 @@ name: Node.js CD on: - push: - branches: ['main'] + pull_request: + branches: + - main + types: [closed] jobs: + if: ${{ github.event.pull_request.merged }} publish-npm: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c2d63..96159e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# Version 2.3.0 + +- `Enhancement`: + - `Case convertion`: Previously algorithms responsible of converting a string to another case was obviously to light, so, the range of managed uses was too poor, i reworked those algorithms and they're now better from far that was they were. The new ones got tested and passes tests, it's more than sure that i didn't test all of cases, but an enhancement of this feature is truely brang to package. +- `Added`: + - `Case` abstract class is now used to reliably create case objects. + - `CamelCase` class implement logic of previous correspondant object. + - `PascalCase` class implement logic of previous correspondant object. + - `SnakeCase` class implement logic of previous correspondant object. + - `LowerCase` class implement logic of previous correspondant object. + - `UpperCase` class implement logic of previous correspondant object. + - `StringUtils (main.ts)` + - `isConsiderableCharSequence(str: string): boolean` method has been implemented and used to check if a given string contains atleast 2 chars (excepted blanks ones). + - `containsConsiderableCharSequence(stringTable: string[]): boolean` method has been implemented and used to check if a given table of string contains atleast one considerable (determined by `isConsiderableCharSequence` criterias) element. + - `containsOnlyConsiderableCharSequences(stringTable: string[]): boolean` method has been implemented and used to check if a given table of string contains only considerable (determined by `isConsiderableCharSequence` criterias) elements. + - `removeBlankChars(str): string` method has been implemented and could be use to remove blank chars from a given string. + - `blendIrrelevantStringsInRelevantOnes(str: string): string[]` method has been implemented and should be used to blend orphan chars (a char adjacent to blank chars) to the last considerable subsequence of char (determined by `isConsiderableCharSequence` criterias) in a given string. +- `Removed`: + - `[BREAKING CHANGES]`: + - `Case` type has been renamed `CaseName` to avoid collision between new `Case` object type and previous `Case` type. + - `ICase` interface has been removed, it was useless to keep working with it. +- `Refactor`: + - `[BREAKING CHANGES]`: + - `determineCase(str: string): ICase` method signature changed to `determineCase(str: string): Case`. + - `convertToCase(str: string, caseToConvert: Case): string` method signature moved to `convertToCase(str: string, caseToConvert: CaseName): string`. + - `knownCases` table is now a `:Case[]` instead of `:ICase[]`. + - Tests has been refactored to avoid rewriting loops again and again, they now use `JestRunner` utils class. + # Version 2.2.0 - `Added`: diff --git a/package.json b/package.json index 206e1da..23a283e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "string-utils-ts", - "version": "2.2.0", + "version": "2.3.0", "description": "Provide some useful functions for strings", "main": "./lib", "scripts": { diff --git a/src/case.ts b/src/case.ts index a7ef80a..83fc751 100644 --- a/src/case.ts +++ b/src/case.ts @@ -1,63 +1,32 @@ -import StringUtilsWord from './word-ending-utils'; - -/** - * This interface provide a structure for literal objects - * It brings reliable way to add unmanaged cases without any other code writing - * - * @interface ICase - * @field {string} name is used to represent a matcher & splitter for case - * @field {RegExp} matcher is used with Regex operations to check if a given string match - * @field {RegExp | string} splitter is used to split given string who match `matcher` - * - */ -interface ICase { - name: Case; - matcher: RegExp; - splitter: RegExp | string; -} +import CamelCase from './case/camel-case'; +import Case from './case/Case'; +import LowerCase from './case/lower-case'; +import PascalCase from './case/pascal-case'; +import SnakeCase from './case/snake-case'; +import UpperCase from './case/upper-case'; +import { StringUtils } from './main'; /** * This type is used to ensure case selection is reliable */ - -export type Case = - | 'snakeCase' - | 'pascalCase' - | 'lowerCase' - | 'upperCase' - | 'camelCase'; +export type CaseName = + | 'SnakeCase' + | 'PascalCase' + | 'LowerCase' + | 'UpperCase' + | 'CamelCase'; /** * This table store few basics cases. * * @method StringUtilsCase.determineCase <- Use it */ -const knownCases: ICase[] = [ - { - name: 'snakeCase', - matcher: /(\w+)_(\w+)/, - splitter: '_', - }, - { - name: 'pascalCase', - matcher: /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/, - splitter: /([A-Z]+[a-z]*)/, - }, - { - name: 'lowerCase', - matcher: /^[a-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\s]*$/, - splitter: '', - }, - { - name: 'upperCase', - matcher: /^[A-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\s]*$/, - splitter: '', - }, - { - name: 'camelCase', - matcher: /^[a-z]+(?:[A-Z][a-z]+)*$/, - splitter: /([A-Z]+[a-z]*)/, - }, +const knownCases: Case[] = [ + new SnakeCase(), + new PascalCase(), + new LowerCase(), + new UpperCase(), + new CamelCase(), ]; export default class StringUtilsCase { @@ -68,7 +37,7 @@ export default class StringUtilsCase { * * @returns {ICase} - The case of given string */ - public static determineCase(str: string): ICase | undefined { + public static determineCase(str: string): Case | undefined { return knownCases.find((caseObject) => caseObject.matcher.test(str)); } @@ -105,31 +74,99 @@ export default class StringUtilsCase { * case: snakeCase * returns: this_is_a_test */ - public static convertToCase(str: string, caseToConvert: Case): string { - if (str.trim().replaceAll(' ', '').length < 2) return str; + public static convertToCase(str: string, caseToConvert: CaseName): string { + switch (caseToConvert) { + case 'LowerCase': + return this.convertToCaseLogic('LowerCase', str); + case 'UpperCase': + return this.convertToCaseLogic('UpperCase', str); + case 'CamelCase': + return this.convertToCaseLogic('CamelCase', str); + case 'PascalCase': + return this.convertToCaseLogic('PascalCase', str); + case 'SnakeCase': + return this.convertToCaseLogic('SnakeCase', str); + } + } - const splittedByCaseString = this.splitByCase(str); - if (splittedByCaseString.length == 1) return str; // Case was unsucessfully determinated + /** + * Returns a given string converted to camelCase + * + * @param {string} str - The string to convert + * + * @example + * str: ThisIsMyExample + * returns: thisIsMyExample + * @example + * str: thisIsMyExample + * returns: thisIsMyExample + */ + public static toCamelCase(str: string): string { + return this.convertToCaseLogic('CamelCase', str); + } - switch (caseToConvert) { - case 'lowerCase': - return splittedByCaseString.join('').toLowerCase(); - case 'upperCase': - return splittedByCaseString.join('').toUpperCase(); - case 'camelCase': - return splittedByCaseString - .map((subSequence, index) => - index == 0 - ? subSequence.toLowerCase() - : StringUtilsWord.formatWord(subSequence), - ) - .join(''); - case 'pascalCase': - return splittedByCaseString - .map((subSequence) => StringUtilsWord.formatWord(subSequence)) - .join(''); - case 'snakeCase': - return splittedByCaseString.join('_').toLowerCase(); + /** + * Returns a given string converted to PamelCase + * + * @param {string} str - The string to convert + * + * @example + * str: ThisIsMyExample + * returns: ThisIsMyExample + * @example + * str: thisIsMyExample + * returns: ThisIsMyExample + */ + public static toPascalCase(str: string): string { + return this.convertToCaseLogic('PascalCase', str); + } + + /** + * Returns a given string converted to snakeCase + * + * @param {string} str - The string to convert + * + * @example + * str: ThisIsMyExample + * returns: this_is_my_example + * @example + * str: thisIsMyExample + * returns: this_is_my_example + */ + public static toSnakeCase(str: string): string { + return this.convertToCaseLogic('SnakeCase', str); + } + + private static convertToCaseLogic(toCase: CaseName, str: string): string { + const correspondantKnownCase = knownCases.find( + (caseInstance: Case) => caseInstance.name == toCase, + ); + + // do not apply any process overload for nothing + if (!StringUtils.isConsiderableCharSequence(str)) return str; + + if (!str.includes(' ')) { + // str do not need blending operation + if (!this.determineCase(str)) return str; + + return correspondantKnownCase.basicConversionReturnFn( + this.splitByCase(str), + str, + ); //Apply stored behavior of correspondantKnownCase and return the processed value } + + // str need blending operation + + const removedBlankChars = StringUtils.removeBlankChars(str); + if (this.determineCase(removedBlankChars).name == toCase) { + //str is per any chance alraedy cased as wanted but needed to be cleanedFrom any blank chars + return removedBlankChars; + } + + return correspondantKnownCase.blendedConversionReturnFn( + this.splitByCase(str), + StringUtils.blendIrrelevantStringsInRelevantOnes(str), + str, + ); //Apply stored behavior of correspondantKnownCase and return the processed value } } diff --git a/src/case/Case.ts b/src/case/Case.ts new file mode 100644 index 0000000..c409544 --- /dev/null +++ b/src/case/Case.ts @@ -0,0 +1,42 @@ +export default abstract class Case { + protected abstract _matcher: RegExp; + protected abstract _splitter: RegExp | string; + + /** + * This method is a wrapper for conversion to targeted case (when `str` is already cased in managed one) + */ + public abstract basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string; + + /** + * This method is a wrapper for conversion to targeted case (when `str` contains spaces) + */ + public abstract blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string; + + /** + * Returns the name of class + */ + public get name(): string { + return this.constructor.name; + } + + /** + * Returns the matcher for Regex linked to targeted case class + */ + public get matcher(): RegExp { + return this._matcher; + } + + /** + * Returns the splitter for Regex or string linked to targeted case class + */ + public get splitter(): RegExp | string { + return this._splitter; + } +} diff --git a/src/case/camel-case.ts b/src/case/camel-case.ts new file mode 100644 index 0000000..715127f --- /dev/null +++ b/src/case/camel-case.ts @@ -0,0 +1,54 @@ +import Case from './Case'; +import StringUtilsCase from '../case'; +import StringUtilsWord from '../word'; +import { StringUtils } from '../main'; + +export default class CamelCase extends Case { + protected _matcher = /^[a-z]+(?:[A-Z][a-z]+)*$/; + protected _splitter = /([A-Z]+[a-z]*)/; + + /* + * `str` do not need blending operation (there's no spaces into it) + * from `splittedByCase`, for each subsequence + * (at this step, it should be atleast a subSequence of length >= 2: because of check if it's a considerable str) + * if current subSequence index is index 0, then lower it + * otherwise format the current subSequence as a word (c.f: with first char uppered and the rest lowered) + */ + public basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string { + return splittedByCase + .map((subSequence, index) => + index == 0 + ? subSequence.toLowerCase() + : StringUtilsWord.formatWord(subSequence), + ) + .join(''); + } + + /* + * `str` was blent (see StringUtils.blendIrrelevantStringsInRelevantOnes) and stored in `blended` + * if blended length < 2 (c.f: blend operation didn't produce more than 1 relevant string) then + * the table to work with in previous statement will be the result of splitted removed from blank chars + * version of `str` (which is never modified). + * Otherwise use the result of blend operation as table to work with (blended) + */ + public blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string { + return ( + blended.length < 2 + ? StringUtilsCase.splitByCase(StringUtils.removeBlankChars(str)) + : blended + ) + .map((subSequence, index) => + index == 0 + ? subSequence.toLowerCase() + : StringUtilsWord.formatWord(subSequence), + ) + .join(''); + } +} diff --git a/src/case/lower-case.ts b/src/case/lower-case.ts new file mode 100644 index 0000000..094edea --- /dev/null +++ b/src/case/lower-case.ts @@ -0,0 +1,21 @@ +import Case from './Case'; + +export default class LowerCase extends Case { + protected _matcher = /^[a-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\s]*$/; + protected _splitter = ''; + + public basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string { + return str.toLowerCase(); + } + + public blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string { + return str.toLowerCase(); + } +} diff --git a/src/case/pascal-case.ts b/src/case/pascal-case.ts new file mode 100644 index 0000000..505b830 --- /dev/null +++ b/src/case/pascal-case.ts @@ -0,0 +1,31 @@ +import Case from './Case'; +import StringUtilsWord from '../word'; +import { StringUtils } from '../main'; + +export default class PascalCase extends Case { + protected _matcher = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/; + protected _splitter = /([A-Z]+[a-z]*)/; + + public basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string { + return StringUtils.removeBlankChars( + !StringUtils.containsConsiderableCharSequence(splittedByCase) + ? StringUtilsWord.formatWord(str) + : StringUtilsWord.formatWords(splittedByCase), + ); + } + + public blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string { + return StringUtils.removeBlankChars( + blended.length < 2 + ? StringUtilsWord.formatWord(str) + : StringUtilsWord.formatWords(blended), + ); + } +} diff --git a/src/case/snake-case.ts b/src/case/snake-case.ts new file mode 100644 index 0000000..d869412 --- /dev/null +++ b/src/case/snake-case.ts @@ -0,0 +1,27 @@ +import Case from './Case'; +import StringUtilsCase from '../case'; +import StringUtilsWord from '../word'; + +export default class SnakeCase extends Case { + protected _matcher = /(\w+)_(\w+)/; + protected _splitter = '_'; + + public basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string { + return splittedByCase.join('_').toLowerCase(); + } + + public blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string { + return blended.length <= 2 + ? StringUtilsCase.splitByCase(blended.join('')).join('_').toLowerCase() + : StringUtilsWord.normalizeSpacesBetweenWords( + blended.join('_'), + ).toLowerCase(); + } +} diff --git a/src/case/upper-case.ts b/src/case/upper-case.ts new file mode 100644 index 0000000..3b41f32 --- /dev/null +++ b/src/case/upper-case.ts @@ -0,0 +1,21 @@ +import Case from './Case'; + +export default class UpperCase extends Case { + protected _matcher = /^[A-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\s]*$/; + protected _splitter = ''; + + public basicConversionReturnFn( + splittedByCase: string[], + str: string, + ): string { + return str.toUpperCase(); + } + + public blendedConversionReturnFn( + splittedByCase: string[], + blended: string[], + str: string, + ): string { + return str.toUpperCase(); + } +} diff --git a/src/index.ts b/src/index.ts index 5f95ffc..d334e91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export * from './main'; export * from './case'; -export * from './word-ending-utils'; +export * from './word'; diff --git a/src/main.ts b/src/main.ts index e100ef8..a82ea74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,5 @@ +import StringUtilsWord from './word'; + export class StringUtils { /** * Check if a given string is blank or not @@ -9,6 +11,73 @@ export class StringUtils { return str.trim().replaceAll(' ', '').length == 0; } + /** + * Returns a boolean indicating if given string is atleast 2 lengthed string + * + * @param {string} str + * @example + * str: ' t' + * returns: false + * @example + * str: 'test' + * returns: true + * @example + * str: 'of ' + * returns: true + */ + public static isConsiderableCharSequence(str: string): boolean { + return str.trim().replaceAll(' ', '').length >= 2; + } + + /** + * Returns a boolean indicating if a given string table contains atleast one considerable subsequence + * + * @param {string[]} stringTable - Should be a table of string + * + * @example + * stringTable: ['This', 'is', 'my', 'test'] + * retunrs: true + * @example + * stringTable: [' ', ' ', ' '] + * retunrs: false + * @example + * stringTable: ['t', 'h', 'i'] + * retruns: false + */ + public static containsConsiderableCharSequence( + stringTable: string[], + ): boolean { + if (this.isBlank(stringTable.join(''))) return false; + return ( + stringTable.find((subsequence) => + this.isConsiderableCharSequence(subsequence), + ) != undefined + ); + } + + /** + * Returns a boolean indicating if a given string table contains only considerable subsequence + * + * @param {string[]} stringTable - Should be a table of string + * + * @example + * stringTable: ['This', 'is', 'my', 'example'] + * returns: true + * @example + * stringTable: ['This', 'i', 's', 'my', 'example'] + * returns: false + */ + public static containsOnlyConsiderableCharSequences( + stringTable: string[], + ): boolean { + if (this.isBlank(stringTable.join(''))) return false; + return ( + stringTable.find( + (subsequence) => !this.isConsiderableCharSequence(subsequence), + ) == undefined + ); + } + /** * Replace a subsequent string at a given position by another string in a given string * @@ -34,4 +103,63 @@ export class StringUtils { return strArray.join(''); } + + /** + * Return a string removed from all blank chars + * + * @param {string} str - The string of which remove blank spaces + * + * @example + * str: This is my example + * returns: Thisismyexample + */ + public static removeBlankChars(str: string): string { + return str.replaceAll(' ', ''); + } + + /** + * Returns a table of string where each element is at least 2 length char + * + * @param {string} str - Should be a string containing spaces and at least 2 letters + * + * @remarks + * This method works only if str starts with a relevant / considerable sub string sequence, it should be rework later to manage following relevant sequences + * + * @example + * str: 'This is my ex a m p l e' + * returns: ['This', 'is', 'my', 'example'] + * @example + * str: 'T h i s m y e x a m p l e' + * returns: ['Thisismyexample'] + * @example + * str: 'Thi s is my exa mple' + * returns: ['This is my exa mple'] + * + */ + public static blendIrrelevantStringsInRelevantOnes(str: string): string[] { + const splittedStr = + StringUtilsWord.normalizeSpacesBetweenWords(str).split(' '); + + if (!this.containsConsiderableCharSequence(splittedStr)) + return [this.removeBlankChars(str)]; + + if (this.containsOnlyConsiderableCharSequences(splittedStr)) + return splittedStr; + + const revelantSubSequences: string[] = []; + + /* If the current subsequence is relevant, push it to revelants table + * Otherwise append the current one to the last relevant subsequence + */ + + splittedStr.forEach((subSequence) => { + if (this.isConsiderableCharSequence(subSequence)) + revelantSubSequences.push(subSequence); + else + revelantSubSequences[revelantSubSequences.length - 1] += + `${subSequence}`; + }); + + return revelantSubSequences; + } } diff --git a/src/word-ending-utils.ts b/src/word.ts similarity index 97% rename from src/word-ending-utils.ts rename to src/word.ts index 9abcfb3..a3629c7 100644 --- a/src/word-ending-utils.ts +++ b/src/word.ts @@ -130,7 +130,7 @@ export default class StringUtilsWord { * returns: Passes */ public static pluralize(word: string): string { - if (word.trim().replaceAll(' ', '').length < 2 || this.isPlural(word)) + if (!StringUtils.isConsiderableCharSequence(word) || this.isPlural(word)) return word; const wordEnding = this.getCorrespondingEnding(word); @@ -150,7 +150,7 @@ export default class StringUtilsWord { * returns: Pass */ public static singularize(word: string): string { - if (word.trim().replaceAll(' ', '').length < 2 || this.isSingular(word)) + if (!StringUtils.isConsiderableCharSequence(word) || this.isSingular(word)) return word; const wordEnding = this.getCorrespondingEnding(word); diff --git a/test/case.spec.ts b/test/case.spec.ts index 59b11da..b4dbdcd 100644 --- a/test/case.spec.ts +++ b/test/case.spec.ts @@ -1,55 +1,100 @@ -import StringUtilsCase, { Case } from '../src/case'; +import StringUtilsCase, { CaseName } from '../src/case'; +import JestRunner from './test.utils'; + +const runner = new JestRunner(StringUtilsCase); describe('Casing operation', () => { - const samples = new Map([ - ['camelCase', 'thisIsMyTest'], - ['pascalCase', 'ThisIsMyTest'], - ['snakeCase', 'this_is_my_test'], - ['upperCase', 'THISISMY23 TEST'], - ['lowerCase', 'thisis02my test'], - [undefined, 'thisisATEST'], - ]); + runner.runBasicTests( + StringUtilsCase.determineCase, + new Map([ + ['thisIsMyTest', 'CamelCase'], + ['ThisIsMyTest', 'PascalCase'], + ['this_is_my_test', 'SnakeCase'], + ['THISISMY23 TEST', 'UpperCase'], + ['thisis02my test', 'LowerCase'], + ['thisisATEST', undefined], + ]), + 'name', + ); - for (const key of samples.keys()) { - test(`Should return '${key}'`, () => { - expect(StringUtilsCase.determineCase(samples.get(key)!)?.name).toBe(key); - }); - } + runner.runBasicTests( + StringUtilsCase.splitByCase, + new Map([ + ['thisIsMyTest', ['this', 'Is', 'My', 'Test']], + ['ThisIsMyTest', ['This', 'Is', 'My', 'Test']], + ['this_is_my_test', ['this', 'is', 'my', 'test']], + ['This is my test', ['This is my test']], + ['THIS', ['T', 'H', 'I', 'S']], + ['this', ['t', 'h', 'i', 's']], + ['this is a test', 'this is a test'.split('')], + ['THIS IS A TEST', 'THIS IS A TEST'.split('')], + ]), + ); - const splittedByCase = new Map([ - ['thisIsMyTest', ['this', 'Is', 'My', 'Test']], - ['ThisIsMyTest', ['This', 'Is', 'My', 'Test']], - ['this_is_my_test', ['this', 'is', 'my', 'test']], - ['This is my test', ['This is my test']], - ['THIS', ['T', 'H', 'I', 'S']], - ['this', ['t', 'h', 'i', 's']], - ['this is a test', 'this is a test'.split('')], - ['THIS IS A TEST', 'THIS IS A TEST'.split('')], - ]); + runner.runBasicTests( + StringUtilsCase.convertToCase, + new Map([ + [() => ['thisIsMyTest', 'SnakeCase'], 'this_is_my_test'], + [() => ['thisIsMyTest', 'CamelCase'], 'thisIsMyTest'], + [() => ['thisIsMyTest', 'PascalCase'], 'ThisIsMyTest'], + [() => ['thisIsMyTest', 'LowerCase'], 'thisismytest'], + [() => ['thisIsMyTest', 'UpperCase'], 'THISISMYTEST'], + [() => ['a', 'CamelCase'], 'a'], + [() => ['this', 'CamelCase'], 'tHIS'], + [() => ['th', 'CamelCase'], 'tH'], + [() => ['thisISMYTEST', 'CamelCase'], 'thisISMYTEST'], + ]), + ); - for (const key of splittedByCase.keys()) { - test(`Should return '[${splittedByCase.get(key)!.toString()}]'`, () => { - expect(StringUtilsCase.splitByCase(key)).toEqual( - splittedByCase.get(key)!, - ); - }); - } + runner.runBasicTests( + StringUtilsCase.toCamelCase, + new Map([ + ['thisIsMyTest', 'thisIsMyTest'], + ['ThisIsMyTest', 'thisIsMyTest'], + ['', ''], + ['th', 'tH'], + [' th', 'tH'], + ['thisIsM y Tes t', 'thisIsMyTest'], + ['this_is_my_test', 'thisIsMyTest'], + ['this is my test', 'thisIsMyTest'], + ['ThisIsMyTest', 'thisIsMyTest'], + ['This Is My Test', 'thisIsMyTest'], + ['thisISMYTEST', 'thisISMYTEST'], + ]), + ); - const convertToCaseExpectedReturns = new Map<[string, Case], string>([ - [['thisIsMyTest', 'snakeCase'], 'this_is_my_test'], - [['thisIsMyTest', 'camelCase'], 'thisIsMyTest'], - [['thisIsMyTest', 'pascalCase'], 'ThisIsMyTest'], - [['thisIsMyTest', 'lowerCase'], 'thisismytest'], - [['thisIsMyTest', 'upperCase'], 'THISISMYTEST'], - [['a', 'camelCase'], 'a'], - [['this', 'camelCase'], 'tHIS'], - [['th', 'camelCase'], 'tH'], - [['thisISMYTEST', 'camelCase'], 'thisISMYTEST'], - ]); + runner.runBasicTests( + StringUtilsCase.toPascalCase, + new Map([ + ['thisIsMyTest', 'ThisIsMyTest'], + ['ThisIsMyTest', 'ThisIsMyTest'], + ['', ''], + ['th', 'Th'], + [' th', 'Th'], + ['thisIsM y Tes t', 'ThisIsMyTest'], + ['this_is_my_test', 'ThisIsMyTest'], + ['this is my test', 'ThisIsMyTest'], + ['ThisIsMyTest', 'ThisIsMyTest'], + ['This Is My Test', 'ThisIsMyTest'], + ['thisISMYTEST', 'thisISMYTEST'], + ]), + ); - for (const [key, value] of convertToCaseExpectedReturns.entries()) { - test(`Should return '${value}' for '${key}'`, () => { - expect(StringUtilsCase.convertToCase(...key)).toBe(value); - }); - } + runner.runBasicTests( + StringUtilsCase.toSnakeCase, + new Map([ + ['thisIsMyTest', 'this_is_my_test'], + ['ThisIsMyTest', 'this_is_my_test'], + ['', ''], + ['th', 't_h'], + [' th', 't_h'], + ['thisIsM y Tes t', 'this_is_my_test'], + ['this_is_my_test', 'this_is_my_test'], + ['this is my test', 'this_is_my_test'], + ['ThisIsMyTest', 'this_is_my_test'], + ['This Is My Test', 'this_is_my_test'], + ['thisISMYTEST', 'thisISMYTEST'], + ['this _ is _ my _ test', 'this_is_my_test'], + ]), + ); }); diff --git a/test/main.spec.ts b/test/main.spec.ts index a0d517c..5570b47 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -1,30 +1,73 @@ import { StringUtils } from '../src/main'; +import JestRunner from './test.utils'; -const isBlankExpectedReturns = new Map([ - ['hey', false], - [' hey ', false], - [' ', true], - ['', true], -]); - -for (const strKey of isBlankExpectedReturns.keys()) { - test(`Should return '${isBlankExpectedReturns.get(strKey)!}' for str = '${strKey}'`, () => { - expect(StringUtils.isBlank(strKey)).toBe( - isBlankExpectedReturns.get(strKey)!, - ); - }); -} - -const replaceAtExpectedReturns = new Map([ - [['This', 2, 'hello'], 'Thhellos'], - [['Hello', 18, 'hi'], 'Hello'], - [['Hi', -2, 'hello'], 'Hi'], -]); - -for (const key of replaceAtExpectedReturns.keys()) { - test(`Should return ${replaceAtExpectedReturns.get(key)!} for '${key}'`, () => { - expect(StringUtils.replaceAt(key[0], key[1], key[2])).toBe( - replaceAtExpectedReturns.get(key)!, - ); - }); -} +const runner = new JestRunner(StringUtils); + +runner.runBasicTests( + StringUtils.isBlank, + new Map([ + ['hey', false], + [' hey ', false], + [' ', true], + ['', true], + ]), +); + +runner.runBasicTests( + StringUtils.replaceAt, + new Map([ + [() => ['This', 2, 'hello'], 'Thhellos'], + [() => ['Hello', 18, 'hi'], 'Hello'], + [() => ['Hi', -2, 'hello'], 'Hi'], + ]), +); + +runner.runBasicTests( + StringUtils.isConsiderableCharSequence, + new Map([ + [' t', false], + ['of', true], + [' of', true], + ['of ', true], + ['f ', false], + ]), +); + +runner.runBasicTests( + StringUtils.removeBlankChars, + new Map([ + ['This is a test', 'Thisisatest'], + ['Thisisatest', 'Thisisatest'], + [' ', ''], + ['', ''], + ]), +); + +runner.runBasicTests( + StringUtils.blendIrrelevantStringsInRelevantOnes, + new Map([ + ['This is my ex a m p l e', ['This', 'is', 'my', 'example']], + ['T h i s i s m y e x a m p l e', ['Thisismyexample']], + ['This is my ex a m pl e ', ['This', 'is', 'my', 'exam', 'ple']], + ]), +); + +runner.runBasicTests( + StringUtils.containsConsiderableCharSequence, + new Map([ + [['This', 'is', 'my', 'test'], true], + [[' ', ' t ', ' h '], false], + [['t', 'h', 'i'], false], + [[' ', ' '], false], + [['T', 'h', 'is'], true], + ]), +); + +runner.runBasicTests( + StringUtils.containsOnlyConsiderableCharSequences, + new Map([ + ['this is my example'.split(' '), true], + ['this i s my example'.split(' '), false], + [' '.split(' '), false], + ]), +); diff --git a/test/test.utils.ts b/test/test.utils.ts new file mode 100644 index 0000000..21935cb --- /dev/null +++ b/test/test.utils.ts @@ -0,0 +1,43 @@ +interface Type extends Function { + new (...args: any[]): T; +} + +export default class JestRunner { + private _classToInvoke: Type; + + constructor(classToInvoke: Type) { + this._classToInvoke = classToInvoke; + } + + public runBasicTests( + fn: Function, + expectedReturns: Map, + inputPropertiesToTestName: string = null, + ): void { + this.checkInvokation(fn); + + for (const [input, output] of expectedReturns.entries()) { + test(`[${fn.name}] Should return '${output} for '${input}''`, () => { + (inputPropertiesToTestName && output + ? expect( + this._classToInvoke[fn.name]( + ...(typeof input == 'function' ? input() : [input]), + )[inputPropertiesToTestName], + ) + : expect( + this._classToInvoke[fn.name]( + ...(typeof input == 'function' ? input() : [input]), + ), + ))[typeof output === 'object' ? 'toEqual' : 'toBe'](output); // if output is a complexe object use 'toEqual' otherwise 'toBe' + }); + } + } + + private checkInvokation(fn: Function) { + // Throw error if _classToInvoke doesn't describe functionToTest or if prototype of functionToTest & function identified with name in _classToInvoke + if (!this._classToInvoke[fn.name] || fn !== this._classToInvoke[fn.name]) + throw new Error( + `${this._classToInvoke.name}.${fn.name} is not the expected tested one`, + ); + } +} diff --git a/test/words.spec.ts b/test/words.spec.ts index 20ee1f0..2f1fbaa 100644 --- a/test/words.spec.ts +++ b/test/words.spec.ts @@ -1,95 +1,88 @@ -import StringUtilsWord, { IWordEnding } from '../src/word-ending-utils'; +import StringUtilsWord, { IWordEnding } from '../src/word'; +import JestRunner from './test.utils'; -describe('Get word ending', () => { - const getWordEndingReturns = new Map([ - ['Pass', 'ss'], - ['Passes', 'sses'], - ['Category', 'y'], - ['Categories', 'ies'], - ['Bees', 'es'], - ['Bee', 'e'], - ['Cars', 's'], - ['Bet', ''], - ]); +const runner = new JestRunner(StringUtilsWord); - for (const key of getWordEndingReturns.keys()) { - test(`Should return '${getWordEndingReturns.get(key)!}' for word = '${key}'`, () => { - expect(StringUtilsWord.getWordEnding(key)).toEqual( - getWordEndingReturns.get(key)!, - ); - }); - } - - const getCorrespondingEndingReturns = new Map([ - [ - 'Passes', - { - pluralForm: 'sses', - singularForm: 'ss', - }, - ], - [ - 'Pass', - { - pluralForm: 'sses', - singularForm: 'ss', - }, - ], - [ - 'Categories', - { - pluralForm: 'ies', - singularForm: 'y', - }, - ], - [ - 'Category', - { - pluralForm: 'ies', - singularForm: 'y', - }, - ], - [ - 'Bees', - { - pluralForm: 'es', - singularForm: 'e', - }, - ], - [ - 'Bee', - { - pluralForm: 'es', - singularForm: 'e', - }, - ], - [ - 'Cars', - { - pluralForm: 's', - singularForm: '', - }, - ], - [ - 'Car', - { - pluralForm: 's', - singularForm: '', - }, - ], - ]); +describe('Get word ending', () => { + runner.runBasicTests( + StringUtilsWord.getWordEnding, + new Map([ + ['Pass', 'ss'], + ['Passes', 'sses'], + ['Category', 'y'], + ['Categories', 'ies'], + ['Bees', 'es'], + ['Bee', 'e'], + ['Cars', 's'], + ['Bet', ''], + ]), + ); - for (const key of getCorrespondingEndingReturns.keys()) { - test(`Should return correct IWordEnding object for word = '${key}'`, () => { - expect(StringUtilsWord.getCorrespondingEnding(key)).toEqual( - getCorrespondingEndingReturns.get(key)!, - ); - }); - } + runner.runBasicTests( + StringUtilsWord.getCorrespondingEnding, + new Map([ + [ + 'Passes', + { + pluralForm: 'sses', + singularForm: 'ss', + }, + ], + [ + 'Pass', + { + pluralForm: 'sses', + singularForm: 'ss', + }, + ], + [ + 'Categories', + { + pluralForm: 'ies', + singularForm: 'y', + }, + ], + [ + 'Category', + { + pluralForm: 'ies', + singularForm: 'y', + }, + ], + [ + 'Bees', + { + pluralForm: 'es', + singularForm: 'e', + }, + ], + [ + 'Bee', + { + pluralForm: 'es', + singularForm: 'e', + }, + ], + [ + 'Cars', + { + pluralForm: 's', + singularForm: '', + }, + ], + [ + 'Car', + { + pluralForm: 's', + singularForm: '', + }, + ], + ]), + ); }); describe('Is singular or plural', () => { - const isPluralReturns = new Map([ + const isPluralExpectedReturns = new Map([ ['Pass', false], ['Passes', true], ['Category', false], @@ -100,109 +93,81 @@ describe('Is singular or plural', () => { ['Bet', false], ]); - for (const key of isPluralReturns.keys()) { - test(`Should return '${isPluralReturns.get(key)!}' for word = '${key}'`, () => { - expect(StringUtilsWord.isPlural(key)).toBe(isPluralReturns.get(key)!); - }); - } - - const isSingularReturns = new Map( - Array.from(isPluralReturns.keys()).map((key) => [ + const isSingularExpectedReturns = new Map( + Array.from(isPluralExpectedReturns.keys()).map((key) => [ key, - !isPluralReturns.get(key), + !isPluralExpectedReturns.get(key), ]), ); - for (const key of isSingularReturns.keys()) { - test(`Should return '${isSingularReturns.get(key)!}' for word = '${key}'`, () => { - expect(StringUtilsWord.isSingular(key)).toBe(isSingularReturns.get(key)!); - }); - } + runner.runBasicTests(StringUtilsWord.isPlural, isPluralExpectedReturns); + runner.runBasicTests(StringUtilsWord.isSingular, isSingularExpectedReturns); }); describe('Move from singular to plural and vice-versa', () => { - const pluralizeWords = new Map([ - ['Pass', 'Passes'], - ['Category', 'Categories'], - ['Car', 'Cars'], - ['Bee', 'Bees'], - ['', ''], - ['List', 'Lists'], - ['Lists', 'Lists'], - ['C', 'C'], - ['Of', 'Ofs'], - [' ', ' '], - ]); - - for (const key of pluralizeWords.keys()) { - test(`Should return '${pluralizeWords.get(key)!}' for word = '${key}'`, () => { - expect(StringUtilsWord.pluralize(key)).toBe(pluralizeWords.get(key)!); - }); - } - - const singularizeWords = new Map([ - ['Passes', 'Pass'], - ['Categories', 'Category'], - ['Cars', 'Car'], - ['Bees', 'Bee'], - ['', ''], - ['Lists', 'List'], - ['List', 'List'], - ['C', 'C'], - ['Ofs', 'Of'], - [' ', ' '], - ]); + runner.runBasicTests( + StringUtilsWord.pluralize, + new Map([ + ['Pass', 'Passes'], + ['Category', 'Categories'], + ['Car', 'Cars'], + ['Bee', 'Bees'], + ['', ''], + ['List', 'Lists'], + ['Lists', 'Lists'], + ['C', 'C'], + ['Of', 'Ofs'], + [' ', ' '], + ]), + ); - for (const key of singularizeWords.keys()) { - test(`Should return '${singularizeWords.get(key)!}' for word = '${key}'`, () => { - expect(StringUtilsWord.singularize(key)).toBe(singularizeWords.get(key)!); - }); - } + runner.runBasicTests( + StringUtilsWord.singularize, + new Map([ + ['Passes', 'Pass'], + ['Categories', 'Category'], + ['Cars', 'Car'], + ['Bees', 'Bee'], + ['', ''], + ['Lists', 'List'], + ['List', 'List'], + ['C', 'C'], + ['Ofs', 'Of'], + [' ', ' '], + ]), + ); }); describe('Normalization of stuffs', () => { - const formatWordExpectedReturns = new Map([ - ['this', 'This'], - [' this', ' This'], - ['', ''], - [' ', ' '], - ['This', 'This'], - ['this Is a test', 'This Is a test'], - ]); - - for (const key of formatWordExpectedReturns.keys()) { - test(`Should return '${formatWordExpectedReturns.get(key)}' for str = '${key}'`, () => { - expect(StringUtilsWord.formatWord(key)).toBe( - formatWordExpectedReturns.get(key)!, - ); - }); - } - - const normalizeSpacesBetweenWordsExpectedReturns = new Map([ - ['This is a test', 'This is a test'], - ['Hello ', 'Hello'], - [' Hello', 'Hello'], - ]); - - for (const key of normalizeSpacesBetweenWordsExpectedReturns.keys()) { - test(`Should return '${normalizeSpacesBetweenWordsExpectedReturns.get(key)!}' for str = '${key}'`, () => { - expect(StringUtilsWord.normalizeSpacesBetweenWords(key)).toBe( - normalizeSpacesBetweenWordsExpectedReturns.get(key)!, - ); - }); - } + runner.runBasicTests( + StringUtilsWord.formatWord, + new Map([ + ['this', 'This'], + [' this', ' This'], + ['', ''], + [' ', ' '], + ['This', 'This'], + ['this Is a test', 'This Is a test'], + ]), + ); - const formatWordsExpectedReturns = new Map([ - ['This is my test', 'This Is My Test'], - [['This', 'is', 'my', 'test'], 'This Is My Test'], - [[' ', ''], ' '], - [' ', ' '], - [['This ', 'is ', 'my ', 'test'], 'This Is My Test'], - ]); + runner.runBasicTests( + StringUtilsWord.normalizeSpacesBetweenWords, + new Map([ + ['This is a test', 'This is a test'], + ['Hello ', 'Hello'], + [' Hello', 'Hello'], + ]), + ); - for (const [key, value] of formatWordsExpectedReturns.entries()) { - test(`Should return '${value}' for '${key}'`, () => { - expect(StringUtilsWord.formatWords(key)).toEqual(value); - }); - } + runner.runBasicTests( + StringUtilsWord.formatWords, + new Map([ + ['This is my test', 'This Is My Test'], + [['This', 'is', 'my', 'test'], 'This Is My Test'], + [[' ', ''], ' '], + [' ', ' '], + [['This ', 'is ', 'my ', 'test'], 'This Is My Test'], + ]), + ); });