package Date::Manip::Date;
# Copyright (c) 1995-2017 Sullivan Beck. All rights reserved.
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.
########################################################################
# Any routine that starts with an underscore (_) is NOT intended for
# public use. They are for internal use in the the Date::Manip
# modules and are subject to change without warning or notice.
#
# ABSOLUTELY NO USER SUPPORT IS OFFERED FOR THESE ROUTINES!
########################################################################
use Date::Manip::Obj;
@ISA = ('Date::Manip::Obj');
require 5.010000;
use warnings;
use strict;
use integer;
use utf8;
use IO::File;
use Storable qw(dclone);
#use re 'debug';
use Date::Manip::Base;
use Date::Manip::TZ;
our $VERSION;
$VERSION='6.60';
END { undef $VERSION; }
########################################################################
# BASE METHODS
########################################################################
sub is_date {
return 1;
}
# Call this every time a new date is put in to make sure everything is
# correctly initialized.
#
sub _init {
my($self) = @_;
$$self{'err'} = '';
$$self{'data'} =
{
'set' => 0, # 1 if the date has been set
# 2 if the date is in the process of being set
# The date as input
'in' => '', # the string that was parsed (if any)
'zin' => '', # the timezone that was parsed (if any)
# The date in the parsed timezone
'date' => [], # the parsed date split
'def' => [0,0,0,0,0,0],
# 1 for each field that came from
# defaults rather than parsed
# '' for an implied field
'tz' => '', # the timezone of the date
'isdst' => '', # 1 if the date is in DST.
'offset' => [], # The offset from GMT
'abb' => '', # The timezone abbreviation.
'f' => {}, # fields used in printing a date
# The date in GMT
'gmt' => [], # the date converted to GMT
# The date in local timezone
'loc' => [], # the date converted to local timezone
};
return;
}
sub _init_args {
my($self) = @_;
my @args = @{ $$self{'args'} };
$self->parse(@args);
return;
}
sub input {
my($self) = @_;
return $$self{'data'}{'in'};
}
########################################################################
# DATE PARSING
########################################################################
sub parse {
my($self,$instring,@opts) = @_;
$self->_init();
my $noupdate = 0;
if (! $instring) {
$$self{'err'} = '[parse] Empty date string';
return 1;
}
my %opts = map { $_,1 } @opts;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
delete $$self{'data'}{'default_time'};
my($done,$y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off,$dow,$got_time,
$default_time,$firsterr);
ENCODING:
foreach my $string ($dmb->_encoding($instring)) {
$got_time = 0;
$default_time = 0;
# Put parse in a simple loop for an easy exit.
PARSE:
{
my(@tmp,$tmp);
$$self{'err'} = '';
# Check the standard date format
$tmp = $dmb->split('date',$string);
if (defined($tmp)) {
($y,$m,$d,$h,$mn,$s) = @$tmp;
$got_time = 1;
last PARSE;
}
# Parse ISO 8601 dates now (which may have a timezone).
if (! exists $opts{'noiso8601'}) {
($done,@tmp) = $self->_parse_datetime_iso8601($string,\$noupdate);
if ($done) {
($y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off) = @tmp;
$got_time = 1;
last PARSE;
}
}
# There's lots of ways that commas may be included. Remove
# them (unless it's preceded and followed by a digit in
# which case it's probably a fractional separator).
$string =~ s/(?<!\d),/ /g;
$string =~ s/,(?!\d)/ /g;
# Some special full date/time formats ('now', 'epoch')
if (! exists $opts{'nospecial'}) {
($done,@tmp) = $self->_parse_datetime_other($string,\$noupdate);
if ($done) {
($y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off) = @tmp;
$got_time = 1;
last PARSE;
}
}
# Parse (and remove) the time (and an immediately following timezone).
($got_time,@tmp) = $self->_parse_time('parse',$string,\$noupdate,%opts);
if ($got_time) {
($string,$h,$mn,$s,$tzstring,$zone,$abb,$off) = @tmp;
}
if (! $string) {
($y,$m,$d) = $self->_def_date($y,$m,$d,\$noupdate);
last;
}
# Parse (and remove) the day of week. Also, handle the simple DoW
# formats.
if (! exists $opts{'nodow'}) {
($done,@tmp) = $self->_parse_dow($string,\$noupdate);
if (@tmp) {
if ($done) {
($y,$m,$d) = @tmp;
$default_time = 1;
last PARSE;
} else {
($string,$dow) = @tmp;
}
}
}
$dow = 0 if (! $dow);
# At this point, the string might contain the following dates:
#
# OTHER
# OTHER ZONE / ZONE OTHER
# DELTA
# DELTA ZONE / ZONE DELTA
# HOLIDAY
# HOLIDAY ZONE / ZONE HOLIDAY
#
# ZONE is only allowed if it wasn't parsed with the time
# Unfortunately, there are some conflicts between zones and
# some other formats, so try parsing the entire string as a date.
(@tmp) = $self->_parse_date($string,$dow,\$noupdate,%opts);
if (@tmp) {
($y,$m,$d,$dow) = @tmp;
$default_time = 1;
last PARSE;
}
# Parse any timezone
if (! $tzstring) {
($string,@tmp) = $self->_parse_tz($string,\$noupdate);
($tzstring,$zone,$abb,$off) = @tmp if (@tmp);
last PARSE if (! $string);
}
# Try the remainder of the string as a date.
if ($tzstring) {
(@tmp) = $self->_parse_date($string,$dow,\$noupdate,%opts);
if (@tmp) {
($y,$m,$d,$dow) = @tmp;
$default_time = 1;
last PARSE;
}
}
# Parse deltas
#
# Occasionally, a delta is entered for a date (which is
# interpreted as the date relative to now). There can be some
# confusion between a date and a delta, but the most
# important conflicts are the ISO 8601 dates (many of which
# could be interpreted as a delta), but those have already
# been taken care of.
#
# We may have already gotten the time:
# 3 days ago at midnight UTC
# (we already stripped off the 'at midnight UTC' above).
#
# We also need to handle the sitution of a delta and a timezone.
# in 2 hours EST
# in 2 days EST
# but only if no time was entered.
if (! exists $opts{'nodelta'}) {
($done,@tmp) =
$self->_parse_delta($string,$dow,$got_time,$h,$mn,$s,\$noupdate);
if (@tmp) {
($y,$m,$d,$h,$mn,$s) = @tmp;
$got_time = 1;
$dow = '';
}
last PARSE if ($done);
# We'll also check the original string to see if it's a valid
# delta since some deltas may have interpreted part of it as
# a time or timezone.
($done,@tmp) =
$self->_parse_delta($instring,$dow,$got_time,$h,$mn,$s,\$noupdate);
if (@tmp) {
($y,$m,$d,$h,$mn,$s) = @tmp;
$got_time = 1;
$dow = '';
($tzstring,$zone,$abb,$off) = ();
}
last PARSE if ($done);
}
# Parse holidays
unless (exists $opts{'noholidays'}) {
($done,@tmp) =
$self->_parse_holidays($string,\$noupdate);
if (@tmp) {
($y,$m,$d) = @tmp;
}
last PARSE if ($done);
}
$$self{'err'} = '[parse] Invalid date string';
last PARSE;
}
# We got an error parsing this encoding of the string. It could
# be that it is a genuine error, or it may be that we simply
# need to try a different encoding. If ALL encodings fail, we'll
# return the error from the first one.
if ($$self{'err'}) {
if (! $firsterr) {
$firsterr = $$self{'err'};
}
next ENCODING;
}
# If we didn't get an error, this is the string to use.
last ENCODING;
}
if ($$self{'err'}) {
$$self{'err'} = $firsterr;
return 1;
}
# Make sure that a time is set
if (! $got_time) {
if ($default_time) {
if (exists $$self{'data'}{'default_time'}) {
($h,$mn,$s) = @{ $$self{'data'}{'default_time'} };
delete $$self{'data'}{'default_time'};
} elsif ($dmb->_config('defaulttime') eq 'midnight') {
($h,$mn,$s) = (0,0,0);
} else {
($h,$mn,$s) = $dmt->_now('time',$noupdate);
$noupdate = 1;
}
$got_time = 1;
} else {
($h,$mn,$s) = $self->_def_time(undef,undef,undef,\$noupdate);
}
}
$$self{'data'}{'set'} = 2;
return $self->_parse_check('parse',$instring,
$y,$m,$d,$h,$mn,$s,$dow,$tzstring,$zone,$abb,$off);
}
sub parse_time {
my($self,$string,@opts) = @_;
my %opts = map { $_,1 } @opts;
my $noupdate = 0;
if (! $string) {
$$self{'err'} = '[parse_time] Empty time string';
return 1;
}
my($y,$m,$d,$h,$mn,$s);
if ($$self{'err'}) {
$self->_init();
}
if ($$self{'data'}{'set'}) {
($y,$m,$d,$h,$mn,$s) = @{ $$self{'data'}{'date'} };
} else {
my $dmt = $$self{'tz'};
($y,$m,$d,$h,$mn,$s) = $dmt->_now('now',$noupdate);
$noupdate = 1;
}
my($tzstring,$zone,$abb,$off);
($h,$mn,$s,$tzstring,$zone,$abb,$off) =
$self->_parse_time('parse_time',$string,\$noupdate,%opts);
return 1 if ($$self{'err'});
$$self{'data'}{'set'} = 2;
return $self->_parse_check('parse_time','',
$y,$m,$d,$h,$mn,$s,'',$tzstring,$zone,$abb,$off);
}
sub parse_date {
my($self,$string,@opts) = @_;
my %opts = map { $_,1 } @opts;
my $noupdate = 0;
if (! $string) {
$$self{'err'} = '[parse_date] Empty date string';
return 1;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d,$h,$mn,$s);
if ($$self{'err'}) {
$self->_init();
}
if ($$self{'data'}{'set'}) {
($y,$m,$d,$h,$mn,$s) = @{ $$self{'data'}{'date'} };
} else {
($h,$mn,$s) = (0,0,0);
}
# Put parse in a simple loop for an easy exit.
my($done,@tmp,$dow);
PARSE:
{
# Parse ISO 8601 dates now
unless (exists $opts{'noiso8601'}) {
($done,@tmp) = $self->_parse_date_iso8601($string,\$noupdate);
if ($done) {
($y,$m,$d) = @tmp;
last PARSE;
}
}
(@tmp) = $self->_parse_date($string,undef,\$noupdate,%opts);
if (@tmp) {
($y,$m,$d,$dow) = @tmp;
last PARSE;
}
$$self{'err'} = '[parse_date] Invalid date string';
return 1;
}
return 1 if ($$self{'err'});
$y = $dmt->_fix_year($y);
$$self{'data'}{'set'} = 2;
return $self->_parse_check('parse_date','',$y,$m,$d,$h,$mn,$s,$dow);
}
sub _parse_date {
my($self,$string,$dow,$noupdate,%opts) = @_;
# There's lots of ways that commas may be included. Remove
# them.
#
# Also remove some words we should ignore.
$string =~ s/,/ /g;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $ign = (exists $$dmb{'data'}{'rx'}{'other'}{'ignore'} ?
$$dmb{'data'}{'rx'}{'other'}{'ignore'} :
$self->_other_rx('ignore'));
$string =~ s/$ign/ /g;
my $of = $+{'of'};
$string =~ s/\s*$//;
return () if (! $string);
my($done,$y,$m,$d,@tmp);
# Put parse in a simple loop for an easy exit.
PARSE:
{
# Parse (and remove) the day of week. Also, handle the simple DoW
# formats.
unless (exists $opts{'nodow'}) {
if (! defined($dow)) {
($done,@tmp) = $self->_parse_dow($string,$noupdate);
if (@tmp) {
if ($done) {
($y,$m,$d) = @tmp;
last PARSE;
} else {
($string,$dow) = @tmp;
}
}
$dow = 0 if (! $dow);
}
}
# Parse common dates
unless (exists $opts{'nocommon'}) {
(@tmp) = $self->_parse_date_common($string,$noupdate);
if (@tmp) {
($y,$m,$d) = @tmp;
last PARSE;
}
}
# Parse less common dates
unless (exists $opts{'noother'}) {
(@tmp) = $self->_parse_date_other($string,$dow,$of,$noupdate);
if (@tmp) {
($y,$m,$d,$dow) = @tmp;
last PARSE;
}
}
# Parse truncated dates
if (! $dow && ! $of) {
(@tmp) = $self->_parse_date_truncated($string,$noupdate);
if (@tmp) {
($y,$m,$d,$dow) = @tmp;
last PARSE;
}
}
return ();
}
return($y,$m,$d,$dow);
}
sub parse_format {
my($self,$format,$string) = @_;
$self->_init();
my $noupdate = 0;
if (! $string) {
$$self{'err'} = '[parse_format] Empty date string';
return 1;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($err,$re) = $self->_format_regexp($format);
return $err if ($err);
return 1 if ($string !~ $re);
my($y,$m,$d,$h,$mn,$s,
$mon_name,$mon_abb,$dow_name,$dow_abb,$dow_char,$dow_num,
$doy,$nth,$ampm,$epochs,$epocho,
$tzstring,$off,$abb,$zone,
$g,$w,$l,$u) =
@+{qw(y m d h mn s
mon_name mon_abb dow_name dow_abb dow_char dow_num doy
nth ampm epochs epocho tzstring off abb zone g w l u)};
while (1) {
# Get y/m/d/h/mn/s from:
# $epochs,$epocho
if (defined($epochs)) {
($y,$m,$d,$h,$mn,$s) = @{ $dmb->secs_since_1970($epochs) };
my $z;
if ($zone) {
$z = $dmt->_zone($zone);
return 'Invalid zone' if (! $z);
} elsif ($abb || $off) {
my $offset = $dmb->_delta_convert('offset',$off);
$z = $dmt->__zone([],$offset,'',$abb,'');
if (! $z) {
$z = $dmt->__zone([],$offset,$abb,'','');
}
return 'Invalid zone' if (! $z);
} else {
$z = $dmt->_now('tz',$noupdate);
$noupdate = 1;
}
my($err,$date) = $dmt->convert_from_gmt([$y,$m,$d,$h,$mn,$s],$z);
($y,$m,$d,$h,$mn,$s) = @$date;
last;
}
if (defined($epocho)) {
($y,$m,$d,$h,$mn,$s) = @{ $dmb->secs_since_1970($epocho) };
last;
}
# Get y/m/d from:
# $y,$m,$d,
# $mon_name,$mon_abb
# $doy,$nth
# $g/$w,$l/$u
if ($mon_name) {
$m = $$dmb{'data'}{'wordmatch'}{'month_name'}{lc($mon_name)};
} elsif ($mon_abb) {
$m = $$dmb{'data'}{'wordmatch'}{'month_abb'}{lc($mon_abb)};
}
if ($nth) {
$d = $$dmb{'data'}{'wordmatch'}{'nth'}{lc($nth)};
}
if ($doy) {
$y = $dmt->_now('y',$noupdate) if (! $y);
$noupdate = 1;
($y,$m,$d) = @{ $dmb->day_of_year($y,$doy) };
} elsif ($g) {
$y = $dmt->_now('y',$noupdate) if (! $y);
$noupdate = 1;
($y,$m,$d) = @{ $dmb->_week_of_year($g,$w,1) };
} elsif ($l) {
$y = $dmt->_now('y',$noupdate) if (! $y);
$noupdate = 1;
($y,$m,$d) = @{ $dmb->_week_of_year($l,$u,7) };
} elsif ($m) {
($y,$m,$d) = $self->_def_date($y,$m,$d,\$noupdate);
}
# Get h/mn/s from:
# $h,$mn,$s,$ampm
if (defined($h)) {
($h,$mn,$s) = $self->_def_time($h,$mn,$s,\$noupdate);
}
if ($ampm) {
if ($$dmb{'data'}{'wordmatch'}{'ampm'}{lc($ampm)} == 2) {
# pm times
$h+=12 unless ($h==12);
} else {
# am times
$h=0 if ($h==12);
}
}
# Get dow from:
# $dow_name,$dow_abb,$dow_char,$dow_num
if ($dow_name) {
$dow_num = $$dmb{'data'}{'wordmatch'}{'day_name'}{lc($dow_name)};
} elsif ($dow_abb) {
$dow_num = $$dmb{'data'}{'wordmatch'}{'day_abb'}{lc($dow_abb)};
} elsif ($dow_char) {
$dow_num = $$dmb{'data'}{'wordmatch'}{'day_char'}{lc($dow_char)};
}
last;
}
if (! $m) {
($y,$m,$d) = $dmt->_now('now',$noupdate);
$noupdate = 1;
}
if (! defined($h)) {
($h,$mn,$s) = (0,0,0);
}
$$self{'data'}{'set'} = 2;
$err = $self->_parse_check('parse_format',$string,
$y,$m,$d,$h,$mn,$s,$dow_num,
$tzstring,$zone,$abb,$off);
if (wantarray) {
my %tmp = %{ dclone(\%+) };
return ($err,%tmp);
}
return $err;
}
BEGIN {
my %y_form = map { $_,1 } qw( Y y s o G L );
my %m_form = map { $_,1 } qw( m f b h B j s o W U );
my %d_form = map { $_,1 } qw( j d e E s o W U );
my %h_form = map { $_,1 } qw( H I k i s o );
my %mn_form = map { $_,1 } qw( M s o );
my %s_form = map { $_,1 } qw( S s o );
my %dow_form = map { $_,1 } qw( v a A w );
my %am_form = map { $_,1 } qw( p s o );
my %z_form = map { $_,1 } qw( Z z N );
my %mon_form = map { $_,1 } qw( b h B );
my %day_form = map { $_,1 } qw( v a A );
sub _format_regexp {
my($self,$format) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
if (exists $$dmb{'data'}{'format'}{$format}) {
return @{ $$dmb{'data'}{'format'}{$format} };
}
my $re;
my $err;
my($y,$m,$d,$h,$mn,$s) = (0,0,0,0,0,0);
my($dow,$ampm,$zone,$G,$W,$L,$U) = (0,0,0,0,0,0,0);
while ($format) {
last if ($format eq '%');
if ($format =~ s/^([^%]+)//) {
$re .= $1;
next;
}
$format =~ s/^%(.)//;
my $f = $1;
if (exists $y_form{$f}) {
if ($y) {
$err = 'Year specified multiple times';
last;
}
$y = 1;
}
if (exists $m_form{$f}) {
if ($m) {
$err = 'Month specified multiple times';
last;
}
$m = 1;
}
if (exists $d_form{$f}) {
if ($d) {
$err = 'Day specified multiple times';
last;
}
$d = 1;
}
if (exists $h_form{$f}) {
if ($h) {
$err = 'Hour specified multiple times';
last;
}
$h = 1;
}
if (exists $mn_form{$f}) {
if ($mn) {
$err = 'Minutes specified multiple times';
last;
}
$mn = 1;
}
if (exists $s_form{$f}) {
if ($s) {
$err = 'Seconds specified multiple times';
last;
}
$s = 1;
}
if (exists $dow_form{$f}) {
if ($dow) {
$err = 'Day-of-week specified multiple times';
last;
}
$dow = 1;
}
if (exists $am_form{$f}) {
if ($ampm) {
$err = 'AM/PM specified multiple times';
last;
}
$ampm = 1;
}
if (exists $z_form{$f}) {
if ($zone) {
$err = 'Zone specified multiple times';
last;
}
$zone = 1;
}
if ($f eq 'G') {
if ($G) {
$err = 'G specified multiple times';
last;
}
$G = 1;
} elsif ($f eq 'W') {
if ($W) {
$err = 'W specified multiple times';
last;
}
$W = 1;
} elsif ($f eq 'L') {
if ($L) {
$err = 'L specified multiple times';
last;
}
$L = 1;
} elsif ($f eq 'U') {
if ($U) {
$err = 'U specified multiple times';
last;
}
$U = 1;
}
###
if ($f eq 'Y') {
$re .= '(?<y>\d\d\d\d)';
} elsif ($f eq 'y') {
$re .= '(?<y>\d\d)';
} elsif ($f eq 'm') {
$re .= '(?<m>\d\d)';
} elsif ($f eq 'f') {
$re .= '(?:(?<m>\d\d)| ?(?<m>\d))';
} elsif (exists $mon_form{$f}) {
my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0];
my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0];
$re .= "(?:(?<mon_name>$nam)|(?<mon_abb>$abb))";
} elsif ($f eq 'j') {
$re .= '(?<doy>\d\d\d)';
} elsif ($f eq 'd') {
$re .= '(?<d>\d\d)';
} elsif ($f eq 'e') {
$re .= '(?:(?<d>\d\d)| ?(?<d>\d))';
} elsif (exists $day_form{$f}) {
my $abb = $$dmb{'data'}{'rx'}{'day_abb'}[0];
my $name = $$dmb{'data'}{'rx'}{'day_name'}[0];
my $char = $$dmb{'data'}{'rx'}{'day_char'}[0];
$re .= "(?:(?<dow_name>$name)|(?<dow_abb>$abb)|(?<dow_char>$char))";
} elsif ($f eq 'w') {
$re .= '(?<dow_num>[1-7])';
} elsif ($f eq 'E') {
my $nth = $$dmb{'data'}{'rx'}{'nth'}[0];
$re .= "(?<nth>$nth)"
} elsif ($f eq 'H' || $f eq 'I') {
$re .= '(?<h>\d\d)';
} elsif ($f eq 'k' || $f eq 'i') {
$re .= '(?:(?<h>\d\d)| ?(?<h>\d))';
} elsif ($f eq 'p') {
my $ampm = $$dmb{data}{rx}{ampm}[0];
$re .= "(?<ampm>$ampm)";
} elsif ($f eq 'M') {
$re .= '(?<mn>\d\d)';
} elsif ($f eq 'S') {
$re .= '(?<s>\d\d)';
} elsif (exists $z_form{$f}) {
$re .= $dmt->_zrx('zrx');
} elsif ($f eq 's') {
$re .= '(?<epochs>\d+)';
} elsif ($f eq 'o') {
$re .= '(?<epocho>\d+)';
} elsif ($f eq 'G') {
$re .= '(?<g>\d\d\d\d)';
} elsif ($f eq 'W') {
$re .= '(?<w>\d\d)';
} elsif ($f eq 'L') {
$re .= '(?<l>\d\d\d\d)';
} elsif ($f eq 'U') {
$re .= '(?<u>\d\d)';
} elsif ($f eq 'c') {
$format = '%a %b %e %H:%M:%S %Y' . $format;
} elsif ($f eq 'C' || $f eq 'u') {
$format = '%a %b %e %H:%M:%S %Z %Y' . $format;
} elsif ($f eq 'g') {
$format = '%a, %d %b %Y %H:%M:%S %Z' . $format;
} elsif ($f eq 'D') {
$format = '%m/%d/%y' . $format;
} elsif ($f eq 'r') {
$format = '%I:%M:%S %p' . $format;
} elsif ($f eq 'R') {
$format = '%H:%M' . $format;
} elsif ($f eq 'T' || $f eq 'X') {
$format = '%H:%M:%S' . $format;
} elsif ($f eq 'V') {
$format = '%m%d%H%M%y' . $format;
} elsif ($f eq 'Q') {
$format = '%Y%m%d' . $format;
} elsif ($f eq 'q') {
$format = '%Y%m%d%H%M%S' . $format;
} elsif ($f eq 'P') {
$format = '%Y%m%d%H:%M:%S' . $format;
} elsif ($f eq 'O') {
$format = '%Y\\-%m\\-%dT%H:%M:%S' . $format;
} elsif ($f eq 'F') {
$format = '%A, %B %e, %Y' . $format;
} elsif ($f eq 'K') {
$format = '%Y-%j' . $format;
} elsif ($f eq 'J') {
$format = '%G-W%W-%w' . $format;
} elsif ($f eq 'x') {
if ($dmb->_config('dateformat') eq 'US') {
$format = '%m/%d/%y' . $format;
} else {
$format = '%d/%m/%y' . $format;
}
} elsif ($f eq 't') {
$re .= "\t";
} elsif ($f eq '%') {
$re .= '%';
} elsif ($f eq '+') {
$re .= '\\+';
}
}
if ($m != $d) {
$err = 'Date not fully specified';
} elsif ( ($h || $mn || $s) && (! $h || ! $mn) ) {
$err = 'Time not fully specified';
} elsif ($ampm && ! $h) {
$err = 'Time not fully specified';
} elsif ($G != $W) {
$err = 'G/W must both be specified';
} elsif ($L != $U) {
$err = 'L/U must both be specified';
}
if ($err) {
$$dmb{'data'}{'format'}{$format} = [$err];
return ($err);
}
$$dmb{'data'}{'format'}{$format} = [0, qr/$re/i];
return @{ $$dmb{'data'}{'format'}{$format} };
}
}
########################################################################
# DATE FORMATS
########################################################################
sub _parse_check {
my($self,$caller,$instring,
$y,$m,$d,$h,$mn,$s,$dow,$tzstring,$zone,$abb,$off) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Check day_of_week for validity BEFORE converting 24:00:00 to the
# next day
if ($dow) {
my $tmp = $dmb->day_of_week([$y,$m,$d]);
if ($tmp != $dow) {
$$self{'err'} = "[$caller] Day of week invalid";
return 1;
}
}
# Handle 24:00:00 times.
if ($h == 24) {
($h,$mn,$s) = (0,0,0);
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],1) };
}
if (! $dmb->check([$y,$m,$d,$h,$mn,$s])) {
$$self{'err'} = "[$caller] Invalid date";
return 1;
}
my $date = [$y+0,$m+0,$d+0,$h+0,$mn+0,$s+0];
#
# We need to check that the date is valid in a timezone. The
# timezone may be referred to with $zone, $abb, or $off, and
# unfortunately, $abb MAY be the name of an abbrevation OR a
# zone in a few cases.
#
my $zonename;
my $abbrev = (defined $abb ? lc($abb) : '');
my $offset = (defined $off ? $dmb->_delta_convert('offset',$off) : '');
my @tmp;
if (defined($zone)) {
$zonename = $dmt->_zone($zone);
if ($zonename) {
@tmp = $self->__parse_check($date,$zonename,$off,$abb);
}
} elsif (defined($abb) || defined($off)) {
$zonename = $dmt->__zone($date,$offset,'',$abbrev,'');
if ($zonename) {
@tmp = $self->__parse_check($date,$zonename,$off,$abb);
}
if (! @tmp && defined($abb)) {
my $tmp = $dmt->_zone($abb);
if ($tmp) {
$zonename = $tmp;
@tmp = $self->__parse_check($date,$zonename,$off,undef);
}
}
} else {
$zonename = $dmt->_now('tz');
if ($zonename) {
@tmp = $self->__parse_check($date,$zonename,$off,$abb);
}
}
if (! $zonename) {
if (defined($zone)) {
$$self{'err'} = "[$caller] Unable to determine timezone: $zone";
} else {
$$self{'err'} = "[$caller] Unable to determine timezone";
}
return 1;
}
if (! @tmp) {
$$self{'err'} = "[$caller] Invalid date in timezone";
return 1;
}
# Store the date
my($a,$o,$isdst) = @tmp;
$self->set('zdate',$zonename,$date,$isdst);
return 1 if ($$self{'err'});
$$self{'data'}{'in'} = $instring;
$$self{'data'}{'zin'} = $zone if (defined($zone));
return 0;
}
sub __parse_check {
my($self,$date,$zonename,$off,$abb) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
if (defined ($off)) {
$off = $dmb->split('offset',$off);
}
foreach my $isdst (0,1) {
my $per = $dmt->date_period($date,$zonename,1,$isdst);
next if (! $per);
my $a = $$per[4];
my $o = $$per[3];
# If $abb is defined, it must match.
next if (defined $abb && lc($a) ne lc($abb));
# If $off is defined, it must match.
if (defined ($off)) {
next if ($$off[0] != $$o[0] ||
$$off[1] != $$o[1] ||
$$off[2] != $$o[2]);
}
return ($a,$o,$isdst);
}
return ();
}
# Set up the regular expressions for ISO 8601 parsing. Returns the
# requested regexp. $rx can be:
# cdate : regular expression for a complete date
# tdate : regular expression for a truncated date
# ctime : regular expression for a complete time
# ttime : regular expression for a truncated time
# date : regular expression for a date only
# time : regular expression for a time only
# UNDEF : regular expression for a valid date and/or time
#
# Date matches are:
# y m d doy w dow yod c
# Time matches are:
# h h24 mn s fh fm
#
sub _iso8601_rx {
my($self,$rx) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
return $$dmb{'data'}{'rx'}{'iso'}{$rx}
if (exists $$dmb{'data'}{'rx'}{'iso'}{$rx});
if ($rx eq 'cdate' || $rx eq 'tdate') {
my $y4 = '(?<y>\d\d\d\d)';
my $y2 = '(?<y>\d\d)';
my $m = '(?<m>0[1-9]|1[0-2])';
my $d = '(?<d>0[1-9]|[12][0-9]|3[01])';
my $doy = '(?<doy>00[1-9]|0[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])';
my $w = '(?<w>0[1-9]|[1-4][0-9]|5[0-3])';
my $dow = '(?<dow>[1-7])';
my $yod = '(?<yod>\d)';
my $cc = '(?<c>\d\d)';
my @cdaterx =
(
"${y4}${m}${d}", # CCYYMMDD
"${y4}\\-${m}\\-${d}", # CCYY-MM-DD
"\\-${y2}${m}${d}", # -YYMMDD
"\\-${y2}\\-${m}\\-${d}", # -YY-MM-DD
"\\-?${y2}${m}${d}", # YYMMDD
"\\-?${y2}\\-${m}\\-${d}", # YY-MM-DD
"\\-\\-${m}\\-?${d}", # --MM-DD --MMDD
"\\-\\-\\-${d}", # ---DD
"${y4}\\-?${doy}", # CCYY-DoY CCYYDoY
"\\-?${y2}\\-?${doy}", # YY-DoY -YY-DoY
# YYDoY -YYDoY
"\\-${doy}", # -DoY
"${y4}W${w}${dow}", # CCYYWwwD
"${y4}\\-W${w}\\-${dow}", # CCYY-Www-D
"\\-?${y2}W${w}${dow}", # YYWwwD -YYWwwD
"\\-?${y2}\\-W${w}\\-${dow}", # YY-Www-D -YY-Www-D
"\\-?${yod}W${w}${dow}", # YWwwD -YWwwD
"\\-?${yod}\\-W${w}\\-${dow}", # Y-Www-D -Y-Www-D
"\\-W${w}\\-?${dow}", # -Www-D -WwwD
"\\-W\\-${dow}", # -W-D
"\\-\\-\\-${dow}", # ---D
);
my $cdaterx = join('|',@cdaterx);
$cdaterx = qr/(?:$cdaterx)/i;
my @tdaterx =
(
"${y4}\\-${m}", # CCYY-MM
"${y4}", # CCYY
"\\-${y2}\\-?${m}", # -YY-MM -YYMM
"\\-${y2}", # -YY
"\\-\\-${m}", # --MM
"${y4}\\-?W${w}", # CCYYWww CCYY-Www
"\\-?${y2}\\-?W${w}", # YY-Www YYWww
# -YY-Www -YYWww
"\\-?W${w}", # -Www Www
"${cc}", # CC
);
my $tdaterx = join('|',@tdaterx);
$tdaterx = qr/(?:$tdaterx)/i;
$$dmb{'data'}{'rx'}{'iso'}{'cdate'} = $cdaterx;
$$dmb{'data'}{'rx'}{'iso'}{'tdate'} = $tdaterx;
} elsif ($rx eq 'ctime' || $rx eq 'ttime') {
my $hh = '(?<h>[0-1][0-9]|2[0-3])';
my $mn = '(?<mn>[0-5][0-9])';
my $ss = '(?<s>[0-5][0-9])';
my $h24a = '(?<h24>24(?::00){0,2})';
my $h24b = '(?<h24>24(?:00){0,2})';
my $h = '(?<h>[0-9])';
my $fh = '(?:[\.,](?<fh>\d*))'; # fractional hours (keep)
my $fm = '(?:[\.,](?<fm>\d*))'; # fractional seconds (keep)
my $fs = '(?:[\.,]\d*)'; # fractional hours (discard)
my $zrx = $dmt->_zrx('zrx');
my @ctimerx =
(
"${hh}${mn}${ss}${fs}?", # HHMNSS[,S+]
"${hh}:${mn}:${ss}${fs}?", # HH:MN:SS[,S+]
"${hh}:?${mn}${fm}", # HH:MN,M+ HHMN,M+
"${hh}${fh}", # HH,H+
"\\-${mn}:?${ss}${fs}?", # -MN:SS[,S+] -MNSS[,S+]
"\\-${mn}${fm}", # -MN,M+
"\\-\\-${ss}${fs}?", # --SS[,S+]
"${hh}:?${mn}", # HH:MN HHMN
"${h24a}", # 24:00:00 24:00 24
"${h24b}", # 240000 2400
"${h}:${mn}:${ss}${fs}?", # H:MN:SS[,S+]
"${h}:${mn}${fm}", # H:MN,M+
);
my $ctimerx = join('|',@ctimerx);
$ctimerx = qr/(?:$ctimerx)(?:\s*$zrx)?/;
my @ttimerx =
(
"${hh}", # HH
"\\-${mn}", # -MN
);
my $ttimerx = join('|',@ttimerx);
$ttimerx = qr/(?:$ttimerx)/;
$$dmb{'data'}{'rx'}{'iso'}{'ctime'} = $ctimerx;
$$dmb{'data'}{'rx'}{'iso'}{'ttime'} = $ttimerx;
} elsif ($rx eq 'date') {
my $cdaterx = $self->_iso8601_rx('cdate');
my $tdaterx = $self->_iso8601_rx('tdate');
$$dmb{'data'}{'rx'}{'iso'}{'date'} = qr/(?:$cdaterx|$tdaterx)/;
} elsif ($rx eq 'time') {
my $ctimerx = $self->_iso8601_rx('ctime');
my $ttimerx = $self->_iso8601_rx('ttime');
$$dmb{'data'}{'rx'}{'iso'}{'time'} = qr/(?:$ctimerx|$ttimerx)/;
} elsif ($rx eq 'fulldate') {
# A parseable string contains:
# a complete date and complete time
# a complete date and truncated time
# a truncated date
# a complete time
# a truncated time
# If the string contains both a time and date, they may be adjacent
# or separated by:
# whitespace
# T (which must be followed by a number)
# a dash
my $cdaterx = $self->_iso8601_rx('cdate');
my $tdaterx = $self->_iso8601_rx('tdate');
my $ctimerx = $self->_iso8601_rx('ctime');
my $ttimerx = $self->_iso8601_rx('ttime');
my $sep = qr/(?:T|\-|\s*)/i;
my $daterx = qr/^\s*(?: $cdaterx(?:$sep(?:$ctimerx|$ttimerx))? |
$tdaterx |
$ctimerx |
$ttimerx
)\s*$/x;
$$dmb{'data'}{'rx'}{'iso'}{'fulldate'} = $daterx;
}
return $$dmb{'data'}{'rx'}{'iso'}{$rx};
}
sub _parse_datetime_iso8601 {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $daterx = $self->_iso8601_rx('fulldate');
my($y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off);
my($doy,$dow,$yod,$c,$w,$fh,$fm,$h24);
if ($string =~ $daterx) {
($y,$m,$d,$h,$mn,$s,$doy,$dow,$yod,$c,$w,$fh,$fm,$h24,
$tzstring,$zone,$abb,$off) =
@+{qw(y m d h mn s doy dow yod c w fh fm h24 tzstring zone abb off)};
if (defined $w || defined $dow) {
($y,$m,$d) = $self->_def_date_dow($y,$w,$dow,$noupdate);
} elsif (defined $doy) {
($y,$m,$d) = $self->_def_date_doy($y,$doy,$noupdate);
} else {
$y = $c . '00' if (defined $c);
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
}
($h,$mn,$s) = $self->_time($h,$mn,$s,$fh,$fm,$h24,undef,$noupdate);
} else {
return (0);
}
return (1,$y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off);
}
sub _parse_date_iso8601 {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $daterx = $self->_iso8601_rx('date');
my($y,$m,$d);
my($doy,$dow,$yod,$c,$w);
if ($string =~ /^$daterx$/) {
($y,$m,$d,$doy,$dow,$yod,$c,$w) =
@+{qw(y m d doy dow yod c w)};
if (defined $w || defined $dow) {
($y,$m,$d) = $self->_def_date_dow($y,$w,$dow,$noupdate);
} elsif (defined $doy) {
($y,$m,$d) = $self->_def_date_doy($y,$doy,$noupdate);
} else {
$y = $c . '00' if (defined $c);
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
}
} else {
return (0);
}
return (1,$y,$m,$d);
}
# Handle all of the time fields.
#
no integer;
sub _time {
my($self,$h,$mn,$s,$fh,$fm,$h24,$ampm,$noupdate) = @_;
if (defined($ampm) && $ampm) {
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
if ($$dmb{'data'}{'wordmatch'}{'ampm'}{lc($ampm)} == 2) {
# pm times
$h+=12 unless ($h==12);
} else {
# am times
$h=0 if ($h==12);
}
}
if (defined $h24) {
return(24,0,0);
} elsif (defined $fh && $fh ne "") {
$fh = "0.$fh";
$s = int($fh * 3600);
$mn = int($s/60);
$s -= $mn*60;
} elsif (defined $fm && $fm ne "") {
$fm = "0.$fm";
$s = int($fm*60);
}
($h,$mn,$s) = $self->_def_time($h,$mn,$s,$noupdate);
return($h,$mn,$s);
}
use integer;
# Set up the regular expressions for other date and time formats. Returns the
# requested regexp.
#
sub _other_rx {
my($self,$rx) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
$rx = '_' if (! defined $rx);
if ($rx eq 'time') {
my $h24 = '(?<h>2[0-3]|1[0-9]|0?[0-9])'; # 0-23 00-23
my $h12 = '(?<h>1[0-2]|0?[1-9])'; # 1-12 01-12
my $mn = '(?<mn>[0-5][0-9])'; # 00-59
my $ss = '(?<s>[0-5][0-9])'; # 00-59
# how to express fractions
my($f1,$f2,$sepfr);
if (exists $$dmb{'data'}{'rx'}{'sepfr'} &&
$$dmb{'data'}{'rx'}{'sepfr'}) {
$sepfr = $$dmb{'data'}{'rx'}{'sepfr'};
} else {
$sepfr = '';
}
if ($sepfr) {
$f1 = "(?:[.,]|$sepfr)";
$f2 = "(?:[.,:]|$sepfr)";
} else {
$f1 = "[.,]";
$f2 = "[.,:]";
}
my $fh = "(?:$f1(?<fh>\\d*))"; # fractional hours (keep)
my $fm = "(?:$f1(?<fm>\\d*))"; # fractional minutes (keep)
my $fs = "(?:$f2\\d*)"; # fractional seconds
# AM/PM
my($ampm);
if (exists $$dmb{'data'}{'rx'}{'ampm'}) {
$ampm = "(?:\\s*(?<ampm>$$dmb{data}{rx}{ampm}[0]))";
}
# H:MN and MN:S separators
my @hm = ("\Q:\E");
my @ms = ("\Q:\E");
if ($dmb->_config('periodtimesep')) {
push(@hm,"\Q.\E");
push(@ms,"\Q.\E");
}
if (exists $$dmb{'data'}{'rx'}{'sephm'} &&
defined $$dmb{'data'}{'rx'}{'sephm'} &&
exists $$dmb{'data'}{'rx'}{'sepms'} &&
defined $$dmb{'data'}{'rx'}{'sepms'}) {
push(@hm,@{ $$dmb{'data'}{'rx'}{'sephm'} });
push(@ms,@{ $$dmb{'data'}{'rx'}{'sepms'} });
}
# How to express the time
# matches = (H, FH, MN, FMN, S, AM, TZSTRING, ZONE, ABB, OFF, ABB)
my @timerx;
for (my $i=0; $i<=$#hm; $i++) {
my $hm = $hm[$i];
my $ms = $ms[$i];
push(@timerx,
"${h12}$hm${mn}$ms${ss}${fs}?${ampm}?", # H12:MN:SS[,S+] [AM]
) if ($ampm);
push(@timerx,
"${h24}$hm${mn}$ms${ss}${fs}?", # H24:MN:SS[,S+]
"(?<h>24)$hm(?<mn>00)$ms(?<s>00)", # 24:00:00
);
}
for (my $i=0; $i<=$#hm; $i++) {
my $hm = $hm[$i];
my $ms = $ms[$i];
push(@timerx,
"${h12}$hm${mn}${fm}${ampm}?", # H12:MN,M+ [AM]
) if ($ampm);
push(@timerx,
"${h24}$hm${mn}${fm}", # H24:MN,M+
);
}
for (my $i=0; $i<=$#hm; $i++) {
my $hm = $hm[$i];
my $ms = $ms[$i];
push(@timerx,
"${h12}$hm${mn}${ampm}?", # H12:MN [AM]
) if ($ampm);
push(@timerx,
"${h24}$hm${mn}", # H24:MN
"(?<h>24)$hm(?<mn>00)", # 24:00
);
}
push(@timerx,
"${h12}${fh}${ampm}", # H12,H+ AM
"${h12}${ampm}", # H12 AM
) if ($ampm);
push(@timerx,
"${h24}${fh}", # H24,H+
);
my $timerx = join('|',@timerx);
my $zrx = $dmt->_zrx('zrx');
my $at = $$dmb{'data'}{'rx'}{'at'};
my $atrx = qr/(?:^|\s+)(?:$at)\s+/;
$timerx = qr/(?:$atrx|^|\s+)(?:$timerx)(?:\s*$zrx)?(?:\s+|$)/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $timerx;
} elsif ($rx eq 'common_1') {
# These are of the format M/D/Y
# Do NOT replace <m> and <d> with a regular expression to
# match 1-12 since the DateFormat config may reverse the two.
my $y4 = '(?<y>\d\d\d\d)';
my $y2 = '(?<y>\d\d)';
my $m = '(?<m>\d\d?)';
my $d = '(?<d>\d\d?)';
my $sep = '(?<sep>[\s\.\/\-])';
my @daterx =
(
"${m}${sep}${d}\\k<sep>$y4", # M/D/YYYY
"${m}${sep}${d}\\k<sep>$y2", # M/D/YY
"${m}${sep}${d}", # M/D
);
my $daterx = join('|',@daterx);
$daterx = qr/^\s*(?:$daterx)\s*$/;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $daterx;
} elsif ($rx eq 'common_2') {
my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0];
my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0];
my $y4 = '(?<y>\d\d\d\d)';
my $y2 = '(?<y>\d\d)';
my $m = '(?<m>\d\d?)';
my $d = '(?<d>\d\d?)';
my $dd = '(?<d>\d\d)';
my $mmm = "(?:(?<mmm>$abb)|(?<month>$nam))";
my $sep = '(?<sep>[\s\.\/\-])';
my $format_mmmyyyy = $dmb->_config('format_mmmyyyy');
my @daterx = ();
push(@daterx,
"${y4}${sep}${m}\\k<sep>$d", # YYYY/M/D
"${mmm}\\s*${dd}\\s*${y4}", # mmmDDYYYY
);
push(@daterx,
"${mmm}\\s*${dd}\\s*${y2}", # mmmDDYY
) if (! $format_mmmyyyy);
push(@daterx,
"${mmm}\\s*${d}", # mmmD
"${d}\\s*${mmm}\\s*${y4}", # DmmmYYYY
"${d}\\s*${mmm}\\s*${y2}", # DmmmYY
"${d}\\s*${mmm}", # Dmmm
"${y4}\\s*${mmm}\\s*${d}", # YYYYmmmD
"${mmm}${sep}${d}\\k<sep>${y4}", # mmm/D/YYYY
"${mmm}${sep}${d}\\k<sep>${y2}", # mmm/D/YY
"${mmm}${sep}${d}", # mmm/D
"${d}${sep}${mmm}\\k<sep>${y4}", # D/mmm/YYYY
"${d}${sep}${mmm}\\k<sep>${y2}", # D/mmm/YY
"${d}${sep}${mmm}", # D/mmm
"${y4}${sep}${mmm}\\k<sep>${d}", # YYYY/mmm/D
"${mmm}${sep}?${d}\\s+${y2}", # mmmD YY mmm/D YY
"${mmm}${sep}?${d}\\s+${y4}", # mmmD YYYY mmm/D YYYY
"${d}${sep}?${mmm}\\s+${y2}", # Dmmm YY D/mmm YY
"${d}${sep}?${mmm}\\s+${y4}", # Dmmm YYYY D/mmm YYYY
"${y2}\\s+${mmm}${sep}?${d}", # YY mmmD YY mmm/D
"${y4}\\s+${mmm}${sep}?${d}", # YYYY mmmD YYYY mmm/D
"${y2}\\s+${d}${sep}?${mmm}", # YY Dmmm YY D/mmm
"${y4}\\s+${d}${sep}?${mmm}", # YYYY Dmmm YYYY D/mmm
"${y4}:${m}:${d}", # YYYY:MM:DD
);
my $daterx = join('|',@daterx);
$daterx = qr/^\s*(?:$daterx)\s*$/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $daterx;
} elsif ($rx eq 'truncated') {
my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0];
my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0];
my $y4 = '(?<y>\d\d\d\d)';
my $mmm = "(?:(?<mmm>$abb)|(?<month>$nam))";
my $sep = '(?<sep>[\s\.\/\-])';
my $format_mmmyyyy = $dmb->_config('format_mmmyyyy');
my @daterx = ();
push(@daterx,
"${mmm}\\s*${y4}", # mmmYYYY
"${y4}\\s*${mmm}", # YYYYmmm
"${y4}${sep}${mmm}", # YYYY/mmm
"${mmm}${sep}${y4}", # mmm/YYYY
) if ($format_mmmyyyy);
if (@daterx) {
my $daterx = join('|',@daterx);
$daterx = qr/^\s*(?:$daterx)\s*$/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $daterx;
} else {
$$dmb{'data'}{'rx'}{'other'}{$rx} = '';
}
} elsif ($rx eq 'dow') {
my $day_abb = $$dmb{'data'}{'rx'}{'day_abb'}[0];
my $day_name = $$dmb{'data'}{'rx'}{'day_name'}[0];
my $on = $$dmb{'data'}{'rx'}{'on'};
my $onrx = qr/(?:^|\s+)(?:$on)\s+/;
my $dowrx = qr/(?:$onrx|^|\s+)(?<dow>$day_name|$day_abb)($|\s+)/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $dowrx;
} elsif ($rx eq 'ignore') {
my $of = $$dmb{'data'}{'rx'}{'of'};
my $ignrx = qr/(?:^|\s+)(?<of>$of)(\s+|$)/;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $ignrx;
} elsif ($rx eq 'miscdatetime') {
my $special = $$dmb{'data'}{'rx'}{'offset_time'}[0];
$special = "(?<special>$special)";
my $secs = "(?<epoch>[-+]?\\d+)";
my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0];
my $mmm = "(?<mmm>$abb)";
my $y4 = '(?<y>\d\d\d\d)';
my $dd = '(?<d>\d\d)';
my $h24 = '(?<h>2[0-3]|[01][0-9])'; # 00-23
my $mn = '(?<mn>[0-5][0-9])'; # 00-59
my $ss = '(?<s>[0-5][0-9])'; # 00-59
my $offrx = $dmt->_zrx('offrx');
my $zrx = $dmt->_zrx('zrx');
my @daterx =
(
"${special}", # now
"${special}\\s+${zrx}", # now EDT
"epoch\\s+$secs", # epoch SECS
"epoch\\s+$secs\\s+${zrx}", # epoch SECS EDT
"${dd}\\/${mmm}\\/${y4}:${h24}:${mn}:${ss}\\s*${offrx}",
# Common log format: 10/Oct/2000:13:55:36 -0700
);
my $daterx = join('|',@daterx);
$daterx = qr/^\s*(?:$daterx)\s*$/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $daterx;
} elsif ($rx eq 'misc') {
my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0];
my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0];
my $next = $$dmb{'data'}{'rx'}{'nextprev'}[0];
my $last = $$dmb{'data'}{'rx'}{'last'};
my $yf = $$dmb{data}{rx}{fields}[1];
my $mf = $$dmb{data}{rx}{fields}[2];
my $wf = $$dmb{data}{rx}{fields}[3];
my $df = $$dmb{data}{rx}{fields}[4];
my $nth = $$dmb{'data'}{'rx'}{'nth'}[0];
my $nth_wom = $$dmb{'data'}{'rx'}{'nth_wom'}[0];
my $special = $$dmb{'data'}{'rx'}{'offset_date'}[0];
my $y = '(?:(?<y>\d\d\d\d)|(?<y>\d\d))';
my $mmm = "(?:(?<mmm>$abb)|(?<month>$nam))";
$next = "(?<next>$next)";
$last = "(?<last>$last)";
$yf = "(?<field_y>$yf)";
$mf = "(?<field_m>$mf)";
$wf = "(?<field_w>$wf)";
$df = "(?<field_d>$df)";
my $fld = "(?:$yf|$mf|$wf)";
$nth = "(?<nth>$nth)";
$nth_wom = "(?<nth>$nth_wom)";
$special = "(?<special>$special)";
my @daterx =
(
"${mmm}\\s+${nth}\\s*$y?", # Dec 1st [1970]
"${nth}\\s+${mmm}\\s*$y?", # 1st Dec [1970]
"$y\\s+${mmm}\\s+${nth}", # 1970 Dec 1st
"$y\\s+${nth}\\s+${mmm}", # 1970 1st Dec
"${next}\\s+${fld}", # next year, next month, next week
"${next}", # next friday
"${last}\\s+${mmm}\\s*$y?", # last friday in october 95
"${last}\\s+${df}\\s+${mmm}\\s*$y?",
# last day in october 95
"${last}\\s*$y?", # last friday in 95
"${nth_wom}\\s+${mmm}\\s*$y?", # nth DoW in MMM [YYYY]
"${nth}\\s*$y?", # nth DoW in [YYYY]
"${nth}\\s+$df\\s+${mmm}\\s*$y?",
# nth day in MMM [YYYY]
"${nth}\\s+${wf}\\s*$y?", # DoW Nth week [YYYY]
"${wf}\\s+(?<n>\\d+)\\s*$y?", # DoW week N [YYYY]
"${special}", # today, tomorrow
"${special}\\s+${wf}", # today week
# British: same as 1 week from today
"${nth}", # nth
"${wf}", # monday week
# British: same as 'in 1 week on monday'
);
my $daterx = join('|',@daterx);
$daterx = qr/^\s*(?:$daterx)\s*$/i;
$$dmb{'data'}{'rx'}{'other'}{$rx} = $daterx;
}
return $$dmb{'data'}{'rx'}{'other'}{$rx};
}
sub _parse_time {
my($self,$caller,$string,$noupdate,%opts) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($timerx,$h,$mn,$s,$fh,$fm,$h24,$ampm,$tzstring,$zone,$abb,$off);
my $got_time = 0;
# Check for ISO 8601 time
#
# This is only called via. parse_time (parse_date uses a regexp
# that matches a full ISO 8601 date/time instead of parsing them
# separately. Since some ISO 8601 times are a substring of non-ISO
# 8601 times (i.e. 12:30 is a substring of '12:30 PM'), we need to
# match entire strings here.
if ($caller eq 'parse_time') {
$timerx = (exists $$dmb{'data'}{'rx'}{'iso'}{'time'} ?
$$dmb{'data'}{'rx'}{'iso'}{'time'} :
$self->_iso8601_rx('time'));
if (! exists $opts{'noiso8601'}) {
if ($string =~ s/^\s*$timerx\s*$//) {
($h,$fh,$mn,$fm,$s,$ampm,$tzstring,$zone,$abb,$off) =
@+{qw(h fh mn fm s ampm tzstring zone abb off)};
($h,$mn,$s) = $self->_def_time($h,$mn,$s,$noupdate);
$h24 = 1 if ($h == 24 && $mn == 0 && $s == 0);
$string =~ s/\s*$//;
$got_time = 1;
}
}
}
# Make time substitutions (i.e. noon => 12:00:00)
if (! $got_time &&
! exists $opts{'noother'}) {
my @rx = @{ $$dmb{'data'}{'rx'}{'times'} };
shift(@rx);
foreach my $rx (@rx) {
if ($string =~ $rx) {
my $repl = $$dmb{'data'}{'wordmatch'}{'times'}{lc($1)};
$string =~ s/$rx/$repl/g;
}
}
}
# Check to see if there is a time in the string
if (! $got_time) {
$timerx = (exists $$dmb{'data'}{'rx'}{'other'}{'time'} ?
$$dmb{'data'}{'rx'}{'other'}{'time'} :
$self->_other_rx('time'));
if ($string =~ s/$timerx/ /) {
($h,$fh,$mn,$fm,$s,$ampm,$tzstring,$zone,$abb,$off) =
@+{qw(h fh mn fm s ampm tzstring zone abb off)};
($h,$mn,$s) = $self->_def_time($h,$mn,$s,$noupdate);
$h24 = 1 if ($h == 24 && $mn == 0 && $s == 0);
$string =~ s/\s*$//;
$got_time = 1;
}
}
# If we called this from $date->parse()
# returns the string and a list of time components
if ($caller eq 'parse') {
if ($got_time) {
($h,$mn,$s) = $self->_time($h,$mn,$s,$fh,$fm,$h24,$ampm,$noupdate);
return ($got_time,$string,$h,$mn,$s,$tzstring,$zone,$abb,$off);
} else {
return (0);
}
}
# If we called this from $date->parse_time()
if (! $got_time || $string) {
$$self{'err'} = "[$caller] Invalid time string";
return ();
}
($h,$mn,$s) = $self->_time($h,$mn,$s,$fh,$fm,$h24,$ampm,$noupdate);
return ($h,$mn,$s,$tzstring,$zone,$abb,$off);
}
# Parse common dates
sub _parse_date_common {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Since we want whitespace to be used as a separator, turn all
# whitespace into single spaces. This is necessary since the
# regexps do backreferences to make sure that separators are
# not mixed.
$string =~ s/\s+/ /g;
my $daterx = (exists $$dmb{'data'}{'rx'}{'other'}{'common_1'} ?
$$dmb{'data'}{'rx'}{'other'}{'common_1'} :
$self->_other_rx('common_1'));
if ($string =~ $daterx) {
my($y,$m,$d) = @+{qw(y m d)};
if ($dmb->_config('dateformat') ne 'US') {
($m,$d) = ($d,$m);
}
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
return($y,$m,$d);
}
$daterx = (exists $$dmb{'data'}{'rx'}{'other'}{'common_2'} ?
$$dmb{'data'}{'rx'}{'other'}{'common_2'} :
$self->_other_rx('common_2'));
if ($string =~ $daterx) {
my($y,$m,$d,$mmm,$month) = @+{qw(y m d mmm month)};
if ($mmm) {
$m = $$dmb{'data'}{'wordmatch'}{'month_abb'}{lc($mmm)};
} elsif ($month) {
$m = $$dmb{'data'}{'wordmatch'}{'month_name'}{lc($month)};
}
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
return($y,$m,$d);
}
return ();
}
# Parse truncated dates
sub _parse_date_truncated {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $daterx = (exists $$dmb{'data'}{'rx'}{'other'}{'truncated'} ?
$$dmb{'data'}{'rx'}{'other'}{'truncated'} :
$self->_other_rx('truncated'));
return () if (! $daterx);
# Since we want whitespace to be used as a separator, turn all
# whitespace into single spaces. This is necessary since the
# regexps do backreferences to make sure that separators are
# not mixed.
$string =~ s/\s+/ /g;
if ($string =~ $daterx) {
my($y,$mmm,$month) = @+{qw(y mmm month)};
my ($m,$d);
if ($mmm) {
$m = $$dmb{'data'}{'wordmatch'}{'month_abb'}{lc($mmm)};
} elsif ($month) {
$m = $$dmb{'data'}{'wordmatch'}{'month_name'}{lc($month)};
}
# Handle all of the mmmYYYY formats
if ($y && $m) {
my $format_mmmyyyy = $dmb->_config('format_mmmyyyy');
if ($format_mmmyyyy eq 'first') {
$d=1;
$$self{'data'}{'default_time'} = [0,0,0];
} else {
$d=$dmb->days_in_month($y,$m);
$$self{'data'}{'default_time'} = [23,59,59];
}
$$self{'data'}{'def'}[0] = '';
$$self{'data'}{'def'}[1] = '';
$$self{'data'}{'def'}[2] = 1;
return($y,$m,$d);
}
}
return ();
}
sub _parse_tz {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my($tzstring,$zone,$abb,$off);
my $rx = $dmt->_zrx('zrx');
if ($string =~ s/(?:^|\s)$rx(?:$|\s)/ /) {
($tzstring,$zone,$abb,$off) = @+{qw(tzstring zone abb off)};
return($string,$tzstring,$zone,$abb,$off);
}
return($string);
}
sub _parse_dow {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d,$dow);
# Remove the day of week
my $rx = (exists $$dmb{'data'}{'rx'}{'other'}{'dow'} ?
$$dmb{'data'}{'rx'}{'other'}{'dow'} :
$self->_other_rx('dow'));
if ($string =~ s/$rx/ /) {
$dow = $+{'dow'};
$dow = lc($dow);
$dow = $$dmb{'data'}{'wordmatch'}{'day_abb'}{$dow}
if (exists $$dmb{'data'}{'wordmatch'}{'day_abb'}{$dow});
$dow = $$dmb{'data'}{'wordmatch'}{'day_name'}{$dow}
if (exists $$dmb{'data'}{'wordmatch'}{'day_name'}{$dow});
} else {
return (0);
}
$string =~ s/\s*$//;
$string =~ s/^\s*//;
return (0,$string,$dow) if ($string);
# Handle the simple DoW format
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
my($w,$dow1);
($y,$w) = $dmb->week_of_year([$y,$m,$d]); # week of year
($y,$m,$d) = @{ $dmb->week_of_year($y,$w) }; # first day
$dow1 = $dmb->day_of_week([$y,$m,$d]); # DoW of first day
$dow1 -= 7 if ($dow1 > $dow);
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],$dow-$dow1) };
return(1,$y,$m,$d);
}
sub _parse_holidays {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d);
if (! exists $$dmb{'data'}{'rx'}{'holidays'}) {
return (0);
}
$string =~ s/\s*$//;
$string =~ s/^\s*//;
my $rx = $$dmb{'data'}{'rx'}{'holidays'};
if ($string =~ $rx) {
my $hol;
($y,$hol) = @+{qw(y holiday)};
$y = $dmt->_now('y',$noupdate) if (! $y);
$y += 0;
$self->_holidays($y-1);
$self->_holidays($y);
$self->_holidays($y+1);
return (0) if (! exists $$dmb{'data'}{'holidays'}{'yhols'}{$y+0}{$hol});
my ($y,$m,$d) = @{ $$dmb{'data'}{'holidays'}{'yhols'}{$y+0}{$hol} };
return(1,$y,$m,$d);
}
return (0);
}
no integer;
sub _parse_delta {
my($self,$string,$dow,$got_time,$h,$mn,$s,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d);
my $delta = $self->new_delta();
my $err = $delta->parse($string);
my $tz = $dmt->_now('tz');
my $isdst = $dmt->_now('isdst');
if (! $err) {
my($dy,$dm,$dw,$dd,$dh,$dmn,$ds) = @{ $$delta{'data'}{'delta'} };
# We can't handle a delta longer than 10000 years
if (abs($dy) > 10000 ||
abs($dm) > 12000 || # 10000*12
abs($dw) > 53000 || # 10000*53
abs($dh) > 87840000 || # 10000*366*24
abs($dmn) > 5270400000 || # 10000*366*24*60
abs($ds) > 316224000000) { # 10000*366*24*60*60
$$self{'err'} = '[parse] Delta too large';
return (1);
}
if ($got_time &&
($dh != 0 || $dmn != 0 || $ds != 0)) {
$$self{'err'} = '[parse] Two times entered or implied';
return (1);
}
if ($got_time) {
($y,$m,$d) = $self->_def_date($y,$m,$d,$noupdate);
} else {
($y,$m,$d,$h,$mn,$s) = $dmt->_now('now',$$noupdate);
$$noupdate = 1;
}
my $business = $$delta{'data'}{'business'};
my($date2,$offset,$abbrev);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta([$y,$m,$d,$h,$mn,$s],
[$dy,$dm,$dw,$dd,$dh,$dmn,$ds],
0,$business,$tz,$isdst);
($y,$m,$d,$h,$mn,$s) = @$date2;
if ($dow) {
if ($dd != 0 || $dh != 0 || $dmn != 0 || $ds != 0) {
$$self{'err'} = '[parse] Day of week not allowed';
return (1);
}
my($w,$dow1);
($y,$w) = $dmb->week_of_year([$y,$m,$d]); # week of year
($y,$m,$d) = @{ $dmb->week_of_year($y,$w) }; # first day
$dow1 = $dmb->day_of_week([$y,$m,$d]); # DoW of first day
$dow1 -= 7 if ($dow1 > $dow);
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],$dow-$dow1) };
}
return (1,$y,$m,$d,$h,$mn,$s);
}
return (0);
}
use integer;
sub _parse_datetime_other {
my($self,$string,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $rx = (exists $$dmb{'data'}{'rx'}{'other'}{'miscdatetime'} ?
$$dmb{'data'}{'rx'}{'other'}{'miscdatetime'} :
$self->_other_rx('miscdatetime'));
if ($string =~ $rx) {
my ($special,$epoch,$y,$mmm,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off) =
@+{qw(special epoch y mmm d h mn s tzstring zone abb off)};
if (defined($special)) {
my $delta = $$dmb{'data'}{'wordmatch'}{'offset_time'}{lc($special)};
my @delta = @{ $dmb->split('delta',$delta) };
my @date = $dmt->_now('now',$$noupdate);
my $tz = $dmt->_now('tz');
my $isdst = $dmt->_now('isdst');
$$noupdate = 1;
my($err,$date2,$offset,$abbrev);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta([@date],[@delta],0,0,$tz,$isdst);
if ($tzstring) {
$date2 = [] if (! defined $date2);
my $offset = (defined $off ? $dmb->_delta_convert('offset',$off) : '');
$zone = (defined $zone ? lc($zone) : '');
my $abbrev = (defined $abb ? lc($abb) : '');
# In some cases, a valid abbreviation is also a valid timezone
my $tmp = $dmt->__zone($date2,$offset,$zone,$abbrev,'');
if (! $tmp && $abbrev && ! $zone) {
$abbrev = $dmt->_zone($abbrev);
$tmp = $dmt->__zone($date2,$offset,$abbrev,'','') if ($abbrev);
}
$zone = $tmp;
return (0) if (! $zone);
my(@tmp) = $dmt->_convert('_parse_datetime_other',$date2,$tz,$zone);
$date2 = $tmp[1];
}
@date = @$date2;
return (1,@date,$tzstring,$zone,$abb,$off);
} elsif (defined($epoch)) {
my $date = [1970,1,1,0,0,0];
my @delta = (0,0,$epoch);
$date = $dmb->calc_date_time($date,\@delta);
my($err);
if ($tzstring) {
my $offset = (defined $off ? $dmb->_delta_convert('offset',$off) : '');
$zone = (defined $zone ? lc($zone) : '');
my $abbrev = (defined $abb ? lc($abb) : '');
# In some cases, a valid abbreviation is also a valid timezone
my $tmp = $dmt->__zone($date,$offset,$zone,$abbrev,'');
if (! $tmp && $abbrev && ! $zone) {
$abbrev = $dmt->_zone($abbrev);
$tmp = $dmt->__zone($date,$offset,$abbrev,'','') if ($abbrev);
}
$zone = $tmp;
return (0) if (! $zone);
($err,$date) = $dmt->convert_from_gmt($date,$zone);
} else {
($err,$date) = $dmt->convert_from_gmt($date);
}
return (1,@$date,$tzstring,$zone,$abb,$off);
} elsif (defined($y)) {
my $m = $$dmb{'data'}{'wordmatch'}{'month_abb'}{lc($mmm)};
return (1,$y,$m,$d,$h,$mn,$s,$tzstring,$zone,$abb,$off);
}
}
return (0);
}
sub _parse_date_other {
my($self,$string,$dow,$of,$noupdate) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d,$h,$mn,$s);
my $rx = (exists $$dmb{'data'}{'rx'}{'other'}{'misc'} ?
$$dmb{'data'}{'rx'}{'other'}{'misc'} :
$self->_other_rx('misc'));
my($mmm,$month,$nextprev,$last,$field_y,$field_m,$field_w,$field_d,$nth);
my($special,$got_m,$n,$got_y);
if ($string =~ $rx) {
($y,$mmm,$month,$nextprev,$last,$field_y,$field_m,$field_w,$field_d,$nth,
$special,$n) =
@+{qw(y mmm month next last field_y field_m field_w field_d
nth special n)};
if (defined($y)) {
$y = $dmt->_fix_year($y);
$got_y = 1;
return () if (! $y);
} else {
$y = $dmt->_now('y',$$noupdate);
$$noupdate = 1;
$got_y = 0;
$$self{'data'}{'def'}[0] = '';
}
if (defined($mmm)) {
$m = $$dmb{'data'}{'wordmatch'}{'month_abb'}{lc($mmm)};
$got_m = 1;
} elsif ($month) {
$m = $$dmb{'data'}{'wordmatch'}{'month_name'}{lc($month)};
$got_m = 1;
}
if ($nth) {
$nth = $$dmb{'data'}{'wordmatch'}{'nth'}{lc($nth)};
}
if ($got_m && $nth && ! $dow) {
# Dec 1st 1970
# 1st Dec 1970
# 1970 Dec 1st
# 1970 1st Dec
$d = $nth;
} elsif ($nextprev) {
my $next = 0;
my $sign = -1;
if ($$dmb{'data'}{'wordmatch'}{'nextprev'}{lc($nextprev)} == 1) {
$next = 1;
$sign = 1;
}
if ($field_y || $field_m || $field_w) {
# next/prev year/month/week
my(@delta);
if ($field_y) {
@delta = ($sign*1,0,0,0,0,0,0);
} elsif ($field_m) {
@delta = (0,$sign*1,0,0,0,0,0);
} else {
@delta = (0,0,$sign*1,0,0,0,0);
}
my @now = $dmt->_now('now',$$noupdate);
my $tz = $dmt->_now('tz');
my $isdst = $dmt->_now('isdst');
$$noupdate = 1;
my($err,$offset,$abbrev,$date2);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta([@now],[@delta],0,0,$tz,$isdst);
($y,$m,$d,$h,$mn,$s) = @$date2;
} elsif ($dow) {
# next/prev friday
my @now = $dmt->_now('now',$$noupdate);
$$noupdate = 1;
($y,$m,$d,$h,$mn,$s) = @{ $self->__next_prev(\@now,$next,$dow,0) };
$dow = 0;
} else {
return ();
}
} elsif ($last) {
if ($field_d && $got_m) {
# last day in october 95
$d = $dmb->days_in_month($y,$m);
} elsif ($dow && $got_m) {
# last friday in october 95
$d = $dmb->days_in_month($y,$m);
($y,$m,$d,$h,$mn,$s) =
@{ $self->__next_prev([$y,$m,$d,0,0,0],0,$dow,1) };
$dow = 0;
} elsif ($dow) {
# last friday in 95
($y,$m,$d,$h,$mn,$s) =
@{ $self->__next_prev([$y,12,31,0,0,0],0,$dow,0) };
} else {
return ();
}
} elsif ($nth && $dow && ! $field_w) {
if ($got_m) {
if ($of) {
# nth DoW of MMM [YYYY]
return () if ($nth > 5);
$d = 1;
($y,$m,$d,$h,$mn,$s) =
@{ $self->__next_prev([$y,$m,1,0,0,0],1,$dow,1) };
my $m2 = $m;
($y,$m2,$d) = @{ $dmb->calc_date_days([$y,$m,$d],7*($nth-1)) }
if ($nth > 1);
return () if (! $m2 || $m2 != $m);
} else {
# DoW, nth MMM [YYYY] (i.e. Sunday, 9th Dec 2008)
$d = $nth;
}
} else {
# nth DoW [in YYYY]
($y,$m,$d,$h,$mn,$s) = @{ $self->__next_prev([$y,1,1,0,0,0],1,$dow,1) };
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],7*($nth-1)) }
if ($nth > 1);
}
} elsif ($field_w && $dow) {
if (defined($n) || $nth) {
# sunday week 22 in 1996
# sunday 22nd week in 1996
$n = $nth if ($nth);
return () if (! $n);
($y,$m,$d) = @{ $dmb->week_of_year($y,$n) };
($y,$m,$d) = @{ $self->__next_prev([$y,$m,$d,0,0,0],1,$dow,1) };
} else {
# DoW week
($y,$m,$d) = $dmt->_now('now',$$noupdate);
$$noupdate = 1;
my $tmp = $dmb->_config('firstday');
($y,$m,$d) = @{ $self->__next_prev([$y,$m,$d,0,0,0],1,$tmp,0) };
($y,$m,$d) = @{ $self->__next_prev([$y,$m,$d,0,0,0],1,$dow,1) };
}
} elsif ($nth && ! $got_y) {
# 'in one week' makes it here too so return nothing in that case so it
# drops through to the deltas.
return () if ($field_d || $field_w || $field_m || $field_y);
($y,$m,$d) = $dmt->_now('now',$$noupdate);
$$noupdate = 1;
$d = $nth;
} elsif ($special) {
my $delta = $$dmb{'data'}{'wordmatch'}{'offset_date'}{lc($special)};
my @delta = @{ $dmb->split('delta',$delta) };
($y,$m,$d) = $dmt->_now('now',$$noupdate);
my $tz = $dmt->_now('tz');
my $isdst = $dmt->_now('isdst');
$$noupdate = 1;
my($err,$offset,$abbrev,$date2);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta([$y,$m,$d,0,0,0],[@delta],0,0,$tz,$isdst);
($y,$m,$d) = @$date2;
if ($field_w) {
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],7) };
}
}
} else {
return ();
}
return($y,$m,$d,$dow);
}
# Supply defaults for missing values (Y/M/D)
sub _def_date {
my($self,$y,$m,$d,$noupdate) = @_;
$y = '' if (! defined $y);
$m = '' if (! defined $m);
$d = '' if (! defined $d);
my $defined = 0;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# If year was not specified, defaults to current year.
#
# We'll also fix the year (turn 2-digit into 4-digit).
if ($y eq '') {
$y = $dmt->_now('y',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[0] = '';
} else {
$y = $dmt->_fix_year($y);
$defined = 1;
}
# If the month was not specifed, but the year was, a default of
# 01 is supplied (this is a truncated date).
#
# If neither was specified, month defaults to the current month.
if ($m ne '') {
$defined = 1;
} elsif ($defined) {
$m = 1;
$$self{'data'}{'def'}[1] = 1;
} else {
$m = $dmt->_now('m',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[1] = '';
}
# If the day was not specified, but the year or month was, a default
# of 01 is supplied (this is a truncated date).
#
# If none were specified, it default to the current day.
if ($d ne '') {
$defined = 1;
} elsif ($defined) {
$d = 1;
$$self{'data'}{'def'}[2] = 1;
} else {
$d = $dmt->_now('d',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[2] = '';
}
return($y,$m,$d);
}
# Supply defaults for missing values (Y/DoY)
sub _def_date_doy {
my($self,$y,$doy,$noupdate) = @_;
$y = '' if (! defined $y);
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# If year was not specified, defaults to current year.
#
# We'll also fix the year (turn 2-digit into 4-digit).
if ($y eq '') {
$y = $dmt->_now('y',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[0] = '';
} else {
$y = $dmt->_fix_year($y);
}
# DoY must be specified.
my($m,$d);
my $ymd = $dmb->day_of_year($y,$doy);
return @$ymd;
}
# Supply defaults for missing values (YY/Www/D) and (Y/Www/D)
sub _def_date_dow {
my($self,$y,$w,$dow,$noupdate) = @_;
$y = '' if (! defined $y);
$w = '' if (! defined $w);
$dow = '' if (! defined $dow);
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# If year was not specified, defaults to current year.
#
# If it was specified and is a single digit, it is the
# year in the current decade.
#
# We'll also fix the year (turn 2-digit into 4-digit).
if ($y ne '') {
if (length($y) == 1) {
my $tmp = $dmt->_now('y',$$noupdate);
$tmp =~ s/.$/$y/;
$y = $tmp;
$$noupdate = 1;
} else {
$y = $dmt->_fix_year($y);
}
} else {
$y = $dmt->_now('y',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[0] = '';
}
# If week was not specified, it defaults to the current
# week. Get the first day of the week.
my($m,$d);
if ($w ne '') {
($y,$m,$d) = @{ $dmb->week_of_year($y,$w) };
} else {
my($nowy,$nowm,$nowd) = $dmt->_now('now',$$noupdate);
$$noupdate = 1;
my $noww;
($nowy,$noww) = $dmb->week_of_year([$nowy,$nowm,$nowd]);
($y,$m,$d) = @{ $dmb->week_of_year($nowy,$noww) };
}
# Handle the DoW
if ($dow eq '') {
$dow = 1;
}
my $n = $dmb->days_in_month($y,$m);
$d += ($dow-1);
if ($d > $n) {
$m++;
if ($m==13) {
$y++;
$m = 1;
}
$d = $d-$n;
}
return($y,$m,$d);
}
# Supply defaults for missing values (HH:MN:SS)
sub _def_time {
my($self,$h,$m,$s,$noupdate) = @_;
$h = '' if (! defined $h);
$m = '' if (! defined $m);
$s = '' if (! defined $s);
my $defined = 0;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# If no time was specified, defaults to 00:00:00.
if ($h eq '' &&
$m eq '' &&
$s eq '') {
$$self{'data'}{'def'}[3] = 1;
$$self{'data'}{'def'}[4] = 1;
$$self{'data'}{'def'}[5] = 1;
return(0,0,0);
}
# If hour was not specified, defaults to current hour.
if ($h ne '') {
$defined = 1;
} else {
$h = $dmt->_now('h',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[3] = '';
}
# If the minute was not specifed, but the hour was, a default of
# 00 is supplied (this is a truncated time).
#
# If neither was specified, minute defaults to the current minute.
if ($m ne '') {
$defined = 1;
} elsif ($defined) {
$m = 0;
$$self{'data'}{'def'}[4] = 1;
} else {
$m = $dmt->_now('mn',$$noupdate);
$$noupdate = 1;
$$self{'data'}{'def'}[4] = '';
}
# If the second was not specified (either the hour or the minute were),
# a default of 00 is supplied (this is a truncated time).
if ($s eq '') {
$s = 0;
$$self{'data'}{'def'}[5] = 1;
}
return($h,$m,$s);
}
########################################################################
# OTHER DATE METHODS
########################################################################
# Gets the date in the parsed timezone (if $type = ''), local timezone
# (if $type = 'local') or GMT timezone (if $type = 'gmt').
#
# Gets the string value in scalar context, the split value in list
# context.
#
sub value {
my($self,$type) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $date;
while (1) {
if (! $$self{'data'}{'set'}) {
$$self{'err'} = '[value] Object does not contain a date';
last;
}
$type = '' if (! $type);
if ($type eq 'gmt') {
if (! @{ $$self{'data'}{'gmt'} }) {
my $zone = $$self{'data'}{'tz'};
my $date = $$self{'data'}{'date'};
if ($zone eq 'Etc/GMT') {
$$self{'data'}{'gmt'} = $date;
} else {
my $isdst = $$self{'data'}{'isdst'};
my($err,$d) = $dmt->convert_to_gmt($date,$zone,$isdst);
if ($err) {
$$self{'err'} = '[value] Unable to convert date to GMT';
last;
}
$$self{'data'}{'gmt'} = $d;
}
}
$date = $$self{'data'}{'gmt'};
} elsif ($type eq 'local') {
if (! @{ $$self{'data'}{'loc'} }) {
my $zone = $$self{'data'}{'tz'};
$date = $$self{'data'}{'date'};
my $local = $dmt->_now('tz',1);
if ($zone eq $local) {
$$self{'data'}{'loc'} = $date;
} else {
my $isdst = $$self{'data'}{'isdst'};
my($err,$d) = $dmt->convert_to_local($date,$zone,$isdst);
if ($err) {
$$self{'err'} = '[value] Unable to convert date to localtime';
last;
}
$$self{'data'}{'loc'} = $d;
}
}
$date = $$self{'data'}{'loc'};
} else {
$date = $$self{'data'}{'date'};
}
last;
}
if ($$self{'err'}) {
if (wantarray) {
return ();
} else {
return '';
}
}
if (wantarray) {
return @$date;
} else {
return $dmb->join('date',$date);
}
}
sub cmp {
my($self,$date) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [cmp] Arguments must be valid dates: date1\n";
return undef;
}
if (! ref($date) eq 'Date::Manip::Date') {
warn "WARNING: [cmp] Argument must be a Date::Manip::Date object\n";
return undef;
}
if ($$date{'err'} || ! $$date{'data'}{'set'}) {
warn "WARNING: [cmp] Arguments must be valid dates: date2\n";
return undef;
}
my($d1,$d2);
if ($$self{'data'}{'tz'} eq $$date{'data'}{'tz'}) {
$d1 = $self->value();
$d2 = $date->value();
} else {
$d1 = $self->value('gmt');
$d2 = $date->value('gmt');
}
return ($d1 cmp $d2);
}
BEGIN {
my %field = qw(y 0 m 1 d 2 h 3 mn 4 s 5);
sub set {
my($self,$field,@val) = @_;
$field = lc($field);
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Make sure $self includes a valid date (unless the entire date is
# being set, in which case it doesn't matter).
my $date = [];
my(@def,$tz,$isdst);
if ($field eq 'zdate') {
# If {data}{set} = 2, we want to preserve the defaults. Also, we've
# already initialized.
#
# It is only set in the parse routines which means that this was
# called via _parse_check.
$self->_init() if ($$self{'data'}{'set'} != 2);
@def = @{ $$self{'data'}{'def'} };
} elsif ($field eq 'date') {
if ($$self{'data'}{'set'} && ! $$self{'err'}) {
$tz = $$self{'data'}{'tz'};
} else {
$tz = $dmt->_now('tz',1);
}
$self->_init();
@def = @{ $$self{'data'}{'def'} };
} else {
return 1 if ($$self{'err'} || ! $$self{'data'}{'set'});
$date = $$self{'data'}{'date'};
$tz = $$self{'data'}{'tz'};
$isdst = $$self{'data'}{'isdst'};
@def = @{ $$self{'data'}{'def'} };
$self->_init();
}
# Check the arguments
my($err,$new_tz,$new_date,$new_time);
if ($field eq 'date') {
if ($#val == 0) {
# date,DATE
$new_date = $val[0];
} elsif ($#val == 1) {
# date,DATE,ISDST
($new_date,$isdst) = @val;
} else {
$err = 1;
}
for (my $i=0; $i<=5; $i++) {
$def[$i] = 0 if ($def[$i]);
}
} elsif ($field eq 'time') {
if ($#val == 0) {
# time,TIME
$new_time = $val[0];
} elsif ($#val == 1) {
# time,TIME,ISDST
($new_time,$isdst) = @val;
} else {
$err = 1;
}
$def[3] = 0 if ($def[3]);
$def[4] = 0 if ($def[4]);
$def[5] = 0 if ($def[5]);
} elsif ($field eq 'zdate') {
if ($#val == 0) {
# zdate,DATE
$new_date = $val[0];
} elsif ($#val == 1 && ($val[1] eq '0' || $val[1] eq '1')) {
# zdate,DATE,ISDST
($new_date,$isdst) = @val;
} elsif ($#val == 1) {
# zdate,ZONE,DATE
($new_tz,$new_date) = @val;
} elsif ($#val == 2) {
# zdate,ZONE,DATE,ISDST
($new_tz,$new_date,$isdst) = @val;
} else {
$err = 1;
}
if ($$self{'data'}{'set'} != 2) {
for (my $i=0; $i<=5; $i++) {
$def[$i] = 0 if ($def[$i]);
}
}
$tz = $dmt->_now('tz',1) if (! $new_tz);
} elsif ($field eq 'zone') {
if ($#val == -1) {
# zone
} elsif ($#val == 0 && ($val[0] eq '0' || $val[0] eq '1')) {
# zone,ISDST
$isdst = $val[0];
} elsif ($#val == 0) {
# zone,ZONE
$new_tz = $val[0];
} elsif ($#val == 1) {
# zone,ZONE,ISDST
($new_tz,$isdst) = @val;
} else {
$err = 1;
}
$tz = $dmt->_now('tz',1) if (! $new_tz);
} elsif (exists $field{$field}) {
my $i = $field{$field};
my $val;
if ($#val == 0) {
$val = $val[0];
} elsif ($#val == 1) {
($val,$isdst) = @val;
} else {
$err = 1;
}
$$date[$i] = $val;
$def[$i] = 0 if ($def[$i]);
} else {
$err = 2;
}
if ($err) {
if ($err == 1) {
$$self{'err'} = '[set] Invalid arguments';
} else {
$$self{'err'} = '[set] Invalid field';
}
return 1;
}
# Handle the arguments (it can be a zone or an offset)
if ($new_tz) {
my $tmp = $dmt->_zone($new_tz);
if ($tmp) {
# A zone/alias
$tz = $tmp;
} else {
# An offset
my $dstflag = '';
$dstflag = ($isdst ? 'dstonly' : 'stdonly') if (defined $isdst);
$tz = $dmb->__zone($date,lc($new_tz),'',$dstflag);
if (! $tz) {
$$self{'err'} = "[set] Invalid timezone argument: $new_tz";
return 1;
}
}
}
if ($new_date) {
if ($dmb->check($new_date)) {
$date = $new_date;
} else {
$$self{'err'} = '[set] Invalid date argument';
return 1;
}
}
if ($new_time) {
if ($dmb->check_time($new_time)) {
$$date[3] = $$new_time[0];
$$date[4] = $$new_time[1];
$$date[5] = $$new_time[2];
} else {
$$self{'err'} = '[set] Invalid time argument';
return 1;
}
}
# Check the date/timezone combination
my($abb,$off);
if ($tz eq 'etc/gmt') {
$abb = 'GMT';
$off = [0,0,0];
$isdst = 0;
} else {
my $per = $dmt->date_period($date,$tz,1,$isdst);
if (! $per) {
$$self{'err'} = '[set] Invalid date/timezone';
return 1;
}
$isdst = $$per[5];
$abb = $$per[4];
$off = $$per[3];
}
# Set the information
$$self{'data'}{'set'} = 1;
$$self{'data'}{'date'} = $date;
$$self{'data'}{'tz'} = $tz;
$$self{'data'}{'isdst'} = $isdst;
$$self{'data'}{'offset'}= $off;
$$self{'data'}{'abb'} = $abb;
$$self{'data'}{'def'} = [ @def ];
return 0;
}
}
########################################################################
# NEXT/PREV METHODS
sub prev {
my($self,@args) = @_;
return 1 if ($$self{'err'} || ! $$self{'data'}{'set'});
my $date = $$self{'data'}{'date'};
$date = $self->__next_prev($date,0,@args);
return 1 if (! defined($date));
$self->set('date',$date);
return 0;
}
sub next {
my($self,@args) = @_;
return 1 if ($$self{'err'} || ! $$self{'data'}{'set'});
my $date = $$self{'data'}{'date'};
$date = $self->__next_prev($date,1,@args);
return 1 if (! defined($date));
$self->set('date',$date);
return 0;
}
sub __next_prev {
my($self,$date,$next,$dow,$curr,$time) = @_;
my ($caller,$sign,$prev);
if ($next) {
$caller = 'next';
$sign = 1;
$prev = 0;
} else {
$caller = 'prev';
$sign = -1;
$prev = 1;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $orig = [ @$date ];
# Check the time (if any)
if (defined($time)) {
if ($dow) {
# $time will refer to a full [H,MN,S]
my($err,$h,$mn,$s) = $dmb->_hms_fields({ 'out' => 'list' },$time);
if ($err) {
$$self{'err'} = "[$caller] invalid time argument";
return undef;
}
$time = [$h,$mn,$s];
} else {
# $time may have leading undefs
my @tmp = @$time;
if ($#tmp != 2) {
$$self{'err'} = "[$caller] invalid time argument";
return undef;
}
my($h,$mn,$s) = @$time;
if (defined($h)) {
$mn = 0 if (! defined($mn));
$s = 0 if (! defined($s));
} elsif (defined($mn)) {
$s = 0 if (! defined($s));
} else {
$s = 0 if (! defined($s));
}
$time = [$h,$mn,$s];
}
}
# Find the next DoW
if ($dow) {
if (! $dmb->_is_int($dow,1,7)) {
$$self{'err'} = "[$caller] Invalid DOW: $dow";
return undef;
}
# Find the next/previous occurrence of DoW
my $curr_dow = $dmb->day_of_week($date);
my $adjust = 0;
if ($dow == $curr_dow) {
$adjust = 1 if ($curr == 0);
} else {
my $num;
if ($next) {
# force $dow to be more than $curr_dow
$dow += 7 if ($dow<$curr_dow);
$num = $dow - $curr_dow;
} else {
# force $dow to be less than $curr_dow
$dow -= 7 if ($dow>$curr_dow);
$num = $curr_dow - $dow;
$num *= -1;
}
# Add/subtract $num days
$date = $dmb->calc_date_days($date,$num);
}
if (defined($time)) {
my ($y,$m,$d,$h,$mn,$s) = @$date;
($h,$mn,$s) = @$time;
$date = [$y,$m,$d,$h,$mn,$s];
}
my $cmp = $dmb->cmp($orig,$date);
$adjust = 1 if ($curr == 2 && $cmp != -1*$sign);
if ($adjust) {
# Add/subtract 1 week
$date = $dmb->calc_date_days($date,$sign*7);
}
return $date;
}
# Find the next Time
if (defined($time)) {
my ($h,$mn,$s) = @$time;
my $orig = [ @$date ];
my $cmp;
if (defined $h) {
# Find next/prev HH:MN:SS
@$date[3..5] = @$time;
$cmp = $dmb->cmp($orig,$date);
if ($cmp == -1) {
if ($prev) {
$date = $dmb->calc_date_days($date,-1);
}
} elsif ($cmp == 1) {
if ($next) {
$date = $dmb->calc_date_days($date,1);
}
} else {
if (! $curr) {
$date = $dmb->calc_date_days($date,$sign);
}
}
} elsif (defined $mn) {
# Find next/prev MN:SS
@$date[4..5] = @$time[1..2];
$cmp = $dmb->cmp($orig,$date);
if ($cmp == -1) {
if ($prev) {
$date = $dmb->calc_date_time($date,[-1,0,0]);
}
} elsif ($cmp == 1) {
if ($next) {
$date = $dmb->calc_date_time($date,[1,0,0]);
}
} else {
if (! $curr) {
$date = $dmb->calc_date_time($date,[$sign,0,0]);
}
}
} else {
# Find next/prev SS
$$date[5] = $$time[2];
$cmp = $dmb->cmp($orig,$date);
if ($cmp == -1) {
if ($prev) {
$date = $dmb->calc_date_time($date,[0,-1,0]);
}
} elsif ($cmp == 1) {
if ($next) {
$date = $dmb->calc_date_time($date,[0,1,0]);
}
} else {
if (! $curr) {
$date = $dmb->calc_date_time($date,[0,$sign,0]);
}
}
}
return $date;
}
$$self{'err'} = "[$caller] Either DoW or time (or both) required";
return undef;
}
########################################################################
# CALC METHOD
sub calc {
my($self,$obj,@args) = @_;
if (ref($obj) eq 'Date::Manip::Date') {
return $self->_calc_date_date($obj,@args);
} elsif (ref($obj) eq 'Date::Manip::Delta') {
return $self->_calc_date_delta($obj,@args);
} else {
return undef;
}
}
sub _calc_date_date {
my($self,$date,@args) = @_;
my $ret = $self->new_delta();
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
$$ret{'err'} = '[calc] First object invalid (date)';
return $ret;
}
if ($$date{'err'} || ! $$date{'data'}{'set'}) {
$$ret{'err'} = '[calc] Second object invalid (date)';
return $ret;
}
# Handle subtract/mode arguments
my($subtract,$mode);
if ($#args == -1) {
($subtract,$mode) = (0,'');
} elsif ($#args == 0) {
if ($args[0] eq '0' || $args[0] eq '1') {
($subtract,$mode) = ($args[0],'');
} else {
($subtract,$mode) = (0,$args[0]);
}
} elsif ($#args == 1) {
($subtract,$mode) = @args;
} else {
$$ret{'err'} = '[calc] Invalid arguments';
return $ret;
}
$mode = 'exact' if (! $mode);
if ($mode !~ /^(business|bsemi|bapprox|approx|semi|exact)$/i) {
$$ret{'err'} = '[calc] Invalid mode argument';
return $ret;
}
# if business mode
# dates must be in the same timezone
# use dates in that zone
#
# otherwise if both dates are in the same timezone && approx/semi mode
# use the dates in that zone
#
# otherwise
# convert to gmt
# use those dates
my($date1,$date2,$tz1,$isdst1,$tz2,$isdst2);
if ($mode eq 'business' || $mode eq 'bapprox' || $mode eq 'bsemi') {
if ($$self{'data'}{'tz'} eq $$date{'data'}{'tz'}) {
$date1 = [ $self->value() ];
$date2 = [ $date->value() ];
$tz1 = $$self{'data'}{'tz'};
$tz2 = $tz1;
$isdst1 = $$self{'data'}{'isdst'};
$isdst2 = $$date{'data'}{'isdst'};
} else {
$$ret{'err'} = '[calc] Dates must be in the same timezone for ' .
'business mode calculations';
return $ret;
}
} elsif (($mode eq 'approx' || $mode eq 'semi') &&
$$self{'data'}{'tz'} eq $$date{'data'}{'tz'}) {
$date1 = [ $self->value() ];
$date2 = [ $date->value() ];
$tz1 = $$self{'data'}{'tz'};
$tz2 = $tz1;
$isdst1 = $$self{'data'}{'isdst'};
$isdst2 = $$date{'data'}{'isdst'};
} else {
$date1 = [ $self->value('gmt') ];
$date2 = [ $date->value('gmt') ];
$tz1 = 'GMT';
$tz2 = $tz1;
$isdst1 = 0;
$isdst2 = 0;
}
# Do the calculation
my(@delta);
if ($subtract) {
if ($mode eq 'business' || $mode eq 'exact' || $subtract == 2) {
@delta = @{ $self->__calc_date_date($mode,$date2,$tz2,$isdst2,
$date1,$tz1,$isdst1) };
} else {
@delta = @{ $self->__calc_date_date($mode,$date1,$tz1,$isdst1,
$date2,$tz2,$isdst2) };
@delta = map { -1*$_ } @delta;
}
} else {
@delta = @{ $self->__calc_date_date($mode,$date1,$tz1,$isdst1,
$date2,$tz2,$isdst2) };
}
# Save the delta
if ($mode eq 'business' || $mode eq 'bapprox' || $mode eq 'bsemi') {
$ret->set('business',\@delta);
} else {
$ret->set('delta',\@delta);
}
return $ret;
}
sub __calc_date_date {
my($self,$mode,$date1,$tz1,$isdst1,$date2,$tz2,$isdst2) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($dy,$dm,$dw,$dd,$dh,$dmn,$ds) = (0,0,0,0,0,0,0);
if ($mode eq 'approx' || $mode eq 'bapprox') {
my($y1,$m1,$d1,$h1,$mn1,$s1) = @$date1;
my($y2,$m2,$d2,$h2,$mn2,$s2) = @$date2;
$dy = $y2-$y1;
$dm = $m2-$m1;
if ($dy || $dm) {
# If $d1 is greater than the number of days allowed in the
# month $y2/$m2, set it equal to the number of days. In other
# words:
# Jan 31 2006 to Feb 28 2008 = 2 years 1 month
#
my $dim = $dmb->days_in_month($y2,$m2);
$d1 = $dim if ($d1 > $dim);
$date1 = [$y2,$m2,$d1,$h1,$mn1,$s1];
}
}
if ($mode eq 'semi' || $mode eq 'approx') {
# Calculate the number of weeks/days apart (temporarily ignoring
# DST effects).
$dd = $dmb->days_since_1BC($date2) -
$dmb->days_since_1BC($date1);
$dw = int($dd/7);
$dd -= $dw*7;
# Adding $dd to $date1 gives: ($y2,$m2,$d2, $h1,$mn1,$s1)
# Make sure this is valid (taking into account DST effects).
# If it isn't, make it valid.
if ($dw || $dd) {
my($y1,$m1,$d1,$h1,$mn1,$s1) = @$date1;
my($y2,$m2,$d2,$h2,$mn2,$s2) = @$date2;
$date1 = [$y2,$m2,$d2,$h1,$mn1,$s1];
}
if ($dy || $dm || $dw || $dd) {
my $force = ( ($dw > 0 || $dd > 0) ? 1 : -1 );
my($off,$isdst,$abb);
($date1,$off,$isdst,$abb) =
$self->_calc_date_check_dst($date1,$tz2,$isdst2,$force);
}
}
if ($mode eq 'bsemi' || $mode eq 'bapprox') {
# Calculate the number of weeks. Ignore the days
# part. Also, since there are no DST effects, we don't
# have to check for validity.
$dd = $dmb->days_since_1BC($date2) -
$dmb->days_since_1BC($date1);
$dw = int($dd/7);
$dd = 0;
$date1 = $dmb->calc_date_days($date1,$dw*7);
}
if ($mode eq 'exact' || $mode eq 'semi' || $mode eq 'approx') {
my $sec1 = $dmb->secs_since_1970($date1);
my $sec2 = $dmb->secs_since_1970($date2);
$ds = $sec2 - $sec1;
{
no integer;
$dh = int($ds/3600);
$ds -= $dh*3600;
}
$dmn = int($ds/60);
$ds -= $dmn*60;
}
if ($mode eq 'business' || $mode eq 'bsemi' || $mode eq 'bapprox') {
# Make sure both are work days
$date1 = $self->__nextprev_business_day(0,0,1,$date1);
$date2 = $self->__nextprev_business_day(0,0,1,$date2);
my($y1,$m1,$d1,$h1,$mn1,$s1) = @$date1;
my($y2,$m2,$d2,$h2,$mn2,$s2) = @$date2;
# Find out which direction we need to move $date1 to get to $date2
my $dir = 0;
if ($y1 < $y2) {
$dir = 1;
} elsif ($y1 > $y2) {
$dir = -1;
} elsif ($m1 < $m2) {
$dir = 1;
} elsif ($m1 > $m2) {
$dir = -1;
} elsif ($d1 < $d2) {
$dir = 1;
} elsif ($d1 > $d2) {
$dir = -1;
}
# Now do the day part (to get to the same day)
$dd = 0;
while ($dir) {
($y1,$m1,$d1) = @{ $dmb->calc_date_days([$y1,$m1,$d1],$dir) };
$dd += $dir if ($self->__is_business_day([$y1,$m1,$d1,0,0,0],0));
$dir = 0 if ($y1 == $y2 && $m1 == $m2 && $d1 == $d2);
}
# Both dates are now on a business day, and during business
# hours, so do the hr/min/sec part trivially
$dh = $h2-$h1;
$dmn = $mn2-$mn1;
$ds = $s2-$s1;
}
return [ $dy,$dm,$dw,$dd,$dh,$dmn,$ds ];
}
no integer;
sub _calc_date_delta {
my($self,$delta,$subtract) = @_;
my $ret = $self->new_date();
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
$$ret{'err'} = '[calc] Date object invalid';
return $ret;
}
if ($$delta{'err'}) {
$$ret{'err'} = '[calc] Delta object invalid';
return $ret;
}
# Get the date/delta fields
$subtract = 0 if (! $subtract);
my @delta = @{ $$delta{'data'}{'delta'} };
my @date = @{ $$self{'data'}{'date'} };
my $business = $$delta{'data'}{'business'};
my $tz = $$self{'data'}{'tz'};
my $isdst = $$self{'data'}{'isdst'};
# We can't handle a delta longer than 10000 years
my($dy,$dm,$dw,$dd,$dh,$dmn,$ds) = @delta;
if (abs($dy) > 10000 ||
abs($dm) > 12000 || # 10000*12
abs($dw) > 53000 || # 10000*53
abs($dh) > 87840000 || # 10000*366*24
abs($dmn) > 5270400000 || # 10000*366*24*60
abs($ds) > 316224000000) { # 10000*366*24*60*60
$$ret{'err'} = '[calc] Delta too large';
return $ret;
}
my($err,$date2,$offset,$abbrev);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta([@date],[@delta],$subtract,$business,$tz,$isdst);
if ($err) {
$$ret{'err'} = '[calc] Unable to perform calculation';
} elsif ($$date2[0]<0 || $$date2[0]>9999) {
$$ret{'err'} = '[calc] Delta produces date outside valid range';
} else {
$$ret{'data'}{'set'} = 1;
$$ret{'data'}{'date'} = $date2;
$$ret{'data'}{'tz'} = $tz;
$$ret{'data'}{'isdst'} = $isdst;
$$ret{'data'}{'offset'}= $offset;
$$ret{'data'}{'abb'} = $abbrev;
}
return $ret;
}
use integer;
sub __calc_date_delta {
my($self,$date,$delta,$subtract,$business,$tz,$isdst) = @_;
my ($dy,$dm,$dw,$dd,$dh,$dmn,$ds) = @$delta;
my @date = @$date;
my ($err,$date2,$offset,$abbrev);
# In business mode, daylight saving time is ignored, so days are
# of a constant, known length, so they'll be done in the exact
# function. Otherwise, they'll be done in the approximate function.
#
# Also in business mode, if $subtract = 2, then the starting date
# must be a business date or an error occurs.
my($dd_exact,$dd_approx);
if ($business) {
$dd_exact = $dd;
$dd_approx = 0;
if ($subtract == 2 && ! $self->__is_business_day($date,1)) {
return (1);
}
} else {
$dd_exact = 0;
$dd_approx = $dd;
}
if ($subtract == 2 && ($dy || $dm || $dw || $dd_approx)) {
# For subtract=2:
# DATE = RET + DELTA
#
# The delta consisists of an approximate part (which is added first)
# and an exact part (added second):
# DATE = RET + DELTA(approx) + DELTA(exact)
# DATE = RET' + DELTA(exact)
# where RET' = RET + DELTA(approx)
#
# For an exact delta, subtract==2 and subtract==1 are equivalent,
# so this can be written:
# DATE - DELTA(exact) = RET'
#
# So the inverse subtract only needs to include the approximate
# portion of the delta.
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta_exact([@date],[-1*$dd_exact,-1*$dh,-1*$dmn,-1*$ds],
$business,$tz,$isdst);
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta_inverse($date2,[$dy,$dm,$dw,$dd_approx],
$business,$tz,$isdst)
if (! $err);
} else {
# We'll add the approximate part, followed by the exact part.
# After the approximate part, we need to make sure we're on
# a valid business day in business mode.
($dy,$dm,$dw,$dd_exact,$dd_approx,$dh,$dmn,$ds) =
map { -1*$_ } ($dy,$dm,$dw,$dd_exact,$dd_approx,$dh,$dmn,$ds)
if ($subtract);
@$date2 = @date;
if ($dy || $dm || $dw || $dd) {
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta_approx($date2,[$dy,$dm,$dw,$dd_approx],
$business,$tz,$isdst);
} elsif ($business) {
$date2 = $self->__nextprev_business_day(0,0,1,$date2);
}
($err,$date2,$offset,$isdst,$abbrev) =
$self->__calc_date_delta_exact($date2,[$dd_exact,$dh,$dmn,$ds],
$business,$tz,$isdst)
if (! $err && ($dd_exact || $dh || $dmn || $ds));
}
return($err,$date2,$offset,$isdst,$abbrev);
}
# Do the inverse part of a calculation.
#
# $delta = [$dy,$dm,$dw,$dd]
#
sub __calc_date_delta_inverse {
my($self,$date,$delta,$business,$tz,$isdst) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my @date2;
# Given: DATE1, DELTA
# Find: DATE2
# where DATE2 + DELTA = DATE1
#
# Start with:
# DATE2 = DATE1 - DELTA
#
# if (DATE2+DELTA < DATE1)
# while (1)
# DATE2 = DATE2 + 1 day
# if DATE2+DELTA < DATE1
# next
# elsif DATE2+DELTA > DATE1
# return ERROR
# else
# return DATE2
# done
#
# elsif (DATE2+DELTA > DATE1)
# while (1)
# DATE2 = DATE2 - 1 day
# if DATE2+DELTA > DATE1
# next
# elsif DATE2+DELTA < DATE1
# return ERROR
# else
# return DATE2
# done
#
# else
# return DATE2
if ($business) {
my $date1 = $date;
my ($err,$date2,$off,$isd,$abb,@del,$tmp,$cmp);
@del = map { $_*-1 } @$delta;
($err,$date2,$off,$isd,$abb) =
$self->__calc_date_delta_approx($date,[@del],$business,$tz,$isdst);
($err,$tmp,$off,$isd,$abb) =
$self->__calc_date_delta_approx($date2,$delta,$business,$tz,$isdst);
$cmp = $self->_cmp_date($tmp,$date1);
if ($cmp < 0) {
while (1) {
$date2 = $self->__nextprev_business_day(0,1,0,$date2);
($err,$tmp,$off,$isd,$abb) =
$self->__calc_date_delta_approx($date2,$delta,$business,$tz,$isdst);
$cmp = $self->_cmp_date($tmp,$date1);
if ($cmp < 0) {
next;
} elsif ($cmp > 0) {
return (1);
} else {
last;
}
}
} elsif ($cmp > 0) {
while (1) {
$date2 = $self->__nextprev_business_day(1,1,0,$date2);
($err,$tmp,$off,$isd,$abb) =
$self->__calc_date_delta_approx($date2,$delta,$business,$tz,$isdst);
$cmp = $self->_cmp_date($tmp,$date1);
if ($cmp > 0) {
next;
} elsif ($cmp < 0) {
return (1);
} else {
last;
}
}
}
@date2 = @$date2;
} else {
my @tmp = @$date[0..2]; # [y,m,d]
my @hms = @$date[3..5]; # [h,m,s]
my $date1 = [@tmp];
my $date2 = $dmb->_calc_date_ymwd($date1,$delta,1);
my $tmp = $dmb->_calc_date_ymwd($date2,$delta);
my $cmp = $self->_cmp_date($tmp,$date1);
if ($cmp < 0) {
while (1) {
$date2 = $dmb->calc_date_days($date2,1);
$tmp = $dmb->_calc_date_ymwd($date2,$delta);
$cmp = $self->_cmp_date($tmp,$date1);
if ($cmp < 0) {
next;
} elsif ($cmp > 0) {
return (1);
} else {
last;
}
}
} elsif ($cmp > 0) {
while (1) {
$date2 = $dmb->calc_date_days($date2,-1);
$tmp = $dmb->_calc_date_ymwd($date2,$delta);
$cmp = $self->_cmp_date($tmp,$date1);
if ($cmp > 0) {
next;
} elsif ($cmp < 0) {
return (1);
} else {
last;
}
}
}
@date2 = (@$date2,@hms);
}
# Make sure DATE2 is valid (within DST constraints) and
# return it.
my($date2,$abb,$off,$err);
($date2,$off,$isdst,$abb) = $self->_calc_date_check_dst([@date2],$tz,$isdst,0);
return (1) if (! defined($date2));
return (0,$date2,$off,$isdst,$abb);
}
sub _cmp_date {
my($self,$date0,$date1) = @_;
return ($$date0[0] <=> $$date1[0] ||
$$date0[1] <=> $$date1[1] ||
$$date0[2] <=> $$date1[2]);
}
# Do the approximate part of a calculation.
#
sub __calc_date_delta_approx {
my($self,$date,$delta,$business,$tz,$isdst) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d,$h,$mn,$s) = @$date;
my($dy,$dm,$dw,$dd) = @$delta;
#
# Do the year/month part.
#
# If we are past the last day of a month, move the date back to
# the last day of the month. i.e. Jan 31 + 1 month = Feb 28.
#
$y += $dy if ($dy);
$dmb->_mod_add(-12,$dm,\$m,\$y) # -12 means 1-12 instead of 0-11
if ($dm);
my $dim = $dmb->days_in_month($y,$m);
$d = $dim if ($d > $dim);
#
# Do the week part.
#
# The week is treated as 7 days for both business and non-business
# calculations.
#
# In a business calculation, make sure we're on a business date.
#
if ($business) {
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],$dw*7) } if ($dw);
($y,$m,$d,$h,$mn,$s) =
@{ $self->__nextprev_business_day(0,0,1,[$y,$m,$d,$h,$mn,$s]) };
} else {
$dd += $dw*7;
}
#
# Now do the day part. $dd is always 0 in business calculations.
#
if ($dd) {
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],$dd) };
}
#
# At this point, we need to make sure that we're a valid date
# (within the constraints of DST).
#
# If it is not valid in this offset, try the other one. If neither
# works, then we want the the date to be 24 hours later than the
# previous day at this time (if $dd > 0) or 24 hours earlier than
# the next day at this time (if $dd < 0). We'll use the 24 hour
# definition even for business days, but then we'll double check
# that the resulting date is a business date.
#
my $force = ( ($dd > 0 || $dw > 0 || $dm > 0 || $dy > 0) ? 1 : -1 );
my($off,$abb);
($date,$off,$isdst,$abb) =
$self->_calc_date_check_dst([$y,$m,$d,$h,$mn,$s],$tz,$isdst,$force);
return (0,$date,$off,$isdst,$abb);
}
# Do the exact part of a calculation.
#
sub __calc_date_delta_exact {
my($self,$date,$delta,$business,$tz,$isdst) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
if ($business) {
# Simplify hours/minutes/seconds where the day length is defined
# by the start/end of the business day.
my ($dd,$dh,$dmn,$ds) = @$delta;
my ($y,$m,$d,$h,$mn,$s)= @$date;
my ($hbeg,$mbeg,$sbeg) = @{ $$dmb{'data'}{'calc'}{'workdaybeg'} };
my ($hend,$mend,$send) = @{ $$dmb{'data'}{'calc'}{'workdayend'} };
my $bdlen = $$dmb{'data'}{'len'}{'bdlength'};
no integer;
my $tmp;
$ds += $dh*3600 + $dmn*60;
$tmp = int($ds/$bdlen);
$dd += $tmp;
$ds -= $tmp*$bdlen;
$dh = int($ds/3600);
$ds -= $dh*3600;
$dmn = int($ds/60);
$ds -= $dmn*60;
use integer;
if ($dd) {
my $prev = 0;
if ($dd < 1) {
$prev = 1;
$dd *= -1;
}
($y,$m,$d,$h,$mn,$s) =
@{ $self->__nextprev_business_day($prev,$dd,0,[$y,$m,$d,$h,$mn,$s]) };
}
# At this point, we're adding less than a day for the
# hours/minutes/seconds part AND we know that the current
# day is during business hours.
#
# We'll add them (without affecting days... we'll need to
# test things by hand to make sure we should or shouldn't
# do that.
$dmb->_mod_add(60,$ds,\$s,\$mn);
$dmb->_mod_add(60,$dmn,\$mn,\$h);
$h += $dh;
# Note: it's possible that $h > 23 at this point or $h < 0
if ($h > $hend ||
($h == $hend && $mn > $mend) ||
($h == $hend && $mn == $mend && $s > $send) ||
($h == $hend && $mn == $mend && $s == $send)) {
# We've gone past the end of the business day.
my $t2 = $dmb->calc_time_time([$h,$mn,$s],[$hend,$mend,$send],1);
while (1) {
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],1) };
last if ($self->__is_business_day([$y,$m,$d,$h,$mn,$s]));
}
($h,$mn,$s) = @{ $dmb->calc_time_time([$hbeg,$mbeg,$sbeg],$t2) };
} elsif ($h < $hbeg ||
($h == $hbeg && $mn < $mbeg) ||
($h == $hbeg && $mn == $mbeg && $s < $sbeg)) {
# We've gone back past the start of the business day.
my $t2 = $dmb->calc_time_time([$hbeg,$mbeg,$sbeg],[$h,$mn,$s],1);
while (1) {
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],-1) };
last if ($self->__is_business_day([$y,$m,$d,$h,$mn,$s]));
}
($h,$mn,$s) = @{ $dmb->calc_time_time([$hend,$mend,$send],$t2,1) };
}
# Now make sure that the date is valid within DST constraints.
my $force = ( ($dd > 0 || $dh > 0 || $dmn > 0 || $ds > 0) ? 1 : -1 );
my($off,$abb);
($date,$off,$isdst,$abb) =
$self->_calc_date_check_dst([$y,$m,$d,$h,$mn,$s],$tz,$isdst,$force);
return (0,$date,$off,$isdst,$abb);
} else {
# Convert to GTM
# Do the calculation
# Convert back
my ($dd,$dh,$dm,$ds) = @$delta; # $dd is always 0
my $del = [$dh,$dm,$ds];
my ($err,$offset,$abbrev);
($err,$date,$offset,$isdst,$abbrev) =
$dmt->_convert('__calc_date_delta_exact',$date,$tz,'GMT',$isdst);
$date = $dmb->calc_date_time($date,$del,0);
($err,$date,$offset,$isdst,$abbrev) =
$dmt->_convert('__calc_date_delta_exact',$date,'GMT',$tz,$isdst);
return($err,$date,$offset,$isdst,$abbrev);
}
}
# This checks to see which time (STD or DST) a date is in. It checks
# $isdst first, and the other value (1-$isdst) second.
#
# If the date is found in either time, it is returned.
#
# If the date is NOT found, then we got here by adding/subtracting 1 day
# from a different value, and we've obtained an invalid value. In this
# case, if $force = 0, then return nothing.
#
# If $force = 1, then go to the previous day and add 24 hours. If force
# is -1, then go to the next day and subtract 24 hours.
#
# Returns:
# ($date,$off,$isdst,$abb)
# or
# (undef)
#
sub _calc_date_check_dst {
my($self,$date,$tz,$isdst,$force) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($abb,$off,$err);
# Try the date as is in both ISDST and 1-ISDST times
my $per = $dmt->date_period($date,$tz,1,$isdst);
if ($per) {
$abb = $$per[4];
$off = $$per[3];
return($date,$off,$isdst,$abb);
}
$per = $dmt->date_period($date,$tz,1,1-$isdst);
if ($per) {
$isdst = 1-$isdst;
$abb = $$per[4];
$off = $$per[3];
return($date,$off,$isdst,$abb);
}
# If we made it here, the date is invalid in this timezone.
# Either return undef, or add/subtract a day from the date
# and find out what time period we're in (all we care about
# is the ISDST value).
if (! $force) {
return(undef);
}
my($dd);
if ($force > 0) {
$date = $dmb->calc_date_days($date,-1);
$dd = 1;
} else {
$date = $dmb->calc_date_days($date,+1);
$dd = -1;
}
$per = $dmt->date_period($date,$tz,1,$isdst);
$isdst = (1-$isdst) if (! $per);
# Now, convert it to GMT, add/subtract 24 hours, and convert
# it back.
($err,$date,$off,$isdst,$abb) = $dmt->convert_to_gmt($date,$tz,$isdst);
$date = $dmb->calc_date_days($date,$dd);
($err,$date,$off,$isdst,$abb) = $dmt->convert_from_gmt($date,$tz);
return($date,$off,$isdst,$abb);
}
########################################################################
# MISC METHODS
sub secs_since_1970_GMT {
my($self,$secs) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
if (defined $secs) {
my $date = $dmb->secs_since_1970($secs);
my $err;
($err,$date) = $dmt->convert_from_gmt($date);
return 1 if ($err);
$self->set('date',$date);
return 0;
}
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [secs_since_1970_GMT] Object must contain a valid date\n";
return undef;
}
my @date = $self->value('gmt');
$secs = $dmb->secs_since_1970(\@date);
return $secs;
}
sub week_of_year {
my($self,$first) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [week_of_year] Object must contain a valid date\n";
return undef;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $date = $$self{'data'}{'date'};
my $y = $$date[0];
my($day,$dow,$doy,$f);
$doy = $dmb->day_of_year($date);
# The date in January which must belong to the first week, and
# it's DayOfWeek.
if ($dmb->_config('jan1week1')) {
$day=1;
} else {
$day=4;
}
$dow = $dmb->day_of_week([$y,1,$day]);
# The start DayOfWeek. If $first is passed in, use it. Otherwise,
# use FirstDay.
if (! $first) {
$first = $dmb->_config('firstday');
}
# Find the pseudo-date of the first day of the first week (it may
# be negative meaning it occurs last year).
$first -= 7 if ($first > $dow);
$day -= ($dow-$first);
return 0 if ($day>$doy); # Day is in last week of previous year
return (($doy-$day)/7 + 1);
}
sub complete {
my($self,$field) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [complete] Object must contain a valid date\n";
return undef;
}
if (! $field) {
return 1 if (! $$self{'data'}{'def'}[1] &&
! $$self{'data'}{'def'}[2] &&
! $$self{'data'}{'def'}[3] &&
! $$self{'data'}{'def'}[4] &&
! $$self{'data'}{'def'}[5]);
return 0;
}
if ($field eq 'm') {
return 1 if (! $$self{'data'}{'def'}[1]);
}
if ($field eq 'd') {
return 1 if (! $$self{'data'}{'def'}[2]);
}
if ($field eq 'h') {
return 1 if (! $$self{'data'}{'def'}[3]);
}
if ($field eq 'mn') {
return 1 if (! $$self{'data'}{'def'}[4]);
}
if ($field eq 's') {
return 1 if (! $$self{'data'}{'def'}[5]);
}
return 0;
}
sub convert {
my($self,$zone) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [convert] Object must contain a valid date\n";
return 1;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $zonename = $dmt->_zone($zone);
if (! $zonename) {
$$self{'err'} = "[convert] Unable to determine timezone: $zone";
return 1;
}
my $date0 = $$self{'data'}{'date'};
my $zone0 = $$self{'data'}{'tz'};
my $isdst0 = $$self{'data'}{'isdst'};
my($err,$date,$off,$isdst,$abb) = $dmt->convert($date0,$zone0,$zonename,$isdst0);
if ($err) {
$$self{'err'} = '[convert] Unable to convert date to new timezone';
return 1;
}
$self->_init();
$$self{'data'}{'date'} = $date;
$$self{'data'}{'tz'} = $zonename;
$$self{'data'}{'isdst'} = $isdst;
$$self{'data'}{'offset'} = $off;
$$self{'data'}{'abb'} = $abb;
$$self{'data'}{'set'} = 1;
return 0;
}
########################################################################
# BUSINESS DAY METHODS
sub is_business_day {
my($self,$checktime) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [is_business_day] Object must contain a valid date\n";
return undef;
}
my $date = $$self{'data'}{'date'};
return $self->__is_business_day($date,$checktime);
}
sub __is_business_day {
my($self,$date,$checktime) = @_;
my($y,$m,$d,$h,$mn,$s) = @$date;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Return 0 if it's a weekend.
my $dow = $dmb->day_of_week([$y,$m,$d]);
return 0 if ($dow < $dmb->_config('workweekbeg') ||
$dow > $dmb->_config('workweekend'));
# Return 0 if it's not during work hours (and we're checking
# for that).
if ($checktime &&
! $dmb->_config('workday24hr')) {
my $t = $dmb->join('hms',[$h,$mn,$s]);
my $t0 = $dmb->join('hms',$$dmb{'data'}{'calc'}{'workdaybeg'});
my $t1 = $dmb->join('hms',$$dmb{'data'}{'calc'}{'workdayend'});
return 0 if ($t lt $t0 || $t gt $t1);
}
# Check for holidays
if (! $$dmb{'data'}{'init_holidays'}) {
$self->_holidays($y-1);
$self->_holidays($y);
$self->_holidays($y+1);
}
return 0 if (exists $$dmb{'data'}{'holidays'}{'dates'} &&
exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0} &&
exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0} &&
exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0});
return 1;
}
sub list_holidays {
my($self,$y) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
$y = $dmt->_now('y',1) if (! $y);
$self->_holidays($y-1);
$self->_holidays($y);
$self->_holidays($y+1);
my @ret;
my @m = sort { $a <=> $b } keys %{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0} };
foreach my $m (@m) {
my @d = sort { $a <=> $b } keys %{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m} };
foreach my $d (@d) {
my $hol = $self->new_date();
$hol->set('date',[$y,$m,$d,0,0,0]);
push(@ret,$hol);
}
}
return @ret;
}
sub holiday {
my($self) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [holiday] Object must contain a valid date\n";
return undef;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d) = @{ $$self{'data'}{'date'} };
$self->_holidays($y-1);
$self->_holidays($y);
$self->_holidays($y+1);
if (exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0} &&
exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0} &&
exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0}) {
my @tmp = @{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} };
foreach my $tmp (@tmp) {
$tmp = '' if ($tmp =~ /DMunnamed/);
}
if (wantarray) {
return () if (! @tmp);
return @tmp;
} else {
return '' if (! @tmp);
return $tmp[0];
}
}
return undef;
}
sub next_business_day {
my($self,$off,$checktime) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [next_business_day] Object must contain a valid date\n";
return undef;
}
my $date = $$self{'data'}{'date'};
$date = $self->__nextprev_business_day(0,$off,$checktime,$date);
$self->set('date',$date);
return;
}
sub prev_business_day {
my($self,$off,$checktime) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [prev_business_day] Object must contain a valid date\n";
return undef;
}
my $date = $$self{'data'}{'date'};
$date = $self->__nextprev_business_day(1,$off,$checktime,$date);
$self->set('date',$date);
return;
}
sub __nextprev_business_day {
my($self,$prev,$off,$checktime,$date) = @_;
my($y,$m,$d,$h,$mn,$s) = @$date;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Get day 0
while (! $self->__is_business_day([$y,$m,$d,$h,$mn,$s],$checktime)) {
if ($checktime) {
($y,$m,$d,$h,$mn,$s) =
@{ $self->__next_prev([$y,$m,$d,$h,$mn,$s],1,undef,0,
$$dmb{'data'}{'calc'}{'workdaybeg'}) };
} else {
# Move forward 1 day
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],1) };
}
}
# Move $off days into the future/past
while ($off > 0) {
while (1) {
if ($prev) {
# Move backward 1 day
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],-1) };
} else {
# Move forward 1 day
($y,$m,$d) = @{ $dmb->calc_date_days([$y,$m,$d],1) };
}
last if ($self->__is_business_day([$y,$m,$d,$h,$mn,$s]));
}
$off--;
}
return [$y,$m,$d,$h,$mn,$s];
}
sub nearest_business_day {
my($self,$tomorrow) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [nearest_business_day] Object must contain a valid date\n";
return undef;
}
my $date = $$self{'data'}{'date'};
$date = $self->__nearest_business_day($tomorrow,$date);
# If @date is empty, the date is a business day and doesn't need
# to be changed.
return if (! defined($date));
$self->set('date',$date);
return;
}
sub __nearest_business_day {
my($self,$tomorrow,$date) = @_;
# We're done if this is a business day
return undef if ($self->__is_business_day($date,0));
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
$tomorrow = $dmb->_config('tomorrowfirst') if (! defined $tomorrow);
my($a1,$a2);
if ($tomorrow) {
($a1,$a2) = (1,-1);
} else {
($a1,$a2) = (-1,1);
}
my ($y,$m,$d,$h,$mn,$s) = @$date;
my ($y1,$m1,$d1) = ($y,$m,$d);
my ($y2,$m2,$d2) = ($y,$m,$d);
while (1) {
($y1,$m1,$d1) = @{ $dmb->calc_date_days([$y1,$m1,$d1],$a1) };
if ($self->__is_business_day([$y1,$m1,$d1,$h,$mn,$s],0)) {
($y,$m,$d) = ($y1,$m1,$d1);
last;
}
($y2,$m2,$d2) = @{ $dmb->calc_date_days([$y2,$m2,$d2],$a2) };
if ($self->__is_business_day([$y2,$m2,$d2,$h,$mn,$s],0)) {
($y,$m,$d) = ($y2,$m2,$d2);
last;
}
}
return [$y,$m,$d,$h,$mn,$s];
}
# We need to create all the objects which will be used to determine holidays.
# By doing this once only, a lot of time is saved.
#
sub _holiday_objs {
my($self) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
$$dmb{'data'}{'holidays'}{'init'} = 1;
# Go through all of the strings from the config file.
#
my (@str) = @{ $$dmb{'data'}{'sections'}{'holidays'} };
$$dmb{'data'}{'holidays'}{'defs'} = [];
# Keep track of the holiday names
my $unnamed = 0;
LINE:
while (@str) {
my($string) = shift(@str);
my($name) = shift(@str);
if (! $name) {
$unnamed++;
$name = "DMunnamed $unnamed";
}
# If $string is a parse_date string AND it contains a year, we'll
# store the date as a holiday, but not store the holiday description
# so it never needs to be re-parsed.
my $date = $self->new_date();
my $err = $date->parse_date($string);
if (! $err) {
my($y,$m,$d) = @{ $$date{'data'}{'date'} };
if ($$date{'data'}{'def'}[0] eq '') {
# Lines of the form: Jun 12
#
# We will NOT cache this holiday because we want to only
# cache holidays from lines like 'Jun 12 1972' during this
# phase so we find conflicts.
push(@{ $$dmb{'data'}{'holidays'}{'defs'} },$name,$string);
} else {
# Lines of the form: Jun 12 1972
#
# We'll cache these to make sure we don't have two lines:
# Jun 12 1972 = Some Holiday
# Jun 13 1972 = Some Holiday
if (exists $$dmb{'data'}{'holidays'}{'hols'}{$name}{$y+0}) {
warn "WARNING: Holiday defined twice for one year: $name [$y]\n";
next LINE;
}
$$dmb{'data'}{'holidays'}{'yhols'}{$y+0}{$name} = [$y,$m,$d];
$$dmb{'data'}{'holidays'}{'hols'}{$name}{$y+0} = [$y,$m,$d];
if (exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0}) {
push @{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} },$name;
} else {
$$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} = [ $name ];
}
}
next LINE;
}
$date->err(1);
# If $string is a recurrence, we'll create a Recur object (which we
# only have to do once) and store it.
my $recur = $self->new_recur();
$err = $recur->parse($string);
if (! $err) {
push(@{ $$dmb{'data'}{'holidays'}{'defs'} },$name,$recur);
next LINE;
}
$recur->err(1);
warn "WARNING: invalid holiday description: $string\n";
}
return;
}
# Make sure that holidays are done for a given year.
#
sub _holidays {
my($self,$year) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
return if ($$dmb{'data'}{'holidays'}{'ydone'}{$year+0});
$self->_holiday_objs() if (! $$dmb{'data'}{'holidays'}{'init'});
# Parse the year
# Get the objects and set them to use the new year. Also, get the
# range for recurrences.
my @hol = @{ $$dmb{'data'}{'holidays'}{'defs'} };
my $beg = "$year-01-01-00:00:00";
my $end = "$year-12-31-23:59:59";
# Get the date for each holiday.
$$dmb{'data'}{'init_holidays'} = 1;
$$dmb{'data'}{'tmpnow'} = [$year,1,1,0,0,0];
HOLIDAY:
while (@hol) {
my $name = shift(@hol);
my $obj = shift(@hol);
# Each holiday only gets defined once per year
next if (exists $$dmb{'data'}{'holidays'}{'hols'}{$name}{$year+0});
if (ref($obj)) {
# It's a recurrence
# We have to initialize the recurrence as it may contain idates
# and dates outside of this range that are not correct.
$obj->_init_dates();
# If the recurrence has a date range built in, we won't override it.
# Otherwise, we'll only look for dates in this year.
my @dates;
if ($obj->start() && $obj->end()) {
@dates = $obj->dates();
} else {
@dates = $obj->dates($beg,$end,1);
}
foreach my $date (@dates) {
my($y,$m,$d) = @{ $$date{'data'}{'date'} };
$$dmb{'data'}{'holidays'}{'yhols'}{$year+0}{$name} = [$y,$m,$d];
$$dmb{'data'}{'holidays'}{'hols'}{$name}{$year+0} = [$y,$m,$d];
if (exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0}) {
push @{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} },$name;
} else {
$$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} = [$name];
}
}
} else {
my $date = $self->new_date();
$date->parse_date($obj);
my($y,$m,$d) = @{ $$date{'data'}{'date'} };
$$dmb{'data'}{'holidays'}{'yhols'}{$year+0}{$name} = [$y,$m,$d];
$$dmb{'data'}{'holidays'}{'hols'}{$name}{$year+0} = [$y,$m,$d];
if (exists $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0}) {
push @{ $$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} },$name;
} else {
$$dmb{'data'}{'holidays'}{'dates'}{$y+0}{$m+0}{$d+0} = [$name];
}
}
}
$$dmb{'data'}{'init_holidays'} = 0;
$$dmb{'data'}{'tmpnow'} = [];
$$dmb{'data'}{'holidays'}{'ydone'}{$year+0} = 1;
return;
}
########################################################################
# PRINTF METHOD
BEGIN {
my %pad_0 = map { $_,1 } qw ( Y m d H M S I j G W L U );
my %pad_sp = map { $_,1 } qw ( y f e k i );
my %hr = map { $_,1 } qw ( H k I i );
my %dow = map { $_,1 } qw ( v a A w );
my %num = map { $_,1 } qw ( Y m d H M S y f e k I i j G W L U );
sub printf {
my($self,@in) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [printf] Object must contain a valid date\n";
return undef;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my($y,$m,$d,$h,$mn,$s) = @{ $$self{'data'}{'date'} };
my(@out);
foreach my $in (@in) {
my $out = '';
while ($in) {
last if ($in eq '%');
# Everything up to the first '%'
if ($in =~ s/^([^%]+)//) {
$out .= $1;
next;
}
# Extended formats: %<...>
if ($in =~ s/^%<([^>]+)>//) {
my $f = $1;
my $val;
if ($f =~ /^a=([1-7])$/) {
$val = $$dmb{'data'}{'wordlist'}{'day_abb'}[$1-1];
} elsif ($f =~ /^v=([1-7])$/) {
$val = $$dmb{'data'}{'wordlist'}{'day_char'}[$1-1];
} elsif ($f =~ /^A=([1-7])$/) {
$val = $$dmb{'data'}{'wordlist'}{'day_name'}[$1-1];
} elsif ($f =~ /^p=([1-2])$/) {
$val = $$dmb{'data'}{'wordlist'}{'ampm'}[$1-1];
} elsif ($f =~ /^b=(0?[1-9]|1[0-2])$/) {
$val = $$dmb{'data'}{'wordlist'}{'month_abb'}[$1-1];
} elsif ($f =~ /^B=(0?[1-9]|1[0-2])$/) {
$val = $$dmb{'data'}{'wordlist'}{'month_name'}[$1-1];
} elsif ($f =~ /^E=(0?[1-9]|[1-4][0-9]|5[0-3])$/) {
$val = $$dmb{'data'}{'wordlist'}{'nth'}[$1-1];
} else {
$val = '%<' . $1 . '>';
}
$out .= $val;
next;
}
# Normals one-character formats
$in =~ s/^%(.)//s;
my $f = $1;
if (exists $$self{'data'}{'f'}{$f}) {
$out .= $$self{'data'}{'f'}{$f};
next;
}
my ($val,$pad,$len,$dow);
if (exists $pad_0{$f}) {
$pad = '0';
}
if (exists $pad_sp{$f}) {
$pad = ' ';
}
if ($f eq 'G' || $f eq 'W') {
my($yy,$ww) = $dmb->_week_of_year(1,[$y,$m,$d]);
if ($f eq 'G') {
$val = $yy;
$len = 4;
} else {
$val = $ww;
$len = 2;
}
}
if ($f eq 'L' || $f eq 'U') {
my($yy,$ww) = $dmb->_week_of_year(7,[$y,$m,$d]);
if ($f eq 'L') {
$val = $yy;
$len = 4;
} else {
$val = $ww;
$len = 2;
}
}
if ($f eq 'Y' || $f eq 'y') {
$val = $y;
$len = 4;
}
if ($f eq 'm' || $f eq 'f') {
$val = $m;
$len = 2;
}
if ($f eq 'd' || $f eq 'e') {
$val = $d;
$len = 2;
}
if ($f eq 'j') {
$val = $dmb->day_of_year([$y,$m,$d]);
$len = 3;
}
if (exists $hr{$f}) {
$val = $h;
if ($f eq 'I' || $f eq 'i') {
$val -= 12 if ($val > 12);
$val = 12 if ($val == 0);
}
$len = 2;
}
if ($f eq 'M') {
$val = $mn;
$len = 2;
}
if ($f eq 'S') {
$val = $s;
$len = 2;
}
if (exists $dow{$f}) {
$dow = $dmb->day_of_week([$y,$m,$d]);
}
###
if (exists $num{$f}) {
while (length($val) < $len) {
$val = "$pad$val";
}
$val = substr($val,2,2) if ($f eq 'y');
} elsif ($f eq 'b' || $f eq 'h') {
$val = $$dmb{'data'}{'wordlist'}{'month_abb'}[$m-1];
} elsif ($f eq 'B') {
$val = $$dmb{'data'}{'wordlist'}{'month_name'}[$m-1];
} elsif ($f eq 'v') {
$val = $$dmb{'data'}{'wordlist'}{'day_char'}[$dow-1];
} elsif ($f eq 'a') {
$val = $$dmb{'data'}{'wordlist'}{'day_abb'}[$dow-1];
} elsif ($f eq 'A') {
$val = $$dmb{'data'}{'wordlist'}{'day_name'}[$dow-1];
} elsif ($f eq 'w') {
$val = $dow;
} elsif ($f eq 'p') {
my $i = ($h >= 12 ? 1 : 0);
$val = $$dmb{'data'}{'wordlist'}{'ampm'}[$i];
} elsif ($f eq 'Z') {
$val = $$self{'data'}{'abb'};
} elsif ($f eq 'N') {
my $off = $$self{'data'}{'offset'};
$val = $dmb->join('offset',$off);
} elsif ($f eq 'z') {
my $off = $$self{'data'}{'offset'};
$val = $dmb->join('offset',$off);
$val =~ s/://g;
$val =~ s/00$//;
} elsif ($f eq 'E') {
$val = $$dmb{'data'}{'wordlist'}{'nth_dom'}[$d-1];
} elsif ($f eq 's') {
$val = $self->secs_since_1970_GMT();
} elsif ($f eq 'o') {
my $date2 = $self->new_date();
$date2->parse('1970-01-01 00:00:00');
my $delta = $date2->calc($self);
$val = $delta->printf('%sys');
} elsif ($f eq 'l') {
my $d0 = $self->new_date();
my $d1 = $self->new_date();
$d0->parse('-0:6:0:0:0:0:0'); # 6 months ago
$d1->parse('+0:6:0:0:0:0:0'); # in 6 months
$d0 = $d0->value();
$d1 = $d1->value();
my $date = $self->value();
if ($date lt $d0 || $date ge $d1) {
$in = '%b %e %Y' . $in;
} else {
$in = '%b %e %H:%M' . $in;
}
$val = '';
} elsif ($f eq 'c') {
$in = '%a %b %e %H:%M:%S %Y' . $in;
$val = '';
} elsif ($f eq 'C' || $f eq 'u') {
$in = '%a %b %e %H:%M:%S %Z %Y' . $in;
$val = '';
} elsif ($f eq 'g') {
$in = '%a, %d %b %Y %H:%M:%S %Z' . $in;
$val = '';
} elsif ($f eq 'D') {
$in = '%m/%d/%y' . $in;
$val = '';
} elsif ($f eq 'r') {
$in = '%I:%M:%S %p' . $in;
$val = '';
} elsif ($f eq 'R') {
$in = '%H:%M' . $in;
$val = '';
} elsif ($f eq 'T' || $f eq 'X') {
$in = '%H:%M:%S' . $in;
$val = '';
} elsif ($f eq 'V') {
$in = '%m%d%H%M%y' . $in;
$val = '';
} elsif ($f eq 'Q') {
$in = '%Y%m%d' . $in;
$val = '';
} elsif ($f eq 'q') {
$in = '%Y%m%d%H%M%S' . $in;
$val = '';
} elsif ($f eq 'P') {
$in = '%Y%m%d%H:%M:%S' . $in;
$val = '';
} elsif ($f eq 'O') {
$in = '%Y-%m-%dT%H:%M:%S' . $in;
$val = '';
} elsif ($f eq 'F') {
$in = '%A, %B %e, %Y' . $in;
$val = '';
} elsif ($f eq 'K') {
$in = '%Y-%j' . $in;
$val = '';
} elsif ($f eq 'x') {
if ($dmb->_config('dateformat') eq 'US') {
$in = '%m/%d/%y' . $in;
} else {
$in = '%d/%m/%y' . $in;
}
$val = '';
} elsif ($f eq 'J') {
$in = '%G-W%W-%w' . $in;
$val = '';
} elsif ($f eq 'n') {
$val = "\n";
} elsif ($f eq 't') {
$val = "\t";
} else {
$val = $f;
}
if ($val ne '') {
$$self{'data'}{'f'}{$f} = $val;
$out .= $val;
}
}
push(@out,$out);
}
if (wantarray) {
return @out;
} elsif (@out == 1) {
return $out[0];
}
return ''
}
}
########################################################################
# EVENT METHODS
sub list_events {
my($self,@args) = @_;
if ($$self{'err'} || ! $$self{'data'}{'set'}) {
warn "WARNING: [list_events] Object must contain a valid date\n";
return undef;
}
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Arguments
my($date,$day,$format);
if (@args && $args[$#args] eq 'dates') {
pop(@args);
$format = 'dates';
} else {
$format = 'std';
}
if (@args && $#args==0 && ref($args[0]) eq 'Date::Manip::Date') {
$date = $args[0];
} elsif (@args && $#args==0 && $args[0]==0) {
$day = 1;
} elsif (@args) {
warn "ERROR: [list_events] unknown argument list\n";
return [];
}
# Get the beginning/end dates we're looking for events in
my($beg,$end);
if ($date) {
$beg = $self;
$end = $date;
} elsif ($day) {
$beg = $self->new_date();
$end = $self->new_date();
my($y,$m,$d) = $self->value();
$beg->set('date',[$y,$m,$d,0,0,0]);
$end->set('date',[$y,$m,$d,23,59,59]);
} else {
$beg = $self;
$end = $self;
}
if ($beg->cmp($end) == 1) {
my $tmp = $beg;
$beg = $end;
$end = $tmp;
}
# We need to get a list of all events which may apply.
my($y0) = $beg->value();
my($y1) = $end->value();
foreach my $y ($y0..$y1) {
$self->_events_year($y);
}
my @events = ();
foreach my $i (keys %{ $$dmb{'data'}{'events'} }) {
my $event = $$dmb{'data'}{'events'}{$i};
my $type = $$event{'type'};
my $name = $$event{'name'};
if ($type eq 'specified') {
my $d0 = $$dmb{'data'}{'events'}{$i}{'beg'};
my $d1 = $$dmb{'data'}{'events'}{$i}{'end'};
push @events,[$d0,$d1,$name];
} elsif ($type eq 'ym' || $type eq 'date') {
foreach my $y ($y0..$y1) {
if (exists $$dmb{'data'}{'events'}{$i}{$y}) {
my($d0,$d1) = @{ $$dmb{'data'}{'events'}{$i}{$y} };
push @events,[$d0,$d1,$name];
}
}
} elsif ($type eq 'recur') {
my $rec = $$dmb{'data'}{'events'}{$i}{'recur'};
my $del = $$dmb{'data'}{'events'}{$i}{'delta'};
my @d = $rec->dates($beg,$end);
foreach my $d0 (@d) {
my $d1 = $d0->calc($del);
push @events,[$d0,$d1,$name];
}
}
}
# Next we need to see which ones apply.
my @tmp;
foreach my $e (@events) {
my($d0,$d1,$name) = @$e;
push(@tmp,$e) if ($beg->cmp($d1) != 1 &&
$end->cmp($d0) != -1);
}
# Now format them...
if ($format eq 'std') {
@events = sort { $$a[0]->cmp($$b[0]) ||
$$a[1]->cmp($$b[1]) ||
$$a[2] cmp $$b[2] } @tmp;
} elsif ($format eq 'dates') {
my $p1s = $self->new_delta();
$p1s->parse('+0:0:0:0:0:0:1');
@events = ();
my (@tmp2);
foreach my $e (@tmp) {
my $name = $$e[2];
if ($$e[0]->cmp($beg) == -1) {
# Event begins before the start
push(@tmp2,[$beg,'+',$name]);
} else {
push(@tmp2,[$$e[0],'+',$name]);
}
my $d1 = $$e[1]->calc($p1s);
if ($d1->cmp($end) == -1) {
# Event ends before the end
push(@tmp2,[$d1,'-',$name]);
}
}
return () if (! @tmp2);
@tmp2 = sort { $$a[0]->cmp($$b[0]) ||
$$a[1] cmp $$b[1] ||
$$a[2] cmp $$b[2] } @tmp2;
# @tmp2 is now:
# ( [ DATE1, OP1, NAME1 ], [ DATE2, OP2, NAME2 ], ... )
# which is sorted by date.
my $d = $tmp2[0]->[0];
if ($beg->cmp($d) != 0) {
push(@events,[$beg]);
}
my %e;
while (1) {
# If the first element is the same date as we're
# currently working with, just perform the operation
# and remove it from the list. If the list is not empty,
# we'll proceed to the next element.
my $d0 = $tmp2[0]->[0];
if ($d->cmp($d0) == 0) {
my $e = shift(@tmp2);
my $op = $$e[1];
my $n = $$e[2];
if ($op eq '+') {
$e{$n} = 1;
} else {
delete $e{$n};
}
next if (@tmp2);
}
# We need to store the existing %e.
my @n = sort keys %e;
push(@events,[$d,@n]);
# If the list is empty, we're done. Otherwise, we need to
# reset the date and continue.
last if (! @tmp2);
$d = $tmp2[0]->[0];
}
}
return @events;
}
# The events of type date and ym are determined on a year-by-year basis
#
sub _events_year {
my($self,$y) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
my $tz = $dmt->_now('tz',1);
return if (exists $$dmb{'data'}{'eventyears'}{$y});
$self->_event_objs() if (! $$dmb{'data'}{'eventobjs'});
my $d = $self->new_date();
$d->config('forcedate',"${y}-01-01-00:00:00,$tz");
my $hrM1 = $d->new_delta();
$hrM1->set('delta',[0,0,0,0,0,59,59]);
my $dayM1 = $d->new_delta();
$dayM1->set('delta',[0,0,0,0,23,59,59]);
foreach my $i (keys %{ $$dmb{'data'}{'events'} }) {
my $event = $$dmb{'data'}{'events'}{$i};
my $type = $$event{'type'};
if ($type eq 'ym') {
my $beg = $$event{'beg'};
my $end = $$event{'end'};
my $d0 = $d->new_date();
$d0->parse_date($beg);
$d0->set('time',[0,0,0]);
my $d1;
if ($end) {
$d1 = $d0->new_date();
$d1->parse_date($end);
$d1->set('time',[23,59,59]);
} else {
$d1 = $d0->calc($dayM1);
}
$$dmb{'data'}{'events'}{$i}{$y} = [ $d0,$d1 ];
} elsif ($type eq 'date') {
my $beg = $$event{'beg'};
my $end = $$event{'end'};
my $del = $$event{'delta'};
my $d0 = $d->new_date();
$d0->parse($beg);
my $d1;
if ($end) {
$d1 = $d0->new_date();
$d1->parse($end);
} elsif ($del) {
$d1 = $d0->calc($del);
} else {
$d1 = $d0->calc($hrM1);
}
$$dmb{'data'}{'events'}{$i}{$y} = [ $d0,$d1 ];
}
}
return;
}
# This parses the raw event list. It only has to be done once.
#
sub _event_objs {
my($self) = @_;
my $dmt = $$self{'tz'};
my $dmb = $$dmt{'base'};
# Only parse once.
$$dmb{'data'}{'eventobjs'} = 1;
my $hrM1 = $self->new_delta();
$hrM1->set('delta',[0,0,0,0,0,59,59]);
my $M1 = $self->new_delta();
$M1->set('delta',[0,0,0,0,0,0,-1]);
my @tmp = @{ $$dmb{'data'}{'sections'}{'events'} };
my $i = 0;
while (@tmp) {
my $string = shift(@tmp);
my $name = shift(@tmp);
my @event = split(/\s*;\s*/,$string);
if ($#event == 0) {
# YMD/YM
my $d1 = $self->new_date();
my $err = $d1->parse_date($event[0]);
if (! $err) {
if ($$d1{'data'}{'def'}[0] eq '') {
# YM
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'ym',
'name' => $name,
'beg' => $event[0] };
} else {
# YMD
my $d2 = $d1->new_date();
my ($y,$m,$d) = $d1->value();
$d1->set('time',[0,0,0]);
$d2->set('date',[$y,$m,$d,23,59,59]);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2 };
}
next;
}
# Date
$err = $d1->parse($event[0]);
if (! $err) {
if ($$d1{'data'}{'def'}[0] eq '') {
# Date (no year)
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'date',
'name' => $name,
'beg' => $event[0],
'delta' => $hrM1
};
} else {
# Date (year)
my $d2 = $d1->calc($hrM1);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2
};
}
next;
}
# Recur
my $r = $self->new_recur();
$err = $r->parse($event[0]);
if ($err) {
warn "ERROR: invalid event definition (must be Date, YMD, YM, or Recur)\n"
. " $string\n";
next;
}
my @d = $r->dates();
if (@d) {
foreach my $d (@d) {
my $d2 = $d->calc($hrM1);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2
};
}
} else {
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'recur',
'name' => $name,
'recur' => $r,
'delta' => $hrM1
};
}
} elsif ($#event == 1) {
my($o1,$o2) = @event;
# YMD;YMD
# YM;YM
my $d1 = $self->new_date();
my $err = $d1->parse_date($o1);
if (! $err) {
my $d2 = $self->new_date();
$err = $d2->parse_date($o2);
if ($err) {
warn "ERROR: invalid event definition (must be YMD;YMD or YM;YM)\n"
. " $string\n";
next;
} elsif ($$d1{'data'}{'def'}[0] ne $$d2{'data'}{'def'}[0]) {
warn "ERROR: invalid event definition (YMD;YM or YM;YMD not allowed)\n"
. " $string\n";
next;
}
if ($$d1{'data'}{'def'}[0] eq '') {
# YM;YM
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'ym',
'name' => $name,
'beg' => $o1,
'end' => $o2
};
} else {
# YMD;YMD
$d1->set('time',[0,0,0]);
$d2->set('time',[23,59,59]);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2 };
}
next;
}
# Date;Date
# Date;Delta
$err = $d1->parse($o1);
if (! $err) {
my $d2 = $self->new_date();
$err = $d2->parse($o2,'nodelta');
if (! $err) {
# Date;Date
if ($$d1{'data'}{'def'}[0] ne $$d2{'data'}{'def'}[0]) {
warn "ERROR: invalid event definition (year must be absent or\n"
. " included in both dats in Date;Date)\n"
. " $string\n";
next;
}
if ($$d1{'data'}{'def'}[0] eq '') {
# Date (no year)
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'date',
'name' => $name,
'beg' => $o1,
'end' => $o2
};
} else {
# Date (year)
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2
};
}
next;
}
# Date;Delta
my $del = $self->new_delta();
$err = $del->parse($o2);
if ($err) {
warn "ERROR: invalid event definition (must be Date;Date or\n"
. " Date;Delta) $string\n";
next;
}
$del = $del->calc($M1);
if ($$d1{'data'}{'def'}[0] eq '') {
# Date (no year)
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'date',
'name' => $name,
'beg' => $o1,
'delta' => $del
};
} else {
# Date (year)
$d2 = $d1->calc($del);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2
};
}
next;
}
# Recur;Delta
my $r = $self->new_recur();
$err = $r->parse($o1);
my $del = $self->new_delta();
if (! $err) {
$err = $del->parse($o2);
}
if ($err) {
warn "ERROR: invalid event definition (must be Date;Date, YMD;YMD, "
. " YM;YM, Date;Delta, or Recur;Delta)\n"
. " $string\n";
next;
}
$del = $del->calc($M1);
my @d = $r->dates();
if (@d) {
foreach my $d1 (@d) {
my $d2 = $d1->calc($del);
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'specified',
'name' => $name,
'beg' => $d1,
'end' => $d2
};
}
} else {
$$dmb{'data'}{'events'}{$i++} = { 'type' => 'recur',
'name' => $name,
'recur' => $r,
'delta' => $del
};
}
} else {
warn "ERROR: invalid event definition\n"
. " $string\n";
next;
}
}
return;
}
1;
# Local Variables:
# mode: cperl
# indent-tabs-mode: nil
# cperl-indent-level: 3
# cperl-continued-statement-offset: 2
# cperl-continued-brace-offset: 0
# cperl-brace-offset: 0
# cperl-brace-imaginary-offset: 0
# cperl-label-offset: 0
# End: