I am trying to symmetrically split a string in JavaScript. By that I mean, if it is 8 characters, then divide it into chunks of 2. If it is 9 characters, into chunks of 3. If it is n characters, divide it into chunks of 2 and 3 so that it forms a mirror image in the center.
const segmentNumber = (value) => {
let array = value.split('')
if (array.length % 3 === 0) {
return chunkArray(array, 3).join('·')
} else if (array.length % 2 === 0) {
return chunkArray(array, 2).join('·')
} else {
let reversed = array.slice().reverse()
let a = 0
let ar = []
let zr = []
while (true) {
ar.push(array.slice(a, a + 2).join(''))
zr.push(reversed.slice(a, a + 2).join(''))
array = array.slice(a + 2)
reversed = reversed.slice(a + 2)
a += 2
let modulus
if (array.length % 3 === 0) {
modulus = 3
} else if (array.length % 2 === 0) {
modulus = 2
}
if (modulus) {
return ar.concat(chunkArray(array, modulus)).concat(zr.reverse())
}
}
}
return
}
function chunkArray(arr, len) {
var chunks = [],
i = 0,
n = arr.length;
while (i < n) {
chunks.push(arr.slice(i, i += len));
}
return chunks;
}
I can chunk the string but not sure how to make it symmetrically chunked. For example:
How do you generate this sort of pattern, where the smaller numbers are on the outside, and the bigger number 3 is toward the center, so it forms a mirror image?
How do I improve what I am sort of implementing?
a solver that solves by calling itself
Recursion and generators are the natural solution to this problem. We can use inductive reasoning to write segment(t) for any number, t -
t
is less than zero, there are no valid segments. Stop iterationt
is non-negative. If t
is zero, yield the empty segment, []
t
is positive. Yield the singleton segment, [t]
, and for each i
of the range 1..t
, for each segment s
of the sub-problem t - i * 2
, symmetrically prepend and append i
to the segment and yieldfunction* segment(t) {
if (t < 0) return // 1
if (t == 0) return yield [] // 2
yield [t] // 3
for (let i = 1; i < t; i++)
for (const s of segment(t - i * 2))
yield [i, ...s, i]
}
console.log("== 7 ==")
for (const s of segment(7))
console.log(s.join(" · "))
console.log("== 8 ==")
for (const s of segment(8))
console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }
== 7 ==
7
1 · 5 · 1
1 · 1 · 3 · 1 · 1
1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 2 · 1 · 2 · 1
2 · 3 · 2
2 · 1 · 1 · 1 · 2
3 · 1 · 3
== 8 ==
8
1 · 6 · 1
1 · 1 · 4 · 1 · 1
1 · 1 · 1 · 2 · 1 · 1 · 1
1 · 1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 1 · 2 · 2 · 1 · 1
1 · 2 · 2 · 2 · 1
1 · 2 · 1 · 1 · 2 · 1
1 · 3 · 3 · 1
2 · 4 · 2
2 · 1 · 2 · 1 · 2
2 · 1 · 1 · 1 · 1 · 2
2 · 2 · 2 · 2
3 · 2 · 3
3 · 1 · 1 · 3
4 · 4
why t - i * 2
?
The sub-problem is t - i * 2
, or t
minus two i
s. The subsequent yield expression is adding two i
to the result, so they must be subtracted from the sub-problem in order to balance the equation -
for (const s of segment(t - i * 2)) // subtract two i from t
yield [i, ...s, i] // add two i to the segment
why recursive technique?
The advantages to this approach are numerous -
static, arbitrary split sizes | ❌ |
only 2's and 3's | ❌ |
modulus arithmetic | ❌ |
conditional variable assignments | ❌ |
array .slice or .reverse |
❌ |
hard-coded .join |
❌ |
string-to-number conversion | ❌ |
parseInt |
❌ |
division and rounding | ❌ |
every combination found | ✅ |
two local variables | ✅ |
two simple if conditions |
✅ |
pause, resume, or cancel | ✅ |
visualization
Given segment(4)
-
[4] // t = 4
[1, , 1]
\ /
...[2] // t = 2
[1, , 1]
\ /
...[1, , 1]
\ /
...[] // t = 0
[2, , 2]
\ /
...[] // t = 0
[3, , 3]
\ /
...❌ // t = -2
[4]
[1,2,1]
[1,1,1,1]
[2,2]
change output ordering
This does not require a surgical rewiring of the algorithm or adjustment in the way you think about the problem. By changing the order of the yield
expressions, you change the order of the output -
function* segment(t) {
if (t < 0) return
if (t == 0) return yield []
for (let i = 1; i < t; i++)
for (const s of segment(t - i * 2))
yield [i, ...s, i]
yield [t] // ✅ yield singleton segment last
}
console.log("== 7 ==")
for (const s of segment(7))
console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }
== 7 ==
1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 1 · 3 · 1 · 1
1 · 2 · 1 · 2 · 1
1 · 5 · 1
2 · 1 · 1 · 1 · 2
2 · 3 · 2
3 · 1 · 3
7
minimum segment length
Perhaps you want the smallest segment to be 2
or 3
. By adding a min
parameter, this can be decided by the caller instead of hard coding it into the segment
function -
function* segment(t, min = 0) { // ✅ add min parameter
if (t < min) return // ✅ t < min
if (t == 0) return yield []
for (let i = Math.max(1, min); i < t; i++) // ✅ max(1, min)
for (const s of segment(t - i * 2, min)) // ✅ pass min
yield [i, ...s, i]
yield [t]
}
console.log("== 18 ==")
for (const s of segment(18, 3)) // ✅ segments of 3 or greater
console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }
maximum segment length
In a similar fashion, a max
parameter can be added to control the maximum segment length. Avoid hard-coding this in the function to retain increased flexibility for the caller -
function* segment(t, min = 0, max = t) { // ✅ add max parameter
if (t < min) return
if (t == 0) return yield []
for (let i = Math.max(1, min); i < t; i++)
for (const s of segment(t - i * 2, min, max)) // ✅ pass max
yield [i, ...s, i]
if (t <= max) yield [t] // ✅ if (t <= max)
}
console.log("== 18 ==")
for (const s of segment(18, 3, 8)) // ✅ segments between 3 and 8
console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }
== 18 ==
3 · 3 · 6 · 3 · 3
3 · 4 · 4 · 4 · 3
4 · 3 · 4 · 3 · 4
5 · 8 · 5
6 · 6 · 6
7 · 4 · 7
increasing toward the center
If you would like the numbers to increase toward the center, that is a matter of adding yet another configurable parameter, init
. As you can see, each nuanced criteria can be carefully added with only minor adjustments to the original algorithm -
function* segment(t, min = 0, max = t, init = 1) { // ✅ init = 1
if (t < min || t < init) return // ✅ || t < init
if (t == 0) return yield []
for (let i = Math.max(init, min); i < t; i++) // ✅ init
for (const s of segment(t - i * 2, min, max, i + 1)) // ✅ i + 1
yield [i, ...s, i]
if (t <= max) yield [t]
}
console.log("== 36 ==")
for (const s of segment(36, 2, 9))
console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }
== 36 ==
2 · 3 · 4 · 5 · 8 · 5 · 4 · 3 · 2
2 · 5 · 7 · 8 · 7 · 5 · 2
3 · 4 · 7 · 8 · 7 · 4 · 3
3 · 5 · 6 · 8 · 6 · 5 · 3
split the string
To split a string we can write split
which takes a string and a value returned by segment
-
const split = (t, [s, ...segments]) =>
s == null
? []
: [t.substring(0, s), ...split(t.substring(s), segments)]
function* segment(t) {
if (t < 0) return
if (t == 0) return yield []
for (let i = 1; i < t; i++)
for (const s of segment(t - i * 2))
yield [i, ...s, i]
yield [t]
}
const word = "function"
for (const s of segment(word.length)) // ✅ segment word.length
console.log(split(word, s).join(" · ")) // ✅ split word using s
.as-console-wrapper { min-height: 100%; top: 0; }
f · u · n · c · t · i · o · n
f · u · n · ct · i · o · n
f · u · nc · ti · o · n
f · u · ncti · o · n
f · un · c · t · io · n
f · un · ct · io · n
f · unc · tio · n
f · unctio · n
fu · n · c · t · i · on
fu · n · ct · i · on
fu · nc · ti · on
fu · ncti · on
fun · c · t · ion
fun · ct · ion
func · tion
function
optimization
You could speed up the algorithm by eliminating the check for some combinations. The outer for
loop goes from 1..t
but could be shortened to 1..Math.floor(t/2)
. This improves the performance of segment
but adds some complexity. For sake of clarity this was left out and remains an update for the reader.
without generators
Although generators are a great fit for combinatorics problems, they are not required. We can lift the entire program into an Array context and have segment
return an array of results instead of an iterator. Notice the ergonomics are not as good and the data nesting level has forcibly been increased by one. It does however follow the same inductive reasoning as the original algorithm -
function segment(t) {
if (t < 0) return [] // if (t < 0) return
if (t == 0) return [[]] // if (t == 0) return yield []
return [ //
[t], // yield [t]
...range(1, t).flatMap(i => // for (let i = 0; i<t; i++)
segment(t - i * 2).map(s => // for (const s of segment(t - i * 2))
[[i, ...s, i]] // yield [i, ...s, i]
)
)
]
}
function range(a, b) {
return Array.from(Array(b - a), (_, n) => n + a)
}
console.log("== 7 ==")
for (const s of segment(7))
console.log(s.join(" · "))
== 7 ==
7
1,5,1
1,1,3,1,1
1,1,1,1,1,1,1
1,2,1,2,1
2,3,2
This will produce pattern for numbers greater than 6, and always have at least one '2' on the outside (except 9)
const splitter = n => {
if (n < 7) {
throw 'bad'
}
let twos = 0;
let threes = Math.ceil(n / 3);
// 9 is an edge case
if (n !== 9) {
let remain;
do {
--threes;
remain = n - threes * 3;
} while (remain % 4);
if (threes < 0) {
threes = n / 3;
remain = 0;
}
twos = remain / 4;
}
return `${'2'.repeat(twos)}${'3'.repeat(threes)}${'2'.repeat(twos)}`.split('');
}
for (let i = 7; i < 50; ++i) console.log(i, splitter(i).join('-'))
hmmm, I see you don't have 2's in all your cases
Simple change ...
const splitter = n => {
if (n < 7) {
throw 'bad'
}
let twos = 0;
let threes = Math.floor(n / 3) + 1;
// 9 is an edge case
let remain;
do {
--threes;
remain = n - threes * 3;
} while (remain % 4);
if (threes < 0) {
threes = n / 3;
remain = 0;
}
twos = remain / 4;
return `${'2'.repeat(twos)}${'3'.repeat(threes)}${'2'.repeat(twos)}`.split('');
}
for (let i = 7; i < 50; ++i) console.log(i, splitter(i).join('-'))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With