Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
slice-js so far...

slice-js

So far...

A talk by @inconshreveable at The Strange Loop 2016 called "Idealized Commit Logs: Code Simplification via Program Slicing" about the amazing tools that can be built with program slicing inspired me to work on this project. Learn more about program slicing here.

This is actual output from slice-js (not yet open source), a program slicing tool that I'm working on right now. I can't wait to work out more kinks, make it more practically useful and show it to you all!

The match-sorter.js file is the transpiled CommonJS version of match-sorter and you can find that here.

The match-sorter.sliced.js file is the sliced version of that file based on this call into the module:

function (matchSorter) {
  return matchSorter(['hi', 'hey', 'hello', 'sup', 'yo'], 'y')
}

Notice that this results in significant code savings. Imagine doing this same kind of thing with one function call to lodash! (I'm working on that) :)

The tests for slice-js do several things to ensure the slice is correct and I think it's pretty rad:

  1. Instruments the code you give it for coverage
  2. Runs the usage function you provide (as above) on that code
  3. Uses the original code and coverage data to create the program slice via a (growing) 400 line Babel plugin :) ❤️ Babel!
  4. Takes a snapshot of the slice for manual verification (❤️ Jest!)
  5. Instruments the slice for coverage
  6. Runs the usage function again, except this time on the sliced code
  7. Validates that the coverage of the slice is 100% (this ensures that there's nothing more to slice out)
  8. Validates that the result from the call to the original file and the call to the sliced code is the same (ensuring that the sliced version wouldn't break your usage).

My mind is racing with the potential for a tool like this. So many practical applications. I can't wait to work out more of the bugs, make the tool more useful than just an experiment, and release an alpha. When is it coming? I don't know. I hope soon. Do you want to help? Let me know

'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; /**
* @name match-sorter
* @license MIT license.
* @copyright (c) 2016 Kent C. Dodds
* @author Kent C. Dodds <kent@doddsfamily.us>
*/
var _diacritic = require('diacritic');
var _diacritic2 = _interopRequireDefault(_diacritic);
var _globalObject = require('global-object');
var _globalObject2 = _interopRequireDefault(_globalObject);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var rankings = {
EQUAL: 6,
STARTS_WITH: 5,
WORD_STARTS_WITH: 4,
CONTAINS: 3,
ACRONYM: 2,
MATCHES: 1,
NO_MATCH: 0
};
matchSorter.rankings = rankings;
/**
* Takes an array of items and a value and returns a new array with the items that match the given value
* @param {Array} items - the items to sort
* @param {String} value - the value to use for ranking
* @param {Object} options - Some options to configure the sorter
* @return {Array} - the new sorted array
*/
function matchSorter(items, value) {
var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var keys = options.keys;
var _options$threshold = options.threshold;
var threshold = _options$threshold === undefined ? rankings.MATCHES : _options$threshold;
var matchedItems = items.reduce(reduceItemsToRanked, []);
return matchedItems.sort(sortRankedItems).map(function (_ref) {
var item = _ref.item;
return item;
});
function reduceItemsToRanked(matches, item, index) {
var _getHighestRanking = getHighestRanking(item, keys, value, options);
var rank = _getHighestRanking.rank;
var keyIndex = _getHighestRanking.keyIndex;
if (rank >= threshold) {
matches.push({ item: item, rank: rank, index: index, keyIndex: keyIndex });
}
return matches;
}
}
/**
* Gets the highest ranking for value for the given item based on its values for the given keys
* @param {*} item - the item to rank
* @param {Array} keys - the keys to get values from the item for the ranking
* @param {String} value - the value to rank against
* @param {Object} options - options to control the ranking
* @return {Number} - the highest ranking
*/
function getHighestRanking(item, keys, value, options) {
if (!keys) {
return { rank: getMatchRanking(item, value, options), keyIndex: -1 };
}
var valuesToRank = getAllValuesToRank(item, keys);
return valuesToRank.reduce(function (_ref2, itemValue, i) {
var rank = _ref2.rank;
var keyIndex = _ref2.keyIndex;
var newRank = getMatchRanking(itemValue, value, options);
if (newRank > rank) {
rank = newRank;
keyIndex = i;
}
return { rank: rank, keyIndex: keyIndex };
}, { rank: rankings.NO_MATCH, keyIndex: -1 });
}
/**
* Gives a rankings score based on how well the two strings match.
* @param {String} testString - the string to test against
* @param {String} stringToRank - the string to rank
* @param {Object} options - options for the match (like keepDiacritics for comparison)
* @returns {Number} the ranking for how well stringToRank matches testString
*/
function getMatchRanking(testString, stringToRank, options) {
/* eslint complexity:[2, 8] */
testString = prepareValueForComparison(testString, options);
stringToRank = prepareValueForComparison(stringToRank, options);
// too long
if (stringToRank.length > testString.length) {
return rankings.NO_MATCH;
}
// equals
if (testString === stringToRank) {
return rankings.EQUAL;
}
// starts with
if (testString.indexOf(stringToRank) === 0) {
return rankings.STARTS_WITH;
}
// word starts with
if (testString.indexOf(' ' + stringToRank) !== -1) {
return rankings.WORD_STARTS_WITH;
}
// contains
if (testString.indexOf(stringToRank) !== -1) {
return rankings.CONTAINS;
} else if (stringToRank.length === 1) {
// If the only character in the given stringToRank
// isn't even contained in the testString, then
// it's definitely not a match.
return rankings.NO_MATCH;
}
// acronym
if (getAcronym(testString).indexOf(stringToRank) !== -1) {
return rankings.ACRONYM;
}
return stringsByCharOrder(testString, stringToRank);
}
/**
* Generates an acronym for a string.
*
* @param {String} string the string for which to produce the acronym
* @returns {String} the acronym
*/
function getAcronym(string) {
var acronym = '';
var wordsInString = string.split(' ');
wordsInString.forEach(function (wordInString) {
var splitByHyphenWords = wordInString.split('-');
splitByHyphenWords.forEach(function (splitByHyphenWord) {
acronym += splitByHyphenWord.substr(0, 1);
});
});
return acronym;
}
/**
* Returns a rankings.matches or noMatch score based on whether
* the characters in the stringToRank are found in order in the
* testString
* @param {String} testString - the string to test against
* @param {String} stringToRank - the string to rank
* @returns {Number} the ranking for how well stringToRank matches testString
*/
function stringsByCharOrder(testString, stringToRank) {
var charNumber = 0;
function findMatchingCharacter(matchChar, string) {
var found = false;
for (var j = charNumber; j < string.length; j++) {
var stringChar = string[j];
if (stringChar === matchChar) {
found = true;
charNumber = j + 1;
break;
}
}
return found;
}
for (var i = 0; i < stringToRank.length; i++) {
var matchChar = stringToRank[i];
var found = findMatchingCharacter(matchChar, testString);
if (!found) {
return rankings.NO_MATCH;
}
}
return rankings.MATCHES;
}
/**
* Sorts items that have a rank, index, and keyIndex
* @param {Object} a - the first item to sort
* @param {Object} b - the second item to sort
* @return {Number} -1 if a should come first, 1 if b should come first
* Note: will never return 0
*/
function sortRankedItems(a, b) {
var aFirst = -1;
var bFirst = 1;
var aRank = a.rank;
var aIndex = a.index;
var aKeyIndex = a.keyIndex;
var bRank = b.rank;
var bIndex = b.index;
var bKeyIndex = b.keyIndex;
var same = aRank === bRank;
if (same) {
if (aKeyIndex === bKeyIndex) {
return aIndex < bIndex ? aFirst : bFirst;
} else {
return aKeyIndex < bKeyIndex ? aFirst : bFirst;
}
} else {
return aRank > bRank ? aFirst : bFirst;
}
}
/**
* Prepares value for comparison by stringifying it, removing diacritics (if specified), and toLowerCase-ing it
* @param {String} value - the value to clean
* @param {Object} options - {keepDiacritics: whether to remove diacritics}
* @return {String} the prepared value
*/
function prepareValueForComparison(value, _ref3) {
var keepDiacritics = _ref3.keepDiacritics;
value = '' + value; // toString
if (!keepDiacritics) {
value = _diacritic2.default.clean(value);
}
return value.toLowerCase();
}
/**
* Gets value for key in item at arbitrarily nested keypath
* @param {Object} item - the item
* @param {Object|Function} key - the potentially nested keypath or property callback
* @return {String} - the value at nested keypath
*/
function getItemValue(item, key) {
if (typeof key === 'function') {
return key(item);
}
var isNested = key.indexOf('.') !== -1;
if (!isNested) {
return item[key];
}
return key.split('.').reduce(function (itemObj, nestedKey) {
return itemObj[nestedKey];
}, item);
}
/**
* Gets all the values for the given keys in the given item and returns an array of those values
* @param {Object} item - the item from which the values will be retrieved
* @param {Array} keys - the keys to use to retrieve the values
* @return {Array} the values in an array
*/
function getAllValuesToRank(item, keys) {
return keys.reduce(function (allVals, key) {
return allVals.concat(getItemValue(item, key));
}, []);
}
// some manual ✨ magic umd ✨ here because Rollup isn't capable of exposing our module the way we want
// see dist-test/index.js
/* istanbul ignore next */
if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object' && typeof module !== 'undefined') {
matchSorter.default = matchSorter;
module.exports = matchSorter;
Object.defineProperty(exports, '__esModule', { value: true });
} else if (typeof define === 'function' && define.amd) {
// eslint-disable-line
define(function () {
return matchSorter;
}); // eslint-disable-line
} else {
_globalObject2.default.matchSorter = matchSorter; // eslint-disable-line
}
'use strict';
/**
* @name match-sorter
* @license MIT license.
* @copyright (c) 2016 Kent C. Dodds
* @author Kent C. Dodds <kent@doddsfamily.us>
*/
var _diacritic = require('diacritic');
var _diacritic2 = _interopRequireDefault(_diacritic);
var _globalObject = require('global-object');
function _interopRequireDefault(obj) {
return { default: obj };
}
var rankings = {
EQUAL: 6,
STARTS_WITH: 5,
WORD_STARTS_WITH: 4,
CONTAINS: 3,
ACRONYM: 2,
MATCHES: 1,
NO_MATCH: 0
};
matchSorter.rankings = rankings;
function matchSorter(items, value) {
var options = {};
var keys = options.keys;
var threshold = rankings.MATCHES;
var matchedItems = items.reduce(function (matches, item, index) {
var _getHighestRanking = getHighestRanking(item, keys, value, options);
var rank = _getHighestRanking.rank;
var keyIndex = _getHighestRanking.keyIndex;
if (rank >= threshold) {
matches.push({ item: item, rank: rank, index: index, keyIndex: keyIndex });
}
return matches;
}, []);
return matchedItems.sort(sortRankedItems).map(function (_ref) {
var item = _ref.item;
return item;
});
}
function getHighestRanking(item, keys, value, options) {
return { rank: getMatchRanking(item, value, options), keyIndex: -1 };
}
function getMatchRanking(testString, stringToRank, options) {
testString = prepareValueForComparison(testString, options);
stringToRank = prepareValueForComparison(stringToRank, options);
if (testString.indexOf(stringToRank) === 0) {
return rankings.STARTS_WITH;
}
if (testString.indexOf(stringToRank) !== -1) {
return rankings.CONTAINS;
} else {
return rankings.NO_MATCH;
}
}
function sortRankedItems(a, b) {
var aRank = a.rank;
var bRank = b.rank;
return 1;
}
function prepareValueForComparison(value, _ref3) {
value = '' + value;
value = _diacritic2.default.clean(value);
return value.toLowerCase();
}
matchSorter.default = matchSorter;
module.exports = matchSorter;
Object.defineProperty(exports, '__esModule', { value: true });
Owner

kentcdodds commented Sep 28, 2016

I noticed that there are a few statements in the slice that don't really need to be (like b.index;). Like I said, it's definitely still a work in progress!

Holy crap. This is amazing. I would love to be a part of this when it lands in a repo. Great work! 👍

Awesome

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