Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F102047206
PhutilCalendarRecurrenceRule.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sun, Feb 16, 14:00
Size
26 KB
Mime Type
text/x-php
Expires
Tue, Feb 18, 14:00 (1 d, 23 h)
Engine
blob
Format
Raw Data
Handle
24270191
Attached To
rPHU libphutil
PhutilCalendarRecurrenceRule.php
View Options
<?php
final
class
PhutilCalendarRecurrenceRule
extends
PhutilCalendarRecurrenceSource
{
private
$startDateTime
;
private
$frequency
;
private
$frequencyScale
;
private
$interval
=
1
;
private
$bySecond
=
array
();
private
$byMinute
=
array
();
private
$byHour
=
array
();
private
$byDay
=
array
();
private
$byMonthDay
=
array
();
private
$byYearDay
=
array
();
private
$byWeekNumber
=
array
();
private
$byMonth
=
array
();
private
$bySetPosition
=
array
();
private
$weekStart
=
self
::
WEEKDAY_MONDAY
;
private
$cursorSecond
;
private
$cursorMinute
;
private
$cursorHour
;
private
$cursorWeek
;
private
$cursorDay
;
private
$cursorMonth
;
private
$cursorYear
;
private
$setSeconds
;
private
$setMinutes
;
private
$setHours
;
private
$setDays
;
private
$setMonths
;
private
$setYears
;
private
$stateSecond
;
private
$stateMinute
;
private
$stateHour
;
private
$stateDay
;
private
$stateMonth
;
private
$stateYear
;
private
$initialMonth
;
private
$initialYear
;
const
FREQUENCY_SECONDLY
=
'SECONDLY'
;
const
FREQUENCY_MINUTELY
=
'MINUTELY'
;
const
FREQUENCY_HOURLY
=
'HOURLY'
;
const
FREQUENCY_DAILY
=
'DAILY'
;
const
FREQUENCY_WEEKLY
=
'WEEKLY'
;
const
FREQUENCY_MONTHLY
=
'MONTHLY'
;
const
FREQUENCY_YEARLY
=
'YEARLY'
;
const
SCALE_SECONDLY
=
1
;
const
SCALE_MINUTELY
=
2
;
const
SCALE_HOURLY
=
3
;
const
SCALE_DAILY
=
4
;
const
SCALE_WEEKLY
=
5
;
const
SCALE_MONTHLY
=
6
;
const
SCALE_YEARLY
=
7
;
const
WEEKDAY_SUNDAY
=
'SU'
;
const
WEEKDAY_MONDAY
=
'MO'
;
const
WEEKDAY_TUESDAY
=
'TU'
;
const
WEEKDAY_WEDNESDAY
=
'WE'
;
const
WEEKDAY_THURSDAY
=
'TH'
;
const
WEEKDAY_FRIDAY
=
'FR'
;
const
WEEKDAY_SATURDAY
=
'SA'
;
const
WEEKINDEX_SUNDAY
=
0
;
const
WEEKINDEX_MONDAY
=
1
;
const
WEEKINDEX_TUESDAY
=
2
;
const
WEEKINDEX_WEDNESDAY
=
3
;
const
WEEKINDEX_THURSDAY
=
4
;
const
WEEKINDEX_FRIDAY
=
5
;
const
WEEKINDEX_SATURDAY
=
6
;
private
static
function
getAllWeekdayConstants
()
{
return
array_keys
(
self
::
getWeekdayIndexMap
());
}
private
static
function
getWeekdayIndexMap
()
{
static
$map
=
array
(
self
::
WEEKDAY_SUNDAY
=>
self
::
WEEKINDEX_SUNDAY
,
self
::
WEEKDAY_MONDAY
=>
self
::
WEEKINDEX_MONDAY
,
self
::
WEEKDAY_TUESDAY
=>
self
::
WEEKINDEX_TUESDAY
,
self
::
WEEKDAY_WEDNESDAY
=>
self
::
WEEKINDEX_WEDNESDAY
,
self
::
WEEKDAY_THURSDAY
=>
self
::
WEEKINDEX_THURSDAY
,
self
::
WEEKDAY_FRIDAY
=>
self
::
WEEKINDEX_FRIDAY
,
self
::
WEEKDAY_SATURDAY
=>
self
::
WEEKINDEX_SATURDAY
,
);
return
$map
;
}
private
static
function
getWeekdayIndex
(
$weekday
)
{
$map
=
self
::
getWeekdayIndexMap
();
if
(!
isset
(
$map
[
$weekday
]))
{
$constants
=
array_keys
(
$map
);
throw
new
Exception
(
pht
(
'Weekday "%s" is not a valid weekday constant. Valid constants '
.
'are: %s.'
,
implode
(
', '
,
$constants
)));
}
return
$map
[
$weekday
];
}
public
function
setStartDateTime
(
PhutilCalendarDateTime
$start
)
{
$this
->
startDateTime
=
$start
;
return
$this
;
}
public
function
getStartDateTime
()
{
return
$this
->
startDateTime
;
}
public
function
setFrequency
(
$frequency
)
{
static
$map
=
array
(
self
::
FREQUENCY_SECONDLY
=>
self
::
SCALE_SECONDLY
,
self
::
FREQUENCY_MINUTELY
=>
self
::
SCALE_MINUTELY
,
self
::
FREQUENCY_HOURLY
=>
self
::
SCALE_HOURLY
,
self
::
FREQUENCY_DAILY
=>
self
::
SCALE_DAILY
,
self
::
FREQUENCY_WEEKLY
=>
self
::
SCALE_WEEKLY
,
self
::
FREQUENCY_MONTHLY
=>
self
::
SCALE_MONTHLY
,
self
::
FREQUENCY_YEARLY
=>
self
::
SCALE_YEARLY
,
);
if
(
empty
(
$map
[
$frequency
]))
{
throw
new
Exception
(
pht
(
'RRULE FREQ "%s" is invalid. Valid frequencies are: %s.'
,
$frequency
,
implode
(
', '
,
array_keys
(
$map
))));
}
$this
->
frequency
=
$frequency
;
$this
->
frequencyScale
=
$map
[
$frequency
];
return
$this
;
}
public
function
getFrequency
()
{
return
$this
->
frequency
;
}
public
function
getFrequencyScale
()
{
return
$this
->
frequencyScale
;
}
public
function
setInterval
(
$interval
)
{
if
(!
is_int
(
$interval
))
{
throw
new
Exception
(
pht
(
'RRULE INTERVAL "%s" is invalid: interval must be an integer.'
,
$interval
));
}
if
(
$interval
<
1
)
{
throw
new
Exception
(
pht
(
'RRULE INTERVAL "%s" is invalid: interval must be 1 or more.'
,
$interval
));
}
$this
->
interval
=
$interval
;
return
$this
;
}
public
function
getInterval
()
{
return
$this
->
interval
;
}
public
function
setBySecond
(
array
$by_second
)
{
$this
->
assertByRange
(
'BYSECOND'
,
$by_second
,
0
,
60
);
$this
->
bySecond
=
array_fuse
(
$by_second
);
return
$this
;
}
public
function
getBySecond
()
{
return
$this
->
bySecond
;
}
public
function
setByMinute
(
array
$by_minute
)
{
$this
->
assertByRange
(
'BYMINUTE'
,
$by_minute
,
0
,
59
);
$this
->
byMinute
=
array_fuse
(
$by_minute
);
return
$this
;
}
public
function
getByMinute
()
{
return
$this
->
byMinute
;
}
public
function
setByHour
(
array
$by_hour
)
{
$this
->
assertByRange
(
'BYHOUR'
,
$by_hour
,
0
,
23
);
$this
->
byHour
=
array_fuse
(
$by_hour
);
return
$this
;
}
public
function
getByHour
()
{
return
$this
->
byHour
;
}
public
function
setByDay
(
array
$by_day
)
{
$constants
=
self
::
getAllWeekdayConstants
();
$constants
=
implode
(
'|'
,
$constants
);
$pattern
=
'/^(?:[+-]?([1-9]
\d
?))?('
.
$constants
.
')
\z
/'
;
foreach
(
$by_day
as
$value
)
{
$matches
=
null
;
if
(!
preg_match
(
$pattern
,
$value
,
$matches
))
{
throw
new
Exception
(
pht
(
'RRULE BYDAY value "%s" is invalid: rule part must be in the '
.
'expected form (like "MO", "-3TH", or "+2SU").'
,
$value
));
}
// The maximum allowed value is 53, which corresponds to "the 53rd
// Monday every year" or similar when evaluated against a YEARLY rule.
$maximum
=
53
;
$magnitude
=
(
int
)
$matches
[
1
];
if
(
$magnitude
>
$maximum
)
{
throw
new
Exception
(
pht
(
'RRULE BYDAY value "%s" has an offset with magnitude "%s", but '
.
'the maximum permitted value is "%s".'
,
$value
,
$magnitude
,
$maximum
));
}
}
$this
->
byDay
=
array_fuse
(
$by_day
);
return
$this
;
}
public
function
getByDay
()
{
return
$this
->
byDay
;
}
public
function
setByMonthDay
(
array
$by_month_day
)
{
$this
->
assertByRange
(
'BYMONTHDAY'
,
$by_month_day
,
-
31
,
31
,
false
);
$this
->
byMonthDay
=
array_fuse
(
$by_month_day
);
return
$this
;
}
public
function
getByMonthDay
()
{
return
$this
->
byMonthDay
;
}
public
function
setByYearDay
(
$by_year_day
)
{
$this
->
assertByRange
(
'BYYEARDAY'
,
$by_year_day
,
-
366
,
366
,
false
);
$this
->
byYearDay
=
array_fuse
(
$by_year_day
);
return
$this
;
}
public
function
getByYearDay
()
{
return
$this
->
byYearDay
;
}
public
function
setByMonth
(
array
$by_month
)
{
$this
->
assertByRange
(
'BYMONTH'
,
$by_month
,
1
,
12
);
$this
->
byMonth
=
array_fuse
(
$by_month
);
return
$this
;
}
public
function
getByMonth
()
{
return
$this
->
byMonth
;
}
public
function
setByWeekNumber
(
array
$by_week_number
)
{
$this
->
assertByRange
(
'BYWEEKNO'
,
$by_week_number
,
-
53
,
53
,
false
);
$this
->
byWeekNumber
=
$by_week_number
;
return
$this
;
}
public
function
getByWeekNumber
()
{
return
$this
->
byWeekNumber
;
}
public
function
setBySetPosition
(
array
$by_set_position
)
{
$this
->
assertByRange
(
'BYSETPOS'
,
$by_set_position
,
-
366
,
366
,
false
);
$this
->
bySetPosition
=
$by_set_position
;
return
$this
;
}
public
function
getBySetPosition
()
{
return
$this
->
bySetPosition
;
}
public
function
setWeekStart
(
$week_start
)
{
// Make sure this is a valid weekday constant.
self
::
getWeekdayIndex
(
$week_start
);
$this
->
weekStart
=
$week_start
;
return
$this
;
}
public
function
getWeekStart
()
{
return
$this
->
weekStart
;
}
public
function
resetSource
()
{
$date
=
$this
->
getStartDateTime
();
$this
->
cursorSecond
=
$date
->
getSecond
();
$this
->
cursorMinute
=
$date
->
getMinute
();
$this
->
cursorHour
=
$date
->
getHour
();
// TODO: Figure this out.
$this
->
cursorWeek
=
null
;
$this
->
cursorDay
=
$date
->
getDay
();
$this
->
cursorMonth
=
$date
->
getMonth
();
$this
->
cursorYear
=
$date
->
getYear
();
$this
->
setSeconds
=
array
();
$this
->
setMinutes
=
array
();
$this
->
setHours
=
array
();
$this
->
setDays
=
array
();
$this
->
setMonths
=
array
();
$this
->
setYears
=
array
();
$this
->
stateSecond
=
null
;
$this
->
stateMinute
=
null
;
$this
->
stateHour
=
null
;
$this
->
stateDay
=
null
;
$this
->
stateMonth
=
null
;
$this
->
stateYear
=
null
;
$this
->
initialMonth
=
$this
->
cursorMonth
;
$this
->
initialYear
=
$this
->
cursorYear
;
}
public
function
getNextEvent
(
$cursor
)
{
$date
=
$this
->
getStartDateTime
();
$all_day
=
$date
->
getIsAllDay
();
if
(
$all_day
)
{
$this
->
nextDay
();
}
else
{
$this
->
nextSecond
();
}
$result
=
id
(
new
PhutilCalendarAbsoluteDateTime
())
->
setViewerTimezone
(
$this
->
getViewerTimezone
())
->
setYear
(
$this
->
stateYear
)
->
setMonth
(
$this
->
stateMonth
)
->
setDay
(
$this
->
stateDay
);
if
(
$all_day
)
{
$result
->
setIsAllDay
(
true
);
}
else
{
$result
->
setHour
(
$this
->
stateHour
)
->
setMinute
(
$this
->
stateMinute
)
->
setSecond
(
$this
->
stateSecond
);
}
return
$result
;
}
protected
function
nextSecond
()
{
if
(
$this
->
setSeconds
)
{
$this
->
stateSecond
=
array_pop
(
$this
->
setSeconds
);
return
;
}
$frequency
=
$this
->
getFrequency
();
$interval
=
$this
->
getInterval
();
$is_secondly
=
(
$frequency
==
self
::
FREQUENCY_SECONDLY
);
$by_second
=
$this
->
getBySecond
();
$by_setpos
=
$this
->
getBySetPosition
();
while
(!
$this
->
setSeconds
)
{
$this
->
nextMinute
();
if
(
$is_secondly
||
$by_second
)
{
$seconds
=
$this
->
newSecondsSet
(
(
$is_secondly
?
$interval
:
1
),
$by_second
);
}
else
{
$seconds
=
array
(
$this
->
cursorSecond
,
);
}
if
(
$is_secondly
&&
$by_setpos
)
{
$seconds
=
$this
->
applySetPos
(
$seconds
,
$by_setpos
);
}
$this
->
setSeconds
=
array_reverse
(
$seconds
);
}
$this
->
stateSecond
=
array_pop
(
$this
->
setSeconds
);
}
protected
function
nextMinute
()
{
if
(
$this
->
setMinutes
)
{
$this
->
stateMinute
=
array_pop
(
$this
->
setMinutes
);
return
;
}
$frequency
=
$this
->
getFrequency
();
$interval
=
$this
->
getInterval
();
$scale
=
$this
->
getFrequencyScale
();
$is_minutely
=
(
$frequency
===
self
::
FREQUENCY_MINUTELY
);
$by_minute
=
$this
->
getByMinute
();
$by_setpos
=
$this
->
getBySetPosition
();
while
(!
$this
->
setMinutes
)
{
$this
->
nextHour
();
if
(
$is_minutely
||
$by_minute
)
{
$minutes
=
$this
->
newMinutesSet
(
(
$is_minutely
?
$interval
:
1
),
$by_minute
);
}
else
if
(
$scale
<
self
::
SCALE_MINUTELY
)
{
$minutes
=
$this
->
newMinutesSet
(
1
,
array
());
}
else
{
$minutes
=
array
(
$this
->
cursorMinute
,
);
}
if
(
$is_minutely
&&
$by_setpos
)
{
$minutes
=
$this
->
applySetPos
(
$minutes
,
$by_setpos
);
}
$this
->
setMinutes
=
array_reverse
(
$minutes
);
}
$this
->
stateMinute
=
array_pop
(
$this
->
setMinutes
);
}
protected
function
nextHour
()
{
if
(
$this
->
setHours
)
{
$this
->
stateHour
=
array_pop
(
$this
->
setHours
);
return
;
}
$frequency
=
$this
->
getFrequency
();
$interval
=
$this
->
getInterval
();
$scale
=
$this
->
getFrequencyScale
();
$is_hourly
=
(
$frequency
===
self
::
FREQUENCY_HOURLY
);
$by_hour
=
$this
->
getByHour
();
$by_setpos
=
$this
->
getBySetPosition
();
while
(!
$this
->
setHours
)
{
$this
->
nextDay
();
if
(
$is_hourly
||
$by_hour
)
{
$hours
=
$this
->
newHoursSet
(
(
$is_hourly
?
$interval
:
1
),
$by_hour
);
}
else
if
(
$scale
<
self
::
SCALE_HOURLY
)
{
$hours
=
$this
->
newHoursSet
(
1
,
array
());
}
else
{
$hours
=
array
(
$this
->
cursorHour
,
);
}
if
(
$is_hourly
&&
$by_setpos
)
{
$hours
=
$this
->
applySetPos
(
$hours
,
$by_setpos
);
}
$this
->
setHours
=
array_reverse
(
$hours
);
}
$this
->
stateHour
=
array_pop
(
$this
->
setHours
);
}
protected
function
nextDay
()
{
if
(
$this
->
setDays
)
{
$this
->
stateDay
=
array_pop
(
$this
->
setDays
);
return
;
}
$frequency
=
$this
->
getFrequency
();
$interval
=
$this
->
getInterval
();
$scale
=
$this
->
getFrequencyScale
();
$is_daily
=
(
$frequency
===
self
::
FREQUENCY_DAILY
);
$is_weekly
=
(
$frequency
===
self
::
FREQUENCY_WEEKLY
);
$by_day
=
$this
->
getByDay
();
$by_monthday
=
$this
->
getByMonthDay
();
$by_yearday
=
$this
->
getByYearDay
();
$by_weekno
=
$this
->
getByWeekNumber
();
$by_setpos
=
$this
->
getBySetPosition
();
$week_start
=
$this
->
getWeekStart
();
while
(!
$this
->
setDays
)
{
$this
->
nextMonth
();
if
(
$is_daily
||
$by_day
||
$by_monthday
||
$by_yearday
||
$by_weekno
)
{
$weeks
=
$this
->
newDaysSet
(
(
$is_daily
?
$interval
:
null
),
(
$is_weekly
?
$interval
:
null
),
$by_day
,
$by_monthday
,
$by_yearday
,
$by_weekno
,
$week_start
);
}
else
if
(
$scale
<
self
::
SCALE_DAILY
)
{
$weeks
=
$this
->
newDaysSet
(
1
,
null
,
array
(),
array
(),
array
(),
array
(),
$week_start
);
}
else
{
// The cursor day may not actually exist in the current month, so
// make sure the day is valid before we generate a set which contains
// it.
$year_map
=
$this
->
getYearMap
(
$this
->
stateYear
,
$week_start
);
if
(
$this
->
cursorDay
>
$year_map
[
'monthDays'
][
$this
->
stateMonth
])
{
$weeks
=
array
(
array
(),
);
}
else
{
$weeks
=
array
(
array
(
$this
->
cursorDay
),
);
}
}
// Apply weekly BYSETPOS, if one exists.
if
(
$is_weekly
&&
$by_setpos
)
{
$weeks
=
$this
->
applySetPos
(
$weeks
,
$by_setpos
);
}
// Unpack the weeks into days.
$days
=
array_mergev
(
$weeks
);
// Apply daily BYSETPOS, if one exists.
if
(
$is_daily
&&
$by_setpos
)
{
$days
=
$this
->
applySetPos
(
$days
,
$by_setpos
);
}
$this
->
setDays
=
array_reverse
(
$days
);
}
$this
->
stateDay
=
array_pop
(
$this
->
setDays
);
}
protected
function
nextMonth
()
{
if
(
$this
->
setMonths
)
{
$this
->
stateMonth
=
array_pop
(
$this
->
setMonths
);
return
;
}
$frequency
=
$this
->
getFrequency
();
$interval
=
$this
->
getInterval
();
$scale
=
$this
->
getFrequencyScale
();
$is_monthly
=
(
$frequency
===
self
::
FREQUENCY_MONTHLY
);
$by_month
=
$this
->
getByMonth
();
$by_setpos
=
$this
->
getBySetPosition
();
// If we have a BYMONTHDAY, we consider that set of days in every month.
// For example, "FREQ=YEARLY;BYMONTHDAY=3" means "the third day of every
// month", so we need to expand the month set if the constraint is present.
$by_monthday
=
$this
->
getByMonthDay
();
// Likewise, we need to generate all months if we have BYYEARDAY.
$by_yearday
=
$this
->
getByYearDay
();
while
(!
$this
->
setMonths
)
{
$this
->
nextYear
();
$is_dynamic
=
$is_monthly
||
$by_month
||
$by_monthday
||
$by_yearday
||
(
$scale
<
self
::
SCALE_MONTHLY
);
if
(
$is_dynamic
)
{
$months
=
$this
->
newMonthsSet
(
(
$is_monthly
?
$interval
:
1
),
$by_month
);
}
else
{
$months
=
array
(
$this
->
cursorMonth
,
);
}
if
(
$is_monthly
&&
$by_setpos
)
{
$months
=
$this
->
applySetPos
(
$months
,
$by_setpos
);
}
$this
->
setMonths
=
array_reverse
(
$months
);
}
$this
->
stateMonth
=
array_pop
(
$this
->
setMonths
);
}
protected
function
nextYear
()
{
$this
->
stateYear
=
$this
->
cursorYear
;
$frequency
=
$this
->
getFrequency
();
$is_yearly
=
(
$frequency
===
self
::
FREQUENCY_YEARLY
);
if
(
$is_yearly
)
{
$interval
=
$this
->
getInterval
();
}
else
{
$interval
=
1
;
}
$this
->
cursorYear
=
$this
->
cursorYear
+
$interval
;
}
private
function
newSecondsSet
(
$interval
,
$set
)
{
// TODO: This doesn't account for leap seconds. In theory, it probably
// should, although this shouldn't impact any real events.
$seconds_in_minute
=
60
;
if
(
$this
->
cursorSecond
>=
$seconds_in_minute
)
{
$this
->
cursorSecond
-=
$seconds_in_minute
;
return
array
();
}
list
(
$cursor
,
$result
)
=
$this
->
newIteratorSet
(
$this
->
cursorSecond
,
$interval
,
$set
,
$seconds_in_minute
);
$this
->
cursorSecond
=
(
$cursor
-
$seconds_in_minute
);
return
$result
;
}
private
function
newMinutesSet
(
$interval
,
$set
)
{
// NOTE: This value is legitimately a constant! Amazing!
$minutes_in_hour
=
60
;
if
(
$this
->
cursorMinute
>=
$minutes_in_hour
)
{
$this
->
cursorMinute
-=
$minutes_in_hour
;
return
array
();
}
list
(
$cursor
,
$result
)
=
$this
->
newIteratorSet
(
$this
->
cursorMinute
,
$interval
,
$set
,
$minutes_in_hour
);
$this
->
cursorMinute
=
(
$cursor
-
$minutes_in_hour
);
return
$result
;
}
private
function
newHoursSet
(
$interval
,
$set
)
{
// TODO: This doesn't account for hours caused by daylight savings time.
// It probably should, although this seems unlikely to impact any real
// events.
$hours_in_day
=
24
;
if
(
$this
->
cursorHour
>=
$hours_in_day
)
{
$this
->
cursorHour
-=
$hours_in_day
;
return
array
();
}
list
(
$cursor
,
$result
)
=
$this
->
newIteratorSet
(
$this
->
cursorHour
,
$interval
,
$set
,
$hours_in_day
);
$this
->
cursorHour
=
(
$cursor
-
$hours_in_day
);
return
$result
;
}
private
function
newDaysSet
(
$interval_day
,
$interval_week
,
$by_day
,
$by_monthday
,
$by_yearday
,
$by_weekno
,
$week_start
)
{
$year_map
=
$this
->
getYearMap
(
$this
->
stateYear
,
$week_start
);
$selection
=
array
();
if
(
$interval_week
)
{
while
(
true
)
{
// TODO: This is all garbage?
if
(
$this
->
cursorWeek
>
$year_map
[
'weekCount'
])
{
$this
->
cursorWeek
-=
$year_map
[
'weekCount'
];
break
;
}
foreach
(
$year_map
[
'weeks'
][
$this
->
cursorWeek
]
as
$key
)
{
$selection
[]
=
$year_map
[
'info'
][
$key
];
}
$last
=
last
(
$selection
);
if
(
$last
[
'month'
]
>
$this
->
stateMonth
)
{
break
;
}
$this
->
cursorWeek
+=
$interval_week
;
}
}
else
{
$month_idx
=
$this
->
stateMonth
;
if
(!
$interval_day
)
{
$interval_day
=
1
;
}
// If we have a BYDAY, BYMONTHDAY, BYYEARDAY or BYWEEKNO selector and
// this isn't the initial month, reset the day cursor to the first of the
// month to make sure we examine the entire month. If we don't do this,
// we can have a situation where an event occurs "every Monday in
// October", but has a start date on the 19th of August, and misses
// Mondays in October prior to the 19th.
if
(
$by_day
||
$by_monthday
||
$by_yearday
||
$by_weekno
)
{
if
(
$this
->
stateYear
!==
$this
->
initialYear
||
$this
->
stateMonth
!==
$this
->
initialMonth
)
{
$this
->
cursorDay
=
1
;
}
}
while
(
true
)
{
$month_days
=
$year_map
[
'monthDays'
][
$month_idx
];
if
(
$this
->
cursorDay
>
$month_days
)
{
$this
->
cursorDay
-=
$month_days
;
break
;
}
$day_idx
=
$this
->
cursorDay
;
$key
=
"{$month_idx}M{$day_idx}D"
;
$selection
[]
=
$year_map
[
'info'
][
$key
];
$this
->
cursorDay
+=
$interval_day
;
}
}
$weeks
=
array
();
foreach
(
$selection
as
$key
=>
$info
)
{
if
(
$info
[
'month'
]
!=
$this
->
stateMonth
)
{
continue
;
}
if
(
$by_day
)
{
// TODO: This only handles "BYDAY=MO,TU". It does not yet properly
// handle "BYDAY=+1FR" (e.g., the first Friday in the month).
if
(
empty
(
$by_day
[
$info
[
'weekday'
]]))
{
continue
;
}
}
if
(
$by_monthday
)
{
if
(
empty
(
$by_monthday
[
$info
[
'monthday'
]])
&&
empty
(
$by_monthday
[
$info
[
'-monthday'
]]))
{
continue
;
}
}
if
(
$by_yearday
)
{
if
(
empty
(
$by_yearday
[
$info
[
'yearday'
]])
&&
empty
(
$by_yearday
[
$info
[
'-yearday'
]]))
{
continue
;
}
}
if
(
$by_weekno
)
{
if
(
empty
(
$by_weekno
[
$info
[
'week'
]])
&&
empty
(
$by_weekno
[
$info
[
'-week'
]]))
{
continue
;
}
}
$weeks
[
$info
[
'week'
]][]
=
$info
[
'monthday'
];
}
return
array_values
(
$weeks
);
}
private
function
newMonthsSet
(
$interval
,
$set
)
{
// NOTE: This value is also a real constant! Wow!
$months_in_year
=
12
;
if
(
$this
->
cursorMonth
>
$months_in_year
)
{
$this
->
cursorMonth
-
$months_in_year
;
return
array
();
}
list
(
$cursor
,
$result
)
=
$this
->
newIteratorSet
(
$this
->
cursorMonth
,
$interval
,
$set
,
$months_in_year
+
1
);
$this
->
cursorMonth
=
(
$cursor
-
$months_in_year
);
return
$result
;
}
public
static
function
getYearMap
(
$year
,
$week_start
)
{
static
$maps
=
array
();
$weekday_index
=
self
::
getWeekdayIndex
(
$week_start
);
$key
=
"{$year}/{$week_start}"
;
if
(
isset
(
$maps
[
$key
]))
{
return
$maps
[
$key
];
}
$map
=
self
::
newYearMap
(
$year
,
$weekday_index
);
$maps
[
$key
]
=
$map
;
return
$maps
[
$key
];
}
private
static
function
newYearMap
(
$year
,
$weekday_index
)
{
$is_leap
=
((
$year
%
4
===
0
)
&&
(
$year
%
100
!==
0
))
||
(
$year
%
400
===
0
);
// There may be some clever way to figure out which day of the week a given
// year starts on and avoid the cost of a DateTime construction, but I
// wasn't able to turn it up and we only need to do this once per year.
$datetime
=
new
DateTime
(
"{$year}-01-01"
,
new
DateTimeZone
(
'UTC'
));
$weekday
=
(
int
)
$datetime
->
format
(
'w'
);
if
(
$is_leap
)
{
$max_day
=
366
;
}
else
{
$max_day
=
365
;
}
$month_days
=
array
(
1
=>
31
,
2
=>
$is_leap
?
29
:
28
,
3
=>
31
,
4
=>
30
,
5
=>
31
,
6
=>
30
,
7
=>
31
,
8
=>
31
,
9
=>
30
,
10
=>
31
,
11
=>
30
,
12
=>
31
,
);
// Per the spec, the first week of the year must contain at least four
// days. If the week starts on a Monday but the year starts on a Saturday,
// the first couple of days don't count as a week. In this case, the first
// week will begin on January 3.
$first_week_size
=
0
;
$first_weekday
=
$weekday
;
for
(
$year_day
=
1
;
$year_day
<=
$max_day
;
$year_day
++)
{
$first_weekday
=
(
$first_weekday
+
1
)
%
7
;
if
(
$first_weekday
===
$weekday_index
)
{
break
;
}
$first_week_size
++;
}
if
(
$first_week_size
>=
4
)
{
$week_number
=
1
;
}
else
{
$week_number
=
0
;
}
$info_map
=
array
();
$weekday_map
=
self
::
getWeekdayIndexMap
();
$weekday_map
=
array_flip
(
$weekday_map
);
$month_number
=
1
;
$month_day
=
1
;
for
(
$year_day
=
1
;
$year_day
<=
$max_day
;
$year_day
++)
{
$key
=
"{$month_number}M{$month_day}D"
;
$info
=
array
(
'year'
=>
$year
,
'key'
=>
$key
,
'month'
=>
$month_number
,
'monthday'
=>
$month_day
,
'-monthday'
=>
-
$month_days
[
$month_number
]
+
$month_day
-
1
,
'yearday'
=>
$year_day
,
'-yearday'
=>
-
$max_day
+
$year_day
-
1
,
'week'
=>
$week_number
,
'weekday'
=>
$weekday_map
[
$weekday
],
);
$info_map
[
$key
]
=
$info
;
$weekday
=
(
$weekday
+
1
)
%
7
;
if
(
$weekday
===
$weekday_index
)
{
$week_number
++;
}
$month_day
=
(
$month_day
+
1
);
if
(
$month_day
>
$month_days
[
$month_number
])
{
$month_day
=
1
;
$month_number
++;
}
}
// Now that we know how many weeks the year has, we can compute the
// negative offsets.
foreach
(
$info_map
as
$key
=>
$info
)
{
$week
=
$info
[
'week'
];
if
(!
$week
)
{
// If this day is part of the "zeroth" week of the year, it does not
// get a reverse index. In particular, it is not week "-53" (ethe
// 53rd week from the end of the year) in a 52-week year.
$week_value
=
0
;
}
else
{
$week_value
=
-
$week_number
+
$week
-
1
;
}
$info
[
'-week'
]
=
$week_value
;
}
return
array
(
'info'
=>
$info_map
,
'weekCount'
=>
$week_number
,
'dayCount'
=>
$max_day
,
'monthDays'
=>
$month_days
,
);
}
private
function
newIteratorSet
(
$cursor
,
$interval
,
$set
,
$limit
)
{
if
(
$interval
<
1
)
{
throw
new
Exception
(
pht
(
'Invalid iteration interval ("%d"), must be at least 1.'
,
$interval
));
}
$result
=
array
();
$seen
=
array
();
$ii
=
$cursor
;
while
(
true
)
{
if
(!
$set
||
isset
(
$set
[
$ii
]))
{
$result
[]
=
$ii
;
}
$ii
=
(
$ii
+
$interval
);
if
(
$ii
>=
$limit
)
{
break
;
}
}
sort
(
$result
);
$result
=
array_values
(
$result
);
return
array
(
$ii
,
$result
);
}
private
function
applySetPos
(
array
$values
,
array
$setpos
)
{
$select
=
array
();
$count
=
count
(
$values
);
foreach
(
$setpos
as
$pos
)
{
if
(
$pos
>
0
&&
$pos
<=
$count
)
{
$select
[]
=
(
$pos
-
1
);
}
else
if
(
$pos
<
0
&&
$pos
>=
-
$count
)
{
$select
[]
=
(
$count
+
$pos
);
}
}
sort
(
$select
);
return
array_select_keys
(
$values
,
$select
);
}
private
function
assertByRange
(
$source
,
array
$values
,
$min
,
$max
,
$allow_zero
=
true
)
{
foreach
(
$values
as
$value
)
{
if
(!
is_int
(
$value
))
{
throw
new
Exception
(
pht
(
'Value "%s" in RRULE "%s" parameter is invalid: values must be '
.
'integers.'
,
$value
,
$source
));
}
if
(
$value
<
$min
||
$value
>
$max
)
{
throw
new
Exception
(
pht
(
'Value "%s" in RRULE "%s" parameter is invalid: it must be '
.
'between %s and %s.'
,
$value
,
$source
,
$min
,
$max
));
}
if
(!
$value
&&
!
$allow_zero
)
{
throw
new
Exception
(
pht
(
'Value "%s" in RRULE "%s" parameter is invalid: it must not '
.
'be zero.'
,
$value
,
$source
));
}
}
}
}
Event Timeline
Log In to Comment