18. února 2008

Jak nás vypek DateFormat.parse

Tak jsme zase objevili jednu, pro mě překvapivou, skutečnost. Ale vezmu to popořadě. Chceme parsovat datumy s časem reprezentované stringem. Používáme Javu, takže jednoduchý úkol pro DateFormat:


DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date datum = format.parse(retez);

Tento postup má pro mě nelogickou nevýhodu. Tj. pokud si například někdo zplete pořadí měsíce a dne v měsící (u tohoto formátu to není moc pravděpodobné, ale jsou jiné, kde to jde velmi jednoduše), stejně nám to datum vytvoří. Např. parserování textu 2008-24-04 1:30:43 neznamá chybu, ale datum 2009-12-04 1:30:43.
Absolutně tuto vlastnost nechápu, ale třeba se někomu hodí. Proč je ovšem implicitní, to nepochopím nikdy. Naštěstí se dá jednoduše vypnout:

DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.setLenient(false);
Date datum = format.parse(retez);

Takže tentokrát už dostaneme, pro mě očekávanou, ParseException.
A teď se pomalu blížíme k dnešnímu překvapení. Co se stane, pokud na vstupu bude řetěz 2008-04-24 1:30:43 PM? Možnosti jsou tři: ParseException, datum 2008-04-24 13:30:43 nebo datum 2008-04-24 1:30:43? Osobně jsem předpokládal možnost první, přežil bych možnost druhou, ale pravdou je možnost třetí. DateFormat si i v ne-lenient módu odignoruje část vstupního řetězce. To jsem opravdu nečekal a dost mě to zarazilo. Takže pokud někdo chce skutečně správnou funknci parserování data z řetězce, musí použít:

DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.setLenient(false);
ParsePosition p = new ParsePosition(0);
Date datum = format.parse(retez, p);
if (p.getIndex() < retez.length() - 1) {
throw new ParseException(...);
}

Pozor, v případě použití ParsePosition metoda parse nevyhazuje výjimku, musíte si chybu ošetřit sami.
Tak a máme konečně, dle mého názoru, správné parserování řetězce na text. Kód je o poznání složitější než ten, se kterým jsme začínali. Škoda, zřejmě chceme to, co není normální pro ostatní.

PS. Díky Luboši ...

15 komentářů:

Jan Novotný řekl(a)...

Dovolil jsem si prolinkovat tvůj článek z postu: http://blog.novoj.net/2008/02/13/jeste-porad-se-drzite-jdk-kdyz-je-po-ruce-joda-time/

Myslím, že se tam hodí pro dokreslení kvality implementace date API v JDK.

Anonymní řekl(a)...

Musim sa zastat APIcka. V javadocu je to o lenient napisane. To s tym orezanim stringu v ne-lenient mode ale je neprijemne? Tipol by som si ze to nie je zamyslana funkcionalita ale bug :)

Anonymní řekl(a)...

suhlasim s tym, ze api pre pracu s casom v jave nie je prave najlepsie, ale popisovana funkcionalita urcite nie je bug!!! vid javadoc metody parse triedy DateFormat:
Parses text from the beginning of the given string to produce a date. The method may not use the entire text of the given string.
niekedy naozaj nie je na skodu citat tie javadocy...
;)

Anonymní řekl(a)...

Mno, že je něco v souladu s dokumentací ještě neznamená, že to není špatně :-)

Na druhou stranu, když mám formátovací řetězec yyyy-MM-dd HH:mm:ss, fakt mi přijde divný očekávat, že se bude knihovna snažit odhadovat, co znamená to "PM". Takhle kdyby ten formátovací řetězec byl yyyy-MM-dd HH:mm:ss aa, to by byla jiná.

Anonymní řekl(a)...

Ale datum "2008-04-24 1:30:43 PM" není stejný formát jako "2008-04-24 1:30:43" nebo "2008-04-24 13:30:43". Ufak

Jira řekl(a)...

No ono je to ještě zajímavější, tušíte někdo jaký je rozdíl ve formátech yyyy-MM-dd HH:mm:ss a yyyy-M-d HH:mm:ss z hlediska parsování? Odpovím si sám žádný. A jak podle toho API vynutit, že měsíc a den mají mít odicí nulu pokud jsou jednociferné?

A když to v javadocu je, dokážete mi říct od kdy to tam je? Je velmi pravděpodobné, že javadoc k metodě parse jsem četl, pokud někdy, pak tak před 8-10 lety. Bylo to tam tehdy? Je velmi pravděpodobné, že to bylo dodáno na základě stížností programátorů ...

Prostě a jistě todle API je hrůza a běs a nikdo mě nepřesvědčí o opaku

Anonymní řekl(a)...

no ja som to nasiel v javadocu javy 1.4.2 a ta je uz nejaky ten piatok vonku. tak neviem ci je vas hnev opravneny resp. smerovany na spravneho vinnika...
api takehoto rozsahu sa asi neda napisat po prvy krat bez chyby (alebo nejednoznacnosti) a ja nevidim nic zle v tom ze to v urcitej verzii proste doplnili aby sa vyhli nejednoznacnostiam.

Anonymní řekl(a)...

a este 1 vec: to spravanie v pripade parsovania z patternov yyyy-MM-dd HH:mm:ss a yyyy-M-d HH:mm:ss je v javadocu 1.4.2 tiez popisane... ;)

Jira řekl(a)...

Víte ono to vždy není o psaní do manuálu, jsou nějaké obecně poplatné zvyklosti, které je nutné respektovat a pak se nemusí zapisovat do manuálů. Jako vám nikdo do návodu k autu nepíše, že když píchnete musíte vyměnit kolo.

1.4.2, ta vyšla tak někdy 2005, to není nějaky pátek, když vezmeme v potaz jak dlouho Java existuje. Když jsem se koukal na JavaDoc 1.3.1, tak tam není o těchto věcech ani zmínky.

Já jsem přesvědčený, že můj hněv je směrovaný na správného viníka, definitivně to ve mě vzbuzuje dojem, že JODA je cesta a Sun ať se de vycpat. Když udělám procesor a instrukce ADD bude sčítat pokud první operand bude větší než 100 jinak odečítat, napíšu to do dokumentace, pak je vše, dle vás, v pořádku. Nenechte se vysmát!

A o rozdílu d versus dd v podstatě platí totéž. Pomocí standardních prostředků Javy nejsem schopen při parsování zajistit, aby se mi to rozparsovalo pouze pokud je den 2 ciferný. A to opět není v pořádku. nebo je?

Anonymní řekl(a)...

sensible defaults je velmi uzitocna a napomocna vec, ale jdk nie je len taka obycajna kniznica, ktora sa moze len tak bez spatnej kompatibility menit z verzie na verziu. sun uznal ze v 1.3 je to matuce a tak to v 1.4 aspon lepsie popisal. to je jedine co mohol spravit (a jedine co bolo imho spravne). ako by ste to vyriesili vy?

btw. pri d versus dd sa joda sprava rovnako ako time api v jdk

Jira řekl(a)...

Správné? A co to třeba opravit? Představit nový mód, jako máme lenient, mohlo být lenient2 a vystaráno. To by bylo správné.

Se koukněte na ten kód, který musí každý napsat, aby parsoval datumy alespoň spolehlivě, místo volání parse, se musí nastavit formater, vytvořit position, předat do prase, zkontrolovat position, případně vyhodit výjimku (nebo něco jiného) ... a pak teprve pracovat s rozparsovaným objektem. Tomu říkám uživatelsky přítulné.

Anonymní řekl(a)...

tak ci tak, ignorovanim dokumentacie (ci uz jej necitanim alebo nepisanim) mate pred sebou este vela nametov pre vas blog... napriklad:
http://jirablog.blogspot.com/2008/02/jak-nas-vypek-numberformatparse.html

<snip>
ako to ze
NumberFormat.getInstance().parse("1000 malych prasiatok");
nehodi vynimku???

alebo preco
NumberFormat.getInstance().parse("10M malych prasiatok");
nevrati 10000000???
</snip>

nechcem vsak vyzievat tak ze znevazujem vase usilie, rad som si precital tento clanok a urcite si nabuduce pri praci so SimpleDateFormat-om dam velky pozor, ale uznajte, toto nie je len chyba neporiadneho sunu

Mike řekl(a)...

Jenom par poznamek k tomu d versus dd. Unika mi nejak duvod proc by parser striktne hlidal jestli je vstupni den dvouciferny. Proc nutit uzivatele psat o znak navic (0)? A pokud jsou data z jineho stroje tak je to taky jedno. Tato funkcionalita by nicmene mohla byt implementovana, nicemu to nebude na skodu.

Jinak k tomu parsovani v clanku.. Staci se podivat do JDK na zdrojak tridy SimpleDateFormat, je to sice docela slozite (mohlo by to byt napsano lepe a prehledneji), nicmene tam je videt ze parsovani vstupni textu proti formatu probiha jakoby soubezne. Parser narazi na konec retezce formatu a ukonci parsovani, nezajima ho co je jeste dal a ani nemusi ...

Anonymní řekl(a)...

Pro parsovani bych taky volil, aby datum bylo d i dd. Pak to ovsem nemuzu pouzit pro test formatu. Ale ted uz mame jine prostredky.
Pro formatovani jasne dva znaky.

Anonymní řekl(a)...

Priznam se ze take uplne nechapu Vase rozcarovani, pripada mi samozrejme ze pokud pouzijete stejnou formatovaci masku na dva rozdilne datove formaty, pak vznikne problem...