Repository: exadel-inc/esl
Pull request #470 (2021-09-10 11:16:45)

Re-merge #455
https://github.com/exadel-inc/esl/pull/455 remerge

github-actions[bot]: ****CLA Assistant Lite bot**** All contributors have signed the CLA  ✍️ 

Patch:
diff --git a/src/modules/esl-utils/README.md b/src/modules/esl-utils/README.md
index 1ac813e10..58f0e5d99 100644
--- a/src/modules/esl-utils/README.md
+++ b/src/modules/esl-utils/README.md
@@ -85,6 +85,8 @@ Even if you use the whole utils module, it is still tiny.
 
     - ##### [Array](./misc/array.ts) - array utils (uniq, flat, wrap, etc.).
   
+    - ##### [Set](./misc/set.ts) - set/array with uniq values utils
+
     - ##### [Format](./misc/format.ts) - string format utils.
   
     - ##### [Function](./misc/functions.ts) - simple functions and types.
diff --git a/src/modules/esl-utils/all.ts b/src/modules/esl-utils/all.ts
index 0ab64e644..4842d5b3c 100644
--- a/src/modules/esl-utils/all.ts
+++ b/src/modules/esl-utils/all.ts
@@ -1,10 +1,12 @@
-// Common
+// Miscellanies
 export * as UID from './misc/uid';
+export * as SetUtils from './misc/set';
 export * as ArrayUtils from './misc/array';
 export * as ObjectUtils from './misc/object';
 export * as FormatUtils from './misc/format';
 export * as FunctionUtils from './misc/functions';
 
+// Memoization decorator
 export * from './misc/memoize';
 
 // Common
diff --git a/src/modules/esl-utils/misc/array.ts b/src/modules/esl-utils/misc/array.ts
index 44b70923f..a727e0dea 100644
--- a/src/modules/esl-utils/misc/array.ts
+++ b/src/modules/esl-utils/misc/array.ts
@@ -9,11 +9,11 @@ export const tuple = <T>(arr: T[]): Tuple<T>[] => arr.reduce((acc: Tuple<T>[], e
   return acc;
 }, []);
 
-/** Flat array */
+/** Flat array - unwraps one level of nested arrays */
 export const flat = <T>(arr: (null | T | T[])[]): T[] =>
   arr.reduce((acc: T[], el) => el ? acc.concat(el) : acc, []) as T[];
 
-/** Wrap to array */
+/** Wrap passed object to array */
 export const wrap = <T>(arr: undefined | null | T | T[]): T[] => {
   if (arr === undefined || arr === null) return [];
   if (Array.isArray(arr)) return arr;
@@ -29,9 +29,9 @@ export const uniq = <T> (arr: T[]): T[] => {
   return result;
 };
 
-/** Crete an array filled with the range [0,..,N-1] */
+/** Create an array filled with the range [0,..,N-1] */
 export function range(n: number): number[];
-/** Crete an array filled with values returned by the filler callback */
+/** Create an array filled with values returned by the filler callback */
 export function range<T>(n: number, filler: (i: number) => T): T[];
 export function range(n: number, filler: (i: number) => any = identity): any[] {
   const arr = Array(n);
diff --git a/src/modules/esl-utils/misc/set.ts b/src/modules/esl-utils/misc/set.ts
new file mode 100644
index 000000000..82eac466a
--- /dev/null
+++ b/src/modules/esl-utils/misc/set.ts
@@ -0,0 +1,36 @@
+/** Create an array of unique values that presented in each of the passed arrays */
+export function intersection<T>(...rest: T[][]): T[];
+export function intersection<T>(a: T[], b: T[], ...rest: T[][]): T[] {
+  if (arguments.length < 2) return a ? a.slice() : [];
+  if (rest.length) return intersection(a, intersection(b, ...rest));
+  return a.filter(Set.prototype.has, new Set(b));
+}
+
+/** Create an array with unique values from each of the passed arrays */
+export function union<T>(...rest: T[][]): T[]  {
+  const set = new Set();
+  rest.forEach(item => item.forEach(i => set.add(i)));
+  const result: any[] = [];
+  set.forEach(value => result.push(value));
+  return result;
+}
+
+/** Creates an array of unique values from the first array that are not present in the other arrays */
+export function complement<T>(...rest: T[][]): T[];
+export function complement<T>(a: T[], b: T[], ...rest: T[][]): T[] | undefined {
+  if (!b) return a ? a.slice() : [];
+  if (rest.length > 1)  return complement(a, complement(b, ...rest));
+  const setB = new Set(b);
+  return a.filter(element => !setB.has(element));
+}
+
+/**
+ * @returns if the passed arrays have a full intersection
+ * Expect uniq values in collections
+ */
+export function fullIntersection<T>(a: T[], b: T[]): boolean {
+  if (a.length === 0 && b.length === 0) return true;
+  const [larger, smaller] = a.length >= b.length ? [a, b] : [b, a];
+  const set = new Set(larger);
+  return !smaller.filter((item: T) => !set.has(item)).length;
+}


Test patch:
diff --git a/src/modules/esl-utils/misc/test/set.test.ts b/src/modules/esl-utils/misc/test/set.test.ts
new file mode 100644
index 000000000..3045fbb6c
--- /dev/null
+++ b/src/modules/esl-utils/misc/test/set.test.ts
@@ -0,0 +1,50 @@
+import {complement, fullIntersection, intersection, union} from '../set';
+
+describe('set', () => {
+  test('intersection', () => {
+    const a = {a: 3};
+    expect(intersection()).toEqual([]);
+    expect(intersection([1])).toEqual([1]);
+    expect(intersection([],[])).toEqual([]);
+    expect(intersection([1],[1])).toEqual([1]);
+    expect(intersection([1, 2, 3], [1, 2], [1, 2, 4])).toEqual([1, 2]);
+    expect(intersection([1, [2, 3], a], [a, 1, [2, 3]])).toEqual([1, a]);
+    expect(intersection([null, 1, 2, 3, [6]], [4, 5])).toEqual([]);
+    expect(intersection([1, 2, 3, [6]], [4, 5, 1, 2], [1])).toEqual([1]);
+    expect(intersection([NaN],[NaN])).toEqual([NaN]);
+  });
+
+  test('union', () => {
+    const a = {a:3};
+    expect(union()).toEqual([]);
+    expect(union([1])).toEqual([1]);
+    expect(union([], [])).toEqual([]);
+    expect(union([1], [1])).toEqual([1]);
+    expect(union([1, 2], [1, 2], [1, 2])).toEqual([1, 2,]);
+    expect(union([1, [2, 3], a], [a, 1, [2, 3]])).toEqual([1, [2, 3], a, [2, 3]]);
+    expect(union([1, 2, 3, null, [6]], [4, 5], [1, 2, 3, null, [6]], [4, 5])).toEqual([1, 2, 3, null, [6], 4, 5, [6]]);
+    expect(union([NaN],[NaN])).toEqual([NaN]);
+  });
+
+  test('complement', () => {
+    const a = {a: 3};
+    expect(complement()).toEqual([]);
+    expect(complement([1])).toEqual([1]);
+    expect(complement([],[])).toEqual([]);
+    expect(complement([1],[1])).toEqual([]);
+    expect(complement([1, 2, 3], [1, 2], [1, 2, 5])).toEqual([3]);
+    expect(complement([1, [2, 3], a], [a, 1, [2, 3]])).toEqual([[2, 3]]);
+    expect(complement([null, 1, 2, 3,  [6]], [4, 5])).toEqual([null, 1, 2, 3,[6]]);
+    expect(complement([NaN],[NaN])).toEqual([]);
+  });
+
+  test('fullIntersection', () => {
+    const a = {a: 3};
+    expect(fullIntersection([],[])).toEqual(true);
+    expect(fullIntersection([1],[1])).toEqual(true);
+    expect(fullIntersection([1, 2, 3], [1, 2])).toEqual(true);
+    expect(fullIntersection([1, [2, 3], a], [a, 1, [2, 3]])).toEqual(false);
+    expect(fullIntersection([null, 1, 2, 3,  [6]], [4, 5])).toEqual(false);
+    expect(fullIntersection([null, 1, 2, 3,  [6]], [4, 5, null, 1, 2, 3,  [6]])).toEqual(false);
+  });
+})