From 9812c1dfb400a796ba7075a088f2e1d397027dfd Mon Sep 17 00:00:00 2001 From: stdlib-bot Date: Fri, 4 Aug 2023 16:10:15 +0000 Subject: [PATCH] Auto-generated commit --- .github/.keepalive | 1 - README.md | 29 ++++- benchmark/benchmark.js | 108 ++++++++++++++++-- bin/cli | 28 +++-- docs/repl.txt | 21 +++- docs/types/index.d.ts | 63 +++++++++++ docs/types/test.ts | 59 +++++++++- docs/usage.txt | 1 + etc/cli_opts.json | 3 +- lib/main.js | 68 +++++++++--- package.json | 8 +- test/test.cli.js | 84 ++++++++++++++ test/test.js | 246 ++++++++++++++++++++++++++++++++++++++++- 13 files changed, 676 insertions(+), 43 deletions(-) delete mode 100644 .github/.keepalive diff --git a/.github/.keepalive b/.github/.keepalive deleted file mode 100644 index ac8d282..0000000 --- a/.github/.keepalive +++ /dev/null @@ -1 +0,0 @@ -2023-08-01T04:03:31.718Z diff --git a/README.md b/README.md index 792bb23..eaf7dc0 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ The [branches.md][branches-url] file summarizes the available branches and displ var removeFirst = require( '@stdlib/string-remove-first' ); ``` -#### removeFirst( str\[, n] ) +#### removeFirst( str\[, n]\[, options] ) -Removes the first character of a `string`. +Removes the first character(s) of a `string`. ```javascript var out = removeFirst( 'last man standing' ); @@ -63,7 +63,17 @@ out = removeFirst( 'Hidden Treasures' ); // returns 'idden Treasures' ``` -If provided a second argument, the function removes the first `n` characters. +The function supports the following options: + +- **mode**: type of characters to return. Must be one of the following: + + - `'grapheme'`: grapheme clusters. Appropriate for strings containing visual characters which can span multiple Unicode code points (e.g., emoji). + - `'code_point'`: Unicode code points. Appropriate for strings containing visual characters which are comprised of more than one Unicode code unit (e.g., ideographic symbols and punctuation and mathematical alphanumerics). + - `'code_unit'`: UTF-16 code units. Appropriate for strings containing visual characters drawn from the basic multilingual plane (BMP) (e.g., common characters, such as those from the Latin, Greek, and Cyrillic alphabets). + + Default: `'grapheme'`. + +By default, the function returns the first character. To return the first `n` characters, provide a second argument specifying the number of characters to return. ```javascript var out = removeFirst( 'foo bar', 4 ); @@ -77,6 +87,18 @@ out = removeFirst( 'foo bar', 10 ); + + +
+ +## Notes + +- By default, the function assumes the general case in which an input string may contain an arbitrary number of grapheme clusters. This assumption comes with a performance cost. Accordingly, if an input string is known to only contain visual characters of a particular type (e.g., only alphanumeric), one can achieve better performance by specifying the appropriate `mode` option. + +
+ + +
## Examples @@ -145,6 +167,7 @@ Options: -V, --version Print the package version. --n Number of characters to remove. Default: 1. --split sep Delimiter for stdin data. Default: '/\\r?\\n/'. + --mode mode Type of character to return. Default: 'grapheme'. ```
diff --git a/benchmark/benchmark.js b/benchmark/benchmark.js index 125b887..41c8eb9 100644 --- a/benchmark/benchmark.js +++ b/benchmark/benchmark.js @@ -1,7 +1,7 @@ /** * @license Apache-2.0 * -* Copyright (c) 2018 The Stdlib Authors. +* Copyright (c) 2023 The Stdlib Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ var bench = require( '@stdlib/bench' ); var isString = require( '@stdlib/assert-is-string' ).isPrimitive; -var fromCodePoint = require( '@stdlib/string-from-code-point' ); var pkg = require( './../package.json' ).name; var removeFirst = require( './../lib' ); @@ -30,16 +29,111 @@ var removeFirst = require( './../lib' ); // MAIN // bench( pkg, function benchmark( b ) { - var str; + var values; var out; var i; + values = [ + 'beep boop', + 'foo bar', + 'xyz abc' + ]; + + b.tic(); + for ( i = 0; i < b.iterations; i++ ) { + out = removeFirst( values[ i%values.length ] ); + if ( typeof out !== 'string' ) { + b.fail( 'should return a string' ); + } + } + b.toc(); + if ( !isString( out ) ) { + b.fail( 'should return a string' ); + } + b.pass( 'benchmark finished' ); + b.end(); +}); + +bench( pkg+':mode=grapheme', function benchmark( b ) { + var values; + var opts; + var out; + var i; + + values = [ + 'beep boop', + 'foo bar', + 'xyz abc' + ]; + opts = { + 'mode': 'grapheme' + }; + + b.tic(); + for ( i = 0; i < b.iterations; i++ ) { + out = removeFirst( values[ i%values.length ], opts ); + if ( typeof out !== 'string' ) { + b.fail( 'should return a string' ); + } + } + b.toc(); + if ( !isString( out ) ) { + b.fail( 'should return a string' ); + } + b.pass( 'benchmark finished' ); + b.end(); +}); + +bench( pkg+':mode=code_point', function benchmark( b ) { + var values; + var opts; + var out; + var i; + + values = [ + 'beep boop', + 'foo bar', + 'xyz abc' + ]; + opts = { + 'mode': 'code_point' + }; + + b.tic(); + for ( i = 0; i < b.iterations; i++ ) { + out = removeFirst( values[ i%values.length ], opts ); + if ( typeof out !== 'string' ) { + b.fail( 'should return a string' ); + } + } + b.toc(); + if ( !isString( out ) ) { + b.fail( 'should return a string' ); + } + b.pass( 'benchmark finished' ); + b.end(); +}); + +bench( pkg+':mode=code_unit', function benchmark( b ) { + var values; + var opts; + var out; + var i; + + values = [ + 'beep boop', + 'foo bar', + 'xyz abc' + ]; + opts = { + 'mode': 'code_unit' + }; + b.tic(); for ( i = 0; i < b.iterations; i++ ) { - str = fromCodePoint( i%126 ) + 'eep boop'; - out = removeFirst( str ); - if ( out !== 'eep boop' ) { - b.fail( 'should return a shorter string' ); + out = removeFirst( values[ i%values.length ], opts ); + if ( typeof out !== 'string' ) { + b.fail( 'should return a string' ); } } b.toc(); diff --git a/bin/cli b/bin/cli index 8a5dc39..0a155cf 100755 --- a/bin/cli +++ b/bin/cli @@ -45,6 +45,7 @@ function main() { var split; var flags; var args; + var opts; var cli; var n; @@ -64,6 +65,12 @@ function main() { } if ( flags.n ) { n = parseInt( flags.n, 10 ); + } else { + n = 1; + } + opts = {}; + if ( flags.mode ) { + opts.mode = flags.mode; } // Get any provided command-line arguments: @@ -81,10 +88,10 @@ function main() { } return stdin( onRead ); } - if ( n ) { - console.log( removeFirst( args[ 0 ], n ) ); // eslint-disable-line no-console - } else { - console.log( removeFirst( args[ 0 ] ) ); // eslint-disable-line no-console + try { + console.log( removeFirst( args[ 0 ], n, opts ) ); // eslint-disable-line no-console + } catch ( error ) { + return cli.error( error ); } /** @@ -107,13 +114,14 @@ function main() { if ( lines[ lines.length-1 ] === '' ) { lines.pop(); } - if ( n ) { - for ( i = 0; i < lines.length; i++ ) { - console.log( removeFirst( lines[ i ], n ) ); // eslint-disable-line no-console + if ( lines.length ) { + try { + console.log( removeFirst( lines[ 0 ], n, opts ) ); // eslint-disable-line no-console + } catch ( error ) { + return cli.error( error ); } - } else { - for ( i = 0; i < lines.length; i++ ) { - console.log( removeFirst( lines[ i ] ) ); // eslint-disable-line no-console + for ( i = 1; i < lines.length; i++ ) { + console.log( removeFirst( lines[ i ], n, opts ) ); // eslint-disable-line no-console } } } diff --git a/docs/repl.txt b/docs/repl.txt index c58bb35..aeca5de 100644 --- a/docs/repl.txt +++ b/docs/repl.txt @@ -1,5 +1,5 @@ -{{alias}}( str[, n] ) +{{alias}}( str[, n][, options] ) Removes the first character(s) of a `string`. Parameters @@ -10,6 +10,25 @@ n: integer (optional) Number of characters to remove. Default: 1. + options: Object (optional) + Options. + + options.mode: string (optional) + Type of characters to return. The following modes are supported: + + - grapheme: grapheme clusters. Appropriate for strings containing visual + characters which can span multiple Unicode code points (e.g., emoji). + - code_point: Unicode code points. Appropriate for strings containing + visual characters which are comprised of more than one Unicode code + unit (e.g., ideographic symbols and punctuation and mathematical + alphanumerics). + - code_unit': UTF-16 code units. Appropriate for strings containing + visual characters drawn from the basic multilingual plane (BMP) (e.g., + common characters, such as those from the Latin, Greek, and Cyrillic + alphabets). + + Default: 'grapheme'. + Returns ------- out: string diff --git a/docs/types/index.d.ts b/docs/types/index.d.ts index b8c5737..58f3204 100644 --- a/docs/types/index.d.ts +++ b/docs/types/index.d.ts @@ -18,6 +18,69 @@ // TypeScript Version: 2.0 +// tslint:disable:unified-signatures + +/** +* Interface describing function options. +*/ +interface Options { + /** + * Specifies the type of characters to return (default: 'grapheme'). + * + * ## Notes + * + * - The following option values are supported: + * + * - `'grapheme'`: grapheme clusters. Appropriate for strings containing visual characters which can span multiple Unicode code points (e.g., emoji). + * - `'code_point'`: Unicode code points. Appropriate for strings containing visual characters which are comprised of more than one Unicode code unit (e.g., ideographic symbols and punctuation and mathematical alphanumerics). + * - `'code_unit'`: UTF-16 code units. Appropriate for strings containing visual characters drawn from the basic multilingual plane (BMP) (e.g., common characters, such as those from the Latin, Greek, and Cyrillic alphabets). + */ + mode?: 'grapheme' | 'code_point' | 'code_unit'; +} + +/** +* Removes the first character(s) of a string. +* +* @param str - input string +* @param n - number of characters to remove (default: 1) +* @param options - options +* @returns updated string +* +* @example +* var out = removeFirst( 'last man standing', 1, { +* 'mode': 'code_unit' +* }); +* // returns 'ast man standing' +* +* @example +* var out = removeFirst( '🐶🐮🐷🐰🐸', 2, { +* 'mode': 'grapheme' +* }); +* // returns '🐷🐰🐸' +*/ +declare function removeFirst( str: string, n: number, options?: Options ): string; + +/** +* Removes the first character of a string. +* +* @param str - input string +* @param options - options +* @returns updated string +* +* @example +* var out = removeFirst( 'last man standing', { +* 'mode': 'code_unit' +* }); +* // returns 'ast man standing' +* +* @example +* var out = removeFirst( '🐶🐮🐷🐰🐸', 2, { +* 'mode': 'grapheme' +* }); +* // returns '🐷🐰🐸' +*/ +declare function removeFirst( str: string, options?: Options ): string; + /** * Removes the first character(s) of a string. * diff --git a/docs/types/test.ts b/docs/types/test.ts index 5d25f23..67c92c8 100644 --- a/docs/types/test.ts +++ b/docs/types/test.ts @@ -24,6 +24,9 @@ import removeFirst = require( './index' ); // The function returns a string... { removeFirst( 'abc' ); // $ExpectType string + removeFirst( 'abc', 1 ); // $ExpectType string + removeFirst( 'abc', {} ); // $ExpectType string + removeFirst( 'abc', 1, {} ); // $ExpectType string } // The compiler throws an error if the function is provided a value other than a string... @@ -36,17 +39,69 @@ import removeFirst = require( './index' ); removeFirst( [] ); // $ExpectError removeFirst( {} ); // $ExpectError removeFirst( ( x: number ): number => x ); // $ExpectError + + removeFirst( true, 1 ); // $ExpectError + removeFirst( false, 1 ); // $ExpectError + removeFirst( null, 1 ); // $ExpectError + removeFirst( undefined, 1 ); // $ExpectError + removeFirst( 5, 1 ); // $ExpectError + removeFirst( [], 1 ); // $ExpectError + removeFirst( {}, 1 ); // $ExpectError + removeFirst( ( x: number ): number => x, 1 ); // $ExpectError + + removeFirst( true, {} ); // $ExpectError + removeFirst( false, {} ); // $ExpectError + removeFirst( null, {} ); // $ExpectError + removeFirst( undefined, {} ); // $ExpectError + removeFirst( 5, {} ); // $ExpectError + removeFirst( [], {} ); // $ExpectError + removeFirst( {}, {} ); // $ExpectError + removeFirst( ( x: number ): number => x, {} ); // $ExpectError + + removeFirst( true, 1, {} ); // $ExpectError + removeFirst( false, 1, {} ); // $ExpectError + removeFirst( null, 1, {} ); // $ExpectError + removeFirst( undefined, 1, {} ); // $ExpectError + removeFirst( 5, 1, {} ); // $ExpectError + removeFirst( [], 1, {} ); // $ExpectError + removeFirst( {}, 1, {} ); // $ExpectError + removeFirst( ( x: number ): number => x, 1, {} ); // $ExpectError } -// The compiler throws an error if the function is provided a second argument that is not a number... +// The compiler throws an error if the function is provided an invalid second argument... { removeFirst( 'abc', true ); // $ExpectError removeFirst( 'abc', false ); // $ExpectError removeFirst( 'abc', null ); // $ExpectError removeFirst( 'abc', 'abc' ); // $ExpectError removeFirst( 'abc', [] ); // $ExpectError - removeFirst( 'abc', {} ); // $ExpectError removeFirst( 'abc', ( x: number ): number => x ); // $ExpectError + + removeFirst( 'abc', true, {} ); // $ExpectError + removeFirst( 'abc', false, {} ); // $ExpectError + removeFirst( 'abc', null, {} ); // $ExpectError + removeFirst( 'abc', '', {} ); // $ExpectError + removeFirst( 'abc', [], {} ); // $ExpectError + removeFirst( 'abc', {}, {} ); // $ExpectError + removeFirst( 'abc', ( x: number ): number => x, {} ); // $ExpectError +} + +// The compiler throws an error if the function is provided an invalid `mode` option... +{ + removeFirst( 'abc', { 'mode': true } ); // $ExpectError + removeFirst( 'abc', { 'mode': false } ); // $ExpectError + removeFirst( 'abc', { 'mode': null } ); // $ExpectError + removeFirst( 'abc', { 'mode': '' } ); // $ExpectError + removeFirst( 'abc', { 'mode': [] } ); // $ExpectError + removeFirst( 'abc', { 'mode': ( x: number ): number => x } ); // $ExpectError + + removeFirst( 'abc', 1, { 'mode': true } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': false } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': null } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': '' } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': [] } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': {} } ); // $ExpectError + removeFirst( 'abc', 1, { 'mode': ( x: number ): number => x } ); // $ExpectError } // The compiler throws an error if the function is provided insufficient arguments... diff --git a/docs/usage.txt b/docs/usage.txt index 2b45e56..9dd960a 100644 --- a/docs/usage.txt +++ b/docs/usage.txt @@ -7,3 +7,4 @@ Options: -V, --version Print the package version. --n Number of characters to remove. Default: 1. --split sep Delimiter for stdin data. Default: '/\\r?\\n/'. + --mode mode Type of character to return. Default: 'grapheme'. diff --git a/etc/cli_opts.json b/etc/cli_opts.json index 5d6ecc9..2ceae45 100644 --- a/etc/cli_opts.json +++ b/etc/cli_opts.json @@ -5,7 +5,8 @@ ], "string": [ "n", - "split" + "split", + "mode" ], "alias": { "help": [ diff --git a/lib/main.js b/lib/main.js index ea394b5..c33cfcd 100644 --- a/lib/main.js +++ b/lib/main.js @@ -21,11 +21,27 @@ // MODULES // var isString = require( '@stdlib/assert-is-string' ).isPrimitive; +var isPlainObject = require( '@stdlib/assert-is-plain-object' ); +var hasOwnProp = require( '@stdlib/assert-has-own-property' ); +var contains = require( '@stdlib/array-base-assert-contains' ).factory; var isNonNegativeInteger = require( '@stdlib/assert-is-nonnegative-integer' ).isPrimitive; -var nextGraphemeClusterBreak = require( '@stdlib/string-next-grapheme-cluster-break' ); +var removeFirstCodeUnit = require( '@stdlib/string-base-remove-first' ); +var removeFirstCodePoint = require( '@stdlib/string-base-remove-first-code-point' ); +var removeFirstGraphemeCluster = require( '@stdlib/string-base-remove-first-grapheme-cluster' ); // eslint-disable-line id-length var format = require( '@stdlib/string-format' ); +// VARIABLES // + +var MODES = [ 'grapheme', 'code_point', 'code_unit' ]; +var FCNS = { + 'grapheme': removeFirstGraphemeCluster, + 'code_point': removeFirstCodePoint, + 'code_unit': removeFirstCodeUnit +}; +var isMode = contains( MODES ); + + // MAIN // /** @@ -33,8 +49,12 @@ var format = require( '@stdlib/string-format' ); * * @param {string} str - input string * @param {NonNegativeInteger} [n=1] - number of characters to remove +* @param {Options} [options] - options +* @param {string} [options.mode="grapheme"] - type of "character" to return (must be either `grapheme`, `code_point`, or `code_unit`) * @throws {TypeError} must provide a string primitive * @throws {TypeError} second argument must be a nonnegative integer +* @throws {TypeError} options argument must be an object +* @throws {TypeError} must provide valid options * @returns {string} updated string * * @example @@ -61,28 +81,48 @@ var format = require( '@stdlib/string-format' ); * var out = removeFirst( 'foo bar', 4 ); * // returns 'bar' */ -function removeFirst( str, n ) { - var nextBreak; +function removeFirst( str ) { + var options; + var nargs; + var opts; + var n; + if ( !isString( str ) ) { throw new TypeError( format( 'invalid argument. First argument must be a string. Value: `%s`.', str ) ); } - if ( arguments.length > 1 ) { + opts = { + 'mode': 'grapheme' + }; + nargs = arguments.length; + if ( nargs === 1 ) { + n = 1; + } else if ( nargs === 2 ) { + n = arguments[ 1 ]; + if ( isPlainObject( n ) ) { + options = n; + n = 1; + } else if ( !isNonNegativeInteger( n ) ) { + throw new TypeError( format( 'invalid argument. Second argument must be a nonnegative integer. Value: `%s`.', n ) ); + } + } else { // nargs > 2 + n = arguments[ 1 ]; if ( !isNonNegativeInteger( n ) ) { throw new TypeError( format( 'invalid argument. Second argument must be a nonnegative integer. Value: `%s`.', n ) ); } - nextBreak = 0; - while ( n > 0 ) { - nextBreak = nextGraphemeClusterBreak( str, nextBreak ); - n -= 1; + options = arguments[ 2 ]; + if ( !isPlainObject( options ) ) { + throw new TypeError( format( 'invalid argument. Options argument must be an object. Value: `%s`.', options ) ); } - } else { - nextBreak = nextGraphemeClusterBreak( str ); } - // Value of `nextBreak` will be -1 if and only if `str` is an empty string or `str` has only 1 extended grapheme cluster... - if ( str === '' || nextBreak === -1 ) { - return ''; + if ( options ) { + if ( hasOwnProp( options, 'mode' ) ) { + opts.mode = options.mode; + if ( !isMode( opts.mode ) ) { + throw new TypeError( format( 'invalid option. `%s` option must be one of the following: "%s". Value: `%s`.', 'mode', MODES.join( '", "' ), opts.mode ) ); + } + } } - return str.substring( nextBreak ); + return FCNS[ opts.mode ]( str, n ); } diff --git a/package.json b/package.json index 0e02d66..7ecf36b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,10 @@ "url": "https://github.com/stdlib-js/stdlib/issues" }, "dependencies": { + "@stdlib/array-base-assert-contains": "^0.0.1", + "@stdlib/assert-has-own-property": "^0.0.7", "@stdlib/assert-is-nonnegative-integer": "^0.0.7", + "@stdlib/assert-is-plain-object": "^0.0.7", "@stdlib/assert-is-regexp-string": "^0.0.9", "@stdlib/assert-is-string": "^0.0.8", "@stdlib/cli-ctor": "^0.0.3", @@ -48,8 +51,10 @@ "@stdlib/process-read-stdin": "^0.0.7", "@stdlib/regexp-eol": "^0.0.7", "@stdlib/streams-node-stdin": "^0.0.7", + "@stdlib/string-base-remove-first": "github:stdlib-js/string-base-remove-first#main", + "@stdlib/string-base-remove-first-code-point": "github:stdlib-js/string-base-remove-first-code-point#main", + "@stdlib/string-base-remove-first-grapheme-cluster": "github:stdlib-js/string-base-remove-first-grapheme-cluster#main", "@stdlib/string-format": "^0.0.3", - "@stdlib/string-next-grapheme-cluster-break": "^0.0.9", "@stdlib/utils-regexp-from-string": "^0.0.9" }, "devDependencies": { @@ -57,7 +62,6 @@ "@stdlib/assert-is-windows": "^0.0.7", "@stdlib/bench": "^0.0.12", "@stdlib/process-exec-path": "^0.0.7", - "@stdlib/string-from-code-point": "^0.0.9", "@stdlib/string-replace": "^0.0.11", "tape": "git+https://github.com/kgryte/tape.git#fix/globby", "proxyquire": "^2.0.0", diff --git a/test/test.cli.js b/test/test.cli.js index 146492e..f4a125d 100644 --- a/test/test.cli.js +++ b/test/test.cli.js @@ -182,6 +182,46 @@ tape( 'the command-line interface removes the first `n` characters of a string a } }); +tape( 'the command-line interface supports specifying the type of characters to return', opts, function test( t ) { + var cmd = [ + EXEC_PATH, + '-e', + '"process.stdin.isTTY = true; process.argv[ 2 ] = \'beep\'; process.argv[ 3 ] = \'--mode=code_point\'; require( \''+fpath+'\' );"' + ]; + + exec( cmd.join( ' ' ), done ); + + function done( error, stdout, stderr ) { + if ( error ) { + t.fail( error.message ); + } else { + t.strictEqual( stdout.toString(), 'eep\n', 'expected value' ); + t.strictEqual( stderr.toString(), '', 'does not print to `stderr`' ); + } + t.end(); + } +}); + +tape( 'if provided an invalid option, the command-line interface prints an error and sets a non-zero exit code', opts, function test( t ) { + var cmd = [ + EXEC_PATH, + '-e', + '"process.stdin.isTTY = true; process.argv[ 2 ] = \'beep\'; process.argv[ 3 ] = \'--mode=foo\'; require( \''+fpath+'\' );"' + ]; + + exec( cmd.join( ' ' ), done ); + + function done( error, stdout, stderr ) { + if ( error ) { + t.pass( error.message ); + t.strictEqual( error.code, 1, 'expected exit code' ); + } + t.strictEqual( stdout.toString(), '', 'does not print to `stdout`' ); + t.strictEqual( stderr.toString().length > 0, true, 'expected value' ); + t.end(); + } +}); + tape( 'the command-line interface supports use as a standard stream', opts, function test( t ) { var cmd = [ 'printf "beep\nboop"', @@ -247,6 +287,28 @@ tape( 'the command-line interface supports specifying a custom delimiter when us } }); +tape( 'the command-line interface supports specifying the type of characters to return when used as a standard stream', opts, function test( t ) { + var cmd = [ + 'printf \'foo\nbar\nbaz\'', + '|', + EXEC_PATH, + fpath, + '--mode code_point' + ]; + + exec( cmd.join( ' ' ), done ); + + function done( error, stdout, stderr ) { + if ( error ) { + t.fail( error.message ); + } else { + t.strictEqual( stdout.toString(), 'oo\nar\naz\n', 'expected value' ); + t.strictEqual( stderr.toString(), '', 'does not print to `stderr`' ); + } + t.end(); + } +}); + tape( 'the command-line interface supports specifying a custom delimiter when used as a standard stream (regexp)', opts, function test( t ) { var cmd = [ 'printf \'foo\tbar\tbaz\'', @@ -269,6 +331,28 @@ tape( 'the command-line interface supports specifying a custom delimiter when us } }); +tape( 'when used as a standard stream, if provided an invalid option, the command-line interface prints an error and sets a non-zero exit code', opts, function test( t ) { + var cmd = [ + 'printf \'foo\tbar\tbaz\'', + '|', + EXEC_PATH, + fpath, + '--mode=foo' + ]; + + exec( cmd.join( ' ' ), done ); + + function done( error, stdout, stderr ) { + if ( error ) { + t.pass( error.message ); + t.strictEqual( error.code, 1, 'expected exit code' ); + } + t.strictEqual( stdout.toString(), '', 'does not print to `stdout`' ); + t.strictEqual( stderr.toString().length > 0, true, 'expected value' ); + t.end(); + } +}); + tape( 'when used as a standard stream, if an error is encountered when reading from `stdin`, the command-line interface prints an error and sets a non-zero exit code', opts, function test( t ) { var script; var opts; diff --git a/test/test.js b/test/test.js index f91285f..1bc69e1 100644 --- a/test/test.js +++ b/test/test.js @@ -59,6 +59,33 @@ tape( 'the function throws an error if not provided a string', function test( t } }); +tape( 'the function throws an error if not provided a string (options)', function test( t ) { + var values; + var i; + + values = [ + 5, + null, + true, + void 0, + NaN, + [], + {}, + function noop() {} + ]; + + for ( i = 0; i < values.length; i++ ) { + t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] ); + } + t.end(); + + function badValue( value ) { + return function badValue() { + removeFirst( value, {} ); + }; + } +}); + tape( 'the function throws an error if provided a second argument which is not a nonnegative integer', function test( t ) { var values; var i; @@ -71,7 +98,6 @@ tape( 'the function throws an error if provided a second argument which is not a void 0, NaN, [], - {}, function noop() {} ]; @@ -87,8 +113,124 @@ tape( 'the function throws an error if provided a second argument which is not a } }); +tape( 'the function throws an error if provided a second argument which is not a nonnegative integer (options)', function test( t ) { + var values; + var i; + + values = [ + 'abc', + 3.14, + null, + true, + void 0, + NaN, + [], + {}, + function noop() {} + ]; + + for ( i = 0; i < values.length; i++ ) { + t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] ); + } + t.end(); + + function badValue( value ) { + return function badValue() { + removeFirst( 'beep', value, {} ); + }; + } +}); + +tape( 'the function throws an error if provided an options argument which is not an object', function test( t ) { + var values; + var i; + + values = [ + 'abc', + 3, + null, + true, + void 0, + NaN, + [], + function noop() {} + ]; + + for ( i = 0; i < values.length; i++ ) { + t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] ); + } + t.end(); + + function badValue( value ) { + return function badValue() { + removeFirst( 'beep', 1, value ); + }; + } +}); + +tape( 'the function throws an error if provided a `mode` option which is not a supported mode (second argument)', function test( t ) { + var values; + var i; + + values = [ + 'abc', + 3, + null, + true, + void 0, + NaN, + [], + function noop() {} + ]; + + for ( i = 0; i < values.length; i++ ) { + t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] ); + } + t.end(); + + function badValue( value ) { + return function badValue() { + removeFirst( 'beep', { + 'mode': value + }); + }; + } +}); + +tape( 'the function throws an error if provided a `mode` option which is not a supported mode (third argument)', function test( t ) { + var values; + var i; + + values = [ + 'abc', + 3, + null, + true, + void 0, + NaN, + [], + function noop() {} + ]; + + for ( i = 0; i < values.length; i++ ) { + t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] ); + } + t.end(); + + function badValue( value ) { + return function badValue() { + removeFirst( 'beep', 1, { + 'mode': value + }); + }; + } +}); + tape( 'the function returns an empty string if provided an empty string', function test( t ) { t.strictEqual( removeFirst( '' ), '', 'returns empty string' ); + t.strictEqual( removeFirst( '', 1 ), '', 'returns expected value' ); + t.strictEqual( removeFirst( '', {} ), '', 'returns expected value' ); + t.strictEqual( removeFirst( '', 1, {} ), '', 'returns expected value' ); t.end(); }); @@ -122,10 +264,13 @@ tape( 'the function returns the original string if provided zero as the second a out = removeFirst( 'hello world', 0 ); t.strictEqual( out, 'hello world', 'returns original string' ); + out = removeFirst( 'hello world', 0, {} ); + t.strictEqual( out, 'hello world', 'returns expected value' ); + t.end(); }); -tape( 'the function removes the first `n` characters of a given string if provided a second argument', function test( t ) { +tape( 'the function removes the first `n` characters of a given string (default)', function test( t ) { var out; out = removeFirst( 'hello world', 1 ); @@ -151,3 +296,100 @@ tape( 'the function removes the first `n` characters of a given string if provid t.end(); }); + +tape( 'the function supports removing the first `n` characters of a provided string (mode=grapheme)', function test( t ) { + var opts; + var out; + + opts = { + 'mode': 'grapheme' + }; + + out = removeFirst( 'hello world', 1, opts ); + t.strictEqual( out, 'ello world', 'returns expected value' ); + + out = removeFirst( 'hello world', 7, opts ); + t.strictEqual( out, 'orld', 'returns expected value' ); + + out = removeFirst( '!!!', 1, opts ); + t.strictEqual( out, '!!', 'returns expected value' ); + + out = removeFirst( '!!!', 2, opts ); + t.strictEqual( out, '!', 'returns expected value' ); + + out = removeFirst( 'अनुच्छेद', 1, opts ); + t.strictEqual( out, 'नुच्छेद', 'returns expected value' ); + + out = removeFirst( '六书/六書', 1, opts ); + t.strictEqual( out, '书/六書', 'returns expected value' ); + + out = removeFirst( '🌷', 1, opts ); + t.strictEqual( out, '', 'returns expected value' ); + + out = removeFirst( '👉🏿', 1, opts ); + t.strictEqual( out, '', 'returns expected value' ); + + t.end(); +}); + +tape( 'the function supports removing the first `n` characters of a provided string (mode=code_point)', function test( t ) { + var opts; + var out; + + opts = { + 'mode': 'code_point' + }; + + out = removeFirst( 'hello world', 1, opts ); + t.strictEqual( out, 'ello world', 'returns expected value' ); + + out = removeFirst( 'hello world', 7, opts ); + t.strictEqual( out, 'orld', 'returns expected value' ); + + out = removeFirst( '!!!', 1, opts ); + t.strictEqual( out, '!!', 'returns expected value' ); + + out = removeFirst( '!!!', 2, opts ); + t.strictEqual( out, '!', 'returns expected value' ); + + out = removeFirst( 'अनुच्छेद', 1, opts ); + t.strictEqual( out, 'नुच्छेद', 'returns expected value' ); + + out = removeFirst( '六书/六書', 1, opts ); + t.strictEqual( out, '书/六書', 'returns expected value' ); + + out = removeFirst( '🌷', 1, opts ); + t.strictEqual( out, '\udf37', 'returns expected value' ); + t.end(); +}); + +tape( 'the function supports removing the first `n` characters of a provided string (mode=code_unit)', function test( t ) { + var opts; + var out; + + opts = { + 'mode': 'code_unit' + }; + + out = removeFirst( 'hello world', 1, opts ); + t.strictEqual( out, 'ello world', 'returns expected value' ); + + out = removeFirst( 'hello world', 7, opts ); + t.strictEqual( out, 'orld', 'returns expected value' ); + + out = removeFirst( '!!!', 1, opts ); + t.strictEqual( out, '!!', 'returns expected value' ); + + out = removeFirst( '!!!', 2, opts ); + t.strictEqual( out, '!', 'returns expected value' ); + + out = removeFirst( 'अनुच्छेद', 1, opts ); + t.strictEqual( out, 'नुच्छेद', 'returns expected value' ); + + out = removeFirst( '六书/六書', 1, opts ); + t.strictEqual( out, '书/六書', 'returns expected value' ); + + out = removeFirst( '🌷', 1, opts ); + t.strictEqual( out, '\udf37', 'returns expected value' ); + t.end(); +});