The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
###################################################
## Interval.pm		(Time::Interval)
## Andrew N. Hicox	<andrew@hicox.com>
## http://www.hicox.com
##
## a module for dealing with time intervals
###################################################


## Global Stuff ###################################
package	Time::Interval;
use	strict;
require	Exporter;

#class global vars ...
use vars qw($VERSION @EXPORT @ISA %intervals);
@ISA 		= qw(Exporter);
@EXPORT		= qw(&parseInterval &convertInterval &getInterval &coalesce);
$VERSION	= 1.234;
#what everything is worth in seconds
%intervals 	= (
	'days'		=> ((60**2) * 24),
	'hours'		=> (60 **2),
	'minutes'	=> 60,
	'seconds'	=> 1
);


## getInterval ####################################
sub getInterval {
	my $date1 = shift();
	my $date2 = shift();
	my $string = shift() || "";
	if ( (! $date1) || (! $date2) ){
		warn ("two dates are required for the getInterval method");
		return (undef);
	}
	require Date::Parse;
	foreach ($date1, $date2){
		$_ = Date::Parse::str2time($_) || do {
			warn ("failed to parse date: $!\n");
			return (undef);
		};
	}

	my %args = ( seconds => abs($date1 - $date2) );
	if ($string =~/^small/i){
		$args{'Small'} = 1;
	}elsif($string !~/^\s*$/){
		$args{'String'} = 1;
	}
	my $data = parseInterval(%args);
	return ($data);
}


## convertInterval ################################
#'days'		=> $num,
#'hours'	=> $num,
#'minutes'	=> $num,
#'seconds'	=> $num,
#'ConvertTo'	=> 'days'|'hours'|'minutes'|'seconds'
sub convertInterval {
	my %p = @_;
	#ConvertTo, default is seconds
	exists($p{'ConvertTo'}) || do {
		$p{'ConvertTo'} = "seconds";
		warn ("convertInterval: using default ConvertTo (seconds)") if $p{'Debug'};
	};
	#convert everything to seconds
	my $seconds = 0;
	foreach ("days","hours","minutes","seconds"){

		#new 1.233 hotness: quantize seconds.
		if ($_ eq "seconds"){ $p{$_} = int($p{$_}); }

		#as it were
		if (exists($p{$_})){ $seconds += ($intervals{$_} * $p{$_}); }
	}
	#send it back out into the desired output
	return (($seconds/$intervals{$p{'ConvertTo'}}));
}


## parseInterval ##################################
#'days'		=> $num,
#'hours'	=> $num,
#'minutes'	=> $num,
#'seconds'	=> $num,
sub parseInterval {
	my %p = @_;
	#convert everything to seconds
	my $seconds = convertInterval(%p);

	#new 1.233 hotness: quantize seconds.
	$seconds = int($seconds);

	#do the thang
	my %time = (
		'days'		=> 0,
		'hours'		=> 0,
		'minutes'	=> 0,
		'seconds'	=> 0
	);
	while ($seconds > 0){
		foreach ("days","hours","minutes","seconds"){
			if ($seconds >= $intervals{$_}){
				$time{$_} ++;
				$seconds  -= $intervals{$_};
				last;
			}
		}
	}
	#return data
        if ($p{'Small'} && $p{'Small'} != 0) {
		#return a string?
		my @temp = ();
		foreach ("days","hours","minutes","seconds"){
			if ($time{$_} > 0){
				push (@temp, "$time{$_}".substr($_,0,1));
			}
		}
        return join (" ", @temp) || "0s";
	}elsif ($p{'String'} && $p{'String'} != 0){
		#return a string?
		my @temp = ();
		foreach ("days","hours","minutes","seconds"){
			if ($time{$_} > 0){
                          if ($time{$_} == 1) {
				push (@temp, "$time{$_} ".substr($_,0,-1));
                              } else {
				push (@temp, "$time{$_} $_");
                              }
			}
		}
		return (join (", ", @temp)) || "0 seconds";
	}else{
		#return a data structure
		return (\%time);
	}
}


## coalesce #######################################
#coalesce([ [$start1, $end1], [$start2, $end2] ... ])
sub coalesce {
	require Date::Parse;
	my $intervals = shift() || [];
	my %epoch_map = ();
	my ($flag, $repeat) = (0,1);

	#convert each start / end to an epoch pair and stash 'em in epoch_map
	foreach my $int (@{$intervals}) {
		foreach (@{$int}){

			## only convert if it's not already epoch time
			my $epoch = "";
			if ($_ =~/^(\d{10})$/){
				$epoch = $1;
			}else{
				$epoch = Date::Parse::str2time($_);
			}
			$epoch_map{$epoch} = $_;
			$_ = $epoch;
		}
	}

	#sort 'em by start time
	@{$intervals} = sort { $a->[0] <=> $b->[0] } @{$intervals};

	#flatten 'em
	while ($repeat == 1) {
		@{$intervals} = sort {
			#if it's not an array ref, it's been destructo'd
			if ( (ref($a) eq "ARRAY") && (ref($b) eq "ARRAY") ){

				#if b is inside a
				if (($b->[0] >= $a->[0]) && ($b->[0] <= $a->[1])){
					#if b's end time is greater than a's, update a's end time
					if ($b->[1] > $a->[1]){ $a->[1] = $b->[1]; }
					#destructo b
					$b = ();
					$flag = 1;
					return (1);
				#if a is inside b
				}elsif (($a->[0] >= $b->[0]) && ($a->[0] <= $b->[1])){
					#if a's end time is greater than b's, update b's end time
					if ($a->[1] > $b->[1]){ $b->[1] = $a->[1]; }
					#destructo a
					$a = ();
					$flag = 1;
					return (0);
				}else{
					return (0);
				}

			}else{
				return (1);
			}

		} @{$intervals};

		#weed out null elements
		my $i = 0;
		foreach (@{$intervals}){
			if ( ref($_) ne "ARRAY" ){ splice (@{$intervals}, $i, 1); }
			$i ++;
		}

		#decide wether or not to repeat
		if ($flag == 1){
			$repeat = 1;
			$flag = 0;
		}else{
			$repeat = 0;
		}
	}

	#replace the epoch's with their time string equivalents
	foreach my $pair (@{$intervals}){
		if ( ref($pair) eq "ARRAY"){
			foreach (@{$pair}){ $_ = $epoch_map{$_}; }
		}
	}

	#weed out any remaining bum elements
	my $i = 0;
	foreach (@{$intervals}){
		unless (ref($_) eq "ARRAY"){ splice (@{$intervals}, $i, 1); }
		$i ++;
	}
	return ($intervals);
}