skip to Main Content

My goal is to create a regex generator function where I pass a policy criteria object as a parameter and it generates regex according to that.

My attempts so far:

function generateRegexExpression(policy) {
    let pattern = "";
    if (policy.UppercaseLength > 0) pattern += `A-Z{${policy.UppercaseLength},}`;
    if (policy.LowercaseLength > 0) pattern += `a-z{${policy.LowercaseLength},}`;
    if (policy.NonAlphaLength > 0) pattern += `[!@#$%^&*()_+-={}\[\]\|;:'",.<>/?`~]{${policy.NonAlphaLength},}`;
    if (policy.NumericLength > 0) pattern += `\d{${policy.NumericLength},}`;
    if (policy.AlphaLength > 0) pattern += `[A-Za-z]{${policy.AlphaLength},}`;
    pattern = `^[${pattern}]{${policy.MinimumLength},${policy.MaximumLength}}$`;
    return new RegExp(pattern);
  }


  const policy = {
    "MinimumLength": 8,
    "MaximumLength": 20,
    "UppercaseLength": 0,
    "LowercaseLength": 0,
    "NonAlphaLength": 0,
    "NumericLength": 1,
    "AlphaLength": 1
  };



"PolicyRules": [
      "Please choose a new password that is between 8 and 20 characters in length.",
      "Must have at least 1 letter.",
      "Must have at least 1 number.",
      "Your new password should not be same as your username or as your last password.",
      "Choose a password that is different from your last 5 passwords.",
      "Do NOT share your password with anyone."
    ]

2

Answers


  1. This isn’t a direct answer to your question, but might help solve the overarching issue.

    Instead of building a complex regex, you’ll probably be better of if you validate each criteria separately. This allows you to provide the user with a relevant error message. But also gives you the freedom to not use regex if you don’t need to.

    For example to validate MinimumLength there is no need to use regex at all. You could just use input.length >= MinimumLength.

    Here is an example that uses the following validate function:

    function validate(input, criteria) {
      const errors = [];
      for (const [isValid, errorMsg] of criteria) {
        if (!isValid(input)) errors.push(errorMsg);
      }
      return [!errors.length, errors];
    }
    

    This function expects criteria, which is a list of [isValid, errorMsg] combinations. Where isValid is a function that accepts the input as argument. Then returns a truthy value if the input is valid, or a falsy value if the input is not valid. errorMsg is the error that will be returned in the scenario that input is invalid.

    With this function you can build your criteria from your policy fairly simple:

    const criteria = [];
    
    if ("MinimumLength" in policy) criteria.push([
      input => input.length >= policy.MinimumLength,
      `must be at least ${policy.MinimumLength} characters`
    ]);
    
    // ...
    

    With the criteria build you validate your input using:

    const [isValid, errors] = validate(input, criteria);
    

    Where isValid is a true/false value that tells you if the input is valid. And errors are the error messages in criteria that are relevant to the current input.

    document.forms.validate.addEventListener("submit", (event) => {
      event.preventDefault();
    
      const form = event.target;
      const inputs = form.elements;
      
      const policy = JSON.parse(inputs.policy.value);
      const criteria = generatePolicyCriteria(policy);
      const input = inputs.testInput.value;
      const [isValid, errors] = validate(input, criteria);
      
      const output = document.getElementById("output");
      output.innerHTML = "";
      if (isValid) output.append("✓");
      else output.append("✗", toUnorderedList(errors));
    });
    
    function validate(input, criteria) {
      const errors = [];
      for (const [isValid, errorMsg] of criteria) {
        if (!isValid(input)) errors.push(errorMsg);
      }
      return [!errors.length, errors];
    }
    
    function generatePolicyCriteria(policy) {
      const criteria = [];
      
      if ("MinimumLength" in policy) criteria.push([
        input => input.length >= policy.MinimumLength,
        `must be at least ${policy.MinimumLength} characters`
      ]);
      
      if ("MaximumLength" in policy) criteria.push([
        input => input.length <= policy.MaximumLength,
        `must be at least ${policy.MaximumLength} characters`
      ]);
      
      if ("AlphaLength" in policy) criteria.push([
        input => (input.match(/p{Letter}/gu) || []).length >= policy.AlphaLength,
        `must contain at least ${policy.AlphaLength} letters`
      ]);
    
      if ("UppercaseLength" in policy) criteria.push([
        input => (input.match(/p{Uppercase_Letter}/gu) || []).length >= policy.UppercaseLength,
        `must contain at least ${policy.UppercaseLength} uppercase letters`
      ]);
      
      if ("LowercaseLength" in policy) criteria.push([
        input => (input.match(/p{Lowercase_Letter}/gu) || []).length >= policy.LowercaseLength,
        `must contain at least ${policy.LowercaseLength} lowercase letters`
      ]);
      
      if ("NumericLength" in policy) criteria.push([
        input => (input.match(/p{Decimal_Number}/gu) || []).length >= policy.NumericLength,
        `must contain at least ${policy.NumericLength} digits`
      ]);
      
      const specialChars = new Set("!@#$%^&*()_+-={}[]|;:'",.<>/?`~");
      if ("NonAlphaLength" in policy) criteria.push([
        input => Array.from(input).filter(char => specialChars.has(char)).length >= policy.NonAlphaLength,
        `must contain at least ${policy.NonAlphaLength} special characters`
      ]);
      
      return criteria;
    }
    
    function toUnorderedList(items, liContent = item => item) {
      const ul = document.createElement("ul");
      for (const item of items) {
        const li = document.createElement("li");
        li.append(liContent(item));
        ul.append(li);
      }
      return ul;
    }
    <form id="validate">
      <textarea name="policy" rows="9" cols="30">{
      "MinimumLength": 8,
      "MaximumLength": 20,
      "UppercaseLength": 0,
      "LowercaseLength": 0,
      "NonAlphaLength": 0,
      "NumericLength": 1,
      "AlphaLength": 1
    }</textarea>
      <br />
      <input name="testInput" type="text" />
      <div id="output"></div>
      <button>validate</button>
    </form>
    Login or Signup to reply.
  2. First off, a disclaimer: I’m not convinced that using a single regular expression is the best tool for this task. We can make it work anyway, but now we all know that I know that we’re abusing regexps 🙂

    Your current code will produce strange patterns that won’t do anything close to what you want. Since they’re all along the same lines let’s look at one example and see what’s going wrong.

    Suppose we only have an uppercase policy to apply (say, policy.UpperCaseLength = 4). With a bit of whitespace to help see what’s going on, this results in:

    pattern = `^[A-Z{4,}]{8,20}$`
    

    Everything between those square brackets is treated as part of a character class. Although your intention was for {4,} to say that there should be at least four characters matching [A-Z], [A-Z{4,}] is interpreted as "an uppercase character, or {, or 4, or ,, or }".

    What you really want to test is "disregarding other characters in the input, are there 4 separate instances of an uppercase letter?"

    This is actually a pretty easy pattern: we can search for anything (.*) followed by [A-Z], at least four times, i.e. (.*[A-Z]){4,}. In fact we can be slightly more efficient about this – as long as there are four matches we don’t need to keep matching, and while we’re at it we can use a non-greedy quantifier, and a non-capturing group. So, to match a string that contains at least policy.UpperCaseLength instances of an uppercase letter, we could use a pattern:

    `(?:.*?[A-Z]){${policy.UpperCaseLength}}`
    

    We can use this same shape of thinking to make patterns for the other character classes e.g.

    `(?:.*?\d){${policy.NumericLength}}`
    

    We can combine the patterns into one regular expression using look-ahead assertions. An assertion effectively checks the pattern against the input string without "using up" the characters, so we can apply several patterns to the string at once.

    So, if we add in the rules wrapped in the look-ahead assertion syntax ((?= ... )) then our pattern from before looks like:

    pattern = `^(?=(?:.*?[A-Z]){4}){8,20}$`
    

    This still isn’t quite right (in fact it won’t match anything – the look-ahead group matches zero characters, so it could only match an empty string that had at least four uppercase characters!) – what we really want to do with the MinimumLength and MaximumLength policies is check that there are between 8 and 20 instances of any character, i.e. .. So, add that to the pattern:

    pattern = `^(?=(?:.*?[A-Z]){4}).{8,20}$`
    //                             ^^^^^^^ enforce length range
    

    Now the pattern checks that there are at least four uppercase characters, and then that the whole input is between 8 and 20 characters. We can construct other patterns based on the policy in the same way:

    function generateRegexExpression(policy) {
        let pattern = "";
        if (policy.UppercaseLength > 0) pattern += `(?=(?:.*?[A-Z]){${policy.UppercaseLength},})`;
        if (policy.LowercaseLength > 0) pattern += `(?=(?:.*?[a-z]){${policy.LowercaseLength},})`;
        if (policy.NonAlphaLength > 0) pattern += `(?=(?:.*?[!@#$%^&*()_+-={}\[\]\|;:'",.<>/?`~]){${policy.NonAlphaLength},})`;
        if (policy.NumericLength > 0) pattern += `(?=(?:.*?\d){${policy.NumericLength},})`;
        if (policy.AlphaLength > 0) pattern += `(?=(?:.*?[A-Za-z]){${policy.AlphaLength},})`;
        pattern = `^${pattern}.{${policy.MinimumLength},${policy.MaximumLength}}$`;
        return new RegExp(pattern);
      }
    
      const policy = {
        "MinimumLength": 8,
        "MaximumLength": 20,
        "UppercaseLength": 0,
        "LowercaseLength": 0,
        "NonAlphaLength": 0,
        "NumericLength": 1,
        "AlphaLength": 1
      };
    
    const re = generateRegexExpression(policy);
    
    // Test some inputs
    for (const password of [
        "aBcDeFgH1",                   // SUCCESS
        "aBcDeFgH",                    // FAIL -- does not satisfy NumericLength
        "aBcDeF1",                     // FAIL -- MinimumLength
        "1abcDEFGhijklmNOPQrstuvwxyz", // FAIL -- MaximumLength
        "12345678"                     // FAIL -- AlphaLength
    ]) {
      console.log(password + " => " + re.test(password));
    }
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search