Developing a script in small steps
Tags: beginners, perl, learning
Published: 2021-05-17 17:51:25

It all started with a git commit I found: 

> commit 340d908fe537d3163e220c84945bbfc5b49fe8dc
Author: A.
Date:   Wed Apr 14 17:28:50 2021 +0200
Deleted redundant INFO messages. Replaced date strings with dt->ymd() calls. Replaced some if nots with unless. Simplified sub _unpack. Set timezone in DateTime objects. Results are returned as tail calls now. Maybe more minor changes.

A. is a fresh developer in my department and as many future devs she did not learn so far - neither in university nor in books - how to organize her work. How to avoid commits ending with **Maybe more minor changes.** Honestly I know a lot senior devs who still use a version control system like any other backup. So what I wanted to show her, was: 
* How to organize my programming in small steps
* How slowly developing 
* How to make errors without desaster
* How to use your version control
* How to write a helpful commit-message
* How will I later find something I did

So I started a programming-session on my own, and gave her later the repository and we discussed each step. (Honestly: She found immediately an error in my code, that I did not left there intentionally).

The development was in perl, but I hope other devs find the steps valueable too, so I write it down here. (My first longer article in english)

#Step one: all started with a README

```
- Script with parameters
    -d Date
        -> has to be in the past
        -> must be no older then 7 days

```

Of course that was the first commit: 

> commit 
Author: Volker Kroll 
Date:   Wed May 5 16:49:17 2021 +0200
>
>    README
>    
>    * we will have the requirements here

#Now the fun started, the initial script
```
use strict;

use Getopt::Long;

my $date ; ## the startingdate

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

print "submitted Date is: $date\n";
```
The perl people know, that script does not much more then storing a submitted string as parameter --date or -d in the scalar variable $date.
And of course it uses strict as every good perl script does. 

I committed this change: 
> Author: Volker Kroll
> Date:   Wed May 5 16:52:01 2021 +0200
>
>    initial
>    
>    * Parameter d for the date
>
>    TODO:
>    * Validation

#As promised a validation

```
## starts as before
sub is_date {
    my $date = shift;

    return 1 if $date =~ /^\d\d\d\d-\d\d-\d\d$/;
    return 0;

}
if(is_date($date)) {
    print "submitted Date is: $date\n";
}
else {
    print "submitted value $date is no date\n";
}
```
I checked if the submitted date matches the regex `^\d\d\d\d-\d\d-\d\d$` - you are aware, that we perl-devs solve our problems with regexes right?

But enough for the next commit

> Author: Volker Kroll 
> Date:   Wed May 5 16:54:49 2021 +0200
>
>    first check for date
>    
>    * date is valid if regex \d\d\d\d-\d\d-\d\d is valid
>    
>    TODO:
>    * better check
>    * exclude non valid month and day


#More checks
As announced the next step was to better check for the validity of the date  - and stay tuned these steps will be changed later. 

```
sub is_date {
    my $date = shift;

    my $days = ["", 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    my($y, $m, $d) = $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
    if ($m > 0 && $m < 13) {
        print "$m has max $days->[$m] days\n";
        return 1 if $d <= $days->[$m];
    }

    return 0;

}
```

And the next commit even though theses checks are far from perfect

> Author: Volker Kroll 
> Date:   Wed May 5 17:00:23 2021 +0200
>
>    more checking of date via regex
>    
>    * added length per month value
>    * check if day is possible in the month
>    
>    TODO:
>    * that is a bad choice for february

#Let's correct the february
As we saw, my checks did not really work in february - only in leap-years, so I had to handle that with a check wether it is a leap year or not.
```
sub is_date {
    my $date = shift;

    my $days = ["", 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    my($y, $m, $d) = $date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
    if (is_leap_year($y)) {
        $days->[2] = 29;
    }

    if ($m > 0 && $m < 13) {
        print "$m has max $days->[$m] days\n";
        return 1 if $d <= $days->[$m];
    }
    return 0;
}

sub is_leap_year {
    my $y = shift;

    if ($y % 4 == 0) {
        return 1 if $y % 400 == 0;
        return 0 if $y % 100 == 0;
        return 1;
    }
}
```
Now february is handled correctly in every year. So, up the next commit

>Author: Volker Kroll 
>Date:   Wed May 5 17:07:06 2021 +0200
>
>    check works even for february
>    
>    * new function is_leap_year
>    TODO
>    * Refactoring - Removing of all own written calendar functions

#Refactoring - Use of Date Modules
I had to remove my not so elegant and error-prone date-functions. To get you back on track, here is the whole script after removing that and adding a suitable date module
```
use strict;

use Getopt::Long;
use DateTime;

my $date ; ## the startingdate

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

sub is_date {
    my $date = shift;

    my $days = ["", 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    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;
    }
}


if(is_date($date)) {
    print "submitted Date is: $date\n";
}
else {
    print "submitted value $date is no date\n";
}
```
I am using now [DateTime](https://metacpan.org/pod/DateTime) for every date operation. If no DateTime object can be created we assume, that the submitted value is not correct and return 0, else we return a DateTime Object.
> Author: Volker Kroll 
> Date:   Wed May 5 17:24:41 2021 +0200
> 
>    removed own calendar functions and added DateTime
>    
>    * validation via Exception of DateTime->new()
>    
>    TODO:
>    * Refactoring: rename function is_date (returns a dt if true)

Maybe you saw, that I forgot to remove the arry with the days per month... I saw it now, while I write this article

#Clean it up 1/2
```
use strict;

use Getopt::Long;
use DateTime;

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


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

sub get_dt {
    my $date = shift;

    my $days = ["", 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    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;
    }
}

$dt = get_dt($date);

if($dt) {
    print "submitted Date is: $date\n";
}
else {
    print "submitted value $date is no date\n";
}
```
Everything I did is mentioned in the commit-message (and there is still that array of days...

>Author: Volker Kroll 
> Date:   Wed May 5 17:28:04 2021 +0200
>
>    moved function is_date to get_dt
>    
>    * check is now if return of get_dt is a true value
>      a DateTime Object is a true value
>      0 is not
>      => so the check still works
>      => so we have already a working DateTime Object to work with later

#Clean it up 2/2

Now I moved around the code a bit to make it more readable

```
use strict;

use Getopt::Long;
use DateTime;

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


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

$dt = get_dt($date);

if($dt) {
    print "submitted Date is: $date\n";
}
else {
    print "submitted value $date is no date\n";
}
```

The sub get_dt remained the same (and there is still that array)

>Author: Volker Kroll
> Date:   Wed May 5 17:31:00 2021 +0200
> 
>    moved main code up in the file

#Now for more functionality

As we remember the day has to be in the past. So we need to check that. DateTime Objects can easily be compared and DateTime has a function called today(). So my first step was:

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



if(!$dt) {
    print "submitted value $date is no date\n";
}
if($dt < $today) {
    print "submitted date is in the past\n";
}

```

That was easy and it worked out of the box.

>Author: Volker Kroll 
>Date:   Wed May 5 17:33:56 2021 +0200
>
>    check if submitted date is in the past
>    
>    * added new DateTime Object $today
>    * compare $today and DateTime Object of submitted date
>    
>    TODO
>    * next check: not older then 7 days (see README)

#But first let's remove this array....

I think you know how a not existing line looks nevertheless that was worth a commit

>Author: Volker Kroll 
>Date:   Wed May 5 17:37:45 2021 +0200
>
>    removed old arrayref for days in the month

#Check if it was in the last week

In our requirements was that the date has to be in the last seven days. (Whatever that means in detail)

Time for another Date Module [DateTime::Duration](https://metacpan.org/pod/DateTime::Duration) to calculate that difference.

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

    my $dur = DateTime::Duration->new(days => 7);
    $dt->add_duration($dur);
    return 1 if $today < $dt;
}
```
I think that code is easily to understand even for not perl-devs. I add 7 days to the submitted date and compare it with today. (Here is the error that my colleague found immediatly - but more about the bug later).

>Author: Volker Kroll 
> Date:   Wed May 5 17:45:57 2021 +0200
>
>    check for is_last_week
>    
>    * added DateTime::Duration
>    * checked if submitted date was in the last 7 days
>    
>    TODO:
>    * Refactoring: Add the check for in the past in is_last_week

# Cleanup
As mentioned in the TODO we removed the "is in the past" check from the main and added it to the is_last_week
```
sub is_last_week {
    my $dt = shift;
    return unless ref $dt eq "DateTime";
    return if($dt > $today) ;

    my $dur = DateTime::Duration->new(days => 7);
    $dt->add_duration($dur);
    return 1 if $today < $dt;
}
```
Normally all this would end with the this commit:
> Author: Volker Kroll <kroll@strato.de>
> Date:   Wed May 5 17:48:31 2021 +0200
>
>    is_last_week now a valid check for date concerning to specification
 
#The bug
As I wrote before, my colleague spotted a bug in my code. I got this question via chat:

>A:     The line $dt->add_duration($dur); made me wonder. Is the dt object not changed then? Is this call by value or call by reference?

And she was right. So the next commit was from her, modifying my code and correcting it. 

```
diff --git a/skript.pl b/skript.pl
index 2f5594b..426c4af 100644
--- a/skript.pl
+++ b/skript.pl
@@ -30,7 +30,8 @@ sub is_last_week {
     return if($dt > $today) ;
 
     my $dur = DateTime::Duration->new(days => 7);
-    $dt->add_duration($dur);
+    #$dt->add_duration($dur);
+    $today->subtract_duration($dur);
     return 1 if $today < $dt;
 }
```

She changed the direction of the math and modified the no longer used $today

>Author: A.
>Date:   Fri May 7 12:38:15 2021 +0200
>
>    is_last_week subtracts from today instead of adding to dt.

Now the script was correct concerning the requirements. 

#Conclusions
Of course this is a little bit artificial - and most devs will need to confess that they usually commit much bigger chunks of code. Nevertheless, I think it is valuable to develop in these small chunks.

Next steps were making it a bit more modular and added automated tests for further development. But for today this shall be enough.