Regular Expression for date and time(DD/MM/YYYY hh:mm:ss) in QML

In QML2 I didn't find any Calender control and I have implemented a control which takes date and time as input and I am using the regular expression for the validation which matches dates including leap year and other validations.

The main problem is space/backspace should also be considered as a valid for example:

\s\s/\s\s/\s\s \s\s:\s\s:\s\s

Following is the code :

TextField{
    id:textEditDate

    width:parent.width * 0.50
    height:parent.height
    text : "01/01/2017 00:00:00"

    inputMask: "99/99/9999 99:99:99"

    validator: RegExpValidator { regExp: /^(((([0\s][1-9\s]|[1\s][0-9\s]|[2\s][0-8\s])[\/]([0\s][1-9\s]|[1\s][012\s]))|((29|30|31)[\/]([0\s][13578\s]|[1\s][02\s]))|((29|30)[\/]([0\s][4,6,9]|11)))[\/]([19\s[2-9\s][0-9\s])\d\d|(^29[\/]02[\/]([19\s]|[2-9\s][0-9\s])(00|04|08|12|16|20|24|28|32|36|40|44|48|52|56|60|64|68|72|76|80|84|88|92|96)))\s([0-1\s]?[0-9\s]|2[0-3\s]):([0-5\s][0-9\s]):([0-5\s][0-9\s])$/}

    horizontalAlignment: Text.AlignHCenter
    inputMethodHints: Qt.ImhDigitsOnly
}

Now, everything works well except for the year and I am not able to match backspace/space for the year and user is not able to clear the year.

Can you please suggest how to achieve this ? or is there any other method to do this.


Answer

Brief

So I decided to make a really nice regex that actually works on leap years properly! I then added the rest of the logic you required, and voila, a beauty!


Code

See regex in use here

(?(DEFINE)
  (?# Date )
    (?# Day ranges )
    (?<d_day28>0[1-9]|1\d|2[0-8])
    (?<d_day29>0[1-9]|1\d|2\d)
    (?<d_day30>0[1-9]|1\d|2\d|30)
    (?<d_day31>0[1-9]|1\d|2\d|3[01])
    (?# Month specifications )
    (?<d_month28>02)
    (?<d_month29>02)
    (?<d_month30>0[469]|11)
    (?<d_month31>0[13578]|1[02])
    (?# Year specifications )
    (?<d_year>\d+)
    (?<d_yearLeap>(?:\d*?(?:(?:(?!00)[02468][048]|[13579][26])|(?:(?:[02468][048]|[13579][26])00))|[48]00|[48])(?=\D))
    (?# Valid date formats )
    (?<d_format>
      (?&d_day28)\/(?&d_month28)\/(?&d_year)|
      (?&d_day29)\/(?&d_month29)\/(?&d_yearLeap)|
      (?&d_day30)\/(?&d_month30)\/(?&d_year)|
      (?&d_day31)\/(?&d_month31)\/(?&d_year)
    )

  (?# Time )
    (?# Time properties )
    (?<t_period12>(?i)[ap]m|[ap]\.m\.(?-i))
    (?# Hours )
    (?<t_hours12>0\d|1[01])
    (?<t_hours24>[01]\d|2[0-3])
    (?# Minutes )
    (?<t_minutes>[0-5]\d)
    (?# Seconds )
    (?<t_seconds>[0-5]\d)
    (?# Milliseconds )
    (?<t_milliseconds>\d{3})
    (?# Valid time formats )
    (?<t_format>
      (?&t_hours12):(?&t_minutes):(?&t_seconds)(?:\.(?&t_milliseconds))?\ ?(?&t_period12)|
      (?&t_hours24):(?&t_minutes):(?&t_seconds)(?:\.(?&t_milliseconds))?
    )

  (?# Datetime )
    (?<dt_format>(?&d_format)\ (?&t_format))
)
\b(?&dt_format)\b

Or in one line...

See regex in use here

\b(?:(?:0[1-9]|1\d|2[0-8])\/(?:02)\/(?:\d+)|(?:0[1-9]|1\d|2\d)\/(?:02)\/(?:(?:\d*?(?:(?:(?!00)[02468][048]|[13579][26])|(?:(?:[02468][048]|[13579][26])00))|[48]00|[48])(?=\D))|(?:0[1-9]|1\d|2\d|30)\/(?:0[469]|11)\/(?:\d+)|(?:0[1-9]|1\d|2\d|3[01])\/(?:0[13578]|1[02])\/(?:\d+))\ (?:(?:0\d|1[01]):(?:[0-5]\d):(?:[0-5]\d)(?:\.(?:\d{3}))?\ ?(?:(?i)[ap]m|[ap]\.m\.(?-i))|(?:[01]\d|2[0-3]):(?:[0-5]\d):(?:[0-5]\d)(?:\.(?:\d{3}))?)\b

Explanation

I'll explain the first version as the second version is simply a slimmed down version of it. Note that the regex can easily be changed to accommodate for more formats (only 1 format with slight variations is accepted, but this is a very customizable regex).

  • d_days28: Match any number from 01 to 28
  • d_days29: Match any number from 01 to 29
  • d_days30: Match any number from 01 to 30
  • d_days31: Match any number from 01 to 31
  • d_month28: Match months that may only have 28 days (February - thus 02)
  • d_month29: Match months that may only have 29 days (February - thus 02)
  • d_month30: Match months that only have 30 days (April, June, September, November - thus 04, 06, 09, 11)
  • d_month31: Match months that only have 31 days (January, March, May, July, August, October, December - thus 01, 03, 05, 07, 08, 10, 12)
  • d_year: Match any year (must have at least one digit \d)
  • d_yearLeap: I'll break this into multiple segments for better clarity
    • \d*?
      • Match any number of digits, but as few as possible
    • Match one of the following
      • (?:(?:(?!00)[02468][048]|[13579][26])|(?:(?:[02468][048]|[13579][26])00)) - Match one of the following
      • (?:(?!00)[02468][048]|[13579][26]) - Match one of the following
        • One of 02468, followed by one of 048, but not 00
        • One of 13579, followed by one of 26
      • (?:(?:[02468][048]|[13579][26])00) - Match one of the following, followed by 00
        • One of 02468, followed by one of 048
        • One of 13579, followed by one of 26
      • [48]00 - Match 400 or 800
      • [48] - Match 4 or 8
    • (?=\D|\b) - Ensure what follows is either a non-digit character \D or word boundary character \b
  • d_format: This points to previous groups in order to ensure months are properly formatted and match the days/month and days/year(leap year) requirements so that we can ensure proper date validation
  • t_period: This was added in case others needed this for validation purposes
    • Ensures the period is either am, pm, a.m, p.m or their respective uppercase versions (including things such as a.M where multliple cases are used)
  • t_hours12: Match any hour from 00 to 11
  • t_hours24: Match any hour from 00 to 23
  • t_minutes: Match any minutes from 00 to 59
  • t_seconds: Match any seconds from 00 to 59
  • t_milliseconds: Match any 3 digits (000 to 999)
  • t_format: This points to previous groups in order to ensure time is properly formatted. I've added an additional time setting (as well as an addition including milliseconds and time period for others' use)
  • dt_format: Datetime format to check against (in your case it's date time - separation by a space character)
  • Following the define block is \b(?&dt_format)\b, which simply matches the dt_format as specified above, ensuring what precedes and supercedes it is a word boundary character (or no character) \b

Leap year

To further understand the leap year section of the regex...

I am assuming the following:

  • All years are NOT leap years, unless, the following is true
    • ((Year modulo 4 is 0) AND (year modulo 100 is not 0)) OR (year modulo 400 is 0)
    • Source: leap year calculation
    • Leap years have always existed (at least since year 1) - since I don't want to start assuming and do even more research.

The regex works by ensuring:

  1. All leap years that end in 0, 4, 8 are preceded by a 0, 2, 4, 6, 8 (all of which result in 0 after modulus -> i.e. 24 % 4 = 0)
  2. All leap years that end in 2, 6 are preceded by a 1, 3, 5, 7, 9 (all of which result in 0 after modulus -> i.e. 32 % 4 = 0)
  3. All leap years that end in 00, for 1. and 2., are negated ((?!00) does this)
  4. All leap years that end in 00 are preceded by 1. and 2. (exactly the same since 4 * 100 = 400 - nothing needs to be changed except the last two digits)
  5. Add the years 400, 800, 4, 8 since they are not satisfied by any of the above conditions


Edits

October 25th, 2017

Thanks to @sln for the input on the leap year's functionality. The regex below performs slightly faster due to changes provided in the comments of this answer by sln (on a separate question). Changed (?:(?!00)[02468][048]|[13579][26]) to (?:0[48]|[13579][26]|[2468][048]) in the leap year section.

See regex in use here

(?(DEFINE)
  (?# Date )
    (?# Day ranges )
    (?<d_day28>0[1-9]|1\d|2[0-8])
    (?<d_day29>0[1-9]|1\d|2\d)
    (?<d_day30>0[1-9]|1\d|2\d|30)
    (?<d_day31>0[1-9]|1\d|2\d|3[01])
    (?# Month specifications )
    (?<d_month28>02)
    (?<d_month29>02)
    (?<d_month30>0[469]|11)
    (?<d_month31>0[13578]|1[02])
    (?# Year specifications )
    (?<d_year>\d+)
    (?<d_yearLeap>(?:\d*?(?:(?:0[48]|[13579][26]|[2468][048])|(?:(?:[02468][048]|[13579][26])00))|[48]00|[48])(?=\D|\b))
    (?# Valid date formats )
    (?<d_format>
      (?&d_day28)\/(?&d_month28)\/(?&d_year)|
      (?&d_day29)\/(?&d_month29)\/(?&d_yearLeap)|
      (?&d_day30)\/(?&d_month30)\/(?&d_year)|
      (?&d_day31)\/(?&d_month31)\/(?&d_year)
    )

  (?# Time )
    (?# Time properties )
    (?<t_period12>(?i)[ap]m|[ap]\.m\.(?-i))
    (?# Hours )
    (?<t_hours12>0\d|1[01])
    (?<t_hours24>[01]\d|2[0-3])
    (?# Minutes )
    (?<t_minutes>[0-5]\d)
    (?# Seconds )
    (?<t_seconds>[0-5]\d)
    (?# Milliseconds )
    (?<t_milliseconds>\d{3})
    (?# Valid time formats )
    (?<t_format>
      (?&t_hours12):(?&t_minutes):(?&t_seconds)(?:\.(?&t_milliseconds))?\ ?(?&t_period12)|
      (?&t_hours24):(?&t_minutes):(?&t_seconds)(?:\.(?&t_milliseconds))?
    )

  (?# Datetime )
    (?<dt_format>(?&d_format)\ (?&t_format))
)
\b(?&dt_format)\b

Your sequence to match the year is:

([19\s[2-9\s][0-9\s])\d\d

Which looks malformed, as the brackets do not match.

Also, the presence of the two digits (using \d) means that the expression will not match white space.