~cypheon/nextcloud-chores-app

ref: d19a6603c01e63c7545582e5ff313ef4aec0bf9f nextcloud-chores-app/lib/Service/Schedule.php -rw-r--r-- 4.3 KiB
d19a6603 — Johann Rudloff Make overdue sidebar badge dynamic, switch badges to `CounterBubble` component 10 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<?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;
}