Repository: char0n/ramda-adjunct (js)

Issue:
padEnd
**Is your feature request related to a problem? Please describe.**

This 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.

**Describe the solution you'd like**

```js
RA.padEnd(3, 'a'); // => 'a  '
```
The function is of arity=2 and is autocurried.


**Additional context**

This function is just a specialization of [padCharsEnd](https://github.com/char0n/ramda-adjunct/issues/1104) and should/must use this function to implement it's behavior.

[This PR](https://github.com/char0n/ramda-adjunct/pull/1067) should serve as a model template of PR for `padEnd` function. 

Pull request description:
feat(string): add padEnd
Closes #1105



Patch:
diff --git a/package-lock.json b/package-lock.json
index a5b79626be..4eda9cdb9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12186,9 +12186,9 @@
       }
     },
     "terser-webpack-plugin": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.2.0.tgz",
-      "integrity": "sha512-Sv2tyWCEXSi7aHCBOnigWMIFYHScftKytogG08RCmusPRkYMRCxyq2rVdy7s1ahEE13Yoz2ddgPsNU6axP5LJg==",
+      "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",
diff --git a/package.json b/package.json
index d8f95eed07..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.2.0",
+    "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 238ced776b..b1b9a35032 100644
--- a/src/index.js
+++ b/src/index.js
@@ -199,5 +199,6 @@ 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.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/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/types/index.d.ts b/types/index.d.ts
index dcc3de9743..7bca209c16 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1210,9 +1210,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;
         };
@@ -1268,6 +1268,14 @@ declare namespace RamdaAdjunct {
         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/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/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