Skip to content

Instantly share code, notes, and snippets.

@julianjensen
Created May 6, 2018 16:08
Show Gist options
  • Select an option

  • Save julianjensen/b360f41404c1758e3d4fa8666a611525 to your computer and use it in GitHub Desktop.

Select an option

Save julianjensen/b360f41404c1758e3d4fa8666a611525 to your computer and use it in GitHub Desktop.
Various ways of counting vowels with benchmarks
/** ******************************************************************************************************************
* @file This just counts the number of vowels in some text.
*
* To install and play with this, do the following:
*
* 1. `mkdir vowel-count`
* 2. `cd vowel-count`
* 3. `npm init`
* 4. Copy this file into the directory, call it anything you like, but `index.js` is normal.
* 5. `npm i benchmark cli-table2`
* 6. `node index.js`
*
* It does a while to run but it should show you some updates as it runs.
*
* @author Julian Jensen <jjdanois@gmail.com>
* @since 1.0.0
* @date 06-May-2018
*********************************************************************************************************************/
"use strict";
const
assert = require( 'assert' ),
Table = require( 'cli-table2' ),
table = {
_table: new Table( { head: [ 'Name', 'ops/sec', 'MoE', 'Runs', 'Uses spread?' ] } ),
add( rows )
{
if ( rows && rows.length ) this._table.push( ...rows );
return this;
},
show( rows )
{
this.add( rows );
console.log( '\r' + this._table.toString() );
return this;
}
},
Benchmark = require( 'benchmark' ),
suite = new Benchmark.Suite(),
sentence = "No gangster would hold on to a gun after shooting someone (even lowly Spider) with it. Real-life Tommy would have disposed of the revolver and " +
"just picked up another gun. In this case, the M1911, but it could have been any appropriate carry gun. I like it when the little details are covered.",
preSplit = sentence.split( '' ),
print = s => process.stdout.write( '\r' + s + '\r' ),
fix = ( num, n = 2 ) => num.toFixed( n ),
sort_results = ( { stats: a }, { stats: b } ) => a.mean + a.moe > b.mean + b.moe ? 1 : -1,
formatNumber = num => {
const [ int, frac ] = String( num ).split( '.' );
return int.replace( /(?=(?:\d{3})+$)(?!\b)/g, ',' ) + ( frac ? '.' + frac : '' );
},
to_row = ( { hz, id, stats: { sample, rme }, name: result = id } ) =>
[ result, { hAlign: 'right', content: formatNumber( fix( hz, hz < 100 ? 2 : 0 ) ) }, `\xb1${fix( rme )}%`, sample.length, { hAlign: 'center', content: /spread/.test( result ) ? 'Yes' : 'No' } ],
show_cycle = ( { target } ) => print( String( target ) + ' '.repeat( 40 ) ), // process.stdout.write( '\r' + String( target ) + ' '.repeat( 40 ) + '\r' ),
alphabet = [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
vowels = [ 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' ],
vowelMap = { a: 1, e: 1, i: 1, o: 1, u: 1, A: 1, E: 1, I: 1, O: 1, U: 1 };
/**
* Your basic standard `for` loop.
*
* @param {string} s
* @return {number}
*/
function using_basic_for_loop( s )
{
let count = 0;
for ( let index = 0, len = s.length; index < len; ++index )
{
const ch = s[ index ].toLowerCase();
if ( ch === 'a' || ch === 'e' || ch === 'i' || ch === 'o' || ch === 'u' )
++count;
}
return count;
}
/**
* This has more of a JavaScript flavor to it, using the string as an iterable.
*
* @param {string} s
* @return {number}
*/
function using_string_as_iterable( s )
{
let count = 0;
for ( const ch of s )
vowels.includes( ch ) && count++;
return count;
}
/**
* Very JavaScript-y but uses spread with is still a performance killer.
*
* @param {string} s
* @return {number}
*/
function idiomatic_foreach_with_spread( s )
{
let count = 0;
[ ...s ].forEach( ch => vowels.includes( ch ) && count++ );
return count;
}
/**
* Very JavaScript-y but uses `split()` instead of spread and therefore should perform better.
*
* @param {string} s
* @return {number}
*/
function idiomatic_foreach_with_split( s )
{
let count = 0;
s.split( '' ).forEach( ch => vowels.includes( ch ) && count++ );
return count;
}
/**
* The obvious `reduce()` variation with the spread ball-and-chain dragging it down.
*
* @param {string} s
* @return {number}
*/
function using_reduce_spread( s )
{
return [ ...s ].reduce( ( cnt, ch ) => ( vowelMap[ ch ] || 0 ) + cnt, 0 );
}
/**
* The obvious `reduce()` without the spread.
*
* @param {string} s
* @return {number}
*/
function using_reduce_split( s )
{
return s.split( '' ).reduce( ( cnt, ch ) => ( vowelMap[ ch ] || 0 ) + cnt, 0 );
}
const
using_reduce_arrow_spread = s => [ ...s ].reduce( ( cnt, ch ) => ( vowelMap[ ch ] || 0 ) + cnt, 0 ),
using_reduce_arrow_split = s => s.split( '' ).reduce( ( cnt, ch ) => ( vowelMap[ ch ] || 0 ) + cnt, 0 ),
using_reduce_arrow_from = s => Array.from( s ).reduce( ( cnt, ch ) => ( vowelMap[ ch ] || 0 ) + cnt, 0 ),
using_replace = s => s.replace( /[^aeiou]/gi, '' ).length,
using_reduce_and_spread = s => [ ...s ].reduce( ( count, ch ) =>
count + ( ( ch = ch.charCodeAt( 0 ) ) & 0x80 || ch < 0x41 ? 0 : [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ][ ( ch | 0x20 ) - 0x61 ] ), 0 ),
using_reduce_and_split = s => s.split( '' ).reduce( ( count, ch ) =>
count + ( ( ch = ch.charCodeAt( 0 ) ) & 0x80 || ch < 0x41 ? 0 : [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ][ ( ch | 0x20 ) - 0x61 ] ), 0 ),
using_reducer_and_presplit_array_alphabet = () => preSplit.reduce( ( count, ch ) =>
count + ( ( ch = ch.charCodeAt( 0 ) ) & 0x80 || ch < 0x41 ? 0 : alphabet[ ( ch | 0x20 ) - 0x61 ] ), 0 ),
using_reducer_and_array_split_alphabet = s => s.split( '' ).reduce( ( count, ch ) =>
count + ( ( ch = ch.charCodeAt( 0 ) ) & 0x80 || ch < 0x41 ? 0 : alphabet[ ( ch | 0x20 ) - 0x61 ] ), 0 ),
using_reducer_split_and_conditional = s => s.split( '' ).reduce( ( count, ch ) => count + ( ( ch = ch.toLowerCase() ) === 'a' || ch === 'e' || ch === 'i' || ch === 'o' || ch === 'u' ), 0 ),
using_reducer_split_and_conditional_alternate = s => s.split( '' ).reduce( ( count, ch ) => ( ch = ch.toLowerCase() ) === 'a' || ch === 'e' || ch === 'i' || ch === 'o' || ch === 'u' ? count + 1 : count, 0 ),
// All the various functions
funcs = [
using_basic_for_loop,
using_string_as_iterable,
idiomatic_foreach_with_spread,
idiomatic_foreach_with_split,
using_reduce_spread,
using_reduce_split,
using_reduce_arrow_split,
using_reduce_arrow_spread,
using_reduce_arrow_from,
using_replace,
using_reduce_and_spread,
using_reduce_and_split,
using_reducer_and_presplit_array_alphabet,
using_reducer_and_array_split_alphabet,
using_reducer_split_and_conditional,
using_reducer_split_and_conditional_alternate
];
// assert that they all work and we didn't screw anything up
funcs.forEach( fn => assert( fn( sentence ) === 89 ) );
// Add them all to the benchmarking suite
funcs.forEach( fn => suite.add( fn.name.replace( /_/g, ' ' ), fn.bind( null, sentence ) ) );
process.stdout.write( `Starting the benchmark suite...\r` );
suite
.on( 'cycle', show_cycle ) // Show result of each test as it finishes, overwriting the previous results
.on( 'complete', // We're all done, show a nice sorted table of all the results
function() { table.show( this.filter( 'successful' ).sort( sort_results ).map( to_row ) ); } )
.run( { async: true } );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment