some-common-functions-js 1.2.0 → 2.0.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.
package/index.ts ADDED
@@ -0,0 +1,733 @@
1
+ /** File: ./backend/utilityFunctions/commonFunctions.js
2
+ * Description: Common functions to be put here.
3
+ * Start Date End Date Dev Version Description
4
+ * 2025/11/19 ITA 1.00 Genesis.
5
+ * 2025/11/28 ITA 1.01 Added function hasOnlyAll().
6
+ * 2025/12/22 ITA 1.02 Improved documentation of the functions and moved in more functions.
7
+ * 2025/12/26 ITA 1.03 Removed lodash dependency by re-implementing get() and set() object functions, significantly reducing this package size.
8
+ * Added function getNextDifferent() to deal better with duplicate removal from arrays of objects.
9
+ * 2025/12/27 ITA 1.04 Improved the functions getNoDuplicatesArray() and getNextDifferent() to handle more test cases.
10
+ * Added function unset().
11
+ * 2025/12/28 ITA 1.05 Improved documentation of functions to show better on the tooltip in IDEs.
12
+ * Improved deepClone() function to handle Date objects and arrays.
13
+ * Updated get() function to return undefined or supplied default value for paths that do not exist.
14
+ * Updated test.js file accordingly.
15
+ * 2025/12/29 ITA 1.06 Removed unnecessary use of the getPaths() function in the get() function to improve effieciency.
16
+ * 2026/01/01 ITA 1.07 Changed recursive functions to iterative functions, so as to overcome stack limits when functions are used in the front-end (browsers).
17
+ * 2026/01/01 ITA 1.08 Changed an additional recursive function to an iterative function.
18
+ * 2026/01/03 ITA 1.09 deepClone(): Used object spread to prevent reference sharing during object assignment.
19
+ * unset(): Replaced falsy-value evaluation with the `in` operator to correctly detect existing fields.
20
+ * 2026/01/10 2026/10/10 ITA 1.10 get() and unset() functions: For field existence check, replaced the use of 'in' operator with checking whether the field value of the object is undefined.
21
+ * This prevents crashes resulting from using 'in' operator on undefined fields and objects.
22
+ * 2026/01/10 2026/10/10 ITA 1.11 Added more robustness in dealing with non-existent fields in get() and unset() functions.
23
+ * 2026/05/07 2026/05/08 ITA 1.12 Migrated to Typescript.
24
+ * Added a robust functionality for verifying whether a variable is a plain Typescript/Javacript object.
25
+
26
+ */
27
+
28
+ /**Return true if userName is valid
29
+ * @param {string} userName
30
+ * @returns {boolean} true if a userName is valid, otherwise false.
31
+ */
32
+ export function isValidUserName(userName: string): boolean {
33
+ if (!userName) // The username must be provided.
34
+ return false;
35
+
36
+ const regEx = /^[a-zA-Z][a-zA-Z0-9_-]{2,50}$/;
37
+ return regEx.test(userName);
38
+ }
39
+
40
+ /**Return true if a name is valid
41
+ * @param {string} name
42
+ * @returns {boolean} true if a name is valid, otherwise false.
43
+ */
44
+ function isValidName(name: string): boolean {
45
+ if (!name) // The name must be provided.
46
+ return false;
47
+
48
+ const regEx = /^[A-Za-z' -]{2,50}$/;
49
+ return regEx.test(name);
50
+ }
51
+ export { isValidName };
52
+
53
+ /**Return true if userName is valid
54
+ * @param {string} email
55
+ * @returns {boolean} true if an email is valid, otherwise false.
56
+ */
57
+ function isValidEmail(email: string): boolean {
58
+ if (!email)
59
+ return false;
60
+
61
+ const regEx = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
62
+ return regEx.test(email);
63
+ }
64
+ export { isValidEmail };
65
+
66
+ /**Return true if userName is valid
67
+ * @param {string} num phone number
68
+ * @returns {boolean} true if phone number is valid, otherwise false.
69
+ */
70
+ function isValidPhoneNum(num: string): boolean {
71
+ if (!num) // The number must be provided.
72
+ return false;
73
+
74
+ const regEx = /^[0-9]{10,15}$/g;
75
+ return regEx.test(num);
76
+ }
77
+ export { isValidPhoneNum };
78
+
79
+ /**Return true if the name of an organisation is valid
80
+ * @param {string} name an organisation name
81
+ * @returns {boolean} true if an organisation name is valid, otherwise false.
82
+ */
83
+ function isValidOrganisationName(name: string): boolean {
84
+ if (!name) // The name must be provided.
85
+ return false;
86
+
87
+ const regEx = /^[a-zA-Z0-9.\-\(\) ]{2,}$/;
88
+ return regEx.test(name);
89
+ }
90
+ export { isValidOrganisationName };
91
+
92
+ /**Return true if a password is valid
93
+ * @param {string} password
94
+ * @returns {boolean} true if a password is valid, otherwise false.
95
+ */
96
+ function isValidPassword(password: string): boolean {
97
+ if (!password)
98
+ return false;
99
+
100
+ // Password must be at least 6 characters long
101
+ if (password.length < 6)
102
+ return false;
103
+
104
+ // Must contain at least 1 uppercase letter
105
+ if (/[A-Z]/.test(password) === false)
106
+ return false;
107
+
108
+ // Must contain at least 1 lowercase letter
109
+ if (/[a-z]/.test(password) === false)
110
+ return false;
111
+
112
+ // Must contain at least 1 number
113
+ if (/[0-9]/.test(password) === false)
114
+ return false;
115
+
116
+ // Must not contain white space characters
117
+ if (/[\s]/.test(password))
118
+ return false;
119
+
120
+ // Must contain atleast 1 symbol
121
+ if (/[\][!"#$%&'()*+,./:;<=>?@^\\_`{|}~-]/.test(password) === false)
122
+ return false;
123
+
124
+ return true;
125
+ }
126
+ export { isValidPassword };
127
+
128
+ /** Converts the date object to a string of the form CCYY-MM-DD
129
+ * @param {Date} dateObj
130
+ * @returns {string} string of the form CCYY-MM-DD
131
+ */
132
+ function timeStampYyyyMmDd(dateObj: Date): string {
133
+ // Convert the date to string form yyyy-mm-dd
134
+ let year = dateObj.getFullYear().toString();
135
+ let month = (dateObj.getMonth() + 1).toString();
136
+ month = addLeadingZeros(month, 2);
137
+ let day = dateObj.getDate().toString();
138
+ day = addLeadingZeros(day, 2);
139
+ return `${year}-${month}-${day}`;
140
+ } // function timeStampYYYYMMDd(dateObj) {
141
+ export { timeStampYyyyMmDd };
142
+
143
+ /** Converts a date object to a string of the form CCYY-MM-DDThh:mm:ss.ccc, e.g. '2024-02-25T15:00:25.251'
144
+ * @param {Date} dateObj
145
+ * @returns {string} a string of the form CCYY-MM-DDThh:mm:ss.ccc.
146
+ */
147
+ function timeStampString(dateObj: Date): string {
148
+ let hours = addLeadingZeros(dateObj.getHours().toString(), 2);
149
+ let minutes = addLeadingZeros(dateObj.getMinutes().toString(), 2);
150
+ let seconds = addLeadingZeros(dateObj.getSeconds().toString(), 2);
151
+ let milliSec = addLeadingZeros(dateObj.getMilliseconds().toString(), 3);
152
+ return `${timeStampYyyyMmDd(dateObj)}T${hours}:${minutes}:${seconds}.${milliSec}`;
153
+ } // function timeStampString(dateObj) {
154
+ export { timeStampString };
155
+
156
+ /** Returns a numeric string with trailing zeros.
157
+ * E.g.
158
+ * addLeadingZeros(9, 4) = '0009', addLeadingZeros(123, 5) = '00123'
159
+ * @param {string | number} aNumber a numerical string or number.
160
+ * @param {number} newLength the new length of the resulting string.
161
+ * @returns a string of a number with the specified number of leading zeros.
162
+ */
163
+ function addLeadingZeros(aNumber: string | number, newLength: number): string {
164
+
165
+ let newString = aNumber + '';
166
+ const howManyZeros = newLength - newString.length;
167
+ for (let count = 1; count <= howManyZeros; count++)
168
+ newString = '0' + newString;
169
+
170
+ return newString;
171
+ } // function addLeadingZeros(aString, newLength) {
172
+ export { addLeadingZeros };
173
+
174
+ /**Convert numeric input to ZAR currency format string.
175
+ * @param {number | string} aNumber a number or numeric string.
176
+ * @returns a string of the form R 256,534.00
177
+ */
178
+ function toZarCurrencyFormat(aNumber: number | string): string {
179
+ if (typeof aNumber === 'string') {
180
+ aNumber = parseFloat(aNumber);
181
+ }
182
+ const zarCurrencyFormat = new Intl.NumberFormat('en-US', {style: 'currency', currency: 'ZAR'});
183
+ return zarCurrencyFormat.format(aNumber).replace(/ZAR/gi, 'R');
184
+ }
185
+ export { toZarCurrencyFormat };
186
+
187
+ /**
188
+ * Check if a value is a plain Typescript/JavaScript object.
189
+ * @param {any} value
190
+ * @returns {boolean}
191
+ */
192
+ function isPlainObject(value: any): value is Record<string, any> {
193
+ if ((value === null) || (typeof value !== 'object')) { // Eliminate null, undefined and primitive types.
194
+ return false;
195
+ }
196
+ if (Object.prototype.toString.call(value) !== '[object Object]') { // Eliminate arrays, functions, dates, object instances and other non-plain objects.
197
+ return false;
198
+ }
199
+ return value.constructor === Object; // Eliminate objects created with custom constructors.
200
+ }
201
+ export { isPlainObject };
202
+
203
+ /**Return a deep clone of a document object.
204
+ * By using deep cloning, you create a new object that is entirely separate from the original object.
205
+ *
206
+ * So that whatever you do to that clone, such as deletion of fields, does not affect the original object.
207
+ *
208
+ * NB. Works only plain Javascript/Typescript objects. Field values are not cloned if they are not plain Javascript objects, but are returned as is.
209
+ * @template T
210
+ * @param {T} obj a plain Typescript/Javascript object.
211
+ * @returns a Typescript/Javascript object that is separate from the original object.
212
+ * @note For a full deep clone of Arrays, Maps, and Sets (that throws on functions), consider using the native {@link structuredClone}.
213
+ */
214
+ function deepClone<T extends object>(obj: T): T {
215
+ if (!isPlainObject(obj)) // Not a plain Javascript object, return as is.
216
+ return obj;
217
+
218
+ const stack: any[] = [{...obj}];
219
+ let idx = 0;
220
+
221
+ while (idx < stack.length) {
222
+ let element = stack[idx];
223
+ if (isPlainObject(element)) {
224
+ for (let key in element) {
225
+ if (element[key] instanceof Date) { // Date instance
226
+ element[key] = new Date(element[key]);
227
+ }
228
+ else if (isPlainObject(element[key])) { // Plain object instance
229
+ element[key] = {...element[key]}; // This helps to remove reference to the original object.
230
+ }
231
+ stack.push(element[key]);
232
+ }
233
+ }
234
+ idx++;
235
+ }
236
+ return stack[0] as T;
237
+ } // function deepClone(obj) { // Return a deep clone of an object.
238
+ export { deepClone };
239
+
240
+ /**
241
+ * Get the paths (fields) of the plain Javascript object.
242
+ * @template T
243
+ * @param {T} anObject a plain Typescript/Javascript object.
244
+ * @returns a sorted string array of paths.
245
+ */
246
+ function getPaths<T extends object>(anObject: T): string[] {
247
+ if (!isPlainObject(anObject)) // Not a plain Javascript object, empty array.
248
+ return [];
249
+
250
+ // This is where sub-objects are to be stacked, during traversal through the object.
251
+ const stack: any[] = [{ value: anObject}];
252
+ let idx = 0;
253
+
254
+ while (idx < stack.length) {
255
+ const element = stack[idx];
256
+ if (isPlainObject(element.value)) {
257
+ for (const key in element.value) {
258
+ let path = element.path? element.path + "." + key : key;
259
+ stack.push(
260
+ { value: element.value[key], path }
261
+ );
262
+ }
263
+ element.remove = true; /* This has an incomplete path, and will be removed later */
264
+ }
265
+ idx++;
266
+ }
267
+ return stack
268
+ .filter(element => !(element.remove))
269
+ .map(element => element.path);
270
+ } // function getPaths()
271
+ export { getPaths };
272
+
273
+
274
+ /** Return an object with sorted fields, ordered by field name ascending.
275
+ *
276
+ * The returned object is independent of the source object.
277
+ *
278
+ * NB. For comparison of objects, please see objCompare() function.
279
+ * @template T
280
+ * @param {T} pObject plain Typescript/Javascript object.
281
+ * @returns {T} an object with fields sorted in ascending order of field names.
282
+ */
283
+ function getSortedObject<T extends object>(pObject: T): T {
284
+ if (!isPlainObject(pObject)) // Not a plain Javascript object, return as is.
285
+ return pObject;
286
+
287
+ let cloneObj = deepClone(pObject);
288
+ const paths = getPaths(cloneObj).toSorted();
289
+ let sortedObj = {};
290
+ let idx: number;
291
+ for (idx = 0; idx < paths.length; idx++) {
292
+ const path = paths[idx]!;
293
+ const value = get(cloneObj, path);
294
+ set(sortedObj, path, value);
295
+ }
296
+ return sortedObj as T;
297
+ } // function getSortedObject(pObject) {
298
+ export { getSortedObject };
299
+
300
+ /** Get the value of a field specified by the path from an object.
301
+ * @template T
302
+ * @param {T} anObject a Typescript/Javascript object.
303
+ * @param {string} path a path specifying the field whose value is to be obtained.
304
+ * @param {any} [defaultVal=undefined] a default value to return if the path does not exist on the object.
305
+ * @returns {U|undefined} the value of the field specified by the path, otherwise a default value if supplied.
306
+ */
307
+ function get<T extends object, U extends any>(anObject: T, path: string,
308
+ defaultVal: U|undefined = undefined): U|undefined {
309
+ if (!isPlainObject(anObject)) // Not a plain Javascript object, return undefined.
310
+ return undefined;
311
+
312
+ let paths = path.split('.');
313
+ let tempObj: any = anObject;
314
+ for (let idx = 0; idx < paths.length; idx++) {
315
+ let key = paths[idx]!;
316
+ if (!(key in tempObj)) // key not found.
317
+ return defaultVal;
318
+
319
+ tempObj = tempObj[key];
320
+ if (tempObj === undefined)
321
+ return defaultVal;
322
+ }
323
+ if (paths.length === 0) // Empty path, return default value.
324
+ return defaultVal;
325
+
326
+ return tempObj as U|undefined;
327
+ }
328
+ export { get };
329
+
330
+ /** Set the value of a field specified by the path on an object.
331
+ * @template T, U
332
+ * @param {object} anObject a Typescript/Javascript object.
333
+ * @param {string} path a path specifying the field whose value is to be set.
334
+ * @param {U} value the value to set.
335
+ */
336
+ function set<T extends object, U>(anObject: T, path: string, value: U): void {
337
+ if (!isPlainObject(anObject)) // Not a plain Typescript/Javascript object. Do nothing.
338
+ return;
339
+
340
+ let tempObj: any = anObject;
341
+ let paths = path.split('.');
342
+ for (let idx = 0; idx < paths.length; idx++) {
343
+ let key = paths[idx]!;
344
+ if (idx < paths.length - 1) {
345
+ if (!isPlainObject(tempObj)) // Not a plain Typescript/Javascript object, do nothing.
346
+ return;
347
+ if (!(key in tempObj))
348
+ tempObj[key] = {};
349
+
350
+ tempObj = tempObj[key];
351
+ }
352
+ else
353
+ tempObj[key] = value;
354
+ }
355
+ }
356
+ export { set };
357
+
358
+ /** Unset the value of a field specified by the path on an object.
359
+ * @template T
360
+ * @param {T} anObject a Typescript/Javascript object.
361
+ * @param {string} path a path specifying the field whose value is to be set.
362
+ */
363
+ function unset<T extends object>(anObject: T, path: string): void {
364
+ if (!isPlainObject(anObject)) // Not a plain Typescript/Javascript object, do nothing.
365
+ return;
366
+
367
+ let paths = path.split('.');
368
+ let tempObj: any = anObject;
369
+ for (let idx = 0; idx < paths.length; idx++) {
370
+ let key = paths[idx]!;
371
+ if (!isPlainObject(tempObj))
372
+ return;
373
+ if (!(key in tempObj))
374
+ return;
375
+ if (idx < paths.length - 1)
376
+ tempObj = tempObj[key];
377
+ else
378
+ delete tempObj[key];
379
+ }
380
+ }
381
+ export { unset };
382
+
383
+ /**
384
+ * Determine whether an object contains only 1, some or all of the specified fields, and not any other fields.
385
+ * @template T
386
+ * @param {T} anObject a Javascript object.
387
+ * @param {...string[]} fields one or more field names.
388
+ * @returns boolean true or false.
389
+ */
390
+ function hasOnly<T extends object>(anObject: T, ...fields: string[]): boolean {
391
+ if (!isPlainObject(anObject)) // Not a plain Javascript object, return false.
392
+ return false;
393
+
394
+ if (!fields || !(fields.length))
395
+ throw new Error('fields must be specified');
396
+
397
+ const paths = getPaths(anObject);
398
+ let count = 0;
399
+ for (const index in paths) {
400
+ const path = paths[index]!;
401
+
402
+ if (!fields.includes(path))
403
+ return false;
404
+ else
405
+ count++;
406
+ } // for (const index in paths)
407
+
408
+ return (count > 0);
409
+ }
410
+ export { hasOnly };
411
+
412
+ /**
413
+ * Determine whether an object contains all of the specified fields in addition to other fields.
414
+ * @template T
415
+ * @param {T} anObject a Javascript object.
416
+ * @param {...string[]} fields one or field names.
417
+ * @returns boolean true or false.
418
+ */
419
+ function hasAll<T extends object>(anObject: T, ...fields: string[]): boolean {
420
+ if (!fields || !fields.length)
421
+ throw new Error('fields must be specified');
422
+
423
+ const paths = getPaths(anObject);
424
+ let count = 0;
425
+ for (const index in fields) {
426
+ const field = fields[index]!;
427
+
428
+ if (!paths.includes(field))
429
+ return false;
430
+ else
431
+ count++;
432
+ } // for (const index in paths)
433
+
434
+ return (count === fields.length);
435
+ }
436
+ export { hasAll };
437
+
438
+ /**
439
+ * Determine whether an object contains only all of the specified fields. Nothing more, nothing less.
440
+ * @param {T} anObject a Javascript object.
441
+ * @param {...string[]} fields one or field names.
442
+ * @returns boolean true or false.
443
+ */
444
+ function hasOnlyAll<T extends object>(anObject: T, ...fields: string[]): boolean {
445
+ return hasOnly(anObject, ...fields) && hasAll(anObject, ...fields);
446
+ }
447
+ export { hasOnlyAll };
448
+
449
+ /**Binary Search the sorted primitive data array for a value and return the index.
450
+ *
451
+ * ArraySortDir specifies the direction in which the array is sorted (desc or asc).
452
+ *
453
+ * If the array contains the value searched for, then the index returned is the location of this value on the array,
454
+ * otherwise, the index is of closest value in the array that is before or after the search value in terms of sort order.
455
+ *
456
+ * This function can be used also in cases where values are to be inserted into the array while maintaining sort order.
457
+ * @template T
458
+ * @param {Array<T>} anArray an array of primitve type. All element must be the same type.
459
+ * @param {T} searchVal search value
460
+ * @param {number} [startFrom=0] index from which to start. Default: 0.
461
+ * @param {'asc' | 'desc'} [arraySortDir='asc'] sort direction. Must be 'asc' or 'desc'. Default: 'asc'
462
+ * @returns {number} an index. -1 mean value not found.
463
+ */
464
+ function binarySearch<T>(anArray: T[], searchVal: T,
465
+ startFrom: number = 0, arraySortDir: 'asc' | 'desc' = 'asc'): number {
466
+
467
+ const sortDirections = ['asc', 'desc']
468
+ if (!['asc', 'desc'].includes(arraySortDir))
469
+ throw new Error(`arraySortDir must be one of ${sortDirections}`);
470
+
471
+ if (anArray.length === 0)
472
+ return -1; // Empty array.
473
+
474
+ let start = startFrom,
475
+ end = anArray.length - 1;
476
+
477
+ while(start < end) {
478
+ if (compare(anArray[start], searchVal) === 0)
479
+ return start;
480
+ else if (compare(anArray[end], searchVal) === 0)
481
+ return end;
482
+
483
+ const mid = Math.trunc((start + end) / 2);
484
+ const comparison = compare(anArray[mid], searchVal, arraySortDir);
485
+ if (comparison < 0)
486
+ start = mid + 1;
487
+ else if (comparison > 0)
488
+ end = mid - 1;
489
+ else
490
+ return mid;
491
+ } // while(start < end) {
492
+
493
+ return start;
494
+ } // function binarySearch(anArray, arraySortDir, searchVal) {
495
+ export { binarySearch };
496
+
497
+ /** Compare two values of the same primitive type, according to the sort direction.
498
+ * May be used with dates, numbers, booleans and strings. For other types, the result is unpredictable.
499
+ *
500
+ * A return value of -1 means that value1 is before value2 in terms of sort order.
501
+ *
502
+ * A return value of 1 means that value1 is after value2 in terms of sort order.
503
+ *
504
+ * A return value of 0 means that value1 is equal to value2.
505
+ * @template T
506
+ * @param {T} value1
507
+ * @param {T} value2
508
+ * @param {'asc' | 'desc'} [sortDir='asc'] sort direction. Must be 'asc' or 'desc'. Default: 'asc'
509
+ * @returns {number} -1, 0 or 1
510
+ */
511
+ function compare<T>(value1: T, value2: T, sortDir: 'asc' | 'desc' = 'asc'): number {
512
+ if (!['asc', 'desc'].includes(sortDir))
513
+ throw new Error(`sortDir must be one of ${sortDir}`);
514
+
515
+ const returnValue = (sortDir === 'desc'? -1 : 1);
516
+ if (value1 > value2)
517
+ return returnValue;
518
+ else if (value1 < value2)
519
+ return -returnValue;
520
+ else // Avoid if (value1 === value2) because this may yield false for reference types (ie. Dates), because of different memory addresses.
521
+ return 0;
522
+ } // function compare(value1, value2, sortDir) {
523
+ export { compare };
524
+
525
+ /**Binary Search the sorted (ascending or descending order) array of plain Typescript/Javascript objects for a value and return the index.
526
+ *
527
+ * The assumption is that the array is sorted in order of 1 or more sort fields,
528
+ *
529
+ * Examples of sort fields: 'lastName asc', 'firstName', 'address.province asc', 'address.townOrCity asc'.
530
+ *
531
+ * If the array contains the object with values searched for, then the index returned is the location of this value in the array, otherwise,
532
+ * the index is of the closest value in the array that is before or after the searchObj value.
533
+ * Return -1 for an empty array.
534
+ * Assumed field data types are numbers, strings, booleans and dates.
535
+ * This function is to be used also in cases where objects are to be inserted into the array while maintaining sort order.
536
+ * @template T
537
+ * @param {Array<T>} objArray an array of Javascript objects.
538
+ * @param {T} searchObj an object to search for.
539
+ * @param {number} [startFrom=0] index from which to start searching.
540
+ * @param {...string[]} sortFields one or more search fields.
541
+ * @returns {number} an index.
542
+ */
543
+ function binarySearchObj<T extends object>(objArray: T[], searchObj: T, startFrom: number = 0,
544
+ ...sortFields: string[]): number {
545
+ if (!sortFields || !sortFields.length)
546
+ throw new Error('At least one sort field is required.');
547
+
548
+ if (objArray.length === 0)
549
+ return -1;
550
+
551
+ if (objArray.some(element => !isPlainObject(element)))
552
+ throw new Error('All elements in objArray must be plain objects.');
553
+
554
+ let start = startFrom,
555
+ end = objArray.length - 1;
556
+
557
+ while(start < end) {
558
+ if (objCompare(objArray[start]!, searchObj, ...sortFields) === 0)
559
+ return start;
560
+ else if (objCompare(objArray[end]!, searchObj, ...sortFields) === 0)
561
+ return end;
562
+
563
+ let mid = Math.trunc((start + end) / 2);
564
+
565
+ if (objCompare(objArray[mid]!, searchObj, ...sortFields) < 0)
566
+ start = mid + 1;
567
+ else if (objCompare(objArray[mid]!, searchObj, ...sortFields) > 0)
568
+ end = mid - 1;
569
+ else
570
+ return mid;
571
+ } // while(start < end) {
572
+
573
+ return start;
574
+ } // function binarySearchObj(objArray, searchObj, ...comparisonFields) {
575
+ export {binarySearchObj};
576
+
577
+ /**Get the index of the first element in an object array that is different from the target element
578
+ * according to the comparison fields.
579
+ * @template T
580
+ * @param {Array<T>} objArray an array of objects
581
+ * @param {T} targetObj target object
582
+ * @param {number} startFrom index from which to start searching
583
+ * @param {...string[]} comparisonFields the fields sort order of the array. e.g. 'score desc', 'numGames asc'.
584
+ * @returns index of the next different object.
585
+ */
586
+ function getNextDifferent<T extends object>(objArray: T[], targetObj: T, startFrom: number,
587
+ ...comparisonFields: string[]): number {
588
+ if (!comparisonFields || !comparisonFields.length)
589
+ throw new Error('At least one comparison field is required.');
590
+ if (objArray.length === 0)
591
+ return -1;
592
+ if (objArray.some(element => !isPlainObject(element)))
593
+ throw new Error('All elements in objArray must be plain objects.');
594
+
595
+ let start = startFrom,
596
+ end = objArray.length - 1;
597
+
598
+ if (start >= objArray.length) { // throw error if startFrom is outside the bounds of the array.
599
+ throw new Error('startFrom is outside the bounds of the array.');
600
+ }
601
+ // If target object is to the right of objArray[start], then throw an error..
602
+ if (objCompare(targetObj, objArray[start]!, ...comparisonFields) > 0) {
603
+ throw new Error('targetObj is to the right (\'greater than\') objArray[startFrom].');
604
+ }
605
+
606
+ while (start < end) {
607
+ let mid = Math.trunc((start + end) / 2);
608
+ if (objCompare(targetObj, objArray[mid]!, ...comparisonFields) === 0) {
609
+ start = mid + 1;
610
+ }
611
+ else if (objCompare(targetObj, objArray[mid]!, ...comparisonFields) < 0) {
612
+ end = mid;
613
+ }
614
+ }
615
+ if (objCompare(targetObj, objArray[start]!, ...comparisonFields) === 0)
616
+ return -1;
617
+ return start;
618
+ }
619
+ export {getNextDifferent};
620
+
621
+ /**Create an array with duplicates eliminated, according to certain fields. Taking only the first or last object from each duplicate set.
622
+ *
623
+ * If firstOfDuplicates === true, then the first element in each set of duplicates is taken.
624
+ *
625
+ * if firstOfDuplicates === false, then the last element is taken from each set of duplicates.
626
+ *
627
+ * Assumed comparison field data types are Boolean, Number, String, Date.
628
+ *
629
+ * The array must be sorted according to the comparison fields before calling this function.
630
+ * The value of the comparison field must include both the field name and sort direction.
631
+ * Sort direction assumed to be "asc" if not provided.
632
+ * Examples of comparison fields: "firstName", "lastName desc", "address.province asc", "address.townOrCity".
633
+ * @template T
634
+ * @param {Array<T>} objArray an input array of objects
635
+ * @param {boolean} firstOfDuplicates specify whether to take the first or last object in each a duplicate set.
636
+ * @param {...string[]} comparisonFields comparison fieds plus sort order.
637
+ * @returns {Array<T>} an array with no duplicates.
638
+ */
639
+ function getObjArrayWithNoDuplicates<T extends object>(objArray: T[], firstOfDuplicates: boolean,
640
+ ...comparisonFields: string[]): T[] {
641
+
642
+ if (objArray.length <= 1)
643
+ return [...objArray];
644
+
645
+ if (typeof firstOfDuplicates !== 'boolean')
646
+ throw new Error(`firstOfDuplicates must be boolean true or false.`);
647
+
648
+ if (!comparisonFields || !comparisonFields.length)
649
+ throw new Error('At least one comparison field is required.');
650
+
651
+ if (objArray.some(element => !isPlainObject(element)))
652
+ throw new Error('All elements in objArray must be plain objects.');
653
+
654
+ const noDuplicates: T[] = [];
655
+ let grpStart = 0; // Start index of current duplicate group.
656
+ while (grpStart < objArray.length - 1) {
657
+ if (firstOfDuplicates) {
658
+ noDuplicates.push(objArray[grpStart]!);
659
+ }
660
+
661
+ grpStart = getNextDifferent(objArray, objArray[grpStart]!, grpStart + 1, ...comparisonFields);
662
+ if (grpStart < 0)
663
+ break; // No more different objects.
664
+
665
+ let grpEnd = grpStart - 1;
666
+ if (!firstOfDuplicates) {
667
+ noDuplicates.push(objArray[grpEnd]!);
668
+ }
669
+ }
670
+ if (noDuplicates.length === 0) { // All objects are duplicates.
671
+ if (firstOfDuplicates)
672
+ noDuplicates.push(objArray[0]!);
673
+ else
674
+ noDuplicates.push(objArray[objArray.length - 1]!);
675
+ }
676
+ else {
677
+ if (objCompare(noDuplicates[noDuplicates.length - 1]!, objArray[objArray.length - 1]!, ...comparisonFields) !== 0) {
678
+ noDuplicates.push(objArray[objArray.length - 1]!);
679
+ }
680
+ }
681
+
682
+ return noDuplicates;
683
+ } // function getObjArrayWithNoDuplicates(objArray, ...comparisonFields) {
684
+ export {getObjArrayWithNoDuplicates};
685
+
686
+ /**Compare 2 objects according to the comparison fields, and return the result of:
687
+ *
688
+ * -1 if obj1 is before obj2, 1 if obj1 is after obj2, 0 if obj1 is equal to obj2.
689
+ *
690
+ * Each each of the comparisonFields must be of the form 'fieldName sortDirection' or 'fieldName'.
691
+ *
692
+ * Sort directions: 'asc', 'desc'.
693
+ *
694
+ * Field/sort-direction examples: 'lastName desc', 'firstName', 'firstName asc', 'address.provinceName asc'.
695
+ *
696
+ * If sort direction is not provided, then it is assumed to be ascending.
697
+ * @template T
698
+ * @param {T} obj1 first object to compare
699
+ * @param {T} obj2 second object to compare
700
+ * @param {...string[]} comparisonFields one or more comparison fields plus sort order.
701
+ * @returns {number} comparison result: -1, 0 or 1.
702
+ */
703
+ function objCompare<T extends object>(obj1: T, obj2: T, ...comparisonFields: string[]): number {
704
+ if (!comparisonFields || !comparisonFields.length)
705
+ throw new Error('comparisonFields not supplied!');
706
+ if (!isPlainObject(obj1) || !isPlainObject(obj2))
707
+ throw new Error('Both obj1 and obj2 must be plain objects.');
708
+
709
+ const sortDirections = ['', 'asc', 'desc'];
710
+ for (let index = 0; index < comparisonFields.length; index++) {
711
+ const field = comparisonFields[index]!.split(' ');
712
+ const fieldName = field[0]!;
713
+ let sortDir = '';
714
+ if (field.length > 2)
715
+ throw new Error('Each comparison field must be of the form \'fieldName sortDirection\' or \'fieldName\'.');
716
+ if (field.length === 2)
717
+ sortDir = field[1]!;
718
+
719
+ if (!sortDirections.includes(sortDir))
720
+ throw new Error('Sort direction must be one of ' + sortDirections.toString());
721
+
722
+ const value1 = get<T, any>(obj1, fieldName);
723
+ const value2 = get<T, any>(obj2, fieldName);
724
+
725
+ const returnValue = (sortDir === 'desc'? -1: 1);
726
+ if (value1 > value2)
727
+ return returnValue;
728
+ else if (value1 < value2)
729
+ return -returnValue;
730
+ } // for (const field in comparisonFields) {
731
+ return 0;
732
+ } // function comparison(obj1, obj2, ...comparisonFields) {
733
+ export {objCompare};