Date::Span - deal with date/time ranges than span multiple dates


Date-Span documentation Contained in the Date-Span distribution.

Index


Code Index:

NAME

Top

Date::Span -- deal with date/time ranges than span multiple dates

VERSION

Top

version 1.125

SYNOPSIS

Top

 use Date::Span;

 @spanned = range_expand($start, $end);

 print "from $_->[0] to $_->[1]\n" for (@spanned);

DESCRIPTION

Top

This module provides code for dealing with datetime ranges that span multiple calendar days. This is useful for computing, for example, the amount of seconds spent performing a task on each day. Given the following table:

  event   | begun            | ended
 ---------+------------------+------------------
  loading | 2004-01-01 00:00 | 2004-01-01 12:45
  venting | 2004-01-01 12:45 | 2004-01-02 21:15
  running | 2004-01-02 21:15 | 2004-01-03 00:00

We may want to gather the following data:

  date       | event   | time spent
 ------------+---------+----------------
  2004-01-01 | loading | 12.75 hours
  2004-01-01 | venting | 11.25 hours
  2004-01-02 | venting | 21.25 hours
  2004-01-02 | running |  2.75 hours

Date::Span takes a data like the first and produces data more like the second. (Details on exact interface are below.)

FUNCTIONS

Top

range_durations($start, $end)

Given $start and $end as timestamps (in epoch seconds), range_durations returns a list of arrayrefs. Each arrayref is a date (expressed as epoch seconds at midnight) and the number of seconds for which the given range intersects with the date.

range_expand($start, $end)

Given $start and $end as timestamps (in epoch seconds), range_durations returns a list of arrayrefs. Each arrayref is a start and end timestamp. No pair of start and end times will cross a date boundary, and the set of ranges as a whole will be identical to the passed start and end.

range_from_unit(@date_unit)

@date_unit is a specification of a unit of time, in the form:

 @date_unit = ($year, $month, $day, $hour, $minute);

Only $year is mandatory; other arguments may be added, in order. Month is given in the range (0 .. 11). This function will return the first and last second of the given unit.

A code reference may be passed as the last object. It will be used to convert the date specification to a starting time. If no coderef is passed, a simple one using Time::Local (and timegm) will be used.

TODO

Top

This code was just yanked out of a general purpose set of utility functions I've compiled over the years. It should be refactored (internally) and further tested. The interface should stay pretty stable, though.

AUTHOR

Top

Ricardo SIGNES, <rjbs@cpan.org>

COPYRIGHT

Top


Date-Span documentation Contained in the Date-Span distribution.
use strict;
use warnings;

package Date::Span;

use Exporter;
BEGIN { our @ISA = 'Exporter' }

our @EXPORT = qw(range_expand range_durations range_from_unit); ## no critic

our $VERSION = '1.125';

sub _date_time { my $date = $_[0] - (my $time = $_[0] % 86400); ($date, $time) }

sub range_durations {
	my ($start, $end) = @_;
	return if $end < $start;

	my ($start_date, $start_time) = _date_time($start);
	my ($end_date,   $end_time)   = _date_time($end);

	push my @results, [
		$start_date,
		(( $end_date != $start_date ) ? ( 86400 - $start_time ) : ($end - $start))
	];

	push @results, 
		map { [ $start_date + 86400 * $_, 86400 ] }
		(1 .. ($end_date - $start_date - 86400) / 86400)
		if ($end_date - $start_date > 86400);

	push @results, [ $end_date, $end_time ] if $start_date != $end_date;

	return @results;
}

sub range_expand {
	my ($start, $end) = @_;
	return if $end < $start;

	my ($start_date, $start_time) = _date_time($start);
	my ($end_date,   $end_time)   = _date_time($end);

	push my @results, [
		$start, ( ( $end_date != $start_date ) ? ( $start_date + 86399 ) : $end )
	];

	push @results, 
		map { [ $start_date + 86400 * $_, $start_date + 86400 * $_ + 86399 ] }
		(1 .. ($end_date - $start_date - 86400) / 86400)
		if ($end_date - $start_date > 86400);

	push @results, [ $end_date, $end ] if $start_date != $end_date;

	return @results;
}

my @monthdays = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );

sub _is_leap {
	not($_[0] % 4) and (($_[0] % 100) or not($_[0] % 400)) and $_[0] > 0
}

sub _leap_secs {
  _is_leap($_[0]) && $_[1] == 1 ? 86400 : 0
}

sub _begin_secs {
	require Time::Local;
	Time::Local::timegm(
    0,        # $sec
    $_[4]||0, # $min
    $_[3]||0, # $hour
    $_[2]||1, # $mday
    $_[1]||0, # $mon
    $_[0]     # $year
  );
}

sub range_from_unit {
	my $code = (ref($_[-1])||'' eq 'CODE') ? pop : \&_begin_secs;
	return unless @_;
	my ($year,$month,$day,$hour,$min) = @_;
	my $begin_secs = $code->(@_);
	my $length = defined $min   ? 60
             : defined $hour  ? 3600
             : defined $day   ? 86400
             : defined $month ? 86400 * $monthdays[$month+0]
                              + _leap_secs($year, $month)
             :                  86400 * (_is_leap($year) ? 366 : 365);

	return ($begin_secs, $begin_secs + $length - 1);
}

1;