Can this function be rewritten with a regex?
Asked Answered
A

3

9

I want to reformat and validate if a user has provided a valid Belgian enterprise number. Because the input can be all of the following examples:

  • BE 0123.321.123
  • BE0123.321.123
  • BE0123 321 123
  • 0123.321.123
  • 123.321.123
  • 123321123

I've written a function that validates and reformat the input to a 'display' version (BE 0123.123.123) and a 'code' version (123123123). This function looks like this.

formatAndValidateEnterpriseNumber = enterpriseNumber => {
    if(enterpriseNumber === undefined || !enterpriseNumber || (enterpriseNumber || '').length < 3) return { isValid: false, error: 'Please fill in your enterprise number' };

        //Remove space, dots, ...
        enterpriseNumber = enterpriseNumber.toUpperCase();
        enterpriseNumber = enterpriseNumber.replace(/[. ,:-]+/g, '');

        //Check for double country code
        const reDouble = /^[a-zA-Z]{4}/;
        if (reDouble.test(enterpriseNumber)) enterpriseNumber = enterpriseNumber.substring(2);

        if (enterpriseNumber.length < 9 || enterpriseNumber.length > 12) return { isValid: false, error: 'The length of the provided number is incorrect' };

        //Check country code
        const reBE = /^[a-zA-Z]{2}/;
        if (reBE.test(enterpriseNumber)) {
            //Check if country code = BE
            if (enterpriseNumber.slice(0, 2) !== 'BE') return { isValid: false, error: 'Please fill in a Belgian enterprise number' };
            // Remove country code
            else enterpriseNumber = enterpriseNumber.substring(2);
        }

        //Check if first digit is 0
        if (enterpriseNumber.length === 10 && enterpriseNumber.startsWith('0')) enterpriseNumber = enterpriseNumber.substring(1);

        //Check if enterpriseNumber is valid with modulo test
        if (parseInt(97 - (enterpriseNumber.slice(0, 7) % 97), 10) !== parseInt(enterpriseNumber.slice(7, 9), 10))
            return { isValid: false, error: 'The provided number is invalid'}

      return {
            isValid: true,
            enterpriseNumber: enterpriseNumber,
            displayEnterpriseNumber: `BE 0${enterpriseNumber.substring(0, 3)}.${enterpriseNumber.substring(3, 6)}.${enterpriseNumber.substring(6, 9)}`
      };
};

I think it's pretty messy and I'm wondering if this can be improved with one/two regex tests that reformat and validate the user's input?

A second question: Sometimes for account or credit cards numbers the input field had those underscores and lines (-) already in the input box and reformat the number while typing. What is this method called and can this be done for a specific thing like a Belgian enterprise number?

Artificer answered 4/3, 2019 at 19:52 Comment(1)
The modulo test is not easily possible in regex.Merryman
B
8

Yes you can:

^(?:BE)?\s*[0-1]?(\d[. ]*){9}$

This regex should do it!

This source (in Dutch) states what an enterprise number is for Belgium:

It has the country code: BE followed by a 0 or 1 and then followed by 9 digits.

https://regex101.com/r/4SRHxi/4

Explanation:

  • ^: the string has to start with the given regex
  • (?:BE)?: look for a group with BE but ? means it matches zero or one times - ?: means find but don't capture
  • \s*: search for a space that matches zero or unlimited times
  • [0-1]?: check if a zero of one is present zero or one times
  • ((\d[. ]*){9}): Check if 9 digits follow the remaining string, doesn't matter with how many dots or spaces they're padded. Every iteration is captured as the 1st capturing group. This becomes important when we replace later.
  • $: the string has to end

This will check if the input validates.

Editing it into the code version is simple:

«code».replace(/^(?:BE)?\s*[0-1]?((\d[. ]*){9})$/g, function(){
      return arguments[1].replace(/\D/g, "");
});

the g or global modifier will ensure all unwanted characters will be deleted. By using a function with a replace in it to replace all non-digit characters. This functions will output our desired result.

document.querySelector("pre").textContent.split("\n").forEach(function(element){
  if (element.match(/^(?:BE)?\s*[0-1]?(\d[. ]*){9}$/))
  {
     console.log(element.replace(/^(?:BE)?\s*[0-1]?((\d[. ]*){9})$/g, function(){
      return arguments[1].replace(/\D/g, "");
     }));
  }
  else
  {
    console.log(`REJECTED: ${element}`);
  }

});
<pre>
BE 0123.321.123
BE0123.321.123
BE0123 321 123
BE 0123  321  123
BE 01 23 32 11 23
BE 0123 32 11 23
1123.321.123
123.321.123
123321123
AAA3434343A
BE  1233 445 4545 442
</pre>

Rebuilding the String into the correct user friendly way is easy now:

document.querySelector("pre").textContent.split("\n").forEach(function(element) {
  if (element.match(/^(?:BE)?\s*[0-1]?((\d[. ]*){9})$/)) {
    var stripped = element.replace(/^(?:BE)?\s*[0-1]?((\d[. ]*){9})$/g, function(){
          return arguments[1].replace(/\D/g, "");
    });

    //with the modulo check from your code added back in.
    if (97 - (parseInt(stripped.slice(0, 7), 10) % 97) == parseInt(stripped.slice(7, 9), 10)) {
      //use a literal string
      //use substring to put the dots between the sections of three numbers.
      var humanReadable = `BE 0${stripped.substring(0,3)}.${stripped.substring(3,6)}.${stripped.substring(6,9)}`;
      console.log(`CODE: ${stripped}`, `UI: ${humanReadable}`);
    }
  }

});
<pre>
BE 0123.321.123
BE0123.321.123
BE0123 321 123
0123.321.123
123.321.123
123321123
844256524
</pre>

Second Question Yes, this can be done however it requires you to write your own code for it.

Simple version:

document.querySelector("div.enterprisenumber > input").addEventListener("keydown", function(e) {
  let value = this.value;

  //prevent the input from going back to 0
  if ( (value.length == 0 && (e.key == "Backspace" || e.key == "Delete"))) {
    e.preventDefault();
    return false;
  }
}, true);

document.querySelector("div.enterprisenumber > input").addEventListener("keyup", function(e) {
  //reset to a value without dots 
  let value = this.value.replace(/\./g, "");

  //strip the leading zero
  const valueWithout = value;
  //calculate how much iterations we need of a groups of three digits.
  const i = Math.floor(valueWithout.length / 3);
  let newValue = "";
  //check if backspace or delete are used to make sure the dot can be deleted.
  if (valueWithout.length < 9 && !(e.key == "Backspace" || e.key == "Delete")) {
    //only fire when higher than zero
    if (i > 0) {
      let t;
      //t is the index
      for (t = 0; t < i; t++) {
      //slice the correct portion of the string and append a dot, unless we are at the end of the groups
        newValue += valueWithout.slice(t * 3, t * 3 + 3) + (t == 2  ? "" : ".");
      }
      //append the remainder that is not a group of three.
      newValue += valueWithout.slice((t) * 3);
    } else {
      //return the value as is.
      newValue = value;
    }
    //set the new value to the input.
    this.value = newValue;
  }
}, true);

document.querySelector("div.enterprisenumber > input").addEventListener("blur", function(e) {
  let passed = false;
  if (this.value.match(/^(?:BE)?\s*[0-1]?((\d[. ]*){9})$/))
  {
    const value = this.value.replace(/\./g, "");
    //with modulo check
    if (97 - (parseInt(value.slice(0,7), 10) % 97) == value.slice(7, 9))
    {
      passed = true;
    }
  }
  document.querySelector(".enterprisenumber").classList[(passed ? "remove" : "add")]("error");
});

//if focus then focus input
document.querySelector("div.enterprisenumber").addEventListener("click", function(e) {
 if (e.target && e.target.nodeName != "SELECT")
 {
  this.querySelector("input").focus();
 }
});
* {
  box-sizing: border-box;
  font-family: tahoma;
  font-size: 10pt;
}

div.enterprisenumber {
  border: 1px solid #747474;
  width: 300px;
  padding: 0px;
  display: grid;
  grid-template-columns: 25px 40px auto;
  border-radius: 10px;
}

div.enterprisenumber.error {
  border: 1px solid #ff0000;
}

div.enterprisenumber>span {
  grid-column: 1;
  border: 0px;
  padding: 5px;
  background: linear-gradient(to right, rgba(0,0,0, 0.8) 33%, rgba(255,243,54, 0.8) 33%, rgba(255, 243, 54, 0.8) 66%, rgba(255, 15, 33, 0.8) 66%, rgba(255, 15, 33, 0.8) 100%);
  color: #ffffff;
  font-weight: bold;
  text-shadow: 1px 1px #000000;
  border-radius: 10px 10px 10px 10px;
}

div.enterprisenumber>select {
  grid-column: 2;
  border: 0px;
  padding: 5px;
}

div.enterprisenumber>input {
  grid-column: 3;
  border: 0px;
  padding: 5px;
  border-radius: 0px 10px 10px 0px;
}
Enter: 844256524
<div class="enterprisenumber">
  <span>BE</span><select><option value="0">0</option><option value="1">1</option><input value="" maxlength="11" />
</div>
Barsac answered 4/3, 2019 at 19:55 Comment(9)
Wow! Thanks for the explanation. I like the solution. However the modulo test is gone to check if the number is valid (The first 7 digits without the 0 or 1 in front, %97 should be the last two digits).Artificer
that's not that difficult, but not doable in the regexp though.Barsac
Would you suggest to do it separately? First the regex and then another check for the validation of the number parseInt(97 - (enterpriseNumber.slice(0, 7) % 97), 10) !== parseInt(enterpriseNumber.slice(7, 9), 10Artificer
See the solutions posted in my answer. Modulo check should be done separately.Barsac
One last thing. I didn't come up with these examples in my code but something like BE 01 23 32 11 23 or a group separated with more than one space will return false?Artificer
Updated the samples in the first code snippet and yes, they return false!Barsac
Can the regex be updated so this samples can pass the test? Technically seen the number is correct. Only the spaces have to be removed or the groups have to be regrouped from 2 to 3Artificer
Reworked the entire rexexp. Should hopefully meet your expectations. Mouser out.Barsac
\s*? really should just be \s*. The extra laziness gains us nothing, and just causes more backtracking on valid inputs.Indre
F
2

For your example strings, you could match:

^(?:BE\s?)?[01]?(\d{3}([. ])\d{3}\2\d{3}|\d{9})$

That will match

  • ^ Start of string
  • (?:BE\s?)? Optional BE followed by optional whitespace char
  • [01]? Optional zero or 1
  • ( Capturing group
    • \d{3} Match 3 digits
    • ([. ]) Capture in group either a space or digit to use as backreference
    • \d{3}\2\d{3} Match 3 digits, dot or space (\2 is the backreference) and 3 digits
    • | Or
    • \d{9} Match 9 digits
  • ) Close capturing group
  • $ End of string

Regex demo

And then in the replacement use the first capturing group and replace the space or the dot with an empty string.

let pattern = /^(?:BE\s?)?[01]?(\d{3}([. ])\d{3}\2\d{3}|\d{9})$/;
let strings = [
  "BE 0123.321.123",
  "BE0123.321.123",
  "BE0123 321 123",
  "0123.321.123",
  "123.321.123",
  "123321123",
];

strings = strings.map(x => x.replace(pattern, function(m, g) {
  let enterpriseNumber = g.replace(/[. ]/g, "");
  return `BE 0${enterpriseNumber.substring(0, 3)}.${enterpriseNumber.substring(3, 6)}.${enterpriseNumber.substring(6, 9)}`
}));

console.log(strings);
Futtock answered 4/3, 2019 at 20:30 Comment(0)
M
2

Here is an implementation of a BE ____.___.___ style of input. The pattern will be maintained, so the input will be guaranteed to have the "BE" prefix, the space, and the two dots. The validation can then concentrate on completeness and the modulo test.

Note that the input requires the first group to have 4 digits, where the first digit must be a 0 or a 1.

const ent = document.getElementById("ent");
const out = document.getElementById("isvalid");

function format() {
    const re = /^\D*[2-9]+|\D+/g;
    const [i, j] = [this.selectionStart, this.selectionEnd].map(i => {
        i = this.value.slice(0, i).replace(re, "").length;
        return i + 3 + (i >= 4 + format.backspace) + (i >= 7 + format.backspace);
    });
    this.value = "BE " + this.value.replace(re, "").padEnd(10, "_")
                                   .replace(/(....)(...)(...).*/, "$1.$2.$3");
    this.setSelectionRange(i, j);
    format.backspace = false;
    out.textContent = validate(this.value) ? "is valid" : "is invalid";
}

function validate(num) {
    return /^BE [01](\d{3}\.){2}\d{3}$/.test(num) 
            && 97 - num.replace(/\D/g, "").slice(0, 8) % 97 === +num.slice(-2);
}

ent.addEventListener("input", format);
ent.addEventListener("keydown", (e) => format.backspace = e.key == "Backspace");
Belgian enterprise number: <input id="ent" value="BE ____.___.___">
<span id="isvalid"></span>
Merryman answered 4/3, 2019 at 22:50 Comment(1)
Wow, that's a nice solution to the second question.Barsac

© 2022 - 2024 — McMap. All rights reserved.