From e76b043f5674ad66fd7191075a5bc68e92eac181 Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Tue, 3 Feb 2026 23:50:54 -0500 Subject: [PATCH 1/6] fix: cast separator correctly when deserializing --- example/advanced.ts | 4 +- example/index.html | 82 +++++++++++++++++++++++ example/main.ts | 2 +- package.json | 2 +- src/data.spec.ts | 158 +++++++++++++++++++++----------------------- src/data.ts | 7 +- src/namefully.ts | 71 +++++++++++--------- 7 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 example/index.html diff --git a/example/advanced.ts b/example/advanced.ts index ed0ba41..49a5fe4 100644 --- a/example/advanced.ts +++ b/example/advanced.ts @@ -1,4 +1,4 @@ -import { Name, NameBuilder, Title } from '../src/index'; +import { Name, NameBuilder } from 'namefully'; function main() { const builder = NameBuilder.of(Name.first('Nikola'), Name.last('Tesla')); @@ -11,7 +11,7 @@ function main() { builder.add(Name.prefix('Mr')); // Build the name with options if needed - name = builder.build({ title: Title.US }); + name = builder.build({ title: 'US' }); console.log(name.full); // Mr. Nikola Tesla } diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..8460b02 --- /dev/null +++ b/example/index.html @@ -0,0 +1,82 @@ + + + + + Namefully | Demo + + + + + +
+

How to use namefully

+

Simple case

+

Given the name John Smith, use only the first name:

+
+ + +

It can also do that

+

+ Let us say you want to distinguish the name parts of a full name like this + Mr,Smith,John,Ben,Ph.D (comma-separated values), you can achieve the following: +

+
+ +
+ + diff --git a/example/main.ts b/example/main.ts index 267f3bf..93ea8a7 100644 --- a/example/main.ts +++ b/example/main.ts @@ -1,4 +1,4 @@ -import namefully from '../src/index'; +import namefully from 'namefully'; function main() { // Gives a simple name some super power. diff --git a/package.json b/package.json index 9d2ad3d..5e10409 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "format": "prettier --write src example", "lint": "eslint src", "test": "jest", - "test:cov": "jest --collectCoverage", + "test:cov": "jest --coverage", "prebuild": "shx rm -rf dist", "build:esm": "tsc -p tsconfig.json", "build:cjs": "tsc -p tsconfig.cjs.json", diff --git a/src/data.spec.ts b/src/data.spec.ts index 400bec6..74c129d 100644 --- a/src/data.spec.ts +++ b/src/data.spec.ts @@ -2,6 +2,7 @@ import { Namefully } from './namefully.js'; import { InputError, UnknownError } from './error.js'; import { Name, FirstName, LastName } from './name.js'; import { deserialize, SerializedName } from './data.js'; +import { Separator, Title, Surname, NameOrder } from './types.js'; describe('JSON serialization', () => { describe('serialize', () => { @@ -82,27 +83,39 @@ describe('JSON serialization', () => { }); describe('deserialize', () => { - test('should deserialize a simple name from object', () => { - const data: SerializedName = { - names: { - firstName: 'John', - lastName: 'Smith', - }, - config: { - name: 'fullName', - orderedBy: 'firstName', - separator: ' ', - title: 'US', - ending: false, - bypass: false, - surname: 'father', - }, + let defaultConfig: SerializedName['config']; + + beforeEach(() => { + defaultConfig = { + name: 'defaultConfig', + orderedBy: 'firstName', + separator: ' ', + title: 'US', + ending: false, + bypass: true, + surname: 'father', }; + }); - const name = deserialize(data); - expect(name.firstName()).toBe('John'); - expect(name.lastName()).toBe('Smith'); + test('should deserialize a simple name from object', () => { + const name = deserialize({ + names: { firstName: 'John', lastName: 'Smith' }, + config: { ...defaultConfig, name: 'fullName', bypass: false, surname: 'all' }, + }); + expect(name.first).toBe('John'); + expect(name.last).toBe('Smith'); expect(name.full).toBe('John Smith'); + expect(name.middle).toBeUndefined(); + expect(name.prefix).toBeUndefined(); + expect(name.suffix).toBeUndefined(); + + expect(name.config.name).toBe('fullName'); + expect(name.config.orderedBy).toBe('firstName'); + expect(name.config.separator).toBe(Separator.SPACE); + expect(name.config.title).toBe(Title.US); + expect(name.config.ending).toBe(false); + expect(name.config.bypass).toBe(false); + expect(name.config.surname).toBe(Surname.ALL); }); test('should deserialize a name from JSON string', () => { @@ -110,80 +123,67 @@ describe('JSON serialization', () => { names: { firstName: 'Jane', lastName: 'Doe', + suffix: 'Ph.D', }, config: { - name: 'fullName', - orderedBy: 'firstName', - separator: ' ', - title: 'US', - ending: false, - bypass: false, + name: 'byLastName', + orderedBy: 'lastName', + separator: ',', + title: 'UK', + ending: true, + bypass: true, surname: 'father', }, }); const name = deserialize(jsonString); - expect(name.firstName()).toBe('Jane'); - expect(name.lastName()).toBe('Doe'); + expect(name.first).toBe('Jane'); + expect(name.last).toBe('Doe'); + expect(name.full).toBe('Doe Jane, Ph.D'); + expect(name.middle).toBeUndefined(); + expect(name.prefix).toBeUndefined(); + expect(name.suffix).toBe('Ph.D'); + + expect(name.config.name).toBe('byLastName'); + expect(name.config.orderedBy).toBe(NameOrder.LAST_NAME); + expect(name.config.separator).toBe(Separator.COMMA); + expect(name.config.title).toBe(Title.UK); + expect(name.config.ending).toBe(true); + expect(name.config.bypass).toBe(true); + expect(name.config.surname).toBe(Surname.FATHER); }); test('should deserialize a name with prefix and suffix', () => { - const original = new Namefully([Name.prefix('Mr'), Name.first('John'), Name.last('Smith'), Name.suffix('Jr')]); - const serialized = original.serialize(); + const name = deserialize({ + names: { prefix: 'Mr', firstName: 'John', lastName: 'Smith', suffix: 'Jr' }, + config: defaultConfig, + }); - const name = deserialize(serialized); - expect(name.prefix).toBe(original.prefix); - expect(name.suffix).toBe(original.suffix); + expect(name.prefix).toBe('Mr.'); + expect(name.first).toBe('John'); + expect(name.last).toBe('Smith'); + expect(name.suffix).toBe('Jr'); }); test('should deserialize a name with middle names', () => { - const data: SerializedName = { - names: { - firstName: 'John', - middleName: ['Michael', 'David'], - lastName: 'Smith', - }, - config: { - name: 'fullName', - orderedBy: 'firstName', - separator: ' ', - title: 'US', - ending: false, - bypass: false, - surname: 'father', - }, - }; - - const name = deserialize(data); + const name = deserialize({ + names: { firstName: 'John', middleName: ['Michael', 'David'], lastName: 'Smith' }, + config: defaultConfig, + }); expect(name.middleName()).toEqual(['Michael', 'David']); }); test('should deserialize a name with multiple first names', () => { - const data: SerializedName = { - names: { - firstName: { - value: 'John', - more: ['Michael'], - }, - lastName: 'Smith', - }, - config: { - name: 'fullName', - orderedBy: 'firstName', - separator: ' ', - title: 'US', - ending: false, - bypass: false, - surname: 'father', - }, - }; - - const name = deserialize(data); + const name = deserialize({ + names: { firstName: { value: 'John', more: ['Michael'] }, lastName: 'Smith' }, + config: defaultConfig, + }); expect(name.firstName(false)).toBe('John'); + expect(name.firstName(true)).toEqual('John Michael'); }); test('should deserialize a name with hyphenated last name', () => { - const data: SerializedName = { + const name = deserialize({ names: { firstName: 'John', lastName: { @@ -191,19 +191,11 @@ describe('JSON serialization', () => { mother: 'Jones', }, }, - config: { - name: 'fullName', - orderedBy: 'firstName', - separator: ' ', - title: 'US', - ending: false, - bypass: false, - surname: 'father', - }, - }; - - const name = deserialize(data); - expect(name.lastName()).toBe('Smith'); + config: { ...defaultConfig, surname: 'hyphenated' }, + }); + expect(name.first).toBe('John'); + expect(name.last).toBe('Smith-Jones'); + expect(name.full).toBe('John Smith-Jones'); }); test('should throw NameError for invalid data', () => { diff --git a/src/data.ts b/src/data.ts index 4b04f5b..8f89403 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,3 +1,4 @@ +import { Separator } from './types.js'; import { NameBuilder } from './builder.js'; import { Name, FirstName, LastName } from './name.js'; import { type Namefully, NameOptions } from './namefully.js'; @@ -48,6 +49,9 @@ export function deserialize(data: SerializedName | string): Namefully { } const { names, config } = parsed; + const separator = Separator.cast(config.separator); + if (!separator) throw new NameError(config.separator, 'unable to deserialize name for unknown separator'); + const { firstName: fn, lastName: ln, middleName: mn, prefix: px, suffix: sx } = names; const builder = NameBuilder.of(); @@ -57,8 +61,7 @@ export function deserialize(data: SerializedName | string): Namefully { builder.add(typeof fn === 'string' ? Name.first(fn) : new FirstName(fn.value, ...(fn.more ?? []))); builder.add(typeof ln === 'string' ? Name.last(ln) : new LastName(ln.father, ln.mother)); - - return builder.build(config as unknown as NameOptions); + return builder.build({ ...config, separator: separator! } as NameOptions); } catch (error) { if (error instanceof NameError) throw error; diff --git a/src/namefully.ts b/src/namefully.ts index eca7e2e..a912209 100644 --- a/src/namefully.ts +++ b/src/namefully.ts @@ -37,7 +37,7 @@ import { ArrayNameParser, ArrayStringParser, NamaParser, Parser, StringParser } * name piece. * * @see {@link https://www.fbiic.gov/public/2008/nov/Naming_practice_guide_UK_2006.pdf} - * for more info on name standards. + * for more info on name standards. * * **IMPORTANT**: Keep in mind that the order of appearance (or name order) matters * and may be altered through configurable parameters, which will be seen later. @@ -272,7 +272,12 @@ export class Namefully { if ((orderedBy ?? this.config.orderedBy) === NameOrder.FIRST_NAME) { names.push(this.first, ...this.middleName(), this.last + sep); } else { - names.push(this.last, this.first, this.middleName().join(' ') + sep); + names.push(this.last); + if (this.hasMiddle) { + names.push(this.first, this.middleName().join(' ') + sep); + } else { + names.push(this.first + sep); + } } if (this.suffix) names.push(this.suffix); @@ -645,6 +650,37 @@ export class Namefully { return toggleCase(this.birth); } + /** + * Serializes this Namefully instance to a JSON object. + * + * This includes both the name data (with full hierarchy for FirstName and LastName) + * and the configuration, allowing for complete reconstruction of the Namefully instance. + * + * @returns a JSON-serializable object containing name data and config. + */ + serialize(): SerializedName { + const { config, firstName: fn, lastName: ln } = this.#fullName; + + return { + names: { + prefix: this.prefix, + firstName: fn.hasMore ? { value: fn.value, more: fn.more } : fn.value, + middleName: this.hasMiddle ? this.middleName() : undefined, + lastName: ln.hasMother ? { father: ln.father, mother: ln.mother } : ln.value, + suffix: this.suffix, + }, + config: { + name: config.name, + orderedBy: config.orderedBy, + separator: config.separator.token, + title: config.title, + ending: config.ending, + bypass: config.bypass, + surname: config.surname, + }, + }; + } + #toParser(raw: string | string[] | Name[] | JsonName | Parser): Parser { if (raw instanceof Parser) return raw; if (typeof raw === 'string') return new StringParser(raw); @@ -712,37 +748,6 @@ export class Namefully { return ALLOWED_FORMAT_TOKENS.includes(char) ? char : undefined; } } - - /** - * Serializes this Namefully instance to a JSON object. - * - * This includes both the name data (with full hierarchy for FirstName and LastName) - * and the configuration, allowing for complete reconstruction of the Namefully instance. - * - * @returns a JSON-serializable object containing name data and config. - */ - serialize(): SerializedName { - const { config, firstName: fn, lastName: ln } = this.#fullName; - - return { - names: { - prefix: this.prefix, - firstName: fn.hasMore ? { value: fn.value, more: fn.more } : fn.value, - middleName: this.hasMiddle ? this.middleName() : undefined, - lastName: ln.hasMother ? { father: ln.father, mother: ln.mother } : ln.value, - suffix: this.suffix, - }, - config: { - name: config.name, - orderedBy: config.orderedBy, - separator: config.separator.token, - title: config.title, - ending: config.ending, - bypass: config.bypass, - surname: config.surname, - }, - }; - } } /** From a77dc6afa58a38bbd57879005759ab84c7cdb8ab Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Thu, 5 Feb 2026 23:28:38 -0500 Subject: [PATCH 2/6] feat: add support for mononyms (#21) --- example/mononym.ts | 17 +++++++++++ example/parser.ts | 28 ++++++++++++++++++ readme.md | 26 ++++++++++++++-- src/config.ts | 18 +++++++++-- src/constants.ts | 1 + src/data.spec.ts | 12 ++++++++ src/data.ts | 11 +++---- src/fullname.ts | 69 +++++++++++++++++++++++++++++++++++++++++++ src/namefully.spec.ts | 15 ++++++++-- src/namefully.ts | 14 +++++++-- src/parser.ts | 21 +++++++++---- 11 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 example/mononym.ts create mode 100644 example/parser.ts diff --git a/example/mononym.ts b/example/mononym.ts new file mode 100644 index 0000000..f7dbf02 --- /dev/null +++ b/example/mononym.ts @@ -0,0 +1,17 @@ +import namefully from 'namefully'; + +function main() { + const name = namefully('Plato', { mono: true }); + console.log(name.length); // 5 + console.log(name.first); // Plato + console.log(name.middle); // undefined + console.log(name.last); // + console.log(name.public); // Plato + console.log(name.initials()); // ['P'] + console.log(name.format('L, f m')); // , Plato + console.log(name.shorten()); // Plato + console.log(name.zip()); // P. + console.log(name.toUpperCase()); // PLATO +} + +main(); diff --git a/example/parser.ts b/example/parser.ts new file mode 100644 index 0000000..45249f1 --- /dev/null +++ b/example/parser.ts @@ -0,0 +1,28 @@ +import namefully, { Config, FullName, Parser } from 'namefully'; + +class CustomParser extends Parser { + constructor( + raw: string, + public separator = '|', + ) { + super(raw); + } + + parse(options: Partial): FullName { + const [fn, ln] = this.raw.split(this.separator, 2); + return FullName.parse({ firstName: fn.trim(), lastName: ln.trim() }, Config.merge(options)); + } +} + +function main() { + const name = namefully(new CustomParser('Juan | García | Jr.')); + console.log(name.full); // Juan García + console.log(name.first); // Juan + console.log(name.initials()); // ['J', 'G'] + console.log(name.format('L, f m')); // García, Juan + console.log(name.size); // 2 + console.log(name.zip()); // Juan G. + console.log(name.toUpperCase()); // JUAN GARCÍA +} + +main(); diff --git a/readme.md b/readme.md index 03b1d5f..5eecc0d 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,7 @@ [![JSR Version][jsr-version]][jsr-url] [![CI build][ci-img]][ci-url] [![MIT License][license-img]][license-url] +[![DeepWiki][deepwiki-img]][deepwiki-url] Human name handling made easy. [Try it live](https://stackblitz.com/edit/namefully). @@ -204,6 +205,24 @@ const name = new Namefully( ); ``` +### mono + +`boolean | Namon` - default: `false` + +Enables support for mononyms (i.e., single word names). You may also use `Namon` +to assign which name type is being used to represent the mononym. + +> You should know that this goes against the original design philosophy of the library, +> which is intentionally opinionated around shaping and organizing multiple name components. +> Treating a single token as a "full" name makes a lot of the existing API semantics +> somewhat meaningless. + +```ts +const name = new Namefully('Plato', { mono: true }); // throws an exception without this flag. +console.log(name.full); // Plato +console.log(name.initials()); // ['P'] +``` + To sum it all up, the default values are: ```ts @@ -213,7 +232,8 @@ To sum it all up, the default values are: title: Title.UK, ending: false, bypass: true, - surname: Surname.FATHER + surname: Surname.FATHER, + mono: false } ``` @@ -303,7 +323,7 @@ So, this utility understands the name parts as follows: `namefully` does not support certain use cases: -- mononame: `Plato` - a workaround is to set the mononame as both first and last name; +- mononame: `Plato` - enable the mononym flag `Config.mono` to support this. - nickname: `Dwayne "The Rock" Johnson` - use custom parser instead. - multiple prefixes or suffixes: `Prof. Dr. Einstein`. @@ -326,6 +346,8 @@ The underlying content of this utility is licensed under [MIT License][license-u [ci-url]: https://github.com/ralflorent/namefully/actions/workflows/ci.yml [license-img]: https://img.shields.io/npm/l/namefully [license-url]: https://opensource.org/licenses/MIT +[deepwiki-img]: https://deepwiki.com/badge.svg +[deepwiki-url]: https://deepwiki.com/ralflorent/namefully [contributing-url]: https://github.com/ralflorent/namefully/blob/main/CONTRIBUTING.md [examples]: https://github.com/ralflorent/namefully/tree/main/example diff --git a/src/config.ts b/src/config.ts index e882ea8..838fff0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import { NameOrder, Separator, Title, Surname } from './types.js'; +import { NameOrder, Namon, Separator, Title, Surname } from './types.js'; const defaultName = 'default'; const copyAlias = '_copy'; @@ -39,6 +39,7 @@ export class Config { #ending: boolean; #bypass: boolean; #surname: Surname; + #mono: boolean | Namon; /** Cache for multiple instances. */ private static cache = new Map(); @@ -91,6 +92,11 @@ export class Config { return this.#surname; } + /** Whether to parse a single word name as a mononym. */ + get mono(): boolean | Namon { + return this.#mono; + } + /** The name of the cached configuration. */ get name(): string { return this.#name; @@ -104,6 +110,7 @@ export class Config { ending = false, bypass = true, surname = Surname.FATHER, + mono: boolean | Namon = false, ) { this.#name = name; this.#orderedBy = orderedBy; @@ -112,6 +119,7 @@ export class Config { this.#ending = ending; this.#bypass = bypass; this.#surname = surname; + this.#mono = mono; } /** @@ -139,6 +147,7 @@ export class Config { config.#ending = other.ending ?? config.ending; config.#bypass = other.bypass ?? config.bypass; config.#surname = other.surname ?? config.surname; + config.#mono = other.mono ?? config.mono; return config; } } @@ -153,7 +162,7 @@ export class Config { * be named `default_copy`. */ copyWith(options: Partial = {}): Config { - const { name, orderedBy, separator, title, ending, bypass, surname } = options; + const { name, orderedBy, separator, title, ending, bypass, surname, mono } = options; const config = Config.create(this.#genNewName(name ?? this.name + copyAlias)); config.#orderedBy = orderedBy ?? this.orderedBy; config.#separator = separator ?? this.separator; @@ -161,6 +170,7 @@ export class Config { config.#ending = ending ?? this.ending; config.#bypass = bypass ?? this.bypass; config.#surname = surname ?? this.surname; + config.#mono = mono ?? this.mono; return config; } @@ -177,6 +187,7 @@ export class Config { this.#ending = false; this.#bypass = true; this.#surname = Surname.FATHER; + this.#mono = false; Config.cache.set(this.name, this); } @@ -192,12 +203,13 @@ export class Config { /** * Allows the possibility to alter some options after creating a name set. */ - update({ orderedBy, title, ending }: Partial>): void { + update({ orderedBy, title, ending, mono }: Partial>): void { const config = Config.cache.get(this.name); if (!config) return; if (orderedBy !== this.#orderedBy) config.#orderedBy = orderedBy!; if (title !== this.#title) config.#title = title!; if (ending !== this.#ending) config.#ending = ending!; + if (mono !== this.#mono) config.#mono = mono!; } /** Generates a unique new name. */ diff --git a/src/constants.ts b/src/constants.ts index fe7d988..61d33b6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,3 +2,4 @@ export const VERSION = '2.1.0'; export const MIN_NUMBER_OF_NAME_PARTS = 2; export const MAX_NUMBER_OF_NAME_PARTS = 5; export const ALLOWED_FORMAT_TOKENS = ` .,_-()[]<>'"bBfFlLmMnNoOpPsS$`; +export const ZERO_WIDTH_SPACE = String.fromCharCode(8203); diff --git a/src/data.spec.ts b/src/data.spec.ts index 74c129d..50dc25f 100644 --- a/src/data.spec.ts +++ b/src/data.spec.ts @@ -20,6 +20,7 @@ describe('JSON serialization', () => { ending: expect.any(Boolean), bypass: expect.any(Boolean), surname: expect.any(String), + mono: expect.any(Boolean), }), }); }); @@ -94,6 +95,7 @@ describe('JSON serialization', () => { ending: false, bypass: true, surname: 'father', + mono: false, }; }); @@ -116,6 +118,7 @@ describe('JSON serialization', () => { expect(name.config.ending).toBe(false); expect(name.config.bypass).toBe(false); expect(name.config.surname).toBe(Surname.ALL); + expect(name.config.mono).toBe(false); }); test('should deserialize a name from JSON string', () => { @@ -246,5 +249,14 @@ describe('JSON serialization', () => { expect(deserialized.full).toBe(original.full); }); + + test('should serialize and deserialize mononyms', () => { + const original = new Namefully('Plato', { mono: true }); + const serialized = original.serialize(); + const deserialized = deserialize(serialized); + + expect(deserialized.first).toBe(original.first); + expect(deserialized.last).toBe(original.last); + }); }); }); diff --git a/src/data.ts b/src/data.ts index 8f89403..67a9b96 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,5 +1,5 @@ -import { Separator } from './types.js'; import { NameBuilder } from './builder.js'; +import { Namon, Separator } from './types.js'; import { Name, FirstName, LastName } from './name.js'; import { type Namefully, NameOptions } from './namefully.js'; import { InputError, NameError, UnknownError } from './error.js'; @@ -23,6 +23,7 @@ export interface SerializedName { ending: boolean; bypass: boolean; surname: string; + mono: boolean | string; }; } @@ -49,9 +50,6 @@ export function deserialize(data: SerializedName | string): Namefully { } const { names, config } = parsed; - const separator = Separator.cast(config.separator); - if (!separator) throw new NameError(config.separator, 'unable to deserialize name for unknown separator'); - const { firstName: fn, lastName: ln, middleName: mn, prefix: px, suffix: sx } = names; const builder = NameBuilder.of(); @@ -61,7 +59,10 @@ export function deserialize(data: SerializedName | string): Namefully { builder.add(typeof fn === 'string' ? Name.first(fn) : new FirstName(fn.value, ...(fn.more ?? []))); builder.add(typeof ln === 'string' ? Name.last(ln) : new LastName(ln.father, ln.mother)); - return builder.build({ ...config, separator: separator! } as NameOptions); + + const separator = Separator.cast(config.separator); + const mono = typeof config.mono === 'string' ? (Namon.cast(config.mono) ?? false) : config.mono; + return builder.build({ ...config, separator, mono } as NameOptions); } catch (error) { if (error instanceof NameError) throw error; diff --git a/src/fullname.ts b/src/fullname.ts index 0290452..8a41ea9 100644 --- a/src/fullname.ts +++ b/src/fullname.ts @@ -1,5 +1,6 @@ import { Config } from './config.js'; import { Validators } from './validator.js'; +import { ZERO_WIDTH_SPACE } from './constants.js'; import { Nullable, Namon, Title } from './types.js'; import { NameError, UnknownError } from './error.js'; import { FirstName, LastName, Name, JsonName } from './name.js'; @@ -66,6 +67,11 @@ export class FullName { return this.#suffix; } + /** Whether the full name is a single word name. */ + get isMono(): boolean { + return this instanceof Mononym; + } + /** * Parses a JSON name into a full name. * @param {JsonName} json parsable name element @@ -133,6 +139,11 @@ export class FullName { return namon.equal(Namon.MIDDLE_NAME) ? this.#middleName.length > 0 : true; } + toString(): string { + if (this.isMono) return (this as unknown as Mononym).value; + return Array.from(this.toIterable(true)).join(' '); + } + /** Returns an `Iterable` of existing `Name`s. */ *toIterable(flat: boolean = false): Iterable { if (this.#prefix) yield this.#prefix; @@ -153,3 +164,61 @@ export class FullName { yield* this.toIterable(true); } } + +/** + * A single word name or mononym. + * + * This is a special case of `FullName` that is used to represent mononyms. This contradicts + * the original purpose of this library such as shaping and organizing name pieces accordingly. + * + * When enabled via `Config.mono`, this becomes the full name of a human. And as a single name, + * most of the `Namefully` methods become irrelevant. + */ +export class Mononym extends FullName { + readonly #namon!: string; + #type!: Namon; + + /** + * Constructs a mononym from a piece of string. + * @param {string | Name} name to be used to construct the mononym. + */ + constructor(name: string | Name) { + super(); + this.#namon = name.toString(); + this.type = name instanceof Name ? name.type : Namon.FIRST_NAME; + } + + /** + * Re-assigns which name type is being used to represent the mononym. + * + * Ideally, this doesn't really matter as the mononym is always a single piece of name. + * When used as `string`, it must be a valid `Namon` type or else it will default to + * `Namon.FIRST_NAME`. + * @param {string | Namon} type of name to use. + */ + set type(type: string | Namon) { + this.#type = typeof type === 'string' ? (Namon.cast(type) ?? Namon.FIRST_NAME) : type; + this.#build(this.#namon); + } + + /** The type of name being used to represent the mononym. */ + get type(): Namon { + return this.#type; + } + + /** The piece of string treated as a name. */ + get value(): string { + return this.#namon; + } + + #build(name: string): void { + this.setFirstName(ZERO_WIDTH_SPACE).setLastName(ZERO_WIDTH_SPACE).setMiddleName([]).setPrefix(null).setSuffix(null); + + if (this.#type.equal(Namon.FIRST_NAME)) this.setFirstName(name); + else if (this.#type.equal(Namon.LAST_NAME)) this.setLastName(name); + else if (this.#type.equal(Namon.MIDDLE_NAME)) this.setMiddleName([name]); + else if (this.#type.equal(Namon.PREFIX)) this.setPrefix(name); + else if (this.#type.equal(Namon.SUFFIX)) this.setSuffix(name); + else throw new NameError(name, 'invalid mononym type'); + } +} diff --git a/src/namefully.spec.ts b/src/namefully.spec.ts index 74d9e65..bdafefa 100644 --- a/src/namefully.spec.ts +++ b/src/namefully.spec.ts @@ -433,6 +433,7 @@ describe('Namefully', () => { bypass: true, ending: false, surname: Surname.FATHER, + mono: false, }), ); }); @@ -445,6 +446,7 @@ describe('Namefully', () => { title: Title.US, surname: Surname.HYPHENATED, ending: true, + mono: true, }), ).toEqual( expect.objectContaining({ @@ -455,21 +457,24 @@ describe('Namefully', () => { bypass: true, ending: true, surname: Surname.HYPHENATED, + mono: true, }), ); - const { config } = new Namefully('f l', { + const config = Config.merge({ name: 'partial', orderedBy: 'lastName', title: 'US', surname: 'all', ending: true, - }); + mono: Namon.LAST_NAME, + } as Partial); expect(config.name).toBe('partial'); expect(config.orderedBy).toBe(NameOrder.LAST_NAME); expect(config.title).toBe(Title.US); expect(config.ending).toBe(true); expect(config.surname).toBe(Surname.ALL); + expect(config.mono).toBe(Namon.LAST_NAME); }); test('can create more than 1 configuration when necessary', () => { @@ -491,6 +496,7 @@ describe('Namefully', () => { bypass: true, ending: false, surname: Surname.FATHER, + mono: false, }), ); @@ -504,6 +510,7 @@ describe('Namefully', () => { bypass: false, ending: false, surname: Surname.MOTHER, + mono: false, }), ); }); @@ -528,6 +535,7 @@ describe('Namefully', () => { bypass: false, ending: false, surname: Surname.MOTHER, + mono: false, }), ); @@ -541,6 +549,7 @@ describe('Namefully', () => { bypass: false, ending: false, surname: Surname.MOTHER, + mono: false, }), ); @@ -554,6 +563,7 @@ describe('Namefully', () => { bypass: true, ending: false, surname: Surname.FATHER, + mono: false, }), ); @@ -568,6 +578,7 @@ describe('Namefully', () => { bypass: true, ending: false, surname: Surname.FATHER, + mono: false, }), ); }); diff --git a/src/namefully.ts b/src/namefully.ts index a912209..a1b7360 100644 --- a/src/namefully.ts +++ b/src/namefully.ts @@ -36,8 +36,8 @@ import { ArrayNameParser, ArrayStringParser, NamaParser, Parser, StringParser } * this: `John Smith`, where `John` is the first name piece and `Smith`, the last * name piece. * - * @see {@link https://www.fbiic.gov/public/2008/nov/Naming_practice_guide_UK_2006.pdf} - * for more info on name standards. + * @see {@link https://www.fbiic.gov/public/2008/nov/Naming_practice_guide_UK_2006.pdf} for + * more info on name standards. * * **IMPORTANT**: Keep in mind that the order of appearance (or name order) matters * and may be altered through configurable parameters, which will be seen later. @@ -108,8 +108,14 @@ export class Namefully { return this.#fullName.config; } + /** Whether the name is a single word name. */ + get isMono(): boolean { + return this.#fullName.isMono; + } + /** The number of characters of the `birthName`, including spaces. */ get length(): number { + if (this.isMono) return this.#fullName.toString().length; return this.birth.length; } @@ -345,6 +351,7 @@ export class Namefully { const lastInits = this.#fullName.lastName.initials(); if (asJson) return { firstName: firstInits, middleName: midInits, lastName: lastInits }; + if (this.isMono) return [this.#fullName.toString()[0]]; if (only !== NameType.BIRTH_NAME) { return only === NameType.FIRST_NAME ? firstInits : only === NameType.MIDDLE_NAME ? midInits : lastInits; @@ -429,6 +436,7 @@ export class Namefully { } = options; if (this.length <= limit) return this.full; + if (this.isMono) return `${this.initials()}${withPeriod ? '.' : ''}`; const { firstName, lastName, middleName } = this.#fullName; const sep = withPeriod ? '.' : ''; @@ -677,6 +685,7 @@ export class Namefully { ending: config.ending, bypass: config.bypass, surname: config.surname, + mono: config.mono instanceof Namon ? config.mono.key : config.mono, }, }; } @@ -768,4 +777,5 @@ export type NameOptions = Partial<{ ending: boolean; bypass: boolean; surname: Surname | 'father' | 'mother' | 'hyphenated' | 'all'; + mono: boolean | Namon; }>; diff --git a/src/parser.ts b/src/parser.ts index 382e445..929e45f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,10 +1,10 @@ import { Config } from './config.js'; import { NameIndex } from './utils.js'; import { InputError } from './error.js'; -import { FullName } from './fullname.js'; +import { FullName, Mononym } from './fullname.js'; import { Namon, Nullable, Separator } from './types.js'; import { FirstName, LastName, Name, JsonName } from './name.js'; -import { ArrayStringValidator, ArrayNameValidator, NamaValidator } from './validator.js'; +import { ArrayStringValidator, ArrayNameValidator, Validators } from './validator.js'; /** * A parser signature that helps to organize the names accordingly. @@ -65,7 +65,7 @@ export class StringParser extends Parser { parse(options: Partial): FullName { const config = Config.merge(options); const names = this.raw.split(config.separator.token); - return new ArrayStringParser(names).parse(options); + return (names.length === 1 && config.mono ? new MonoParser(names[0]) : new ArrayStringParser(names)).parse(options); } } @@ -113,9 +113,9 @@ export class NamaParser extends Parser { ); if (config.bypass) { - NamaValidator.create().validateKeys(names); + Validators.nama.validateKeys(names); } else { - NamaValidator.create().validate(names); + Validators.nama.validate(names); } return FullName.parse(this.raw, config); @@ -146,3 +146,14 @@ export class ArrayNameParser extends Parser { return fullName; } } + +export class MonoParser extends Parser { + parse(options: Partial): Mononym { + const { bypass, mono } = Config.merge(options); + + if (bypass) Validators.namon.validate(this.raw); + + const type = mono instanceof Namon ? mono : Namon.FIRST_NAME; + return new Mononym(new Name(this.raw.trim(), type)); + } +} From 96123df7ef7b2adefde44a657f08240626f2fb50 Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Fri, 6 Feb 2026 00:11:01 -0500 Subject: [PATCH 3/6] test: add basic coverage for mononyms --- src/namefully.spec.ts | 35 +++++++++++++++++++++++++++++++++++ src/namefully.ts | 5 +++++ 2 files changed, 40 insertions(+) diff --git a/src/namefully.spec.ts b/src/namefully.spec.ts index bdafefa..77730bf 100644 --- a/src/namefully.spec.ts +++ b/src/namefully.spec.ts @@ -1,8 +1,10 @@ import { Config } from './config.js'; import { NameError } from './error.js'; import { NameIndex } from './utils.js'; +import { deserialize } from './data.js'; import { Namefully } from './namefully.js'; import { NameBuilder } from './builder.js'; +import { ZERO_WIDTH_SPACE } from './constants.js'; import { FirstName, LastName, Name } from './name.js'; import { SimpleParser, findNameCase } from './fixtures/helpers.js'; import { Flat, NameOrder, NameType, Namon, Separator, Surname, Title } from './types.js'; @@ -277,6 +279,7 @@ describe('Namefully', () => { test('string', () => { expect(new Namefully('John Smith').toString()).toBe('John Smith'); expect(new Namefully('Jane D Smith').toString()).toBe('Jane D Smith'); + expect(new Namefully('Madonna', { mono: true }).toString()).toBe('Madonna'); }); test('string[]', () => { @@ -304,6 +307,24 @@ describe('Namefully', () => { expect(new Namefully(new SimpleParser('John#Smith')).toString()).toBe('John Smith'); }); + test('deserialize()', () => { + const name = deserialize({ + names: { firstName: 'John', lastName: 'Smith' }, + config: { + name: 'deserialize', + orderedBy: 'firstName', + separator: ' ', + title: 'US', + ending: false, + bypass: true, + surname: 'father', + mono: false, + }, + }); + expect(name).toBeInstanceOf(Namefully); + expect(name.full).toBe('John Smith'); + }); + test('tryParse()', () => { expect(Namefully.tryParse('John')).toBeUndefined(); @@ -420,9 +441,23 @@ describe('Namefully', () => { expect(() => new Namefully('Mr John Joe Sm1th', Config.create('noBypass'))).toThrow(NameError); expect(() => new Namefully('Mr John Joe Smith Ph+', Config.create('noBypass'))).toThrow(NameError); }); + + test('of mononyms', () => { + const name = new Namefully('Madonna', { mono: true }); + expect(name.toString()).toBe('Madonna'); + expect(name.first).toBe('Madonna'); + expect(name.last).toBe(ZERO_WIDTH_SPACE); + expect(name.middle).toBeUndefined(); + expect(name.initials()).toStrictEqual(['M']); + expect(name.format('L, f m')).toBe(ZERO_WIDTH_SPACE + ', Madonna'); + expect(name.shorten()).toBe('Madonna'); + expect(name.zip()).toBe('M.'); + }); }); describe('Config', () => { + beforeEach(() => (Config as any).cache.clear()); + test('creates a default configuration', () => { expect(Config.create()).toEqual( expect.objectContaining({ diff --git a/src/namefully.ts b/src/namefully.ts index a1b7360..eabd709 100644 --- a/src/namefully.ts +++ b/src/namefully.ts @@ -271,6 +271,8 @@ export class Namefully { * ``` */ fullName(orderedBy?: NameOptions['orderedBy']): string { + if (this.isMono) return this.#fullName.toString(); + const sep: string = this.config.ending ? ',' : ''; const names: string[] = []; @@ -297,6 +299,7 @@ export class Namefully { * preset configuration. */ birthName(orderedBy?: NameOptions['orderedBy']): string { + if (this.isMono) return this.#fullName.toString(); orderedBy ??= this.config.orderedBy; return orderedBy === NameOrder.FIRST_NAME ? [this.first, ...this.middleName(), this.last].join(' ') @@ -380,6 +383,8 @@ export class Namefully { * the surname is set as `mother` is equivalent to making it: `FirstName MotherName`. */ shorten(orderedBy?: NameOptions['orderedBy']): string { + if (this.isMono) return this.#fullName.toString(); + orderedBy ??= this.config.orderedBy; const { firstName, lastName } = this.#fullName; return orderedBy === NameOrder.FIRST_NAME From bb8133685251c21dc2263fc13be302e914bd3150 Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Sun, 15 Feb 2026 23:33:04 -0500 Subject: [PATCH 4/6] refactor: improve json name parsing --- example/index.html | 2 +- example/parser.ts | 2 +- src/data.ts | 12 +++--------- src/fullname.spec.ts | 42 ++++++++++++++++++++++++++++++------------ src/fullname.ts | 17 +++++++++-------- src/name.ts | 6 +++--- src/namefully.ts | 4 ++-- src/parser.ts | 26 ++++++++++++-------------- 8 files changed, 61 insertions(+), 50 deletions(-) diff --git a/example/index.html b/example/index.html index 8460b02..5846f97 100644 --- a/example/index.html +++ b/example/index.html @@ -70,7 +70,7 @@

It can also do that

` Flat version: ${name.zip()} `, ` Initials: ${name.initials().join(' ')} `, ` Public: ${name.public} `, - ` Public: ${name.salutation} `, + ` Salutation: ${name.salutation} `, ` Formatted: ${name.format('f (m) l')} `, ]; return `${rows.join('')}
`; diff --git a/example/parser.ts b/example/parser.ts index 45249f1..869a6fd 100644 --- a/example/parser.ts +++ b/example/parser.ts @@ -10,7 +10,7 @@ class CustomParser extends Parser { parse(options: Partial): FullName { const [fn, ln] = this.raw.split(this.separator, 2); - return FullName.parse({ firstName: fn.trim(), lastName: ln.trim() }, Config.merge(options)); + return new FullName(options).setFirstName(fn.trim()).setLastName(ln.trim()); } } diff --git a/src/data.ts b/src/data.ts index 67a9b96..d055395 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,19 +1,13 @@ import { NameBuilder } from './builder.js'; import { Namon, Separator } from './types.js'; -import { Name, FirstName, LastName } from './name.js'; +import { Name, FirstName, LastName, JsonName } from './name.js'; import { type Namefully, NameOptions } from './namefully.js'; import { InputError, NameError, UnknownError } from './error.js'; /** Serialized representation of a Namefully instance. */ export interface SerializedName { /** The name data (with its hierarchy intact). */ - names: { - prefix?: string; - firstName: string | { value: string; more?: string[] }; - middleName?: string[]; - lastName: string | { father: string; mother?: string }; - suffix?: string; - }; + names: JsonName; /** The configuration data. */ config: { name: string; @@ -55,7 +49,7 @@ export function deserialize(data: SerializedName | string): Namefully { if (px) builder.add(Name.prefix(px)); if (sx) builder.add(Name.suffix(sx)); - if (mn) builder.add(...mn.map((n) => Name.middle(n))); + if (mn) builder.add(...(typeof mn === 'string' ? [Name.middle(mn)] : mn.map((n) => Name.middle(n)))); builder.add(typeof fn === 'string' ? Name.first(fn) : new FirstName(fn.value, ...(fn.more ?? []))); builder.add(typeof ln === 'string' ? Name.last(ln) : new LastName(ln.father, ln.mother)); diff --git a/src/fullname.spec.ts b/src/fullname.spec.ts index d0259dc..d47b802 100644 --- a/src/fullname.spec.ts +++ b/src/fullname.spec.ts @@ -1,8 +1,8 @@ import { FirstName, LastName, Name } from './name.js'; +import { NameError, ValidationError } from './error.js'; import { FullName } from './fullname.js'; import { Config } from './config.js'; import { Namon } from './types.js'; -import { NameError } from './error.js'; describe('FullName', () => { let prefix: Name; @@ -31,6 +31,22 @@ describe('FullName', () => { }); runExpectations(fullName); + + fullName = FullName.parse({ + prefix: 'Mr', + firstName: { value: 'John', more: ['David'] }, + middleName: 'Michael', + lastName: { father: 'Smith', mother: 'Jones' }, + suffix: 'Ph.D', + }); + + expect(fullName.prefix?.toString()).toBe('Mr'); + expect(fullName.firstName.toString()).toBe('John'); + expect(fullName.firstName.toString(true)).toBe('John David'); + expect(fullName.middleName.map((n) => n.toString()).join(' ')).toBe('Michael'); + expect(fullName.lastName.toString()).toBe('Smith'); + expect(fullName.lastName.toString('all')).toBe('Smith Jones'); + expect(fullName.suffix?.toString()).toBe('Ph.D'); }); test('builds a full name as it goes', () => { @@ -44,17 +60,19 @@ describe('FullName', () => { runExpectations(fullName); }); - test('builds a full name with no validation rules', () => { - fullName = new FullName(Config.merge({ name: 'withBypass', bypass: true })) - .setFirstName(new FirstName('2Pac')) - .setLastName(new LastName('Shakur')); - - expect(fullName.firstName).toBeInstanceOf(FirstName); - expect(fullName.lastName).toBeInstanceOf(LastName); - expect(fullName.firstName.toString()).toBe('2Pac'); - expect(fullName.lastName.toString()).toBe('Shakur'); - expect(fullName.config).toBeDefined(); - expect(fullName.config.name).toBe('withBypass'); + test('enforces validation rules if needed when building a full name', () => { + try { + fullName = new FullName(Config.merge({ name: 'noBypass', bypass: false })) + .setFirstName(new FirstName('2Pac')) // name with digits is invalid + .setLastName(new LastName('Shakur')); + + fail('Expected NameError to be thrown'); + } catch (error: unknown) { + expect(error).toBeInstanceOf(NameError); + expect((error as NameError).name).toContain('ValidationError'); + expect((error as NameError).source).toContain('2Pac'); + expect((error as ValidationError).nameType).toBe('firstName'); + } }); test('creates a full name as it goes from raw strings', () => { diff --git a/src/fullname.ts b/src/fullname.ts index 8a41ea9..6f6d118 100644 --- a/src/fullname.ts +++ b/src/fullname.ts @@ -79,12 +79,13 @@ export class FullName { */ static parse(json: JsonName, config?: Config): FullName { try { + const { prefix, firstName: fn, middleName: mn, lastName: ln, suffix } = json; return new FullName(config) - .setPrefix(json.prefix) - .setFirstName(json.firstName) - .setMiddleName(json.middleName ?? []) - .setLastName(json.lastName) - .setSuffix(json.suffix); + .setPrefix(prefix) + .setFirstName(typeof fn === 'string' ? fn : new FirstName(fn.value, ...(fn.more ?? []))) + .setMiddleName(typeof mn === 'string' ? [mn] : (mn ?? [])) + .setLastName(typeof ln === 'string' ? ln : new LastName(ln.father, ln.mother)) + .setSuffix(suffix); } catch (error) { if (error instanceof NameError) throw error; @@ -100,7 +101,7 @@ export class FullName { if (!name) return this; if (!this.#config.bypass) Validators.prefix.validate(name); const prefix = name instanceof Name ? name.value : name; - this.#prefix = Name.prefix(this.#config.title === Title.US ? `${prefix}.` : prefix); + this.#prefix = Name.prefix(this.#config.title === Title.US && !prefix.endsWith('.') ? `${prefix}.` : prefix); return this; } @@ -182,8 +183,8 @@ export class Mononym extends FullName { * Constructs a mononym from a piece of string. * @param {string | Name} name to be used to construct the mononym. */ - constructor(name: string | Name) { - super(); + constructor(name: string | Name, options?: Partial) { + super(options ?? { name: 'mononym', mono: true }); this.#namon = name.toString(); this.type = name instanceof Name ? name.type : Namon.FIRST_NAME; } diff --git a/src/name.ts b/src/name.ts index abc9a62..0d5c9d7 100644 --- a/src/name.ts +++ b/src/name.ts @@ -296,8 +296,8 @@ export function isNameArray(value?: unknown): value is Name[] { /** JSON signature for `FullName` data. */ export interface JsonName { prefix?: string; - firstName: string; - middleName?: string[]; - lastName: string; + firstName: string | { value: string; more?: string[] }; + middleName?: string | string[]; + lastName: string | { father: string; mother?: string }; suffix?: string; } diff --git a/src/namefully.ts b/src/namefully.ts index eabd709..042a269 100644 --- a/src/namefully.ts +++ b/src/namefully.ts @@ -347,6 +347,8 @@ export class Namefully { only?: NameType | 'firstName' | 'lastName' | 'middleName' | 'birthName'; asJson?: boolean; }): string[] | Record { + if (this.isMono) return [this.#fullName.toString()[0]]; + const { orderedBy = this.config.orderedBy, only = NameType.BIRTH_NAME, asJson } = options ?? {}; const firstInits = this.#fullName.firstName.initials(); @@ -354,8 +356,6 @@ export class Namefully { const lastInits = this.#fullName.lastName.initials(); if (asJson) return { firstName: firstInits, middleName: midInits, lastName: lastInits }; - if (this.isMono) return [this.#fullName.toString()[0]]; - if (only !== NameType.BIRTH_NAME) { return only === NameType.FIRST_NAME ? firstInits : only === NameType.MIDDLE_NAME ? midInits : lastInits; } else if (orderedBy === NameOrder.FIRST_NAME) { diff --git a/src/parser.ts b/src/parser.ts index 929e45f..2d2613d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -16,6 +16,12 @@ export abstract class Parser { */ constructor(public raw: T) {} + /** + * Parses raw data into a `FullName` while applying some options. + * @param options for additional configuration to apply. + */ + abstract parse(options?: Partial): FullName; + /** * Builds a dynamic `Parser` on the fly and throws a `NameError` when unable * to do so. The built parser only knows how to operate birth names. @@ -32,10 +38,7 @@ export abstract class Parser { } if (length < 2) { - throw new InputError({ - source: text, - message: 'cannot build from invalid input', - }); + throw new InputError({ source: text, message: 'expecting at least 2 name parts' }); } else if (length === 2 || length === 3) { return new StringParser(text); } else { @@ -53,19 +56,14 @@ export abstract class Parser { return Promise.reject(error); } } - - /** - * Parses the raw data into a `FullName` while considering some options. - * @param options for additional configuration to apply. - */ - abstract parse(options?: Partial): FullName; } export class StringParser extends Parser { parse(options: Partial): FullName { - const config = Config.merge(options); - const names = this.raw.split(config.separator.token); - return (names.length === 1 && config.mono ? new MonoParser(names[0]) : new ArrayStringParser(names)).parse(options); + const { separator, mono } = Config.merge(options); + const names = this.raw.split(separator.token); + const parser = names.length === 1 && mono ? new MonoParser(names[0]) : new ArrayStringParser(names); + return parser.parse(options); } } @@ -154,6 +152,6 @@ export class MonoParser extends Parser { if (bypass) Validators.namon.validate(this.raw); const type = mono instanceof Namon ? mono : Namon.FIRST_NAME; - return new Mononym(new Name(this.raw.trim(), type)); + return new Mononym(new Name(this.raw.trim(), type), options); } } From f0829dfaa0f892b707a05fde1354874d7261254f Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Mon, 16 Feb 2026 00:29:10 -0500 Subject: [PATCH 5/6] refactor: apply minor improvements --- readme.md | 6 ++++-- src/builder.ts | 5 +++-- src/config.ts | 16 ++-------------- src/namefully.spec.ts | 10 +++++++++- src/parser.ts | 36 ++++++++++++++++++++++-------------- 5 files changed, 40 insertions(+), 33 deletions(-) diff --git a/readme.md b/readme.md index 5eecc0d..96321e5 100644 --- a/readme.md +++ b/readme.md @@ -247,8 +247,10 @@ import { Config, FullName, Namefully, Parser } from 'namefully'; // Suppose you want to cover this '#' separator class SimpleParser extends Parser { parse(options: Partial): FullName { - const [firstName, lastName] = this.raw.split('#'); - return FullName.parse({ firstName, lastName }, Config.merge(options)); + const [fn, ln] = this.raw.split('#', 2); + return new FullName(options) + .setFirstName(fn.trim()) + .setLastName(ln.trim()); } } diff --git a/src/builder.ts b/src/builder.ts index 66f5750..8fae278 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -145,9 +145,10 @@ export class NameBuilder extends Builder { this.prebuild?.(); const names = [...this.queue]; - ArrayNameValidator.create().validate(names); + const config = Config.merge(options as Partial); + if (!config.mono) ArrayNameValidator.create().validate(names); - this.instance = new Namefully(names, options); + this.instance = new Namefully(names, config); this.postbuild?.(this.instance); return this.instance; diff --git a/src/config.ts b/src/config.ts index 838fff0..c3e04c0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -191,25 +191,13 @@ export class Config { Config.cache.set(this.name, this); } - /** - * Alters the name order between the first and last name, and rearrange the - * order of appearance of a name set. - * @deprecated use `update()` method instead. - */ - updateOrder(orderedBy: NameOrder): void { - this.update({ orderedBy }); - } - - /** - * Allows the possibility to alter some options after creating a name set. - */ - update({ orderedBy, title, ending, mono }: Partial>): void { + /** Allows the possibility to alter behavior-related options after creating a name set. */ + update({ orderedBy, title, ending }: Partial>): void { const config = Config.cache.get(this.name); if (!config) return; if (orderedBy !== this.#orderedBy) config.#orderedBy = orderedBy!; if (title !== this.#title) config.#title = title!; if (ending !== this.#ending) config.#ending = ending!; - if (mono !== this.#mono) config.#mono = mono!; } /** Generates a unique new name. */ diff --git a/src/namefully.spec.ts b/src/namefully.spec.ts index 77730bf..3dd45a6 100644 --- a/src/namefully.spec.ts +++ b/src/namefully.spec.ts @@ -285,6 +285,7 @@ describe('Namefully', () => { test('string[]', () => { expect(new Namefully(['John', 'Smith']).toString()).toBe('John Smith'); expect(new Namefully(['Jane', 'D', 'Smith']).toString()).toBe('Jane D Smith'); + expect(new Namefully(['Madonna'], { mono: true }).toString()).toBe('Madonna'); }); test('json', () => { @@ -295,16 +296,23 @@ describe('Namefully', () => { const names = [new FirstName('John'), new LastName('Smith')]; expect(new Namefully(names).toString()).toBe('John Smith'); expect(new Namefully([Name.first('John'), Name.last('Smith'), Name.suffix('Ph.D')]).birth).toBe('John Smith'); + expect(new Namefully([Name.last('Madonna')], { mono: true }).toString()).toBe('Madonna'); }); test('NameBuilder', () => { const builder = NameBuilder.create(); builder.add(Name.first('John'), Name.last('Smith'), Name.suffix('Ph.D')); expect(builder.build().birth).toBe('John Smith'); + + builder.clear(); + builder.add(Name.middle('Madonna')); + expect(builder.size).toBe(1); + expect(builder.build({ mono: Namon.MIDDLE_NAME }).toString()).toBe('Madonna'); }); test('Parser (Custom Parser)', () => { - expect(new Namefully(new SimpleParser('John#Smith')).toString()).toBe('John Smith'); + const parser = new SimpleParser('John#Smith'); + expect(new Namefully(parser).toString()).toBe('John Smith'); }); test('deserialize()', () => { diff --git a/src/parser.ts b/src/parser.ts index 2d2613d..aee346a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -60,17 +60,19 @@ export abstract class Parser { export class StringParser extends Parser { parse(options: Partial): FullName { - const { separator, mono } = Config.merge(options); - const names = this.raw.split(separator.token); - const parser = names.length === 1 && mono ? new MonoParser(names[0]) : new ArrayStringParser(names); - return parser.parse(options); + const config = Config.merge(options); + const names = this.raw.split(config.separator.token); + return new ArrayStringParser(names).parse(config); } } export class ArrayStringParser extends Parser { parse(options: Partial): FullName { const config = Config.merge(options); - const fullName = new FullName(config); + + if (this.raw.length === 1 && config.mono) { + return new MonoParser(this.raw[0]).parse(config); + } const raw = this.raw.map((n) => n.trim()); const index = NameIndex.when(config.orderedBy, raw.length); @@ -83,8 +85,9 @@ export class ArrayStringParser extends Parser { } const { firstName, lastName, middleName, prefix, suffix } = index; - fullName.setFirstName(new FirstName(raw[firstName])); - fullName.setLastName(new LastName(raw[lastName])); + const fullName = new FullName(config) + .setFirstName(new FirstName(raw[firstName])) + .setLastName(new LastName(raw[lastName])); if (raw.length >= 3) fullName.setMiddleName(raw[middleName].split(config.separator.token)); if (raw.length >= 4) fullName.setPrefix(Name.prefix(raw[prefix])); @@ -123,10 +126,14 @@ export class NamaParser extends Parser { export class ArrayNameParser extends Parser { parse(options: Partial): FullName { const config = Config.merge(options); - const fullName = new FullName(config); - ArrayNameValidator.create().validate(this.raw); + if (this.raw.length === 1 && config.mono) { + return new MonoParser(this.raw[0]).parse(options); + } else { + ArrayNameValidator.create().validate(this.raw); + } + const fullName = new FullName(config); for (const name of this.raw) { if (name.isPrefix) { fullName.setPrefix(name); @@ -145,13 +152,14 @@ export class ArrayNameParser extends Parser { } } -export class MonoParser extends Parser { +export class MonoParser extends Parser { parse(options: Partial): Mononym { - const { bypass, mono } = Config.merge(options); + const config = Config.merge(options); - if (bypass) Validators.namon.validate(this.raw); + if (config.bypass) Validators.namon.validate(this.raw); - const type = mono instanceof Namon ? mono : Namon.FIRST_NAME; - return new Mononym(new Name(this.raw.trim(), type), options); + const type = config.mono instanceof Namon ? config.mono : Namon.FIRST_NAME; + const name = this.raw instanceof Name ? this.raw : new Name(this.raw.trim(), type); + return new Mononym(name, config); } } From 6db72daa57139bc3abcb0e6df148e0a1b48a43c3 Mon Sep 17 00:00:00 2001 From: Ralph Florent Date: Sat, 21 Feb 2026 23:41:48 -0500 Subject: [PATCH 6/6] chore: prep release version 2.2.0 with mononym support --- changelog.md | 17 +++++++++++++++++ jsr.json | 2 +- package.json | 2 +- src/constants.ts | 2 +- src/error.spec.ts | 4 ++++ src/namefully.spec.ts | 15 ++++++++++++++- 6 files changed, 38 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 90524b6..dce6c5e 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,21 @@ This file contains the documentation on the notable changes and bug fixes, and is formatted following this [standard](https://keepachangelog.com/en/1.0.0/). This project also adheres to [Semantic Versioning](https://semver.org/). +## [2.2.0] - 2026-02-22 + +**Added**: + +- Added support for mononyms (enabled via `Config.mono` flag); only available only for + `Namefully(string | string[] | Names[])` or `NameBuilder`. +- Updated FullName parsing to handle new JsonName structure (now hierarchical) +- Added more practical examples + +**Fixed**: + +- Refactored deserialization logic to accommodate flexible name formats +- Adjusted error handling for invalid name inputs +- Fixed deserialization issue when casting separators + ## [2.1.0] - 2025-12-30 **Added**: @@ -204,6 +219,8 @@ This project also adheres to [Semantic Versioning](https://semver.org/). Initial version +[2.2.0]: https://github.com/ralflorent/namefully/compare/v2.1.0...v2.2.0 +[2.1.0]: https://github.com/ralflorent/namefully/compare/v2.0.2...v2.1.0 [2.0.2]: https://github.com/ralflorent/namefully/compare/v2.0.1...v2.0.2 [2.0.1]: https://github.com/ralflorent/namefully/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/ralflorent/namefully/compare/v1.3.0...v2.0.0 diff --git a/jsr.json b/jsr.json index 41949a1..6f7dab4 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@ralflorent/namefully", - "version": "2.1.0", + "version": "2.2.0", "description": "Handle personal names in a particular order, way, or shape.", "keywords": ["format", "parse", "human", "personal", "family", "name"], "exports": "./src/index.ts", diff --git a/package.json b/package.json index 5e10409..97c31f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "namefully", - "version": "2.1.0", + "version": "2.2.0", "description": "Handle personal names in a particular order, way, or shape.", "author": "Ralph Florent", "license": "MIT", diff --git a/src/constants.ts b/src/constants.ts index 61d33b6..705e08c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -export const VERSION = '2.1.0'; +export const VERSION = '2.2.0'; export const MIN_NUMBER_OF_NAME_PARTS = 2; export const MAX_NUMBER_OF_NAME_PARTS = 5; export const ALLOWED_FORMAT_TOKENS = ` .,_-()[]<>'"bBfFlLmMnNoOpPsS$`; diff --git a/src/error.spec.ts b/src/error.spec.ts index ef72376..204a7a2 100644 --- a/src/error.spec.ts +++ b/src/error.spec.ts @@ -111,6 +111,10 @@ describe('InputError', () => { test('is thrown if the wrong argument is provided for a last name', () => { expect(() => Validators.lastName.validate({} as LastName)); }); + + test('is thrown for unknown input types', () => { + expect(() => new Namefully(class {} as never)).toThrow(Errors.InputError); + }); }); describe('NotAllowedError', () => { diff --git a/src/namefully.spec.ts b/src/namefully.spec.ts index 3dd45a6..ff637e3 100644 --- a/src/namefully.spec.ts +++ b/src/namefully.spec.ts @@ -3,6 +3,7 @@ import { NameError } from './error.js'; import { NameIndex } from './utils.js'; import { deserialize } from './data.js'; import { Namefully } from './namefully.js'; +import namefully from './namefully.js'; import { NameBuilder } from './builder.js'; import { ZERO_WIDTH_SPACE } from './constants.js'; import { FirstName, LastName, Name } from './name.js'; @@ -18,7 +19,7 @@ describe('Namefully', () => { }); test('.parts returns the name components as a sequence', () => { - expect(Array.from(name.parts).length).toBe(5); + expect(name.size).toBe(5); for (const part of name.parts) expect(part).toBeInstanceOf(Name); }); @@ -59,8 +60,11 @@ describe('Namefully', () => { test('.deepEqual() checks whether two names are equal from a component perspective', () => { const name1 = new Namefully('John Ben Smith'); const name2 = new Namefully([new FirstName('John', 'Ben'), new LastName('Smith')]); + const name3 = new Namefully([Name.first('John'), Name.middle('Ben'), Name.last('Smith')]); expect(name1.equal(name2)).toBe(true); expect(name1.deepEqual(name2)).toBe(false); + expect(name1.equal(name3)).toBe(true); + expect(name1.deepEqual(name3)).toBe(true); }); test('.get() gets the raw form of a name', () => { @@ -77,6 +81,7 @@ describe('Namefully', () => { expect(name.get('firstName')).toBeInstanceOf(FirstName); expect(name.get('lastName')).toBeInstanceOf(LastName); expect(name.get('suffix')).toBeInstanceOf(Name); + expect(name.get('middle')).toBeUndefined(); }); test('.json() returns a json version of the full name', () => { @@ -115,6 +120,7 @@ describe('Namefully', () => { expect(name.format('f $l.')).toBe('John S.'); expect(name.format('f $m. l')).toBe('John B. Smith'); expect(name.format('$F.$M.$L')).toBe('J.B.S'); + expect(name.format('$f.$m.$l')).toBe('J.B.S'); expect(name.format('$p')).toBe(''); expect(new Namefully('John Smith').format('o')).toBe('SMITH, John'); @@ -393,6 +399,13 @@ describe('Namefully', () => { await expect(Namefully.parse('John')).rejects.toThrow(NameError); }); + + test('default namefully function import', () => { + expect(namefully('John Smith').full).toBe('John Smith'); + expect(namefully(['John', 'Smith']).full).toBe('John Smith'); + expect(namefully([Name.first('John'), Name.last('Smith')]).full).toBe('John Smith'); + expect(namefully(new SimpleParser('John#Smith')).full).toBe('John Smith'); + }); }); describe('can be built with a name', () => {