Repository: char0n/ramda-adjunct (js)

Issue:
anyP
Make sense to wait untill stage-2 is reached. https://github.com/tc39/proposal-promise-any

**Update:**
Already present in other Promise libraries. No need to wait for stage-2.

Pull request:
feat(promise): add anyP
Closes #807.



Patch:
diff --git a/package-lock.json b/package-lock.json
index 0547035828..5570638eb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7067,9 +7067,9 @@
       }
     },
     "husky": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.6.tgz",
-      "integrity": "sha512-bNTgujuC5VS0m8gK70zRpzXq9rccFUFcjuU6L/SFLZxV7eke+6x10Tpk+st3rmgCEsQ4vn+yJk1jx3gvSD8knQ==",
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.8.tgz",
+      "integrity": "sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==",
       "dev": true,
       "requires": {
         "chalk": "^2.4.2",
diff --git a/package.json b/package.json
index 403e9510ff..d4dbd06ec9 100644
--- a/package.json
+++ b/package.json
@@ -156,7 +156,7 @@
     "fantasy-laws": "=1.2.0",
     "folktale": "=2.3.2",
     "glob": "7.1.4",
-    "husky": "3.0.6",
+    "husky": "3.0.8",
     "istanbul": "=0.4.5",
     "jsdoc": "3.6.3",
     "jsverify": "0.8.4",
diff --git a/src/anyP.js b/src/anyP.js
new file mode 100644
index 0000000000..403dde454b
--- /dev/null
+++ b/src/anyP.js
@@ -0,0 +1,33 @@
+import { bind } from 'ramda';
+import curry1 from 'ramda/src/internal/_curry1';
+
+import isFunction from './isFunction';
+import polyfill, { AggregatedError } from './internal/polyfills/Promise.any';
+
+export const anyPPolyfill = curry1(polyfill);
+export { AggregatedError };
+
+/**
+ * Returns a promise that is fulfilled by the first given promise to be fulfilled,
+ * or rejected with an array of rejection reasons if all of the given promises are rejected.
+ *
+ * @func anyP
+ * @memberOf RA
+ * @category Function
+ * @sig [Promise a] -> Promise a
+ * @param {Iterable.<*>} iterable An iterable object such as an Array or String
+ * @return {Promise} Returns a promise that is fulfilled by the first given promise to be fulfilled,
+ * or rejected with an array of rejection reasons if all of the given promises are rejected.
+ * @example
+ *
+ * RA.anyP([
+ *   Promise.resolve(1),
+ *   2,
+ *   Promise.reject(3),
+ * ]); //=> Promise(1)
+ */
+const anyP = isFunction(Promise.any)
+  ? curry1(bind(Promise.any, Promise))
+  : anyPPolyfill;
+
+export default anyP;
diff --git a/src/divideNum.js b/src/divideNum.js
new file mode 100644
index 0000000000..0621d18507
--- /dev/null
+++ b/src/divideNum.js
@@ -0,0 +1,20 @@
+import { divide, flip } from 'ramda';
+
+/**
+ * Divides two numbers, where the second number is divided by the first number.
+ *
+ * @func divideNum
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category Math
+ * @sig Number -> Number -> Number
+ * @param {number} divisor the number to divide by
+ * @param {number} dividend the number to divide
+ * @return {number} A number representing the quotient of dividing the dividend by the divisor
+ * @example
+ *
+ * RA.divideNum(2, 1); //=> 0.5
+ */
+const divideNum = flip(divide);
+
+export default divideNum;
diff --git a/src/index.js b/src/index.js
index e97ef1c7ac..6accea58bd 100644
--- a/src/index.js
+++ b/src/index.js
@@ -75,6 +75,7 @@ export { default as isFalsy } from './isFalsy';
 export { default as isRegExp } from './isRegExp';
 export { default as isNotRegExp } from './isNotRegExp';
 export { default as isSparseArray } from './isSparseArray';
+export { default as isSymbol } from './isSymbol';
 // Function
 export { default as stubUndefined } from './stubUndefined';
 export { default as stubNull } from './stubNull';
@@ -101,6 +102,7 @@ export { default as seq } from './seq';
 export { default as sequencing } from './seq';
 export { default as dispatch } from './dispatch';
 export { default as async } from './async';
+export { default as anyP } from './anyP';
 // List
 export { default as mapIndexed } from './mapIndexed';
 export { default as reduceIndexed } from './reduceIndexed';
@@ -173,9 +175,11 @@ export { default as dropArgs } from './dropArgs';
 // Math
 export { default as round } from './round';
 export { default as ceil } from './ceil';
+export { default as divideNum } from './divideNum';
 export { default as floor } from './floor';
 export { default as trunc } from './trunc';
 export { default as sign } from './sign';
+export { default as subtractNum } from './subtractNum';
 // String
 export { default as replaceAll } from './replaceAll';
 export { default as escapeRegExp } from './escapeRegExp';
diff --git a/src/internal/polyfills/Promise.any.js b/src/internal/polyfills/Promise.any.js
new file mode 100644
index 0000000000..ab26552680
--- /dev/null
+++ b/src/internal/polyfills/Promise.any.js
@@ -0,0 +1,35 @@
+import { map } from 'ramda';
+
+import resolveP from '../../resolveP';
+
+export class AggregatedError extends Error {
+  constructor(errors = [], message) {
+    super(message);
+
+    this.errors = errors;
+  }
+}
+
+const anyPolyfill = iterable => {
+  const exceptions = [];
+
+  return new Promise((resolve, reject) => {
+    const onReject = e => {
+      exceptions.push(e);
+
+      if (exceptions.length === iterable.length) {
+        reject(new AggregatedError(exceptions));
+      }
+    };
+
+    map(
+      p =>
+        resolveP(p)
+          .then(resolve)
+          .catch(onReject),
+      [...iterable]
+    );
+  });
+};
+
+export default anyPolyfill;
diff --git a/src/isSymbol.js b/src/isSymbol.js
new file mode 100644
index 0000000000..749f44f9d6
--- /dev/null
+++ b/src/isSymbol.js
@@ -0,0 +1,30 @@
+import { type } from 'ramda';
+import curry1 from 'ramda/src/internal/_curry1';
+
+/**
+ * Checks if input value is a Symbol.
+ *
+ * @func isSymbol
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category Type
+ * @sig * -> Boolean
+ * @param {*} val The value to test
+ * @return {boolean}
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol|MDN Symbol}
+ * @example
+ *
+ * RA.isSymbol(Symbol('1')); //=> true
+ * RA.isSymbol(Symbol(1)); //=> true
+ * RA.isSymbol('string'); //=> false
+ * RA.isSymbol(undefined); //=> false
+ * RA.isSymbol(null); //=> false
+ */
+const isSymbol = curry1(val => {
+  return (
+    typeof val === 'symbol' ||
+    (typeof val === 'object' && type(val) === 'Symbol')
+  );
+});
+
+export default isSymbol;
diff --git a/src/subtractNum.js b/src/subtractNum.js
new file mode 100644
index 0000000000..fdf9631811
--- /dev/null
+++ b/src/subtractNum.js
@@ -0,0 +1,20 @@
+import { flip, subtract } from 'ramda';
+
+/**
+ * Subtracts its first argument from its second argument.
+ *
+ * @func subtractNum
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category Math
+ * @sig Number -> Number -> Number
+ * @param {number} subtrahend the number to subtract
+ * @param {number} minuend the number to subtract from
+ * @return {number} A number representing the difference of subtracting the subtrahend from the minuend
+ * @example
+ *
+ * RA.subtractNum(3, 5); //=> 2
+ */
+const subtractNum = flip(subtract);
+
+export default subtractNum;
diff --git a/types/index.d.ts b/types/index.d.ts
index 912f2cb3a3..75cd423d44 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -115,6 +115,11 @@ declare namespace RamdaAdjunct {
          */
         isNotUndefined(val: any): boolean;
 
+        /**
+         * Checks if input value is `Symbol`.
+         */
+        isSymbol(val: any): val is Symbol;
+
         /**
          * Checks if input value is `null`.
          */
@@ -784,6 +789,12 @@ declare namespace RamdaAdjunct {
         */
         allSettledP<T>(iterable: Iterable<T>): Promise<Array<SettledPromise<T>>>;
 
+        /**
+         * Returns a promise that is fulfilled by the first given promise to be fulfilled,
+         * or rejected with an array of rejection reasons if all of the given promises are rejected.
+         */
+        anyP<T>(iterable: Iterable<T>): Promise<T>;
+
         /**
          * Composable shortcut for `Promise.resolve`.
          *
@@ -1103,6 +1114,12 @@ declare namespace RamdaAdjunct {
          */
         round(val: number): number;
 
+        /**
+         * Subtracts its first argument from its second argument.
+         */
+        subtractNum(subtrahend: number, minuend: number): number;
+        subtractNum(subtrahend: number): (minuend: number) => number;
+
         /**
          * Returns the smallest integer greater than or equal to a given number.
          *
@@ -1152,6 +1169,12 @@ declare namespace RamdaAdjunct {
         escapeRegExp(val: string): string;
         escapeRegExp<T>(val: T): T;
 
+        /**
+         * Divides two numbers, where the second number is divided by the first number.
+         */
+        divideNum(divisor: number, dividend: number): number;
+        divideNum(divisor: number): (dividend: number) => number;
+
         /**
          * Identity type.
          */


Test patch:
diff --git a/test/anyP.js b/test/anyP.js
new file mode 100644
index 0000000000..f21c2ef4f7
--- /dev/null
+++ b/test/anyP.js
@@ -0,0 +1,191 @@
+import * as R from 'ramda';
+import { assert } from 'chai';
+
+import * as RA from '../src';
+import { anyPPolyfill, AggregatedError } from '../src/anyP';
+
+describe('anyP', function() {
+  context('given list of fulfilled promises', function() {
+    specify('should return first of fulfilled values', async function() {
+      const p1 = RA.resolveP(1);
+      const p2 = RA.resolveP(2);
+      const p3 = RA.resolveP(3);
+      const actual = [p1, p2, p3];
+      const expected = 1;
+
+      assert.strictEqual(await RA.anyP(actual), expected);
+    });
+  });
+
+  context('given list of rejected promises', function() {
+    specify(
+      'should fail with AggregatedError that contains a list of rejected reasons',
+      async function() {
+        const p1 = RA.rejectP(1);
+        const p2 = RA.rejectP(2);
+        const p3 = RA.rejectP(3);
+        const actual = [p1, p2, p3];
+        const expected = [1, 2, 3];
+
+        try {
+          await RA.anyP(actual);
+          throw new Error('resolving should fail');
+        } catch (e) {
+          assert.notStrictEqual(e.message, 'resolving should fail');
+          assert.instanceOf(e, AggregatedError);
+          assert.deepEqual(e.errors, expected);
+        }
+      }
+    );
+  });
+
+  context('given list of fulfilled and rejected promises', function() {
+    specify('should return first of fulfilled values', async function() {
+      const p1 = RA.rejectP(1);
+      const p2 = RA.resolveP(2);
+      const p3 = RA.rejectP(3);
+      const actual = [p1, p2, p3];
+      const expected = 2;
+
+      assert.strictEqual(await RA.anyP(actual), expected);
+    });
+  });
+
+  context('given list of values', function() {
+    specify('should return first of fulfilled values', async function() {
+      const v1 = 1;
+      const v2 = 2;
+      const v3 = 3;
+      const actual = [v1, v2, v3];
+      const expected = 1;
+
+      assert.strictEqual(await RA.anyP(actual), expected);
+    });
+  });
+
+  context('given list of values and promises', function() {
+    specify('should return first of fulfilled values', async function() {
+      const p1 = RA.resolveP(1);
+      const v2 = 2;
+      const p3 = RA.resolveP(3);
+      const actual = [p1, v2, p3];
+      const expected = 1;
+
+      assert.strictEqual(await RA.anyP(actual), expected);
+    });
+  });
+
+  context('given large number of promises', function() {
+    specify('should handle them properly', async function() {
+      const list = R.range(1, 1000);
+      const actual = list.map(RA.resolveP);
+      const expected = list[0];
+
+      assert.strictEqual(await RA.anyP(actual), expected);
+    });
+  });
+
+  it('should support placeholder to specify "gaps"', async function() {
+    const anyP = RA.anyP(R.__);
+    const p1 = RA.resolveP(1);
+    const v2 = 2;
+    const p3 = RA.resolveP(3);
+    const actual = [p1, v2, p3];
+    const expected = 1;
+
+    assert.strictEqual(await anyP(actual), expected);
+  });
+
+  context('anyPPolyfill', function() {
+    context('given list of fulfilled promises', function() {
+      specify('should return first of fulfilled values', async function() {
+        const p1 = RA.resolveP(1);
+        const p2 = RA.resolveP(2);
+        const p3 = RA.resolveP(3);
+        const actual = [p1, p2, p3];
+        const expected = 1;
+
+        assert.strictEqual(await anyPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of rejected promises', function() {
+      specify(
+        'should fail with AggregatedError that contains a list of rejected reasons',
+        async function() {
+          const p1 = RA.rejectP(1);
+          const p2 = RA.rejectP(2);
+          const p3 = RA.rejectP(3);
+          const actual = [p1, p2, p3];
+          const expected = [1, 2, 3];
+
+          try {
+            await anyPPolyfill(actual);
+            throw new Error('resolving should fail');
+          } catch (e) {
+            assert.notStrictEqual(e.message, 'resolving should fail');
+            assert.instanceOf(e, AggregatedError);
+            assert.deepEqual(e.errors, expected);
+          }
+        }
+      );
+    });
+
+    context('given list of fulfilled and rejected promises', function() {
+      specify('should return first of fulfilled values', async function() {
+        const p1 = RA.rejectP(1);
+        const p2 = RA.resolveP(2);
+        const p3 = RA.rejectP(3);
+        const actual = [p1, p2, p3];
+        const expected = 2;
+
+        assert.strictEqual(await anyPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of values', function() {
+      specify('should return first of fulfilled values', async function() {
+        const v1 = 1;
+        const v2 = 2;
+        const v3 = 3;
+        const actual = [v1, v2, v3];
+        const expected = 1;
+
+        assert.strictEqual(await anyPPolyfill(actual), expected);
+      });
+    });
+
+    context('given list of values and promises', function() {
+      specify('should return first of fulfilled values', async function() {
+        const p1 = RA.resolveP(1);
+        const v2 = 2;
+        const p3 = RA.resolveP(3);
+        const actual = [p1, v2, p3];
+        const expected = 1;
+
+        assert.strictEqual(await anyPPolyfill(actual), expected);
+      });
+    });
+
+    context('given large number of promises', function() {
+      specify('should handle them properly', async function() {
+        const list = R.range(1, 1000);
+        const actual = list.map(RA.resolveP);
+        const expected = list[0];
+
+        assert.strictEqual(await anyPPolyfill(actual), expected);
+      });
+    });
+
+    it('should support placeholder to specify "gaps"', async function() {
+      const anyP = anyPPolyfill(R.__);
+      const p1 = RA.resolveP(1);
+      const v2 = 2;
+      const p3 = RA.resolveP(3);
+      const actual = [p1, v2, p3];
+      const expected = 1;
+
+      assert.strictEqual(await anyP(actual), expected);
+    });
+  });
+});
diff --git a/test/divideNum.js b/test/divideNum.js
new file mode 100644
index 0000000000..1277b8baa2
--- /dev/null
+++ b/test/divideNum.js
@@ -0,0 +1,17 @@
+import * as RA from '../src';
+import eq from './shared/eq';
+
+describe('divideNum', function() {
+  it('should divide the second number by the first number', function() {
+    eq(RA.divideNum(2, 1), 0.5);
+    eq(RA.divideNum(2.0, 1.0), 0.5);
+    eq(RA.divideNum(0, 2.0), Infinity);
+    eq(RA.divideNum(0.0, 2.0), Infinity);
+    eq(RA.divideNum(-0.0, 2.0), -Infinity);
+  });
+
+  it('should curry', function() {
+    eq(RA.divideNum(2)(1), 0.5);
+    eq(RA.divideNum(2, 1), 0.5);
+  });
+});
diff --git a/test/isSymbol.js b/test/isSymbol.js
new file mode 100644
index 0000000000..d81c59c82f
--- /dev/null
+++ b/test/isSymbol.js
@@ -0,0 +1,34 @@
+import { assert } from 'chai';
+import * as R from 'ramda';
+
+import * as RA from '../src';
+
+describe('isSymbol', function() {
+  context('given a Symbol as value', function() {
+    it('should return true', function() {
+      assert.isTrue(RA.isSymbol(Symbol('any')));
+      assert.isTrue(RA.isSymbol(Symbol(1)));
+      assert.isTrue(RA.isSymbol(Symbol.iterator));
+    });
+  });
+
+  context('given anything else as value', function() {
+    it('should return false', function() {
+      assert.isFalse(RA.isSymbol(undefined));
+      assert.isFalse(RA.isSymbol(null));
+      assert.isFalse(RA.isSymbol(() => {}));
+      assert.isFalse(RA.isSymbol([]));
+      assert.isFalse(RA.isSymbol({}));
+      assert.isFalse(RA.isSymbol(NaN));
+      assert.isFalse(RA.isSymbol(true));
+      assert.isFalse(RA.isSymbol('string'));
+      assert.isFalse(RA.isSymbol(1));
+    });
+  });
+
+  it('should support placeholder to specify "gaps"', function() {
+    const isSymbol = RA.isSymbol(R.__);
+
+    assert.isFalse(isSymbol('abc'));
+  });
+});
diff --git a/test/subtractNum.js b/test/subtractNum.js
new file mode 100644
index 0000000000..a71dec408f
--- /dev/null
+++ b/test/subtractNum.js
@@ -0,0 +1,15 @@
+import * as RA from '../src';
+import eq from './shared/eq';
+
+describe('subtractNum', function() {
+  it('should subtract the first number from the second number', function() {
+    eq(RA.subtractNum(3, 5), 2);
+    eq(RA.subtractNum(5, 3), -2);
+    eq(RA.subtractNum(3, 'foo'), NaN);
+  });
+
+  it('should curry', function() {
+    eq(RA.subtractNum(3)(5), 2);
+    eq(RA.subtractNum(3, 5), 2);
+  });
+});
diff --git a/types/test/anyP.ts b/types/test/anyP.ts
new file mode 100644
index 0000000000..2073015fb0
--- /dev/null
+++ b/types/test/anyP.ts
@@ -0,0 +1,23 @@
+import * as RA from 'ramda-adjunct';
+
+/**
+ * Helpers.
+ */
+
+const customIterable = {
+    *[Symbol.iterator](): IterableIterator<number> {
+        yield 1;
+        yield 2;
+        yield 3;
+    },
+};
+
+/**
+ * Tests.
+ */
+
+RA.anyP([1, 2]); // $ExpectType Promise<number>
+RA.anyP(['a', 'b']); // $ExpectType Promise<string>
+RA.anyP([1, 'a']); // $ExpectType Promise<string | number>
+RA.anyP([]); // $ExpectType Promise<never>
+RA.anyP(customIterable); // $ExpectType Promise<number>
diff --git a/types/test/divideNum.ts b/types/test/divideNum.ts
new file mode 100644
index 0000000000..7404b32efe
--- /dev/null
+++ b/types/test/divideNum.ts
@@ -0,0 +1,13 @@
+import * as RA from 'ramda-adjunct';
+
+RA.divideNum(2, 1); // $ExpectType number
+RA.divideNum(2.0, 1.0); // $ExpectType number
+RA.divideNum(0, 2.0); // $ExpectType number
+RA.divideNum(0.0, 2.0); // $ExpectType number
+RA.divideNum(-0.0, 2.0); // $ExpectType number
+
+RA.divideNum(2)(1); // $ExpectType number
+RA.divideNum(2.0)(1.0); // $ExpectType number
+RA.divideNum(0)(2.0); // $ExpectType number
+RA.divideNum(0.0)(2.0); // $ExpectType number
+RA.divideNum(-0.0)(2.0); // $ExpectType number
diff --git a/types/test/isSymbol.ts b/types/test/isSymbol.ts
new file mode 100644
index 0000000000..a9a5c15df2
--- /dev/null
+++ b/types/test/isSymbol.ts
@@ -0,0 +1,13 @@
+import * as RA from 'ramda-adjunct';
+
+RA.isSymbol(undefined); // $ExpectType boolean
+RA.isSymbol(null); // $ExpectType boolean
+RA.isSymbol(() => {}); // $ExpectType boolean
+RA.isSymbol(NaN); // $ExpectType boolean
+RA.isSymbol(true); // $ExpectType boolean
+RA.isSymbol('string'); // $ExpectType boolean
+RA.isSymbol(1); // $ExpectType boolean
+RA.isSymbol(Symbol('any')); // $ExpectType boolean
+RA.isSymbol(Symbol(1)); // $ExpectType boolean
+RA.isSymbol(Symbol()); // $ExpectType boolean
+RA.isSymbol(Symbol.iterator); // $ExpectType boolean
diff --git a/types/test/subtractValue.ts b/types/test/subtractValue.ts
new file mode 100644
index 0000000000..125bc81e98
--- /dev/null
+++ b/types/test/subtractValue.ts
@@ -0,0 +1,9 @@
+import * as RA from 'ramda-adjunct';
+
+RA.subtractNum(3, 5); // $ExpectType number
+RA.subtractNum(5, 3); // $ExpectType number
+RA.subtractNum(3, 'foo'); // $ExpectError
+
+RA.subtractNum(3)(5); // $ExpectType number
+RA.subtractNum(5)(3); // $ExpectType number
+RA.subtractNum(3)('foo'); // $ExpectError