ValiDate ISO 8601 par RX

16

Défi

Trouvez l'expression rationnelle la plus courte

  1. valide, c'est-à-dire les correspondances, toutes les dates possibles du calendrier grégorien proleptique (qui s'applique également à toutes les dates avant sa première adoption en 1582) et
  2. ne correspond à aucune date invalide.

Production

La sortie est donc véridique ou falsey.

Contribution

L'entrée est dans l'un des 3 formats de date ISO 8601 étendus - pas d'heure.

Les deux premiers sont ±YYYY-MM-DD(année, mois, jour) et ±YYYY-DDD(année, jour). Les deux ont besoin d'un boîtier spécial pour le jour bissextile. Ils sont naïvement appariés séparément par ces RX étendus:

(?<year>[+-]?\d{4,})-(?<month>\d\d)-(?<day>\d\d)
(?<year>[+-]?\d{4,})-(?<doy>\d{3})

Le troisième format d'entrée est ±YYYY-wWW-D(année, semaine, jour). C'est le plus compliqué en raison du schéma complexe des semaines bissextiles.

(?<year>-?\d{4,})-W(?<week>\d\d)-(?<dow>\d)

Un contrôle de validité basique mais insuffisant pour les trois combinés ressemblerait à ceci:

[+-]?\d{4,}-((0\d|1[0-2])-([0-2]\d|3[01]) ↩
            |([0-2]\d\d|3[0-5]\d|36[0-6]) ↩
            |(W([0-4]\d|5[0-3])-[1-7]))

Conditions

Une année bissextile dans le calendrier grégorien proleptique contient le jour bissextile …-02-29 et dure donc 366 jours, donc …-366existe. Cela se produit dans n'importe quelle année dont le nombre ordinal est divisible par 4, mais pas par 100 à moins qu'il ne soit également divisible par 400. L' année zéro existe dans ce calendrier et c'est une année bissextile.

Une longue année dans le calendrier hebdomadaire ISO contient une 53e semaine, que l'on pourrait qualifier de « semaine bissextile ». Cela se produit toutes les années où le 1er janvier est un jeudi et en outre toutes les années bissextiles où c'est un mercredi. Il se produit généralement tous les 5 ou 6 ans, de façon apparemment irrégulière.

Une année comporte au moins 4 chiffres. Les années de plus de 10 chiffres ne doivent pas être prises en charge, car c'est assez proche de l'âge de l'univers (environ 14 milliards d'années). Le signe plus en tête est facultatif, bien que la norme actuelle suggère qu'il devrait être requis pendant des années avec plus de 4 chiffres.

Les dates partielles ou tronquées, c'est-à-dire avec une précision inférieure au jour, ne doivent pas être acceptées.

Les parties de la notation de date, par exemple le mois, n'ont pas besoin d'être appariées par un groupe qui pourrait être référencé.

Règles

C'est du code-golf. Le regex le plus court sans code exécuté gagne. Mise à jour: Vous pouvez utiliser des fonctionnalités telles que la récursivité et les groupes équilibrés, mais vous serez condamné à une amende d'un facteur 10, avec lequel le nombre de caractères sera ensuite multiplié! Ceci est maintenant différent des règles du golf en code dur: Regex pour la divisibilité par 7 . Une réponse antérieure gagne une égalité.

Cas de test

Tests valides

2015-08-10
2015-10-08
12015-08-10
-2015-08-10
+2015-08-10
0015-08-10
1582-10-10
2015-02-28
2016-02-29
2000-02-29
0000-02-29
-2000-02-29
-2016-02-29
200000-02-29
2016-366
2000-366
0000-366
-2016-366
-2000-366
2015-081
2015-W33-1
2015-W53-7
 2015-08-10 

Le dernier est éventuellement valide, c'est-à-dire que les espaces de début et de fin dans les chaînes d'entrée peuvent être coupés.

Formats invalides

-0000-08-10     # that's an arbitrary decision
15-08-10        # year is at least 4 digits long
2015-8-10       # month (and day) is exactly two digits long, i.e. leading zero is required
015-08-10       # year is at least 4 digits long
20150810        # though a valid ISO format, we require separators; could also be interpreted as a 8-digit year
2015 08 10      # separator must be hyphen-minus
2015.08.10      # separator must be hyphen-minus
2015–08–10      # separator must be hyphen-minus
2015-0810
201508-10       # could be October in the year 201508
2015 - 08 - 10  # no internal spaces allowed
2015-w33-1      # letter ‘W’ must be uppercase
2015W33-1       # it would be unambiguous to omit the separator in front of a letter, but not in the standard
2015W331        # though a valid ISO format we require separators
2015-W331
2015-W33        # a valid ISO date, but we require day-precision
2015W33

Dates invalides

2015        # a valid ISO format, but we require day-precision
2015-08     # a valid ISO format, but we require day-precision
2015-00-10  # month range is 1–12
2015-13-10  # month range is 1–12
2015-08-00  # day range is 1–28 through 31
2015-08-32  # max. day range is 1–31
2015-04-31  # day range for April is 1–30
2015-02-30  # day range for February is 1–28 or 29
2015-02-29  # day range for common February is 1–28
2100-02-29  # most century years are non-leap
-2100-02-29 # most century years are non-leap
2015-000    # day range is 1–365 or 366
2015-366    # day range is 1–365 in common years
2016-367    # day range is 1–366 in leap years
2100-366    # most century years are non-leap
-2100-366   # most century years are non-leap
2015-W00-1  # week range is 1–52 or 53
2015-W54-1  # week range is 1–53 in long years
2016-W53-1  # week range is 1–52 in short years
2015-W33-0  # day range is 1–7
2015-W33-8  # day range is 1–7
Crissov
la source
2
Cette question n'est pas bien définie car le langage regex n'est pas spécifié.
orlp
1
@orlp S'il n'est pas spécifié, le choix n'est pas limité. J'ai écrit «regex» ou «RX» exprès, donc on pourrait utiliser des dialectes qui permettent la récursivité, etc. (c'est-à-dire CFG, pas RG).
Crissov
Je vous suggère fortement de limiter le langage regex, car il sera très amer pour un concurrent de travailler pendant des heures sur une solution pour être battu trivialement par un langage qui est fondamentalement plus puissant. Si vous deviez limiter la langue à la définition CS réelle des expressions régulières (comme DFA), le problème devient une réponse d'optimisation intéressante.
orlp
La validation des dates ISO-8601 à l'aide d'expressions régulières est quelque chose que j'ai dû faire pour le travail. Mais d'accord avec orlp, je pense qu'une langue est nécessaire ici.
Alex A.
1
Regex hérite de Method en Perl 6 et est donc lui-même une forme de code exécutable.
Brad Gilbert b2gills

Réponses:

4

PCRE (également Perl), 778 octets

/^([+-]?\d*((([02468][048]|[13579][26]|\d\d(?!00))([02468][048]|[13579][26]))|\d{4}(?!-02-29|-366))-((?!02-3|(0[469]|11)-31|000)((0[1-9]|1[012])-(0[1-9]|[12]\d|30|31)|([012]\d\d|3([0-5]\d|6[0-6])))|(W(?!00)([0-4]\d|51|52)-[1-7]))|((\+?\d*([02468][048]|[13579][26])|-\d*([02468][159]|[13579][37]))(04|09|15|20|26|32|37|43|48|54|60|65|71|76|82|88|93|99)|(\+?\d*([02468][159]|[13579][37])|-\d*([02468][26]|[13579][048]))(05|11|16|22|28|33|39|44|50|56|61|67|72|78|84|89|95)|(\+?\d*([02468][26]|[13579][048])|-\d*([02468][37]|[13579][159]))(01|07|12|18|24|29|35|40|46|52|57|63|68|74|80|85|91|96)|\+?\d*(([02468][37]|[13579][159])(03|14|20|25|31|36|42|53|59|64|70|76|81|87|92|[049]8))|-\d*(([02468][048]|[13579][26])([059]2|08|13|19|24|30|36|41|47|58|64|69|75|80|86|97)))-W53-[1-7])$/

J'ai inclus les délimiteurs dans le nombre d'octets pour montrer qu'il ne repose sur aucun indicateur.

Il ne correspond pas à des dates valides dans d'autres chaînes, telles que 1234-56-89 2016-02-29 9876-54-32. Le regex est plus court en ne vérifiant pas un maximum de 10 chiffres pour l'année.

Etendu avec des commentaires:

/^  # Start of pattern (no leading space)
  (
    # YEAR
    # Optional sign and digits if more than 4 in year
    [+-]?\d*(
      # Years 00??, 04??, 08?? ... 92??, 96?? OR dd not followed by 00
      # followed by 00, 04, 08 ... 92, 96 OR
      (([02468][048]|[13579][26]|\d\d(?!00))([02468][048]|[13579][26])) |
      # any year not followed by 29 February or day 366
      \d{4}(?!-02-29|-366)
    # dash
    ) -
    # MONTH AND DAY, or DAY OF YEAR, or WEEK OF YEAR AND DAY if less than 53 weeks
    (
      # Not (30 or 31 February OR 31 April, June, September or December OR day 0)
      (?!02-3|(0[469]|11)-31|000)
      (
        # Month         dash         day         OR
        (0[1-9]|1[012]) - (0[1-9]|[12]\d|30|31) |
        # 001-299 OR 300-359 OR 360-366
        ([012]\d\d | 3([0-5]\d | 6[0-6]))
      # OR
      ) |
      (
        # W    01-52    dash    1-7
        W(?!00)([0-4]\d|51|52)-[1-7]
      )
    # OR
    ) |
    # WEEK OF YEAR AND DAY only if week is 53
    (
      # Optional plus and extra year digits
      \+?\d*(
        # Years +0303 - +9998
        ([02468][37]|[13579][159])(03|14|20|25|31|36|42|53|59|64|70|76|81|87|92|[049]8)
      ) |
      # Minus and extra year digits
      -\d*(
        # Years -0002 - -9697
        ([02468][048]|[13579][26])([059]2|08|13|19|24|30|36|41|47|58|64|69|75|80|86|97)
      ) |
      # Years +0004 - +9699, -0104 - -9799
      (\+?\d*([02468][048]|[13579][26])|-\d*([02468][159]|[13579][37]))
          (04|09|15|20|26|32|37|43|48|54|60|65|71|76|82|88|93|99) |
      # Years +0105 - +9795, -0205 - -9895
      (\+?\d*([02468][159]|[13579][37])|-\d*([02468][26]|[13579][048]))
          (05|11|16|22|28|33|39|44|50|56|61|67|72|78|84|89|95) |
      # Years +0201 - +9896, -0301 - -9996
      (\+?\d*([02468][26]|[13579][048])|-\d*([02468][37]|[13579][159]))
          (01|07|12|18|24|29|35|40|46|52|57|63|68|74|80|85|91|96)
    # dash W 53 dash 1-7
    )-W53-[1-7]
  # End of pattern (no trailing space)
  )$/x
CJ Dennis
la source
Je n'ai pas encore tout vérifié, mais il semble que vous gagniez le plus d'octets par (?!…)expressions par rapport à ma solution.
Crissov
1
@Crissov Les (?!…)expressions ne sauvent que quelques octets chacune. J'ai réduit beaucoup d'octets en combinant trois des modèles d'année de semaine / jour de semaine positifs / négatifs en un chacun. Les derniers ne correspondent pas. J'ai donc obtenu 8 longs sous-modèles jusqu'à 5. De plus, puisque |20|25|c'est la même longueur que celle que |2[05]|j'ai choisie pour l'option plus lisible.
CJ Dennis
Cette expression correspond au cas de test -0000-08-10 et ne correspond pas ␠2015-08-10␠aux espaces de début et de fin, mais comme les deux étaient des décisions arbitraires ou des fonctionnalités facultatives, je vais la laisser glisser.
Crissov
Je pense que cette solution a un bug pour les dates au sein de W50.
Crissov
W(?!00)([0-4]\d|51|52)-[1-7]doit être quelque chose d'équivalent W(?!00)([0-4]\d|5[0-2])-[1-7]. Cela ajoute un caractère à la longueur. 779
Crissov
9

PCRE: 603 940 947 949 956 octets

^\s*[+-]?(\d{4,10}-((00[1-9]|0[1-9]\d|[12]\d\d|3[0-5]\d|36[0-5])|(0[1-9]|1[0-2])-(0[1-9]|1\d|2[0-8])|(0[13-9]|1[0-2])-(29|30)|(0[13578]|1[02])-31|W(0[1-9]|[1-4]\d|5[0-2])-[1-7]))|((\d{2,8}([13579][26]|[2468][048]|0[48])|(\d{0,6}([13579][26]|[02468][048])00))-(366|02-29))|(\+?\d{0,6}(([02468][048]|[13579][26])([26]0|71|[38]2|[49]3|[05]4|15|[27]6|37|[48]8|[09]9)|([02468][159]|[13579][37])(50|[16]1|[27]2|33|[48]4|[09]5|[15]6|67|[27]8|[38]9)|([02468][26]|[13579][048])([48]0|[09]1|[15]2|63|[27]4|[38]5|[49]6|[05]7|[16]8|29)|([02468][37]|[13579][159])([27]0|[38]1|[49]2|[05]3|[16]4|25|[37]6|87|[049]8|[5]9))|-\d{0,6}(([02468][048]|[13579][26])(0[28]|1[39]|24|3[06]|4[17]|5[28]|6[49]|75|8[06]|9[27])|([02468][159]|[13579][37])(0[49]|15|2[06]|3[27]|4[38]|54|6[05]|7[16]|8[28]|9[39])|([02468][26]|[13579][048])(0[51]|16|2[28]|3[39]|44|5[06]|6[17]|7[28]|8[49]|95)|([02468][37]|[13579][159])(0[17]|1[28]|2[49]|35|4[06]|5[27]|6[38]|74|8[05]|9[16])))-W53-[1-7]\s*$

Remarque: certaines paires de parenthèses peuvent éventuellement être supprimées.

Divisibilité par 4

Les multiples de 4 se répètent dans un schéma simple:

  • 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, …

Ceci, ou l'inverse, pourrait être mis en correspondance par une expression régulière également simple pour tous les nombres à deux chiffres avec un zéro devant:

(?<divisible-by-four>[13579][26]|[02468][048])
(?<not-divisible-by-four>[13579][048]|[02468][26]|\d[13579])

Cela pourrait économiser quelques octets s'il y avait des classes de caractères pour les chiffres pairs et impairs (comme \oet \e), mais il n'y en a pas pour autant que je sache.

Ans

Cette expression suffirait pour le calendrier julien, mais la détection de l'année bissextile grégorienne doit dans un cas spécial suivre 00avec une divisibilité du siècle par 4:

(?<leap-year>[+-]?(\d{2,8}([13579][26]|[2468][048]|0[48])|(\d{0,6}([13579][26]|[02468][048])00))
(?<year>[+-]?\d{4,10})

Cela nécessiterait quelques modifications pour mettre hors la loi -0000-…(avec -00000-…etc.) ou pour appliquer le signe plus pour les nombres d'années positifs avec plus de 4 chiffres. Ce dernier serait plutôt simple, mais n'est pas obligatoire:

(?<leap-year>([+-]?(\d\d([13579][26]|[2468][048]|0[48])|(([13579][26]|[02468][048])00)))|([+-](\d{3,8}([13579][26]|[2468][048]|0[48])|(\d{1,6}([13579][26]|[02468][048])00))))
(?<year>([+-]?\d{4})|([+-]\d{5,10}))

Jour de l'année

Les dates ordinales à trois chiffres sont assez simples, il suffit de limiter -366les années bissextiles (et de les interdire -000).

(?<ordinal-day>-(00[1-9]|0[1-9]\d|[12]\d\d|3[0-5]\d|36[0-5]))
(?<ordinal-leap-day>-366)

Jour du mois de l'année

Les sept mois avec 31 jours sont 01janvier, 03mars, 05mai, 07juillet, 08août, 10octobre et 12décembre. Quatre mois seulement ont exactement 30 jours, 04avril, 06juin, 09septembre et 11novembre. Enfin, 02février compte 28 jours en années communes et 29 en années bissextiles. Nous pouvons d' abord construire une expression régulière pour les jours toujours valides 01par 28et ajoutez des cas particuliers.

(?<month-day>-(0[1-9]|1[0-2])-(0[1-9]|1\d|2[0-8]))
(?<short-month-day>-(0[13-9]|1[0-2])-(29|30))
(?<long-month-day>-(0[13578]|1[02])-31)
(?<month-leap-day>-02-29)

Ni le mois ni le jour ne doivent être 00non couverts par une version antérieure.

Jour de la semaine de l'année

Toutes les années incluent 52 semaines

(?<week-day>-W(0[1-9]|[1-4]\d|5[0-2])-[1-7])

Les longues années qui incluent la-W53 répétition dans un cycle de 400 ans, par exemple ajoutez 2000 pour le cycle actuel et trouvez l'année en cours dans la troisième entrée:

  • 004, 009, 015, 020, 026, 032, 037, 043, 048, 054, 060, 065, 071, 076, 082, 088, 093, 099,
  • 105, 111, 116, 122, 128, 133, 139, 144, 150, 156, 161, 167, 172, 178, 184, 189, 195,
  • 201, 207, 212, 218, 224, 229, 235, 240, 246, 252, 257, 263, 268, 274, 280, 285, 291, 296,
  • 303, 308, 314, 320, 325, 331, 336, 342, 348, 353, 359, 364, 370, 376, 381, 387, 392, 398.

Chacun des quatre siècles a un motif unique. Il n'y a probablement pas beaucoup de place pour l'optimisation.

  1. 04|09|15|20|26|32|37|43|48|54|60|65|71|76|82|88|93|99
  2. 05|11|16|22|28|33|39|44|50|56|61|67|72|78|84|89|95
  3. 01|07|12|18|24|29|35|40|46|52|57|63|68|74|80|85|91|96
  4. 03|08|14|20|25|31|36|42|48|53|59|64|70|76|81|87|92|98

Nous pouvons regrouper par l'un ou l'autre chiffre pour découvrir que nous pouvons économiser environ deux octets:

  • Regroupé par 1er chiffre.
    1. 0[49]|15|2[06]|3[27]|4[38]|54|6[05]|7[16]|8[28]|9[39]
    2. 05|1[16]|2[28]|3[39]|44|5[06]|6[17]|7[28]|8[49]|95
    3. 0[17]|1[28]|2[49]|35|4[06]|5[27]|6[38]|74|8[05]|9[16]
    4. 0[38]|14|2[05]|3[16]|4[28]|5[39]|64|7[06]|8[17]|9[28]
  • Regroupé par 2e chiffre.
    1. [26]0|71|[38]2|[49]3|[05]4|15|[27]6|37|[48]8|[09]9
    2. 50|[16]1|[27]2|33|[48]4|[09]5|[15]6|67|[27]8|[38]9
    3. [48]0|[09]1|[15]2|63|[27]4|[38]5|[49]6|[05]7|[16]8|29
    4. [27]0|[38]1|[49]2|[05]3|[16]4|25|[37]6|87|[049]8|[5]9

Le nombre de siècle est facilement égalé à nouveau par une variation de l'expression de divisibilité.

  • 1er siècle: [02468][048]|[13579][26]
  • 2ème siècle: [02468][159]|[13579][37]
  • 3ème siècle: [02468][26]|[13579][048]
  • 4ème siècle: [02468][37]|[13579][159]

Jusqu'à présent, cela ne fonctionne que pour les années positives, y compris l'année zéro. Pour les années négatives, nous devons soustraire les valeurs de la liste ci-dessus de 400 et refaire le reste, car le modèle n'est pas symétrique.

  1. 02|08|13|19|24|30|36|41|47|52|58|64|69|75|80|86|92|97
  2. 04|09|15|20|26|32|37|43|48|54|60|65|71|76|82|88|93|99
  3. 05|11|16|22|28|33|39|44|50|56|61|67|72|78|84|89|95
  4. 01|07|12|18|24|29|35|40|46|52|57|63|68|74|80|85|91|96

ou

  1. 0[28]|1[39]|24|3[06]|4[17]|5[28]|6[49]|75|8[06]|9[27]
  2. 0[49]|15|2[06]|3[27]|4[38]|54|6[05]|7[16]|8[28]|9[39]
  3. 0[51]|16|2[28]|3[39]|44|5[06]|6[17]|7[28]|8[49]|95
  4. 0[17]|1[28]|2[49]|35|4[06]|5[27]|6[38]|74|8[05]|9[16]

Mettre tous ensemble

N'importe quelle année

[+-]?\d{4,10}-((00[1-9]|0[1-9]\d|[12]\d\d|3[0-5]\d|36[0-5])|(0[1-9]|1[0-2])-(0[1-9]|1\d|2[0-8])|(0[13-9]|1[0-2])-(29|30)|(0[13578]|1[02])-31|W(0[1-9]|[1-4]\d|5[0-2])-[1-7])

Ajouts d'année bissextile

[+-]?(\d{2,8}([13579][26]|[2468][048]|0[48])|(\d{0,6}([13579][26]|[02468][048])00))-(366|02-29)

Ajouts d'une année bissextile

+?\d{0,6}(([02468][048]|[13579][26])([26]0|71|[38]2|[49]3|[05]4|15|[27]6|37|[48]8|[09]9)|([02468][159]|[13579][37])(50|[16]1|[27]2|33|[48]4|[09]5|[15]6|67|[27]8|[38]9)|([02468][26]|[13579][048])([48]0|[09]1|[15]2|63|[27]4|[38]5|[49]6|[05]7|[16]8|29)|([02468][37]|[13579][159])([27]0|[38]1|[49]2|[05]3|[16]4|25|[37]6|87|[049]8|[5]9))-W53-[1-7]
-\d{0,6}(([02468][048]|[13579][26])(0[28]|1[39]|24|3[06]|4[17]|5[28]|6[49]|75|8[06]|9[27])|([02468][159]|[13579][37])(0[49]|15|2[06]|3[27]|4[38]|54|6[05]|7[16]|8[28]|9[39])|([02468][26]|[13579][048])(0[51]|16|2[28]|3[39]|44|5[06]|6[17]|7[28]|8[49]|95)|([02468][37]|[13579][159])(0[17]|1[28]|2[49]|35|4[06]|5[27]|6[38]|74|8[05]|9[16]))-W53-[1-7]
Crissov
la source
Votre modèle n'est pas ancré au début et à la fin, il correspondra donc à des dates valides dans une chaîne par ailleurs non valide.
CJ Dennis
@CJDennis C'est vrai, je vais ajouter les deux personnages maintenant.
Crissov
J'ai également ajouté des espaces de début et de fin facultatifs \s*.
Crissov