JavaScript RegEx: Format string with dynamic length to block format

Viewed 787

The following RegEx formats given string to following output block pattern:

123 456 78 90 (= 3 digits 3 digits 2 digits 2 digits )

RegEx:

string.replace(/^(\d{3})(\d{3})(\d{2})(\d{2})$/g, '$1 $2 $3 $4');

This example only works, if given input string matches exact length of 10 digits.

How can I adjust the RegEx to work with every string length?

  • 1234 --> 123 4
  • 1234567 --> 123 456 7
  • 123456789 --> 123 456 78 9
  • 1234567890123 --> 123 456 78 90 12 3

Background: I want to format the input string of a form input field directly, when a user enters the number...

3 Answers

anubhava's and Wiktor's solutions are clever, but I don't think I'd use a regular expression to do it; the solutions feel too complex to maintain. (This is a judgement call.) Here's an approach that gets the individual digits and inserts spaces after the third digit and after every subsequent digit whose index position is an odd number:

result = [...str].map((d, i) => d + (i === 2 || (i >= 5 && i % 2 === 1) ? " " : "")).join("");

Live Example:

const tests = [
    ["1234", "123 4"],
    ["1234567", "123 456 7"],
    ["123456789", "123 456 78 9"],
    ["1234567890123", "123 456 78 90 12 3"],
];

function format(str) {
    return [...str].map((d, i) => d + (i === 2 || (i >= 5 && i % 2 === 1) ? " " : "")).join("");
}

for (const [str, expected] of tests) {
    const result = format(str);
    console.log(`|${str}|`, "=>", `|${result}|`, result === expected ? "OK" : "*** ERROR");
}

Here's a version that works in ES5-only environments:

result = str.split("").map(function(d, i) {
    return d + (i === 2 || (i >= 5 && i % 2 === 1) ? " " : "");
}).join("");

Using modern Javascript lookbehind that allows dynamic length assertion, you can use this regex to match:

/(?<=^(?:\d{6}(?:\d{2})*|(?:\d{3}){1,2}))(?=\d+$)/g

Just replace that with a single space i.e. " "

RegEx Demo

Lookbehind:

  • (?<=: Start assertion
    • ^(?:\d{6}(?:\d{2})*: Make sure we have 6 digits followed by 0 or more digits pair behind
    • |: OR
    • (?:\d{3}){1,2}): Make we have one or two sets of 3 digit sets behind
  • ): End assertion

Lookahead:

  • (?=\d+$): Make sure we have at least a digit ahead

You can use

const rx = /^(\d{3})(\d{1,3})?(\d{1,2})?(\d{1,2})?$/;

$('body').on('input', '.info', function(e) {
  this.value = this.value.replace(/\s+/g,'')
   .replace(/^(\d{10}).*/, '$1')
    .replace(rx, (_,w,x,y,z) =>
      z ? `${w} ${x} ${y} ${z}` :
      y ? `${w} ${x} ${y}` :
      x ? `${w} ${x}` : w);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input class="info" maxlength=13>

The ^(\d{3})(\d{1,3})?(\d{1,2})?(\d{1,2})?$ regex will perform live formatting together with the callback function used as the replacement argument. The first three digits are obligatory, the second, third and fourth blocks are optional, but contain at least one digit. Spaces are only added if there is at least one digit in the block. The number is reformatted each time the number is edited:

  • .replace(/\s+/g,'') removes the added spaces
  • .replace(/^(\d{10}).*/, '$1') keeps just the first ten digits if there are more
Related