Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to symmetrically split a string in JavaScript?

Tags:

javascript

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?

like image 232
Lance Avatar asked Aug 30 '25 16:08

Lance


2 Answers

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 -

  1. If the input t is less than zero, there are no valid segments. Stop iteration
  2. (inductive) t is non-negative. If t is zero, yield the empty segment, []
  3. (inductive) 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 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 is. 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
like image 172
Mulan Avatar answered Sep 02 '25 06:09

Mulan


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('-'))
like image 34
Bravo Avatar answered Sep 02 '25 05:09

Bravo