utilitish 0.0.19 → 0.0.20
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-constructor.js +10 -12
- package/dist/array/array-constructor.spec.js +2 -4
- package/dist/array/array-prototype.js +55 -57
- package/dist/array/array-prototype.spec.js +1 -3
- package/dist/index.js +7 -13
- package/dist/map/map-prototype.js +12 -14
- package/dist/map/map-prototype.spec.js +1 -3
- package/dist/object/object-prototype.js +8 -10
- package/dist/object/object-prototype.spec.js +1 -3
- package/dist/set/set-prototype.js +10 -12
- package/dist/set/set-prototype.spec.js +1 -3
- package/dist/string/string-prototype.js +35 -37
- package/dist/string/string-prototype.spec.js +23 -25
- package/dist/utils/core.utils.js +6 -15
- package/dist/utils/core.utils.spec.js +29 -31
- package/dist/utils/logic.utils.js +6 -9
- package/dist/utils/logic.utils.spec.js +13 -15
- package/dist/utils/slugify.config.js +9 -21
- package/dist/utils/slugify.config.spec.js +40 -42
- package/package.json +2 -2
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const core_utils_1 = require("../utils/core.utils");
|
|
4
|
-
const slugify_config_1 = require("../utils/slugify.config");
|
|
1
|
+
import { defineIfNotExists, utilitishError } from '../utils/core.utils';
|
|
2
|
+
import { slugifyString } from '../utils/slugify.config';
|
|
5
3
|
/**
|
|
6
4
|
* @see String.prototype.splitWords
|
|
7
5
|
*/
|
|
8
|
-
|
|
6
|
+
defineIfNotExists(String.prototype, 'splitWords', function () {
|
|
9
7
|
return this.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // helloWorld → hello World
|
|
10
8
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // HTMLParser → HTML Parser
|
|
11
9
|
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
@@ -16,7 +14,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
16
14
|
/**
|
|
17
15
|
* @see String.prototype.camelCase
|
|
18
16
|
*/
|
|
19
|
-
|
|
17
|
+
defineIfNotExists(String.prototype, 'camelCase', function () {
|
|
20
18
|
const words = this.splitWords().map((w) => w.toLowerCase());
|
|
21
19
|
return words
|
|
22
20
|
.map((word, i) => (i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))
|
|
@@ -25,7 +23,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
25
23
|
/**
|
|
26
24
|
* @see String.prototype.kebabCase
|
|
27
25
|
*/
|
|
28
|
-
|
|
26
|
+
defineIfNotExists(String.prototype, 'kebabCase', function () {
|
|
29
27
|
return this.splitWords()
|
|
30
28
|
.map((w) => w.toLowerCase())
|
|
31
29
|
.join('-');
|
|
@@ -33,7 +31,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
33
31
|
/**
|
|
34
32
|
* @see String.prototype.snakeCase
|
|
35
33
|
*/
|
|
36
|
-
|
|
34
|
+
defineIfNotExists(String.prototype, 'snakeCase', function () {
|
|
37
35
|
return this.splitWords()
|
|
38
36
|
.map((w) => w.toLowerCase())
|
|
39
37
|
.join('_');
|
|
@@ -41,7 +39,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
41
39
|
/**
|
|
42
40
|
* @see String.prototype.sentenceCase
|
|
43
41
|
*/
|
|
44
|
-
|
|
42
|
+
defineIfNotExists(String.prototype, 'sentenceCase', function () {
|
|
45
43
|
return this.splitWords()
|
|
46
44
|
.map((w) => w.toLowerCase())
|
|
47
45
|
.join(' ');
|
|
@@ -49,7 +47,7 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
49
47
|
/**
|
|
50
48
|
* @see String.prototype.titleCase
|
|
51
49
|
*/
|
|
52
|
-
|
|
50
|
+
defineIfNotExists(String.prototype, 'titleCase', function () {
|
|
53
51
|
return this.splitWords()
|
|
54
52
|
.map((w) => w.toLowerCase().capitalize())
|
|
55
53
|
.join(' ');
|
|
@@ -57,37 +55,37 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
57
55
|
/**
|
|
58
56
|
* @see String.prototype.truncate
|
|
59
57
|
*/
|
|
60
|
-
|
|
58
|
+
defineIfNotExists(String.prototype, 'truncate', function (n) {
|
|
61
59
|
if (typeof n !== 'number')
|
|
62
|
-
|
|
60
|
+
utilitishError('String.prototype.truncate', 'n must be a number', n);
|
|
63
61
|
if (!Number.isInteger(n))
|
|
64
|
-
|
|
62
|
+
utilitishError('String.prototype.truncate', 'n must be an integer', n);
|
|
65
63
|
if (n < 0)
|
|
66
|
-
|
|
64
|
+
utilitishError('String.prototype.truncate', 'n must be non-negative', n);
|
|
67
65
|
return this.length > n ? this.slice(0, n) + '...' : this.toString();
|
|
68
66
|
});
|
|
69
67
|
/**
|
|
70
68
|
* @see String.prototype.reverse
|
|
71
69
|
*/
|
|
72
|
-
|
|
70
|
+
defineIfNotExists(String.prototype, 'reverse', function () {
|
|
73
71
|
return [...this].reverse().join('');
|
|
74
72
|
});
|
|
75
73
|
/**
|
|
76
74
|
* @see String.prototype.isEmpty
|
|
77
75
|
*/
|
|
78
|
-
|
|
76
|
+
defineIfNotExists(String.prototype, 'isEmpty', function () {
|
|
79
77
|
return this.trim().length === 0;
|
|
80
78
|
});
|
|
81
79
|
/**
|
|
82
80
|
* @see String.prototype.slugify
|
|
83
81
|
*/
|
|
84
|
-
|
|
85
|
-
return
|
|
82
|
+
defineIfNotExists(String.prototype, 'slugify', function (config) {
|
|
83
|
+
return slugifyString(this, config);
|
|
86
84
|
});
|
|
87
85
|
/**
|
|
88
86
|
* @see String.prototype.capitalize
|
|
89
87
|
*/
|
|
90
|
-
|
|
88
|
+
defineIfNotExists(String.prototype, 'capitalize', function () {
|
|
91
89
|
if (this.length === 0)
|
|
92
90
|
return '';
|
|
93
91
|
return this.charAt(0).toUpperCase() + this.slice(1);
|
|
@@ -95,62 +93,62 @@ const slugify_config_1 = require("../utils/slugify.config");
|
|
|
95
93
|
/**
|
|
96
94
|
* @see String.prototype.slugifyEquals
|
|
97
95
|
*/
|
|
98
|
-
|
|
96
|
+
defineIfNotExists(String.prototype, 'slugifyEquals', function (other) {
|
|
99
97
|
if (typeof other !== 'string')
|
|
100
|
-
|
|
98
|
+
utilitishError('String.prototype.slugifyEquals', 'other must be a string', other);
|
|
101
99
|
return this.slugify() === other.slugify();
|
|
102
100
|
});
|
|
103
101
|
/**
|
|
104
102
|
* @see String.prototype.slugifyIncludes
|
|
105
103
|
*/
|
|
106
|
-
|
|
104
|
+
defineIfNotExists(String.prototype, 'slugifyIncludes', function (other) {
|
|
107
105
|
if (typeof other !== 'string')
|
|
108
|
-
|
|
106
|
+
utilitishError('String.prototype.slugifyIncludes', 'other must be a string', other);
|
|
109
107
|
return this.slugify().includes(other.slugify());
|
|
110
108
|
});
|
|
111
109
|
/**
|
|
112
110
|
* @see String.prototype.slugifyStartsWith
|
|
113
111
|
*/
|
|
114
|
-
|
|
112
|
+
defineIfNotExists(String.prototype, 'slugifyStartsWith', function (other) {
|
|
115
113
|
if (typeof other !== 'string')
|
|
116
|
-
|
|
114
|
+
utilitishError('String.prototype.slugifyStartsWith', 'other must be a string', other);
|
|
117
115
|
return this.slugify().startsWith(other.slugify());
|
|
118
116
|
});
|
|
119
117
|
/**
|
|
120
118
|
* @see String.prototype.slugifyEndsWith
|
|
121
119
|
*/
|
|
122
|
-
|
|
120
|
+
defineIfNotExists(String.prototype, 'slugifyEndsWith', function (other) {
|
|
123
121
|
if (typeof other !== 'string')
|
|
124
|
-
|
|
122
|
+
utilitishError('String.prototype.slugifyEndsWith', 'other must be a string', other);
|
|
125
123
|
return this.slugify().endsWith(other.slugify());
|
|
126
124
|
});
|
|
127
125
|
/**
|
|
128
126
|
* @see String.prototype.slugifyIn
|
|
129
127
|
*/
|
|
130
|
-
|
|
128
|
+
defineIfNotExists(String.prototype, 'slugifyIn', function (list) {
|
|
131
129
|
if (!Array.isArray(list))
|
|
132
|
-
|
|
130
|
+
utilitishError('String.prototype.slugifyIn', 'list must be an array', list);
|
|
133
131
|
if (!list.every((item) => typeof item === 'string'))
|
|
134
|
-
|
|
132
|
+
utilitishError('String.prototype.slugifyIn', 'list must be an array of strings', list);
|
|
135
133
|
const slugified = this.slugify();
|
|
136
134
|
return list.some((item) => item.slugify() === slugified);
|
|
137
135
|
});
|
|
138
136
|
/**
|
|
139
137
|
* @see String.prototype.replaceRange
|
|
140
138
|
*/
|
|
141
|
-
|
|
139
|
+
defineIfNotExists(String.prototype, 'replaceRange', function (start, end, replaceString = '') {
|
|
142
140
|
if (!Number.isInteger(start))
|
|
143
|
-
|
|
141
|
+
utilitishError('String.prototype.replaceRange', 'start must be an integer', start);
|
|
144
142
|
if (!Number.isInteger(end))
|
|
145
|
-
|
|
143
|
+
utilitishError('String.prototype.replaceRange', 'end must be an integer', end);
|
|
146
144
|
if (start < 0)
|
|
147
|
-
|
|
145
|
+
utilitishError('String.prototype.replaceRange', 'start cannot be negative', start, RangeError);
|
|
148
146
|
if (end < 0)
|
|
149
|
-
|
|
147
|
+
utilitishError('String.prototype.replaceRange', 'end cannot be negative', end, RangeError);
|
|
150
148
|
if (start > this.length)
|
|
151
|
-
|
|
149
|
+
utilitishError('String.prototype.replaceRange', 'start is out of bounds', start, RangeError);
|
|
152
150
|
if (end > this.length)
|
|
153
|
-
|
|
151
|
+
utilitishError('String.prototype.replaceRange', 'end is out of bounds', end, RangeError);
|
|
154
152
|
if (start > end)
|
|
155
153
|
[start, end] = [end, start];
|
|
156
154
|
return this.slice(0, start) + (replaceString ?? '') + this.slice(end);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require("../string/string-prototype");
|
|
4
|
-
const slugify_config_1 = require("../utils/slugify.config");
|
|
1
|
+
import '../string/string-prototype';
|
|
2
|
+
import { resetSlugifyConfig, setSlugifyConfig, SlugifyConfig } from '../utils/slugify.config';
|
|
5
3
|
describe('String.prototype', () => {
|
|
6
4
|
describe('capitalize()', () => {
|
|
7
5
|
it('should capitalize the first character', () => {
|
|
@@ -94,7 +92,7 @@ describe('String.prototype', () => {
|
|
|
94
92
|
});
|
|
95
93
|
describe('slugify()', () => {
|
|
96
94
|
beforeEach(() => {
|
|
97
|
-
|
|
95
|
+
resetSlugifyConfig(); // Reset to defaults before each test
|
|
98
96
|
});
|
|
99
97
|
it('should slugify a string with default config', () => {
|
|
100
98
|
expect('Hello World!'.slugify()).toBe('hello-world');
|
|
@@ -103,73 +101,73 @@ describe('String.prototype', () => {
|
|
|
103
101
|
});
|
|
104
102
|
describe('custom replacements', () => {
|
|
105
103
|
it('should apply custom replacements per call', () => {
|
|
106
|
-
expect('Test ♀'.slugify(
|
|
107
|
-
expect('User♂@domain.com'.slugify(
|
|
104
|
+
expect('Test ♀'.slugify(SlugifyConfig.builder().withCustomReplacements({ '♀': 'feminin' }).build())).toBe('test-feminin');
|
|
105
|
+
expect('User♂@domain.com'.slugify(SlugifyConfig.builder().withCustomReplacements({ '♂': 'masculin', '@': 'at' }).build())).toBe('usermasculinatdomain-com');
|
|
108
106
|
});
|
|
109
107
|
it('should handle special regex characters in replacements', () => {
|
|
110
|
-
expect('Test [bracket]'.slugify(
|
|
108
|
+
expect('Test [bracket]'.slugify(SlugifyConfig.builder().withCustomReplacements({ '[': 'left', ']': 'right' }).build())).toBe('test-leftbracketright');
|
|
111
109
|
});
|
|
112
110
|
});
|
|
113
111
|
describe('separator customization', () => {
|
|
114
112
|
it('should use custom separator globally', () => {
|
|
115
|
-
|
|
113
|
+
setSlugifyConfig(SlugifyConfig.builder().withSeparator('_').build());
|
|
116
114
|
expect('Hello World'.slugify()).toBe('hello_world');
|
|
117
115
|
expect('Test String'.slugify()).toBe('test_string');
|
|
118
116
|
});
|
|
119
117
|
it('should use custom separator per call', () => {
|
|
120
|
-
expect('Hello World'.slugify(
|
|
121
|
-
expect('Test String'.slugify(
|
|
118
|
+
expect('Hello World'.slugify(SlugifyConfig.builder().withSeparator('_').build())).toBe('hello_world');
|
|
119
|
+
expect('Test String'.slugify(SlugifyConfig.builder().withSeparator('__').build())).toBe('test__string');
|
|
122
120
|
});
|
|
123
121
|
});
|
|
124
122
|
describe('case transformation', () => {
|
|
125
123
|
it('should respect lowercase setting globally', () => {
|
|
126
|
-
|
|
124
|
+
setSlugifyConfig(SlugifyConfig.builder().withCase('default').build());
|
|
127
125
|
expect('Hello World'.slugify()).toBe('Hello-World');
|
|
128
126
|
});
|
|
129
127
|
it('should respect lowercase setting per call', () => {
|
|
130
|
-
expect('Hello World'.slugify(
|
|
128
|
+
expect('Hello World'.slugify(SlugifyConfig.builder().withCase('default').build())).toBe('Hello-World');
|
|
131
129
|
});
|
|
132
130
|
});
|
|
133
131
|
describe('accent removal', () => {
|
|
134
132
|
it('should respect removeAccents setting per call', () => {
|
|
135
|
-
expect('Éléphant'.slugify(
|
|
133
|
+
expect('Éléphant'.slugify(SlugifyConfig.builder().withRemoveAccents(false).build())).toBe('éléphant');
|
|
136
134
|
});
|
|
137
135
|
});
|
|
138
136
|
describe('allowed characters', () => {
|
|
139
137
|
it('should use custom allowed characters', () => {
|
|
140
|
-
expect('Hello123!@#'.slugify(
|
|
138
|
+
expect('Hello123!@#'.slugify(SlugifyConfig.builder()
|
|
141
139
|
.withAllowedChars(/[a-zA-Z0-9_]/)
|
|
142
140
|
.build())).toBe('hello123');
|
|
143
141
|
});
|
|
144
142
|
it('should throw for invalid allowedChars regex in call', () => {
|
|
145
|
-
expect(() =>
|
|
143
|
+
expect(() => SlugifyConfig.builder()
|
|
146
144
|
.withAllowedChars(/foo|bar/)
|
|
147
145
|
.build()).toThrowError(/Invalid allowedChars:/);
|
|
148
146
|
});
|
|
149
147
|
it('should throw for invalid allowedChars regex in global config', () => {
|
|
150
|
-
expect(() =>
|
|
148
|
+
expect(() => setSlugifyConfig(SlugifyConfig.builder()
|
|
151
149
|
.withAllowedChars(/foo|bar/)
|
|
152
150
|
.build())).toThrowError(/Invalid allowedChars:/);
|
|
153
151
|
});
|
|
154
152
|
});
|
|
155
153
|
describe('separator special characters', () => {
|
|
156
154
|
it('should handle regex-special separators without breaking', () => {
|
|
157
|
-
expect('a b+c'.slugify(
|
|
158
|
-
expect('a.b.c'.slugify(
|
|
155
|
+
expect('a b+c'.slugify(SlugifyConfig.builder().withSeparator('.').build())).toBe('a.b.c');
|
|
156
|
+
expect('a.b.c'.slugify(SlugifyConfig.builder().withSeparator('*').build())).toBe('a*b*c');
|
|
159
157
|
});
|
|
160
158
|
});
|
|
161
159
|
describe('max length', () => {
|
|
162
160
|
it('should truncate to max length', () => {
|
|
163
|
-
expect('very-long-string-here'.slugify(
|
|
164
|
-
expect('short'.slugify(
|
|
161
|
+
expect('very-long-string-here'.slugify(SlugifyConfig.builder().withMaxLength(10).build())).toBe('very-long');
|
|
162
|
+
expect('short'.slugify(SlugifyConfig.builder().withMaxLength(10).build())).toBe('short');
|
|
165
163
|
});
|
|
166
164
|
it('should remove trailing separator after truncation', () => {
|
|
167
|
-
expect('hello-world-test'.slugify(
|
|
165
|
+
expect('hello-world-test'.slugify(SlugifyConfig.builder().withMaxLength(8).build())).toBe('hello-wo');
|
|
168
166
|
});
|
|
169
167
|
});
|
|
170
168
|
describe('custom transformers', () => {
|
|
171
169
|
it('should apply custom transformers', () => {
|
|
172
|
-
const config =
|
|
170
|
+
const config = SlugifyConfig.builder()
|
|
173
171
|
.withTransformers([
|
|
174
172
|
(str) => str.replace(/test/g, 'example'),
|
|
175
173
|
(str) => str.toUpperCase(),
|
|
@@ -182,13 +180,13 @@ describe('String.prototype', () => {
|
|
|
182
180
|
describe('configuration merging', () => {
|
|
183
181
|
it('should merge global and local config correctly', () => {
|
|
184
182
|
// Local config should override global defaults
|
|
185
|
-
expect('Test ♀'.slugify(
|
|
183
|
+
expect('Test ♀'.slugify(SlugifyConfig.builder().withSeparator('_').withCustomReplacements({ '♀': 'female' }).build())).toBe('test_female');
|
|
186
184
|
});
|
|
187
185
|
});
|
|
188
186
|
});
|
|
189
187
|
describe('slugifyEquals()', () => {
|
|
190
188
|
beforeEach(() => {
|
|
191
|
-
|
|
189
|
+
resetSlugifyConfig(); // Reset to defaults before each test
|
|
192
190
|
});
|
|
193
191
|
it('should return true when slugified strings are equal', () => {
|
|
194
192
|
expect('Hello World'.slugifyEquals('hello-world')).toBe(true);
|
package/dist/utils/core.utils.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.utilitishError = exports.defineStaticIfNotExists = exports.defineIfNotExists = void 0;
|
|
4
|
-
exports.resolveSelector = resolveSelector;
|
|
5
|
-
exports.assertValidSelector = assertValidSelector;
|
|
6
|
-
exports.isNumberOrString = isNumberOrString;
|
|
7
1
|
/**
|
|
8
2
|
* Defines a **non-enumerable, non-writable, non-configurable** property on a prototype
|
|
9
3
|
* if it does not already exist, or if the existing property is writable or configurable.
|
|
@@ -25,7 +19,7 @@ exports.isNumberOrString = isNumberOrString;
|
|
|
25
19
|
* @remarks
|
|
26
20
|
* Use this function to safely add prototype methods without overwriting stable or built-in ones.
|
|
27
21
|
*/
|
|
28
|
-
const defineIfNotExists = (prototype, name, fn) => {
|
|
22
|
+
export const defineIfNotExists = (prototype, name, fn) => {
|
|
29
23
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
30
24
|
if (!descriptor || descriptor.writable || descriptor.configurable) {
|
|
31
25
|
Object.defineProperty(prototype, name, {
|
|
@@ -36,7 +30,6 @@ const defineIfNotExists = (prototype, name, fn) => {
|
|
|
36
30
|
});
|
|
37
31
|
}
|
|
38
32
|
};
|
|
39
|
-
exports.defineIfNotExists = defineIfNotExists;
|
|
40
33
|
/**
|
|
41
34
|
* Defines a **static** property on a constructor if it does not already exist,
|
|
42
35
|
* or if the existing property is writable or configurable.
|
|
@@ -58,7 +51,7 @@ exports.defineIfNotExists = defineIfNotExists;
|
|
|
58
51
|
* @remarks
|
|
59
52
|
* To forcefully replace an existing property, delete it first or use `Object.defineProperty` directly.
|
|
60
53
|
*/
|
|
61
|
-
const defineStaticIfNotExists = (constructor, name, fn) => {
|
|
54
|
+
export const defineStaticIfNotExists = (constructor, name, fn) => {
|
|
62
55
|
const descriptor = Object.getOwnPropertyDescriptor(constructor, name);
|
|
63
56
|
if (!descriptor || descriptor.writable || descriptor.configurable) {
|
|
64
57
|
Object.defineProperty(constructor, name, {
|
|
@@ -69,7 +62,6 @@ const defineStaticIfNotExists = (constructor, name, fn) => {
|
|
|
69
62
|
});
|
|
70
63
|
}
|
|
71
64
|
};
|
|
72
|
-
exports.defineStaticIfNotExists = defineStaticIfNotExists;
|
|
73
65
|
/**
|
|
74
66
|
* Resolves a selector parameter into a function that extracts a value from an item.
|
|
75
67
|
*
|
|
@@ -98,7 +90,7 @@ exports.defineStaticIfNotExists = defineStaticIfNotExists;
|
|
|
98
90
|
* @throws {TypeError} If the selector is not a function or string key.
|
|
99
91
|
* @throws {TypeError} If no selector is provided and no fallback is given.
|
|
100
92
|
*/
|
|
101
|
-
function resolveSelector(selector, fallback, name = 'selector') {
|
|
93
|
+
export function resolveSelector(selector, fallback, name = 'selector') {
|
|
102
94
|
assertValidSelector(selector);
|
|
103
95
|
if (typeof selector === 'function') {
|
|
104
96
|
return selector;
|
|
@@ -134,7 +126,7 @@ function resolveSelector(selector, fallback, name = 'selector') {
|
|
|
134
126
|
* @remarks
|
|
135
127
|
* Used internally by `resolveSelector` to provide clearer type safety and errors.
|
|
136
128
|
*/
|
|
137
|
-
function assertValidSelector(selector, name = 'selector') {
|
|
129
|
+
export function assertValidSelector(selector, name = 'selector') {
|
|
138
130
|
const isValid = typeof selector === 'function' || typeof selector === 'string';
|
|
139
131
|
if (selector !== undefined && !isValid) {
|
|
140
132
|
throw new TypeError(`${name} must be a function or a string key`);
|
|
@@ -151,11 +143,10 @@ function assertValidSelector(selector, name = 'selector') {
|
|
|
151
143
|
* @param {unknown} value The value to test.
|
|
152
144
|
* @returns {value is string | number} True if the value is a string or number, else false.
|
|
153
145
|
*/
|
|
154
|
-
function isNumberOrString(value) {
|
|
146
|
+
export function isNumberOrString(value) {
|
|
155
147
|
return typeof value === 'string' || typeof value === 'number';
|
|
156
148
|
}
|
|
157
|
-
const utilitishError = (method, message, received, ErrorClass = TypeError) => {
|
|
149
|
+
export const utilitishError = (method, message, received, ErrorClass = TypeError) => {
|
|
158
150
|
const receivedInfo = received !== undefined ? `, received ${ErrorClass === RangeError ? received : typeof received}` : '';
|
|
159
151
|
throw new ErrorClass(`[Utilitish] ${method}: ${message}${receivedInfo}`);
|
|
160
152
|
};
|
|
161
|
-
exports.utilitishError = utilitishError;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const core_utils_1 = require("./core.utils");
|
|
1
|
+
import { assertValidSelector, defineIfNotExists, defineStaticIfNotExists, isNumberOrString, resolveSelector, utilitishError, } from './core.utils';
|
|
4
2
|
describe('core.utils', () => {
|
|
5
3
|
describe('defineIfNotExists()', () => {
|
|
6
4
|
it('should define a new method on a prototype', () => {
|
|
7
5
|
const proto = {};
|
|
8
|
-
|
|
6
|
+
defineIfNotExists(proto, 'hello', () => 'world');
|
|
9
7
|
expect(proto.hello()).toBe('world');
|
|
10
8
|
});
|
|
11
9
|
it('should not overwrite a non-configurable non-writable property', () => {
|
|
@@ -15,7 +13,7 @@ describe('core.utils', () => {
|
|
|
15
13
|
writable: false,
|
|
16
14
|
configurable: false,
|
|
17
15
|
});
|
|
18
|
-
|
|
16
|
+
defineIfNotExists(proto, 'locked', () => 'overwritten');
|
|
19
17
|
expect(proto.locked()).toBe('original');
|
|
20
18
|
});
|
|
21
19
|
it('should overwrite a writable property', () => {
|
|
@@ -25,7 +23,7 @@ describe('core.utils', () => {
|
|
|
25
23
|
writable: true,
|
|
26
24
|
configurable: false,
|
|
27
25
|
});
|
|
28
|
-
|
|
26
|
+
defineIfNotExists(proto, 'flexible', () => 'overwritten');
|
|
29
27
|
expect(proto.flexible()).toBe('overwritten');
|
|
30
28
|
});
|
|
31
29
|
it('should overwrite a configurable property', () => {
|
|
@@ -35,19 +33,19 @@ describe('core.utils', () => {
|
|
|
35
33
|
writable: false,
|
|
36
34
|
configurable: true,
|
|
37
35
|
});
|
|
38
|
-
|
|
36
|
+
defineIfNotExists(proto, 'flexible', () => 'overwritten');
|
|
39
37
|
expect(proto.flexible()).toBe('overwritten');
|
|
40
38
|
});
|
|
41
39
|
it('should define the property as non-enumerable', () => {
|
|
42
40
|
const proto = {};
|
|
43
|
-
|
|
41
|
+
defineIfNotExists(proto, 'hidden', () => 'value');
|
|
44
42
|
expect(Object.keys(proto)).not.toContain('hidden');
|
|
45
43
|
});
|
|
46
44
|
});
|
|
47
45
|
describe('defineStaticIfNotExists()', () => {
|
|
48
46
|
it('should define a new static method on a constructor', () => {
|
|
49
47
|
const ctor = {};
|
|
50
|
-
|
|
48
|
+
defineStaticIfNotExists(ctor, 'create', () => 'created');
|
|
51
49
|
expect(ctor.create()).toBe('created');
|
|
52
50
|
});
|
|
53
51
|
it('should not overwrite a non-configurable non-writable static property', () => {
|
|
@@ -57,94 +55,94 @@ describe('core.utils', () => {
|
|
|
57
55
|
writable: false,
|
|
58
56
|
configurable: false,
|
|
59
57
|
});
|
|
60
|
-
|
|
58
|
+
defineStaticIfNotExists(ctor, 'locked', () => 'overwritten');
|
|
61
59
|
expect(ctor.locked()).toBe('original');
|
|
62
60
|
});
|
|
63
61
|
it('should define the property as non-enumerable', () => {
|
|
64
62
|
const ctor = {};
|
|
65
|
-
|
|
63
|
+
defineStaticIfNotExists(ctor, 'hidden', () => 'value');
|
|
66
64
|
expect(Object.keys(ctor)).not.toContain('hidden');
|
|
67
65
|
});
|
|
68
66
|
});
|
|
69
67
|
describe('resolveSelector()', () => {
|
|
70
68
|
describe('with function selector', () => {
|
|
71
69
|
it('should return a function that applies the selector', () => {
|
|
72
|
-
const sel =
|
|
70
|
+
const sel = resolveSelector((item) => item.x);
|
|
73
71
|
expect(sel({ x: 42 })).toBe(42);
|
|
74
72
|
});
|
|
75
73
|
});
|
|
76
74
|
describe('with string key selector', () => {
|
|
77
75
|
it('should return a function that extracts the property', () => {
|
|
78
|
-
const sel =
|
|
76
|
+
const sel = resolveSelector('name');
|
|
79
77
|
expect(sel({ name: 'Alice' })).toBe('Alice');
|
|
80
78
|
});
|
|
81
79
|
});
|
|
82
80
|
describe('with fallback', () => {
|
|
83
81
|
it('should return the fallback function when no selector is provided', () => {
|
|
84
|
-
const sel =
|
|
82
|
+
const sel = resolveSelector(undefined, (item) => item.toString());
|
|
85
83
|
expect(sel(42)).toBe('42');
|
|
86
84
|
});
|
|
87
85
|
});
|
|
88
86
|
describe('error handling', () => {
|
|
89
87
|
it('should throw TypeError when selector is invalid', () => {
|
|
90
|
-
expect(() =>
|
|
88
|
+
expect(() => resolveSelector(123)).toThrow(TypeError);
|
|
91
89
|
});
|
|
92
90
|
it('should throw TypeError when no selector and no fallback', () => {
|
|
93
|
-
expect(() =>
|
|
91
|
+
expect(() => resolveSelector(undefined, undefined)).toThrow(TypeError);
|
|
94
92
|
});
|
|
95
93
|
});
|
|
96
94
|
});
|
|
97
95
|
describe('assertValidSelector()', () => {
|
|
98
96
|
it('should not throw when selector is a function', () => {
|
|
99
|
-
expect(() =>
|
|
97
|
+
expect(() => assertValidSelector(() => { })).not.toThrow();
|
|
100
98
|
});
|
|
101
99
|
it('should not throw when selector is a string', () => {
|
|
102
|
-
expect(() =>
|
|
100
|
+
expect(() => assertValidSelector('name')).not.toThrow();
|
|
103
101
|
});
|
|
104
102
|
it('should not throw when selector is undefined', () => {
|
|
105
|
-
expect(() =>
|
|
103
|
+
expect(() => assertValidSelector(undefined)).not.toThrow();
|
|
106
104
|
});
|
|
107
105
|
describe('error handling', () => {
|
|
108
106
|
it('should throw TypeError when selector is a number', () => {
|
|
109
|
-
expect(() =>
|
|
107
|
+
expect(() => assertValidSelector(123)).toThrow(TypeError);
|
|
110
108
|
});
|
|
111
109
|
it('should throw TypeError when selector is an object', () => {
|
|
112
|
-
expect(() =>
|
|
110
|
+
expect(() => assertValidSelector({})).toThrow(TypeError);
|
|
113
111
|
});
|
|
114
112
|
});
|
|
115
113
|
});
|
|
116
114
|
describe('isNumberOrString()', () => {
|
|
117
115
|
it('should return true for a string', () => {
|
|
118
|
-
expect(
|
|
116
|
+
expect(isNumberOrString('hello')).toBe(true);
|
|
119
117
|
});
|
|
120
118
|
it('should return true for a number', () => {
|
|
121
|
-
expect(
|
|
119
|
+
expect(isNumberOrString(42)).toBe(true);
|
|
122
120
|
});
|
|
123
121
|
it('should return false for a boolean', () => {
|
|
124
|
-
expect(
|
|
122
|
+
expect(isNumberOrString(true)).toBe(false);
|
|
125
123
|
});
|
|
126
124
|
it('should return false for null', () => {
|
|
127
|
-
expect(
|
|
125
|
+
expect(isNumberOrString(null)).toBe(false);
|
|
128
126
|
});
|
|
129
127
|
it('should return false for an object', () => {
|
|
130
|
-
expect(
|
|
128
|
+
expect(isNumberOrString({})).toBe(false);
|
|
131
129
|
});
|
|
132
130
|
});
|
|
133
131
|
describe('utilitishError()', () => {
|
|
134
132
|
it('should throw a TypeError by default', () => {
|
|
135
|
-
expect(() =>
|
|
133
|
+
expect(() => utilitishError('method', 'message')).toThrow(TypeError);
|
|
136
134
|
});
|
|
137
135
|
it('should throw a RangeError when specified', () => {
|
|
138
|
-
expect(() =>
|
|
136
|
+
expect(() => utilitishError('method', 'message', undefined, RangeError)).toThrow(RangeError);
|
|
139
137
|
});
|
|
140
138
|
it('should include method and message in the error message', () => {
|
|
141
|
-
expect(() =>
|
|
139
|
+
expect(() => utilitishError('String.prototype.test', 'value must be a string')).toThrow('[Utilitish] String.prototype.test: value must be a string');
|
|
142
140
|
});
|
|
143
141
|
it('should include received type in the error message when provided', () => {
|
|
144
|
-
expect(() =>
|
|
142
|
+
expect(() => utilitishError('String.prototype.test', 'value must be a string', 42)).toThrow('[Utilitish] String.prototype.test: value must be a string, received number');
|
|
145
143
|
});
|
|
146
144
|
it('should not include received info when received is undefined', () => {
|
|
147
|
-
expect(() =>
|
|
145
|
+
expect(() => utilitishError('String.prototype.test', 'value must be a string', undefined)).toThrow('[Utilitish] String.prototype.test: value must be a string');
|
|
148
146
|
});
|
|
149
147
|
});
|
|
150
148
|
});
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.sortBy = sortBy;
|
|
4
|
-
const core_utils_1 = require("./core.utils");
|
|
5
|
-
function sortBy(arr, direction, selector) {
|
|
1
|
+
import { isNumberOrString, resolveSelector } from './core.utils';
|
|
2
|
+
export function sortBy(arr, direction, selector) {
|
|
6
3
|
if (arr.length === 0)
|
|
7
4
|
return arr.slice();
|
|
8
|
-
const getValue =
|
|
5
|
+
const getValue = resolveSelector(selector, (item) => item);
|
|
9
6
|
const sample = getValue(arr[0]);
|
|
10
|
-
if (!
|
|
7
|
+
if (!isNumberOrString(sample)) {
|
|
11
8
|
throw new TypeError('Selector must return number or string');
|
|
12
9
|
}
|
|
13
|
-
if (!selector && !arr.every((item) =>
|
|
10
|
+
if (!selector && !arr.every((item) => isNumberOrString(item))) {
|
|
14
11
|
throw new TypeError('Array elements must be number or string if no selector is provided');
|
|
15
12
|
}
|
|
16
13
|
return arr.slice().sort((a, b) => {
|
|
17
14
|
const valA = getValue(a);
|
|
18
15
|
const valB = getValue(b);
|
|
19
|
-
if (!
|
|
16
|
+
if (!isNumberOrString(valA) || !isNumberOrString(valB)) {
|
|
20
17
|
throw new TypeError('Selector must return number or string');
|
|
21
18
|
}
|
|
22
19
|
if (valA > valB)
|