The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package DateTime::BusinessHours;
use DateTime;
use strict;
use warnings;
use integer;

use vars qw($VERSION);

BEGIN
{
    $VERSION = '1.01';
  }


use Class::MethodMaker
    [ scalar => [qw/ datetime1 datetime2 worktiming weekends holidayfile holidays /],
    
    ];

sub new
  {
    my $self=shift;
    my %args=@_;
    my $datetime1 = $args{datetime1} || die "datetime1 parameter required";
    my $datetime2 = $args{datetime2} || die "datetime2 parameter required";
    my $worktiming = $args{worktiming} || [9,18];
    my $weekends = $args{weekends} || [6,7];
    my $holidays = $args{holidays} || "" ;
    my $holidayfile = $args{holidayfile} || "";
    return bless {datetime1=>$datetime1,datetime2=>$datetime2,worktiming=>$worktiming,weekends=>$weekends,holidays=>$holidays,holidayfile=>$holidayfile} , $self;
    
    
  }

sub getdays
    {
      my $self = shift;
      my $datediff=$self->datetime2-$self->datetime1;
      my $days = $datediff->delta_days;
      my $noofweeks = $days/7;
      my $extradays = $days%7;
      my $startday = $self->datetime1->day_of_week;

      #exclude any day in the week marked as holiday (ex: saturday , sunday)
      $days = $days - ($noofweeks * ($#{$self->weekends}+1));  

      #this is for the extra days that dont make up 7 days , to remove holidays from them
      foreach (@{$self->weekends}) {
	if ($startday == $_) {
	  $days = $days - 1;
	  
	}
	else {
	  if ($_ >= $startday) {
	    if ($startday+$extradays >= $_) {
	      $days = $days - 1;
	      
	    }
	  }
	  else {
	    if (7-$startday+$extradays >= $_) {
	      $days = $days -1;
					       
	    }
	  }
	}
      }
      # Read company holiday's from the file 
      if ($self->holidayfile) {
	open(HF , $self->holidayfile) || warn "Predinfed holiday file not found";
	#exclude any holidays that have been marked in the companies academic year
	forHF: foreach(<HF>)
	  {
	    my ($year , $month , $day) = split('-',$_);
	    my $holidate = DateTime->new(year=>$year,month=>$month,day=>$day);
	    #check if mentioned holiday lies in defined weekend , shouldnt deduct twice
	    foreach (@{$self->weekends}) {
	      if ($holidate->day_of_week==$_) {
		last forHF 
	      }
	      if (dateinbetween($holidate)) {
		$days = $days - 1;
		
	      }
	    }


	  }
      }
      
	#added to the new release to also allow holidays as reference needs testing 
	if ($self->holidays) {
	  my $holidays = $self->holidays;
	  
	forHS: foreach(@$holidays)
	    {
	      my ($year , $month , $day) = split('-',$_);
	      my $holidate = DateTime->new(year=>$year,month=>$month,day=>$day);
	      #check if mentioned holiday lies in defined weekend , shouldnt deduct twice
	      foreach (@{$self->weekends}) {
		if ($holidate->day_of_week==$_) {
		  last forHS 
		}
		if (dateinbetween($holidate)) {
		  $days = $days - 1;
		
		}
	      }


	    }


      }      
      return $days;
      
  }          
      
      
    
sub gethours 
      {
	my $self=shift;
	my $days=$self->getdays;

	# (-2)To remove the start day and the last day as they may have different number
	#of working hours or none at all.
	$days -= 2;
	my $hoursinaday = $self->worktiming->[1]-$self->worktiming->[0];
	my $hours = $days * $hoursinaday;
	my $hoursinfirstday;
	my $hoursinlastday;
	
	# To calculate working hours in the first day.
	if ($self->datetime1->hour < $self->worktiming->[0]) {
	   $hoursinfirstday = $hoursinaday;
	}
	  elsif ($self->datetime1->hour > $self->worktiming->[1]) {
	    $hoursinfirstday = 0;
	    
	  }
	  else {
	    $hoursinfirstday = $self->worktiming->[1]-$self->datetime1->hour;
	    }
	# To calculate working hours in the last day  
	if ($self->datetime2->hour > $self->worktiming->[1]) {
	   $hoursinlastday = $hoursinaday;
	}
	
	  elsif ($self->datetime2->hour < $self->worktiming->[0]) {
	    $hoursinlastday = 0;
	    
	  }
	  else {
	    $hoursinlastday = $self->datetime2->hour-$self->worktiming->[0];
	    }
	  
	$hours = $hours + $hoursinfirstday + $hoursinlastday;
	return $hours;
	
      }
      
sub dateinbetween
	{
	my $self = shift;
	my $holidate = shift;
	#cant use >= and <= here as when it is true for  == and == condition,
	#months of the three dates  has to be checked for with similar conditions.
	#The same logic applies when checking for a date in between months 
	#and on equalty  goes down to comparision on days.An alternate method could have been 
	#converting the date to number of days from epouch and comparing them
	if ($holidate->year > $self->datetime1->year and $holidate->year <= $self->datetime2->year) {
	  return 1;
	  }
	if ($holidate->year >= $self->datetime1->year and $holidate->year < $self->datetime2->year) {
	  return 1;
	  }
	if ($holidate->year == $self->datetime1->year and $holidate->year == $self->datetime2->year) {
	  if ($holidate->month > $self->datetime1->month and $holidate->month <= $self->datetime2->month) {
	    return 1;
	     }
	  if ($holidate->month >= $self->datetime1->month and $holidate->month < $self->datetime2->month) {
	    return 1;
	    
	  }
	  if ($holidate->month == $self->datetime1->month and $holidate->month == $self->datetime2->month) {
	    if ($holidate->date >= $self->datetime1->day and $holidate->day <= $self->datetime2->day) {
	      return 1;
	    }
	  }
	}
	return 0;
	}
	
	1;

__END__


=head1 NAME

BusinessHours - An object that calculates business days and hours 

=head1 SYNOPSIS

  use BusinessHours;
  use DateTime;

  my $datetime1=DateTime->new(year=>2007,month=>10,day=>15);
  my $datetime2 = DateTime->now;

  my $testing = BusinessHours->new(datetime1=>$datetime1,
                                  datetime2=>$datetime2,
                                  worktiming=>[9,18], # 9 am to 6 om
                                  weekends=>[6,7], #saturday , sunday
                                  holidays=>["2007-10-31",2007-12-24]
                                  holidayfile=>'holidaylist' #holidaylist is a text file 
                                                             #with each date in a new line
                                                             #in the format yyyy-mm-dd  
                                 );

  print $testing->getdays."\n"; # the total business days 

  print $testing->gethours; # the total business hours


=head1 DESCRIPTION

BusinessHours a class for caculating the business hours between two DateTime objects.It can be useful in situations like escalation where an action has to happen after a certain number of business hours.

=head1 USAGE

Create an instance of the class with the two required datetime objects. 
and use the methods to get the business hours or days.

=head3 Constructors

=over 4

=item * new( ... )

This class method accepts the following parameters in the hash format.

            datetime1    =>  Starting Date 
            datetime2    =>  Ending Date
            worktimings  =>  A list reference with two values.Starting and ending hour of the day.
                             Defaults to [9,18] 
            weekends     =>  A list reference with values of the days that must be considered 
                             as non-working in a week.Defaults to [6,7] - Saturday , Sunday.
            holidays     =>  A list reference with holiday dates.
            holidayfile  =>  The name of a file from which predefined holidays can be excluded
                             from business days /hours calculation. Defaults to no file.


=head3  Methods

This class has two methods.

=over 4

=item * getdays

Returns the number of business days

=item * gethours

Returns the number of business hours.

=back

=head1 AUTHOR

Antano Solar John<solar345@gmail.com>


=head1 COPYRIGHT

Copyright (c) 2007 Antano Solar John.  All rights reserved.  This
program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.


The full text of the license can be found in the LICENSE file included
with this module.

=cut