From acc9a9611150e6b395b58da4e8f5c53bc4a04229 Mon Sep 17 00:00:00 2001 From: Maverik Minett Date: Mon, 15 Sep 2025 20:49:09 -0400 Subject: [PATCH 1/4] update package json with website --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 01858cd..4ed1d32 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "agape", "string" ], + "homepage": "https://agape.dev", "repository": { "type": "git", - "url": "https://github.com/AgapeToolkit/agape-string" + "url": "https://github.com/AgapeToolkit/AgapeToolkit" }, "author": "Maverik Minett", "license": "MIT", From 44bebdfb7925a7b21f314a3cca046426fe5cf417 Mon Sep 17 00:00:00 2001 From: Maverik Minett Date: Mon, 15 Sep 2025 22:06:39 -0400 Subject: [PATCH 2/4] rename functions --- src/index.ts | 14 ++-- src/lib/functions/camelize.spec.ts | 50 ------------ src/lib/functions/kebabify.spec.ts | 80 ------------------- src/lib/functions/pascalize.spec.ts | 70 ---------------- src/lib/functions/pluralize.spec.ts | 68 ---------------- src/lib/functions/quantify.ts | 6 +- src/lib/functions/singularize.spec.ts | 47 ----------- src/lib/functions/snakify.spec.ts | 45 ----------- src/lib/functions/titalize.spec.ts | 56 ------------- src/lib/functions/toCamelCase.spec.ts | 50 ++++++++++++ .../functions/{camelize.ts => toCamelCase.ts} | 12 +-- src/lib/functions/toKebabCase.spec.ts | 80 +++++++++++++++++++ .../functions/{kebabify.ts => toKebabCase.ts} | 14 ++-- src/lib/functions/toPascalCase.spec.ts | 70 ++++++++++++++++ .../{pascalize.ts => toPascalCase.ts} | 12 +-- src/lib/functions/toPlural.spec.ts | 68 ++++++++++++++++ .../functions/{pluralize.ts => toPlural.ts} | 24 +++--- src/lib/functions/toSingular.spec.ts | 47 +++++++++++ .../{singularize.ts => toSingular.ts} | 16 ++-- src/lib/functions/toSnakeCase.spec.ts | 45 +++++++++++ .../functions/{snakify.ts => toSnakeCase.ts} | 12 +-- src/lib/functions/toTitleCase.spec.ts | 56 +++++++++++++ .../functions/{titalize.ts => toTitleCase.ts} | 12 +-- src/lib/functions/toWords.spec.ts | 58 ++++++++++++++ .../functions/{verbalize.ts => toWords.ts} | 14 ++-- src/lib/functions/verbalize.spec.ts | 58 -------------- 26 files changed, 542 insertions(+), 542 deletions(-) delete mode 100644 src/lib/functions/camelize.spec.ts delete mode 100644 src/lib/functions/kebabify.spec.ts delete mode 100644 src/lib/functions/pascalize.spec.ts delete mode 100644 src/lib/functions/pluralize.spec.ts delete mode 100644 src/lib/functions/singularize.spec.ts delete mode 100644 src/lib/functions/snakify.spec.ts delete mode 100644 src/lib/functions/titalize.spec.ts create mode 100644 src/lib/functions/toCamelCase.spec.ts rename src/lib/functions/{camelize.ts => toCamelCase.ts} (72%) create mode 100644 src/lib/functions/toKebabCase.spec.ts rename src/lib/functions/{kebabify.ts => toKebabCase.ts} (82%) create mode 100644 src/lib/functions/toPascalCase.spec.ts rename src/lib/functions/{pascalize.ts => toPascalCase.ts} (72%) create mode 100644 src/lib/functions/toPlural.spec.ts rename src/lib/functions/{pluralize.ts => toPlural.ts} (84%) create mode 100644 src/lib/functions/toSingular.spec.ts rename src/lib/functions/{singularize.ts => toSingular.ts} (85%) create mode 100644 src/lib/functions/toSnakeCase.spec.ts rename src/lib/functions/{snakify.ts => toSnakeCase.ts} (84%) create mode 100644 src/lib/functions/toTitleCase.spec.ts rename src/lib/functions/{titalize.ts => toTitleCase.ts} (73%) create mode 100644 src/lib/functions/toWords.spec.ts rename src/lib/functions/{verbalize.ts => toWords.ts} (83%) delete mode 100644 src/lib/functions/verbalize.spec.ts diff --git a/src/index.ts b/src/index.ts index 9a0d03b..935addd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ -export * from './lib/functions/camelize'; -export * from './lib/functions/kebabify'; -export * from './lib/functions/pascalize'; +export * from './lib/functions/toCamelCase'; +export * from './lib/functions/toKebabCase'; +export * from './lib/functions/toPascalCase'; export * from './lib/functions/quantify'; -export * from './lib/functions/singularize'; -export * from './lib/functions/snakify'; -export * from './lib/functions/titalize'; -export * from './lib/functions/verbalize'; \ No newline at end of file +export * from './lib/functions/toSingular'; +export * from './lib/functions/toSnakeCase'; +export * from './lib/functions/toTitleCase'; +export * from './lib/functions/toWords'; \ No newline at end of file diff --git a/src/lib/functions/camelize.spec.ts b/src/lib/functions/camelize.spec.ts deleted file mode 100644 index 623370b..0000000 --- a/src/lib/functions/camelize.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { camelize } from './camelize'; - -describe('camelize', () => { - it('should convert space-separated words to camelCase', () => { - expect(camelize('first name')).toEqual('firstName'); - expect(camelize('user profile id')).toEqual('userProfileId'); - }); - - it('should convert hyphenated words to camelCase', () => { - expect(camelize('first-name')).toEqual('firstName'); - expect(camelize('user-profile-id')).toEqual('userProfileId'); - }); - - it('should convert snake_case to camelCase', () => { - expect(camelize('first_name')).toEqual('firstName'); - expect(camelize('user_profile_id')).toEqual('userProfileId'); - }); - - it('should handle mixed delimiters', () => { - expect(camelize('user-profile_id')).toEqual('userProfileId'); - expect(camelize('user profile_id-name')).toEqual('userProfileIdName'); - }); - - it('should lowercase the first character', () => { - expect(camelize('First Name')).toEqual('firstName'); - }); - - it('should remove non-alphanumeric characters', () => { - expect(camelize('user@name!')).toEqual('userName'); - expect(camelize('hello.world')).toEqual('helloWorld'); - }); - - it('should handle numbers correctly', () => { - expect(camelize('version 2 id')).toEqual('version2Id'); - expect(camelize('api_2_response')).toEqual('api2Response'); - }); - - it('should return empty string for empty input', () => { - expect(camelize('')).toEqual(''); - }); - - it('should not change already camelCased input', () => { - expect(camelize('alreadyCamelCase')).toEqual('alreadyCamelCase'); - }); - - it('should handle a single word', () => { - expect(camelize('username')).toEqual('username'); - expect(camelize('Username')).toEqual('username'); - }); -}); \ No newline at end of file diff --git a/src/lib/functions/kebabify.spec.ts b/src/lib/functions/kebabify.spec.ts deleted file mode 100644 index e37b558..0000000 --- a/src/lib/functions/kebabify.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { kebabify } from './kebabify'; - -describe('kebabify', () => { - - it('should kebabify space-separated strings', () => { - expect(kebabify('Foo Bar')).toEqual('foo-bar'); - expect(kebabify('The Quick Brown Fox')).toEqual('the-quick-brown-fox'); - }); - - it('should kebabify camelCase', () => { - expect(kebabify('fooBar')).toEqual('foo-bar'); - expect(kebabify('getUserId')).toEqual('get-user-id'); - }); - - it('should kebabify PascalCase', () => { - expect(kebabify('FooBar')).toEqual('foo-bar'); - expect(kebabify('HTMLParser')).toEqual('html-parser'); - expect(kebabify('GetUserID')).toEqual('get-user-id'); - }); - - it('should normalize existing kebab-case', () => { - expect(kebabify('foo-bar')).toEqual('foo-bar'); - }); - - it('should preserve multiple dashes', () => { - expect(kebabify('foo--bar')).toEqual('foo--bar'); - expect(kebabify('foo---bar---baz')).toEqual('foo---bar---baz'); - }); - - it('should convert multiple underscores to multiple hyphens', () => { - expect(kebabify('foo__bar')).toEqual('foo--bar'); - expect(kebabify('foo___bar___baz')).toEqual('foo---bar---baz'); - }); - - it('should convert snake_case to kebab-case', () => { - expect(kebabify('foo_bar')).toEqual('foo-bar'); - expect(kebabify('get_user_id')).toEqual('get-user-id'); - }); - - it('should remove special characters and preserve alphanumerics', () => { - expect(kebabify('foo@bar!baz')).toEqual('foo-bar-baz'); - expect(kebabify('hello.world')).toEqual('hello-world'); - expect(kebabify('what#the$heck')).toEqual('what-the-heck'); - }); - - it('should kebabify acronyms and numbers', () => { - expect(kebabify('APIResponse')).toEqual('api-response'); - expect(kebabify('userID2')).toEqual('user-id-2'); - expect(kebabify('html5Parser')).toEqual('html-5-parser'); - expect(kebabify('v2Response')).toEqual('v2-response'); - }); - - it('should preserve version tokens like v2, v1.0, v2.0.1', () => { - expect(kebabify('v2')).toEqual('v2'); - expect(kebabify('v1.0')).toEqual('v1-0'); - expect(kebabify('v2.0.1')).toEqual('v2-0-1'); - expect(kebabify('apiV1.0')).toEqual('api-v1-0'); - expect(kebabify('apiV2.0.1Response')).toEqual('api-v2-0-1-response'); - }); - - it('should kebabify mixed delimiters and normalize them', () => { - expect(kebabify('foo_bar-baz value')).toEqual('foo-bar-baz-value'); - expect(kebabify('foo_bar--baz')).toEqual('foo-bar--baz'); - }); - - it('should return empty string for empty input', () => { - expect(kebabify('')).toEqual(''); - }); - - it('should return a single lowercase word if input is a single word', () => { - expect(kebabify('FOO')).toEqual('foo'); - expect(kebabify('bar')).toEqual('bar'); - }); - - it('should strip leading and trailing dashes from messy input', () => { - expect(kebabify('---foo--bar---')).toEqual('foo--bar'); - expect(kebabify(' foo bar ')).toEqual('foo-bar'); - }); - -}); \ No newline at end of file diff --git a/src/lib/functions/pascalize.spec.ts b/src/lib/functions/pascalize.spec.ts deleted file mode 100644 index 87ad845..0000000 --- a/src/lib/functions/pascalize.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { pascalize } from './pascalize'; - -describe('pascalize', () => { - it('should convert space-separated words to PascalCase', () => { - expect(pascalize('first name')).toEqual('FirstName'); - expect(pascalize('api response')).toEqual('ApiResponse'); - }); - - it('should convert kebab-case to PascalCase', () => { - expect(pascalize('user-profile')).toEqual('UserProfile'); - expect(pascalize('api-response-code')).toEqual('ApiResponseCode'); - }); - - it('should convert snake_case to PascalCase', () => { - expect(pascalize('user_profile')).toEqual('UserProfile'); - expect(pascalize('api_response_code')).toEqual('ApiResponseCode'); - }); - - it('should convert mixed delimiters to PascalCase', () => { - expect(pascalize('user_profile-id')).toEqual('UserProfileId'); - expect(pascalize('user profile-id_name')).toEqual('UserProfileIdName'); - }); - - it('should remove non-alphanumeric characters', () => { - expect(pascalize('foo@bar!baz')).toEqual('FooBarBaz'); - expect(pascalize('hello.world')).toEqual('HelloWorld'); - expect(pascalize('this#is$clean')).toEqual('ThisIsClean'); - }); - - it('should preserve numeric prefix', () => { - expect(pascalize('123foo bar')).toEqual('123FooBar'); - expect(pascalize('456-api-response')).toEqual('456ApiResponse'); - }); - - it('should retain numbers elsewhere in the string', () => { - expect(pascalize('version 2')).toEqual('Version2'); - expect(pascalize('item_404')).toEqual('Item404'); - expect(pascalize('response code 500')).toEqual('ResponseCode500'); - }); - - it('should preserve clean PascalCase input', () => { - expect(pascalize('UserProfile')).toEqual('UserProfile'); - expect(pascalize('XMLHttpRequest')).toEqual('XMLHttpRequest'); - }); - - it('should capitalize the first letter of lowercase words', () => { - expect(pascalize('username')).toEqual('Username'); - expect(pascalize('profile')).toEqual('Profile'); - }); - - it('should trim and clean excess whitespace', () => { - expect(pascalize(' user profile ')).toEqual('UserProfile'); - expect(pascalize('\t api \n response')).toEqual('ApiResponse'); - }); - - it('should return empty string for empty input', () => { - expect(pascalize('')).toEqual(''); - expect(pascalize(' ')).toEqual(''); - }); - - it('should not break on single-character input', () => { - expect(pascalize('x')).toEqual('X'); - expect(pascalize('X')).toEqual('X'); - }); - - it('should not alter valid all-digit strings except trimming', () => { - expect(pascalize('123')).toEqual('123'); - expect(pascalize(' 123 ')).toEqual('123'); - }); -}); \ No newline at end of file diff --git a/src/lib/functions/pluralize.spec.ts b/src/lib/functions/pluralize.spec.ts deleted file mode 100644 index cae8b11..0000000 --- a/src/lib/functions/pluralize.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { pluralize } from './pluralize'; - - -describe('pluralize', () => { - - it('should pluralize regular nouns', () => { - expect(pluralize('dog')).toEqual('dogs'); - expect(pluralize('book')).toEqual('books'); - expect(pluralize('car')).toEqual('cars'); - }); - - it('should pluralize words ending in "y"', () => { - expect(pluralize('city')).toEqual('cities'); - expect(pluralize('puppy')).toEqual('puppies'); - }); - - it('should pluralize words ending in "s", "x", "z", "ch", "sh"', () => { - expect(pluralize('bus')).toEqual('buses'); - expect(pluralize('box')).toEqual('boxes'); - expect(pluralize('buzz')).toEqual('buzzes'); - expect(pluralize('watch')).toEqual('watches'); - expect(pluralize('dish')).toEqual('dishes'); - }); - - it('should pluralize known irregulars', () => { - expect(pluralize('child')).toEqual('children'); - expect(pluralize('person')).toEqual('people'); - expect(pluralize('mouse')).toEqual('mice'); - expect(pluralize('goose')).toEqual('geese'); - expect(pluralize('foot')).toEqual('feet'); - expect(pluralize('tooth')).toEqual('teeth'); - expect(pluralize('man')).toEqual('men'); - expect(pluralize('woman')).toEqual('women'); - expect(pluralize('analysis')).toEqual('analyses'); - expect(pluralize('cactus')).toEqual('cacti'); - }); - - it('should preserve initial capitalization', () => { - expect(pluralize('City')).toEqual('Cities'); - expect(pluralize('Bus')).toEqual('Buses'); - expect(pluralize('Child')).toEqual('Children'); - expect(pluralize('Person')).toEqual('People'); - expect(pluralize('Box')).toEqual('Boxes'); - }); - - it('should preserve all-uppercase acronyms', () => { - expect(pluralize('API')).toEqual('APIs'); - expect(pluralize('ID')).toEqual('IDs'); - expect(pluralize('HTML')).toEqual('HTMLs'); - }); - - it('should pluralize single-letter words', () => { - expect(pluralize('A')).toEqual('As'); - expect(pluralize('x')).toEqual('xs'); - }); - - it('should handle empty string', () => { - expect(pluralize('')).toEqual(''); - }); - - it('should pluralize words with existing "es" ending appropriately', () => { - expect(pluralize('thesis')).toEqual('theses'); - expect(pluralize('crisis')).toEqual('crises'); - expect(pluralize('diagnosis')).toEqual('diagnoses'); - expect(pluralize('syllabus')).toEqual('syllabi'); - }); - -}); \ No newline at end of file diff --git a/src/lib/functions/quantify.ts b/src/lib/functions/quantify.ts index eca77b8..82c7f14 100644 --- a/src/lib/functions/quantify.ts +++ b/src/lib/functions/quantify.ts @@ -1,11 +1,11 @@ -import { pluralize } from './pluralize'; +import { toPlural } from './toPlural'; /** * Formats a number with a unit, automatically pluralizing the unit based on the count. * * If the `count` is exactly 1, the singular `unit` is used. * Otherwise, it either uses the provided `plural` form or calls - * pluralize(unit) + * toPlural(unit) * to generate one. * * @example @@ -46,7 +46,7 @@ import { pluralize } from './pluralize'; export function quantify(count: number | string, unit: string, plural?: string) { const value = typeof count == 'number' ? count : Number(count); - const label = value === 1 ? unit : plural === undefined ? pluralize(unit) : plural; + const label = value === 1 ? unit : plural === undefined ? toPlural(unit) : plural; return `${count} ${label}` } diff --git a/src/lib/functions/singularize.spec.ts b/src/lib/functions/singularize.spec.ts deleted file mode 100644 index 402337f..0000000 --- a/src/lib/functions/singularize.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { singularize } from './singularize'; - -describe('singularize', () => { - it('should handle regular plurals ending in "s"', () => { - expect(singularize('cats')).toBe('cat'); - expect(singularize('tables')).toBe('table'); - }); - - it('should handle plurals ending in "ies"', () => { - expect(singularize('cities')).toBe('city'); - expect(singularize('bodies')).toBe('body'); - }); - - it('should handle plurals ending in "es" for certain patterns', () => { - expect(singularize('boxes')).toBe('box'); - expect(singularize('wishes')).toBe('wish'); - expect(singularize('buzzes')).toBe('buzz'); - expect(singularize('matches')).toBe('match'); - }); - - it('should handle known irregular plurals', () => { - expect(singularize('people')).toBe('person'); - expect(singularize('children')).toBe('child'); - expect(singularize('geese')).toBe('goose'); - expect(singularize('mice')).toBe('mouse'); - expect(singularize('analyses')).toBe('analysis'); - }); - - it('should preserve original casing', () => { - expect(singularize('People')).toBe('Person'); - expect(singularize('CHILDREN')).toBe('CHILD'); - expect(singularize('Geese')).toBe('Goose'); - expect(singularize('CITIES')).toBe('CITY'); - expect(singularize('Boxes')).toBe('Box'); - }); - - it('should not modify already singular words', () => { - expect(singularize('fish')).toBe('fish'); - expect(singularize('man')).toBe('man'); - expect(singularize('box')).toBe('box'); - }); - - it('should not overcorrect double "s" endings', () => { - expect(singularize('boss')).toBe('boss'); - expect(singularize('glass')).toBe('glass'); - }); -}); diff --git a/src/lib/functions/snakify.spec.ts b/src/lib/functions/snakify.spec.ts deleted file mode 100644 index fa2ccc1..0000000 --- a/src/lib/functions/snakify.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { snakify } from './snakify'; - -describe('snakify', () => { - it('converts camelCase to kebab-case', () => { - expect(snakify('camelCaseExample')).toBe('camel_case_example'); - }); - - it('handles PascalCase correctly', () => { - expect(snakify('PascalCase')).toBe('pascal_case'); - }); - - it('separates acronym and word', () => { - expect(snakify('HTMLParser')).toBe('html_parser'); - }); - - it('handles version tags correctly', () => { - expect(snakify('EmployeeV2')).toBe('employee_v2'); - expect(snakify('ApiV3Response')).toBe('api_v3_response'); - }); - - it('preserves number separation', () => { - expect(snakify('Version2Id')).toBe('version_2_id'); - expect(snakify('HTML5Parser')).toBe('html_5_parser'); - }); - - it('converts spaces to underscores', () => { - expect(snakify('this is spaced')).toBe('this_is_spaced'); - }); - - it('converts dashes and symbols to underscores', () => { - expect(snakify('hello-world!again')).toBe('hello_world_again'); - }); - - it('trims leading and trailing symbols', () => { - expect(snakify('__HelloWorld__')).toBe('hello_world'); - }); - - it('handles a complex example with everything', () => { - expect(snakify(' V2HTML5ApiResponse_v3.1! ')).toBe('v2_html_5_api_response_v3_1'); - }); - - it('returns empty string when input is empty', () => { - expect(snakify('')).toBe(''); - }); -}); diff --git a/src/lib/functions/titalize.spec.ts b/src/lib/functions/titalize.spec.ts deleted file mode 100644 index 1a39ab7..0000000 --- a/src/lib/functions/titalize.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { titalize } from './titalize'; - -describe('titalize', () => { - - it('should capitalize each significant word', () => { - expect(titalize('foo bar')).toEqual('Foo Bar'); - expect(titalize('quick brown fox')).toEqual('Quick Brown Fox'); - }); - - it('should preserve case after hyphens', () => { - expect(titalize('foo-bar')).toEqual('Foo-bar'); - expect(titalize('jack-in-the-box')).toEqual('Jack-in-the-box'); - }); - - it('should lowercase functional words except when first', () => { - expect(titalize('hop on pop')).toEqual('Hop on Pop'); - expect(titalize('ducks in a row')).toEqual('Ducks in a Row'); - expect(titalize('james and the anteater')).toEqual('James and the Anteater'); - expect(titalize('the wonderful world of mystery science')).toEqual('The Wonderful World of Mystery Science'); - expect(titalize('and then there were none')).toEqual('And Then There Were None'); - }); - - it('should capitalize functional words when first', () => { - expect(titalize('in the beginning')).toEqual('In the Beginning'); - expect(titalize('at the gates')).toEqual('At the Gates'); - expect(titalize('by the sea')).toEqual('By the Sea'); - }); - - it('should not change casing of punctuation', () => { - expect(titalize('who framed roger rabbit?')).toEqual('Who Framed Roger Rabbit?'); - expect(titalize('what time is it, mr. fox?')).toEqual('What Time Is It, Mr. Fox?'); - }); - - it('should handle mixed casing inputs by preserving case (seems intentional)', () => { - expect(titalize('ThE gIrL wItH tHe DrAgOn TaTtOo')).toEqual('ThE gIrL wItH tHe DrAgOn TaTtOo'); - }); - - it('should trim and clean excess whitespace', () => { - expect(titalize(' the jungle book ')).toEqual('The Jungle Book'); - expect(titalize('\nhop\non\npop\n')).toEqual('Hop on Pop'); - }); - - it('should work with single-word input', () => { - expect(titalize('frozen')).toEqual('Frozen'); - expect(titalize('and')).toEqual('And'); - }); - - it('should handle acronyms as capitalized', () => { - expect(titalize('the rise of NASA')).toEqual('The Rise of NASA'); - }); - - it('should handle empty string', () => { - expect(titalize('')).toEqual(''); - }); - -}); \ No newline at end of file diff --git a/src/lib/functions/toCamelCase.spec.ts b/src/lib/functions/toCamelCase.spec.ts new file mode 100644 index 0000000..fdf2b4e --- /dev/null +++ b/src/lib/functions/toCamelCase.spec.ts @@ -0,0 +1,50 @@ +import { toCamelCase } from './toCamelCase'; + +describe('toCamelCase', () => { + it('should convert space-separated words to camelCase', () => { + expect(toCamelCase('first name')).toEqual('firstName'); + expect(toCamelCase('user profile id')).toEqual('userProfileId'); + }); + + it('should convert hyphenated words to camelCase', () => { + expect(toCamelCase('first-name')).toEqual('firstName'); + expect(toCamelCase('user-profile-id')).toEqual('userProfileId'); + }); + + it('should convert snake_case to camelCase', () => { + expect(toCamelCase('first_name')).toEqual('firstName'); + expect(toCamelCase('user_profile_id')).toEqual('userProfileId'); + }); + + it('should handle mixed delimiters', () => { + expect(toCamelCase('user-profile_id')).toEqual('userProfileId'); + expect(toCamelCase('user profile_id-name')).toEqual('userProfileIdName'); + }); + + it('should lowercase the first character', () => { + expect(toCamelCase('First Name')).toEqual('firstName'); + }); + + it('should remove non-alphanumeric characters', () => { + expect(toCamelCase('user@name!')).toEqual('userName'); + expect(toCamelCase('hello.world')).toEqual('helloWorld'); + }); + + it('should handle numbers correctly', () => { + expect(toCamelCase('version 2 id')).toEqual('version2Id'); + expect(toCamelCase('api_2_response')).toEqual('api2Response'); + }); + + it('should return empty string for empty input', () => { + expect(toCamelCase('')).toEqual(''); + }); + + it('should not change already camelCased input', () => { + expect(toCamelCase('alreadyCamelCase')).toEqual('alreadyCamelCase'); + }); + + it('should handle a single word', () => { + expect(toCamelCase('username')).toEqual('username'); + expect(toCamelCase('Username')).toEqual('username'); + }); +}); diff --git a/src/lib/functions/camelize.ts b/src/lib/functions/toCamelCase.ts similarity index 72% rename from src/lib/functions/camelize.ts rename to src/lib/functions/toCamelCase.ts index 80510d6..8b606e7 100644 --- a/src/lib/functions/camelize.ts +++ b/src/lib/functions/toCamelCase.ts @@ -4,33 +4,33 @@ * * @example * ```ts - * camelize("hello world"); // "helloWorld" + * toCamelCase("hello world"); // "helloWorld" * ``` * * @example * ```ts - * camelize("Hello_world"); // "helloWorld" + * toCamelCase("Hello_world"); // "helloWorld" * ``` * * @example * ```ts - * camelize("API_response_code"); // "apiResponseCode" + * toCamelCase("API_response_code"); // "apiResponseCode" * ``` * * @example * ```ts - * camelize("user-42-profile"); // "user42Profile" + * toCamelCase("user-42-profile"); // "user42Profile" * ``` * * @example * ```ts - * camelize("ThisIsALongVariableName"); // "thisIsALongVariableName" + * toCamelCase("ThisIsALongVariableName"); // "thisIsALongVariableName" * ``` * * @param input The input string to be camelized. * @returns The camelCased version of the input string. */ -export function camelize(input: string): string { +export function toCamelCase(input: string): string { const parts = input .replace(/[^A-Za-z0-9]+/g, ' ') .replace(/([0-9])([A-Za-z])/g, '$1 $2') // digit → letter diff --git a/src/lib/functions/toKebabCase.spec.ts b/src/lib/functions/toKebabCase.spec.ts new file mode 100644 index 0000000..06c3a8c --- /dev/null +++ b/src/lib/functions/toKebabCase.spec.ts @@ -0,0 +1,80 @@ +import { toKebabCase } from './toKebabCase'; + +describe('toKebabCase', () => { + + it('should kebabify space-separated strings', () => { + expect(toKebabCase('Foo Bar')).toEqual('foo-bar'); + expect(toKebabCase('The Quick Brown Fox')).toEqual('the-quick-brown-fox'); + }); + + it('should kebabify camelCase', () => { + expect(toKebabCase('fooBar')).toEqual('foo-bar'); + expect(toKebabCase('getUserId')).toEqual('get-user-id'); + }); + + it('should kebabify PascalCase', () => { + expect(toKebabCase('FooBar')).toEqual('foo-bar'); + expect(toKebabCase('HTMLParser')).toEqual('html-parser'); + expect(toKebabCase('GetUserID')).toEqual('get-user-id'); + }); + + it('should normalize existing kebab-case', () => { + expect(toKebabCase('foo-bar')).toEqual('foo-bar'); + }); + + it('should preserve multiple dashes', () => { + expect(toKebabCase('foo--bar')).toEqual('foo--bar'); + expect(toKebabCase('foo---bar---baz')).toEqual('foo---bar---baz'); + }); + + it('should convert multiple underscores to multiple hyphens', () => { + expect(toKebabCase('foo__bar')).toEqual('foo--bar'); + expect(toKebabCase('foo___bar___baz')).toEqual('foo---bar---baz'); + }); + + it('should convert snake_case to kebab-case', () => { + expect(toKebabCase('foo_bar')).toEqual('foo-bar'); + expect(toKebabCase('get_user_id')).toEqual('get-user-id'); + }); + + it('should remove special characters and preserve alphanumerics', () => { + expect(toKebabCase('foo@bar!baz')).toEqual('foo-bar-baz'); + expect(toKebabCase('hello.world')).toEqual('hello-world'); + expect(toKebabCase('what#the$heck')).toEqual('what-the-heck'); + }); + + it('should kebabify acronyms and numbers', () => { + expect(toKebabCase('APIResponse')).toEqual('api-response'); + expect(toKebabCase('userID2')).toEqual('user-id-2'); + expect(toKebabCase('html5Parser')).toEqual('html-5-parser'); + expect(toKebabCase('v2Response')).toEqual('v2-response'); + }); + + it('should preserve version tokens like v2, v1.0, v2.0.1', () => { + expect(toKebabCase('v2')).toEqual('v2'); + expect(toKebabCase('v1.0')).toEqual('v1-0'); + expect(toKebabCase('v2.0.1')).toEqual('v2-0-1'); + expect(toKebabCase('apiV1.0')).toEqual('api-v1-0'); + expect(toKebabCase('apiV2.0.1Response')).toEqual('api-v2-0-1-response'); + }); + + it('should kebabify mixed delimiters and normalize them', () => { + expect(toKebabCase('foo_bar-baz value')).toEqual('foo-bar-baz-value'); + expect(toKebabCase('foo_bar--baz')).toEqual('foo-bar--baz'); + }); + + it('should return empty string for empty input', () => { + expect(toKebabCase('')).toEqual(''); + }); + + it('should return a single lowercase word if input is a single word', () => { + expect(toKebabCase('FOO')).toEqual('foo'); + expect(toKebabCase('bar')).toEqual('bar'); + }); + + it('should strip leading and trailing dashes from messy input', () => { + expect(toKebabCase('---foo--bar---')).toEqual('foo--bar'); + expect(toKebabCase(' foo bar ')).toEqual('foo-bar'); + }); + +}); diff --git a/src/lib/functions/kebabify.ts b/src/lib/functions/toKebabCase.ts similarity index 82% rename from src/lib/functions/kebabify.ts rename to src/lib/functions/toKebabCase.ts index 0e2f4dd..4a7b380 100644 --- a/src/lib/functions/kebabify.ts +++ b/src/lib/functions/toKebabCase.ts @@ -8,38 +8,38 @@ import { PLACEHOLDER } from "../private/constants"; * * @example * ```ts - * kebabify("hello world"); // "hello-world" + * toKebabCase("hello world"); // "hello-world" * ``` * * @example * ```ts - * kebabify("first_name"); // "first-name" + * toKebabCase("first_name"); // "first-name" * ``` * * @example * ```ts - * kebabify("userProfile42"); // "user-profile-42" + * toKebabCase("userProfile42"); // "user-profile-42" * ``` * * @example * ```ts - * kebabify("APIResponseV2"); // "api-response-v2" + * toKebabCase("APIResponseV2"); // "api-response-v2" * ``` * * @example * ```ts - * kebabify("HTML5_Parser v3"); // "html-5-parser-v3" + * toKebabCase("HTML5_Parser v3"); // "html-5-parser-v3" * ``` * * @example * ```ts - * kebabify(" Leading_and trailing "); // "leading-and-trailing" + * toKebabCase(" Leading_and trailing "); // "leading-and-trailing" * ``` * * @param input A string to be converted to kebab-case. * @returns The kebab-cased version of the input string. */ -export function kebabify(input: string): string { +export function toKebabCase(input: string): string { const versionMatches: string[] = []; // Step 1: replace version tokens (v2) with unique placeholders diff --git a/src/lib/functions/toPascalCase.spec.ts b/src/lib/functions/toPascalCase.spec.ts new file mode 100644 index 0000000..8694d73 --- /dev/null +++ b/src/lib/functions/toPascalCase.spec.ts @@ -0,0 +1,70 @@ +import { toPascalCase } from './toPascalCase'; + +describe('toPascalCase', () => { + it('should convert space-separated words to PascalCase', () => { + expect(toPascalCase('first name')).toEqual('FirstName'); + expect(toPascalCase('api response')).toEqual('ApiResponse'); + }); + + it('should convert kebab-case to PascalCase', () => { + expect(toPascalCase('user-profile')).toEqual('UserProfile'); + expect(toPascalCase('api-response-code')).toEqual('ApiResponseCode'); + }); + + it('should convert snake_case to PascalCase', () => { + expect(toPascalCase('user_profile')).toEqual('UserProfile'); + expect(toPascalCase('api_response_code')).toEqual('ApiResponseCode'); + }); + + it('should convert mixed delimiters to PascalCase', () => { + expect(toPascalCase('user_profile-id')).toEqual('UserProfileId'); + expect(toPascalCase('user profile-id_name')).toEqual('UserProfileIdName'); + }); + + it('should remove non-alphanumeric characters', () => { + expect(toPascalCase('foo@bar!baz')).toEqual('FooBarBaz'); + expect(toPascalCase('hello.world')).toEqual('HelloWorld'); + expect(toPascalCase('this#is$clean')).toEqual('ThisIsClean'); + }); + + it('should preserve numeric prefix', () => { + expect(toPascalCase('123foo bar')).toEqual('123FooBar'); + expect(toPascalCase('456-api-response')).toEqual('456ApiResponse'); + }); + + it('should retain numbers elsewhere in the string', () => { + expect(toPascalCase('version 2')).toEqual('Version2'); + expect(toPascalCase('item_404')).toEqual('Item404'); + expect(toPascalCase('response code 500')).toEqual('ResponseCode500'); + }); + + it('should preserve clean PascalCase input', () => { + expect(toPascalCase('UserProfile')).toEqual('UserProfile'); + expect(toPascalCase('XMLHttpRequest')).toEqual('XMLHttpRequest'); + }); + + it('should capitalize the first letter of lowercase words', () => { + expect(toPascalCase('username')).toEqual('Username'); + expect(toPascalCase('profile')).toEqual('Profile'); + }); + + it('should trim and clean excess whitespace', () => { + expect(toPascalCase(' user profile ')).toEqual('UserProfile'); + expect(toPascalCase('\t api \n response')).toEqual('ApiResponse'); + }); + + it('should return empty string for empty input', () => { + expect(toPascalCase('')).toEqual(''); + expect(toPascalCase(' ')).toEqual(''); + }); + + it('should not break on single-character input', () => { + expect(toPascalCase('x')).toEqual('X'); + expect(toPascalCase('X')).toEqual('X'); + }); + + it('should not alter valid all-digit strings except trimming', () => { + expect(toPascalCase('123')).toEqual('123'); + expect(toPascalCase(' 123 ')).toEqual('123'); + }); +}); diff --git a/src/lib/functions/pascalize.ts b/src/lib/functions/toPascalCase.ts similarity index 72% rename from src/lib/functions/pascalize.ts rename to src/lib/functions/toPascalCase.ts index d2b932a..1340e94 100644 --- a/src/lib/functions/pascalize.ts +++ b/src/lib/functions/toPascalCase.ts @@ -6,33 +6,33 @@ * * @example * ```ts - * pascalize("hello world"); // "HelloWorld" + * toPascalCase("hello world"); // "HelloWorld" * ``` * * @example * ```ts - * pascalize("first_name"); // "FirstName" + * toPascalCase("first_name"); // "FirstName" * ``` * * @example * ```ts - * pascalize("user42Profile"); // "User42Profile" + * toPascalCase("user42Profile"); // "User42Profile" * ``` * * @example * ```ts - * pascalize("API response code"); // "ApiResponseCode" + * toPascalCase("API response code"); // "ApiResponseCode" * ``` * * @example * ```ts - * pascalize(" messy__input--string "); // "MessyInputString" + * toPascalCase(" messy__input--string "); // "MessyInputString" * ``` * * @param input A string to be returned in PascalCase. * @returns The PascalCased version of the input string. */ -export function pascalize(input: string): string { +export function toPascalCase(input: string): string { const parts = input .replace(/[^A-Za-z0-9]+/g, ' ') .replace(/([0-9])([A-Za-z])/g, '$1 $2') // digit → letter diff --git a/src/lib/functions/toPlural.spec.ts b/src/lib/functions/toPlural.spec.ts new file mode 100644 index 0000000..cfca215 --- /dev/null +++ b/src/lib/functions/toPlural.spec.ts @@ -0,0 +1,68 @@ +import { toPlural } from './toPlural'; + + +describe('toPlural', () => { + + it('should pluralize regular nouns', () => { + expect(toPlural('dog')).toEqual('dogs'); + expect(toPlural('book')).toEqual('books'); + expect(toPlural('car')).toEqual('cars'); + }); + + it('should pluralize words ending in "y"', () => { + expect(toPlural('city')).toEqual('cities'); + expect(toPlural('puppy')).toEqual('puppies'); + }); + + it('should pluralize words ending in "s", "x", "z", "ch", "sh"', () => { + expect(toPlural('bus')).toEqual('buses'); + expect(toPlural('box')).toEqual('boxes'); + expect(toPlural('buzz')).toEqual('buzzes'); + expect(toPlural('watch')).toEqual('watches'); + expect(toPlural('dish')).toEqual('dishes'); + }); + + it('should pluralize known irregulars', () => { + expect(toPlural('child')).toEqual('children'); + expect(toPlural('person')).toEqual('people'); + expect(toPlural('mouse')).toEqual('mice'); + expect(toPlural('goose')).toEqual('geese'); + expect(toPlural('foot')).toEqual('feet'); + expect(toPlural('tooth')).toEqual('teeth'); + expect(toPlural('man')).toEqual('men'); + expect(toPlural('woman')).toEqual('women'); + expect(toPlural('analysis')).toEqual('analyses'); + expect(toPlural('cactus')).toEqual('cacti'); + }); + + it('should preserve initial capitalization', () => { + expect(toPlural('City')).toEqual('Cities'); + expect(toPlural('Bus')).toEqual('Buses'); + expect(toPlural('Child')).toEqual('Children'); + expect(toPlural('Person')).toEqual('People'); + expect(toPlural('Box')).toEqual('Boxes'); + }); + + it('should preserve all-uppercase acronyms', () => { + expect(toPlural('API')).toEqual('APIs'); + expect(toPlural('ID')).toEqual('IDs'); + expect(toPlural('HTML')).toEqual('HTMLs'); + }); + + it('should pluralize single-letter words', () => { + expect(toPlural('A')).toEqual('As'); + expect(toPlural('x')).toEqual('xs'); + }); + + it('should handle empty string', () => { + expect(toPlural('')).toEqual(''); + }); + + it('should pluralize words with existing "es" ending appropriately', () => { + expect(toPlural('thesis')).toEqual('theses'); + expect(toPlural('crisis')).toEqual('crises'); + expect(toPlural('diagnosis')).toEqual('diagnoses'); + expect(toPlural('syllabus')).toEqual('syllabi'); + }); + +}); diff --git a/src/lib/functions/pluralize.ts b/src/lib/functions/toPlural.ts similarity index 84% rename from src/lib/functions/pluralize.ts rename to src/lib/functions/toPlural.ts index f38710d..7353bb4 100644 --- a/src/lib/functions/pluralize.ts +++ b/src/lib/functions/toPlural.ts @@ -13,63 +13,63 @@ import { preserveCasing } from '../private/util'; * * @example * ```ts - * pluralize("city"); // "cities" + * toPlural("city"); // "cities" * ``` * * @example * ```ts - * pluralize("box"); // "boxes" + * toPlural("box"); // "boxes" * ``` * * @example * ```ts - * pluralize("child"); // "children" + * toPlural("child"); // "children" * ``` * * @example * ```ts - * pluralize("person"); // "people" + * toPlural("person"); // "people" * ``` * * @example * ```ts - * pluralize("API"); // "APIs" + * toPlural("API"); // "APIs" * ``` * * @example * ```ts - * pluralize("ID"); // "IDs" + * toPlural("ID"); // "IDs" * ``` * * @example * ```ts - * pluralize("File"); // "Files" + * toPlural("File"); // "Files" * ``` * * @example * ```ts - * pluralize("buzz"); // "buzzes" + * toPlural("buzz"); // "buzzes" * ``` * * @example * ```ts - * pluralize("CPU"); // "CPUs" + * toPlural("CPU"); // "CPUs" * ``` * * @example * ```ts - * pluralize("DOG"); // "DOGS" + * toPlural("DOG"); // "DOGS" * ``` * * @example * ```ts - * pluralize("A"); // "As" + * toPlural("A"); // "As" * ``` * * @param word A singular word to be pluralized. * @returns The plural form of the input word. */ -export function pluralize(word: string): string { +export function toPlural(word: string): string { if (word.length === 0) return ''; const lower = word.toLowerCase(); diff --git a/src/lib/functions/toSingular.spec.ts b/src/lib/functions/toSingular.spec.ts new file mode 100644 index 0000000..2f20ed3 --- /dev/null +++ b/src/lib/functions/toSingular.spec.ts @@ -0,0 +1,47 @@ +import { toSingular } from './toSingular'; + +describe('toSingular', () => { + it('should handle regular plurals ending in "s"', () => { + expect(toSingular('cats')).toBe('cat'); + expect(toSingular('tables')).toBe('table'); + }); + + it('should handle plurals ending in "ies"', () => { + expect(toSingular('cities')).toBe('city'); + expect(toSingular('bodies')).toBe('body'); + }); + + it('should handle plurals ending in "es" for certain patterns', () => { + expect(toSingular('boxes')).toBe('box'); + expect(toSingular('wishes')).toBe('wish'); + expect(toSingular('buzzes')).toBe('buzz'); + expect(toSingular('matches')).toBe('match'); + }); + + it('should handle known irregular plurals', () => { + expect(toSingular('people')).toBe('person'); + expect(toSingular('children')).toBe('child'); + expect(toSingular('geese')).toBe('goose'); + expect(toSingular('mice')).toBe('mouse'); + expect(toSingular('analyses')).toBe('analysis'); + }); + + it('should preserve original casing', () => { + expect(toSingular('People')).toBe('Person'); + expect(toSingular('CHILDREN')).toBe('CHILD'); + expect(toSingular('Geese')).toBe('Goose'); + expect(toSingular('CITIES')).toBe('CITY'); + expect(toSingular('Boxes')).toBe('Box'); + }); + + it('should not modify already singular words', () => { + expect(toSingular('fish')).toBe('fish'); + expect(toSingular('man')).toBe('man'); + expect(toSingular('box')).toBe('box'); + }); + + it('should not overcorrect double "s" endings', () => { + expect(toSingular('boss')).toBe('boss'); + expect(toSingular('glass')).toBe('glass'); + }); +}); diff --git a/src/lib/functions/singularize.ts b/src/lib/functions/toSingular.ts similarity index 85% rename from src/lib/functions/singularize.ts rename to src/lib/functions/toSingular.ts index 771008d..7ddfb1b 100644 --- a/src/lib/functions/singularize.ts +++ b/src/lib/functions/toSingular.ts @@ -13,43 +13,43 @@ import { preserveCasing } from '../private/util'; * * @example * ```ts - * singularize("cities"); // "city" + * toSingular("cities"); // "city" * ``` * * @example * ```ts - * singularize("boxes"); // "box" + * toSingular("boxes"); // "box" * ``` * * @example * ```ts - * singularize("children"); // "child" + * toSingular("children"); // "child" * ``` * * @example * ```ts - * singularize("teeth"); // "tooth" + * toSingular("teeth"); // "tooth" * ``` * * @example * ```ts - * singularize("dogs"); // "dog" + * toSingular("dogs"); // "dog" * ``` * * @example * ```ts - * singularize("BUZZES"); // "BUZZ" + * toSingular("BUZZES"); // "BUZZ" * ``` * * @example * ```ts - * singularize("IDs"); // "ID" + * toSingular("IDs"); // "ID" * ``` * * @param word Plural word to be converted to singular. * @returns The singular form of the input word. */ -export function singularize(word: string): string { +export function toSingular(word: string): string { const lower = word.toLowerCase(); const irregulars: Record = { diff --git a/src/lib/functions/toSnakeCase.spec.ts b/src/lib/functions/toSnakeCase.spec.ts new file mode 100644 index 0000000..b9cd9cc --- /dev/null +++ b/src/lib/functions/toSnakeCase.spec.ts @@ -0,0 +1,45 @@ +import { toSnakeCase } from './toSnakeCase'; + +describe('toSnakeCase', () => { + it('converts camelCase to snake_case', () => { + expect(toSnakeCase('camelCaseExample')).toBe('camel_case_example'); + }); + + it('handles PascalCase correctly', () => { + expect(toSnakeCase('PascalCase')).toBe('pascal_case'); + }); + + it('separates acronym and word', () => { + expect(toSnakeCase('HTMLParser')).toBe('html_parser'); + }); + + it('handles version tags correctly', () => { + expect(toSnakeCase('EmployeeV2')).toBe('employee_v2'); + expect(toSnakeCase('ApiV3Response')).toBe('api_v3_response'); + }); + + it('preserves number separation', () => { + expect(toSnakeCase('Version2Id')).toBe('version_2_id'); + expect(toSnakeCase('HTML5Parser')).toBe('html_5_parser'); + }); + + it('converts spaces to underscores', () => { + expect(toSnakeCase('this is spaced')).toBe('this_is_spaced'); + }); + + it('converts dashes and symbols to underscores', () => { + expect(toSnakeCase('hello-world!again')).toBe('hello_world_again'); + }); + + it('trims leading and trailing symbols', () => { + expect(toSnakeCase('__HelloWorld__')).toBe('hello_world'); + }); + + it('handles a complex example with everything', () => { + expect(toSnakeCase(' V2HTML5ApiResponse_v3.1! ')).toBe('v2_html_5_api_response_v3_1'); + }); + + it('returns empty string when input is empty', () => { + expect(toSnakeCase('')).toBe(''); + }); +}); diff --git a/src/lib/functions/snakify.ts b/src/lib/functions/toSnakeCase.ts similarity index 84% rename from src/lib/functions/snakify.ts rename to src/lib/functions/toSnakeCase.ts index 9bfa036..8cee4ec 100644 --- a/src/lib/functions/snakify.ts +++ b/src/lib/functions/toSnakeCase.ts @@ -9,33 +9,33 @@ import { PLACEHOLDER } from '../private/constants'; * * @example * ```ts - * snakify("hello world"); // "hello_world" + * toSnakeCase("hello world"); // "hello_world" * ``` * * @example * ```ts - * snakify("UserProfileV2"); // "user_profile_v2" + * toSnakeCase("UserProfileV2"); // "user_profile_v2" * ``` * * @example * ```ts - * snakify("HTML5 Parser"); // "html_5_parser" + * toSnakeCase("HTML5 Parser"); // "html_5_parser" * ``` * * @example * ```ts - * snakify(" messy-input_string "); // "messy_input_string" + * toSnakeCase(" messy-input_string "); // "messy_input_string" * ``` * * @example * ```ts - * snakify("ReportV3Final"); // "report_v3_final" + * toSnakeCase("ReportV3Final"); // "report_v3_final" * ``` * * @param input A string to be converted to snake_case. * @returns The snake_cased version of the input. */ -export function snakify(input: string): string { +export function toSnakeCase(input: string): string { const versionMatches: string[] = []; // Step 1: replace version tokens (v2) with unique placeholders diff --git a/src/lib/functions/toTitleCase.spec.ts b/src/lib/functions/toTitleCase.spec.ts new file mode 100644 index 0000000..b112339 --- /dev/null +++ b/src/lib/functions/toTitleCase.spec.ts @@ -0,0 +1,56 @@ +import { toTitleCase } from './toTitleCase'; + +describe('toTitleCase', () => { + + it('should capitalize each significant word', () => { + expect(toTitleCase('foo bar')).toEqual('Foo Bar'); + expect(toTitleCase('quick brown fox')).toEqual('Quick Brown Fox'); + }); + + it('should preserve case after hyphens', () => { + expect(toTitleCase('foo-bar')).toEqual('Foo-bar'); + expect(toTitleCase('jack-in-the-box')).toEqual('Jack-in-the-box'); + }); + + it('should lowercase functional words except when first', () => { + expect(toTitleCase('hop on pop')).toEqual('Hop on Pop'); + expect(toTitleCase('ducks in a row')).toEqual('Ducks in a Row'); + expect(toTitleCase('james and the anteater')).toEqual('James and the Anteater'); + expect(toTitleCase('the wonderful world of mystery science')).toEqual('The Wonderful World of Mystery Science'); + expect(toTitleCase('and then there were none')).toEqual('And Then There Were None'); + }); + + it('should capitalize functional words when first', () => { + expect(toTitleCase('in the beginning')).toEqual('In the Beginning'); + expect(toTitleCase('at the gates')).toEqual('At the Gates'); + expect(toTitleCase('by the sea')).toEqual('By the Sea'); + }); + + it('should not change casing of punctuation', () => { + expect(toTitleCase('who framed roger rabbit?')).toEqual('Who Framed Roger Rabbit?'); + expect(toTitleCase('what time is it, mr. fox?')).toEqual('What Time Is It, Mr. Fox?'); + }); + + it('should handle mixed casing inputs by preserving case (seems intentional)', () => { + expect(toTitleCase('ThE gIrL wItH tHe DrAgOn TaTtOo')).toEqual('ThE gIrL wItH tHe DrAgOn TaTtOo'); + }); + + it('should trim and clean excess whitespace', () => { + expect(toTitleCase(' the jungle book ')).toEqual('The Jungle Book'); + expect(toTitleCase('\nhop\non\npop\n')).toEqual('Hop on Pop'); + }); + + it('should work with single-word input', () => { + expect(toTitleCase('frozen')).toEqual('Frozen'); + expect(toTitleCase('and')).toEqual('And'); + }); + + it('should handle acronyms as capitalized', () => { + expect(toTitleCase('the rise of NASA')).toEqual('The Rise of NASA'); + }); + + it('should handle empty string', () => { + expect(toTitleCase('')).toEqual(''); + }); + +}); diff --git a/src/lib/functions/titalize.ts b/src/lib/functions/toTitleCase.ts similarity index 73% rename from src/lib/functions/titalize.ts rename to src/lib/functions/toTitleCase.ts index 520994f..08efd4a 100644 --- a/src/lib/functions/titalize.ts +++ b/src/lib/functions/toTitleCase.ts @@ -7,33 +7,33 @@ * * @example * ```ts - * titalize("the quick brown fox"); // "The Quick Brown Fox" + * toTitleCase("the quick brown fox"); // "The Quick Brown Fox" * ``` * * @example * ```ts - * titalize("a tale of two cities"); // "A Tale of Two Cities" + * toTitleCase("a tale of two cities"); // "A Tale of Two Cities" * ``` * * @example * ```ts - * titalize("in the heart of the sea"); // "In the Heart of the Sea" + * toTitleCase("in the heart of the sea"); // "In the Heart of the Sea" * ``` * * @example * ```ts - * titalize("API reference guide"); // "API Reference Guide" + * toTitleCase("API reference guide"); // "API Reference Guide" * ``` * * @example * ```ts - * titalize(" war and peace "); // "War and Peace" + * toTitleCase(" war and peace "); // "War and Peace" * ``` * * @param input The string to be converted to title case. * @returns The input string formatted in title case. */ -export function titalize(input: string): string { +export function toTitleCase(input: string): string { const smallWords = new Set([ 'a', 'an', 'and', 'at', 'be', 'but', 'by', 'for', 'in', 'of', 'on', 'the', 'to' diff --git a/src/lib/functions/toWords.spec.ts b/src/lib/functions/toWords.spec.ts new file mode 100644 index 0000000..f18b025 --- /dev/null +++ b/src/lib/functions/toWords.spec.ts @@ -0,0 +1,58 @@ +import { toWords } from './toWords'; + +describe('toWords', () => { + + it('should verbalize kebab-case', () => { + expect(toWords('foo-bar')).toEqual('Foo bar'); + expect(toWords('user-profile-id')).toEqual('User profile id'); + }); + + it('should verbalize snake_case', () => { + expect(toWords('foo_bar')).toEqual('Foo bar'); + expect(toWords('user_profile_id')).toEqual('User profile id'); + }); + + it('should verbalize camelCase', () => { + expect(toWords('fooBar')).toEqual('Foo bar'); + expect(toWords('userProfileId')).toEqual('User profile id'); + }); + + it('should verbalize PascalCase', () => { + expect(toWords('FooBar')).toEqual('Foo bar'); + expect(toWords('UserProfileId')).toEqual('User profile id'); + }); + + it('should handle mixed delimiters', () => { + expect(toWords('user-profile_id')).toEqual('User profile id'); + expect(toWords('userProfile_id-name')).toEqual('User profile id name'); + }); + + it('should preserve and separate numbers', () => { + expect(toWords('version2Id')).toEqual('Version 2 id'); + expect(toWords('api_v2_response')).toEqual('Api v2 response'); + expect(toWords('html5Parser')).toEqual('Html 5 parser'); + }); + + it('should trim and clean up input', () => { + expect(toWords(' user profile ')).toEqual('User profile'); + }); + + it('should capitalize the first letter of the result', () => { + expect(toWords('foo')).toEqual('Foo'); + expect(toWords('userProfile')).toEqual('User profile'); + }); + + it('should handle empty input', () => { + expect(toWords('')).toEqual(''); + }); + + it('should handle a single uppercase acronym', () => { + expect(toWords('NASA')).toEqual('NASA'); + expect(toWords('XMLHttpRequest')).toEqual('XML http request'); + }); + + it('should keep v and the version number together', () => { + expect(toWords("EmployeeV2")).toEqual("Employee v2") + }) + +}); diff --git a/src/lib/functions/verbalize.ts b/src/lib/functions/toWords.ts similarity index 83% rename from src/lib/functions/verbalize.ts rename to src/lib/functions/toWords.ts index 608ac07..4d39aeb 100644 --- a/src/lib/functions/verbalize.ts +++ b/src/lib/functions/toWords.ts @@ -9,38 +9,38 @@ import { PLACEHOLDER } from '../private/constants'; * * @example * ```ts - * verbalize("helloWorld"); // "Hello world" + * toWords("helloWorld"); // "Hello world" * ``` * * @example * ```ts - * verbalize("HTML5Parser"); // "HTML 5 parser" + * toWords("HTML5Parser"); // "HTML 5 parser" * ``` * * @example * ```ts - * verbalize("api_v2_response"); // "Api v2 response" + * toWords("api_v2_response"); // "Api v2 response" * ``` * * @example * ```ts - * verbalize("User-ID"); // "User ID" + * toWords("User-ID"); // "User ID" * ``` * * @example * ```ts - * verbalize("employeeV3Final"); // "Employee v3 final" + * toWords("employeeV3Final"); // "Employee v3 final" * ``` * * @example * ```ts - * verbalize("version_2_0_release"); // "Version 2 0 release" + * toWords("version_2_0_release"); // "Version 2 0 release" * ``` * * @param input A string to be returned as spoken words. * @returns A natural-language version of the input identifier. */ -export function verbalize(input: string): string { +export function toWords(input: string): string { const versionMatches: string[] = []; const preserved = input.replace(/(v\d+(\.\d+)*)(?=[^a-zA-Z0-9]|$)/gi, (_, match) => { diff --git a/src/lib/functions/verbalize.spec.ts b/src/lib/functions/verbalize.spec.ts deleted file mode 100644 index 792b1ae..0000000 --- a/src/lib/functions/verbalize.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { verbalize } from './verbalize'; - -describe('verbalize', () => { - - it('should verbalize kebab-case', () => { - expect(verbalize('foo-bar')).toEqual('Foo bar'); - expect(verbalize('user-profile-id')).toEqual('User profile id'); - }); - - it('should verbalize snake_case', () => { - expect(verbalize('foo_bar')).toEqual('Foo bar'); - expect(verbalize('user_profile_id')).toEqual('User profile id'); - }); - - it('should verbalize camelCase', () => { - expect(verbalize('fooBar')).toEqual('Foo bar'); - expect(verbalize('userProfileId')).toEqual('User profile id'); - }); - - it('should verbalize PascalCase', () => { - expect(verbalize('FooBar')).toEqual('Foo bar'); - expect(verbalize('UserProfileId')).toEqual('User profile id'); - }); - - it('should handle mixed delimiters', () => { - expect(verbalize('user-profile_id')).toEqual('User profile id'); - expect(verbalize('userProfile_id-name')).toEqual('User profile id name'); - }); - - it('should preserve and separate numbers', () => { - expect(verbalize('version2Id')).toEqual('Version 2 id'); - expect(verbalize('api_v2_response')).toEqual('Api v2 response'); - expect(verbalize('html5Parser')).toEqual('Html 5 parser'); - }); - - it('should trim and clean up input', () => { - expect(verbalize(' user profile ')).toEqual('User profile'); - }); - - it('should capitalize the first letter of the result', () => { - expect(verbalize('foo')).toEqual('Foo'); - expect(verbalize('userProfile')).toEqual('User profile'); - }); - - it('should handle empty input', () => { - expect(verbalize('')).toEqual(''); - }); - - it('should handle a single uppercase acronym', () => { - expect(verbalize('NASA')).toEqual('NASA'); - expect(verbalize('XMLHttpRequest')).toEqual('XML http request'); - }); - - it('should keep v and the version number together', () => { - expect(verbalize("EmployeeV2")).toEqual("Employee v2") - }) - -}); \ No newline at end of file From 65e11fcdc3792629b9d39853df558a1cc9b90641 Mon Sep 17 00:00:00 2001 From: Maverik Minett Date: Mon, 15 Sep 2025 22:12:50 -0400 Subject: [PATCH 3/4] bump version --- README.md | 205 +++++++++++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 150 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 406bd9b..3ab1024 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,190 @@ # @agape/string -String and token manipulation +String manipulation and transformation utilities for TypeScript. -## Synopsis +## ✨ Functions -``` -camelize('user name') // userName -camelize('email-address') // emailAddress +### `toCamelCase(input: string)` +Converts a string to camelCase format. -kebabify('Display Name') // display-name -kebabify('userProfileId') // user-profile-id +### `toKebabCase(input: string)` +Converts a string to kebab-case format. -pascalize('user id') // UserId -pascalize('api_response_code') // ApiResponseCode +### `toPascalCase(input: string)` +Converts a string to PascalCase format. -pluralize('city') // cities -pluralize('analysis') // analyses +### `toSnakeCase(input: string)` +Converts a string to snake_case format. -quantify(1, 'item') // 1 item -quantify(3, 'box') // 3 boxes +### `toTitleCase(input: string)` +Converts a string to Title Case format. -singularize('cities') // city -singularize('analyses') // analysis +### `toWords(input: string)` +Converts identifiers to natural language words. -snakify('userName') // user_name -snakify('APIResponseCode') // api_response_code +### `toPlural(word: string)` +Converts a singular word to its plural form. -titalize('the lord of the rings') // The Lord of the Rings -titalize('war and peace') // War and Peace +### `toSingular(word: string)` +Converts a plural word to its singular form. -verbalize('user-profile-id') // User profile id -verbalize('XMLHttpRequest') // XML Http Request -``` +### `quantify(count: number | string, unit: string, plural?: string)` +Formats a number with a unit, automatically pluralizing based on count. + +--- + +## 🚀 Example + +```ts +import { + toCamelCase, + toKebabCase, + toPascalCase, + toSnakeCase, + toTitleCase, + toWords, + toPlural, + toSingular, + quantify +} from '@agape/string'; + +// Case conversion +toCamelCase('user name') // "userName" +toCamelCase('email-address') // "emailAddress" + +toKebabCase('Display Name') // "display-name" +toKebabCase('userProfileId') // "user-profile-id" -## Description +toPascalCase('user id') // "UserId" +toPascalCase('api_response_code') // "ApiResponseCode" + +toSnakeCase('userName') // "user_name" +toSnakeCase('APIResponseCode') // "api_response_code" + +toTitleCase('the lord of the rings') // "The Lord of the Rings" +toTitleCase('war and peace') // "War and Peace" + +// Natural language +toWords('user-profile-id') // "User profile id" +toWords('XMLHttpRequest') // "XML Http Request" + +// Pluralization +toPlural('city') // "cities" +toPlural('analysis') // "analyses" +toPlural('API') // "APIs" + +toSingular('cities') // "city" +toSingular('analyses') // "analysis" +toSingular('APIs') // "API" + +// Quantification +quantify(1, 'item') // "1 item" +quantify(3, 'box') // "3 boxes" +quantify(1, 'child', 'children') // "1 child" +quantify(2, 'child', 'children') // "2 children" +quantify(5, 'CPU') // "5 CPUs" +``` -Translate strings between different representations. +--- -## Functions +## 🔧 Function Details -`camelize(input: string)` +### `toCamelCase(input: string)` +Converts a string to camelCase by removing non-alphanumeric separators and capitalizing each word except the first. -Convert to camel case. +**Examples:** +- `"hello world"` → `"helloWorld"` +- `"API_response_code"` → `"apiResponseCode"` +- `"user-42-profile"` → `"user42Profile"` -`kebabify(input: string)` +### `toKebabCase(input: string)` +Converts a string to kebab-case by replacing spaces, underscores, and camelCase transitions with dashes. Preserves version tokens like "v2". -Converted to kebab-case: lower case, word boundaries replaced with dashes. +**Examples:** +- `"hello world"` → `"hello-world"` +- `"UserProfileV2"` → `"user-profile-v2"` +- `"HTML5 Parser"` → `"html-5-parser"` -`pascalize(input: string)` +### `toPascalCase(input: string)` +Converts a string to PascalCase by removing non-alphanumeric characters, splitting on casing and digits, and capitalizing each word. -Remove all symbols and spaces, captialize words. +**Examples:** +- `"hello world"` → `"HelloWorld"` +- `"user42Profile"` → `"User42Profile"` +- `"API response code"` → `"ApiResponseCode"` -`pluralize(input: string)` +### `toSnakeCase(input: string)` +Converts a string to snake_case by replacing spaces, dashes, and camelCase transitions with underscores. Preserves version tokens. -Adds an 's' to most words. Words that end in 'y' are changed to 'ies'. -Words that end in s have 'es' appended to the word. Handles special cases -like children and geese. +**Examples:** +- `"hello world"` → `"hello_world"` +- `"UserProfileV2"` → `"user_profile_v2"` +- `"HTML5 Parser"` → `"html_5_parser"` -`quantify(value: number, unit: string, plural?: string)` +### `toTitleCase(input: string)` +Converts a string to Title Case by capitalizing the first letter of each word, except for small words (like "of", "and", "the") unless they appear at the beginning. -The value will be paired with the unit, either singular or plural form +**Examples:** +- `"the quick brown fox"` → `"The Quick Brown Fox"` +- `"a tale of two cities"` → `"A Tale of Two Cities"` +- `"API reference guide"` → `"API Reference Guide"` -`singularize(input: string)` +### `toWords(input: string)` +Converts identifiers to natural language words by splitting camelCase, PascalCase, snake_case, and kebab-case into space-separated words. -Converts a word to it's singular form if it is a plural. Removes the 's' from -most words. Replacies 'ies' with 'y'. Removes 'es' from the end of a word. -Handles special cases like 'child' and 'goose'. +**Examples:** +- `"userProfileId"` → `"User profile id"` +- `"XMLHttpRequest"` → `"XML Http Request"` +- `"api_v2_response"` → `"Api v2 response"` -`snakify(input: string)` +### `toPlural(word: string)` +Converts a singular word to its plural form using common English pluralization rules. -Converted to snake_case: lower case, word boundaries replaced with underscores. +**Features:** +- Handles irregular plurals (child → children, person → people) +- Preserves acronyms (API → APIs, ID → IDs) +- Maintains original casing -`titalize(input: number)` +**Examples:** +- `"city"` → `"cities"` +- `"box"` → `"boxes"` +- `"child"` → `"children"` +- `"API"` → `"APIs"` -The first letter of each word is capitalized with the exception of -`a, an, and, at, be, but, by, for, if, in, of, on, the, to` which are only -capitalized if they are the first word in the string, otherwise they -are converted to lowercase. +### `toSingular(word: string)` +Converts a plural word to its singular form using common English patterns. -`verbalize(input: number)` +**Features:** +- Handles irregular plurals (children → child, people → person) +- Preserves original casing +- Smart about double letters (boss → boss, not bo) -First character capitalized, word boundaries replaced with spaces. +**Examples:** +- `"cities"` → `"city"` +- `"boxes"` → `"box"` +- `"children"` → `"child"` +- `"APIs"` → `"API"` +### `quantify(count: number | string, unit: string, plural?: string)` +Formats a number with a unit, automatically pluralizing the unit based on the count. -## Author +**Parameters:** +- `count`: Number of units (number or string) +- `unit`: Label for the singular unit +- `plural`: Optional plural label (auto-generated if not provided) -Maverik Minett maverik.minett@gmail.com +**Examples:** +- `quantify(1, 'item')` → `"1 item"` +- `quantify(3, 'box')` → `"3 boxes"` +- `quantify(1, 'child', 'children')` → `"1 child"` +- `quantify(2, 'child', 'children')` → `"2 children"` +--- -## Copyright +## 📚 Documentation -© 2020-2025 Maverik Minett +See the full API documentation at [agape.dev/api](https://agape.dev/api). -## License +## 📦 Agape Toolkit -MIT +This package is part of the [Agape Toolkit](https://github.com/AgapeToolkit/AgapeToolkit) - a comprehensive collection of TypeScript utilities and libraries for modern web development. diff --git a/package.json b/package.json index 4ed1d32..724ba6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agape/string", - "version": "2.2.6", + "version": "3.0.0", "description": "String and token manipulation", "main": "./cjs/index.js", "module": "./es2020/index.js", From 202124a530b82d1715ef6c663ea7934df6b6c2c1 Mon Sep 17 00:00:00 2001 From: Maverik Minett Date: Mon, 15 Sep 2025 22:21:58 -0400 Subject: [PATCH 4/4] update github action --- .github/workflows/validate-merge-request.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-merge-request.yml b/.github/workflows/validate-merge-request.yml index 5fcf967..e73e5e4 100644 --- a/.github/workflows/validate-merge-request.yml +++ b/.github/workflows/validate-merge-request.yml @@ -1,11 +1,14 @@ -name: Validate @agape/string in monorepo context +name: Validate @agape/string on: pull_request: jobs: test: + name: Unit Tests runs-on: ubuntu-latest + env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} steps: - name: Checkout agape-string (this repo) @@ -53,7 +56,7 @@ jobs: fi ' - - name: Replace string code in monorepo + - name: Replace library code in monorepo run: | rm -rf ../AgapeToolkit/libs/string cp -r . ../AgapeToolkit/libs/string