From eb8ac1892c8ac95aff85c7444ff9b8417afb1534 Mon Sep 17 00:00:00 2001 From: Eric King Date: Mon, 13 Jul 2026 11:49:12 -0500 Subject: [PATCH 1/4] chore(deps-dev): updated dev dependencies --- package.json | 2 +- pnpm-lock.yaml | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 5b47136..c24799b 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "lint-staged": "^17.0.8", "prettier": "^3.9.5", "rimraf": "^6.1.3", - "semantic-release": "^25.0.5", + "semantic-release": "^25.0.6", "typedoc": "^0.28.20", "validate-package-exports": "^1.2.1", "vitest": "^4.1.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bc7db1..e04b58f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^6.1.3 version: 6.1.3 semantic-release: - specifier: ^25.0.5 - version: 25.0.5(typescript@6.0.3) + specifier: ^25.0.6 + version: 25.0.6(typescript@6.0.3) typedoc: specifier: ^0.28.20 version: 0.28.20(typescript@6.0.3) @@ -1239,8 +1239,8 @@ packages: resolution: {integrity: sha512-H7AeU7BrEeEOiV82h0s/IPyiRClSUU+NCYU8/rVfrTGY1e7zL+WaIYdVhkJLq49r2WIvURNoQVi7X2cbVoSjOQ==} engines: {node: '>=22.0.0'} - '@webdeveric/utils@0.83.0': - resolution: {integrity: sha512-eE2HjbZIQ/KB+YH5P3fi2PTjoZKxtTTPMTQbHqX8TmLjsnN6PmRQAtIBWfR2SBCV1Rd+c4QXwCE5AqgyO6Tx2Q==} + '@webdeveric/utils@0.83.1': + resolution: {integrity: sha512-pIcll4hz2vXjvwGsIvHRUBDqBeDTC4mWQnY3RtD23bCL+E+ij+8FCqOoobHwxb8NGWbnSYb6Njh7fZRS8P7ddQ==} engines: {node: '>=18.0.0'} abbrev@4.0.0: @@ -2503,8 +2503,8 @@ packages: json-stringify-nice@1.1.4: resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} @@ -3324,8 +3324,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - semantic-release@25.0.5: - resolution: {integrity: sha512-mn61SUJwtM8ThrWn2WmgLVpwVJeG/hPSupua1psdMoufmwRIPyvRLkRkL0JDXkP67OntlLWUYnBnfVc8EDO3/g==} + semantic-release@25.0.6: + resolution: {integrity: sha512-EGS5PWCDavYD4jAE4vKQ+VKcwXFpxLsKg05UcrUQwqxUCM1KtNRx9ZdMyYJL3z2aTyAH9zDgQSNFjRvbsIuKHQ==} engines: {node: ^22.14.0 || >= 24.10.0} hasBin: true @@ -4722,7 +4722,7 @@ snapshots: '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 content-type: 2.0.0 - json-with-bigint: 3.5.8 + json-with-bigint: 3.5.10 universal-user-agent: 7.0.3 '@octokit/types@16.0.0': @@ -4798,7 +4798,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.6(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -4808,13 +4808,13 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.6(typescript@6.0.3) transitivePeerDependencies: - supports-color '@semantic-release/error@4.0.0': {} - '@semantic-release/github@12.0.9(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/github@12.0.9(semantic-release@25.0.6(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -4830,7 +4830,7 @@ snapshots: lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.6(typescript@6.0.3) tinyglobby: 0.2.17 undici: 7.28.0 url-join: 5.0.0 @@ -4838,7 +4838,7 @@ snapshots: - kerberos - supports-color - '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.6(typescript@6.0.3))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 @@ -4853,11 +4853,11 @@ snapshots: rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.6(typescript@6.0.3) semver: 7.8.5 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.6(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -4867,7 +4867,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.6(typescript@6.0.3) transitivePeerDependencies: - supports-color @@ -5241,7 +5241,7 @@ snapshots: '@webdeveric/ts-data-structures@1.0.1': {} - '@webdeveric/utils@0.83.0': {} + '@webdeveric/utils@0.83.1': {} abbrev@4.0.0: {} @@ -6670,7 +6670,7 @@ snapshots: json-stringify-nice@1.1.4: {} - json-with-bigint@3.5.8: {} + json-with-bigint@3.5.10: {} json5@1.0.2: dependencies: @@ -7470,13 +7470,13 @@ snapshots: dependencies: xmlchars: 2.2.0 - semantic-release@25.0.5(typescript@6.0.3): + semantic-release@25.0.6(typescript@6.0.3): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.6(typescript@6.0.3)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.9(semantic-release@25.0.5(typescript@6.0.3)) - '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) - '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/github': 12.0.9(semantic-release@25.0.6(typescript@6.0.3)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.6(typescript@6.0.3)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.6(typescript@6.0.3)) aggregate-error: 5.0.0 cosmiconfig: 9.0.2(typescript@6.0.3) debug: 4.4.3 @@ -7990,7 +7990,7 @@ snapshots: validate-package-exports@1.2.1: dependencies: '@npmcli/arborist': 9.9.0 - '@webdeveric/utils': 0.83.0 + '@webdeveric/utils': 0.83.1 npm-packlist: 11.3.0 transitivePeerDependencies: - supports-color From bac84a676045dfca87980ba4e34ac9a9628586b0 Mon Sep 17 00:00:00 2001 From: Eric King Date: Mon, 13 Jul 2026 11:53:07 -0500 Subject: [PATCH 2/4] feat: added `codePointLength()` --- src/codePointLength.ts | 13 +++++++++++++ src/index.ts | 1 + test/codePointLength.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 src/codePointLength.ts create mode 100644 test/codePointLength.test.ts diff --git a/src/codePointLength.ts b/src/codePointLength.ts new file mode 100644 index 0000000..232dfe8 --- /dev/null +++ b/src/codePointLength.ts @@ -0,0 +1,13 @@ +/** + * Get the number of Unicode code points in `input`, unlike `string.length` which counts UTF-16 code units. + * + * @example + * ```ts + * codePointLength('hello'); // 5 + * codePointLength('๐Ÿ˜€'); // 1 + * '๐Ÿ˜€'.length; // 2 + * ``` + */ +export function codePointLength(input: string): number { + return [...input].length; +} diff --git a/src/index.ts b/src/index.ts index 78c33c0..f0aa4b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ export * from './capitalize.js'; export * from './cartesian.js'; export * from './chunk.js'; export * from './clamp.js'; +export * from './codePointLength.js'; export * from './combinations.js'; export * from './comment.js'; export * from './compose.js'; diff --git a/test/codePointLength.test.ts b/test/codePointLength.test.ts new file mode 100644 index 0000000..6f7e893 --- /dev/null +++ b/test/codePointLength.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { codePointLength } from '../src/codePointLength.js'; + +describe('codePointLength()', () => { + it('Empty input returns 0', () => { + expect(codePointLength('')).toBe(0); + }); + + it('Counts ASCII characters', () => { + expect(codePointLength('hello')).toBe(5); + }); + + it('Counts a single character made of a surrogate pair as 1', () => { + expect(codePointLength('๐Ÿ˜€')).toBe(1); + expect('๐Ÿ˜€'.length).toBe(2); + }); + + it('Counts a mix of ASCII and surrogate pair characters', () => { + expect(codePointLength('a๐Ÿ˜€b๐Ÿ˜€c')).toBe(5); + expect('a๐Ÿ˜€b๐Ÿ˜€c'.length).toBe(7); + }); +}); From 0377a4adff8fe66bcc190b73bde6fcc4dc3acd81 Mon Sep 17 00:00:00 2001 From: Eric King Date: Mon, 13 Jul 2026 14:10:41 -0500 Subject: [PATCH 3/4] feat: added `graphemeLength()` --- src/graphemeLength.ts | 22 ++++++++++++++++++++++ src/index.ts | 1 + test/graphemeLength.test.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/graphemeLength.ts create mode 100644 test/graphemeLength.test.ts diff --git a/src/graphemeLength.ts b/src/graphemeLength.ts new file mode 100644 index 0000000..247b955 --- /dev/null +++ b/src/graphemeLength.ts @@ -0,0 +1,22 @@ +const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + +/** + * Get the number of grapheme clusters (user-perceived characters) in `input`, unlike `string.length` which counts UTF-16 code units. + * + * @example + * ```ts + * graphemeLength('hello'); // 5 + * graphemeLength('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); // 1 + * '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'.length; // 11 + * ``` + */ +export function graphemeLength(input: string): number { + let count = 0; + + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars + for (const _ of segmenter.segment(input)) { + count++; + } + + return count; +} diff --git a/src/index.ts b/src/index.ts index f0aa4b6..a5ddeb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ export * from './getPaths.js'; export * from './getRandomItem.js'; export * from './getSortedObject.js'; export * from './getType.js'; +export * from './graphemeLength.js'; export * from './has.js'; export * from './hasAdditionalProperties.js'; export * from './inRange.js'; diff --git a/test/graphemeLength.test.ts b/test/graphemeLength.test.ts new file mode 100644 index 0000000..d6297f7 --- /dev/null +++ b/test/graphemeLength.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { graphemeLength } from '../src/graphemeLength.js'; + +describe('graphemeLength()', () => { + it('Empty input returns 0', () => { + expect(graphemeLength('')).toBe(0); + }); + + it('Counts ASCII characters', () => { + expect(graphemeLength('hello')).toBe(5); + }); + + it('Counts a single character made of a surrogate pair as 1', () => { + expect(graphemeLength('๐Ÿ˜€')).toBe(1); + expect('๐Ÿ˜€'.length).toBe(2); + }); + + it('Counts a ZWJ emoji sequence as a single grapheme', () => { + expect(graphemeLength('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ')).toBe(1); + expect('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'.length).toBe(11); + }); + + it('Counts a combining character sequence as a single grapheme', () => { + expect(graphemeLength('eฬ')).toBe(1); + expect('eฬ'.length).toBe(2); + }); + + it('Counts a mix of ASCII and multi-code-unit graphemes', () => { + expect(graphemeLength('a๐Ÿ˜€b๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆc')).toBe(5); + }); +}); From 786f5cbcc245f0f973a4bf79387ec8f658bcc4b5 Mon Sep 17 00:00:00 2001 From: Eric King Date: Mon, 13 Jul 2026 16:32:44 -0500 Subject: [PATCH 4/4] chore(linting): removed `pathGroups` option --- .eslintrc.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 66a939c..772379b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,14 +56,7 @@ "caseInsensitive": true }, "groups": ["builtin", "external", "internal", "parent", ["sibling", "index"], "type"], - "newlines-between": "always", - "pathGroups": [ - { - "pattern": "@{src,commands}/**", - "group": "internal", - "position": "after" - } - ] + "newlines-between": "always" } ], "sort-imports": "off"