Kontrola data

Školení, která pořádám

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, http://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, diskuse: 29 (nové: 0)

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 :)

Ivan:

Este by som doplnil jeden uzitocny odkaz: http://www.regexlib.com

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.

ikona Jakub Vrána OpenID:

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

Michal:

Bzzzz .... internal brain overflow.

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

ikona MiSHAK:

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

Borek:

Wow :)

bs:

Jo jo, holt když někdo umí, tak umí! Smekám.

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.

ikona Jakub Vrána OpenID:

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.

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.

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 ;)

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.

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

DarkLogic:

vyborny kod a presne ten jsem hledal.

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}$

ikona Jakub Vrána OpenID:

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

MK:

30.02.2009 tvemu vyrazu vyhovuje a nemelo by

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)

Ondro:

Tam zas nevyhovuje napr: 29.2.2012 a pod.

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.

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. :)))

ikona Jakub Vrána OpenID:

Díky za upozornění, opravil jsem to.

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.

Laborator:

Nevyhovuje: 30.03.2011, prosim s dovolenim abyste opravil.

Laborator:

Nevyhovuje: 29.02.2012, prosim s dovolenim abyste opravil.

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.

Filip Procházka (@HosipLan):

Použij raději třídu Datetime

ikona Jakub Vrána OpenID:

V článku je jasně uvedeno, že se kontroluje pouze formát bez úvodních nul.

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2016 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.