Bug 8015: Add unit tests for SimpleMARC and MarcModificationTemplates routines
[koha.git] / Koha / Calendar.pm
index 495f8ce..6a12530 100644 (file)
@@ -33,28 +33,31 @@ sub _init {
     my $self       = shift;
     my $branch     = $self->{branchcode};
     my $dbh        = C4::Context->dbh();
     my $self       = shift;
     my $branch     = $self->{branchcode};
     my $dbh        = C4::Context->dbh();
-    my $repeat_sth = $dbh->prepare(
-'SELECT * from repeatable_holidays WHERE branchcode = ? AND ISNULL(weekday) = ?'
+    my $weekly_closed_days_sth = $dbh->prepare(
+'SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL'
     );
     );
-    $repeat_sth->execute( $branch, 0 );
+    $weekly_closed_days_sth->execute( $branch );
     $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
     Readonly::Scalar my $sunday => 7;
     $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
     Readonly::Scalar my $sunday => 7;
-    while ( my $tuple = $repeat_sth->fetchrow_hashref ) {
+    while ( my $tuple = $weekly_closed_days_sth->fetchrow_hashref ) {
         $self->{weekly_closed_days}->[ $tuple->{weekday} ] = 1;
     }
         $self->{weekly_closed_days}->[ $tuple->{weekday} ] = 1;
     }
-    $repeat_sth->execute( $branch, 1 );
+    my $day_month_closed_days_sth = $dbh->prepare(
+'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL'
+    );
+    $day_month_closed_days_sth->execute( $branch );
     $self->{day_month_closed_days} = {};
     $self->{day_month_closed_days} = {};
-    while ( my $tuple = $repeat_sth->fetchrow_hashref ) {
-        $self->{day_month_closed_days}->{ $tuple->{day} }->{ $tuple->{month} } =
+    while ( my $tuple = $day_month_closed_days_sth->fetchrow_hashref ) {
+        $self->{day_month_closed_days}->{ $tuple->{month} }->{ $tuple->{day} } =
           1;
     }
           1;
     }
-    my $special = $dbh->prepare(
-'SELECT day, month, year, title, description FROM special_holidays WHERE ( branchcode = ? ) AND (isexception = ?)'
+
+    my $exception_holidays_sth = $dbh->prepare(
+'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 1'
     );
     );
-    $special->execute( $branch, 1 );
+    $exception_holidays_sth->execute( $branch );
     my $dates = [];
     my $dates = [];
-    while ( my ( $day, $month, $year, $title, $description ) =
-        $special->fetchrow ) {
+    while ( my ( $day, $month, $year ) = $exception_holidays_sth->fetchrow ) {
         push @{$dates},
           DateTime->new(
             day       => $day,
         push @{$dates},
           DateTime->new(
             day       => $day,
@@ -65,10 +68,13 @@ sub _init {
     }
     $self->{exception_holidays} =
       DateTime::Set->from_datetimes( dates => $dates );
     }
     $self->{exception_holidays} =
       DateTime::Set->from_datetimes( dates => $dates );
-    $special->execute( $branch, 1 );
+
+    my $single_holidays_sth = $dbh->prepare(
+'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 0'
+    );
+    $single_holidays_sth->execute( $branch );
     $dates = [];
     $dates = [];
-    while ( my ( $day, $month, $year, $title, $description ) =
-        $special->fetchrow ) {
+    while ( my ( $day, $month, $year ) = $single_holidays_sth->fetchrow ) {
         push @{$dates},
           DateTime->new(
             day       => $day,
         push @{$dates},
           DateTime->new(
             day       => $day,
@@ -78,85 +84,131 @@ sub _init {
           )->truncate( to => 'day' );
     }
     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
           )->truncate( to => 'day' );
     }
     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
-    $self->{days_mode} = C4::Context->preference('useDaysMode');
+    $self->{days_mode}       = C4::Context->preference('useDaysMode');
+    $self->{test}            = 0;
     return;
 }
 
 sub addDate {
     my ( $self, $startdate, $add_duration, $unit ) = @_;
     return;
 }
 
 sub addDate {
     my ( $self, $startdate, $add_duration, $unit ) = @_;
-    my $base_date = $startdate->clone();
+
+    # Default to days duration (legacy support I guess)
     if ( ref $add_duration ne 'DateTime::Duration' ) {
         $add_duration = DateTime::Duration->new( days => $add_duration );
     }
     if ( ref $add_duration ne 'DateTime::Duration' ) {
         $add_duration = DateTime::Duration->new( days => $add_duration );
     }
-    $unit ||= q{};    # default days ?
-    my $days_mode = $self->{days_mode};
-    Readonly::Scalar my $return_by_hour => 10;
-    my $day_dur = DateTime::Duration->new( days => 1 );
-    if ( $add_duration->is_negative() ) {
-        $day_dur = DateTime::Duration->new( days => -1 );
+
+    $unit ||= 'days'; # default days ?
+    my $dt;
+
+    if ( $unit eq 'hours' ) {
+        # Fixed for legacy support. Should be set as a branch parameter
+        Readonly::Scalar my $return_by_hour => 10;
+
+        $dt = $self->addHours($startdate, $add_duration, $return_by_hour);
+    } else {
+        # days
+        $dt = $self->addDays($startdate, $add_duration);
     }
     }
-    if ( $days_mode eq 'Datedue' ) {
 
 
-        my $dt = $base_date + $add_duration;
-        while ( $self->is_holiday($dt) ) {
+    return $dt;
+}
 
 
-            # TODOP if hours set to 10 am
-            $dt->add_duration($day_dur);
-            if ( $unit eq 'hours' ) {
-                $dt->set_hour($return_by_hour);    # Staffs specific
-            }
+sub addHours {
+    my ( $self, $startdate, $hours_duration, $return_by_hour ) = @_;
+    my $base_date = $startdate->clone();
+
+    $base_date->add_duration($hours_duration);
+
+    # If we are using the calendar behave for now as if Datedue
+    # was the chosen option (current intended behaviour)
+
+    if ( $self->{days_mode} ne 'Days' &&
+          $self->is_holiday($base_date) ) {
+
+        if ( $hours_duration->is_negative() ) {
+            $base_date = $self->prev_open_day($base_date);
+        } else {
+            $base_date = $self->next_open_day($base_date);
         }
         }
-        return $dt;
-    } elsif ( $days_mode eq 'Calendar' ) {
-        if ( $unit eq 'hours' ) {
-            $base_date->add_duration($add_duration);
-            while ( $self->is_holiday($base_date) ) {
-                $base_date->add_duration($day_dur);
 
 
-            }
+        $base_date->set_hour($return_by_hour);
+
+    }
+
+    return $base_date;
+}
+
+sub addDays {
+    my ( $self, $startdate, $days_duration ) = @_;
+    my $base_date = $startdate->clone();
+
+    if ( $self->{days_mode} eq 'Calendar' ) {
+        # use the calendar to skip all days the library is closed
+        # when adding
+        my $days = abs $days_duration->in_units('days');
 
 
+        if ( $days_duration->is_negative() ) {
+            while ($days) {
+                $base_date = $self->prev_open_day($base_date);
+                --$days;
+            }
         } else {
         } else {
-            my $days = abs $add_duration->in_units('days');
             while ($days) {
             while ($days) {
-                $base_date->add_duration($day_dur);
-                if ( $self->is_holiday($base_date) ) {
-                    next;
-                } else {
-                    --$days;
-                }
+                $base_date = $self->next_open_day($base_date);
+                --$days;
             }
         }
             }
         }
-        if ( $unit eq 'hours' ) {
-            my $dt = $base_date->clone()->subtract( days => 1 );
-            if ( $self->is_holiday($dt) ) {
-                $base_date->set_hour($return_by_hour);    # Staffs specific
+
+    } else { # Days or Datedue
+        # use straight days, then use calendar to push
+        # the date to the next open day if Datedue
+        $base_date->add_duration($days_duration);
+
+        if ( $self->{days_mode} eq 'Datedue' ) {
+            # Datedue, then use the calendar to push
+            # the date to the next open day if holiday
+            if ( $self->is_holiday($base_date) ) {
+                if ( $days_duration->is_negative() ) {
+                    $base_date = $self->prev_open_day($base_date);
+                } else {
+                    $base_date = $self->next_open_day($base_date);
+                }
             }
         }
             }
         }
-        return $base_date;
-    } else {    # Days
-        return $base_date + $add_duration;
     }
     }
+
+    return $base_date;
 }
 
 sub is_holiday {
     my ( $self, $dt ) = @_;
 }
 
 sub is_holiday {
     my ( $self, $dt ) = @_;
-    my $dow = $dt->day_of_week;
+    my $localdt = $dt->clone();
+    my $day   = $localdt->day;
+    my $month = $localdt->month;
+
+    $localdt->truncate( to => 'day' );
+
+    if ( $self->{exception_holidays}->contains($localdt) ) {
+        # exceptions are not holidays
+        return 0;
+    }
+
+    my $dow = $localdt->day_of_week;
+    # Representation fix
+    # TODO: Shouldn't we shift the rest of the $dow also?
     if ( $dow == 7 ) {
         $dow = 0;
     }
     if ( $dow == 7 ) {
         $dow = 0;
     }
+
     if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
         return 1;
     }
     if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
         return 1;
     }
-    $dt->truncate( to => 'day' );
-    my $day   = $dt->day;
-    my $month = $dt->month;
+
     if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
         return 1;
     }
     if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
         return 1;
     }
-    if ( $self->{exception_holidays}->contains($dt) ) {
-        return 1;
-    }
-    if ( $self->{single_holidays}->contains($dt) ) {
+
+    if ( $self->{single_holidays}->contains($localdt) ) {
         return 1;
     }
 
         return 1;
     }
 
@@ -164,30 +216,63 @@ sub is_holiday {
     return 0;
 }
 
     return 0;
 }
 
+sub next_open_day {
+    my ( $self, $dt ) = @_;
+    my $base_date = $dt->clone();
+
+    $base_date->add(days => 1);
+
+    while ($self->is_holiday($base_date)) {
+        $base_date->add(days => 1);
+    }
+
+    return $base_date;
+}
+
+sub prev_open_day {
+    my ( $self, $dt ) = @_;
+    my $base_date = $dt->clone();
+
+    $base_date->add(days => -1);
+
+    while ($self->is_holiday($base_date)) {
+        $base_date->add(days => -1);
+    }
+
+    return $base_date;
+}
+
 sub days_between {
     my $self     = shift;
     my $start_dt = shift;
     my $end_dt   = shift;
 
 sub days_between {
     my $self     = shift;
     my $start_dt = shift;
     my $end_dt   = shift;
 
-    my $datestart_temp = $start_dt->clone();
-    my $dateend_temp = $end_dt->clone();
+    if ( $start_dt->compare($end_dt) > 0 ) {
+        # swap dates
+        my $int_dt = $end_dt;
+        $end_dt = $start_dt;
+        $start_dt = $int_dt;
+    }
+
 
     # start and end should not be closed days
 
     # start and end should not be closed days
-    $datestart_temp->truncate( to => 'day' );
-    $dateend_temp->truncate( to => 'day' );
-    my $duration = $dateend_temp - $datestart_temp;
-    while ( DateTime->compare( $datestart_temp, $dateend_temp ) == -1 ) {
-        $datestart_temp->add( days => 1 );
-        if ( $self->is_holiday($datestart_temp) ) {
-            $duration->subtract( days => 1 );
+    my $days = $start_dt->delta_days($end_dt)->delta_days;
+    for (my $dt = $start_dt->clone();
+        $dt <= $end_dt;
+        $dt->add(days => 1)
+    ) {
+        if ($self->is_holiday($dt)) {
+            $days--;
         }
     }
         }
     }
-    return $duration;
+    return DateTime::Duration->new( days => $days );
 
 }
 
 sub hours_between {
 
 }
 
 sub hours_between {
-    my ($self, $start_dt, $end_dt) = @_;
+    my ($self, $start_date, $end_date) = @_;
+    my $start_dt = $start_date->clone();
+    my $end_dt = $end_date->clone();
     my $duration = $end_dt->delta_ms($start_dt);
     $start_dt->truncate( to => 'day' );
     $end_dt->truncate( to => 'day' );
     my $duration = $end_dt->delta_ms($start_dt);
     $start_dt->truncate( to => 'day' );
     $end_dt->truncate( to => 'day' );
@@ -195,12 +280,19 @@ sub hours_between {
     # However for hourly loans the logic should be expanded to
     # take into account open/close times then it would be a duration
     # of library open hours
     # However for hourly loans the logic should be expanded to
     # take into account open/close times then it would be a duration
     # of library open hours
-    while ( DateTime->compare( $start_dt, $end_dt ) == -1 ) {
-        $start_dt->add( days => 1 );
-        if ( $self->is_holiday($start_dt) ) {
-            $duration->subtract( hours => 24 );
+    my $skipped_days = 0;
+    for (my $dt = $start_dt->clone();
+        $dt <= $end_dt;
+        $dt->add(days => 1)
+    ) {
+        if ($self->is_holiday($dt)) {
+            ++$skipped_days;
         }
     }
         }
     }
+    if ($skipped_days) {
+        $duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days));
+    }
+
     return $duration;
 
 }
     return $duration;
 
 }
@@ -220,7 +312,41 @@ sub _mockinit {
     );
     push @{$dates}, $special;
     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
     );
     push @{$dates}, $special;
     $self->{single_holidays} = DateTime::Set->from_datetimes( dates => $dates );
-    $self->{days_mode} = 'Calendar';
+
+    # if not defined, days_mode defaults to 'Calendar'
+    if ( !defined($self->{days_mode}) ) {
+        $self->{days_mode} = 'Calendar';
+    }
+
+    $self->{test} = 1;
+    return;
+}
+
+sub set_daysmode {
+    my ( $self, $mode ) = @_;
+
+    # if not testing this is a no op
+    if ( $self->{test} ) {
+        $self->{days_mode} = $mode;
+    }
+
+    return;
+}
+
+sub clear_weekly_closed_days {
+    my $self = shift;
+    $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];    # Sunday only
+    return;
+}
+
+sub add_holiday {
+    my $self = shift;
+    my $new_dt = shift;
+    my @dt = $self->{single_holidays}->as_list;
+    push @dt, $new_dt;
+    $self->{single_holidays} =
+      DateTime::Set->from_datetimes( dates => \@dt );
+
     return;
 }
 
     return;
 }
 
@@ -237,9 +363,9 @@ This documentation refers to Koha::Calendar version 0.0.1
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
-  use Koha::Calendat
+  use Koha::Calendar
 
 
-  my $c = Koha::Calender->new( branchcode => 'MAIN' );
+  my $c = Koha::Calendar->new( branchcode => 'MAIN' );
   my $dt = DateTime->now();
 
   # are we open
   my $dt = DateTime->now();
 
   # are we open
@@ -275,11 +401,36 @@ Currently unit is only used to invoke Staffs return Monday at 10 am rule this
 parameter will be removed when issuingrules properly cope with that
 
 
 parameter will be removed when issuingrules properly cope with that
 
 
+=head2 addHours
+
+    my $dt = $calendar->addHours($date, $dur, $return_by_hour )
+
+C<$date> is a DateTime object representing the starting date of the interval.
+
+C<$offset> is a DateTime::Duration to add to it
+
+C<$return_by_hour> is an integer value representing the opening hour for the branch
+
+
+=head2 addDays
+
+    my $dt = $calendar->addDays($date, $dur)
+
+C<$date> is a DateTime object representing the starting date of the interval.
+
+C<$offset> is a DateTime::Duration to add to it
+
+C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
+
+Currently unit is only used to invoke Staffs return Monday at 10 am rule this
+parameter will be removed when issuingrules properly cope with that
+
+
 =head2 is_holiday
 
 $yesno = $calendar->is_holiday($dt);
 
 =head2 is_holiday
 
 $yesno = $calendar->is_holiday($dt);
 
-passed at DateTime object returns 1 if it is a closed day
+passed a DateTime object returns 1 if it is a closed day
 0 if not according to the calendar
 
 =head2 days_between
 0 if not according to the calendar
 
 =head2 days_between
@@ -287,7 +438,40 @@ passed at DateTime object returns 1 if it is a closed day
 $duration = $calendar->days_between($start_dt, $end_dt);
 
 Passed two dates returns a DateTime::Duration object measuring the length between them
 $duration = $calendar->days_between($start_dt, $end_dt);
 
 Passed two dates returns a DateTime::Duration object measuring the length between them
-ignoring closed days
+ignoring closed days. Always returns a positive number irrespective of the
+relative order of the parameters
+
+=head2 next_open_day
+
+$datetime = $calendar->next_open_day($duedate_dt)
+
+Passed a Datetime returns another Datetime representing the next open day. It is
+intended for use to calculate the due date when useDaysMode syspref is set to either
+'Datedue' or 'Calendar'.
+
+=head2 prev_open_day
+
+$datetime = $calendar->prev_open_day($duedate_dt)
+
+Passed a Datetime returns another Datetime representing the previous open day. It is
+intended for use to calculate the due date when useDaysMode syspref is set to either
+'Datedue' or 'Calendar'.
+
+=head2 set_daysmode
+
+For testing only allows the calling script to change days mode
+
+=head2 clear_weekly_closed_days
+
+In test mode changes the testing set of closed days to a new set with
+no closed days. TODO passing an array of closed days to this would
+allow testing of more configurations
+
+=head2 add_holiday
+
+Passed a datetime object this will add it to the calendar's list of
+closed days. This is for testing so that we can alter the Calenfar object's
+list of specified dates
 
 =head1 DIAGNOSTICS
 
 
 =head1 DIAGNOSTICS