DEV Community

Volker Kroll
Volker Kroll

Posted on

Date handling modularized

Thanks to mentioning my first post on the perl weekly newsletter a lot people read the first part of this serie of articles. (Don't know yet, how many parts it will be.) So I decided to write a follow-up.

What can you expect:

  • Moving functions to modules
  • testing functions automatically
  • change a module to a class
  • inherit the class from a base class

To summarize where we left our little skript in part one:

use strict;

use Getopt::Long;
use DateTime;
use DateTime::Duration;

my $date;   ## the starting date argument
my $dt;     ## DateTime Object of starting date


GetOptions(
        "date=s"         => \$date,
        );

$dt = get_dt($date);
my $today = DateTime->today();



if(!$dt) {
    print "submitted value $date is no date\n";
}
if(is_last_week($dt)) {
    print "submitted date was in the last week\n";
}

sub is_last_week {
    my $dt = shift;
    return unless ref $dt eq "DateTime";
    return if($dt > $today) ;

    my $dur = DateTime::Duration->new(days => 7);
    $today->subtract_duration($dur);
    return 1 if $today < $dt;
}


sub get_dt {
    my $date = shift;

    my($y, $m, $d) = $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
    my $dt;
    eval {
        $dt = DateTime->new(year => $y, month => $m, day => $d);
    }; 
    if ($@) {
        print STDERR "Error while generating DateTime: $@\n";
        return 0;
    }
    else {
        return $dt;
    }
}

Enter fullscreen mode Exit fullscreen mode

The script checks a parameter -d if it is a valid date and that the date is in the last week.

Calling the script without a paremeter end with a not so nice error:

perl skript.pl
Error while generating DateTime: Validation failed for type named DayOfMonth declared in package DateTime::Types (/home/kroll/.plenv/versions/5.32.1/lib/perl5/site_perl/5.32.1/x86_64-linux/DateTime/Types.pm) at line 29 in sub named (eval) with value undef

Trace begun at Specio::Exception->new line 57
Specio::Exception::throw('Specio::Exception', 'message', 'Validation failed for type named DayOfMonth declared in package DateTime::Types (/home/kroll/.plenv/versions/5.32.1/lib/perl5/site_perl/5.32.1/x86_64-linux/DateTime/Types.pm) at line 29 in sub named (eval) with value undef', 'type', 'Specio::Constraint::Simple=HASH(0x55a2fa53c0a8)', 'value', undef) called at (eval 201) line 91
DateTime::_check_new_params('year', undef, 'month', undef, 'day', undef) called at /home/kroll/.plenv/versions/5.32.1/lib/perl5/site_perl/5.32.1/x86_64-linux/DateTime.pm line 176
DateTime::new('DateTime', 'year', undef, 'month', undef, 'day', undef) called at skript.pl line 45
eval {...} at skript.pl line 44
main::get_dt(undef) called at skript.pl line 15

submitted value is no date

We will write this on our to-do-list for later.

But for today we want to put the date handling out of the script in a module, so that the script later can do the heavy lifting but the date handling can be reused in other scripts.

Generating a new and empty module

For the perl people it is quite obvious, you generate a directory lib and put the module in and do the normal perl boilerplate:

package Date;

use strict;

use DateTime;
use DateTime::Duration;


1;
Enter fullscreen mode Exit fullscreen mode

And as the test-driven guy I am usually, I added my first test too. In t/date.t you will find:

use Test::More;

use_ok("Date");

done_testing();
Enter fullscreen mode Exit fullscreen mode

And of course this will result green:

$ prove -lv
t/date.t ..
ok 1 - use Date;
1..1
ok

So we can commit it:

Author: Volker Kroll
Date: Thu May 6 11:59:53 2021 +0200

new Module Date

  • Date is empty
  • t/date.t has a test, that it loads correctly

Move the get_dt in Date.pm

Not much to do then get the code from the script and paste it in our module. But because I prefer modules to be a little more object-oriented I also added a constructor in the - little weird old oo-perl style.

sub new {
    my $class = shift;
    my $datestring = shift|| undef;
    if ($datestring) {
        bless get_dt($datestring), $class;
    }
}
Enter fullscreen mode Exit fullscreen mode

And a little more testing of course:

use_ok("Date");

my $d = Date::get_dt('2020-01-02');
isa_ok($d, DateTime);
my $date = Date->new('2020-01-02');
isa_ok($date, "Date");
Enter fullscreen mode Exit fullscreen mode

Resulting in:

$ prove -lv
t/date.t ..
ok 1 - use Date;
ok 2 - An object of class 'DateTime' isa 'DateTime'
ok 3 - An object of class 'Date' isa 'Date'
1..3
ok

Enough code to commit it:

Author: Volker Kroll kroll@strato.de
Date: Thu May 6 12:11:15 2021 +0200

get_dt now in Date

  • get_dt is called like in skript.pl Date::get_dt(yyyy-mm-dd)
  • additionally you can do a Date->new(yyyy-mm-dd) returned is an object of class Date (that is a DateTime)

Why not make Date a child of DateTime?

While working on my module I thought it might be a good idea to inherit from DateTime so that if I do a Date->new(...) I get a DateTime object back. But, that did not work immediatly, because I need to add a lot of code of DateTime then for follow that constructor. (And to be honest, I forgot how to only add a little in my constructor and letting the "real constructor" do the heavy lifting of DateTime, so I decided for this project to rename my constructor to create (that's easy in perl, where you can name your constructor like you want to).

package Date;

use strict;

use base qw(DateTime);
use DateTime;
use DateTime::Duration;


sub create {
    my $class = shift;
    my $datestring = shift|| undef;
    if ($datestring) {
        return bless get_dt($datestring), $class;
    }
}

Enter fullscreen mode Exit fullscreen mode

Also I wanted my function is_last_week in the module too, we had it before in our little script. For making the tests more easy, I added the possibility to "cheat with today" as A. mentioned. When I add a parameter today in the call it uses that not the "real today":

sub is_last_week {
    my $dt = shift;
    return unless ref $dt;
    my $today = shift ||  DateTime->today();
    return if($dt > $today) ;

    my $dur = DateTime::Duration->new(days => 7);
    $dt->add_duration($dur);
    return 1 if $today < $dt;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Now I was able to add some more testing:

use Test::More;
use Data::Dumper;
use DateTime;

use_ok("Date");
test_new();
test_is_last_week();
done_testing();

sub test_new {
    my $d = Date::get_dt('2020-01-02');
    isa_ok($d, DateTime);
    my $date = Date->create('2020-01-02');
    isa_ok($date, Date);
    isa_ok($date, DateTime);
}

sub test_is_last_week {
    my $date = Date->create('2020-01-02');
    my $today = DateTime->new(year => 2020, month => 01, day => 05);
    can_ok($date, "is_last_week");
    isa_ok($date, "Date");
    isa_ok($today, "DateTime");
    is($date->is_last_week($today), 1, "2020-01-02 was in the week before 2020-01-05 ");
    is($date->is_last_week(), 0, "2020-01-02 was not last week from today ");

}

Enter fullscreen mode Exit fullscreen mode

Again resulting in the expected output:

$ prove -lv
t/date.t ..
ok 1 - use Date;
ok 2 - An object of class 'DateTime' isa 'DateTime'
ok 3 - An object of class 'Date' isa 'Date'
ok 4 - An object of class 'Date' isa 'DateTime'
ok 5 - Date->can('is_last_week')
ok 6 - An object of class 'Date' isa 'Date'
ok 7 - An object of class 'DateTime' isa 'DateTime'
ok 8 - 2020-01-02 was in the week before 2020-01-05
ok 9 - 2020-01-02 was not last week from today
1..9
ok

The next step is of course -- as usual -- the commit

Author: Volker Kroll
Date: Fri May 7 11:38:11 2021 +0200

added Tests for date was in last week

  • changed new to create to not interfere with DateTime new
  • add function for is_last_week
  • for testing purposes it is possible to call is_last_week with a given "today" so tests don't become red

Just a few more tests

sub test_is_last_week {
    my $date = Date->create('2020-01-02');
    my $today = DateTime->new(year => 2020, month => 01, day => 05);
    can_ok($date, "is_last_week");
    isa_ok($date, "Date");
    isa_ok($today, "DateTime");
    is($date->is_last_week($today), 1, "2020-01-02 was in the week before 2020-01-05 ");
    is($date->is_last_week(), 0, "2020-01-02 was not last week from today ");
    is($date->is_last_week(), 0, "2020-01-02 was not last week from today ");

}

Enter fullscreen mode Exit fullscreen mode

Now the whole module is tested enough to use it.

t/date.t ..
ok 1 - use Date;
ok 2 - An object of class 'DateTime' isa 'DateTime'
ok 3 - An object of class 'Date' isa 'Date'
ok 4 - An object of class 'Date' isa 'DateTime'
ok 5 - Date->can('is_last_week')
ok 6 - An object of class 'Date' isa 'Date'
ok 7 - An object of class 'DateTime' isa 'DateTime'
ok 8 - 2020-01-02 was in the week before 2020-01-05
ok 9 - 2020-01-02 was not last week from today
ok 10 - 2020-01-02 was not last week from today
1..10
ok

Only a final commit

Author: Volker Kroll
Date: Fri May 7 14:21:09 2021 +0200

some more testing in date.t

Conclusion

Only a few changes changed a lot. Now we are able to easily test the functions we developed. Additionally we can later reuse our (now DateTime) object. But the biggest benefit for the following work is to remove the date handling from the script that will have other work to do. So we separeted the different parts which make them more easy to modify and use.

What will be the next logical step? That is depended of the requirements and the users of the software. But I would probably fix the "bad error message" when called without a parameter. Usually I call the help function (that still needs to be written), when a mandatory parameter is missing. But that will be done in a third part -- maybe -- stay tuned.

Top comments (4)

Collapse
 
matthewpersico profile image
Matthew O. Persico

This is a very good example for anyone, not just new Perl programmers. Commit early and often. Small commits, one topic each. Test everything. Very well done. I don't need a Date module and I know all this already, and I still want to follow along. Kudos.

Collapse
 
vkroll profile image
Volker Kroll

"Commit early and often. " -- Thanks for your comment, that was exactly what I wanted to show.

Collapse
 
jnareb profile image
Jakub Narębski

Why do you have the following line twice?

is($date->is_last_week(), 0, "2020-01-02 was not last week from today ");
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vkroll profile image
Volker Kroll

I had a bug in the code and I found it but did not confess it in the article -- that is a remain of the bugfix. (Did not notice so far, I should remove it.)