Skip to content

Instantly share code, notes, and snippets.

@nickiaconis
Last active February 25, 2018 06:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nickiaconis/85692900b4675b3d262e5d17d933a37d to your computer and use it in GitHub Desktop.
Save nickiaconis/85692900b4675b3d262e5d17d933a37d to your computer and use it in GitHub Desktop.
Expand Properties Benchmark
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var EXPAND_REGEX = /\{([^}]*)\}/;
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
var properties = [pattern];
for (let property, i = 0; i < properties.length; i++) {
for (let match; (property = properties[i]) && (match = EXPAND_REGEX.exec(property));) {
properties.splice(i, 1, ...duplicateAndReplace(property, match));
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]'));
}
}
function duplicateAndReplace(property, match) {
var all = [];
var parts = match[1].split(',');
parts.forEach((part) => {
all.push(property.substring(0, match.index) +
part +
property.substring(match.index + match[0].length));
});
return all;
}
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var SPLIT_REGEX = /\{|\}/;
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
var parts = pattern.split(SPLIT_REGEX);
var properties = [parts];
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (part.indexOf(',') >= 0) {
properties = duplicateAndReplace(properties, part.split(','), i);
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].join('').replace(END_WITH_EACH_REGEX, '.[]'));
}
}
function duplicateAndReplace(properties, currentParts, index) {
var all = [];
properties.forEach((property) => {
currentParts.forEach((part) => {
var current = property.slice(0);
current[index] = part;
all.push(current);
});
});
return all;
}
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var EXPAND_REGEX = /\{([^{}]*)\}/;
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
var properties = [pattern];
for (let property, i = 0; i < properties.length; i++) {
for (let match; (property = properties[i]) && (match = EXPAND_REGEX.exec(property));) {
properties.splice(i, 1, ...duplicateAndReplace(property, match));
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]'));
}
}
function duplicateAndReplace(property, match) {
var all = [];
var parts = match[1].split(',');
parts.forEach((part) => {
all.push(property.substring(0, match.index) +
part +
property.substring(match.index + match[0].length));
});
return all;
}
var Benchmark = require('benchmark');
var babel = require('babel-core');
var fs = require('fs');
var BENCHMARK_STRING = 'model.{a,b,c}.d.{e,f}.g';
function K() {}
var before = eval(babel.transform(fs.readFileSync('./before.js')).code);
var comma = eval(babel.transform(fs.readFileSync('./comma.js')).code);
var alternate = eval(babel.transform(fs.readFileSync('./alternate.js')).code);
var matches = eval(babel.transform(fs.readFileSync('./matches.js')).code);
var iteration = eval(babel.transform(fs.readFileSync('./iteration.js')).code);
var nested = eval(babel.transform(fs.readFileSync('./nested.js')).code);
var suite = new Benchmark.Suite('expand properties');
suite.add('status quo', function () {
before(BENCHMARK_STRING, K);
});
suite.add('comma fix', function() {
comma(BENCHMARK_STRING, K);
});
suite.add('alternate fix', function() {
alternate(BENCHMARK_STRING, K);
});
suite.add('regex match fix', function() {
matches(BENCHMARK_STRING, K);
});
suite.add('iteration fix', function() {
iteration(BENCHMARK_STRING, K);
});
suite.add('nested', function () {
nested(BENCHMARK_STRING, K);
});
suite.on('cycle', function(event) {
console.log(String(event.target));
});
suite.run({ async: true });
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
const properties = [pattern];
let bookmark, inside = false;
for (let i = pattern.length; i > 0; --i) {
let current = pattern[i - 1];
switch(current) {
case '}':
if (!inside) {
bookmark = i - 1;
inside = true;
}
break;
case '{':
if (inside) {
const expansion = pattern.slice(i, bookmark).split(',');
for (let j = properties.length; j > 0; --j) {
let [property] = properties.splice(j - 1, 1);
for (let k = 0; k < expansion.length; ++k) {
properties.push(property.slice(0, i - 1) +
expansion[k] +
property.slice(bookmark + 1));
}
}
inside = false;
}
break;
default:
break;
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]'));
}
}
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var EXPAND_REGEX = /\{([^}]*)\}/g;
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
const processed = [pattern];
let match;
let slack = 0;
while(match = EXPAND_REGEX.exec(pattern)) {
let focus = processed.pop();
processed.push(focus.slice(0, match.index - slack));
processed.push(match[1].split(','));
processed.push(focus.substring(match.index + match[0].length - slack));
slack = match.index + match[0].length;
}
const properties = [processed[0]];
for (let i = 1; i < processed.length; ++i) {
let strOrArray = processed[i];
for (let j = properties.length; j > 0; --j) {
if (typeof strOrArray === 'string') {
properties[j - 1] += strOrArray;
} else {
let currentProperty = properties[j - 1];
properties.splice(j - 1, 1, ...strOrArray.map((str) => currentProperty + str));
}
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]'));
}
}
// import { assert } from 'ember-metal/debug';
/**
@module ember
@submodule ember-metal
*/
var END_WITH_EACH_REGEX = /\.@each$/;
/**
Expands `pattern`, invoking `callback` for each expansion.
The only pattern supported is brace-expansion, anything else will be passed
once to `callback` directly.
Example
```js
function echo(arg){ console.log(arg); }
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar'
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
Ember.expandProperties('foo.{bar.{biz,baz},[]}', echo) //=> 'foo.bar.biz', 'foo.bar.baz', 'foo.[]'
```
@method expandProperties
@for Ember
@private
@param {String} pattern The property pattern to expand.
@param {Function} callback The callback to invoke. It is invoked once per
expansion, and is passed the expansion.
*/
export default function expandProperties(pattern, callback) {
// assert('A computed property key must be a string', typeof pattern === 'string');
// assert(
// 'Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"',
// pattern.indexOf(' ') === -1
// );
var properties = [pattern];
for (let property, i = 0; i < properties.length; i++) {
for (let match; (property = properties[i]) && (match = matchOuterMostBraces(property));) {
properties.splice(i, 1, ...duplicateAndReplace(property, match));
}
}
for (let i = 0; i < properties.length; i++) {
callback(properties[i].replace(END_WITH_EACH_REGEX, '.[]'));
}
}
function duplicateAndReplace(property, match) {
var all = [];
var parts = splitOutsideBraces(match[1], ',');
parts.forEach((part) => {
all.push(property.substring(0, match.index) +
part +
property.substring(match.index + match[0].length));
});
return all;
}
function splitOutsideBraces(string, splitChar) {
var parts = [];
// search for commas to split on
for (var stack = 0, start = 0, end = 0; end < string.length; ++end) {
switch (string[end]) {
case '{':
// enter brace expansion
++stack;
break;
case '}':
// exit brace expansion
stack = Math.max(stack - 1, 0);
break;
case splitChar[0]:
// split on given character, but only if not inside a brace expansion
// then restart search after the character
if (!stack) {
parts.push(string.slice(start, end));
start = end + 1;
}
break;
default:
// any other character is a no-op
break;
}
}
// add the last part
if (start !== end) {
parts.push(string.slice(start, end));
}
return parts;
}
function matchOuterMostBraces(string) {
let openIndex = string.indexOf('{');
// ensure there is at least one brace expansion
if (openIndex < 0) {
return null;
}
// search for the matching brace for this expansion
let closeIndex = openIndex;
for (let stack = 1; stack > 0 && ++closeIndex < string.length;) {
switch (string[closeIndex]) {
case '{':
// enter brace expansion
++stack;
break;
case '}':
// exit brace expansion
--stack;
break;
default:
// any other character is a no-op
break;
}
}
// ensure there is a matching brace
if (closeIndex >= string.length) {
return null;
}
// simulate a regex match object, with a capturing group inside the outer braces
let match = [string.slice(openIndex, closeIndex + 1), string.slice(openIndex + 1, closeIndex)];
match.index = openIndex;
match.input = string;
return match;
}

$ node index.js
status quo x 183,314 ops/sec ±7.01% (81 runs sampled)
comma fix x 88,004 ops/sec ±5.18% (81 runs sampled)
alternate fix x 88,608 ops/sec ±4.89% (84 runs sampled)
regex match fix x 36,670 ops/sec ±5.04% (83 runs sampled)
iteration fix x 175,519 ops/sec ±5.20% (84 runs sampled)
nested x 88,474 ops/sec ±3.91% (82 runs sampled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment