July 13th, 2006 by depesz | | 1 comment »
Did it help? If yes - maybe you can help me?

tak się złożyło, że brałem udział w projekcie którego częścią miało być przygotowanie formatu wymiany danych opartego na xml'u, a także przygotowanie do tego schemy walidującej.

trywiał.

zrobiłem xml'a, wszystko fajnie. w kilku miejscach trzeba było podać datę i godzinę.

klient chciał by było human-readable, więc wyspecifikowałem coś takiego:

format jasny i prosty. okazało się, że nie. xml schema aby móc walidować datę musi mieć ją podaną tak: 2006-07-13T20:47:00 (a przynajmniej tak mówi nasz firmowy magik od schemy).

jak format jest inny to można walidować regexp'em.

oops. myślę sobie.

walidacja miała być mocna więc te daty i godziny trzeba by jakoś dobrze sprawdzać.

formatu zmienić nie mogę bo zatwierdzony przez komisję w skład której wchodzili ludzie z 4 krajów (taki trochę międzynarodowy projekt).

no to kombinujemy z regexpem.

godzina była relatywnie prosta:

  • sekundy i minuty są z zakresu 00 do 59. więc najprostszy regexp na nie: [0-5]\d (używam tu notacji perlowej bo w perlu tego regexpa testowaliśmy, ale finalnie został przerobiony na xml-schemowego regexpa przez zamianę \d na [0-9]).
  • godziny – no tu trudniej od 00 do 19 to nie problem. potem jeszcze 20-23. da się: ( [01]\d | 2[0-3] ) (używam notacji regexpów perla z rozszerzeniem /x dzięki czemu jest to trochę czytelniejsze.
  • cały regexp na godzinę: ( [01]\d | 2[0-3] ) : [0-5]\d : [0-5]\d . tak. wiem, że mogłem na końcu dać: ( : [0-5]\d){2}, ale wiedziałem, że będę to potem konwertował do regexpów ze schemy, więc wolałem nie przeginać z “cudami".

data. no to jest trudniejsze. haczyk tkwi w latach przestępnych. w dodatku na przestępność są 3 reguły: %4, %100 i %400.

nic to. twardym trzeba być, nie miętkim.

stwierdziłem, że zrobię tego regexpa bazując na ciągu wielu alternatyw:

  • dla dowolnego roku, część miesięcy (01, 03, 05, 07, 08, 10, 12) ma 31 dni, więc można użyć: \d\d\d\d – (0[13578]|1[02]) – ([012]\d|3[01])
  • dla dowolnego roku, część miesięcy (04, 05, 09, 11) ma 30 dni, więc używam: \d\d\d\d – (0[469]|11) – ([012]\d|30)
  • w każdym roku luty ma co najmniej 28 dni: \d\d\d\d – 02 – ([01]\d|2[0-8])

no i teraz zaczynają się schody.

zacznijmy od prostszego – przestępne są te lata które są podzielne przez 4, ale nie przez 100.

ponieważ 100 jest podzielne przez 4, to znaczy, że:

  • pierwsze dwie cyfry roku są nieistotne
  • ostatnie dwie – nie mogą być 00 (bo wtedy rok jest podzielny przez 100)
  • ostatnie dwie muszą być podzielne przez 4

no to już mamy pewne zręby. w jaki sposób regexpem poznać czy liczba jest podzielna przez 4?

zobaczmy jakie to liczby. $ perl -e ‘$_%4||printf “%02u, “, $_ for 0..99'

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

co tu widać? niewiele. ale może jakbym tak zapisał to w ten sposób:

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

WOW! mam regułkę. jeśli pierwsza cyfra jest parzysta, to druga musi być jedną z 0, 4, 8. a jeśli nieparzysta, to druga musi być równa 2 lub 6.

zapis regexpowy: ( [02468][048] | [13579][26] )

no tak. ale na to trzeba nałożyć warunek, że te cyfry to nie może być 00. to powoduje, że jeśli pierwszą cyfrą (z dwóch) jest 0, to druga może być tylko 4 lub 8. tak więc regexp na liczbę 2 cyfrową podzielną przez 4, różną od 00 jest:

( 0[48] | [2468][048] | [13579][26]

tak więc pierwszy warunek na rok przestępny – podzielny przez 4, ale niepodzielny przez 100 wygląda tak:

\d\d ( 0[48] | [2468][048] | [13579][26] ) – 02 – [012]\d

ok. pozostało wziąć warunek na lata podzielne przez 100, ale podzielne też przez 400. i to sie okazuje trywialne. po tak jak przed chwilą liczyłem lata podzielne przez 4, tak lata podzielne przez 400 liczy się tak samo:

( 0[48] | [2468][048] | [13579][26] ) 00 – 02 – [012]\d
finalny regexp walidujący datę:

(
\d\d\d\d – (0[13578]|1[02]) – ([012]\d|3[01])
|
\d\d\d\d – (0[469]|11) – ([012]\d|30)
|
\d\d\d\d – 02 – ([01]\d|2[0-8])
|
\d\d ( 0[48] | [2468][048] | [13579][26] ) – 02 – [012]\d
|
( 0[48] | [2468][048] | [13579][26] ) 00 – 02 – [012]\d
)
dodać do tego godzinę i już 🙂 super.

najzabawniejsze w tym, że przeprowadzony przeze mnie test pokazał, że ten regexp jest szybszy niż walidowanie bazujące na porównywaniu liczb!

zrobiłem testowy program. odpaliłem i dostałem taki wynik:

  • function  5348/s
  • regexp   14045/s

dziwne.

oczywiście nie polecam walidowania daty regexpami. czytelniejsze są normalne funkcje. a prędkość 5300/s to i tak aż za dużo.

  1. One comment

  2. # Koziołek
    Nov 24, 2006

    yyy… niezłe, ale odpaliłem Twój program i
    Rate function regexp
    function 3900/s — -59%
    regexp 9452/s 142% —

    Czyli funkcja szybsza. Chyba dużo zależy też od szybkości samego kompa. U mnie ruszyłem to na Cpu 200MHz + 1GBRam… i wynik jaki jest każdy widzi 🙁

Leave a comment