The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
package Date::Handler;

use strict;
use Carp;
use Data::Dumper;
use vars qw(@ISA $VERSION);

$VERSION = '1.2';

use POSIX qw(floor strftime mktime setlocale);

use Date::Handler::Constants;
use constant DEFAULT_FORMAT_STRING => '%c';
use constant DEFAULT_TIMEZONE => 'GMT';
use constant DEFAULT_LOCALE => 'en_US';

use constant DELTA_CLASS => 'Date::Handler::Delta';

use constant INTUITIVE_MONTH_CALCULATIONS => 0;
use constant INTUITIVE_TIME_CALCULATIONS => 0;
use constant INTUITIVE_DST_ADJUSTMENTS => 0;

use overload (
	'""'	=> 'AsScalar',
	'0+'	=> 'AsNumber',
	'+'	=>	'Add',
	'-'	=>	'Sub',
	'<=>'	=>	'Cmp',
	'++' => 'Incr',
	'*' => sub { croak "Cannot multiply an absolute date"; },
	'**' => sub { croak "Cannot power an absolute date"; },
	'/' => sub { croak "Cannot divide an absolute date"; },
	fallback	=> 1,
);


sub new
{
	my $classname = shift;
	my $args = shift;


	# Allow classic style arguments passing. # Thanks to Roland Rauch <roland@rauch.com> for the spot 
	unless (ref($args) eq "HASH") 
	{ 
		unshift(@_, $args);             
		$args = {@_};                
	}

	my $self = {};
	bless $self, $classname;

	croak "No args to new()" if not defined $args;
	croak "Argument to new() is not a hashref" if not ref($args) =~ /HASH/;
	croak "No date specified for new()" if not defined $args->{date};

	my $date = $args->{date};

	my $timezone = $args->{time_zone} || $self->DEFAULT_TIMEZONE();
	$self->TimeZone($timezone);

	$self->{locale} = "";
	if(defined $args->{locale})
	{
		$self->SetLocale($args->{locale}) || $self->SetLocale($self->DEFAULT_LOCALE());
	}
	else
	{
		$self->SetLocale($self->DEFAULT_LOCALE());
	}

	if(not defined $self->Locale())
	{
		warn "Impossible to set locale OR default locale correctly. Defaulting to GMT/UTC.";
		$self->SetLocale('GMT');
	}


	if(ref($date) =~ /SCALAR/)
	{
		if($date !~ /\s/ && $date !~ /[A-Za-z]/)
		{
			$self->{epoch} = $date;
		}
	}
	elsif(ref($date) =~ /ARRAY/)
	{
		$self->{epoch} = $self->Array2Epoch($date);
	}
	elsif(ref($date) =~ /HASH/)
	{
		$self->{epoch} = $self->Array2Epoch([ 
						$date->{year},
						$date->{month},
						$date->{day},
						$date->{hour},
						$date->{min},
						$date->{sec},
		]);
	}
	else
	{
		if($date !~ /\s/ && $date !~ /[A-Za-z]/)
		{
			$self->{epoch} = $date;
		}
	}

	$self->{_intuitive_day} = $args->{intuitive_day} if($self->INTUITIVE_MONTH_CALCULATIONS());
	$self->{_intuitive_hour} = $args->{intuitive_hour} if($self->INTUITIVE_TIME_CALCULATIONS());

	croak "Date format not recognized." if not defined $self->{epoch};

	return $self;
}	


#Accessors (Might want to optimised some of those)
sub Year { return shift->AsArray()->[0]; }
sub Day { return shift->AsArray()->[2]; }
sub Hour { return shift->AsArray()->[3]; }
sub Min { return shift->AsArray()->[4]; }
sub Sec { return shift->AsArray()->[5]; }


#To be consistent with our WeekDay function, wich is zero based.
sub Month
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return strftime('%m', localtime($self->{epoch}));
}

sub Epoch
{
	my $self = shift;

	if(@_)
	{
		my $epoch = shift;

		$self->{epoch} = $epoch;
	}

	return $self->{epoch};
}

sub TimeZone
{
	my $self = shift;
	
	if(@_)
	{
		my $time_zone = shift;
		$self->{time_zone} = $time_zone;
	}

	return $self->{time_zone};
}

sub Locale
{
	my $self = shift;

	if(@_)
	{
		warn "Calling Locale() with an argument to set the locale is deprecated. Please use SetLocale(locale) instead.\n";

		return $self->SetLocale(@_); 
	}

	return $self->{locale};
}

sub SetLocale
{
	my $self = shift;
	my $locale = shift;

	croak "No locale passed to SetLocale()" if not defined $locale;

	my $locale_return = POSIX::setlocale(&POSIX::LC_TIME, $locale);

	if( defined $locale_return )
	{
		$self->{locale} = $locale;
		$self->{locale_realname} = $locale_return;

		return $self->{locale};
	}

	print STDERR "Locale $locale does not seem to be implemented on this system, keeping locale ".$self->{locale}."\n";
	return undef;
}

	
sub LocaleRealName
{
	my $self = shift;

	return $self->{locale_realname} || $self->DEFAULT_LOCALE();
}
	
#Time Conversion and info methods 

sub TimeZoneName
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	#Old code.
	#my ($std,$dst) = POSIX::tzname();
	#return $std." / ".$dst;

	return strftime("%Z", localtime($self->{epoch}) );
}

sub LocalTime
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return localtime($self->{epoch});
}


sub TimeFormat
{
	my $self = shift;
	my $format_string = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();
		
	$format_string ||= $self->DEFAULT_FORMAT_STRING(); 

	return strftime($format_string, localtime($self->{epoch}));
}


sub GmtTime
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return gmtime($self->{epoch});
}

sub UtcTime
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return gmtime($self->{epoch});
}


#Idea and base code for this function from:
# Larry Rosler, February 13, 1999, Thanks Larry! -<bbeausej@pobox.com>

sub GmtOffset 
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	#Old code.
	#use Time::Local;
	#my $gmt_time = timegm( gmtime $self->{epoch} );
	#my $local_time = timelocal( gmtime $self->{epoch} );


	my $now = $self->Epoch();
	
	my ($l_min, $l_hour, $l_year, $l_yday) = (localtime $now)[1, 2, 5, 7];
	my ($g_min, $g_hour, $g_year, $g_yday) = (gmtime $now)[1, 2, 5, 7];

	return (($l_min - $g_min)/60 + $l_hour - $g_hour + 24 * ($l_year - $g_year || $l_yday - $g_yday)) * 3600;
}


#Useful methods
sub MonthName
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return strftime('%B', localtime($self->{epoch}));
}

sub WeekDay
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return strftime('%u', localtime($self->{epoch}));
}

sub WeekDayName
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return strftime('%A', localtime($self->{epoch}));
}

sub FirstWeekDayOfMonth
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();
		
	return (($self->WeekDay() - $self->Day() % 7) + 8) % 7;
}

sub WeekOfMonth
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return int(($self->Day() + $self->FirstWeekDayOfMonth() - 1) / 7) + 1;
}
	

sub DaysInMonth
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	my $month = $self->Month() - 1;

	if($month == 1) #Feb
	{
		return 29 if $self->IsLeapYear();
		return 28;
	}
	else
	{
		return $DAYS_IN_MONTH->{$month};
	}
}
	
sub DayLightSavings
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	my @self_localtime = localtime($self->{epoch});

	return $self_localtime[8]; 
}

sub DayOfYear
{
	my $self = shift;
	
	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();
	
	my @self_localtime = localtime($self->{epoch});

	return $self_localtime[7];
}

sub DaysInYear
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return 365 if !$self->IsLeapYear();
	return 366 if $self->IsLeapYear();
}

sub DaysLeftInYear
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	my $days = $self->DaysInYear();
	my $day = $self->DayOfYear();

	return $days - $day;
}	

sub LastDayOfMonth
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	if($self->Day() >= $self->DaysInMonth())
	{
		return 1;
	}
		
}

sub IsLeapYear
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	my $year = $self->Year();
 
	return 1 if( !($year % 400) );
	return 1 if( !($year %4) && ($year % 100) );
	return 0;
}

sub IntuitiveDay
{
	my $self = shift;
	my $intuitive_day = shift;

	if($intuitive_day)
	{
		$self->{_intuitive_day} = $intuitive_day;	
	}
	return $self->{_intuitive_day};
}

sub IntuitiveHour
{
	my $self = shift;
	my $intuitive_hour = shift;
	
	if($intuitive_hour)
	{
		$self->{_intuitive_hour} = $intuitive_hour;
	}
	return $self->{_intuitive_hour};
}

sub Array2Epoch
{
	my $self = shift;
	my $input = shift;

	my ($y,$m,$d,$h,$mm,$ss) = @{$input}[0,1,2,3,4,5];

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	return mktime(
							$ss || 0, 
							$mm || 0, 
							$h || 0, 
							$d || 1,
							($m || 1)-1,
							($y || 2000)-1900,
							0,
							0,
							-1);
}


#Oveload methods.

sub AsScalar { return shift->TimeFormat(shift); }
sub AsNumber { return shift->{epoch}; }

sub AsArray
{
	my $self = shift;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();


	my ($ss,$mm,$h,$d,$m,$y) = localtime($self->{epoch});
	$y += 1900;
	$m += 1;

	return [$y,$m,$d,$h,$mm,$ss];
}

sub AsHash
{
	my $self = shift;

	my $self_array = $self->AsArray();

	return {
				year => $self_array->[0],
				month => $self_array->[1],
				day => $self_array->[2],
				hour => $self_array->[3],
				min => $self_array->[4],
				sec => $self_array->[5],
	};
}


sub Add 
{
	my ($self, $delta) = @_;

	if(!ref($delta))
	{
		$delta = $self->DELTA_CLASS()->new([0,0,0,0,0,$delta]);
		return $self + $delta;
	}
	elsif($delta->isa($self->DELTA_CLASS()))
	{
		local $ENV{'TZ'} = $self->TimeZone();
		local $ENV{'LC_TIME'} = $self->Locale();


		my $epoch = $self->{epoch};

		my $newdate = ref($self)->new({ date => $epoch, time_zone => $self->TimeZone() });

		my $self_array = $newdate->AsArray();
		#Take care of the months.
		$self_array->[1] += $delta->Months();

		my $years = floor(($self_array->[1]-1)/12);
		$self_array->[1] -= 12*$years;

		#Take care of the years.
		$self_array->[0] += $years;

		my $posix_date = ref($self)->new({ date => $self_array,
						 time_zone => $self->TimeZone(), 
		});

		if($self->INTUITIVE_MONTH_CALCULATIONS())
		{
			if((($self->Month() + $delta->Months() - 1) % 12 + 1) != $posix_date->Month())
			{
				my $compensation_seconds = 86400 * $posix_date->Day();
				my $compensated_epoch = $posix_date->Epoch();
	
				$compensated_epoch -= $compensation_seconds;
	
				$posix_date->Epoch($compensated_epoch);
	
				$posix_date->{_intuitive_day} = $self->{_intuitive_day} || $self->Day();
			}
			else
			{
				if($self->{_intuitive_day})
				{
					my $lastdayofmonth = $self->{_intuitive_day};
					my $compensated_seconds = 86400 * ($lastdayofmonth - $posix_date->Day());
					if($compensated_seconds > 0)
					{
						my $epoch = $posix_date->Epoch();
						$epoch += $compensated_seconds;
						$posix_date->Epoch($epoch);
					}
	
					if($self->{_intuitive_day} > $lastdayofmonth)
					{
						$posix_date->{_intuitive_day} = $self->{_intuitive_day};
					}
					
				}
			}
		}

		#Take care of the seconds
		my $posix_epoch = $posix_date->Epoch();
		$posix_epoch += $delta->Seconds();
		$posix_date->Epoch($posix_epoch);


		my $adjustment_epoch = $posix_date->Epoch();
		my $add_intuitive_hour = 0;
		my $intuitive_hour;

		if($posix_date->DayLightSavings() && !$self->DayLightSavings())
		{
			my $posix_hour = $posix_date->Hour();
			$posix_hour -= 1;
			$intuitive_hour = $posix_hour;

			if($self->INTUITIVE_DST_ADJUSTMENTS())
			{
				$adjustment_epoch -= 3600;	
				$posix_date->Epoch($adjustment_epoch);

				$posix_hour = 0 if $posix_hour == 24;	
				if($posix_date->Hour() != $posix_hour)
				{
					$add_intuitive_hour = 1;
					$adjustment_epoch += 3600;
					$posix_date->Epoch($adjustment_epoch);
				}	
			}
		}
		elsif(!$posix_date->DayLightSavings() && $self->DayLightSavings())
		{

			my $posix_hour = $posix_date->Hour();
			$posix_hour += 1;
			$intuitive_hour = $posix_hour;

			if($self->INTUITIVE_DST_ADJUSTMENTS())
			{
				$adjustment_epoch += 3600;
				$posix_date->Epoch($adjustment_epoch);

				$posix_hour = 0 if $posix_hour == 24;	
				if($posix_date->Hour() != $posix_hour)
				{
					$add_intuitive_hour = 1;
					$adjustment_epoch -= 3600;
					$posix_date->Epoch($adjustment_epoch);
				}	
			}
		}

		if($self->INTUITIVE_TIME_CALCULATIONS())
		{
			if($add_intuitive_hour)
			{
				$posix_date->{_intuitive_hour} = $intuitive_hour;
			}

			if(defined $self->{_intuitive_hour})
			{
				my $hour = $posix_date->Hour();
				my $intuitive_epoch = $posix_date->Epoch();

				if($hour > $self->{_intuitive_hour})
				{		
					$intuitive_epoch -= 3600;
					$posix_date->Epoch($intuitive_epoch);
				}
				#elsif($hour < $self->{_intuitive_hour})
				#{
				#	print STDERR "Intuitive Adjust +1 hour\n";
				#	$intuitive_epoch += 3600;
				#	$posix_date->Epoch($intuitive_epoch);
				#}
			}
		}

		return $posix_date;

	}
	else
	{
		croak "Trying to add/substract an unknown object to a Date::Handler";
	}
}


sub Sub
{
	my ($self, $delta) = @_;

	if(!ref($delta))
	{
		$delta = $self->DELTA_CLASS()->new([0,0,0,0,0,$delta]);
		return $self - $delta;
	}
	elsif($delta->isa($self->DELTA_CLASS()))
	{
		return $self->Add(-$delta);
	}
	elsif($delta->isa('Date::Handler'))
	{

		my $seconds = $self->Epoch() - $delta->Epoch();

		if(($self->DayLightSavings() && !$delta->DayLightSavings()) ||
		   !$self->DayLightSavings() && $delta->DayLightSavings())	
		{
			$seconds += 3600;
		}

		return $self->DELTA_CLASS()->new($seconds);
	}
	else
	{
		croak "Cannot substract something else than a ".$self->DELTA_CLASS()." or Date::Handler or constant from a Date::Handler";
	}
}


sub Cmp 
{
	my ($self, $date, $reverse) = @_;

	my $cmp_date;

	if(!ref($date))
	{
		$cmp_date = $date;
	}
	elsif($date->isa('Date::Handler'))
	{
		$cmp_date = $date->{epoch};
	}
	elsif($date->isa($self->DELTA_CLASS()))
	{
		croak "Cannot compare a Date::Handler to a Delta.";
	}
	else
	{
		croak "Trying to compare a Date::Handler to an unknown object.";
	}
		
	return $self->{epoch} <=> $cmp_date;
}

sub Incr
{
	my ($self) = @_;

	my $epoch = $self->{epoch};
	$epoch++;

	return ref($self)->new({ date => $epoch, time_zone => $self->TimeZone() });
}


sub AllInfo
{
	my $self = shift;
	my $out_string;

	local $ENV{'TZ'} = $self->TimeZone();
	local $ENV{'LC_TIME'} = $self->Locale();

	$out_string .= "LocalTime: ".$self->LocalTime()."\n";
	$out_string .= "TimeFormat: ".$self->TimeFormat()."\n";
	$out_string .= "Epoch: ".$self->Epoch()."\n";
	$out_string .= "Locale: ".$self->Locale()."\n";
	$out_string .= "LocaleRealName: ".$self->LocaleRealName()."\n";
	$out_string .= "TimeZone: ".$self->TimeZone()." (".$self->TimeZoneName().")\n";
	$out_string .= "DayLightSavings: ".$self->DayLightSavings()."\n";
	$out_string .= "GMT Time: ".$self->GmtTime()."\n";	
	$out_string .= "GmtOffset: ".$self->GmtOffset()." (".($self->GmtOffset() / 60 / 60).")\n";
	$out_string .= "Year: ".$self->Year()."\n";
	$out_string .= "Month: ".$self->Month()."\n";
	$out_string .= "Day: ".$self->Day()."\n";
	$out_string .= "Hour: ".$self->Hour()."\n";
	$out_string .= "Min: ".$self->Min()."\n";
	$out_string .= "Sec: ".$self->Sec()."\n";
	$out_string .= "WeekDay: ".$self->WeekDay()."\n";
	$out_string .= "WeekDayName: ".$self->WeekDayName()."\n";
	$out_string .= "FirstWeekDayOfMonth: ".$self->FirstWeekDayOfMonth()."\n";
	$out_string .= "WeekOfMonth: ".$self->WeekOfMonth()."\n";
	$out_string .= "DayOfYear: ".$self->DayOfYear()."\n";
	$out_string .= "MonthName: ".$self->MonthName()."\n";
	$out_string .= "DaysInMonth: ".$self->DaysInMonth()."\n";
	$out_string .= "Leap Year: ".$self->IsLeapYear()."\n";
	$out_string .= "DaysInYear: ".$self->DaysInYear()."\n";
	$out_string .= "DaysLeftInYear: ".$self->DaysLeftInYear()."\n";
	$out_string .= "Intuitive Day: ".$self->IntuitiveDay()."\n";
	$out_string .= "Intuitive Hour: ".$self->IntuitiveHour()."\n";
	$out_string .= "\n\n";
	return $out_string;
}

666;
__END__