8
\$\begingroup\$

I'd like to restrict user input in a form to a decimal with max two numbers before and 4 after decimal point.

Valid values would be:

50
0.1
1.23
4.5678
12.943

I'm currently doing the following:

$(function() {
  $('input').on('input', function() {
    this.value = this.value
      .replace(/[^\d.]/g, '')             // numbers and decimals only
      .replace(/(^[\d]{2})[\d]/g, '$1')   // not more than 2 digits at the beginning
      .replace(/(\..*)\./g, '$1')         // decimal can't exist more than once
      .replace(/(\.[\d]{4})./g, '$1');    // not more than 4 digits after decimal
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" />

This works just fine and I'm happy with the JS replace() method of restricting form input (yes I am doing server side validation as well).

I'm mostly interested to know if there is a more concise way to set up the regex(es) so I can reduce and/or eliminate some of the code here. It seems like I should be able to get the same result with fewer replacements.

\$\endgroup\$
  • 1
    \$\begingroup\$ Does something like this.value.match(/^\d{0,2}.?\d{0,4}$/) not work? (There's two large edge-cases with this regex) \$\endgroup\$ – Peilonrayz May 10 '16 at 22:48
  • 3
    \$\begingroup\$ [\d] is redundant and can be replaced with \d. I'm not a big fan of this approach, however. The input element's behaviour will be very unpredictable if the user inadvertently misses out the decimal point while typing, or tries to move the decimal point from one place to another. \$\endgroup\$ – squeamish ossifrage May 10 '16 at 22:49
  • \$\begingroup\$ @JoeWallis - I don't understand how that would work. Can you give me a working example that does the same thing as my snippet and uses your suggestion? \$\endgroup\$ – billynoah May 10 '16 at 23:02
  • \$\begingroup\$ @squeamishossifrage - thanks - that's a slight improvement. Can you elaborate on the isse you have with this approach? I've tried it in every browser and it appears to behave rock solid. What does "misses out the decimal point" mean? \$\endgroup\$ – billynoah May 10 '16 at 23:03
  • \$\begingroup\$ FYI, we normally don't allow code to be modified after answers have been posted. \$\endgroup\$ – 200_success May 11 '16 at 5:45
7
\$\begingroup\$

I think that you are going about this the wrong way. Rather than thinking of what you should remove, you should think of what you should match.

You are only allowing zero to two digits before a decimal place and so you can use the pattern \d{0,2}.

To include a decimal place you can use the pattern \.?.

And finally you can include zero to four numbers after the decimal place with the pattern \d{0,4}.

If you were to merge these together you will end up with \d{0,2}\.?\d{0,4}. This has the problem of matching both fullstops, and numbers 6 digits long.

To amend this you can can use the or operator (i.e. |) but you will need to duplicate your regex. And can result in \d{0,2}\.\d{0,4}|\d{0,2}.?|.?\d{0,4}.

As an alternate to this you can thank that you only include the second half if there is a decimal, and limits the input to two numbers rather than four. Via \d{0,2}(\.\d{0,4})?.

Both the above patterns have their pros and cons. Now you want to match these and so you don't want to use replace. This gives you the option of exec or match. I'd use match due to the simplicity. As you always want the first match and this will always return a match you can simply use something like str.match(//)[0].

This can result in:

$(function() {
  $('input').on('input', function() {
    this.value = this.value.match(/\d{0,2}(\.\d{0,4})?/)[0];
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" />

This has the problem that, if you enter 12.1234 and then change it to 123.1234 it'll be replaced to 12. This is as you want it to ignore all numbers after the first two and match the decimal and the last four. To explain this with the demonstration of capture groups you want (12)3(.1234) and then you'd just join them together. To achieve this you will want to add capture groups to the above, and use a non-capture group to get empty strings, so (\d{0,2})((?:\.\d{0,4})?). This doesn't capture the 3, and so to add this we'll need to capture everything apart from a decimal point. [^.]*. Put this in the above function and you should get what you want. (\d{0,2})[^.]*((?:\.\d{0,4})?). To properly use this we'll need to use exec rather than match too.

The other problem with the above is as 200_success says:

For example, if you type 12 into the text field, then place the cursor before the 1, and press x, your code is able to discard just the x. Joe's answer, on the other hand, discards the x and everything after it.

You can change the single regex to account for this input x12. But it won't work with 1x2 either. To account for this we can use your first replace to limit the input to just numbers and decimals.

And, without adding 200_success' position stuff, should result in:

$(function() {
  $('input').on('input', function() {
    match = (/(\d{0,2})[^.]*((?:\.\d{0,4})?)/g).exec(this.value.replace(/[^\d.]/g, ''));
    this.value = match[1] + match[2];
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" />

This as far as I can tell works exactly as yours does. But there is one difference, that I don't think you really want. If you copy and paste 1234.12345678 into both my input and yours they're different.

Mine results in 12.1234, where yours results in 124.1234678.

\$\endgroup\$
  • \$\begingroup\$ this is very cool stuff joe and i agree a better approach. I need to study your regex's a bit more but thanks for the input! one question - you mention the pros and cons of the options you mentioned. can you elaborate on that at all? they both seem to get the job done. \$\endgroup\$ – billynoah May 10 '16 at 23:39
  • 2
    \$\begingroup\$ @billynoah If you enter 12.1234 and then change it to 123.1234 it'll be replaced to 12. The longer one has the con of being longer, but with a little bit of tweaking it'd possibly be more robust a solution that the shorter one. I've not thought out every possible input, and so I'm sure there are edge-cases to both of them, that the other is better at. \$\endgroup\$ – Peilonrayz May 10 '16 at 23:42
  • \$\begingroup\$ ah yes.. that's an issue which the replace approach i'm using above does not have. \$\endgroup\$ – billynoah May 10 '16 at 23:53
  • \$\begingroup\$ i'm glad i learned something about using match here but unless i can replicate the original functionality i'd say it's a step backwards. i'm leaving my upvote in place but it shouldn't be the accepted answer if it doesn't at least accomplish the same thing \$\endgroup\$ – billynoah May 10 '16 at 23:54
  • \$\begingroup\$ @billynoah Since it was mostly to give an example of your request, I don't think it's too bad, I think it now works exactly as you'd want, excluding 200_success' additions. \$\endgroup\$ – Peilonrayz May 11 '16 at 9:20
5
\$\begingroup\$

@JoeWallis is on the right track, but his solution requires more discussion.

If you do decide to use a single validation regex, then you should use the HTML5 pattern attribute. On browsers that support HTML5 pattern validation, you get some of the functionality that you seek for free, without any JavaScript. (See the "Native HTML5" field in the demo below.)

Note that not all browsers support pattern validation: Safari is a notable weirdo. Furthermore, browsers that do support pattern validation would mark the field as invalid rather than making an instant correction for you. Therefore, a jQuery supplement might still be desirable.

One problem with your handler, as well as Joe's, is that it will place the cursor at the end of the text after every keystroke. That makes a poor user experience if the user is trying to insert some characters someplace other than the end. My code below saves and restores the caret position. (Caveat: the API to make that work on Internet Explorer is nasty, and I haven't implemented it here.)

One strength of your original solution, which cannot be matched with the single-regex validation technique, is that it is able to exclude just the garbage. For example, if you type 12 into the text field, then place the cursor before the 1, and press x, your code is able to discard just the x. Joe's answer, on the other hand, discards the x and everything after it. I've chosen to use \d{1,2} instead of \d{0,2}, which mitigates that behaviour slightly, but it still doesn't behave as intuitively as yours.

$(function() {
  $('.jquery input[pattern]').on('input', function() {
    var pos = this.selectionStart;
    var re = $(this).attr('pattern');
    $(this).val($(this).val().match(re)[0]);
    
    // Restore caret position after setting value
    this.setSelectionRange(pos, pos);
  });
});
input:invalid {
  background-color: pink;
}
<!DOCTYPE html>
<html>
  <head>
    <title>Number validation demo</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  </head>
  <body>
    <label class="native">Native HTML5: <input type="text" pattern="\d{1,2}(\.\d{0,4})?"></label>
    <label class="jquery">With jQuery: <input type="text" pattern="\d{1,2}(\.\d{0,4})?"></label>
  </body>
</html>

\$\endgroup\$
  • \$\begingroup\$ That's interesting @200_success - although neither field prevents me from typing letters. It just turns red when I do. Is that what's intended? \$\endgroup\$ – billynoah May 11 '16 at 5:27
  • \$\begingroup\$ Also, regarding the insertion point issue - I actually did something similar to what you did there but didn't want to mention it because I felt it was so hacky and would detract from the question. See example of my full solution here: se-128036.dev.zuma-design.com \$\endgroup\$ – billynoah May 11 '16 at 5:27
  • \$\begingroup\$ I can't access your demo (HTTP 403 error). In any case, we encourage you to post real code on Code Review, not some sanitized version of it, precisely so that we can see and improve the ugly parts. \$\endgroup\$ – 200_success May 11 '16 at 5:30
  • 1
    \$\begingroup\$ Demo is fixed. I'll bear that in mind and post unsanitized code in the future questions. Thanks for letting me know. \$\endgroup\$ – billynoah May 11 '16 at 5:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.