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:
- 10: 2-3-3-2
- 11: 2-2-3-2-2
- 12: 3-3-3-3
- 13: 2-3-3-3-2
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?
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:
- 10: 2-3-3-2
- 11: 2-2-3-2-2
- 12: 3-3-3-3
- 13: 2-3-3-3-2
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?
Share Improve this question edited May 4, 2022 at 2:28 Lance Pollard asked May 4, 2022 at 2:19 Lance PollardLance Pollard 79.7k98 gold badges333 silver badges611 bronze badges 10- Chunk into pairs (2) then rematch as many of them as possible so that you've got the desired output. – k.tten Commented May 4, 2022 at 2:27
- 1 is it only ever going to be 3's and 2's? – Bravo Commented May 4, 2022 at 2:28
- This is a quite vague question. It's not clear what the input can be. – Ram Commented May 4, 2022 at 2:32
- are there no restrictions like the most number of 3 should be chosen? as a certain length might have different binations of symmetrical chunks. like 18 which can be 333333 and 22233222. And based on your samples above, it can be possible that it can produce only 3s – NightEye Commented May 4, 2022 at 2:32
- 1 @Bravo, that may be the case i believe, optimizing the 3s inside then pad 2 outside if possible. – NightEye Commented May 4, 2022 at 2:34
5 Answers
Reset to default 9a 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 -
- If the input
t
is less than zero, there are no valid segments. Stop iteration - (inductive)
t
is non-negative. Ift
is zero, yield the empty segment,[]
- (inductive)
t
is positive. Yield the singleton segment,[t]
, and for eachi
of the range 1..t
, for each segments
of the sub-problemt - i * 2
, symmetrically prepend and appendi
to the segment and yield
function* 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 bination 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 binations. 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 plexity. For sake of clarity this was left out and remains an update for the reader.
without generators
Although generators are a great fit for binatorics 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('-'))
function segmentNumber(value)
{
for (let i = 0; i <= value; i++)
{
d = (value - 3 * i)/4
if (d >= 0 && d == parseInt(d))
{
return Array(d).fill(2).concat(Array(i).fill(3), Array(d).fill(2))
}
}
}
Does this meet your output? I am not sure with what your inputs are so I have only tested it on the sample values you provided instead.
function segmentNumber(length) {
let outside = 0;
if (length < 6)
return 'Only 6 and greater';
if(length % 3 == 0)
return new Array(length / 3).fill(3).join('-');
else {
while(length % 3 != 0 && length > 6) {
outside++;
length = length - 4;
}
if(length == 4)
return new Array(Math.floor(++outside * 2)).fill(2).join('-');
else
return [...new Array(outside).fill(2),
...new Array(Math.floor(length / 3)).fill(3),
...new Array(outside).fill(2)].join('-');
}
}
console.log(segmentNumber(10))
console.log(segmentNumber(11))
console.log(segmentNumber(12))
console.log(segmentNumber(13))
Criteria
From what I gathered from the OP and ments, the objective is return a symmetrical series of 2s and 3s from a given integer. So here's a few criteria I tried to follow:
From the start and end of the result are the 2s then the 3s are positioned up to the center:
10 => 2 3 3 2
Numbers that shouldn't be accepted are numbers lower than 4 and 5 as well. So a limit will be set for anything less than 6 (4 is symmetrical but I didn't want to write extra code to include it)
If the given number is odd, it will have a 3 in the center.
11 => 2 2 3 2 2
A symmetrical pattern only needs one half of a pattern to be built so divide the number by 2 (if odd parity subtract 3 before dividing by 2).
if (int < 6) return "Must be an integer greater than 5"; let parity = int & 1 ? 'odd' : 'even'; if (parity === 'odd') { int = int - 3; } side = int / 2;
Find how many iterations are needed to divide the number by 3 and then the remaining (if any) by 2.
The remaining number can only be a 4, 2, or 0 in order to maintain symmetry. If the remaining number is 1, then the total number of iterations to divide by 3 should be one less and the remaining number 1 will be a 4.
Two separate loops to run division by 3 and division by 2.
let temp = Math.floor(side / 3); let rem = side % 3; if (rem === 1) { temp = temp - 1; rem = 4; } for (let i = 0; i < temp; i++) { result.push(3); } for (let i = 0; i < rem / 2; i++) { result.push(2); }
Next, make a copy of the
result
array with.slice(0)
and.reverse()
it. Ifparity
was"odd"
thenunshift(3)
to the front ofresult
. Finally.concat()
the reversed array in front of theresult
array, and then.join(' ')
it into a string.const left = result.slice(0).reverse(); if (parity === 'odd') { result.unshift(3); } return left.concat(result).join(' ');
// Utility function
const log = (data, str = true) => {
if (!str) return console.log(data);
return console.log(JSON.stringify(data));
}
const x23 = int => {
let result = [];
let side, odd;
if (int < 6) return "Must be an integer greater than 5";
let parity = int & 1 ? 'odd' : 'even';
if (parity === 'odd') {
int = int - 3;
}
side = int / 2;
let temp = Math.floor(side / 3);
let rem = side % 3;
if (rem === 1) {
temp = temp - 1;
rem = 4;
}
for (let i = 0; i < temp; i++) {
result.push(3);
}
for (let i = 0; i < rem / 2; i++) {
result.push(2);
}
const left = result.slice(0).reverse();
if (parity === 'odd') {
left.push(3);
}
return left.concat(result).join(' ');
}
log('5: '+x23(5));
log('6: '+x23(6));
log('7: '+x23(7));
log('8: '+x23(8));
log('9: '+x23(9));
log('10: '+x23(10));
log('11: '+x23(11));
log('12: '+x23(12));
log('13: '+x23(13));
log('14: '+x23(14));
log('15: '+x23(15));
log('16: '+x23(16));
log('17: '+x23(17));
log('18: '+x23(18));
log('19: '+x23(19));
log('20: '+x23(20));
log('21: '+x23(21));
log('22: '+x23(22));
log('23: '+x23(23));
log('24: '+x23(24));
log('53: '+x23(53));
log('99: '+x23(99));
.as-console-wrapper {
min-height: 100% !important;
}