ValiDate ISO 8601 oleh RX

16

Tantangan

Temukan regex terpendek itu

  1. memvalidasi, yaitu kecocokan, setiap tanggal yang memungkinkan dalam Proleptik Gregorian (yang juga berlaku untuk semua tanggal sebelum adopsi pertamanya pada 1582) dan
  2. tidak cocok dengan tanggal yang tidak valid.

Keluaran

Karena itu, output itu benar atau salah.

Memasukkan

Input dalam salah satu dari 3 ISO 8601 yang diperluas format tanggal - tidak ada waktu.

Dua yang pertama adalah ±YYYY-MM-DD(tahun, bulan, hari) dan ±YYYY-DDD(tahun, hari). Keduanya membutuhkan casing khusus untuk hari kabisat. Mereka secara naif dicocokkan secara terpisah oleh RX yang diperluas ini:

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

Format input ketiga adalah ±YYYY-wWW-D(tahun, minggu, hari). Ini yang rumit karena pola minggu kabisat yang kompleks.

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

Pemeriksaan validitas dasar, tetapi tidak cukup untuk ketiga kombinasi akan terlihat seperti ini:

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

Kondisi

Sebuah tahun kabisat dalam kalender Gregorian proleptic berisi hari lompatan …-02-29 dan dengan demikian itu adalah 366 hari panjang, maka …-366ada. Ini terjadi di setiap tahun yang nomor urutnya dapat dibagi dengan 4, tetapi tidak oleh 100 kecuali itu juga dapat dibagi dengan 400. Tahun nol ada dalam kalender ini dan itu adalah tahun kabisat.

Setahun yang panjang dalam kalender minggu ISO berisi minggu ke-53, yang bisa disebut " minggu kabisat ". Ini terjadi di semua tahun di mana 1 Januari adalah hari Kamis dan juga di semua tahun kabisat di mana itu hari Rabu. Ternyata terjadi setiap 5 atau 6 tahun biasanya, dalam pola yang tampaknya tidak teratur.

Setahun memiliki setidaknya 4 digit. Tahun dengan lebih dari 10 digit tidak harus didukung, karena itu cukup dekat dengan usia alam semesta (sekitar 14 miliar tahun). Tanda tambah utama adalah opsional, meskipun standar aktual menunjukkan bahwa tanda tersebut harus diisi selama bertahun-tahun dengan lebih dari 4 digit.

Tanggal parsial atau terpotong, yaitu dengan kurang dari ketepatan hari, tidak boleh diterima.

Bagian-bagian dari notasi tanggal, misalnya bulan, tidak harus dicocokkan dengan grup yang dapat direferensikan.

Aturan

Ini adalah kode-golf. Regex terpendek tanpa kode yang dieksekusi akan menang. Pembaruan: Anda dapat menggunakan fitur seperti rekursi dan grup seimbang, tetapi akan didenda oleh faktor 10, yang jumlah karakternya kemudian dikalikan dengan! Ini sekarang berbeda dari aturan dalam Golf kode keras: Regex untuk dapat dibagi dengan 7 . Jawaban sebelumnya memenangkan dasi.

Uji kasus

Tes yang valid

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 

Yang terakhir adalah opsional opsional, yaitu ruang memimpin dan tertinggal di string input dapat dipangkas.

Format tidak valid

-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

Tanggal tidak valid

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
sumber
2
Pertanyaan ini tidak terdefinisi dengan baik karena bahasa regex tidak ditentukan.
orlp
1
@ orlp Jika tidak ditentukan, pilihannya tidak terbatas. Saya menulis "regex" atau "RX" dengan sengaja, sehingga orang dapat menggunakan dialek yang memungkinkan rekursi dll. (Yaitu CFG, bukan RG).
Crissov
Saya sangat menyarankan Anda untuk membatasi bahasa regex, karena akan sangat buruk bagi kontestan untuk bekerja berjam-jam pada solusi hanya untuk dipukuli oleh bahasa yang secara fundamental lebih kuat. Jika Anda membatasi bahasa ke definisi CS aktual dari ekspresi reguler (seperti DFA), maka masalahnya menjadi jawaban optimisasi yang menarik.
orlp
Memvalidasi tanggal ISO-8601 menggunakan ekspresi reguler adalah sesuatu yang sebenarnya harus saya lakukan untuk bekerja. Tapi setuju dengan orlp, saya pikir bahasa diperlukan di sini.
Alex A.
1
Regex mewarisi dari Metode dalam Perl 6 jadi itu sendiri merupakan bentuk kode yang dapat dieksekusi.
Brad Gilbert b2gills

Jawaban:

4

PCRE (juga Perl), 778 byte

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

Saya telah menyertakan pembatas dalam hitungan byte untuk menunjukkan bahwa ia tidak bergantung pada flag.

Itu tidak cocok dengan tanggal yang valid dalam string lain, seperti 1234-56-89 2016-02-29 9876-54-32. Regex lebih pendek dengan tidak memeriksa maksimal 10 digit untuk tahun ini.

Diperpanjang dengan komentar:

/^  # 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
sumber
Saya belum memeriksa semuanya, tetapi sepertinya Anda mendapatkan byte terbanyak dengan (?!…)ekspresi dibandingkan dengan solusi saya.
Crissov
1
@ Crissov (?!…)Ekspresi masing-masing hanya menyimpan beberapa byte. Saya mengurangi banyak byte dengan menggabungkan tiga dari pola tahun positif / negatif minggu-tahun / hari-minggu menjadi masing-masing satu. Yang terakhir tidak saling berhubungan. Jadi saya mendapat 8 sub-pola panjang menjadi 5. Juga, karena |20|25|panjangnya sama dengan |2[05]|saya untuk opsi yang lebih mudah dibaca.
CJ Dennis
Ungkapan ini cocok dengan test case -0000-08-10 dan tidak cocok ␠2015-08-10␠dengan spasi putih terkemuka dan tertinggal, tetapi karena keduanya adalah keputusan sewenang-wenang atau fitur opsional saya akan membiarkannya meluncur.
Crissov
Saya pikir solusi ini memiliki bug untuk tanggal dalam W50.
Crissov
W(?!00)([0-4]\d|51|52)-[1-7]harus setara dengan W(?!00)([0-4]\d|5[0-2])-[1-7]. Ini menambahkan satu karakter ke panjangnya. 779
Crissov
9

PCRE: 603 940 947 949 956 byte

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

Catatan: Beberapa pasang tanda kurung mungkin bisa dijatuhkan.

Dapat dibagi oleh 4

Kelipatan 4 berulang dalam pola sederhana:

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

Ini, atau kebalikannya, bisa dicocokkan dengan ekspresi reguler sederhana yang sama untuk semua angka dua digit dengan nol depan:

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

Ini bisa menghemat beberapa byte jika ada kelas karakter untuk digit ganjil dan genap (seperti \odan \e), tetapi tidak ada sejauh yang saya ketahui.

Bertahun-tahun

Ungkapan itu akan cukup untuk kalender Julian, tetapi deteksi tahun kabisat Gregorian perlu kasus khusus membuntuti 00dengan pembagian abad dengan 4:

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

Ini akan membutuhkan beberapa perubahan untuk melarang -0000-…(bersama dengan -00000-…dll.) Atau untuk menegakkan tanda tambah untuk angka tahun positif dengan lebih dari 4 digit. Yang terakhir akan agak sederhana, tetapi tidak diperlukan:

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

Hari dalam setahun

Tiga digit tanggal ordinal agak sederhana, kita hanya perlu membatasi -366tahun kabisat (dan melarang -000).

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

Hari bulan dalam setahun

Tujuh bulan dengan 31 hari adalah 01Januari, 03Maret, 05Mei, 07Juli, 08Agustus, 10Oktober dan 12Desember. Hanya empat bulan, tepatnya 30 hari, 04April, 06Juni, 09September , dan 11November. Akhirnya, 02Februari memiliki 28 hari di tahun-tahun umum dan 29 di tahun kabisat. Pertama kita dapat membangun sebuah ekspresi reguler untuk hari-hari selalu berlaku 01melalui 28dan kemudian menambahkan kasus-kasus khusus.

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

Baik bulan maupun hari tidak boleh 00yang tidak dicakup oleh versi sebelumnya.

Hari dalam seminggu tahun

Semua tahun termasuk 52 minggu

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

Tahun-tahun yang panjang termasuk-W53 pengulangan dalam siklus 400 tahun, mis. Tambahkan 2000 untuk siklus saat ini dan temukan tahun ini di entri ketiga:

  • 004, 009, 015, 020, 026, 032, 037, 043, 048, 054, 060, 065, 071, 076, 082, 088, 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, 387, 392, 398.

Masing-masing dari empat abad memiliki pola yang unik. Mungkin tidak ada banyak ruang untuk optimasi.

  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

Kami dapat mengelompokkan berdasarkan angka untuk mengetahui bahwa kami dapat menghemat dua byte atau lebih:

  • Dikelompokkan oleh digit pertama.
    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]
  • Dikelompokkan oleh digit ke-2.
    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

Angka abad mudah dicocokkan lagi dengan variasi ekspresi keterbagian.

  • Abad ke-1: [02468][048]|[13579][26]
  • Abad ke-2: [02468][159]|[13579][37]
  • Abad ke-3: [02468][26]|[13579][048]
  • Abad ke-4: [02468][37]|[13579][159]

Sejauh ini, ini hanya bekerja untuk tahun-tahun positif, termasuk tahun nol. Untuk tahun negatif, kita harus mengurangi nilai dari daftar di atas dari 400 dan melakukan sisanya lagi, karena polanya tidak simetris.

  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

atau

  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]

Menyatukan semuanya

Setiap tahun

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

Penambahan tahun kabisat

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

Penambahan tahun kabisat

+?\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
sumber
Pola Anda tidak berlabuh di awal dan akhir, sehingga akan cocok dengan tanggal yang valid di dalam string yang tidak valid.
CJ Dennis
@ CJDennis Itu benar, saya akan menambahkan dua karakter sekarang.
Crissov
Saya juga menambahkan spasi tambahan awal dan akhir \s*.
Crissov