X Pahadi
X Pahadi

Reputation: 7443

JavaScript RegExp to automatically format Pattern

I have seen a lot of functions that format telephone or number (comma and decimals) in stackflow community like this question here and others. Here's what I want to:

Step 1: Maintain a Library for patterns like this:

var library = {
    fullDate : {
        pattern : /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/,
        error : "Invalid Date format. Use YYYY-MM-DD format."
    },
    fullDateTime : {
        pattern : /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}$/,
        error : "Invalid DateTime format. Use YYYY-MM-DD HH:MM (24-hour) format."
    },
    tel : {
        pattern : /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/,
        error : "Invalid Telephone format."
    }
};

Step 2: Automatically add a character as they type. For exapmple, add a - after 4 numbers in Date.

I have a text field say:

<input type="text" data-validate="fullDate" placeholder="YYYY-MM-DD"/>

And possible place to start script as:

$('body').on('keyup','input',function(){
   var validate = $(this).data('validate');
   var pattern = library[validate].pattern;


    //Some more steps here....

});

But, I cannot make any further because I am new to RegExp. Here's a startup fiddle. Anyone?

Further Notes: I have been able to validate using the following functions but what I want to is automatically make pattern:

function validate(libraryItem, subject){
    var item = library[libraryItem];
    if(item !== undefined){
        var pattern = item.pattern;
        if(validatePattern(pattern, subject)){
            return true;
        } else {
            return item.error;
        }
    }
    return false;
}


function validatePattern(pattern, subject){
    return pattern.test(subject);
}

Upvotes: 13

Views: 4608

Answers (4)

user557597
user557597

Reputation:

This could be done with a single regex.
This requires a MM:DD and HH:MM to be 2 digits and YYYY to be 4 digits on a
fully valid entry, but matches all the partials.

It could be made to allow a single digit validity for the 2 digit valid mentioned.
But doing so, will make premature suggestions on the - - [ ] : form.
If you want to not inject the suggestion, then 1 or 2 digits is fine.

JavaScript doesn't allow lookbehind assertions, so partial field expressions
are below the valid field expressions in their respective groups.

Basically what happens is the input is rewritten on each key press event.
All you do is match the current input within the event handler.

Without suggestions, you just write over the input with the entire match (group 0).

The match (group 0) will only contain a valid partial or full match.

The valid completed field capture groups are 1 through 5
[ Year, Month, Day, Hours, Minutes ]

The incomplete field captures are groups 6 through 10
[ Minutes, Hours, Day, Month, Year ]

This is the logic:

// Note 1 - can handle control chars by just returning.
// Note 2 - can avoid rewrite by keeping a global of last good,
//          then return if current == last.

if ( last char of group 0 is a dash '-' or space ' ' or colon ':' 
     or any of groups 6 - 10 matched
     or group 5 matched )
   set input equal to the group 0 string;  

else if ( group 4 matched )  // Hours
   set input equal to  group 0 string + ':';  

else if ( group 3 matched )  // Day
   set input equal to group 0 string + ' ';  

else if ( group 1 or 2 matched )  // Year or Month
   set input equal to  group 0 string + '-';   

else   // Here, effectively strips bad chars from input box
       // before they are displayed.
   set input equal to  group 0 string;   

Note that if a group didn't match it's value will be NULL
and to check the entire validity, there should be no partials and
groups 1 - 3 must be complete for just YYYY-MM-DD or 1 - 5 with the optional
time HH:MM

Final Note: This is a parser, and effectively a test case of the look and feel, ie. flicker, of real time input rewrite.
If it goes well, the logic in the handler can include Day validation (and rewrite) based on the month.
Also, the premise can be expanded to any type of input, any type of form and
form delimiter combinations, etc..
If it works, you can build a library.

 # /^(?:(19\d{2}|20[0-1]\d|202[0-5])(?:-(?:(0[1-9]|1[0-2])(?:-(?:(0[1-9]|[1-2]\d|3[0-1])(?:[ ](?:(0\d|1\d|2[0-3])(?::(?:(0\d|[1-5][0-9])|([0-5]))?)?|([0-2]))?)?|([0-3]))?)?|([01]))?)?|(19\d?|20[0-2]?|[12]))/


 ^                             # BOL 
 (?:
      (                             # (1 start), Year 1900 - 2025
           19 \d{2} 
        |  20 [0-1] \d 
        |  202 [0-5] 
      )                             # (1 end)
      (?:
           -                             # -
           (?:
                (                             # (2 start), Month    00 - 12
                     0 [1-9] 
                  |  1 [0-2] 
                )                             # (2 end)
                (?:
                     -                             # -
                     (?:
                          (                             # (3 start), Day   00 - 31
                               0 [1-9] 
                            |  [1-2] \d 
                            |  3 [0-1] 
                          )                             # (3 end)
                          (?:
                               [ ]                           # space
                               (?:
                                    (                             # (4 start), Hour  00 - 23
                                         0 \d 
                                      |  1 \d 
                                      |  2 [0-3] 
                                    )                             # (4 end)
                                    (?:
                                         :                             # :
                                         (?:
                                              (                             # (5 start), Minutes  00 - 59
                                                   0 \d 
                                                |  [1-5] [0-9]                                             
                                              )                             # (5 end)
                                           |  
                                              ( [0-5] )                     # (6)
                                         )?
                                    )?
                                 |  
                                    ( [0-2] )                     # (7)
                               )?
                          )?
                       |  
                          ( [0-3] )                     # (8)
                     )?
                )?
             |  
                ( [01] )                      # (9)

           )?
      )?
   |  
      (                             # (10 start)
           19 \d? 
        |  
           20 [0-2]? 
        |  
           [12] 
      )                             # (10 end)
 )

Upvotes: 2

Dinesh Devkota
Dinesh Devkota

Reputation: 1417

It is not as complicated as you think. What you are looking for is JQuery Masked input and other alternative libraries. Here is the documentation. All you need is:

 <input id="date" type="text" placeholder="YYYY-MM-DD"/>

and the script:

 $("#date").mask("9999-99-99",{placeholder:"YYYY-MM-DD"});

Here is demo pen link: http://codepen.io/anon/pen/gpRyBp

To implement validation use this library: https://github.com/RobinHerbots/jquery.inputmask

Upvotes: 8

maraca
maraca

Reputation: 8743

It is only possible to add a character if it is the single possible choice at this point. An example would be a regex for the YYYY-MM-DD HH24:mm format: the -, the : and the (space) could be added. Here is the corresponding regex (/ ommitted to make it more readable, it is stricter than the one in the question, some illegal dates are still possible like 31st of February):

^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]$

For fixed length inputs you could use @DineshDevkota's solution to add the literals and verify the whole text with regex. I think it's the cleanest and simplest solution. You could also capture the year, month and day to verify the day mathematically. Also rules like "date not in the future" or "max. 100 years in past" can only be veryfied in JS and not with just regex.

The only additional patterns that come to mind where a character could be automatically added:

  1. A + after a literal, e.g. on A+ add an A
  2. Minimal occurances in general, e.g. on (?:foo){2,5} add foofoo not to be confused with [fo]{2,5} where no characters can be added
  3. Literals after the max length of a variable part, e.g. on (?:foo){1,3}bar add bar after the text is foofoofoo before it is not possible.
  4. Add remainders e.g. foo|bar add ar when b was typed and oo when f was typed (also possible in the pattern shown in 3.) but this won't work for ^[a-z]+?(?:foo|bar)$ because we don't know when the user plans on ending the text and it can get really complicated (foo|flo|flu|food|fish only sh can be added after fi).

As seen in 3. and 4. those additional cases where characters could be added are of very limited use as soon as there are parts with variable length. You would have to parse the regex, split it in literal and regex parts. Then you have to parse/analyse the regex parts to incorporate the additional cases mentioned above where characters could be added. Really not worth the trouble if you ask me. (Not a single character can be added in your tel pattern.)

Upvotes: 1

Anurag Peshne
Anurag Peshne

Reputation: 1547

What needed here is breaking up the regular expression in sub expression which matches part of the string and suggest completion based upon next character in the Regular Expression. I wrote a naive Parser which parses the expression and divides into atomic subexpression.

var parser = function(input) {
    var tokenStack = [];
    var suggestions = [];
    var suggestion;
    var lookAhead;

    if (input[0] === '/')
        input = input.slice(1, input.length - 1);

    var i;
    for (i = 0; i < input.length - 1; i++) {
        lookAhead = input[i + 1];
        switch (input[i]) {
        case '(':
            tokenStack.push('(');
            break;
        case '[':
            tokenStack.push('[');
            break;
        case ')':
            if (tokenStack[tokenStack.length - 1] === '(') {
                tokenStack.pop();
                if (tokenStack.length === 0) {
                    suggestion = generateSuggestion(input, i);
                    if (suggestion !== null)
                        suggestions.push(suggestion);
                }
            }
            else
                throw 'bracket mismatch';
            break;
        case ']':
            if (lookAhead === '{') {
                while (input[i] !== '}')
                    i++;
            }
            if (tokenStack[tokenStack.length - 1] === '[') {
                tokenStack.pop();
                if (tokenStack.length === 0) {
                    suggestion = generateSuggestion(input, i);
                    if (suggestion !== null)
                        suggestions.push(suggestion);
                }
            }
            else
                throw 'bracket mismatch';
            break;
        default:
            if (tokenStack.length === 0) {
                suggestion = generateSuggestion(input, i);
                if (suggestion !== null)
                    suggestions.push(suggestion);
            }
            break;
        }
    }
    return suggestions;
}

var generateSuggestion = function(input, index) {
    if (input[index].match(/[a-zA-Z\-\ \.:]/) !== null)
        return {
            'regex': input.slice(0, index) + '$',
            'suggestion': input[index]
        };
    else
        return null;
}

Here is sample input and output of parser()

parser('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/');
// output:
[ { regex: '^[0-9]{4}$', suggestion: '-' },
  { regex: '^[0-9]{4}-[0-9]{1,2}$', suggestion: '-' } ]

Thus on every keyup you need to check the list of RegExp generated by parser and if any of it matches the input then use the suggestion.

EDIT:

Edited generateSuggestion to match only full expression. Here is sample fiddle: http://jsfiddle.net/a7kkL6xu/6/

With backspace ignored: http://jsfiddle.net/a7kkL6xu/7/

Upvotes: 4

Related Questions