5

what is the fastest way to check for a valid DateTime? I need to take into account not just that the string cointains year, month, day, hour and minute, but also that the datetime is valid, eg: 2017-02-29 10:00 should be considered not valid because it is 29th in a non leap year.

I have an array of string elements (300k elements) in the format: YYYYMMDDHHmm, and I need to check each line in the fastest way possible.

Using moment.js to check validity of each elements requires around 5s in a regular for loop:

for (let i = 0; i < length; i++) {
    let el = datetimes[i];
    let d = moment.utc(el, "YYYYMMDDHHmm");
    d.isValid();
}

Are there faster alternatives?

smellyarmpits
  • 1,080
  • 3
  • 13
  • 32

2 Answers2

7

Rule out as much as you can before calling any kind of string matcher or regular expression.

moment is going to evaluate that expression EVERY time. (unless is has come creative caching going on)

You can check for falsy values first (undefined, null, 0, '', etc).

if (!el) {
    // not valid
}

You can check the length after checking null and undefined (thanks Luca)

if (el.length !== 12) {
   // not valid
}

You can also precompile a REGEX and use that.

// define this outside of your loop
let rx2 = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})$/

function leapYear(year) {
  return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}

function checkValidDate(strDate) {
  if (!strDate || strDate.length !== 12) throw new Error('invalid date')
  let m = rx2.exec(strDate)
  if (!m) throw new Error('invalid date')

  let year = parseInt(m[1])
  if (year < 2000 || year >= 2100) throw new Error('bad year')

  let month = parseInt(m[2])
  if (month > 11) throw new Error('bad month')

  let day = parseInt(m[3])
  // base 0 days and months
  switch (month) {
    case 0, 2, 4, 5, 6, 7, 9, 11:
      if (day > 30) throw new Error('bad day')
      break;
    case 3, 5, 8, 10:
      if (day > 29) throw new Error('bad day')
      break;
    case 1:
      if (day > 28) throw new Error('bad day')
      if (day === 28 && !isLeapYear(year)) throw new Error('bad day')
      break;
  }

  let hour = parseInt(m[4])
  if (hour > 23) throw new Error('bad hour')

  let minute = parseInt([5])
  if (hour > 59) throw new Error('bad minute')
}

try {
  checkValidDate('asdf')
  console.log('valid')
} catch (e) {
  console.error(e)
}

try {
  checkValidDate('200011241230')
  console.log('valid')
} catch (e) {
  console.error(e)
}

try {
  checkValidDate('200001300000')
  console.log('valid')
} catch (e) {
  console.error(e)
}
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
  • 1
    Or, check if the length is less than 12 – Luca Kiebel Aug 08 '18 at 11:03
  • 1
    Thank you for your answer, but your solution just checks that a string is a possible valid date. I guess I haven't explained well. I also need to check that the DateTime that it should represent is valid. For instance: your solution would flag as valid 201899999999. I need to spot these cases (eg. Feb 29th in non leap years). Same thing applies to the time part of the DateTime – smellyarmpits Aug 08 '18 at 13:38
  • That's why I added the rx2. The question was the 'fastest' way. I will adjust rx2 but you need to clearly define the date parameters in your question. – Steven Spungin Aug 08 '18 at 14:02
  • @MarcoGalassi see updated code snippet for guidance. – Steven Spungin Aug 08 '18 at 14:05
  • @MarcoGalassi Are your months and days 0 or 1 based, etc. It's up to you to validate, but this is about as fast as it gonna get unless you write a lexer... – Steven Spungin Aug 08 '18 at 14:10
  • your code is ok, but it doesn't tell me if the string represents a valid date. 201899999999 would still considered valid. – smellyarmpits Aug 08 '18 at 14:12
  • You can solve that yourself or post another question. The posted question was not how to parse a valid date, it was what is the fastest way to parse. The stated solution breaks the date into integers that you can simply compare as needed. Press 'run code snippet' and scroll down... – Steven Spungin Aug 08 '18 at 14:19
  • Maybe use a similar regular expression to convert the string into a ISO8601 string, and attempt something like: (new Date(convertedDateString)).toString() === 'Invalid Date'. How would that perform? – Firephp Aug 08 '18 at 14:20
  • 1
    @Firephp That's gonna slow things down by parsing it again. All the elements are already converted to ints after the RegEx does it's thing. – Steven Spungin Aug 08 '18 at 14:22
  • Well I meant in a different solution, based on this one. But, by using this solution, one could probably just initialize a Date using the values in the variables then. (new Date(``${year}-${month}-${day}T${hour}:${minute}``)).toString() === 'Invalid Date' (would be an invalid date) – Firephp Aug 08 '18 at 14:25
  • @Firephp Yep, that might possibly be faster then the original moment(...) call in the loop. – Steven Spungin Aug 08 '18 at 14:26
  • Well, my question was how to check if a string represents a valid datetime. And, your solution, although I understand that it can be useful in certain contexts, does not what I am looking for because it does not check if a DateTime is valid.. – smellyarmpits Aug 08 '18 at 15:31
  • what is your min and max year? are your months 0 or 1 based? are your days 0 or 1 based? – Steven Spungin Aug 08 '18 at 15:33
  • Marco, did you try using the addition I posted in my comment? I believe by attempting to instantiate a Date, and calling toString(), you can determine if a date time is considered valid based on whether or not the returned value is a stringified date, or 'Invalid Date'. You can get the values needed to assemble the date time string by using the values from Steven's solution. – Firephp Aug 09 '18 at 01:52
  • What if the date is 2000 02 30 00 00...? Some implementations will roll over extra days into the next month? The validation parameters need to be defined somewhere. Leap day checks... Maybe pass the date through to new Date or other only if it is questionable. That will help increase speed. – Steven Spungin Aug 09 '18 at 01:58
  • Months and days are 0 based and min and max years can be min: 2000 and max: 2100. Sure, some implementations will roll 2000-02-30 00:00 over in the next month. But 2000-02-30 is not a valid date. @Firephp your solution with new Date() rolls "2018-02-30" to "2018-03-02T00:00" so it does not work for me – smellyarmpits Aug 09 '18 at 06:55
  • Maybe add a leap year check, along with a check for an invalid leap year date. https://stackoverflow.com/questions/16353211/check-if-year-is-leap-year-in-javascript – Firephp Aug 09 '18 at 12:29
  • 1
    @Firephp I updated with zero based month and day, and leap year check. It's weird to use zero based months and days... – Steven Spungin Aug 09 '18 at 12:44
  • 1
    Next step would be to read chars 4 or 2 at a time and parse them, and check the integer, before moving on, so as to not RX the whole input.... @MarcoGalassi are you satisfied yet? If it's not fast enough we can create a giant hash table with every date/time in that range! Good thing you are not using seconds :) – Steven Spungin Aug 09 '18 at 13:03
  • 1
    yeah, i've tried something similar following advices on other resources before you posted. Now i'm satisfied ahaha :) – smellyarmpits Aug 09 '18 at 15:31
0

You can write a RegExp for checking date strings

This is the sample rexexp for checking YYYYMMDDHHmm

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

Please check this bin https://jsbin.com/zawojov/edit?js,console

Ayhan
  • 183
  • 1
  • 3
  • 15