@oscarpalmer/atoms 0.180.0 → 0.182.0

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.
Files changed (46) hide show
  1. package/dist/array/find.d.mts +42 -1
  2. package/dist/array/find.mjs +5 -1
  3. package/dist/array/get.mjs +2 -2
  4. package/dist/array/index.d.mts +3 -3
  5. package/dist/array/index.mjs +3 -3
  6. package/dist/index.d.mts +238 -25
  7. package/dist/index.mjs +242 -19
  8. package/dist/internal/array/find.d.mts +4 -3
  9. package/dist/internal/array/find.mjs +5 -2
  10. package/dist/internal/array/index-of.d.mts +42 -1
  11. package/dist/internal/array/index-of.mjs +5 -1
  12. package/dist/internal/math/aggregate.mjs +2 -2
  13. package/dist/internal/result.mjs +2 -2
  14. package/dist/internal/string.d.mts +5 -3
  15. package/dist/internal/string.mjs +14 -5
  16. package/dist/internal/value/equal.mjs +2 -2
  17. package/dist/internal/value/handlers.mjs +2 -2
  18. package/dist/kalas.d.mts +61 -0
  19. package/dist/kalas.mjs +93 -0
  20. package/dist/string/case.mjs +10 -4
  21. package/dist/string/normalize.d.mts +55 -0
  22. package/dist/string/normalize.mjs +93 -0
  23. package/dist/value/index.d.mts +2 -1
  24. package/dist/value/index.mjs +2 -1
  25. package/dist/value/merge.d.mts +13 -0
  26. package/dist/value/merge.mjs +3 -1
  27. package/dist/value/shake.d.mts +7 -0
  28. package/dist/value/shake.mjs +16 -0
  29. package/package.json +1 -1
  30. package/src/array/exists.ts +1 -1
  31. package/src/array/find.ts +58 -0
  32. package/src/array/get.ts +2 -2
  33. package/src/index.ts +3 -0
  34. package/src/internal/array/find.ts +24 -4
  35. package/src/internal/array/index-of.ts +59 -1
  36. package/src/internal/math/aggregate.ts +2 -2
  37. package/src/internal/result.ts +6 -3
  38. package/src/internal/string.ts +26 -8
  39. package/src/internal/value/equal.ts +2 -2
  40. package/src/internal/value/handlers.ts +2 -2
  41. package/src/kalas.ts +167 -0
  42. package/src/string/case.ts +20 -4
  43. package/src/string/normalize.ts +169 -0
  44. package/src/value/index.ts +1 -0
  45. package/src/value/merge.ts +17 -1
  46. package/src/value/shake.ts +36 -0
@@ -1,5 +1,5 @@
1
1
  import type {NumericalValues, PlainObject} from '../../models';
2
- import {isNumber} from '../is';
2
+ import {isNonNumber} from '../is';
3
3
 
4
4
  // #region Types
5
5
 
@@ -40,7 +40,7 @@ export function aggregate(type: AggregationType, array: unknown[], key: unknown)
40
40
 
41
41
  const value = callback == null ? item : callback(item as never, index, array);
42
42
 
43
- if (!isNumber(value)) {
43
+ if (isNonNumber(value)) {
44
44
  continue;
45
45
  }
46
46
 
@@ -1,14 +1,17 @@
1
+ import type {PlainObject} from '../models';
1
2
  import type {Err, ExtendedErr, Ok, Result} from '../result/models';
2
- import {isPlainObject} from './is';
3
+ import {isNonPlainObject} from './is';
3
4
 
4
5
  // #region Functions
5
6
 
6
7
  function _isResult(value: unknown, okValue: boolean): value is Result<unknown, unknown> {
7
- if (!isPlainObject(value)) {
8
+ if (isNonPlainObject(value)) {
8
9
  return false;
9
10
  }
10
11
 
11
- return value.ok === okValue && (okValue ? PROPERTY_VALUE : PROPERTY_ERROR) in value;
12
+ return (
13
+ (value as PlainObject).ok === okValue && (okValue ? PROPERTY_VALUE : PROPERTY_ERROR) in value
14
+ );
12
15
  }
13
16
 
14
17
  /**
@@ -1,5 +1,3 @@
1
- import {compact} from './array/compact';
2
-
3
1
  // #region Functions
4
2
 
5
3
  /**
@@ -36,15 +34,35 @@ export function ignoreKey(key: string): boolean {
36
34
  }
37
35
 
38
36
  /**
39
- * Join an array of values into a string
40
- * @param value Array of values
37
+ * Join an array of values into a string _(while ignoring empty values)_
38
+ *
39
+ * _(`null`, `undefined`, and any values that become whitespace-only strings are considered empty)_
40
+ * @param array Array of values
41
41
  * @param delimiter Delimiter to use between values
42
42
  * @returns Joined string
43
43
  */
44
- export function join(value: unknown[], delimiter?: string): string {
45
- return compact(value)
46
- .map(getString)
47
- .join(typeof delimiter === 'string' ? delimiter : '');
44
+ export function join(array: unknown[], delimiter?: string): string {
45
+ if (!Array.isArray(array)) {
46
+ return '';
47
+ }
48
+
49
+ const {length} = array;
50
+
51
+ if (length === 0) {
52
+ return '';
53
+ }
54
+
55
+ const values: string[] = [];
56
+
57
+ for (let index = 0; index < length; index += 1) {
58
+ const item = getString(array[index]);
59
+
60
+ if (item.trim().length > 0) {
61
+ values.push(item);
62
+ }
63
+ }
64
+
65
+ return values.join(typeof delimiter === 'string' ? delimiter : '');
48
66
  }
49
67
 
50
68
  function tryCallback<T, U>(value: T, callback: (value: T) => U): U {
@@ -1,5 +1,5 @@
1
1
  import type {ArrayOrPlainObject, Constructor, PlainObject, TypedArray} from '../../models';
2
- import {isPlainObject, isPrimitive, isTypedArray} from '../is';
2
+ import {isNonPlainObject, isPlainObject, isPrimitive, isTypedArray} from '../is';
3
3
  import {getCompareHandlers} from './handlers';
4
4
 
5
5
  // #region Types
@@ -377,7 +377,7 @@ function getEqualOptions(input?: boolean | EqualOptions): Options {
377
377
  return options;
378
378
  }
379
379
 
380
- if (!isPlainObject(input)) {
380
+ if (isNonPlainObject(input)) {
381
381
  return options;
382
382
  }
383
383
 
@@ -1,5 +1,5 @@
1
1
  import type {Constructor, GenericCallback} from '../../models';
2
- import {isConstructor} from '../is';
2
+ import {isNonConstructor} from '../is';
3
3
 
4
4
  type Options = {
5
5
  callback: GenericCallback;
@@ -47,7 +47,7 @@ function getHandlers(owner: GenericCallback, options: Options) {
47
47
  }
48
48
  },
49
49
  register(constructor: Constructor, handler?: string | GenericCallback) {
50
- if (!isConstructor(constructor) || handler === owner) {
50
+ if (isNonConstructor(constructor) || handler === owner) {
51
51
  return;
52
52
  }
53
53
 
package/src/kalas.ts ADDED
@@ -0,0 +1,167 @@
1
+ import {noop} from './internal/function/misc';
2
+ import type {GenericCallback} from './models';
3
+
4
+ // #region Types
5
+
6
+ class Events<Map extends Record<string, GenericCallback>> {
7
+ readonly #kalas: Kalas<Map>;
8
+
9
+ constructor(kalas: Kalas<Map>) {
10
+ this.#kalas = kalas;
11
+ }
12
+
13
+ /**
14
+ * Subscribe to an event with a callback
15
+ * @param event Event name
16
+ * @param callback Callback function
17
+ * @returns Unsubscriber function
18
+ */
19
+ subscribe<Event extends keyof Map>(event: Event, callback: Map[Event]): Unsubscriber {
20
+ return this.#kalas.subscribe(event, callback);
21
+ }
22
+
23
+ /**
24
+ * Unsubscribe from an event with a callback _(or all callbacks, if no callback is provided)_
25
+ * @param event Event name
26
+ * @param callback Callback function
27
+ * @returns Unsubscriber function
28
+ */
29
+ unsubscribe<Event extends keyof Map>(event: Event, callback?: Map[Event]): void {
30
+ return this.#kalas.unsubscribe(event, callback);
31
+ }
32
+ }
33
+
34
+ class Kalas<Map extends Record<string, GenericCallback>> {
35
+ readonly #names: Set<keyof Map>;
36
+
37
+ readonly #subscribers = new Map<keyof Map, Set<Map[keyof Map]>>();
38
+
39
+ /**
40
+ * Events interface for subscribing and unsubscribing to events
41
+ */
42
+ declare readonly events: Events<Map>;
43
+
44
+ constructor(names: (keyof Map)[]) {
45
+ this.#names = new Set(names);
46
+
47
+ Object.defineProperty(this, 'events', {
48
+ value: new Events(this),
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Remove all event subscribers
54
+ */
55
+ clear(): void {
56
+ this.#subscribers.clear();
57
+ }
58
+
59
+ /**
60
+ * Emit an event with parameters
61
+ * @param event Event name
62
+ * @param parameters Event parameters
63
+ */
64
+ emit<Event extends keyof Map>(event: Event, ...parameters: Parameters<Map[Event]>) {
65
+ const subscribers = this.#subscribers.get(event);
66
+
67
+ if (subscribers == null) {
68
+ return;
69
+ }
70
+
71
+ for (const callback of subscribers) {
72
+ callback(...parameters);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Subscribe to an event with a callback
78
+ * @param event Event name
79
+ * @param callback Callback function
80
+ * @returns Unsubscriber function
81
+ */
82
+ subscribe<Event extends keyof Map>(event: Event, callback: Map[Event]): Unsubscriber {
83
+ if (!this.#names.has(event) || typeof callback !== 'function') {
84
+ return noop;
85
+ }
86
+
87
+ let subscribers = this.#subscribers.get(event);
88
+
89
+ if (subscribers == null) {
90
+ subscribers = new Set();
91
+
92
+ this.#subscribers.set(event, subscribers);
93
+ }
94
+
95
+ subscribers.add(callback);
96
+
97
+ return () => {
98
+ subscribers?.delete(callback);
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Unsubscribe from an event with a callback _(or all callbacks, if no callback is provided)_
104
+ * @param event Event name
105
+ * @param callback Callback function
106
+ */
107
+ unsubscribe<Event extends keyof Map>(event: Event, callback?: Map[Event]): void {
108
+ if (!this.#names.has(event) || (callback != null ? typeof callback !== 'function' : false)) {
109
+ return;
110
+ }
111
+
112
+ const subscribers = this.#subscribers.get(event);
113
+
114
+ if (subscribers == null) {
115
+ return;
116
+ }
117
+
118
+ if (callback == null) {
119
+ subscribers.clear();
120
+ } else {
121
+ subscribers.delete(callback);
122
+ }
123
+
124
+ if (callback == null || subscribers.size === 0) {
125
+ this.#subscribers.delete(event);
126
+ }
127
+ }
128
+ }
129
+
130
+ export type Unsubscriber = () => void;
131
+
132
+ // #endregion
133
+
134
+ // #region Functions
135
+
136
+ /**
137
+ * Create a Kalas _(party)_ for named events
138
+ * @param names Event names
139
+ * @returns Kalas instance
140
+ */
141
+ export function kalas<Events extends Record<string, GenericCallback>>(
142
+ names: (keyof Events)[],
143
+ ): Kalas<Events> {
144
+ if (
145
+ !Array.isArray(names) ||
146
+ names.length === 0 ||
147
+ !names.every(name => typeof name === 'string')
148
+ ) {
149
+ throw new Error(MESSAGE);
150
+ }
151
+
152
+ return new Kalas<Events>(names);
153
+ }
154
+
155
+ // #endregion
156
+
157
+ // #region Variables
158
+
159
+ const MESSAGE = 'Kalas requires an array of event names.';
160
+
161
+ // #endregion
162
+
163
+ // #region Exports
164
+
165
+ export {type Events, type Kalas};
166
+
167
+ // #endregion
@@ -58,7 +58,13 @@ export function kebabCase(value: string): string {
58
58
  * @returns Lower-cased string
59
59
  */
60
60
  export function lowerCase(value: string): string {
61
- return typeof value === 'string' ? value.toLocaleLowerCase() : '';
61
+ if (typeof value !== 'string') {
62
+ return '';
63
+ }
64
+
65
+ memoizedLowerCase ??= memoize(v => v.toLocaleLowerCase());
66
+
67
+ return memoizedLowerCase.run(value);
62
68
  }
63
69
 
64
70
  /**
@@ -85,12 +91,12 @@ export function snakeCase(value: string): string {
85
91
  * @returns Title-cased string
86
92
  */
87
93
  export function titleCase(value: string): string {
88
- if (typeof value !== 'string') {
94
+ if (typeof value !== 'string' || value.length === 0) {
89
95
  return '';
90
96
  }
91
97
 
92
98
  memoizedTitleCase ??= memoize(v =>
93
- v.length < 1 ? capitalize(v) : join(words(v).map(capitalize), ' '),
99
+ v.length < 2 ? capitalize(v) : join(words(v).map(capitalize), ' '),
94
100
  );
95
101
 
96
102
  return memoizedTitleCase.run(value);
@@ -167,7 +173,13 @@ function toCaseCallback(this: Options, value: string): string {
167
173
  * @returns Upper-cased string
168
174
  */
169
175
  export function upperCase(value: string): string {
170
- return typeof value === 'string' ? value.toLocaleUpperCase() : '';
176
+ if (typeof value !== 'string' || value.length === 0) {
177
+ return '';
178
+ }
179
+
180
+ memoizedUpperCase ??= memoize(v => v.toLocaleUpperCase());
181
+
182
+ return memoizedUpperCase.run(value);
171
183
  }
172
184
 
173
185
  // #endregion
@@ -207,6 +219,10 @@ const delimiters: Record<Case, string> = {
207
219
 
208
220
  let memoizedCapitalize: Memoized<(value: string) => string>;
209
221
 
222
+ let memoizedLowerCase: Memoized<(value: string) => string>;
223
+
210
224
  let memoizedTitleCase: Memoized<(value: string) => string>;
211
225
 
226
+ let memoizedUpperCase: Memoized<(value: string) => string>;
227
+
212
228
  // #endregion
@@ -0,0 +1,169 @@
1
+ import {memoize, type Memoized} from '../function/memoize';
2
+ import {isPlainObject} from '../internal/is';
3
+ import {lowerCase} from './case';
4
+
5
+ // #region Types
6
+
7
+ /**
8
+ * Options for normalizing a string
9
+ */
10
+ export type NormalizeOptions = {
11
+ /**
12
+ * Remove diacritical marks from the string? _(defaults to `true`)_
13
+ */
14
+ deburr?: boolean;
15
+ /**
16
+ * Convert the string to lower case? _(defaults to `true`)_
17
+ */
18
+ lowerCase?: boolean;
19
+ /**
20
+ * Trim the string? _(defaults to `true`)_
21
+ */
22
+ trim?: boolean;
23
+ };
24
+
25
+ /**
26
+ * String normalizer function
27
+ */
28
+ export type Normalizer = {
29
+ /**
30
+ * Normalize a string
31
+ * @param value String to normalize
32
+ * @returns Normalized string
33
+ */
34
+ (value: string): string;
35
+ };
36
+
37
+ type Options = Required<NormalizeOptions>;
38
+
39
+ // #endregion
40
+
41
+ // #region Functions
42
+
43
+ /**
44
+ * Deburr a string, removing diacritical marks
45
+ * @param value String to deburr
46
+ * @returns Deburred string
47
+ */
48
+ export function deburr(value: string): string {
49
+ if (typeof value !== 'string') {
50
+ return '';
51
+ }
52
+
53
+ deburrMemoizer ??= memoize(value => {
54
+ let deburred = value.normalize(DEBURR_NORMALIZATION).replace(DEBURR_PATTERN_SIMPLE, '');
55
+
56
+ deburred = deburred.replace(
57
+ DEBURR_PATTERN_CHARACTERS,
58
+ (_, character) => DEBURR_CHARACTERS[character as keyof typeof DEBURR_CHARACTERS],
59
+ );
60
+
61
+ return deburred;
62
+ });
63
+
64
+ return deburrMemoizer.run(value);
65
+ }
66
+
67
+ function getNormalizeOptions(input?: NormalizeOptions): Options {
68
+ const options = isPlainObject(input) ? input : {};
69
+
70
+ return {
71
+ deburr: options.deburr !== false,
72
+ lowerCase: options.lowerCase !== false,
73
+ trim: options.trim !== false,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Initialize a string normalizer
79
+ * @param options Normalization options
80
+ * @returns Normalizer function
81
+ */
82
+ export function initializeNormalizer(options?: NormalizeOptions): Normalizer {
83
+ const normalization = getNormalizeOptions(options);
84
+
85
+ return (value: string) => normalizeString(value, normalization);
86
+ }
87
+
88
+ /**
89
+ * Normalize a string
90
+ *
91
+ * By default, the string will be trimmed, deburred, and then lowercased
92
+ * @param value String to normalize
93
+ * @param options Normalization options
94
+ * @returns Normalized string
95
+ */
96
+ export function normalize(value: string, options?: NormalizeOptions): string {
97
+ return normalizeString(value, getNormalizeOptions(options));
98
+ }
99
+
100
+ normalize.initialize = initializeNormalizer;
101
+
102
+ function normalizeString(value: string, options: Options): string {
103
+ if (typeof value !== 'string') {
104
+ return '';
105
+ }
106
+
107
+ let result = value;
108
+
109
+ if (options.trim) {
110
+ result = result.trim();
111
+ }
112
+
113
+ if (options.deburr) {
114
+ result = deburr(result);
115
+ }
116
+
117
+ if (options.lowerCase) {
118
+ result = lowerCase(result);
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ // #endregion
125
+
126
+ // #region Variables
127
+
128
+ const DEBURR_CHARACTERS = {
129
+ Æ: 'AE',
130
+ æ: 'ae',
131
+ Ð: 'D',
132
+ ð: 'd',
133
+ Đ: 'D',
134
+ đ: 'd',
135
+ Ħ: 'H',
136
+ ħ: 'h',
137
+ IJ: 'IJ',
138
+ ij: 'ij',
139
+ İ: 'I',
140
+ ı: 'i',
141
+ ĸ: 'k',
142
+ Ŀ: 'L',
143
+ ŀ: 'l',
144
+ Ł: 'L',
145
+ ł: 'l',
146
+ Ŋ: 'N',
147
+ ŋ: 'n',
148
+ ʼn: "'n",
149
+ Œ: 'OE',
150
+ œ: 'oe',
151
+ Ø: 'O',
152
+ ø: 'o',
153
+ ſ: 's',
154
+ ß: 'ss',
155
+ Þ: 'TH',
156
+ þ: 'th',
157
+ Ŧ: 'T',
158
+ ŧ: 't',
159
+ };
160
+
161
+ const DEBURR_NORMALIZATION = 'NFD';
162
+
163
+ const DEBURR_PATTERN_CHARACTERS = new RegExp(`(${Object.keys(DEBURR_CHARACTERS).join('|')})`, 'g');
164
+
165
+ const DEBURR_PATTERN_SIMPLE = /[\u0300-\u036f]/g;
166
+
167
+ let deburrMemoizer: Memoized<typeof deburr>;
168
+
169
+ // #endregion
@@ -1,4 +1,5 @@
1
1
  export {omit} from './omit';
2
2
  export {pick} from './pick';
3
+ export {shake, type Shaken} from './shake';
3
4
  export {smush, type Smushed} from './smush';
4
5
  export {unsmush, type Unsmushed} from './unsmush';
@@ -8,16 +8,29 @@ import type {ArrayOrPlainObject, NestedPartial, PlainObject} from '../models';
8
8
  * Options for merging values
9
9
  */
10
10
  export type MergeOptions = {
11
+ /**
12
+ * Assign values to the first array or object instead of creating a new one?
13
+ */
14
+ assignValues?: boolean;
11
15
  /**
12
16
  * Key _(or key epxressions)_ for values that should be replaced
17
+ *
13
18
  * ```ts
14
19
  * merge([{items: [1, 2, 3]}, {items: [99]}]); // {items: [99]}
15
20
  * ```
16
21
  */
17
22
  replaceableObjects?: string | RegExp | Array<string | RegExp>;
23
+ /**
24
+ * Skip nullable values when merging objects?
25
+ *
26
+ * ```ts
27
+ * merge({a: 1, b: 2}, {b: null, c: 3}, {d: null}); // {a: 1, b: 2, c: 3}
28
+ * ```
29
+ */
18
30
  skipNullableAny?: boolean;
19
31
  /**
20
32
  * Skip nullable values when merging arrays?
33
+ *
21
34
  * ```ts
22
35
  * merge([1, 2, 3], [null, null, 99]); // [1, 2, 99]
23
36
  * ```
@@ -35,6 +48,7 @@ export type Merger<Model extends ArrayOrPlainObject = ArrayOrPlainObject> = (
35
48
  ) => Model;
36
49
 
37
50
  type Options = {
51
+ assignValues: boolean;
38
52
  replaceableObjects: ReplaceableObjectsCallback | undefined;
39
53
  skipNullableAny: boolean;
40
54
  skipNullableInArrays: boolean;
@@ -48,6 +62,7 @@ type ReplaceableObjectsCallback = (name: string) => boolean;
48
62
 
49
63
  function getMergeOptions(options?: MergeOptions): Options {
50
64
  const actual: Options = {
65
+ assignValues: false,
51
66
  replaceableObjects: undefined,
52
67
  skipNullableAny: false,
53
68
  skipNullableInArrays: false,
@@ -59,6 +74,7 @@ function getMergeOptions(options?: MergeOptions): Options {
59
74
 
60
75
  actual.replaceableObjects = getReplaceableObjects(options.replaceableObjects);
61
76
 
77
+ actual.assignValues = options.assignValues === true;
62
78
  actual.skipNullableAny = options.skipNullableAny === true;
63
79
  actual.skipNullableInArrays = options.skipNullableInArrays === true;
64
80
 
@@ -132,7 +148,7 @@ function mergeObjects(
132
148
  ): ArrayOrPlainObject {
133
149
  const {length} = values;
134
150
  const isArray = values.every(Array.isArray);
135
- const merged = (isArray ? [] : {}) as PlainObject;
151
+ const merged = (options.assignValues ? values[0] : isArray ? [] : {}) as PlainObject;
136
152
 
137
153
  for (let outerIndex = 0; outerIndex < length; outerIndex += 1) {
138
154
  const item = values[outerIndex] as PlainObject;
@@ -0,0 +1,36 @@
1
+ import {isNonPlainObject} from '../is';
2
+ import type {PlainObject} from '../models';
3
+
4
+ // #region Types
5
+
6
+ export type Shaken<Value extends PlainObject> = {
7
+ [Key in keyof Value]: Value[Key] extends undefined ? never : Value[Key];
8
+ };
9
+
10
+ // #endregion
11
+
12
+ // #region Functions
13
+
14
+ export function shake<Value extends PlainObject>(value: Value): Shaken<Value> {
15
+ const shaken: PlainObject = {};
16
+
17
+ if (isNonPlainObject(value)) {
18
+ return shaken as Shaken<Value>;
19
+ }
20
+
21
+ const keys = Object.keys(value) as (keyof Value)[];
22
+ const {length} = keys;
23
+
24
+ for (let index = 0; index < length; index += 1) {
25
+ const key = keys[index];
26
+ const val = value[key];
27
+
28
+ if (val !== undefined) {
29
+ shaken[key] = val;
30
+ }
31
+ }
32
+
33
+ return shaken as Shaken<Value>;
34
+ }
35
+
36
+ // #endregion