Skip to content

Commit 39f1c7a

Browse files
Parse string array into common heads
1 parent 2beeb81 commit 39f1c7a

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@
3232
"ts"
3333
]
3434
},
35-
"dependencies": {},
35+
"dependencies": {
36+
"lodash": "^4.17.10"
37+
},
3638
"devDependencies": {
3739
"@babel/cli": "^7.0.0-beta.54",
3840
"@babel/core": "^7.0.0-beta.54",
3941
"@babel/preset-env": "^7.0.0-beta.54",
4042
"@babel/preset-typescript": "^7.0.0-beta.54",
4143
"@types/jest": "^23.3.1",
44+
"@types/lodash": "^4.14.114",
4245
"husky": "^0.14.3",
4346
"jest": "^23.4.1",
4447
"prettier": "^1.13.7",

src/utils/string.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { groupByCommonHead } from './string';
2+
3+
describe('groupByCommonHead', () => {
4+
it('returns empty object when no words', () => {
5+
expect(groupByCommonHead([])).toEqual({});
6+
});
7+
8+
it('returns the entirety of words when no common head', () => {
9+
const words = ['bar', 'foo'];
10+
expect(groupByCommonHead(words)).toEqual({
11+
bar: {},
12+
foo: {},
13+
});
14+
});
15+
16+
it('returns the entirety of repeated equal words', () => {
17+
const words = ['foo', 'foo', 'foo', 'foo'];
18+
expect(groupByCommonHead(words)).toEqual({
19+
foo: {},
20+
});
21+
});
22+
23+
it('returns the entirety of multiple, repeated equal words', () => {
24+
const words = ['bar', 'bar', 'bar', 'bar', 'foo', 'foo', 'foo', 'foo'];
25+
expect(groupByCommonHead(words)).toEqual({
26+
bar: {},
27+
foo: {},
28+
});
29+
});
30+
31+
it('returns an empty string for partial words', () => {
32+
const words = ['foo', 'foobar'];
33+
expect(groupByCommonHead(words)).toEqual({
34+
foo: { '': {}, bar: {} },
35+
});
36+
});
37+
38+
it('returns the common head of two words', () => {
39+
const words = ['bar', 'baz'];
40+
expect(groupByCommonHead(words)).toEqual({
41+
ba: { r: {}, z: {} },
42+
});
43+
});
44+
45+
it('returns multiple depths of partial words', () => {
46+
const words = ['foo', 'foobar', 'foobaz'];
47+
expect(groupByCommonHead(words)).toEqual({
48+
foo: { '': {}, ba: { r: {}, z: {} } },
49+
});
50+
});
51+
52+
it('returns the common head of multiple words', () => {
53+
const words = ['bar', 'baz', 'quux', 'quuz'];
54+
expect(groupByCommonHead(words)).toEqual({
55+
ba: { r: {}, z: {} },
56+
quu: { x: {}, z: {} },
57+
});
58+
});
59+
60+
it('preserves leading whitespace', () => {
61+
const words = [' foo', 'foo'];
62+
expect(groupByCommonHead(words)).toEqual({
63+
' foo': {},
64+
foo: {},
65+
});
66+
});
67+
68+
it('preserves trailing whitespace', () => {
69+
const words = ['foo ', 'foo'];
70+
expect(groupByCommonHead(words)).toEqual({
71+
foo: { '': {}, ' ': {} },
72+
});
73+
});
74+
75+
it('preserves mid-word whitespace', () => {
76+
const words = ['foo bar', 'foobar'];
77+
expect(groupByCommonHead(words)).toEqual({
78+
foo: { ' bar': {}, bar: {} },
79+
});
80+
});
81+
});

src/utils/string.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { keys, map, partition, uniq } from 'lodash';
2+
3+
type Char = string;
4+
interface ICharTree extends Record<Char, CharLeaf> {}
5+
type CharLeaf = ICharTree;
6+
7+
const terminator: ICharTree = {};
8+
9+
/**
10+
* Arrange a head character and its suffixes into a tree.
11+
* Flatten leaves of the character tree containing a single letter.
12+
* For example, { f: { o: { o: { '': {}, bar: {} } } } } flattens
13+
* to { foo: { '': {}, bar: {} } }
14+
*
15+
* @param headChar A character prefix
16+
* @param tailGroup A character tree of suffixes to headChar
17+
* @returns A character tree with tailGroup branching from headChar
18+
*/
19+
function mergeGroups(headChar: Char, tailGroup: ICharTree): ICharTree {
20+
const tails = keys(tailGroup);
21+
if (tails.length > 1) {
22+
return { [headChar]: tailGroup };
23+
}
24+
25+
const onlyTail = tails[0];
26+
return { [headChar + onlyTail]: tailGroup[onlyTail] };
27+
}
28+
29+
/**
30+
* Parse a list of words to build a tree of common prefixes
31+
*
32+
* @param words A list of words to parse
33+
* @returns A tree of words grouped by the initial characters they share
34+
*/
35+
function groupUniqueByCommonHead(words: string[]): ICharTree {
36+
if (words.length === 0) {
37+
return terminator;
38+
}
39+
40+
const wordToMatch = words[0];
41+
42+
if (wordToMatch === '') {
43+
// End of the target word reached. Include an empty string to signify that
44+
// a word ends at this spot, and group any remaining words in the tree.
45+
const [, nonEmptyWords] = partition(words, word => word === '');
46+
return { '': terminator, ...groupByCommonHead(nonEmptyWords) };
47+
}
48+
49+
// Begin a new tree containing all words starting with the same letter as wordToMatch
50+
const charToMatch = wordToMatch[0];
51+
const [wordsMatched, wordsMissed] = partition(words, ['[0]', charToMatch]);
52+
53+
const tailsMatched = map(wordsMatched, word => word.substring(1));
54+
const tailsMatchedGrouped = groupByCommonHead(tailsMatched);
55+
56+
const groupWithChildren = mergeGroups(charToMatch, tailsMatchedGrouped);
57+
58+
return { ...groupWithChildren, ...groupByCommonHead(wordsMissed) };
59+
}
60+
61+
/** @borrows groupUniqueByCommonHead as groupByCommonHead */
62+
export function groupByCommonHead(words: string[]): ICharTree {
63+
const uniqueWords = uniq(words);
64+
return groupUniqueByCommonHead(uniqueWords);
65+
}

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,10 @@
547547
version "23.3.1"
548548
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf"
549549

550+
"@types/lodash@^4.14.114":
551+
version "4.14.114"
552+
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.114.tgz#e6f251af5994dd0d7ce141f9241439b4f40270f6"
553+
550554
abab@^1.0.4:
551555
version "1.0.4"
552556
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"

0 commit comments

Comments
 (0)