<?php
namespace OCA\Chores\Service;
use \DateInterval;
use \DateTimeImmutable;
use \Exception;
/**
* A schedule-string is composed of 3 parts separated by colons
*
* unit:interval:scontraints
*
* unit:
* d - days
* w - week
* m - months
* s - single occurence, do not repeat, delete after completion
* o - on demand, no due date, do not delete
*
* interval:
* an integer specifying how many units until next due date
*
* constraint:
* if unit is | constraint can be
* -----------+----------------------------------
* d,s,o | '-' (no constraint)
* -----------+----------------------------------
* w | '-' (no constraint)
* | 0..6 weekday, starting with 0=sunday
* | + ',even', or ',odd' (NOT IMPLEMENTED)
* -----------+----------------------------------
* m | '-' (no constraint)
* | 0 .. 31 (to always occur on this day of the month)
* | (when the month has less days, the last day of the
* | month is chosen)
*/
class Schedule {
const SINGLE = 's';
const DAYS = 'd';
const WEEKS = 'w';
const MONTHS = 'm';
const ON_DEMAND = 'o';
protected /* int */ $interval;
protected /* ?int */ $constraint;
protected /* string(unit) */ $unit;
private $durationString;
protected function __construct(int $interval, string $unit, string $constraint) {
$this->interval = $interval;
$this->unit = $unit;
$this->constraint = Schedule::validateConstraint($unit, $constraint);
$this->durationString = "P" . $this->interval . Schedule::getDurationUnit($this->unit);
}
private function baseAdvance(): DateInterval {
return new DateInterval($this->durationString);
}
private function getNudge(DateTimeImmutable $naiveNextDate): DateInterval {
switch ($this->unit) {
case Schedule::WEEKS:
$isWeekday = intval($naiveNextDate->format("w"));
$weekdayDiff = $this->constraint - $isWeekday;
$nudge = posmod($weekdayDiff, 7);
if ($nudge >= 4) {
$nudge = $nudge - 7;
}
return makeDaysInterval($nudge);
default: throw new Exception("impossible");
}
}
public function nextDueDate(\DateTimeImmutable $completedDate): \DateTimeImmutable {
if ($this->unit == Schedule::ON_DEMAND) {
// for on-demand chores, we reuse the due field to store "last done date"
return $completedDate;
}
$naiveNextDate = $completedDate->add($this->baseAdvance());
if ($this->constraint !== null) {
$nudge = $this->getNudge($naiveNextDate);
return $naiveNextDate->add($nudge);
} else {
return $naiveNextDate;
}
}
public static function parseSchedule(string $scheduleDesc): Schedule {
$parts = explode(":", $scheduleDesc, 3);
return new Schedule(validateInt($parts[1], 0, 60), $parts[0], $parts[2]);
}
private static function getDurationUnit(string $unit): string {
switch ($unit) {
case Schedule::DAYS:
return 'D';
case Schedule::WEEKS:
return 'W';
case Schedule::MONTHS:
return 'M';
case Schedule::ON_DEMAND:
return '///ON_DEMAND';
default:
throw new Exception("invalid unit: $unit");
}
}
private static function validateConstraint(string $unit, string $constraint): ?int {
if ($constraint == "-") {
return null;
}
switch($unit) {
case Schedule::DAYS: throw new Exception("invalid constraint for " . $unit . ": " . $constraint);
case Schedule::WEEKS: // fallthrough
return validateInt($constraint, 0, 6);
case Schedule::MONTHS: //
return validateInt($constraint, 1, 31);
}
throw new Exception("invalid constraint for " . $unit . ": " . $constraint);
}
}
function isint(string $s): bool {
return !!preg_match('/^\d+$/', $s);
}
function validateInt(string $s, int $min, int $max): int {
if (!isint($s)) {
throw new Exception("invalid int: " . $s);
}
$i = intval($s);
if ($i < $min || $i > $max) {
throw new Exception("value out of range, valid range: " . $min . "..." . $max);
}
return $i;
}
function posmod(int $a, int $b): int {
return ($b + ($a % $b)) % $b;
}
function makeDaysInterval(int $days): DateInterval {
$i = new DateInterval("P" . abs($days) . "D");
if ($days < 0) {
$i->invert = 1;
}
return $i;
}