Repository: char0n/ramda-adjunct (js)

Issue:
unzipObjWith
To form a partial isomorphism with `R.zipObj` the transformation needs to look like this:

```js
const patient = {
  'abbrev@1':{
    version:'1.0.9',
    resolved:'https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135'
  },
  'shell-quote@git+https://github.com/srghma/node-shell-quote.git#without_unlicenced_jsonify':{
    version:'1.6.0',
    resolved:'git+https://github.com/srghma/node-shell-quote.git#0aa381896e0cd7409ead15fd444f225807a61e0a'
  },
  '@graphile/plugin-supporter@git+https://1234user:1234pass@git.graphile.com/git/users/1234user/postgraphile-supporter.git':{
    version:'1.6.0',
    resolved:'git+https://1234user:1234pass@git.graphile.com/git/users/1234user/postgraphile-supporter.git#1234commit'
  },
}

const result = unzipObjWith((value, key) => [key, { ...value, name: key }]), patient)

const expected = [
  [
    'abbrev@1', 
    'shell-quote@git+https://github.com/srghma/node-shell-quote.git#without_unlicenced_jsonify',
    '@graphile/plugin-supporter@git+https://1234user:1234pass@git.graphile.com/git/users/1234user/postgraphile-supporter.git',
 ],
 [
  {
    name: 'abbrev@1',
    version:'1.0.9',
    resolved:'https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135'
  },
  {
    name: 'shell-quote@git+https://github.com/srghma/node-shell-quote.git#without_unlicenced_jsonify',
    version:'1.6.0',
    resolved:'git+https://github.com/srghma/node-shell-quote.git#0aa381896e0cd7409ead15fd444f225807a61e0a'
  },
  {
    name:   '@graphile/plugin-supporter@git+https://1234user:1234pass@git.graphile.com/git/users/1234user/postgraphile-supporter.git',
    version:'1.6.0',
    resolved:'git+https://1234user:1234pass@git.graphile.com/git/users/1234user/postgraphile-supporter.git#1234commit'
  },
 ]
]

expect(result).toEqual(result)
```

So you'll need to do `expected[1]` to get what you need. I like that this is generic concept which plays with the rest of the `zipping` algos. 

Pull request:
feat(object): add unzipObjWith
Closes #801


Patch:
diff --git a/package-lock.json b/package-lock.json
index 413b2efdb7..4eda9cdb9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12186,18 +12186,18 @@
       }
     },
     "terser-webpack-plugin": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.1.3.tgz",
-      "integrity": "sha512-z5Utx0TxmirZvRNL1GC795tlDM+bO83ZfcbtkL1y1VLoWtZ7S2a9+HFCLnabSRE/Yjsu4zCEX6U6CIRo4dVmcQ==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.2.1.tgz",
+      "integrity": "sha512-jwdauV5Al7zopR6OAYvIIRcxXCSvLjZjr7uZE8l2tIWb/ryrGN48sJftqGf5k9z09tWhajx53ldp0XPI080YnA==",
       "dev": true,
       "requires": {
         "cacache": "^13.0.1",
         "find-cache-dir": "^3.0.0",
         "jest-worker": "^24.9.0",
-        "schema-utils": "^2.4.1",
+        "schema-utils": "^2.5.0",
         "serialize-javascript": "^2.1.0",
         "source-map": "^0.6.1",
-        "terser": "^4.3.8",
+        "terser": "^4.3.9",
         "webpack-sources": "^1.4.3"
       },
       "dependencies": {
@@ -12239,12 +12239,6 @@
             "unique-filename": "^1.1.1"
           }
         },
-        "commander": {
-          "version": "2.20.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz",
-          "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==",
-          "dev": true
-        },
         "find-cache-dir": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz",
@@ -12339,9 +12333,9 @@
           }
         },
         "schema-utils": {
-          "version": "2.4.1",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.4.1.tgz",
-          "integrity": "sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==",
+          "version": "2.5.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.5.0.tgz",
+          "integrity": "sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ==",
           "dev": true,
           "requires": {
             "ajv": "^6.10.2",
@@ -12370,17 +12364,6 @@
             "minipass": "^3.0.0"
           }
         },
-        "terser": {
-          "version": "4.3.8",
-          "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.8.tgz",
-          "integrity": "sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ==",
-          "dev": true,
-          "requires": {
-            "commander": "^2.20.0",
-            "source-map": "~0.6.1",
-            "source-map-support": "~0.5.12"
-          }
-        },
         "yallist": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
diff --git a/package.json b/package.json
index 73bbd026f4..4558b35620 100644
--- a/package.json
+++ b/package.json
@@ -173,7 +173,7 @@
     "rimraf": "3.0.0",
     "sanctuary-show": "=1.0.0",
     "sinon": "=7.5.0",
-    "terser-webpack-plugin": "2.1.3",
+    "terser-webpack-plugin": "2.2.1",
     "testem": "3.0.0",
     "tslint": "=5.14.0",
     "typescript": "=3.5.3",
diff --git a/src/index.js b/src/index.js
index 6c210e0b67..b99ab646ae 100644
--- a/src/index.js
+++ b/src/index.js
@@ -162,6 +162,7 @@ export { default as spreadProp } from './spreadProp';
 export { default as spreadPath } from './spreadPath';
 export { default as flattenProp } from './flattenProp';
 export { default as flattenPath } from './flattenPath';
+export { default as unzipObjWith } from './unzipObjWith';
 // Relation
 export { default as lensEq } from './lensEq';
 export { default as lensNotEq } from './lensNotEq';
@@ -196,5 +197,9 @@ export { default as replaceAll } from './replaceAll';
 export { default as escapeRegExp } from './escapeRegExp';
 export { default as trimStart } from './trimStart';
 export { default as trimLeft } from './trimStart'; // alias of trimStart
+export { default as trimEnd } from './trimEnd';
+export { default as trimRight } from './trimEnd'; // alias of trimEnd
+export { default as padCharsEnd } from './padCharsEnd';
+export { default as padEnd } from './padEnd';
 // Types
 export { default as Identity } from './fantasy-land/Identity';
diff --git a/src/internal/polyfills/String.padCharsEnd.js b/src/internal/polyfills/String.padCharsEnd.js
new file mode 100644
index 0000000000..824a0b83d5
--- /dev/null
+++ b/src/internal/polyfills/String.padCharsEnd.js
@@ -0,0 +1,25 @@
+import isFunction from '../../isFunction';
+import repeat from './String.repeat';
+
+const padCharsEndPolyfill = (padString, targetLength, value) => {
+  // eslint-disable-next-line no-bitwise
+  let finalLength = targetLength >> 0;
+  let finalPadString = String(
+    typeof padString !== 'undefined' ? padString : ' '
+  );
+
+  if (value.length > finalLength) {
+    return String(value);
+  }
+  finalLength -= value.length;
+  if (finalLength > finalPadString.length) {
+    const remainingLength = finalLength / finalPadString.length;
+    finalPadString += isFunction(String.prototype.repeat)
+      ? finalPadString.repeat(remainingLength)
+      : repeat(finalPadString, remainingLength);
+  }
+
+  return String(value) + finalPadString.slice(0, finalLength);
+};
+
+export default padCharsEndPolyfill;
diff --git a/src/internal/polyfills/String.replaceAll.js b/src/internal/polyfills/String.replaceAll.js
new file mode 100644
index 0000000000..9193a3a7da
--- /dev/null
+++ b/src/internal/polyfills/String.replaceAll.js
@@ -0,0 +1,50 @@
+const checkArguments = (searchValue, replaceValue, str) => {
+  if (str == null || searchValue == null || replaceValue == null) {
+    throw TypeError('Input values must not be `null` or `undefined`');
+  }
+};
+
+const checkValue = (value, valueName) => {
+  if (typeof value !== 'string') {
+    if (!(value instanceof String)) {
+      throw TypeError(`\`${valueName}\` must be a string`);
+    }
+  }
+};
+
+const checkSearchValue = searchValue => {
+  if (
+    typeof searchValue !== 'string' &&
+    !(searchValue instanceof String) &&
+    !(searchValue instanceof RegExp)
+  ) {
+    throw TypeError('`searchValue` must be a string or an regexp');
+  }
+};
+
+const returnResult = (searchValue, replaceValue, str) => {
+  // searchValue is an empty string
+  if (searchValue === '') return str.replace(/(?:)/g, replaceValue);
+
+  // searchValue is a global regexp
+  if (searchValue instanceof RegExp) {
+    if (searchValue.flags.indexOf('g') === -1) {
+      throw TypeError('`.replaceAll` does not allow non-global regexes');
+    }
+    return str.replace(searchValue, replaceValue);
+  }
+
+  // common case
+  return str.split(searchValue).join(replaceValue);
+};
+
+const replaceAll = (searchValue, replaceValue, str) => {
+  checkArguments(searchValue, replaceValue, str);
+  checkValue(str, 'str');
+  checkValue(replaceValue, 'replaceValue');
+  checkSearchValue(searchValue);
+
+  return returnResult(searchValue, replaceValue, str);
+};
+
+export default replaceAll;
diff --git a/src/internal/polyfills/String.trimEnd.js b/src/internal/polyfills/String.trimEnd.js
new file mode 100644
index 0000000000..4c5e20b90d
--- /dev/null
+++ b/src/internal/polyfills/String.trimEnd.js
@@ -0,0 +1,5 @@
+import { replace } from 'ramda';
+
+const trimStart = replace(/\s+$/, '');
+
+export default trimStart;
diff --git a/src/padCharsEnd.js b/src/padCharsEnd.js
new file mode 100644
index 0000000000..797e4ec83b
--- /dev/null
+++ b/src/padCharsEnd.js
@@ -0,0 +1,37 @@
+import { curry, invoker, flip } from 'ramda';
+
+import polyfill from './internal/polyfills/String.padCharsEnd';
+import isFunction from './isFunction';
+
+export const padCharsEndStrPolyfill = curry(polyfill);
+
+export const padCharsEndStrInvoker = flip(invoker(2, 'padEnd'));
+
+/**
+ * The function pads the current string with a given string
+ * (repeated, if needed) so that the resulting string reaches a given length.
+ * The padding is applied from the end of the current string.
+ *
+ * @func padCharsEnd
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category String
+ * @sig String -> Number -> String -> String
+ * @param {string} padString The string to pad the current string with
+ * @param {number} targetLength The length of the resulting string once
+ * the current string has been padded
+ * @param {string} value String value to be padded
+ * @return {string} A new string of the specified length with the pad string
+ * applied at the end of the current string
+ * @see {@link RA.padEnd|padEnd}, {@link RA.padCharsStart|padCharsStart}, {@link RA.padStart|padStart}
+ * @example
+ *
+ * RA.padCharsEnd('-', 3, 'a'); // => 'a--'
+ * RA.padCharsEnd('foo', 10, 'abc'); // => 'abcfoofoof'
+ * RA.padCharsEnd('123456', 6, 'abc'); // => 'abc123'
+ */
+const padCharsEnd = isFunction(String.prototype.padEnd)
+  ? padCharsEndStrInvoker
+  : padCharsEndStrPolyfill;
+
+export default padCharsEnd;
diff --git a/src/padEnd.js b/src/padEnd.js
new file mode 100644
index 0000000000..bc31e06974
--- /dev/null
+++ b/src/padEnd.js
@@ -0,0 +1,25 @@
+import padCharsEnd from './padCharsEnd';
+
+/**
+ * The function pads the current string with an empty string
+ * so that the resulting string reaches a given length.
+ * The padding is applied from the end of the current string.
+ *
+ * @func padEnd
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category String
+ * @sig Number -> String -> String
+ * @param {number} targetLength The length of the resulting string once
+ * the current string has been padded
+ * @param {string} value String value to be padded
+ * @return {string} A new string of the specified length with the pad string
+ * applied at the end of the current string
+ * @see {@link RA.padCharsEnd|padCharsEnd}, {@link RA.padCharsStart|padCharsStart}, {@link RA.padStart|padStart}
+ * @example
+ *
+ * RA.padEnd(3, 'a'); // => 'a  '
+ */
+const padEnd = padCharsEnd(' ');
+
+export default padEnd;
diff --git a/src/replaceAll.js b/src/replaceAll.js
index 5efb47a31d..9b1f5d5684 100644
--- a/src/replaceAll.js
+++ b/src/replaceAll.js
@@ -1,6 +1,11 @@
-import { curryN, split, join, pipe } from 'ramda';
+import { curryN, invoker } from 'ramda';
 
-import isRegExp from './isRegExp';
+import isFunction from './isFunction';
+import polyfill from './internal/polyfills/String.replaceAll';
+
+export const replaceAllPolyfill = curryN(3, polyfill);
+
+export const replaceAllInvoker = invoker(2, 'replaceAll');
 
 /**
  * Replaces all substring matches in a string with a replacement.
@@ -10,25 +15,22 @@ import isRegExp from './isRegExp';
  * @since {@link https://char0n.github.io/ramda-adjunct/2.17.0|v2.17.0}
  * @category String
  * @sig String -> String -> String -> String
- * @param {string} searchValue The substring to match
+ * @param {string} searchValue The substring or a global RegExp to match
  * @param {string} replaceValue The string to replace the matches with
  * @param {string} str The String to do the search and replacement in
  * @return {string} A new string containing all the `searchValue` replaced with the `replaceValue`
- * @throws {Error} When `searchValue` is RegExp
+ * @throws {TypeError} When invalid arguments provided
  * @see {@link http://ramdajs.com/docs/#replace|R.replace}, {@link https://github.com/tc39/proposal-string-replaceall|TC39 proposal}
  * @example
  *
  * RA.replaceAll('ac', 'ef', 'ac ab ac ab'); //=> 'ef ab ef ab'
+ * RA.replaceAll('', '_', 'xxx'); //=> '_x_x_x_'
+ * RA.replaceAll(/x/g, 'v', 'xxx'); //=> 'vvv'
+ * RA.replaceAll(/x/, 'v', 'xxx'); //=> TypeError
  */
-const replaceAll = curryN(3, (searchValue, replaceValue, str) => {
-  if (isRegExp(searchValue)) {
-    throw new Error('searchValue must be a String, not a RegExp.');
-  }
 
-  return pipe(
-    split(String(searchValue)),
-    join(String(replaceValue))
-  )(str);
-});
+const replaceAll = isFunction(String.prototype.replaceAll)
+  ? replaceAllInvoker
+  : replaceAllPolyfill;
 
 export default replaceAll;
diff --git a/src/trimEnd.js b/src/trimEnd.js
new file mode 100644
index 0000000000..7d3ebb4d3f
--- /dev/null
+++ b/src/trimEnd.js
@@ -0,0 +1,30 @@
+import { invoker } from 'ramda';
+
+import polyfill from './internal/polyfills/String.trimEnd';
+import isFunction from './isFunction';
+
+export const trimEndPolyfill = polyfill;
+
+export const trimEndInvoker = invoker(0, 'trimEnd');
+
+/**
+ * Removes whitespace from the end of a string.
+ *
+ * @func trimEnd
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @category String
+ * @sig String -> String
+ * @param {string} value String value to have the whitespace removed from the end
+ * @return {string} A new string representing the calling string stripped of whitespace from its end (right end).
+ * @see {@link RA.trimEnd|trimEnd}
+ * @example
+ *
+ * RA.trimEnd('abc   '); //=> 'abc'
+ */
+
+const trimEnd = isFunction(String.prototype.trimEnd)
+  ? trimEndInvoker
+  : trimEndPolyfill;
+
+export default trimEnd;
diff --git a/src/unzipObjWith.js b/src/unzipObjWith.js
new file mode 100644
index 0000000000..0741b25d1a
--- /dev/null
+++ b/src/unzipObjWith.js
@@ -0,0 +1,45 @@
+import {
+  apply,
+  curryN,
+  flip,
+  map,
+  pipe,
+  toPairs,
+  transpose,
+  when,
+} from 'ramda';
+
+import lengthEq from './lengthEq';
+
+/**
+ * Creates a new list out of the supplied object by applying the function to each key/value pairing.
+ *
+ * @func unzipObjWith
+ * @memberOf RA
+ * @category Object
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.22.0|v2.22.0}
+ * @sig  (v, k) => [k, v] -> { k: v } -> [[k], [v]]
+ * @param {Function} fn The function to transform each value-key pair
+ * @param {Object} obj Object to unzip
+ * @return {Array} A pair of tw lists: a list of keys and a list of values
+ * @see {@link https://ramdajs.com/docs/#zipObj|zipObj}, {@link RA.zipObjWith|zipObjWith}
+ * @example
+ *
+ * RA.unzipObjWith((v, k) => [`new${k.toUpperCase()}`, 2 * v], { a: 1, b: 2, c: 3 });
+ * //=> [['newA', 'newB', 'newC'], [2, 4, 6]]
+ */
+const unzipObjWith = curryN(2, (fn, obj) =>
+  pipe(
+    toPairs,
+    map(
+      pipe(
+        flip,
+        apply
+      )(fn)
+    ),
+    transpose,
+    when(lengthEq(0), () => [[], []])
+  )(obj)
+);
+
+export default unzipObjWith;
diff --git a/types/index.d.ts b/types/index.d.ts
index 990271a80c..191c3f73de 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -820,6 +820,12 @@ declare namespace RamdaAdjunct {
         flattenProp(prop: string | number, obj: object): object;
         flattenProp(prop: string | number): (obj: object) => object;
 
+        /**
+         * Creates a new list out of the supplied object by applying the function to each key/value pairing.
+         */
+        unzipObjWith<T, U, V>(fn: (v: T, k: string) => [U, V], obj: { [k: string]: T }): [U[], V[]];
+        unzipObjWith<T, U, V>(fn: (v: T, k: string) => [U, V]): (obj: { [k: string]: T }) => [U[], V[]];
+
         /**
          * Composable shortcut for `Promise.all`.
          *
@@ -1210,9 +1216,9 @@ declare namespace RamdaAdjunct {
         /**
          * Replace all substring matches in a string with a replacement.
          */
-        replaceAll(searchValue: string, replaceValue: string, str: string): string;
-        replaceAll(searchValue: string): (replaceValue: string, str: string) => string;
-        replaceAll(searchValue: string): {
+        replaceAll(searchValue: string | RegExp, replaceValue: string, str: string): string;
+        replaceAll(searchValue: string | RegExp): (replaceValue: string, str: string) => string;
+        replaceAll(searchValue: string | RegExp): {
             (replaceValue: string, str: string): string;
             (replaceValue: string): (str: string) => string;
         };
@@ -1253,6 +1259,29 @@ declare namespace RamdaAdjunct {
          */
         trimStart(value: string): string;
         trimLeft(value: string): string;
+
+        /**
+         * Removes whitespace from the end of a string.
+         */
+        trimEnd(value: string): string;
+        trimRight(value: string): string;
+
+        /**
+         * The function pads the current string with a given string
+         * (repeated, if needed) so that the resulting string reaches a given length.
+         * The padding is applied from the end of the current string.
+         */
+        padCharsEnd(padString: string, targetLength: number, value: string): string;
+        padCharsEnd(padString: string, targetLength: number): (value: string) => string;
+        padCharsEnd(padString: string): (targetLength: number, value: string) => string;
+
+        /**
+         * The function pads the current string with an empty string
+         * so that the resulting string reaches a given length.
+         * The padding is applied from the end of the current string.
+         */
+        padEnd(targetLength: number, value: string): string;
+        padEnd(targetLength: number): (value: string) => string;
     }
 }
 


Test patch:
diff --git a/test/padCharsEnd.js b/test/padCharsEnd.js
new file mode 100644
index 0000000000..952aa84cb0
--- /dev/null
+++ b/test/padCharsEnd.js
@@ -0,0 +1,213 @@
+import * as RA from '../src';
+import {
+  padCharsEndStrInvoker,
+  padCharsEndStrPolyfill,
+} from '../src/padCharsEnd';
+import eq from './shared/eq';
+
+describe('padCharsEnd', function() {
+  specify('should pad string with given padString', function() {
+    eq(RA.padCharsEnd('-', 3, 'a'), 'a--');
+  });
+
+  context('given targetLength as 0', function() {
+    specify('should return original string', function() {
+      eq(RA.padCharsEnd('$', 0, 'abc'), 'abc');
+    });
+  });
+
+  context('given targetLength less than string length', function() {
+    specify('should return original string', function() {
+      eq(RA.padCharsEnd('$', 1, 'abc'), 'abc');
+    });
+  });
+
+  context('given targetLength less than equal to padString length', function() {
+    specify('should trim padString', function() {
+      eq(RA.padCharsEnd('123456', 6, 'abc'), 'abc123');
+      eq(RA.padCharsEnd('123456', 5, 'abc'), 'abc12');
+    });
+  });
+
+  context('given targetLength supplied as float', function() {
+    specify('should convert targetLength to integer', function() {
+      eq(RA.padCharsEnd('$', 7.5, 'abc'), 'abc$$$$');
+    });
+  });
+
+  context(
+    'given targetLength is a value coercible to valid integer number',
+    function() {
+      specify('should return padded string', function() {
+        eq(RA.padCharsEnd('*', '5', 'abc'), 'abc**');
+      });
+    }
+  );
+
+  context('given targeLength is a negative number', function() {
+    specify('should return original string', function() {
+      eq(RA.padCharsEnd('$', -10, 'abc'), 'abc');
+    });
+  });
+
+  context('given padString set to undefined', function() {
+    specify('should return string padded with spaces', function() {
+      eq(RA.padCharsEnd(undefined, 5, '200'), '200  ');
+    });
+  });
+
+  context('given null padString', function() {
+    specify('should convert null to string type', function() {
+      eq(RA.padCharsEnd(null, 5, 'abc'), 'abcnu');
+    });
+  });
+
+  specify('should be curried', function() {
+    eq(RA.padCharsEnd('*', 5, 'abc'), 'abc**');
+    eq(RA.padCharsEnd('*', 5)('abc'), 'abc**');
+    eq(RA.padCharsEnd('*')(5, 'abc'), 'abc**');
+    eq(RA.padCharsEnd('*')(5)('abc'), 'abc**');
+  });
+
+  context('padCharsEndStrInvoker', function() {
+    beforeEach(function() {
+      if (RA.isNotFunction(String.prototype.padEnd)) {
+        this.skip();
+      }
+    });
+
+    specify('should pad string with given padString', function() {
+      eq(padCharsEndStrInvoker('-', 3, 'a'), 'a--');
+    });
+
+    context('given targetLength as 0', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrInvoker('$', 0, 'abc'), 'abc');
+      });
+    });
+
+    context('given targetLength less than string length', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrInvoker('$', 1, 'abc'), 'abc');
+      });
+    });
+
+    context(
+      'given targetLength less than equal to padString length',
+      function() {
+        specify('should trim padString', function() {
+          eq(padCharsEndStrInvoker('123456', 6, 'abc'), 'abc123');
+          eq(padCharsEndStrInvoker('123456', 5, 'abc'), 'abc12');
+        });
+      }
+    );
+
+    context('given targetLength supplied as float', function() {
+      specify('should convert targetLength to integer', function() {
+        eq(padCharsEndStrInvoker('$', 7.5, 'abc'), 'abc$$$$');
+      });
+    });
+
+    context(
+      'given targetLength is a value coercible to valid integer number',
+      function() {
+        specify('should return padded string', function() {
+          eq(padCharsEndStrInvoker('*', '5', 'abc'), 'abc**');
+        });
+      }
+    );
+
+    context('given targeLength is a negative number', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrInvoker('$', -10, 'abc'), 'abc');
+      });
+    });
+
+    context('given padString set to undefined', function() {
+      specify('should return string padded with spaces', function() {
+        eq(padCharsEndStrInvoker(undefined, 5, '200'), '200  ');
+      });
+    });
+
+    context('given null padString', function() {
+      specify('should convert null to string type', function() {
+        eq(padCharsEndStrInvoker(null, 5, 'abc'), 'abcnu');
+      });
+    });
+
+    specify('should be curried', function() {
+      eq(padCharsEndStrInvoker('*', 5, 'abc'), 'abc**');
+      eq(padCharsEndStrInvoker('*', 5)('abc'), 'abc**');
+      eq(padCharsEndStrInvoker('*')(5, 'abc'), 'abc**');
+      eq(padCharsEndStrInvoker('*')(5)('abc'), 'abc**');
+    });
+  });
+
+  context('padCharsEndStrPolyfill', function() {
+    specify('should pad string with given padString', function() {
+      eq(padCharsEndStrPolyfill('-', 3, 'a'), 'a--');
+    });
+
+    context('given targetLength as 0', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrPolyfill('$', 0, 'abc'), 'abc');
+      });
+    });
+
+    context('given targetLength less than string length', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrPolyfill('$', 1, 'abc'), 'abc');
+      });
+    });
+
+    context(
+      'given targetLength less than equal to padString length',
+      function() {
+        specify('should trim padString', function() {
+          eq(padCharsEndStrPolyfill('123456', 6, 'abc'), 'abc123');
+          eq(padCharsEndStrPolyfill('123456', 5, 'abc'), 'abc12');
+        });
+      }
+    );
+
+    context('given targetLength supplied as float', function() {
+      specify('should convert targetLength to integer', function() {
+        eq(padCharsEndStrPolyfill('$', 7.5, 'abc'), 'abc$$$$');
+      });
+    });
+
+    context(
+      'given targetLength is a value coercible to valid integer number',
+      function() {
+        specify('should return padded string', function() {
+          eq(padCharsEndStrPolyfill('*', '5', 'abc'), 'abc**');
+        });
+      }
+    );
+
+    context('given targeLength is a negative number', function() {
+      specify('should return original string', function() {
+        eq(padCharsEndStrPolyfill('$', -10, 'abc'), 'abc');
+      });
+    });
+
+    context('given padString set to undefined', function() {
+      specify('should return string padded with spaces', function() {
+        eq(padCharsEndStrPolyfill(undefined, 5, '200'), '200  ');
+      });
+    });
+
+    context('given null padString', function() {
+      specify('should convert null to string type', function() {
+        eq(padCharsEndStrPolyfill(null, 5, 'abc'), 'abcnu');
+      });
+    });
+
+    specify('should be curried', function() {
+      eq(padCharsEndStrPolyfill('*', 5, 'abc'), 'abc**');
+      eq(padCharsEndStrPolyfill('*', 5)('abc'), 'abc**');
+      eq(padCharsEndStrPolyfill('*')(5, 'abc'), 'abc**');
+      eq(padCharsEndStrPolyfill('*')(5)('abc'), 'abc**');
+    });
+  });
+});
diff --git a/test/padEnd.js b/test/padEnd.js
new file mode 100644
index 0000000000..c13c5d772c
--- /dev/null
+++ b/test/padEnd.js
@@ -0,0 +1,46 @@
+import * as RA from '../src';
+import eq from './shared/eq';
+
+describe('padEnd', function() {
+  specify('should pad string with spaces', function() {
+    eq(RA.padEnd(3, 'a'), 'a  ');
+  });
+
+  context('given targetLength as 0', function() {
+    specify('should return original string', function() {
+      eq(RA.padEnd(0, 'abc'), 'abc');
+    });
+  });
+
+  context('given targetLength less than string length', function() {
+    specify('should return original string', function() {
+      eq(RA.padEnd(1, 'abc'), 'abc');
+    });
+  });
+
+  context('given targetLength supplied as float', function() {
+    specify('should convert targetLength to integer', function() {
+      eq(RA.padEnd(7.5, 'abc'), 'abc    ');
+    });
+  });
+
+  context(
+    'given targetLength is a value coercible to valid integer number',
+    function() {
+      specify('should return padded string', function() {
+        eq(RA.padEnd('5', 'abc'), 'abc  ');
+      });
+    }
+  );
+
+  context('given targeLength is a negative number', function() {
+    specify('should return original string', function() {
+      eq(RA.padEnd(-10, 'abc'), 'abc');
+    });
+  });
+
+  specify('should be curried', function() {
+    eq(RA.padEnd(5, 'abc'), 'abc  ');
+    eq(RA.padEnd(5)('abc'), 'abc  ');
+  });
+});
diff --git a/test/replaceAll.js b/test/replaceAll.js
index e50e809524..4e12413725 100644
--- a/test/replaceAll.js
+++ b/test/replaceAll.js
@@ -1,6 +1,7 @@
 import { assert } from 'chai';
 
 import * as RA from '../src';
+import { replaceAllPolyfill } from '../src/replaceAll';
 
 describe('replaceAll', function() {
   it('should replace all matches', function() {
@@ -11,12 +12,6 @@ describe('replaceAll', function() {
     assert.strictEqual(actual, expected);
   });
 
-  context('given searchValue is RegExp', function() {
-    specify('should throw Error', function() {
-      assert.throws(() => RA.replaceAll(/a/g, 'c', 'abc'));
-    });
-  });
-
   context('given empty string', function() {
     specify('should return original value', function() {
       assert.strictEqual(RA.replaceAll('a', 'c', ''), '');
@@ -47,4 +42,73 @@ describe('replaceAll', function() {
     assert.strictEqual(RA.replaceAll('a', 'c')('aba'), 'cbc');
     assert.strictEqual(RA.replaceAll('a')('c')('aba'), 'cbc');
   });
+
+  context('given searchValue is a non-global RegExp', function() {
+    specify('should throw Error', function() {
+      assert.throws(() => RA.replaceAll(/a/, 'c', 'abc'), TypeError);
+    });
+  });
+});
+
+describe('replaceAllPolyfill', function() {
+  context('given searchValue is a non-global RegExp', function() {
+    specify('should throw Error', function() {
+      assert.throws(() => replaceAllPolyfill(/a/, 'c', 'abc'), TypeError);
+    });
+  });
+
+  it('should support global RegExp searchValue', function() {
+    const value = 'xxx';
+    const actual = replaceAllPolyfill(/x/g, 'v', value);
+    const expected = 'vvv';
+
+    assert.strictEqual(actual, expected);
+  });
+
+  it('should support empty searchValue', function() {
+    const value = 'xxx';
+    const actual = replaceAllPolyfill('', '_', value);
+    const expected = '_x_x_x_';
+
+    assert.strictEqual(actual, expected);
+  });
+
+  it('should replace all matches', function() {
+    const value = 'ab cd ab cd ab cd';
+    const actual = replaceAllPolyfill('ab', 'ef', value);
+    const expected = 'ef cd ef cd ef cd';
+
+    assert.strictEqual(actual, expected);
+  });
+
+  context('given empty string', function() {
+    specify('should return original value', function() {
+      assert.strictEqual(replaceAllPolyfill('a', 'c', ''), '');
+    });
+  });
+
+  it('should support boxing wrappers', function() {
+    const value = new String('ab cd ab cd ab cd');
+    const searchValue = new String('ab');
+    const replaceValue = new String('ef');
+    const actual = replaceAllPolyfill(searchValue, replaceValue, value);
+    const expected = 'ef cd ef cd ef cd';
+
+    assert.strictEqual(actual, expected);
+  });
+
+  it('should replace in very big strings', function() {
+    const bigString = 'ab cd'.repeat(10000);
+    const expected = 'ab ef'.repeat(10000);
+    const actual = replaceAllPolyfill('cd', 'ef', bigString);
+
+    assert.strictEqual(actual, expected);
+  });
+
+  it('should curry', function() {
+    assert.strictEqual(replaceAllPolyfill('a', 'c', 'aba'), 'cbc');
+    assert.strictEqual(replaceAllPolyfill('a')('c', 'aba'), 'cbc');
+    assert.strictEqual(replaceAllPolyfill('a', 'c')('aba'), 'cbc');
+    assert.strictEqual(replaceAllPolyfill('a')('c')('aba'), 'cbc');
+  });
 });
diff --git a/test/trimEnd.js b/test/trimEnd.js
new file mode 100644
index 0000000000..17d8466294
--- /dev/null
+++ b/test/trimEnd.js
@@ -0,0 +1,110 @@
+import * as R from 'ramda';
+
+import * as RA from '../src';
+import { trimEndInvoker, trimEndPolyfill } from '../src/trimEnd';
+import eq from './shared/eq';
+
+describe('trimEnd', function() {
+  specify('should remove whitespace from the end of a string', function() {
+    eq(RA.trimEnd('abc   '), 'abc');
+  });
+
+  specify(
+    'should not remove whitespace from the rest of the string',
+    function() {
+      eq(RA.trimEnd(' a b c'), ' a b c');
+    }
+  );
+
+  context('given an empty string', function() {
+    specify('should return an empty string', function() {
+      eq(RA.trimEnd(''), '');
+    });
+  });
+
+  context('given an string with whitespaces only', function() {
+    specify('should return an empty string', function() {
+      eq(RA.trimEnd('   '), '');
+    });
+  });
+
+  specify('should support placeholder to specify "gaps"', function() {
+    const trimEnd = RA.trimEnd(R.__);
+
+    eq(trimEnd('abc   '), 'abc');
+  });
+
+  context('trimEndInvoker', function() {
+    beforeEach(function() {
+      if (RA.isNotFunction(String.prototype.trimEnd)) {
+        this.skip();
+      }
+    });
+
+    specify('should remove whitespace from the end of a string', function() {
+      eq(trimEndInvoker('abc   '), 'abc');
+    });
+
+    specify(
+      'should not remove whitespace from the rest of the string',
+      function() {
+        eq(trimEndInvoker(' a b c'), ' a b c');
+      }
+    );
+
+    context('given an empty string', function() {
+      specify('should return an empty string', function() {
+        eq(trimEndInvoker(''), '');
+      });
+    });
+
+    context('given an string with whitespaces only', function() {
+      specify('should return an empty string', function() {
+        eq(trimEndInvoker('   '), '');
+      });
+    });
+
+    specify('should support placeholder to specify "gaps"', function() {
+      const trimEnd = trimEndInvoker(R.__);
+
+      eq(trimEnd('abc   '), 'abc');
+    });
+  });
+
+  context('trimEndPolyfill', function() {
+    specify('should remove whitespace from the end of a string', function() {
+      eq(trimEndPolyfill('abc   '), 'abc');
+    });
+
+    specify(
+      'should not remove whitespace from the rest of the string',
+      function() {
+        eq(trimEndPolyfill(' a b c'), ' a b c');
+      }
+    );
+
+    context('given an empty string', function() {
+      specify('should return an empty string', function() {
+        eq(trimEndPolyfill(''), '');
+      });
+    });
+
+    context('given an string with whitespaces only', function() {
+      specify('should return an empty string', function() {
+        eq(trimEndPolyfill('   '), '');
+      });
+    });
+
+    specify('should support placeholder to specify "gaps"', function() {
+      const trimEnd = trimEndPolyfill(R.__);
+
+      eq(trimEnd('abc   '), 'abc');
+    });
+  });
+});
+
+describe('trimRight', function() {
+  it('should be alias of trimEnd', function() {
+    eq(RA.trimRight === RA.trimEnd, true);
+  });
+});
diff --git a/test/unzipObjWith.js b/test/unzipObjWith.js
new file mode 100644
index 0000000000..9e83519696
--- /dev/null
+++ b/test/unzipObjWith.js
@@ -0,0 +1,51 @@
+import { assert } from 'chai';
+import * as R from 'ramda';
+
+import * as RA from '../src';
+
+describe('unzipObjWith', function() {
+  it('should unzip an object into pair of lists', function() {
+    const fn = (value, key) => [key, { ...value, name: key }];
+    const obj = {
+      packageA: { version: '1.0.9' },
+      packageB: { version: '1.6.1' },
+      packageC: { version: '1.6.0' },
+    };
+
+    const actual = RA.unzipObjWith(fn, obj);
+
+    const expected = [
+      ['packageA', 'packageB', 'packageC'],
+      [
+        { name: 'packageA', version: '1.0.9' },
+        { name: 'packageB', version: '1.6.1' },
+        { name: 'packageC', version: '1.6.0' },
+      ],
+    ];
+
+    assert.deepEqual(actual, expected);
+  });
+
+  context('given empty object', function() {
+    specify('should return a pair of empty arrays', function() {
+      const fn = (value, key) => [key, value];
+      const obj = {};
+
+      const actual = RA.unzipObjWith(fn, obj);
+      const expected = [[], []];
+
+      assert.deepEqual(actual, expected);
+    });
+  });
+
+  it('should curry', function() {
+    const fn = (v, k) => [`new${k.toUpperCase()}`, 2 * v];
+    const obj = { a: 1, b: 2, c: 3 };
+    const expected = [['newA', 'newB', 'newC'], [2, 4, 6]];
+
+    assert.deepEqual(RA.unzipObjWith(fn, obj), expected);
+    assert.deepEqual(RA.unzipObjWith(fn)(obj), expected);
+    assert.deepEqual(RA.unzipObjWith()(fn)(obj), expected);
+    assert.deepEqual(RA.unzipObjWith(R.__, obj)(fn), expected);
+  });
+});
diff --git a/types/test/padCharsEnd.ts b/types/test/padCharsEnd.ts
new file mode 100644
index 0000000000..c38a42dba7
--- /dev/null
+++ b/types/test/padCharsEnd.ts
@@ -0,0 +1,10 @@
+import * as RA from 'ramda-adjunct';
+
+RA.padCharsEnd('-', 3, 'a'); // $ExpectType string
+RA.padCharsEnd('$', 5)('abc'); // $ExpectType string
+RA.padCharsEnd('*')(10, 'hello'); // $ExpectType string
+
+RA.padCharsEnd(1, 2, '3'); // $ExpectError
+RA.padCharsEnd('#', '10', 'afsd'); // $ExpectError
+RA.padCharsEnd(undefined, '12', 54); // $ExpectError
+RA.padCharsEnd(null, 12, 'hello'); // $ExpectError
diff --git a/types/test/padEnd.ts b/types/test/padEnd.ts
new file mode 100644
index 0000000000..e0616df01e
--- /dev/null
+++ b/types/test/padEnd.ts
@@ -0,0 +1,11 @@
+import * as RA from 'ramda-adjunct';
+
+RA.padEnd(3, 'a'); // $ExpectType string
+RA.padEnd(5)('abc'); // $ExpectType string
+RA.padEnd(10, 'hello'); // $ExpectType string
+
+RA.padEnd('10', 'afsd'); // $ExpectError
+RA.padEnd('12', 54); // $ExpectError
+RA.padEnd(null, 'a'); // $ExpectError
+RA.padEnd(undefined, 12); // $ExpectError
+RA.padEnd(undefined, 'abc'); // $ExpectError
diff --git a/types/test/replaceAll.ts b/types/test/replaceAll.ts
new file mode 100644
index 0000000000..7739bb5fda
--- /dev/null
+++ b/types/test/replaceAll.ts
@@ -0,0 +1,5 @@
+import * as RA from 'ramda-adjunct';
+
+RA.replaceAll('ab', 'ef', 'ab cd ab cd ab cd'); // $ExpectType string
+RA.replaceAll('', 'x', 'xxx'); // $ExpectType string
+RA.replaceAll(/x/g, 'v', 'xxx'); // $ExpectType string
diff --git a/types/test/trimEnd.ts b/types/test/trimEnd.ts
new file mode 100644
index 0000000000..ac20f41ad0
--- /dev/null
+++ b/types/test/trimEnd.ts
@@ -0,0 +1,20 @@
+import * as RA from 'ramda-adjunct';
+
+RA.trimEnd('abc'); // $ExpectType string
+RA.trimEnd(''); // $ExpectType string
+
+RA.trimEnd(1); // $ExpectError
+RA.trimEnd({}); // $ExpectError
+RA.trimEnd(null); // $ExpectError
+RA.trimEnd(undefined); // $ExpectError
+RA.trimEnd([]); // $ExpectError
+
+// alias
+RA.trimRight('abc'); // $ExpectType string
+RA.trimRight(''); // $ExpectType string
+
+RA.trimRight(1); // $ExpectError
+RA.trimRight({}); // $ExpectError
+RA.trimRight(null); // $ExpectError
+RA.trimRight(undefined); // $ExpectError
+RA.trimRight([]); // $ExpectError
diff --git a/types/test/unzipObjWith.ts b/types/test/unzipObjWith.ts
new file mode 100644
index 0000000000..db3ba4667e
--- /dev/null
+++ b/types/test/unzipObjWith.ts
@@ -0,0 +1,11 @@
+import * as RA from 'ramda-adjunct';
+
+RA.unzipObjWith((v, k) => [k, 1], { a: 1 }); // $ExpectType [string[], number[]]
+RA.unzipObjWith((v, k) => [2, 'hello'])({ a: 1 }); // $ExpectType [number[], string[]]
+RA.unzipObjWith((v, k) => [k, 1], {}); // $ExpectType [string[], number[]]
+RA.unzipObjWith<string, boolean, number>((v, k) => [true, 5], { a: 'b' }); // $ExpectType [boolean[], number[]]
+
+RA.unzipObjWith((v, k) => true, { a: 1 }); // $ExpectError
+RA.unzipObjWith((v, k) => [k, v], []); // $ExpectError
+RA.unzipObjWith<string, boolean, number>((v, k) => [k, v], { a: 'b' }); // $ExpectError
+RA.unzipObjWith((v, k) => [k, v], undefined); // $ExpectError