ValiDate ISO 8601 da RX

16

Desafio

Encontre o menor regex que

  1. valida, ou seja, combina, todas as datas possíveis no calendário gregoriano prolético (que também se aplica a todas as datas anteriores à sua primeira adoção em 1582) e
  2. não corresponde a nenhuma data inválida.

Resultado

A saída é, portanto, verdadeira ou falsa.

Entrada

A entrada está em qualquer um dos três formatos de data ISO 8601 expandidos - sem hora.

Os dois primeiros são ±YYYY-MM-DD(ano, mês, dia) e ±YYYY-DDD(ano, dia). Ambos precisam de revestimento especial para o dia do salto. Eles são ingenuamente correspondidos separadamente por esses RXs estendidos:

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

O terceiro formato de entrada é ±YYYY-wWW-D(ano, semana, dia). É o complicado por causa do padrão complexo da semana dos saltos.

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

Uma verificação de validade básica, mas insuficiente, para os três combinados, seria mais ou menos assim:

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

Condições

Um ano bissexto no calendário gregoriano proléptico contém o dia bissexto …-02-29 e, portanto, tem 366 dias, portanto, …-366existe. Isso acontece em qualquer ano cujo número ordinal é divisível por 4, mas não por 100, a menos que também seja divisível por 400. O ano zero existe neste calendário e é um ano bissexto.

Um longo ano no calendário semanal da ISO contém uma 53ª semana, que pode ser denominada " semana bissexta ". Isso acontece em todos os anos em que 1º de janeiro é quinta-feira e, adicionalmente, em todos os anos bissextos em que é quarta-feira. Ocorre geralmente a cada 5 ou 6 anos, em um padrão aparentemente irregular.

Um ano tem pelo menos 4 dígitos. Anos com mais de 10 dígitos não precisam ser suportados, porque isso é próximo o suficiente da idade do universo (cerca de 14 bilhões de anos). O sinal de adição inicial é opcional, embora o padrão real sugira que seja necessário por anos com mais de 4 dígitos.

Datas parciais ou truncadas, ou seja, com precisão menor que o dia, não devem ser aceitas.

As partes da notação de data, por exemplo, o mês, não precisam ser correspondidas por um grupo que possa ser referenciado.

Regras

Isso é código-golfe. A regex mais curta sem o código executado vence. Atualização: Você pode usar recursos como grupos recursivos e equilibrados, mas será multado por um fator de 10, com o qual a contagem de caracteres será multiplicada! Agora, isso é diferente das regras do código hard golf: Regex para divisibilidade por 7 . A resposta anterior vence um empate.

Casos de teste

Testes válidos

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 

O último é opcionalmente válido, ou seja, os espaços à esquerda e à direita nas sequências de entrada podem ser cortados.

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

Datas 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
fonte
2
Esta pergunta não está bem definida porque o idioma regex não está especificado.
orlp
1
@orlp Se não for especificado, a escolha não é limitada. Eu escrevi “regex” ou “RX” de propósito, para que se pudesse usar dialetos que permitam recursão etc. (por exemplo, CFG, não RG).
Crissov
Eu sugiro fortemente que você limite o idioma regex, porque será muito azedo que um concorrente trabalhe por horas em uma solução apenas para ser trivialmente derrotado por um idioma que é fundamentalmente mais poderoso. Se você limitar o idioma à definição real de expressões regulares do CS (como o DFA), o problema se tornará uma resposta interessante de otimização.
orlp
Validar datas ISO-8601 usando expressões regulares é algo que realmente precisei fazer no trabalho. Mas concordo com o orlp, acho que é necessário um idioma aqui.
Alex A.
1
O Regex herda de Method no Perl 6 e, portanto, é uma forma de código executável.
Brad Gilbert b2gills

Respostas:

4

PCRE (também 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])$/

Incluí os delimitadores na contagem de bytes para mostrar que ele não depende de nenhum sinalizador.

Ele não coincidir com datas válidas dentro de outras cadeias, como 1234-56-89 2016-02-29 9876-54-32. A regex é mais curta, não verificando um máximo de 10 dígitos para o ano.

Ampliado com comentários:

/^  # 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
fonte
Ainda não verifiquei tudo, mas parece que você ganha mais bytes por (?!…)expressões em comparação à minha solução.
Crissov
1
@Crissov As (?!…)expressões salvam apenas alguns bytes cada. Reduzi muitos bytes combinando três dos padrões de ano positivo / negativo de semana do ano / dia da semana em um cada. Os últimos não correspondem um ao outro. Então, eu tenho 8 sub-padrões longos até 5. Além disso, uma vez que |20|25|é o mesmo comprimento que |2[05]|eu fui para a opção mais legível.
CJ Dennis
Essa expressão corresponde ao caso de teste -0000-08-10 e não corresponde ␠2015-08-10␠ao espaço em branco à esquerda e à direita, mas como ambas foram decisões arbitrárias ou recursos opcionais, deixarei isso para lá.
Crissov
Eu acho que esta solução tem um bug para datas dentro do W50.
Crissov
W(?!00)([0-4]\d|51|52)-[1-7]deve ser algo equivalente a W(?!00)([0-4]\d|5[0-2])-[1-7]. Isso adiciona um caractere ao comprimento. 779
Crissov 30/10/19
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: Alguns pares de parênteses podem ser descartados.

Divisibilidade por 4

Os múltiplos de 4 se repetem em um padrão simples:

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

Isso, ou o inverso, poderia ser correspondido por uma expressão regular igualmente simples para todos os números de dois dígitos com zero à esquerda:

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

Ele poderia economizar alguns bytes se houvesse classes de caracteres para dígitos pares e ímpares (como \oe \e), mas não existem até onde eu saiba.

Anos

Essa expressão seria suficiente para o calendário juliano, mas a detecção gregoriana do ano bissexto precisa seguir um caso especial, 00com divisibilidade do século em 4:

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

Isso precisaria de algumas alterações para ilegalizar -0000-…(junto com -00000-…etc.) ou para aplicar o sinal de mais para números positivos de ano com mais de 4 dígitos. O último seria bastante simples, mas não é necessário:

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

Dia do ano

As datas ordinais de três dígitos são bastante simples, basta restringir -366a anos bissextos (e não 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)

Dia do mês do ano

Os sete meses com 31 dias são 01janeiro, 03março, 05maio, 07julho, 08agosto, 10outubro e 12dezembro. Apenas quatro meses têm exatamente 30 dias, 04abril, 06junho, 09setembro e 11novembro. Finalmente, 02fevereiro tem 28 dias em anos comuns e 29 em anos bissextos. Podemos primeiro construir uma expressão regular para os dias sempre válidos 01através de 28e, em seguida, adicionar casos especiais.

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

Não deve ser mês nem dia 00que não foi coberto por uma versão anterior.

Dia da semana do ano

Todos os anos incluem 52 semanas

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

Anos longos que incluem-W53 repetição em um ciclo de 400 anos, por exemplo, adicione 2000 para o ciclo atual e encontre o ano atual na terceira 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 um dos quatro séculos tem um padrão único. Provavelmente não há muito espaço para otimização.

  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 um dígito para descobrir que podemos salvar dois bytes ou mais:

  • Agrupados pelo 1º 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 pelo segundo 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

O número do século é facilmente correspondido novamente por uma variação da expressão da divisibilidade.

  • Século I: [02468][048]|[13579][26]
  • Século II: [02468][159]|[13579][37]
  • Século III: [02468][26]|[13579][048]
  • Século IV: [02468][37]|[13579][159]

Até agora, isso funciona apenas para anos positivos, incluindo o ano zero. Para anos negativos, temos que subtrair os valores da lista acima de 400 e fazer o resto novamente, porque o padrão não é 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

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]

Juntando tudo

Qualquer ano

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

Adições de ano bissexto

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

Adições de ano de semana bissexta

+?\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
fonte
Seu padrão não está ancorado no início e no final, portanto, ele corresponderá a datas válidas dentro de uma sequência inválida.
CJ Dennis
@CJDennis Isso é verdade, vou adicionar os dois caracteres agora.
Crissov
Também adicionei espaços iniciais e finais opcionais \s*.
Crissov