Files
RecoReco/Model/OccurrenceCalculator.php

71 lines
2.3 KiB
PHP

<?php
namespace Kanboard\Plugin\RecoReco\Model;
/**
* Pure calendar math for RecoReco.
*
* Given an anchor due date (the drift-free pattern: day-of-month + time), a frequency, and the
* "last day" flag, it computes occurrence datetimes. It has NO container dependencies, so it is
* unit-testable in isolation (see Test/OccurrenceCalculatorTest.php).
*
* Occurrences are always computed from the anchor's day, never by marching a clamped date forward,
* so a "30th" schedule goes 30, 28/29, 30, 30... and never drifts to the 28th.
*
* v0.3 implements monthly-by-day; the other frequencies arrive in v1.1 (occurrenceFrom returns null
* for them until then).
*/
class OccurrenceCalculator
{
const MAX_MONTHS = 120;
/**
* The earliest occurrence at/after $reference (when $inclusive) or strictly after it.
*
* @param int $anchor Unix timestamp: the pattern (day-of-month + time).
* @param string $frequency
* @param bool $lastDay
* @param int $reference Unix timestamp.
* @param bool $inclusive
* @return int|null Occurrence timestamp, or null if the frequency is not yet supported.
*/
public function occurrenceFrom($anchor, $frequency, $lastDay, $reference, $inclusive = false)
{
switch ($frequency) {
case 'monthly_day':
return $this->monthlyByDay((int) $anchor, (bool) $lastDay, (int) $reference, (bool) $inclusive);
default:
return null;
}
}
private function monthlyByDay($anchor, $lastDay, $reference, $inclusive)
{
$anchorDay = (int) date('j', $anchor);
$h = (int) date('G', $anchor);
$i = (int) date('i', $anchor);
$s = (int) date('s', $anchor);
$y = (int) date('Y', $reference);
$m = (int) date('n', $reference);
for ($k = 0; $k < self::MAX_MONTHS; $k++) {
$lastDom = (int) date('t', mktime(0, 0, 0, $m, 1, $y));
$day = $lastDay ? $lastDom : min($anchorDay, $lastDom);
$occurrence = mktime($h, $i, $s, $m, $day, $y);
if ($inclusive ? $occurrence >= $reference : $occurrence > $reference) {
return $occurrence;
}
$m++;
if ($m > 12) {
$m = 1;
$y++;
}
}
return null;
}
}