http://www.perlmonks.org?node_id=614932

Trihedralguy has asked for the wisdom of the Perl Monks concerning the following question:

While I have been trying to figure out how to handle date validation, I have come to decide that regex isnt the answer and I just need a simple date validation script. I could use a module, but all the date validation modules that people have suggested required a small list of extra modules to install. Me being in-experienced with installing modules on a server I don't want to screw up doesn't help either. However, someone did suggest to me that "Time::Local" was a good easy "core" way to validate dates, but I haven't been able to find an example of how to go about doing that using Time::Local. If anyone has any past experience to making a quick validation script, that would be very helpful.

Basically all the script needs to do is accept input from variable $userdate and validate whether or not its valid. If its valid, great, do nothing, if its not valid set an error variable.

I haven't gotten anything to work using Time::Local so I'm sorry I can't show any good working code that could be edited.

Replies are listed 'Best First'.
Re: Simple Date Validation
by Corion (Patriarch) on May 11, 2007 at 15:36 UTC

    Looking at the Time::Local documentation, I see the following:

    The timelocal() and timegm() functions perform range checking on the input $sec, $min, $hour, $mday, and $mon values by default.

    So, I would just exploit that and hand the parts of the date to, say, timelocal, and see if it accepts these values as valid or not:

    my ($sec,$min,$hour) = qw( 0 0 12 ); my ($mday, $mon, $year) = qw(11 5 2007); $mon--; # because that's how unix/C treat the month value eval { my $dummy = timelocal($sec,$min,$hour,$mday,$mon,$year); }; if (my $err = $@) { print "This is an invalid date."; } else { print "Yay"; };

    Splitting up $userdate into the parts making up the day, month and year is left as an exercise to the reader.

      How do I go about handing a date that is in this format: MM/DD/YYYY into that type of validation?
        $userdate = "12/31/2005"; my ($mon, $mday, $year) = split /\//, $userdate;

        --shmem

        _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                      /\_¯/(q    /
        ----------------------------  \__(m.====·.(_("always off the crowd"))."·
        ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
        Just split it.

        Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

      unfortunately timelocal does not validate dates properly...

      # perl -e 'use Time::Local; print timelocal(0,0,0,31,02,2007);'
      1175295600
      #

        Works as designed and documented. Maybe you didn't mean "02" as the month index but "01" (for February)?

        Q:\>perl -MTime::Local -e "print timelocal(0,0,0,31,01,2007)" Day '31' out of range 1..28 at -e line 1
Re: Simple Date Validation
by shmem (Chancellor) on May 11, 2007 at 16:07 UTC

    What does validate mean in this context? Do you want to check whether the submitted date conforms to some format? Or do you want to check whether the submitted date is in some valid range?

    It all depends on the format used in $userdate. Is it YYYY-MM-DD ? DD.MM.YY ? MM-DD-YYYY ? Jan 11 05 ? Does it contain time?

    Without some restrition on the accepted format, there's no way to tell whether 01.02.03 is valid, and which part of that string means year, month and day respectively.

    If you have some pre-defined format, a regular expression would suffice to check whether $userdate complies. Then you know which fields mean what. To convert e.g. Jan 12 2007 to a unix timestamp with Time::Local and validate there's no day or month overflow (e.g. Apr 31 2007):

    use Time::Local; my $userdate = "Jan 12 2007"; my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov + Dec); @monthname {@months} = 0 .. 11; my ($m, $day, $year) = split /\s+/, $userdate; defined $monthname {$m} or die "no valid month\n"; my $month = $monthname {$m}; for ($day, $year) { /^\d+$/ or die "$_ invalid\n" } my $timestamp = eval { local $SIG{__DIE__}; timelocal (0, 0, 0, $day, +$month, $year) }; if ($@) { warn "invalid date: $@\n" } else { my @ltime = localtime ($timestamp); printf "date: %02d %s %04d\n", $ltime [3], $months [$ltime[4]], $l +time [5] + 1900; } print "done\n"; __END__

    That properly warns of e.g. "Feb 31 2007" being an invalid date, but reports "29 Feb 2004" as being correct.

    --shmem

    _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                  /\_¯/(q    /
    ----------------------------  \__(m.====·.(_("always off the crowd"))."·
    ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
Re: Simple Date Validation
by johngg (Canon) on May 11, 2007 at 15:44 UTC
    I have the following which I converted from C a long time ago when I was very new to Perl. It was set up for the UK so the Julian/Gregorian change is in 1752.

    # ------ sub isLeap # ------ { my $year = shift or croak "isleap(): no year supplied\n"; croak "isLeap(): year not numeric\n" unless $year =~ /^\d+$/; return 0 if $year % 4; return 1 if $year < 1753; return 1 if $year % 100; return 1 unless $year % 400; return 0; } # ------- sub valDate # ------- { my($year, $month, $day) = @_; return 0 unless $year =~ /^\d+$/ and $month =~ /^\d+$/ and $day =~ /^\d+$/; my $daysinm = [ [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]]; return 0 if $year < 1 or $year > 9999; return 0 if $month < 1 or $month > 12; return 0 if $day < 1 or $day > $daysinm->[isLeap($year)]->[$month - 1]; return 0 if $year == 1752 and $month == 9 and ($day > 2 and $day < 14); return 1; }

    It works for any year from 1 to 9999 (not y10k compliant I'm afraid ;-)

    I hope this is of use.

    Cheers,

    JohnGG

      not y10k compliant I'm afraid ;-)

      Not Julian-compliant either!

      You need to change

      return 1 if $year < 1753

      to

      return 1 if $year < 1753 and not $year %4;

      </nitpick>

      Update: Memo to /me: Test before you post! (See below.)

        No, that's not necessary at all.

        By the time we reach that line in the subroutine we are only left with years that are divisible by 4 because of the line above, which is

        return 0 if $year % 4;

        so your amendment, and not $year %4, is superfluous.

        Cheers,

        JohnGG

Re: Simple Date Validation
by starX (Chaplain) on May 11, 2007 at 15:27 UTC
    Someone else here will probably be able to offer a more elegant solution, but I've done something like:

    my $time; # Scalar to store the time in appropriate format for compari +sons with the database time stamp. my ($year, $month, $day, $hour, $minute, $second); # for assembling ti +me stamp. my @localtime = localtime; # Buffer to store time returned from localt +ime function $year = 1900 + $localtime[5]; $month = $localtime[4] + 1; # because localtime starts counting months + at 0 $month = sprintf("%02d", $month); # force 2 digit format. $day = sprintf("%02d", $localtime[3]); $hour = sprintf("%02d", $localtime[2]); $minute = sprintf("%02d", $localtime[1]); $second = sprintf("%02d", $localtime[0]); $time = $year.$month.$day.$hour.$minute.$second;

    And then compared the time string against what I was trying to validate. This node is where I was using the code snips above, and it was fairly specific to the format I was validating against, but I don't see any reason why you couldn't do a similar comparison between $userDate and $time if you've got your $time string formated according to your needs.

Re: Simple Date Validation
by 2xlp (Sexton) on May 11, 2007 at 19:34 UTC

    I'd learn how to install modules.

    CPAN Modules are neat -- you can install them server side if you have root privs, or you can just install them into your local directory

    I'd strongly suggest running the cpan shell ( $perl -MCPAN -e shell ) , then upgrading cpan ( install Bundle::CPAN ) and following the instructions to restart the shell and install modules away. Once you get comfortable with that, you can su/sudo to root and install system-wide -- or not.

    Date::Calc is really the best thing you can use for this situation. It's fast and takes into consideration all of the crazy issues that other people have encountered.

Re: Simple Date Validation
by RL (Monk) on May 11, 2007 at 17:37 UTC

    Some code I tend to use.

    Problem I see with users providing dates: No way to make sure how they write it into the input field. It might be valid even if it's not the required format.
    It's even possible a script might be used in Europe, where time format is different to US :)

    examples of valid dates as of real life as I see them:
    1.12.2006
    01.12.2006
    01.12.06
    12/1/2006
    12/01/06

    right sub parse_datestring { # parses a datestring # - allows to provide a format of how to interprete day,month,year + in the datestring. # default format is: DD/MM/YY # -> calling this method with format=>'d.m.y' computes fa +ster than format=>'dd.mm.yy' ! # - any non-digit character in the datestring is considered a deli +miter! # - 2-digit years will be considered 1930-2029 # - by default the date calculated from the datestring will be val +idated # -> validate=>'0' disables validation # - return value is an array of ($day, $month, $year) # -> $day and $month are returned 2-digit # -> $year is returned 4 digit # ===> in case of invalid date ALL of the above will be unde +f # ===> in case of omitted day or month, the appropriate arra +y elements will be undef # - in case of errors such as invalid date $errstr is set and migh +t be retrieved by $handle->errstr(); # - example call: # my ($day,$mon,$year) = $handle->parse_datestring( 'datestring +' => '02.02.02', # 'format' => + 'd.m.y', # optional # 'validate' +=> 0, # optional # ) # my $self = shift; my %classargs = (@_); # reset $errstr $errstr = ''; my ($d,$m,$y); # make sure 'datestring' is not ommited! if (! $classargs{'datestring'}) { $errstr = 'ERROR parsing datestring: \'datestring\' is missing +'; } # make sure delimiter becomes '/' $classargs{'datestring'} =~ s/\D+/\//gi; # handle input like '06.2004' or '6.06' - assuming month of year if ($classargs{'datestring'} =~ /^(\d{1,2})\/(\d{2,4})$/gi ) { $m = $1; $y = $2; } # handle input like '2004' - assuming year elsif ($classargs{'datestring'} =~ /^\d{2,4}$/gi ) { $y = $classargs{'datestring'}; } # handle all other input - requesting days (possibly regardless of + the year like '06.08' # need to reorder according to format and delimiter else { if (! $classargs{'format'}) { # assume default format DD/MM/Y +Y(YY) ($d, $m, $y) = split(/\//, $classargs{'datestring'}); } else { # parse with provided formatting information my (%lookup,$first,$second,$third); #%position); # make sure delimiter becomes '/' $classargs{'format'} =~ s/[^dmyDMY]+/\//gi;right # need lower case $classargs{'format'} = lc ($classargs{'format'}); if (length($classargs{'format'}) >5) { $classargs{'format'} =~ s/([dmyDMY])+/$1/g; } ($first,$second,$third) = split(/\//, $classargs{'format'} +); ($lookup{$first}, $lookup{$second}, $lookup{$third}) = split(/\//, $classargs{'datestring' +}); $d = $lookup{'d'}; $m = $lookup{'m'}; $y = $lookup{'y'}; } } if (length($d) == 1) { $d = '0'.$d; } if (length($m) == 1) { $m = '0'.$m; } if (length($y) == 2) { if ($y >= 30) { $y += 1900; } else { $y += 2000; } } # validate date # NOTE: validate only if $m or $d are available! unless ($classargs{'validate'} eq '0') { my $m_index = int($m)-1; # validate month if ($m) { if (($m_index <0) || ($m_index >11)) { $errstr = "ERROR parsing datestring: not a valid month +"; } } if ($d) { # validate day my @days_per_month = (31,28,31,30,31,30,31,31,30,31,30,31) +; # check for leap-year if ( ((($y % 4) == 0) && (($y % 100) != 0)) || (($y % 400) == 0) ) { $days_per_month[1] ++; } # check day my $d_integer = int($d); if ( ($d_integer < 1) || ($d_integer > $days_per_month[$m_ +index]) ) { $errstr = "ERROR parsing datestring: not a valid day i +n month $m"; } } } return ($d, $m, $y); }

    If others see assumtions being wrong or any other chance for improvement I'll appreciate that very much as well.

    Hope it helps to find a solution to your problem.
    RL

    update
    s/right/write/

Re: Simple Date Validation
by Trihedralguy (Pilgrim) on May 11, 2007 at 16:21 UTC
    This is what I have so far
    if ($dated =~ /^(?:[01]\d\/[0-3]\d\/(?:19|20)\d\d)?$/) { my ($sec,$min,$hour) = qw( 0 0 12 ); my ($mday, $mon, $year) = split /\//, $dated; $mon--; # because that's how unix/C treat the month value eval { my $dummy = timelocal($sec,$min,$hour,$mday,$mon,$year); }; if (my $err = $@) { $passfail = 0; $errmsg = 'Unable to use date format. Please use "MM/DD/YYYY" +or MM-DD-YYYY for the Date Delivered Field'; $errtitle = 'Validation Error'; $seconds = 7; } else { }; } else { #User entered an invaild date. $passfail = 0; $errmsg = 'Unable to use date format. Please use "MM/DD/YYYY" or M +M-DD-YYYY for the Date Delivered Field'; $errtitle = 'Validation Error'; $seconds = 7; } if ($dreceived =~ /^(?:[01]\d\/[0-3]\d\/(?:19|20)\d\d)?$/) { } else { #User entered an invaild date. $passfail = 0; $errmsg = 'Unable to use date format. Please use "MM/DD/YYYY" or M +M-DD-YYYY on the Date Received field'; $errtitle = 'Validation Error'; $seconds = 7; }

    But its still validating dates like: 15/10/2007

      But its still validating dates like: 15/10/2007

      Of course, since you

      my ($mday, $mon, $year) = split /\//, $dated;

      which means you treat $dated as DD/MM/YYYY.

      --shmem

      _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                    /\_¯/(q    /
      ----------------------------  \__(m.====·.(_("always off the crowd"))."·
      ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
Re: Simple Date Validation
by Anonymous Monk on May 12, 2007 at 05:15 UTC
    man:Time::Local
Re: Simple Date Validation
by CountZero (Bishop) on May 13, 2007 at 06:24 UTC
    If you want to validate dates only, the examples and suggestions given above are OK, but if you include the time part as well, things get all of a sudden much more complicated as at some moments second(s) have been added to make up for the Earth's irregular motion (look at it as some sort of Julian/Gregorian jump, but for seconds only) and if you wish to check for that, it gets very complicated!

    CountZero

    "If you have four groups working on a compiler, you'll get a 4-pass compiler." - Conway's Law

Re: Simple Date Validation
by chrism01 (Friar) on May 13, 2007 at 23:03 UTC
    This depends on whether the original date input method is via a (eg Web) GUI, but if it is, it's much easier/safer to just offer drop down lists for date components (using names for mths). This sidesteps the problem and avoids the American vs European issue of MM-DD vs DD-MM.
    You can even have mth names lists in the various langs you expect.
Re: Simple Date Validation
by Anonymous Monk on May 13, 2007 at 06:02 UTC
    perl -MCPAN -e 'install Module::With::Dependencies'