15

I need a regular expression to validate durations in the ISO 8601 duration format (with the exception of fractional parts which I don't need).

PnYnMnDTnHnMnS

PnW

Here is what I have:

^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$

The only problem is that the strings P and PT are allowed with this regex as all of the parts are "zero or one" ?.

  • There needs to be at least one component (date or time)
  • If there is a T then there needs to be a time component (H, M, or S)
  • If there is a T then there may or may not be any date components (Y, M, or D)
  • Overflow is allowed (e.g. P72H is mostly equivalent to P3D)

Acceptable inputs:

P1Y        // date component only
P2MT30M    // date and time components
PT6H       // time component only
P5W        // another date component

Unacceptable inputs:

P         // no components
PT        // no components
P3MT      // T specified but not time components

Right now the invalid strings are passing client-side validation but failing on the server-side because it's passed into DateInteval but I'd like to fail on the client side if possible. If everyone was using Chrome 40+ I could specify minlength='3' on the input element to help but that isn't the case unfortunately.

Community
  • 1
  • 1
rink.attendant.6
  • 44,500
  • 61
  • 101
  • 156

5 Answers5

25

If you have almost all parts optional, but you want to make sure there is something else after P or T, you can make use of look-aheads:

^P(?=\d+[YMWD])(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$
  ^^^^^^^^^^^^                               ^^^^^^^^^^^^

They require a sequence of digits followed by a letter from the specified set to appear right after the preceding pattern.

See demo

UPDATE

If P can be "empty", use

^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d+[HMS])(\d+H)?(\d+M)?(\d+S)?)?$

See another demo. Here, (?!$) makes sure the string is not equal to P, and there must be some other symbols on the right.

Or, as @UlugbekUmirov suggests, it is enough to just use T(?=\d) (since all the optional parts start with a digit):

^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$

UPDATE 2

If the numbers can be both float or integers, add (?:\.\d+)? after each \d+. Here is an updated pattern from Update 1:

^P(?!$)(\d+(?:\.\d+)?Y)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?W)?(\d+(?:\.\d+)?D)?(T(?=\d)(\d+(?:\.\d+)?H)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?S)?)?$
Wiktor Stribiżew
  • 607,720
  • 39
  • 448
  • 563
3

The answers above don't include the situation of a decimal fraction (see here for details). The decimal fraction can occur on the last element. The following regex includes decimal fraction:

^P(?!$)((\d+Y)|(\d+\.\d+Y$))?((\d+M)|(\d+\.\d+M$))?((\d+W)|(\d+\.\d+W$))?((\d+D)|(\d+\.\d+D$))?(T(?=\d)((\d+H)|(\d+\.\d+H$))?((\d+M)|(\d+\.\d+M$))?(\d+(\.\d+)?S)?)??$

See here for tests.

Rafi
  • 2,433
  • 1
  • 25
  • 33
1

According to this message about ISO 8601-2:2019(en), a negative duration is expressed with a minus - sign before the P and it also seems that an explicit positive duration can be indicated with a plus + sign.

Useful to include [-+]? or (-|\+)? before P

Finally, That message also shows the following examples from ISO 8601-2:2019(en) indicating that each component of the duration can be negatively signed.

| EXAMPLE 7 '-P2M1D' is equivalent to 'P-2M-1D'.
| EXAMPLE 8 '-P5DT10H' is equivalent to 'P-5DT-10H'.

Adding [-+]? before digits in components and allowing -+ in the lookahead for T makes sense to be 8601-2 compliant

^[-+]?P(?!$)(([-+]?\d+Y)|([-+]?\d+\.\d+Y$))?(([-+]?\d+M)|([-+]?\d+\.\d+M$))?(([-+]?\d+W)|([-+]?\d+\.\d+W$))?(([-+]?\d+D)|([-+]?\d+\.\d+D$))?(T(?=[\d+-])(([-+]?\d+H)|([-+]?\d+\.\d+H$))?(([-+]?\d+M)|([-+]?\d+\.\d+M$))?([-+]?\d+(\.\d+)?S)?)??$

pltc
  • 5,836
  • 1
  • 13
  • 31
Paul Higgs
  • 11
  • 3
0

Answers above require additional post-processing. /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/ An output:

["P2Y9M3DT12H31M8.001S", "", "2", "9", "3", "D", "12", "31", "8.001", index: 0, input: "P2Y9M3DT12H31M8.001S", groups: undefined]
0

If you are searching for a regular expression containing a start date and duration, check this out:

^(\d{4}(-\d{2}(-\d{2})?(?!:))?(T\d{2}(:\d{2}(:\d{2})?(\.\d+)?)?)?(Z|([+,-]\d{2}(:\d{2})?))?)?P(([0-9]+([.,][0-9]*)?Y)?([0-9]+([.,][0-9]*)?M)?([0-9]+([.,][0-9]*)?D)?T?([0-9]+([.,][0-9]*)?H)?([0-9]+([.,][0-9]*)?M)?([0-9]+([.,][0-9]*)?S)?)|\d{4}-?(0[1-9]|11|12)-?(?:[0-2]\d|30|31)T((?:[0-1][0-9]|[2][0-3]):?(?:[0-5][0-9]):?(?:[0-5][0-9]|60)|2400|24:00)$
Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
elias
  • 1
  • 1