Date handling modularized
Tags: perl, beginners, learning, tdd
Published: 2021-05-28 14:34:08

Thanks to mentioning my first post on the [perl weekly newsletter](https://perlweekly.com/archive/513.html) 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;
    }
}

```
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;
```
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();
```
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;
    }
}
```
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");
```
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;
    }
}

```
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;
}
```
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 ");

}

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

}

```

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.