diff --git a/lib/node_modules/@stdlib/string/base/left-pad/lib/main.js b/lib/node_modules/@stdlib/string/base/left-pad/lib/main.js index d7c357cf4019..ff676f22a60e 100644 --- a/lib/node_modules/@stdlib/string/base/left-pad/lib/main.js +++ b/lib/node_modules/@stdlib/string/base/left-pad/lib/main.js @@ -21,16 +21,17 @@ // MODULES // var repeat = require( '@stdlib/string/base/repeat' ); +var numGraphemeClusters = require( '@stdlib/string/num-grapheme-clusters' ); var ceil = require( '@stdlib/math/base/special/ceil' ); // MAIN // /** -* Left pads a string such that the padded string has a length of at least `len`. +* Left pads a string such that the padded string has a length of at least `len` grapheme clusters (i.e., user-perceived characters). * * @param {string} str - string to pad -* @param {NonNegativeInteger} len - minimum string length +* @param {NonNegativeInteger} len - minimum string length (in grapheme clusters) * @param {string} pad - string used to pad * @returns {string} padded string * @@ -45,9 +46,14 @@ var ceil = require( '@stdlib/math/base/special/ceil' ); * @example * var str = lpad( 'boop', 12, 'beep' ); * // returns 'beepbeepboop' +* +* @example +* // Padding counts grapheme clusters, not UTF-16 code units, so a surrogate pair counts as one character: +* var str = lpad( '😀', 3, 'x' ); +* // returns 'xx😀' */ function lpad( str, len, pad ) { - var n = ( len - str.length ) / pad.length; + var n = ( len - numGraphemeClusters( str ) ) / numGraphemeClusters( pad ); if ( n <= 0 ) { return str; } diff --git a/lib/node_modules/@stdlib/string/base/left-pad/test/test.js b/lib/node_modules/@stdlib/string/base/left-pad/test/test.js index 99b3e0f5d0fa..433747ad7700 100644 --- a/lib/node_modules/@stdlib/string/base/left-pad/test/test.js +++ b/lib/node_modules/@stdlib/string/base/left-pad/test/test.js @@ -55,3 +55,21 @@ tape( 'if the specified string length is less than or equal to the input string t.strictEqual( lpad( 'boop', 4, 'beep' ), 'boop', 'returns input string (=)' ); t.end(); }); + +tape( 'the function measures string length in grapheme clusters, not UTF-16 code units, so a string containing a surrogate pair is treated as one character', function test( t ) { + var str = lpad( '😀', 3, 'x' ); + t.strictEqual( str, 'xx😀', 'pads based on grapheme cluster count' ); + t.end(); +}); + +tape( 'the function supports padding with a multi-code-unit pad string without splitting a surrogate pair', function test( t ) { + var str = lpad( 'a', 3, '😀' ); + t.strictEqual( str, '😀😀a', 'repeats the whole pad character' ); + t.end(); +}); + +tape( 'KNOWN LIMITATION: if the pad string is a bare Unicode combining mark, repeating it does not increase the grapheme cluster count (per UAX #29 GB9, consecutive Extend characters merge into one cluster), so the output may fall short of the requested length', function test( t ) { + var str = lpad( 'a', 5, '̈' ); // U+0308 COMBINING DIAERESIS + t.strictEqual( str, '̈̈̈̈a', 'pinned current behavior; result has only 2 grapheme clusters, not 5' ); + t.end(); +}); diff --git a/lib/node_modules/@stdlib/string/base/right-pad/lib/main.js b/lib/node_modules/@stdlib/string/base/right-pad/lib/main.js index f1e803efab2b..9285b62b4fd4 100644 --- a/lib/node_modules/@stdlib/string/base/right-pad/lib/main.js +++ b/lib/node_modules/@stdlib/string/base/right-pad/lib/main.js @@ -21,16 +21,17 @@ // MODULES // var repeat = require( '@stdlib/string/base/repeat' ); +var numGraphemeClusters = require( '@stdlib/string/num-grapheme-clusters' ); var ceil = require( '@stdlib/math/base/special/ceil' ); // MAIN // /** -* Right pads a string such that the padded string has a length of at least `len`. +* Right pads a string such that the padded string has a length of at least `len` grapheme clusters (i.e., user-perceived characters). * * @param {string} str - string to pad -* @param {NonNegativeInteger} len - minimum string length +* @param {NonNegativeInteger} len - minimum string length (in grapheme clusters) * @param {string} pad - string used to pad * @returns {string} padded string * @@ -45,9 +46,14 @@ var ceil = require( '@stdlib/math/base/special/ceil' ); * @example * var str = rpad( 'boop', 12, 'beep' ); * // returns 'boopbeepbeep' +* +* @example +* // Padding counts grapheme clusters, not UTF-16 code units, so a surrogate pair counts as one character: +* var str = rpad( '😀', 3, 'x' ); +* // returns '😀xx' */ function rpad( str, len, pad ) { - var n = ( len - str.length ) / pad.length; + var n = ( len - numGraphemeClusters( str ) ) / numGraphemeClusters( pad ); if ( n <= 0 ) { return str; } diff --git a/lib/node_modules/@stdlib/string/base/right-pad/test/test.js b/lib/node_modules/@stdlib/string/base/right-pad/test/test.js index f7321fc9b505..e27bd111d8fa 100644 --- a/lib/node_modules/@stdlib/string/base/right-pad/test/test.js +++ b/lib/node_modules/@stdlib/string/base/right-pad/test/test.js @@ -55,3 +55,21 @@ tape( 'if the specified string length is less than or equal to the input string t.strictEqual( rpad( 'boop', 4, 'beep' ), 'boop', 'returns input string (=)' ); t.end(); }); + +tape( 'the function measures string length in grapheme clusters, not UTF-16 code units, so a string containing a surrogate pair is treated as one character', function test( t ) { + var str = rpad( '😀', 3, 'x' ); + t.strictEqual( str, '😀xx', 'pads based on grapheme cluster count' ); + t.end(); +}); + +tape( 'the function supports padding with a multi-code-unit pad string without splitting a surrogate pair', function test( t ) { + var str = rpad( 'a', 3, '😀' ); + t.strictEqual( str, 'a😀😀', 'repeats the whole pad character' ); + t.end(); +}); + +tape( 'KNOWN LIMITATION: if the pad string is a bare Unicode combining mark, repeating it does not increase the grapheme cluster count (per UAX #29 GB9, consecutive Extend characters merge into the preceding cluster), so the output may fall short of the requested length', function test( t ) { + var str = rpad( 'a', 5, '̈' ); // U+0308 COMBINING DIAERESIS + t.strictEqual( str, 'ä̈̈̈', 'pinned current behavior; result has only 1 grapheme cluster, not 5' ); + t.end(); +}); diff --git a/lib/node_modules/@stdlib/string/left-pad/README.md b/lib/node_modules/@stdlib/string/left-pad/README.md index 45066f7970a1..d50613eb2717 100644 --- a/lib/node_modules/@stdlib/string/left-pad/README.md +++ b/lib/node_modules/@stdlib/string/left-pad/README.md @@ -32,13 +32,19 @@ var lpad = require( '@stdlib/string/left-pad' ); #### lpad( str, len\[, pad] ) -Left pads a string such that the padded string has a length of **at least** `len`. +Left pads a string such that the padded string has a length of **at least** `len` grapheme clusters (i.e., user-perceived characters, not UTF-16 code units). ```javascript var str = lpad( 'a', 5 ); // returns ' a' ``` +```javascript +// A surrogate pair (e.g., an emoji) counts as a single character: +var str = lpad( '😀', 3, 'x' ); +// returns 'xx😀' +``` + By default, an input string is padded with a Unicode "space" character (U+0020). To pad with a different character or sequence of characters, provide a `pad` string. ```javascript @@ -67,6 +73,8 @@ str = lpad( 'boop', 12, 'beep' ); // returns 'epbeepboop' ``` +- Length is measured in grapheme clusters. If `pad` is a string whose grapheme cluster count does not grow when repeated — a bare Unicode combining mark is the notable case, per [UAX #29][unicode-uax-29] a run of `Extend` characters with no preceding base character merges into a single cluster no matter how many times it repeats — the returned string may have fewer than `len` grapheme clusters. + - This function differs from [`String.prototype.padStart`][mdn-string-padstart] in the following ways: - The function is **not** guaranteed to return a string having a length exactly equal to `len` (as explained above). @@ -220,6 +228,8 @@ $ echo -n 'boop\tbeep' | lpad --len 8 --split '\t' [mdn-string-padstart]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart +[unicode-uax-29]: https://unicode.org/reports/tr29/ + [@stdlib/string/pad]: https://github.com/stdlib-js/stdlib/tree/develop/lib/node_modules/%40stdlib/string/pad diff --git a/lib/node_modules/@stdlib/string/left-pad/test/test.js b/lib/node_modules/@stdlib/string/left-pad/test/test.js index 8a1a40a23943..248c46e39b62 100644 --- a/lib/node_modules/@stdlib/string/left-pad/test/test.js +++ b/lib/node_modules/@stdlib/string/left-pad/test/test.js @@ -154,3 +154,9 @@ tape( 'if the specified string length is less than or equal to the input string t.strictEqual( lpad( 'boop', 4, 'beep' ), 'boop', 'returns input string (=)' ); t.end(); }); + +tape( 'the function measures string length in grapheme clusters, not UTF-16 code units, so a string containing a surrogate pair is treated as one character', function test( t ) { + var str = lpad( '😀', 3, 'x' ); + t.strictEqual( str, 'xx😀', 'pads based on grapheme cluster count' ); + t.end(); +}); diff --git a/lib/node_modules/@stdlib/string/pad/README.md b/lib/node_modules/@stdlib/string/pad/README.md index 1210f6ae4f14..36b16cdff017 100644 --- a/lib/node_modules/@stdlib/string/pad/README.md +++ b/lib/node_modules/@stdlib/string/pad/README.md @@ -38,13 +38,21 @@ var pad = require( '@stdlib/string/pad' ); #### pad( str, len\[, options] ) -Pads a `string` such that the padded `string` has a `length` of `len`. +Pads a `string` such that the padded `string` has a `length` of `len` grapheme clusters (i.e., user-perceived characters, not UTF-16 code units). ```javascript var str = pad( 'a', 5 ); // returns 'a ' ``` +```javascript +// A surrogate pair (e.g., an emoji) counts as a single character and is never split apart: +var str = pad( '😀', 3, { + 'rpad': 'x' +}); +// returns '😀xx' +``` + The function accepts the following `options`: - **lpad**: `string` used to left pad. Default: `''`. diff --git a/lib/node_modules/@stdlib/string/pad/lib/main.js b/lib/node_modules/@stdlib/string/pad/lib/main.js index ef7f02acac4a..03ee8d7cfd9d 100644 --- a/lib/node_modules/@stdlib/string/pad/lib/main.js +++ b/lib/node_modules/@stdlib/string/pad/lib/main.js @@ -29,6 +29,8 @@ var ceil = require( '@stdlib/math/base/special/ceil' ); var lpad = require( '@stdlib/string/left-pad' ); var rpad = require( '@stdlib/string/right-pad' ); var abs = require( '@stdlib/math/base/special/abs' ); +var numGraphemeClusters = require( '@stdlib/string/num-grapheme-clusters' ); +var sliceGraphemeClusters = require( '@stdlib/string/base/slice-grapheme-clusters' ); var FLOAT64_MAX_SAFE_INTEGER = require( '@stdlib/constants/float64/max-safe-integer' ); var validate = require( './validate.js' ); @@ -90,6 +92,8 @@ function pad( str, len, options ) { var isodd; var right; var left; + var slen; + var tlen; var opts; var err; var tmp; @@ -110,8 +114,10 @@ function pad( str, len, options ) { throw err; } } + // Note: `len` counts grapheme clusters (i.e., user-perceived characters), not UTF-16 code units, so that multi-code-unit characters (e.g., astral-plane code points, combining marks) are never split apart. + slen = numGraphemeClusters( str ); if ( opts.lpad && opts.rpad ) { - n = ( len-str.length ) / 2; + n = ( len-slen ) / 2; if ( n === 0 ) { return str; } @@ -122,9 +128,9 @@ function pad( str, len, options ) { if ( n < 0 ) { n = floor( abs( n ) ); nleft = n; - nright = str.length - n; + nright = slen - n; - // If |len-str.length| is an odd number, take away an additional character from one side... + // If |len-slen| is an odd number, take away an additional character from one side... if ( isodd ) { if ( opts.centerRight ) { nright -= 1; @@ -132,15 +138,15 @@ function pad( str, len, options ) { nleft += 1; } } - return str.substring( nleft, nright ); + return sliceGraphemeClusters( str, nleft, nright ); } - nleft = ceil( n / opts.lpad.length ); + nleft = ceil( n / numGraphemeClusters( opts.lpad ) ); left = repeat( opts.lpad, nleft ); - nright = ceil( n / opts.rpad.length ); + nright = ceil( n / numGraphemeClusters( opts.rpad ) ); right = repeat( opts.rpad, nright ); - // If (len-str.length) is an odd number, give one side one extra character... + // If (len-slen) is an odd number, give one side one extra character... n = tmp; nleft = n; nright = n; @@ -151,19 +157,20 @@ function pad( str, len, options ) { nright += 1; } } - left = left.substring( 0, nleft ); - right = right.substring( 0, nright ); + left = sliceGraphemeClusters( left, 0, nleft ); + right = sliceGraphemeClusters( right, 0, nright ); return left + str + right; } if ( opts.lpad ) { tmp = lpad( str, len, opts.lpad ); - return tmp.substring( tmp.length-len ); + tlen = numGraphemeClusters( tmp ); + return sliceGraphemeClusters( tmp, tlen-len, tlen ); } if ( opts.rpad ) { - return ( rpad( str, len, opts.rpad ) ).substring( 0, len ); + return sliceGraphemeClusters( rpad( str, len, opts.rpad ), 0, len ); } if ( opts.rpad === void 0 ) { - return ( rpad( str, len, ' ' ) ).substring( 0, len ); + return sliceGraphemeClusters( rpad( str, len, ' ' ), 0, len ); } throw new RangeError( format( 'invalid argument. At least one padding option must have a length greater than 0. Left padding: `%s`. Right padding: `%s`.', opts.lpad, opts.rpad ) ); } diff --git a/lib/node_modules/@stdlib/string/pad/test/test.js b/lib/node_modules/@stdlib/string/pad/test/test.js index c7202d3d5415..cf3fe72454da 100644 --- a/lib/node_modules/@stdlib/string/pad/test/test.js +++ b/lib/node_modules/@stdlib/string/pad/test/test.js @@ -289,3 +289,63 @@ tape( 'if the specified string length is less than or equal to the input string t.end(); }); + +tape( 'the function measures string length in grapheme clusters, not UTF-16 code units, so a string containing a surrogate pair is treated as one character', function test( t ) { + var str; + + str = pad( '😀', 3 ); + t.strictEqual( str, '😀 ', 'right padded, default pad string' ); + + str = pad( '😀', 3, { + 'lpad': 'x' + }); + t.strictEqual( str, 'xx😀', 'left padded' ); + + str = pad( '😀', 3, { + 'rpad': 'x' + }); + t.strictEqual( str, '😀xx', 'right padded' ); + + t.end(); +}); + +tape( 'the function never splits a surrogate pair when truncating a string that already exceeds the specified length', function test( t ) { + var str; + + // Two astral characters (4 code units, 2 grapheme clusters) trimmed to 1 grapheme cluster: + str = pad( '😀😁', 1, { + 'rpad': 'x' + }); + t.strictEqual( str, '😀', 'right padded (<); no lone surrogate' ); + + str = pad( '😀😁', 1, { + 'lpad': 'x' + }); + t.strictEqual( str, '😁', 'left padded (<); no lone surrogate' ); + + str = pad( '😀😁😂', 2, { + 'lpad': '@', + 'rpad': '!' + }); + t.strictEqual( str, '😁😂', 'centered (<); no lone surrogate' ); + + t.end(); +}); + +tape( 'the function supports padding with a multi-code-unit pad string without splitting a surrogate pair', function test( t ) { + var str; + + str = pad( 'a', 4, { + 'lpad': '😀', + 'rpad': '!' + }); + t.strictEqual( str, '😀a!!', 'left/right padded with an astral lpad character' ); + + str = pad( 'a', 4, { + 'lpad': '!', + 'rpad': '😀' + }); + t.strictEqual( str, '!a😀😀', 'left/right padded with an astral rpad character' ); + + t.end(); +}); diff --git a/lib/node_modules/@stdlib/string/right-pad/README.md b/lib/node_modules/@stdlib/string/right-pad/README.md index 94c821d67a88..cc96d1d4a7e5 100644 --- a/lib/node_modules/@stdlib/string/right-pad/README.md +++ b/lib/node_modules/@stdlib/string/right-pad/README.md @@ -32,13 +32,19 @@ var rpad = require( '@stdlib/string/right-pad' ); #### rpad( str, len\[, pad] ) -Right pads a `string` such that the padded `string` has a `length` of **at least** `len`. +Right pads a `string` such that the padded `string` has a `length` of **at least** `len` grapheme clusters (i.e., user-perceived characters, not UTF-16 code units). ```javascript var str = rpad( 'a', 5 ); // returns 'a ' ``` +```javascript +// A surrogate pair (e.g., an emoji) counts as a single character: +var str = rpad( '😀', 3, 'x' ); +// returns '😀xx' +``` + By default, an input `string` is padded with `spaces`. To pad with a different character or sequence of characters, provide a `pad` string. ```javascript @@ -67,6 +73,8 @@ str = rpad( 'beep', 12, 'boop' ); // returns 'beepboopbo' => length 10 ``` +- Length is measured in grapheme clusters. If `pad` is a string whose grapheme cluster count does not grow when repeated — a bare Unicode combining mark is the notable case, per [UAX #29][unicode-uax-29] a run of `Extend` characters merges into a single cluster no matter how many times it repeats — the returned string may have fewer than `len` grapheme clusters. + @@ -199,6 +207,8 @@ beep%%%% [mdn-regexp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +[unicode-uax-29]: https://unicode.org/reports/tr29/ + [@stdlib/string/left-pad]: https://github.com/stdlib-js/stdlib/tree/develop/lib/node_modules/%40stdlib/string/left-pad diff --git a/lib/node_modules/@stdlib/string/right-pad/test/test.js b/lib/node_modules/@stdlib/string/right-pad/test/test.js index cd4ef1ad5225..8c5d21b28507 100644 --- a/lib/node_modules/@stdlib/string/right-pad/test/test.js +++ b/lib/node_modules/@stdlib/string/right-pad/test/test.js @@ -151,3 +151,9 @@ tape( 'if the specified string length is less than or equal to the input string t.strictEqual( rpad( 'beep', 4, 'boop' ), 'beep', 'returns input string (=)' ); t.end(); }); + +tape( 'the function measures string length in grapheme clusters, not UTF-16 code units, so a string containing a surrogate pair is treated as one character', function test( t ) { + var str = rpad( '😀', 3, 'x' ); + t.strictEqual( str, '😀xx', 'pads based on grapheme cluster count' ); + t.end(); +});