Skip to content

Instantly share code, notes, and snippets.

@jmacqueen
Created February 23, 2016 20:37
Show Gist options
  • Save jmacqueen/b8db614814fada543aa8 to your computer and use it in GitHub Desktop.
Save jmacqueen/b8db614814fada543aa8 to your computer and use it in GitHub Desktop.
Functional approach to subdividing list of option strings into optgroups based on greedy matching the beginning of the strings
/* jshint esversion: 6 */
// Two strings enter...one string leaves
const longerString = (a,b) => {
if (a.length > b.length) { return a; }
return b;
};
// The longest possible match from the beginnning of the strings is returned
const stringHeadMatch = function(a,b) {
var i = 0;
while (a.charAt(i) && b.charAt(i) && a.charAt(i) === b.charAt(i)) { i++; }
return a.substr(0, i);
};
// Match string at current array index against another relative entry
// includes guards to keep us in the array
const matchWith = (index, shift, strArr) => {
if ( index + shift < 0 || index + shift >= strArr.length ) return '';
return stringHeadMatch(strArr[index], strArr[index + shift]);
};
// return new array correlating each string with
// a) longer of the match with previous or match with next
// b) if no matches, use the string itself as the match
// looks like [[match, string], [match, string], [string, string], ...]
const maxMatches = (stringArray) => stringArray.map( (str, index, strArr) => {
const match = matchPrevOrNextMax(strArr, index);
return [(match === '') ? str : match, str];
});
// returns a single array with all options sharing a "group"...or in this case, the second part of all pairs sharing the same first part
// maxMatchArray expects the form [[string, option],[string, option],[string, option],...] like we get from maxMatches
const optionsMatchingGroup = (groupName, maxMatchArray) => maxMatchArray.filter( (item) => item[0] === groupName ).map( (item) => item[1] );
// returns the longer string head match with the previous and next strings in the array
const matchPrevOrNextMax = (strArr, index) => {
return longerString( matchWith(index, 1, strArr), matchWith(index, -1, strArr) );
};
// Pretty much what it says. The string gets the first group.length characters removed
const trimHeadOfStringByStringLength = (str1, str2) => {
const str3 = str2 || '';
return str1.slice(str3.length);
};
// returns an array of objects describing the option groups and options
// [ {group: string, options: [string, string]}, {group: null, options: [string]} ]
const groupedOptions = function(stringArray) {
const maxMatchArray = maxMatches(stringArray);
// yields {groupName: count}
const groupCounts = maxMatchArray.reduce( (memo, item) => {
memo[item[0]] = memo[item[0]] || 0;
memo[item[0]] += 1;
return memo;
}, {});
return Object.keys(groupCounts).reduce( (memo, group) => {
const groupName = (groupCounts[group] > 1) ? group : null;
memo.push({group: groupName, options: optionsMatchingGroup(group, maxMatchArray)});
return memo;
}, []);
};
// converts a single option group object to <optgroup> and <option>s HTML string
const optionsGroupToHTML = (optGroup) => {
const optionHTML = optGroup.options.reduce( (memo, opt) => {
memo += '<option>' + trimHeadOfStringByStringLength(opt, optGroup.group) + '</option>';
return memo;
}, '');
if (optGroup.group) {
return `<optgroup label="${optGroup.group}">${optionHTML}</optgroup>`;
}
return optionHTML;
};
// iterate over the whole array of option groups and concatenate them into a string of HTML
const makeSelect = (stringArray) => {
return groupedOptions(stringArray).reduce( (memo, group) => memo += optionsGroupToHTML(group) , '<select>' ) + '</select>';
};
const arr = [
'ABC - abc',
'ABC - def',
'ABCDEF - abc',
'ABCDEF - def',
'ABCDEF - ghi',
'ABCDEF - jkl',
'ABCDEFGH - abc',
'CBA - def',
'DEF - def'
];
// console.log(JSON.stringify( groupedOptions(arr), null, 2 ));
console.log( makeSelect(arr) );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment