Kontrola data

V PHP existuje pro kontrolu data funkce checkdate přijímající den, měsíc a rok ve třech parametrech. Pokud uživatel datum zadává jako řetězec, je možné ho na komponenty snadno rozdělit např. regulárním výrazem nebo funkcí explode. Kdybychom tuto funkci k dispozici neměli, mohli bychom platnost data zkontrolovat podle pravidel našeho kalendáře:

  1. Únor má 28-29 dní, měsíce 4, 6, 9 a 11 mají 30 dní, ostatní 31 dní.
  2. Každý rok dělitelný čtyřmi má únor 29 dní, jinak 28.
  3. V každé celé století nedělitelné čtyřmi má únor ale 28 dní.

Tato pravidla by se dala snadno zalgoritmizovat, já jsem je pojal jako cvičení z regulárních výrazů. Datum se přijímá ve formátu d.m.rrrr.

  1. Základ pro kontrolu rozsahů je jednoduchý – den může být 1-31, měsíc 1-12 a rok povolíme jakýkoliv čtyřciferný:
    ^([1-9]|[12][0-9]|3[01])\.([1-9]|1[012])\.[0-9]{4}$
  2. Pro kontrolu počtu dní v měsíci použijeme aserce – za třicítkou nesmí následovat únor, za jednatřicítkou smí být pouze odpovídající měsíce:
    ^([1-9]|[12][0-9]|30(?=\.[^2])|31(?=\.([13578][02]?\.)))\.([1-9]|1[012])\.[0-9]{4}$
  3. Roky dělitelné čtyřmi vyřešíme další asercí – pokud je za devětadvacítkou únor, tak musí být rok dělitelný čtyřmi (číslo je dělitelné čtyřmi, pokud je předposlední číslice sudá a poslední 0, 4, 8 nebo pokud je lichá a poslední 2, 6):
    ^([1-9]|19|[12][0-8]|29(?=\.([^2]|2\.([0-9]{2}([02468][048]|[13579][26]))))|30(?=\.[^2])|31(?=\.([13578][02]?\.)))\.([1-9]|1[012])\.[0-9]{4}$
  4. A konečně kontrola století zajistí finální řešení – v celé století má únor 29 dní, jen pokud je dělitelné čtyřmi:
    ^([1-9]|19|[12][0-8]|29(?=\.([^2]|2\.(([02468][048]|[13579][26])00|[0-9]{2}(0[48]|[2468][048]|[13579][26]))))|30(?=\.[^2])|31(?=\.([13578][02]?\.)))\.([1-9]|1[012])\.[0-9]{4}$

Výsledkem je tato funkce:

<?php
/** Kontrola data
* @param string datum ve formátu d.m.rrrr
* @return bool platnost data
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function platne_datum($datum) {
    return preg_match('~^([1-9]|19|[12][0-8]|29(?=\\.([^2]|2\\.(([02468][048]|[13579][26])00|[0-9]{2}(0[48]|[2468][048]|[13579][26]))))|30(?=\\.[^2])|31(?=\\.([13578][02]?\\.)))\\.([1-9]|1[012])\\.[0-9]{4}$~D', $datum);
}
?>

Ne že bych tuto funkci někomu doporučoval používat, algoritmizace pomocí regulárních výrazů není právě přehledná.

Jakub Vrána, Řešení problému, 7.4.2006, on-line

Diskuse

Ivan:

Ak potrebujeme zistit to ci datum je alebo nie je platny, tak ju doporucujte. Ak ale potrebujeme povedat preco neni platny, budeme musiet pouzivat ine postupy. V kazdom pripade je to znova fajn prispevok do zbierky :)
7.4.2006 10:22:38

Ivan:

Este by som doplnil jeden uzitocny odkaz: http://www.regexlib.com
7.4.2006 10:24:19

ikona dgx:

Hezká kuriozita :-)

Ještě by stálo za to ošetřit možný výskyt bílého místa za tečkou, včetně znaku pevné mezery.
7.4.2006 14:37:47

ikona Jakub Vrána:

To bych to z toho raději nejdřív vyházel - aserce by se ještě víc zesložitily.
7.4.2006 14:45:14

Michal:

Bzzzz .... internal brain overflow.

Tohle bez komentare placnout nekam do programu tak mam jistotu ze uz me zamestnavatel nikdy nemuze vyrazit ;-)
7.4.2006 15:14:49

ikona MiSHAK:

No tak tohle je lepší :-) Právě jsem řešil přestupný rok v js se hodí (uvedu i copyright :-)
7.4.2006 15:19:48

Borek:

Wow :)
7.4.2006 16:24:39

bs:

Jo jo, holt když někdo umí, tak umí! Smekám.
7.4.2006 21:51:53

mára:

Jen bych rád upřesnil, jak je to s těmi celými stoletími. Celé století musí být dělitelné ne 4 (čtyřmi), ale 400 (čtyřmisty), aby měl únor 29 dní. Celé století je totiž vždy dělitelné 4, ale to samozřejmě nikterak nesnižuje kvalitu příspěvku.
13.4.2006 17:02:10

ikona Jakub Vrána:

Slovo "století" používám ve významu "žijeme ve 21. století". Pojmem "celé století" mám na mysli rok s dvěma nulami na konci.
13.4.2006 17:09:34

mára:

No, já ani nic jiného neměl na mysli. Například rok 2000 byl přestupný, ale rok 2100 nebude přestupný, protože není dělitelný 400.
14.4.2006 11:17:29

mára:

Když jsem to dopsal a šel si uvařit kafe, tak mi to "docvaklo", jaks' to myslel. Ano, pokud se můj příklad vykrátí 100, tak z toho vyjde ten tvůj ;)
14.4.2006 11:27:24

Cohen:

Zminena funkce me natolik uchvatila, ze ji od doby, co jsem na ni narazil, pouzivam ve vsech svych programech a skriptech. Takze timto autorovi vrele dekuji.

Narazil jsem ale na takovy prekladatelsky orisek, se kterym si nevim rady. Potreboval bych overit, jestli datum, ktere uzivatel zadava, neni pondeli - a to pondeli velikonocni. Lamu si s tim hlavu uz nekolik hodin a vhodny regularni vyraz ne a ne vymyslet. Kdybyste mi mohl poradit, nebo me aspon popostrcit spravnym smerem, byl bych neskonale vdecny.
17.4.2006 05:40:03

ptmhd:

na to staci astronomicky kalendar
jarna rovnodennost -> jar -> prvy cyklus mesacnych fazi v jari -> velka noc sa pocita podla neho. preto ten mesacny rozkmit medzirocny
17.4.2006 08:22:43

DarkLogic:

vyborny kod a presne ten jsem hledal.
17.8.2007 04:07:00

lix:

dík za ten reg vyraz;)
posilam upgrade (datum 31.03.2009 nevyhovovalo):

^(0?[1-9]|19|[12][0-8]|29(?=\.([^2]|0?2\.(([02468][048]|[13579][26])00|[0-9]{2}(0[48]|[2468][048]|[13579][26]))))|30(?=\.[^2])|31(?=\.(0?[13578]|[13578][02]?\.)))\.(0?[1-9]|1[012])\.[0-9]{4}$
5.3.2009 08:18:59

ikona Jakub Vrána:

Vždyť je jasně uvedeno, že regulární výraz kontroluje datum ve formátu d.m.rrrr.
5.3.2009 10:03:21

MK:

30.02.2009 tvemu vyrazu vyhovuje a nemelo by
4.5.2009 22:45:31

Dominik:

teď by to mělo sežrat i datum dd.mm.rrrr

preg_match('~^(0[1-9]|[1-9]|19|[12][0-8]|29(?=\\.([^2]|2\\.(([02468][048]|[13579][26])00|[0-9]{2}(0[48]|[2468][048]|[13579][26]))))|30(?=\\.[^2])|31(?=\\.([13578][02]?\\.)))\\.(0[1-9]|[1-9]|1[012])\\.[0-9]{4}$~D', $datum)
19.6.2010 22:50:42

Ondro:

Tam zas nevyhovuje napr: 29.2.2012 a pod.
23.11.2010 21:53:28

Tom:

No to je mazec...!

Zatím jsem se prokousal k myšlence 3. a tomu, jak je řešená dělitenost roku čtyřmi.

Ale už cestou jsem ztratil jednu věc:
Kde se vzala za sebou ty dvě zpětná lomítka v první polovině výrazu 2., za první asercí? :o)

^([1-9]|[12][0-9]|30(?=\\.[^2])|31(?=\.([13578][02]?\.)))\.([1-9]|1[012])\.[0-9]{4}$

P.S. tohle je moje druhé setkání s regexp, tak proto ten blbý dotaz.
20.12.2011 04:22:11

Tom:

Aha, odpovím si sám... je to chybka, která tam pronikla z výsledného stringu použitého jako argument php funkce.

Zpětné lomítko slouží jako escape znak pro zápis skutečného znaku tečka, a v php se navíc toto zpětné lomítko escapuje dalším zpětným lomítkem, aby to bylo opravdu zpětné lomítko...

Autor zřejmě kopítroval ze svého oblíbeného výsledného php stringu, ale při "přepisu" do pouhé regexp formy mu v té změti znaků něco uteklo. Vůbec se autorovi nedivím. :)))
20.12.2011 04:34:30

ikona Jakub Vrána:

Díky za upozornění, opravil jsem to.
20.12.2011 08:34:59

Tom:

Díky za opravu. :)

Dívím se, že nikdo přede mnou už na tohle téma neprudil.

Přiznám se, že jsem na to koukal jako vrána (to není impertinence! :))) ) ve čtyři hodiny ráno a byl zmaten. Ale nakonec jsem to do půl páté pobral všechno, i včetně těch přestupně-nepřestupných roků. Pěkná práce.
20.12.2011 23:02:34

Laborator:

Nevyhovuje: 30.03.2011, prosim s dovolenim abyste opravil.
21.2.2012 08:33:45

Laborator:

Nevyhovuje: 29.02.2012, prosim s dovolenim abyste opravil.
21.2.2012 08:34:42

Laborator:

Nefunguje 01.01.2011 - možná v regulárním výrazu něco přidat aby se povolily: 0 + [0-9]. 0 + [0-9]. Uprimne prosim jestli byste pro me udelal opravu, diky moc.
21.2.2012 08:37:38

Filip Procházka (@HosipLan):

Použij raději třídu Datetime
21.2.2012 09:41:49

ikona Jakub Vrána:

V článku je jasně uvedeno, že se kontroluje pouze formát bez úvodních nul.
22.2.2012 06:40:40
avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.