This answer assumes that my suggestion in the comments was correct, namely, that you want something that checks all permutations of the input array for a run of letters. That is, if we're given ['aa', 'ba', 'ac']
, then we want to check 'aabaac'
, 'aaacba'
, 'baaaac'
, 'baacaa'
, 'acaaba'
, and 'acbaaa'
to find that 'baaaac'
contains a run of four a
's. If this is not the case, then feel free to ignore this one.
Breaking it down
To do this, I would like to break it down in parts. I would like to write a function that takes the input strings, finds all permutations of these strings, concatenates the strings in each permutation into a single string, finds the longest single-letter streak in each one, and then selects the result with the longest streak.
I would like a main function, then, that looks like this:
const longestSingleLetterSequence = (arr) =>
maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))
which would be used like this:
longestSingleLetterSequence (['aa', 'ba', 'ac'])
//=> {"char": "a", "streak": 4, "word": "baaaac"}
But this means we need to write three helper functions, longestStreak
, permutations
, and maximumBy
. Note that the last two might be genuinely reusable functions.
So let's look at creating these functions. We can write permutations
this way:
const permutations = (xs) =>
xs .length == 0
? [[]]
: xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => x + p))
by using a simple helper excluding
, which takes an index and an array and returns a copy of that array excluding the value at that index.
permutations
firsts checks if the input array is empty. If it is, we simply return an array containing just the empty array. Otherwise, for each element, we remove it from the list, recursively permute the remaining elements, and prepend that initial one to each result.
Our helper is simple. It could be inlined into permutations
, but I think both are cleaner if they're separated this way. It might look like this:
const excluding = (i) => (xs) =>
[... xs .slice (0, i), ... xs .slice (i + 1)]
Then we can write maximumBy
, which takes a function and returns a function which takes an array and returns that element for which the function returns the highest value. It does this through a simple reduce
call:
const maximumBy = (fn) => (xs) =>
xs .reduce (({val, max}, x, i, _, xVal = fn (x)) =>
(i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
{}
) .val
It's worth noting that this is not specific to numbers. It works with any values that can be compared with <
, so numbers, strings, dates, or objects with valueOf
methods.
These function so far are generic utility functions we can easily imagine reusing across projects. The next one, longestStreak
, is more specific to this project:
const longestStreak = ([... chars]) => {
let {streakChar, streak} = chars .reduce (
({currChar, count, streakChar, streak}, char) => ({
currChar: char,
count: char == currChar ? count + 1 : 1,
streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
streak: Math .max (char == currChar ? count + 1 : 1, streak),
}),
{streak: -1}
)
return {char: streakChar, streak, word: chars .join ('')}
}
Like many maxima problems, we can do this with .reduce
. Here we restructure the input string into an array of characters with a parameter of [... cs]
. Then as we fold our array, we keep returning an object with the structure {currChar, count, streakChar, streak}
, which hold the currentChar we're tracking, the count of them seen so far, as well as the the character for the longest streak seen so far and that count. We start with an initial value of {streak: -1}
.
When we're through the string, we return the streak character and its length (as well as the word that contains it; unnecessary here, but seemingly useful for the larger problem.)
Here is what it looks like altogether:
// Utility functions
const excluding = (i) => (xs) =>
[... xs .slice (0, i), ... xs .slice (i + 1)]
const permutations = (xs) =>
xs .length == 0
? [[]]
: xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => [x, ... p]))
const maximumBy = (fn) => (xs) =>
xs .reduce (({val, max}, x, i, _, xVal = fn (x)) =>
(i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
{}
) .val
// Helper function
const longestStreak = ([... chars]) => {
let {streakChar, streak} = chars .reduce (
({currChar, count, streakChar, streak}, char) => ({
currChar: char,
count: char == currChar ? count + 1 : 1,
streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
streak: Math .max (char == currChar ? count + 1 : 1, streak),
}),
{streak: -1}
)
return {char: streakChar, streak, word: chars .join ('')}
}
// Main function
const longestSingleLetterSequence = (arr) =>
maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))
// Demos
console .log (longestSingleLetterSequence (['aa', 'ba', 'ac']))
console .log (longestSingleLetterSequence (["ccdd", "bbbb", "bbab"]))
.as-console-wrapper {max-height: 100% !important; top: 0}
Using additional helpers for clean-up
There is one thing I really don't like about the main function. You need to read very carefully to understand the order of operations:
const longestSingleLetterSequence = (arr) =>
maximumBy (x => x .streak) (permutations (arr) .map (xs => xs .join ('')) .map (longestStreak))
//`-------- step 4 --------' `---- step 1 ---' `--------- step 2 -------' `------ step 3 ------'
I'm one of the primary authors of Ramda, and it provides some very useful tools to help manage such complexity. But those tools are easy to write ourselves. So with a few additional helpers, I would actually write the main function this way:
const longestSingleLetterSequence = pipe (
permutations,
map (join ('')),
map (longestStreak),
maximumBy (prop ('streak'))
)
And here the steps just run sequentially line after line. I won't go into detail about these helper functions, but if you want to see it in action, you can expand this snippet:
// Utility functions
const pipe = (...fns) => (...args) =>
fns .slice (1) .reduce ((a, fn) => fn (a), fns[0] (...args))
const map = (fn) => (xs) => xs .map (x => fn (x))
const prop = (p) => (o) => o [p]
const join = (sep) => (xs) => xs .join (sep)
const excluding = (i) => (xs) => [... xs .slice (0, i), ... xs .slice (i + 1)]
const permutations = (xs) =>
xs .length == 0
? [[]]
: xs .flatMap ((x, i) => permutations (excluding (i) (xs)) .map (p => [x, ... p]))
const maximumBy = (fn) => (xs) =>
xs .reduce (({val, max}, x, i, _, xVal = fn (x)) =>
(i == 0 || xVal > max) ? {max: xVal, val: x} : {val, max},
{}
) .val
// Helper function
const longestStreak = ([... chars]) => {
let {streakChar, streak} = chars .reduce (
({currChar, count, streakChar, streak}, char) => ({
currChar: char,
count: char == currChar ? count + 1 : 1,
streakChar: (char == currChar ? count + 1 : 1) > streak ? currChar : streakChar,
streak: Math .max (char == currChar ? count + 1 : 1, streak),
}),
{streak: -1}
)
return {char: streakChar, streak, word: chars .join ('')}
}
// Main function
const longestSingleLetterSequence = pipe (
permutations,
map (join ('')),
map (longestStreak),
maximumBy (prop ('streak'))
)
// Demo
console .log (longestSingleLetterSequence (['aa', 'ba', 'ac']))
console .log (longestSingleLetterSequence (["ccdd", "bbbb", "bbab"]))
.as-console-wrapper {max-height: 100% !important; top: 0}