ValiDate ISO 8601 de RX

16

Desafío

Encuentra la expresión regular más corta que

  1. valida, es decir, coincide, cada fecha posible en el calendario gregoriano proleptico (que también se aplica a todas las fechas antes de su primera adopción en 1582) y
  2. no coincide con ninguna fecha no válida

Salida

El resultado es, por lo tanto, verdadero o falso.

Entrada

La entrada está en cualquiera de los 3 formatos de fecha ISO 8601 expandidos , no hay veces.

Los dos primeros son ±YYYY-MM-DD(año, mes, día) y ±YYYY-DDD(año, día). Ambos necesitan una carcasa especial para el día bisiesto. Son ingenuamente emparejados por separado por estos RX extendidos:

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

El tercer formato de entrada es ±YYYY-wWW-D(año, semana, día). Es el complicado debido al complejo patrón de semana bisiesto.

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

Una comprobación de validez básica pero insuficiente para los tres combinados se vería así:

[+-]?\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]))

Condiciones

Un año bisiesto en el calendario gregoriano proleptico contiene el dia bisiesto …-02-29 y por lo tanto es de 366 dias, por lo tanto …-366existe. Esto sucede en cualquier año cuyo número ordinal es divisible por 4, pero no por 100 a menos que también sea divisible por 400. El año cero existe en este calendario y es un año bisiesto.

Un año largo en el calendario de la semana ISO contiene una 53ª semana, que se podría llamar una " semana bisiesta ". Esto sucede en todos los años donde el 1 de enero es jueves y, además, en todos los años bisiestos donde es miércoles. Resulta que ocurre cada 5 o 6 años por lo general, en un patrón aparentemente irregular.

Un año tiene al menos 4 dígitos. Los años con más de 10 dígitos no tienen que ser compatibles, porque eso es lo suficientemente cercano a la edad del universo (aproximadamente 14 mil millones de años). El signo más inicial es opcional, aunque el estándar real sugiere que debería requerirse durante años con más de 4 dígitos.

No se deben aceptar fechas parciales o truncadas, es decir, con una precisión inferior al día.

Las partes de la notación de fecha, por ejemplo, el mes, no tienen que coincidir con un grupo al que se pueda hacer referencia.

Reglas

Este es el código de golf. La expresión regular más corta sin código ejecutado gana. Actualización: puede usar características como la recursividad y grupos equilibrados, pero será multado por un factor de 10, ¡con el cual se multiplicará el recuento de caracteres! Esto ahora es diferente de las reglas en Hard code golf: Regex para la divisibilidad por 7 . La respuesta anterior gana un empate.

Casos de prueba

Pruebas válidas

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 

El último es opcionalmente válido, es decir, se pueden recortar los espacios iniciales y finales en las cadenas de entrada.

Formatos inválidos

-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

Fechas inválidas

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
fuente
2
Esta pregunta no está bien definida porque el lenguaje regex no está especificado.
orlp
1
@orlp Si no se especifica, la elección no está limitada. Escribí "regex" o "RX" a propósito, por lo que uno podría usar dialectos que permitan la recursividad, etc. (es decir, CFG, no RG).
Crissov
Le sugeriría encarecidamente que limite el lenguaje regex, porque será muy difícil para un concursante trabajar durante horas en una solución solo para ser derrotado trivialmente por un lenguaje que es fundamentalmente más poderoso. Si tuviera que limitar el lenguaje a la definición real de CS de expresiones regulares (como DFA), entonces el problema se convierte en una interesante respuesta de optimización.
orlp
Validar las fechas ISO-8601 usando expresiones regulares es algo que realmente he tenido que hacer para trabajar. Pero de acuerdo con orlp, creo que un lenguaje es necesario aquí.
Alex A.
1
Regex hereda de Method en Perl 6, por lo que es una forma de código ejecutable.
Brad Gilbert b2gills

Respuestas:

4

PCRE (también Perl), 778 bytes

/^([+-]?\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])$/

He incluido los delimitadores en el recuento de bytes para mostrar que no se basa en ningún indicador.

No , no coincide con fechas válidas dentro de otras cadenas, como por ejemplo 1234-56-89 2016-02-29 9876-54-32. La expresión regular es más corta al no verificar un máximo de 10 dígitos para el año.

Extendido con comentarios:

/^  # 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
fuente
Todavía no he comprobado todo, pero parece que obtienes la mayor cantidad de bytes por (?!…)expresiones en comparación con mi solución.
Crissov
1
@Crissov Las (?!…)expresiones solo guardan unos pocos bytes cada una. Reduje muchos bytes combinando tres de los patrones positivos / negativos de la semana del año / día de la semana en uno cada uno. Los últimos no se corresponden entre sí. Así que obtuve 8 subpatrones largos hasta 5. Además, dado que |20|25|es la misma longitud |2[05]|que elegí para la opción más legible.
CJ Dennis
Esta expresión coincide con el caso de prueba -0000-08-10 y no coincide ␠2015-08-10␠con los espacios en blanco iniciales y finales, pero dado que ambas fueron decisiones arbitrarias o características opcionales, lo dejaré pasar.
Crissov
Creo que esta solución tiene un error para las fechas dentro de W50.
Crissov
W(?!00)([0-4]\d|51|52)-[1-7]debe ser algo equivalente a W(?!00)([0-4]\d|5[0-2])-[1-7]. Esto agrega un carácter a la longitud. 779
Crissov
9

PCRE: 603 940 947 949 956 bytes

^\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*$

Nota: Algunos pares de paréntesis podrían ser eliminados.

Divisibilidad por 4

Los múltiplos de 4 se repiten en un patrón 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, ...

Esto, o el inverso, podría coincidir con una expresión regular igualmente simple para todos los números de dos dígitos con cero a la izquierda:

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

Podría guardar algunos bytes si hubiera clases de caracteres para dígitos pares e impares (como \oy \e), pero no hay hasta donde yo sé.

Años

Esa expresión sería suficiente para el calendario juliano, pero la detección del año bisiesto gregoriano necesita un seguimiento de casos especiales 00con divisibilidad de siglo por 4:

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

Esto necesitaría algunos cambios para prohibir -0000-…(junto con -00000-…etc.) o para hacer cumplir el signo más para números de año positivos con más de 4 dígitos. Esto último sería bastante simple, pero no es obligatorio:

(?<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}))

Día del año

Las fechas ordinales de tres dígitos son bastante simples, solo tenemos que restringir -366a los años bisiestos (y no permitir -000).

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

Día del mes del año

Los siete meses con 31 días son 01enero, 03marzo, 05mayo, 07julio, 08agosto, 10octubre y 12diciembre. Solo cuatro meses tienen exactamente 30 días, 04abril, 06junio, 09septiembre y 11noviembre. Finalmente, 02febrero tiene 28 días en años comunes y 29 en años bisiestos. En primer lugar, podemos construir una expresión regular para los días siempre válidos 01a través 28y luego añadir los casos especiales.

(?<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)

No debe haber mes ni día 00que no haya sido cubierto por una versión anterior.

Día de la semana del año

Todos los años incluyen 52 semanas.

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

Años largos que incluyen la-W53 repetición en un ciclo de 400 años, por ejemplo, agregue 2000 para el ciclo actual y encuentre el año actual en la tercera entrada:

  • 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.

Cada uno de los cuatro siglos tiene un patrón único. Probablemente no haya mucho espacio para la optimización.

  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

Podemos agrupar por cualquier dígito para descubrir que podemos guardar dos bytes más o menos:

  • Agrupados por 1er dígito.
    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]
  • Agrupados por 2do dígito.
    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

El número de siglo se puede volver a igualar fácilmente mediante una variación de la expresión de divisibilidad.

  • 1er siglo: [02468][048]|[13579][26]
  • 2do siglo: [02468][159]|[13579][37]
  • Siglo III: [02468][26]|[13579][048]
  • 4to siglo: [02468][37]|[13579][159]

Hasta ahora, esto solo funciona durante años positivos, incluido el año cero. Para los años negativos, tenemos que restar los valores de la lista anterior de 400 y hacer el resto nuevamente, porque el patrón no es simétrico.

  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

o

  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]

Poniendolo todo junto

Cualquier año

[+-]?\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])

Adiciones de año bisiesto

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

Adiciones de año bisiesto

+?\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
fuente
Su patrón no está anclado al principio y al final, por lo que coincidirá con fechas válidas dentro de una cadena que de otra manera no sería válida.
CJ Dennis
@CJDennis Eso es cierto, agregaré los dos personajes ahora.
Crissov
También he agregado espacios iniciales y finales opcionales \s*.
Crissov