|
Server : Apache/2.4.62 System : FreeBSD fbsdweb2.web.rcn.net 14.1-RELEASE FreeBSD 14.1-RELEASE releng/14.1-n267679-10e31f0946d8 GENERIC amd64 User : www ( 80) PHP Version : 8.3.8 Disable Function : NONE Directory : /domains/compasssysweb/calendar/CalciumDir39/Calendar/ |
Upload File : |
# Copyright 1999-2003, Fred Steinberg, Brown Bear Software
# RepeatInfo
# RepeatInfo holds information needed to handle the repeating part of
# repeating events. An event that repeats would have one of these objects
# in it.
package RepeatInfo;
use strict;
use vars '$AUTOLOAD';
use Calendar::Date;
sub new {
my $class = shift;
my ($startDate, $endDate, $period, $frequency,
$monthWeek, $monthMonth, $skipWeekends) = @_;
my $self = {};
bless $self, $class;
# If you want open-ended repeating, use Date->openPast() and openFuture()
$self->{'startDate'} = Date->new ($startDate) || Date->openPast();
$self->{'endDate'} = Date->new ($endDate) || Date->openFuture();
$self->{'period'} = $period if $period;
$self->{'frequency'} = $frequency if $frequency;
$self->{'monthWeek'} = $monthWeek if $monthWeek;
$self->{'monthMonth'} = $monthMonth if $monthMonth;
$self->{'skipWeekends'} = $skipWeekends if $skipWeekends;
$self->{'monthDay'} = $self->{'startDate'}->dayOfWeek if $monthWeek;
# $self->{'exclusions'} = [];
# Fixup the period; either a 'day' (or 'dayBanner'), 'week', 'month', or
# 'year', or a list of day of the week integers
if ($period && $period !~ /day|week|month|year/i) {
my @days = split /\s+/, $self->{'period'};
$self->{'period'} = \@days;
}
# And the same for $monthWeek; either a single int, a space separated list
if ($monthWeek) {
my @weeks = split /\s+/, $monthWeek;
$self->{'monthWeek'} = \@weeks;
}
$self;
}
# Get/set methods done by AUTOLOAD
sub AUTOLOAD {
my $self = shift;
my $name = $AUTOLOAD;
$name =~ s/.*://; # get rid of package names, etc.
return unless $name =~ /[^A-Z]/; # ignore all cap methods; e.g. DESTROY
# Make sure it's a valid field, eh wot?
die "Bad Field Name to RepeatInfo! '$name'\n"
unless {period => 1,
frequency => 1,
startDate => 1,
endDate => 1,
monthWeek => 1,
monthMonth => 1,
monthDay => 1,
skipWeekends => 1}->{$name};
# exclusions
$self->{$name} = shift if (@_);
$self->{$name};
}
# See if we land on the specified date
sub applies {
my $self = shift;
my ($date) = @_;
# return false if we're out of range
return undef unless ($self->{'startDate'} <= $date) and
($self->{'endDate'} >= $date);
# return false if this date is in our list of excluded dates (i.e. this
# date was specifically deleted from the repeating event)
return undef if $self->excluded ($date);
# return false if it's a weekend and we're skipping weekends
return undef if $self->skipWeekends && $date->isWeekend;
# otherwise see if we fall on the specified date
# First we check for the Repeat Every Nth (day|week|year) type
if (defined $self->{'period'} && !ref ($self->{'period'})) {
# If repeating by day, it's easy.
if ($self->{'period'} =~ /day/i) {
# check degenerate case first
return 1 if $self->{'frequency'} == 1;
# find how many days since the start; if diff mod freq is 0, a hit
my $delta = $self->{'startDate'}->deltaDays ($date);
return !($delta % $self->{'frequency'});
}
# If repeating by month or year, it's also easy. Notice first that
# if the day (and month for year) doesn't match, we return false
# right away.
if ($self->{'period'} =~ /month/i) {
return undef if ($self->{'startDate'}->day() != $date->day());
return 1 if $self->{'frequency'} == 1;
# Ok, find how many months apart we are, and mod it by the
# frequency. Always 12 months in a year.
my $delta = $self->{'startDate'}->deltaMonths ($date);
return !($delta % $self->{'frequency'});
}
if ($self->{'period'} =~ /year/i) {
return undef if ($self->{'startDate'}->day() != $date->day() or
$self->{'startDate'}->month() != $date->month());
return 1 if $self->{'frequency'} == 1;
my $delta = $self->{'startDate'}->deltaYears ($date);
return !($delta % $self->{'frequency'});
}
if ($self->{'period'} =~ /week/i) {
return undef if ($self->{'startDate'}->dayOfWeek() !=
$date->dayOfWeek());
return 1 if $self->{'frequency'} == 1;
my $delta = $self->{'startDate'}->deltaWeeks ($date);
return !($delta % $self->{'frequency'});
}
return undef;
} elsif (defined $self->{'period'} && ref ($self->{'period'})) {
# OK, it must be of the repeat on every M,W,F type
my $dow = $date->dayOfWeek ();
return undef unless grep {/$dow/} @{$self->{'period'}};
return 1 if $self->{'frequency'} == 1;
my $delta = $self->{'startDate'}->deltaWeeks ($date, 1);
return !($delta % $self->{'frequency'});
}
# Well now, me must be repeating in the special month way, e.g. First
# Tuesday of every 3rd month.
# Lets see if we're in the right month
if ($self->{'monthMonth'} > 1) {
my $delta = $self->{'startDate'}->deltaMonths ($date);
return undef if ($delta % $self->{'monthMonth'});
}
# Ok, we're on the right month, check the nth occurence in month. Note
# that the 5th occurrence means the last, which might be the 4th.
# montWeek can be a list, which means, e.g. "1st and 3rd xday"
foreach (@{$self->{'monthWeek'}}) {
my $nth = Date->getNthWeekday ($date->year, $date->month,
$self->{'monthDay'}, $_);
return 1 if ($nth and $date == $nth);
}
return undef;
}
# Find all dates this event falls on in the range, add to the hash passed in.
# This is fairly gross, and needs to be rewritten properly.
sub addToDateHash {
my $self = shift;
my ($hash, $fromDate, $toDate, $theEvent, $prefs) = @_;
# return right away if outside our range
return if (($self->{'startDate'} > $toDate) or
($self->{'endDate'} < $fromDate));
# OK, now the hard part. Do the 'Repeat Every Nth (day|week|year)' type
if (defined $self->{'period'}) {
# First, find the limits of the range
# Get date of earliest event we could possibly care about.
my ($rangeStart, $rangeEnd);
if ($fromDate <= $self->{'startDate'}) {
$rangeStart = Date->new ($self->{'startDate'});
} else {
$rangeStart = Date->new ($fromDate);
}
# Similarly, for latest event.
if ($toDate > $self->{'endDate'}) {
$rangeEnd = $self->{'endDate'};
} else {
$rangeEnd = Date->new ($toDate);
}
# If repeating by day
if ($self->{'period'} =~ /day/i) {
# find how many days since the repeat start to range start
my $delta = $self->{'startDate'}->deltaDays ($rangeStart);
my $offset = $delta % $self->{'frequency'};
if ($offset) {
$offset = $self->{'frequency'} - $offset;
}
# and add to the hash
for ($rangeStart += $offset;
$rangeStart <= $rangeEnd;
$rangeStart += $self->{'frequency'}) {
next if $self->excluded ($rangeStart);
next if $self->skipWeekends && $rangeStart->isWeekend;
$hash->{"$rangeStart"} = [] unless $hash->{"$rangeStart"};
push @{$hash->{"$rangeStart"}}, $theEvent;
}
return;
}
# If repeating by month
if ($self->{'period'} =~ /month/i) {
my $repeatOnDay = $self->{'startDate'}->day();
# Go to first of next month if we're already past the day of
# the repeat
if ($rangeStart->day > $repeatOnDay) {
$rangeStart = $rangeStart->firstOfMonth->addMonths (1);
return if ($rangeStart > $rangeEnd);
}
# find how many months since the repeat start to range start
my $delta = $self->{'startDate'}->deltaMonths ($rangeStart);
# mod by the frequency, to find first month
my $offset = $delta % $self->{'frequency'};
$offset = $self->{'frequency'} - $offset if $offset;
# and add to the hash
my $thisDay = $rangeStart;
$thisDay->addMonths ($offset);
if ($thisDay->day() > $repeatOnDay) {
$thisDay->addMonths ($self->{'frequency'});
}
my $theDay = Date->new ($thisDay);
my $i = 1;
while ($theDay <= $rangeEnd) {
# use last day of month, if not enough days in month
if ($theDay->daysInMonth < $repeatOnDay) {
$theDay = Date->new ($theDay->year,
$theDay->month,
$theDay->daysInMonth);
} else {
$theDay = Date->new ($theDay->year(),
$theDay->month(),
$repeatOnDay);
}
unless ($self->excluded ($theDay) or
$self->skipWeekends && $theDay->isWeekend) {
$hash->{"$theDay"} = [] unless $hash->{"$theDay"};
push @{$hash->{"$theDay"}}, $theEvent;
}
# next if $self->excluded ($theDay);
# next if $self->skipWeekends && $theDay->isWeekend;
# $hash->{"$theDay"} = [] unless $hash->{"$theDay"};
# push @{$hash->{"$theDay"}}, $theEvent;
# need this so addMonths adjusts for months with < 31 days
# if 31st (or 30th for Feb.) is used.
$theDay = Date->new ($thisDay);
$theDay->addMonths ($i++ * $self->{frequency});
}
return;
}
# If repeating by year; one day, every N years
if ($self->{'period'} =~ /year/i) {
my $monthDay = Date->new($self->{'startDate'});
$monthDay->year ($rangeStart->year());
# If our range is within the same year (typically it will be),
# return right away unless the repeat date is in our range.
if ($rangeStart->year() == $rangeEnd->year()) {
return if ($monthDay < $fromDate) || ($monthDay > $toDate);
}
# find how many years from start of repeat to start of range
my $delta = $self->{'startDate'}->deltaYears ($rangeStart);
# if range crosses into next year, adjust delta unless we occur
# in range in the earlier year
if ($rangeStart->year != $rangeEnd->year and
$monthDay < $rangeStart) {
$delta++;
}
my $offset = $delta % $self->{'frequency'};
if ($offset) {
$offset = $self->{'frequency'} - $offset;
}
my $start = Date->new ($rangeStart);
# Get correct starting year. What a bother.
if ($rangeStart > $self->{'startDate'}) {
$start->month ($self->{'startDate'}->month());
$start->day ($self->{'startDate'}->day());
$start->addYears(1) if ($monthDay < $rangeStart);
}
# And add to the hash, if the date falls in our range!
for ($start->addYears ($offset);
$start <= $rangeEnd;
$start->addYears ($self->{'frequency'})) {
next if $self->excluded ($start);
next if $self->skipWeekends && $start->isWeekend;
$hash->{"$start"} = [] unless $hash->{"$start"};
push @{$hash->{"$start"}}, $theEvent;
}
}
# If repeating by week; can be 1 day each week, or a list of days
# of the week. And don't forget about weeks that start on Sunday vs
# Monday. Oh, such a pain.
if ($self->{'period'} =~ /week/i or ref ($self->{'period'})) {
my @dayList;
if (ref ($self->{'period'})) {
@dayList = sort @{$self->{'period'}};
} else {
push @dayList, $self->{'startDate'}->dayOfWeek;
}
my $dayOfWeek = $dayList[0];
# Set the Repeat Start to be the date of the first specified
# day (so, if the repeat is specified as M W F, but RepeatStart
# is a Friday, we go back to Monday. (Unless it's a case like
# repeat every Sat. and Sun., but start date is a Wednesday;
# then we go forward.)
my $repeatStart = Date->new ($self->{'startDate'});
if ($repeatStart->dayOfWeek > $dayOfWeek) {
while ($repeatStart->dayOfWeek != $dayOfWeek) {
$repeatStart--;
}
} else {
while ($repeatStart->dayOfWeek != $dayOfWeek) {
$repeatStart++;
}
}
# Set RangeStart to the first of the week
my $first = $prefs->StartWeekOn || 7;
my $theStart = $rangeStart->firstOfWeek ($first);
# Compute num weeks from Repeat Start to this date, and mod by the
# frequency, to find the week with the first event
my $deltaWeeks = $repeatStart->deltaWeeks ($theStart, $first);
my $offset = $deltaWeeks % $self->{'frequency'};
if ($offset) {
$offset = $self->{'frequency'} - $offset;
}
# OK, lets account for weeks that don't start on 1.
# If not Monday, we assume it starts on Sunday.
if ($first != 1) {
# @dayList = map {(($_+7-$first) % 7)+1} @dayList;
# The GUI only allows starting on Sunday, and don't mod,
# since we want Sunday and Saturday to stick together...ack
# Shift each day
# If multiple days of week, this Sunday is really next week
if (@dayList > 1) {
@dayList = map {$_ + 1} @dayList;
} else {
@dayList = map {$_ == 7 ? 1 : $_ + 1} @dayList;
}
$first = 1;
}
# Finally, add the ding-dang events
for ($theStart->addWeeks ($offset);
$theStart <= $rangeEnd;
$theStart->addWeeks ($self->{'frequency'})) {
foreach (@dayList) {
my $fnord = Date->new ($theStart - $first + $_);
next if $self->excluded ($fnord);
next if $self->skipWeekends && $fnord->isWeekend;
if ($fnord >= $rangeStart and $fnord <= $rangeEnd) {
$hash->{"$fnord"} = [] unless $hash->{"$fnord"};
push @{$hash->{"$fnord"}}, $theEvent;
}
}
}
return;
}
return;
}
# Well now, me must be repeating in the special month way, e.g. First
# and Third Tuesday of every 3rd month.
# Make range as small as needed
my $theDay = Date->new ($fromDate);
$theDay = Date->new ($self->{'startDate'})
if ($theDay < $self->{'startDate'});
my $delta = $self->{'startDate'}->deltaMonths ($theDay);
my $offset = $delta % $self->{'monthMonth'};
$theDay->addMonths (-$offset);
$toDate = $self->{'endDate'} if ($toDate > $self->{'endDate'});
while ($theDay <= $toDate) {
foreach my $n (@{$self->{'monthWeek'}}) {
my $nth = Date->getNthWeekday ($theDay->year(), $theDay->month(),
$self->{'monthDay'}, $n);
next unless $nth;
$theDay = $nth;
next if $self->excluded ($theDay);
next if $self->skipWeekends && $theDay->isWeekend;
if ($theDay >= $fromDate and $theDay >= $self->{'startDate'} and
$theDay <= $toDate) {
$hash->{"$theDay"} = [] unless $hash->{"$theDay"};
push @{$hash->{"$theDay"}}, $theEvent;
}
}
$theDay->day(1);
$theDay->addMonths ($self->{'monthMonth'});
}
}
# Use this to keep track of which instances of a repeating event we deleted
sub excludeThisInstance {
my $self = shift;
my ($date) = @_;
$self->{exclusions} = [] unless $self->{exclusions};
push @{$self->{exclusions}}, $date;
}
# Return true if date is in excluded list
sub excluded {
my $self = shift;
my ($date) = @_;
if ($self->{exclusions}) {
foreach (@{$self->{exclusions}}) {
return 1 if ($date == $_);
}
}
return undef;
}
# Set or Get list of excluded dates; return ref to list of Date objects
sub exclusionList {
my $self = shift;
my $listRef = shift;
return ($self->{exclusions} || []) unless defined $listRef;
$self->{exclusions} = $listRef;
}
# Find the next N (e.g. 4) occurrences of a repeating event. We'll be lazy
# and stupid and avoid writing more code by calling addToDateHash. Return a
# ref to a hash w/date=>eventlistref
sub nextNOccurrences {
my $self = shift;
my ($event, $n, $startFromDate, $prefs) = @_;
$n ||= 1;
$startFromDate ||= Date->new; # today
# Guess a toDate based on repeat period. Make it big enough to get
# enough events, but small enough to not waste time filling the hash
# w/too many events. E.g., for N=5, repeat by week, the date range will
# be 150 days, or about 5 months (so "repeat every 5th week" will work.)
my $size = 7;
for ($self->period || '') {
$size = /day/ && 7
|| /week/ && 30
|| /month/ && 365
|| /year/ && 365*5
|| 7;
}
my $toDate = $startFromDate + $n * $size;
$toDate = $self->endDate if ($toDate > $self->endDate);
my %hash;
while (keys %hash < $n and
$startFromDate <= $self->endDate) {
$self->addToDateHash (\%hash, $startFromDate, $toDate, $event, $prefs);
$startFromDate = $toDate + 1;
$toDate += $n * $size;
$toDate = $self->endDate if ($toDate > $self->endDate);
}
my %returnHash;
foreach (sort {Date->new($a) <=> Date->new($b)} keys %hash) {
$returnHash{$_} = $hash{$_};
last unless --$n;
}
\%returnHash;
}
sub bannerize {
my $self = shift;
my $f = $self->frequency || 0;
my $p = $self->period || '';
return ($f == 1 and $p =~ /dayBanner/i);
}
1;