utilitish 0.0.16 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/array/array-prototype.d.ts +44 -0
- package/dist/array/array-prototype.js +33 -0
- package/dist/array/array-prototype.spec.js +99 -0
- package/dist/string/string-prototype.d.ts +34 -0
- package/dist/string/string-prototype.js +17 -1
- package/dist/string/string-prototype.spec.js +25 -0
- package/dist/utils/core.utils.js +5 -1
- package/package.json +9 -2
|
@@ -409,5 +409,49 @@ declare global {
|
|
|
409
409
|
* - Throws on the first non-string item encountered in the array.
|
|
410
410
|
*/
|
|
411
411
|
slugifyIncludes(value: string): boolean;
|
|
412
|
+
/**
|
|
413
|
+
* Groups array elements into multiple groups based on an array of keys returned by the selector.
|
|
414
|
+
* Unlike `groupBy`, a single element can appear in multiple groups.
|
|
415
|
+
*
|
|
416
|
+
* @this {T[]} The array to group.
|
|
417
|
+
* @param {(item: T) => K[]} selector - A function returning an array of keys for each element.
|
|
418
|
+
* @returns {Map<K, T[]>} A Map where each key maps to an array of matching elements.
|
|
419
|
+
* @throws {TypeError} If selector is not a function or a string key.
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* const posts = [
|
|
423
|
+
* { title: 'A', tags: ['js', 'ts'] },
|
|
424
|
+
* { title: 'B', tags: ['ts', 'node'] },
|
|
425
|
+
* ];
|
|
426
|
+
* posts.groupByMany(post => post.tags);
|
|
427
|
+
* // Map { 'js' => [{ title: 'A', ... }], 'ts' => [{ title: 'A', ... }, { title: 'B', ... }], 'node' => [{ title: 'B', ... }] }
|
|
428
|
+
*
|
|
429
|
+
* @remarks
|
|
430
|
+
* - Elements with an empty key array are ignored.
|
|
431
|
+
* - Order of elements within each group follows the original array order.
|
|
432
|
+
*/
|
|
433
|
+
groupByMany<K>(this: T[], selector: Selector<T, K[]>): Map<K, T[]>;
|
|
434
|
+
/**
|
|
435
|
+
* Classifies array elements into named groups based on predicate functions.
|
|
436
|
+
* Each element is tested against all predicates and can appear in multiple groups.
|
|
437
|
+
*
|
|
438
|
+
* @this {T[]} The array to classify.
|
|
439
|
+
* @param {Record<K, (item: T) => boolean>} predicates - An object mapping group names to predicate functions.
|
|
440
|
+
* @returns {Map<K, T[]>} A Map where each key maps to elements matching the predicate.
|
|
441
|
+
* @throws {TypeError} If predicates is not a valid object.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* users.classify({
|
|
445
|
+
* adult: u => u.age >= 18,
|
|
446
|
+
* admin: u => u.roles.includes('ADMIN'),
|
|
447
|
+
* premium: u => u.premium,
|
|
448
|
+
* });
|
|
449
|
+
*
|
|
450
|
+
* @remarks
|
|
451
|
+
* - Elements matching no predicate are ignored.
|
|
452
|
+
* - Elements can appear in multiple groups.
|
|
453
|
+
* - The return type infers K as a union of the literal keys of the predicates object.
|
|
454
|
+
*/
|
|
455
|
+
classify<K extends string>(this: T[], predicates: Record<K, Selector<T, Boolean>>): Map<K, T[]>;
|
|
412
456
|
}
|
|
413
457
|
}
|
|
@@ -232,3 +232,36 @@ const logic_utils_1 = require("../utils/logic.utils");
|
|
|
232
232
|
return item.slugify() === slugified;
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
|
+
/**
|
|
236
|
+
* @see Array.prototype.groupByMany
|
|
237
|
+
*/
|
|
238
|
+
(0, core_utils_1.defineIfNotExists)(Array.prototype, 'groupByMany', function (selector) {
|
|
239
|
+
const getKeys = (0, core_utils_1.resolveSelector)(selector);
|
|
240
|
+
const result = new Map();
|
|
241
|
+
for (const item of this) {
|
|
242
|
+
const keys = getKeys(item);
|
|
243
|
+
for (const key of keys) {
|
|
244
|
+
if (!result.has(key))
|
|
245
|
+
result.set(key, []);
|
|
246
|
+
result.get(key).push(item);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
});
|
|
251
|
+
/**
|
|
252
|
+
* @see Array.prototype.classify
|
|
253
|
+
*/
|
|
254
|
+
(0, core_utils_1.defineIfNotExists)(Array.prototype, 'classify', function (predicates) {
|
|
255
|
+
if (typeof predicates !== 'object' || predicates === null)
|
|
256
|
+
(0, core_utils_1.utilitishError)('Array.prototype.classify', 'predicates must be a non-null object', predicates);
|
|
257
|
+
const resolvedPredicates = Object.fromEntries(Object.entries(predicates).map(([key, fn]) => [key, (0, core_utils_1.resolveSelector)(fn)]));
|
|
258
|
+
const entries = Object.entries(resolvedPredicates);
|
|
259
|
+
const result = new Map(entries.map(([key]) => [key, []]));
|
|
260
|
+
for (const item of this) {
|
|
261
|
+
for (const [key, predicate] of entries) {
|
|
262
|
+
if (!!predicate(item))
|
|
263
|
+
result.get(key).push(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
});
|
|
@@ -533,4 +533,103 @@ describe('Array.prototype', () => {
|
|
|
533
533
|
});
|
|
534
534
|
});
|
|
535
535
|
});
|
|
536
|
+
describe('Array.prototype.groupByMany', () => {
|
|
537
|
+
describe('with selector function', () => {
|
|
538
|
+
it('should group elements by multiple keys', () => {
|
|
539
|
+
const posts = [
|
|
540
|
+
{ title: 'A', tags: ['js', 'ts'] },
|
|
541
|
+
{ title: 'B', tags: ['ts', 'node'] },
|
|
542
|
+
];
|
|
543
|
+
const result = posts.groupByMany((p) => p.tags);
|
|
544
|
+
expect(result.get('js')).toEqual([{ title: 'A', tags: ['js', 'ts'] }]);
|
|
545
|
+
expect(result.get('ts')).toHaveLength(2);
|
|
546
|
+
expect(result.get('node')).toEqual([{ title: 'B', tags: ['ts', 'node'] }]);
|
|
547
|
+
});
|
|
548
|
+
it('should ignore elements with empty key array', () => {
|
|
549
|
+
const posts = [
|
|
550
|
+
{ title: 'A', tags: [] },
|
|
551
|
+
{ title: 'B', tags: ['ts'] },
|
|
552
|
+
];
|
|
553
|
+
const result = posts.groupByMany((p) => p.tags);
|
|
554
|
+
expect(result.has('ts')).toBe(true);
|
|
555
|
+
expect(result.size).toBe(1);
|
|
556
|
+
});
|
|
557
|
+
it('should return empty Map for empty array', () => {
|
|
558
|
+
expect([].groupByMany((x) => x.tags)).toEqual(new Map());
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
describe('error handling', () => {
|
|
562
|
+
it('should throw TypeError when selector is invalid', () => {
|
|
563
|
+
expect(() => [{ tags: ['a'] }].groupByMany(123)).toThrow(TypeError);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe('Array.prototype.classify', () => {
|
|
568
|
+
describe('with predicate object', () => {
|
|
569
|
+
it('should classify elements into named groups', () => {
|
|
570
|
+
const users = [
|
|
571
|
+
{ name: 'Alice', age: 25, admin: true, premium: false },
|
|
572
|
+
{ name: 'Bob', age: 16, admin: false, premium: true },
|
|
573
|
+
{ name: 'Carol', age: 30, admin: true, premium: true },
|
|
574
|
+
];
|
|
575
|
+
const result = users.classify({
|
|
576
|
+
adult: (u) => u.age >= 18,
|
|
577
|
+
admin: (u) => u.admin,
|
|
578
|
+
premium: (u) => u.premium,
|
|
579
|
+
});
|
|
580
|
+
expect(result.get('adult')).toHaveLength(2);
|
|
581
|
+
expect(result.get('admin')).toHaveLength(2);
|
|
582
|
+
expect(result.get('premium')).toHaveLength(2);
|
|
583
|
+
});
|
|
584
|
+
it('should allow an element to appear in multiple groups', () => {
|
|
585
|
+
const users = [{ name: 'Alice', age: 25, admin: true, premium: true }];
|
|
586
|
+
const result = users.classify({
|
|
587
|
+
adult: (u) => u.age >= 18,
|
|
588
|
+
admin: (u) => u.admin,
|
|
589
|
+
});
|
|
590
|
+
expect(result.get('adult')).toHaveLength(1);
|
|
591
|
+
expect(result.get('admin')).toHaveLength(1);
|
|
592
|
+
});
|
|
593
|
+
it('should ignore elements matching no predicate', () => {
|
|
594
|
+
const users = [{ name: 'Bob', age: 16, admin: false, premium: false }];
|
|
595
|
+
const result = users.classify({
|
|
596
|
+
adult: (u) => u.age >= 18,
|
|
597
|
+
admin: (u) => u.admin,
|
|
598
|
+
});
|
|
599
|
+
expect(result.get('adult')).toHaveLength(0);
|
|
600
|
+
expect(result.get('admin')).toHaveLength(0);
|
|
601
|
+
});
|
|
602
|
+
it('should return Map with empty arrays for empty input', () => {
|
|
603
|
+
const result = [].classify({ adult: (u) => u.age >= 18 });
|
|
604
|
+
expect(result.get('adult')).toEqual([]);
|
|
605
|
+
});
|
|
606
|
+
it('should work with key', () => {
|
|
607
|
+
const users = [
|
|
608
|
+
{ name: 'Alice', age: 0, admin: false, premium: false },
|
|
609
|
+
{ name: 'Bob', age: 16, admin: false, premium: false },
|
|
610
|
+
{ name: 'Chloe', age: 24, admin: true, premium: false },
|
|
611
|
+
];
|
|
612
|
+
const result = users.classify({
|
|
613
|
+
admin: 'admin',
|
|
614
|
+
age: 'age',
|
|
615
|
+
});
|
|
616
|
+
expect(result.get('admin')).toHaveLength(1);
|
|
617
|
+
expect(result.get('age')).toHaveLength(2);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
describe('error handling', () => {
|
|
621
|
+
it('should throw Error when key selector is invalid', () => {
|
|
622
|
+
expect(() => [{ id: 1 }].classify(123)).toThrow(Error);
|
|
623
|
+
});
|
|
624
|
+
it('should throw TypeError when predicates is not an object', () => {
|
|
625
|
+
expect(() => [].classify('invalid')).toThrow(TypeError);
|
|
626
|
+
});
|
|
627
|
+
it('should throw TypeError when predicates is null', () => {
|
|
628
|
+
expect(() => [].classify(null)).toThrow(TypeError);
|
|
629
|
+
});
|
|
630
|
+
it('should throw TypeError when a predicate is not a function', () => {
|
|
631
|
+
expect(() => [{ age: 25 }].classify({ adult: 'not an attribut' })).toThrow(TypeError);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
});
|
|
536
635
|
});
|
|
@@ -87,6 +87,40 @@ declare global {
|
|
|
87
87
|
* - Uses `splitWords()` internally
|
|
88
88
|
*/
|
|
89
89
|
snakeCase(): string;
|
|
90
|
+
/**
|
|
91
|
+
* Converts this string to sentence case by splitting into words and joining them in lowercase.
|
|
92
|
+
*
|
|
93
|
+
* @this {string} The string to convert.
|
|
94
|
+
* @returns {string} The string with all words lowercased and joined by spaces.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* 'helloWorld'.sentenceCase(); // 'hello world'
|
|
98
|
+
* 'Hello-World'.sentenceCase(); // 'hello world'
|
|
99
|
+
* 'hello_world'.sentenceCase(); // 'hello world'
|
|
100
|
+
* 'HELLO WORLD'.sentenceCase(); // 'hello world'
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* - Uses `splitWords` internally to handle camelCase, kebab-case, snake_case, and spaces.
|
|
104
|
+
* - All words are lowercased before joining.
|
|
105
|
+
*/
|
|
106
|
+
sentenceCase(): string;
|
|
107
|
+
/**
|
|
108
|
+
* Converts this string to title case by capitalizing the first letter of each word.
|
|
109
|
+
*
|
|
110
|
+
* @this {string} The string to convert.
|
|
111
|
+
* @returns {string} The string with each word capitalized and joined by spaces.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* 'helloWorld'.titleCase(); // 'Hello World'
|
|
115
|
+
* 'hello-world'.titleCase(); // 'Hello World'
|
|
116
|
+
* 'hello_world'.titleCase(); // 'Hello World'
|
|
117
|
+
* 'HELLO WORLD'.titleCase(); // 'Hello World'
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* - Uses `splitWords` internally to handle camelCase, kebab-case, snake_case, and spaces.
|
|
121
|
+
* - Each word is lowercased then capitalized.
|
|
122
|
+
*/
|
|
123
|
+
titleCase(): string;
|
|
90
124
|
/**
|
|
91
125
|
* Truncates the string to a maximum number of characters, appending '...' if truncated.
|
|
92
126
|
*
|
|
@@ -8,7 +8,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
8
8
|
(0, core_utils_1.defineIfNotExists)(String.prototype, 'splitWords', function () {
|
|
9
9
|
return this.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // helloWorld → hello World
|
|
10
10
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // HTMLParser → HTML Parser
|
|
11
|
-
.replace(/[
|
|
11
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
12
12
|
.trim()
|
|
13
13
|
.split(/\s+/)
|
|
14
14
|
.filter(Boolean);
|
|
@@ -38,6 +38,22 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
38
38
|
.map((w) => w.toLowerCase())
|
|
39
39
|
.join('_');
|
|
40
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* @see String.prototype.sentenceCase
|
|
43
|
+
*/
|
|
44
|
+
(0, core_utils_1.defineIfNotExists)(String.prototype, 'sentenceCase', function () {
|
|
45
|
+
return this.splitWords()
|
|
46
|
+
.map((w) => w.toLowerCase())
|
|
47
|
+
.join(' ');
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* @see String.prototype.titleCase
|
|
51
|
+
*/
|
|
52
|
+
(0, core_utils_1.defineIfNotExists)(String.prototype, 'titleCase', function () {
|
|
53
|
+
return this.splitWords()
|
|
54
|
+
.map((w) => w.toLowerCase().capitalize())
|
|
55
|
+
.join(' ');
|
|
56
|
+
});
|
|
41
57
|
/**
|
|
42
58
|
* @see String.prototype.truncate
|
|
43
59
|
*/
|
|
@@ -18,6 +18,13 @@ describe('String.prototype', () => {
|
|
|
18
18
|
expect('hello world'.splitWords()).toEqual(['hello', 'world']);
|
|
19
19
|
expect('HTMLParser'.splitWords()).toEqual(['HTML', 'Parser']);
|
|
20
20
|
});
|
|
21
|
+
it('should split with accent', () => {
|
|
22
|
+
expect('attaqueChargé'.splitWords()).toEqual(['attaque', 'Chargé']);
|
|
23
|
+
expect('attaque-chargé'.splitWords()).toEqual(['attaque', 'chargé']);
|
|
24
|
+
expect('attaque_chargé'.splitWords()).toEqual(['attaque', 'chargé']);
|
|
25
|
+
expect('attaque chargé'.splitWords()).toEqual(['attaque', 'chargé']);
|
|
26
|
+
expect('ATTAQUEChargé'.splitWords()).toEqual(['ATTAQUE', 'Chargé']);
|
|
27
|
+
});
|
|
21
28
|
});
|
|
22
29
|
describe('camelCase()', () => {
|
|
23
30
|
it('should convert to camelCase', () => {
|
|
@@ -40,6 +47,24 @@ describe('String.prototype', () => {
|
|
|
40
47
|
expect('hello_world_test'.snakeCase()).toBe('hello_world_test');
|
|
41
48
|
});
|
|
42
49
|
});
|
|
50
|
+
describe('sentenceCase()', () => {
|
|
51
|
+
it('should convert to sentence case', () => {
|
|
52
|
+
expect('helloWorld'.sentenceCase()).toBe('hello world');
|
|
53
|
+
expect('hello-world'.sentenceCase()).toBe('hello world');
|
|
54
|
+
expect('hello_world'.sentenceCase()).toBe('hello world');
|
|
55
|
+
expect('HELLO WORLD'.sentenceCase()).toBe('hello world');
|
|
56
|
+
expect('attaqueChargé'.sentenceCase()).toBe('attaque chargé');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('titleCase()', () => {
|
|
60
|
+
it('should convert to title case', () => {
|
|
61
|
+
expect('helloWorld'.titleCase()).toBe('Hello World');
|
|
62
|
+
expect('hello-world'.titleCase()).toBe('Hello World');
|
|
63
|
+
expect('hello_world'.titleCase()).toBe('Hello World');
|
|
64
|
+
expect('HELLO WORLD'.titleCase()).toBe('Hello World');
|
|
65
|
+
expect('attaqueChargé'.titleCase()).toBe('Attaque Chargé');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
43
68
|
describe('truncate()', () => {
|
|
44
69
|
it('should truncate and add ... if needed', () => {
|
|
45
70
|
expect('hello world'.truncate(5)).toBe('hello...');
|
package/dist/utils/core.utils.js
CHANGED
|
@@ -104,7 +104,11 @@ function resolveSelector(selector, fallback, name = 'selector') {
|
|
|
104
104
|
return selector;
|
|
105
105
|
}
|
|
106
106
|
if (typeof selector === 'string') {
|
|
107
|
-
return (item) =>
|
|
107
|
+
return (item) => {
|
|
108
|
+
if (!(selector in item))
|
|
109
|
+
throw new TypeError(`selector string must be a key of the item`);
|
|
110
|
+
return item[selector];
|
|
111
|
+
};
|
|
108
112
|
}
|
|
109
113
|
if (selector) {
|
|
110
114
|
throw new TypeError(`${name} must be a function or a string key`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "utilitish",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/FDonovan12/utilitish.git"
|
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
"provenance": false
|
|
11
11
|
},
|
|
12
12
|
"description": "",
|
|
13
|
+
"moduleResolution": "Node16",
|
|
13
14
|
"main": "dist/index.js",
|
|
14
15
|
"types": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
15
22
|
"files": [
|
|
16
23
|
"dist"
|
|
17
24
|
],
|
|
@@ -19,7 +26,7 @@
|
|
|
19
26
|
"build": "tsc",
|
|
20
27
|
"test": "jest",
|
|
21
28
|
"docs": "typedoc",
|
|
22
|
-
"release": "npm test && npm run build && npm version patch && git push --follow-tags && npm publish"
|
|
29
|
+
"release": "(npm whoami || npm login) && npm test && npm run build && npm version patch && git push --follow-tags && npm publish"
|
|
23
30
|
},
|
|
24
31
|
"keywords": [
|
|
25
32
|
"typescript",
|