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;
- while ( my $tuple = $repeat_sth->fetchrow_hashref ) {
+ while ( my $tuple = $weekly_closed_days_sth->fetchrow_hashref ) {
$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} = {};
- 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;
}
- 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 = [];
- while ( my ( $day, $month, $year, $title, $description ) =
- $special->fetchrow ) {
+ while ( my ( $day, $month, $year ) = $exception_holidays_sth->fetchrow ) {
push @{$dates},
DateTime->new(
day => $day,
}
$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 = [];
- while ( my ( $day, $month, $year, $title, $description ) =
- $special->fetchrow ) {
+ while ( my ( $day, $month, $year ) = $single_holidays_sth->fetchrow ) {
push @{$dates},
DateTime->new(
day => $day,
)->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 ) = @_;
- 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 );
}
- $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 {
- my $days = abs $add_duration->in_units('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 ) = @_;
- 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 ( $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 ( $self->{exception_holidays}->contains($dt) ) {
- return 1;
- }
- if ( $self->{single_holidays}->contains($dt) ) {
+
+ if ( $self->{single_holidays}->contains($localdt) ) {
return 1;
}
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;
- 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
- $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 {
- 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' );
# 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;
}
);
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;
}
=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
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);
-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
$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