Skip to content

Commit f9a1b5c

Browse files
authored
Merge pull request #50 from SonnySha/develop
Add Weekly Odd/Even scheduling methods for more specific schedule options and update documentation
2 parents d3f38d4 + e01c881 commit f9a1b5c

18 files changed

+1069
-43
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,19 @@ $schedule->daily()->from('2025-01-01')->to('2025-12-31');
114114
$schedule->weekly(['monday', 'wednesday', 'friday'])->forYear(2025);
115115

116116
// Weekly with time period (convenience method - combines weekly() and addPeriod())
117-
$schedule->weekDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')
118-
->forYear(2025);
117+
$schedule->weekDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025);
118+
119+
// Weekly odd (specific days) – runs only on odd-numbered weeks
120+
$schedule->weeklyOdd(['monday', 'wednesday', 'friday'])->forYear(2025);
121+
122+
// Weekly odd with time period (convenience method - combines weeklyOdd() and addPeriod())
123+
$schedule->weekOddDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025)
124+
125+
// Weekly even (specific days) – runs only on even-numbered weeks
126+
$schedule->weeklyEven(['monday', 'wednesday', 'friday'])->forYear(2025);
127+
128+
// Weekly even with time period (convenience method - combines weeklyEven() and addPeriod())
129+
$schedule->weekEvenDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025)
119130

120131
// Bi-weekly (week of the start date by default, optional anchor)
121132
$schedule->biweekly(['tuesday', 'thursday'])->from('2025-01-07')->to('2025-03-31');
@@ -140,6 +151,7 @@ $schedule->annually(['days_of_month' => [1, 15], 'start_month' => 4])->from('202
140151

141152
```php
142153
$schedule->from('2025-01-15'); // Single date
154+
$schedule->on('2025-01-15'); // Alias (alternative syntax) for from()
143155
$schedule->from('2025-01-01')->to('2025-12-31'); // Date range
144156
$schedule->between('2025-01-01', '2025-12-31'); // Alternative syntax
145157
$schedule->forYear(2025); // Entire year shortcut
@@ -164,6 +176,9 @@ $schedule->addPeriod('14:00', '17:00');
164176
// Check if there is at least one bookable slot on the day
165177
$isBookable = $doctor->isBookableAt('2025-01-15', 60);
166178

179+
// Check if a specific time range is bookable
180+
$isBookable = $doctor->isBookableAtTime('2025-01-15', '9:00', '9:30');
181+
167182
// Get bookable slots
168183
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
169184

@@ -186,7 +201,7 @@ $schedule->isAppointment();
186201
$schedule->isBlocked();
187202
```
188203

189-
> `isAvailableAt()` is deprecated in favor of `isBookableAt()` and `getBookableSlots()`. Use the bookable APIs for all new code.
204+
> `isAvailableAt()` is deprecated in favor of `isBookableAt()` `isBookableAtTime()` and `getBookableSlots()`. Use the bookable APIs for all new code.
190205
191206
---
192207

src/Builders/ScheduleBuilder.php

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Zap\Data\MonthlyFrequencyConfig;
1414
use Zap\Data\QuarterlyFrequencyConfig;
1515
use Zap\Data\SemiAnnuallyFrequencyConfig;
16-
use Zap\Data\WeeklyFrequencyConfig;
1716
use Zap\Enums\Frequency;
1817
use Zap\Enums\ScheduleTypes;
1918
use Zap\Models\Schedule;
@@ -154,16 +153,42 @@ public function daily(): self
154153
}
155154

156155
/**
157-
* Set schedule as weekly recurring.
156+
* Internal helper to configure a weekly frequency for the schedule.
157+
*
158+
* This method centralizes the logic for setting weekly recurring schedules,
159+
* including standard weeks, odd weeks, and time periods.
160+
* It prevents code duplication across public methods like weekly(), weekDays(), weeklyOdd() and weekOddDays()
161+
*
162+
* @param Frequency $frequency The frequency type (WEEKLY, WEEKLY_ODD, etc.)
163+
* @param array $days Array of days the schedule applies to
164+
* @param string|null $startTime Optional start time for the daily period
165+
* @param string|null $endTime Optional end time for the daily period
166+
* @return self Returns the current instance for method chaining
158167
*/
159-
public function weekly(array $days = []): self
168+
private function setWeeklyFrequency(Frequency $frequency, array $days, ?string $startTime = null, ?string $endTime = null): self
160169
{
161170
$this->attributes['is_recurring'] = true;
162-
$this->attributes['frequency'] = Frequency::WEEKLY;
163-
$this->attributes['frequency_config'] = WeeklyFrequencyConfig::fromArray([
171+
$this->attributes['frequency'] = $frequency;
172+
173+
$configClass = app($frequency->configClass());
174+
$this->attributes['frequency_config'] = $configClass->fromArray([
164175
'days' => $days,
165176
]);
166177

178+
if ($startTime !== null && $endTime !== null) {
179+
$this->addPeriod($startTime, $endTime, null);
180+
}
181+
182+
return $this;
183+
}
184+
185+
/**
186+
* Set schedule as weekly recurring.
187+
*/
188+
public function weekly(array $days = []): self
189+
{
190+
$this->setWeeklyFrequency(Frequency::WEEKLY, $days);
191+
167192
return $this;
168193
}
169194

@@ -172,13 +197,47 @@ public function weekly(array $days = []): self
172197
*/
173198
public function weekDays(array $days, string $startTime, string $endTime): self
174199
{
175-
$this->attributes['is_recurring'] = true;
176-
$this->attributes['frequency'] = Frequency::WEEKLY;
177-
$this->attributes['frequency_config'] = WeeklyFrequencyConfig::fromArray([
178-
'days' => $days,
179-
]);
200+
$this->setWeeklyFrequency(Frequency::WEEKLY, $days, $startTime, $endTime);
201+
202+
return $this;
203+
}
204+
205+
/**
206+
* Set schedule as weekly recurring on odd weeks.
207+
*/
208+
public function weeklyOdd(array $days = []): self
209+
{
210+
$this->setWeeklyFrequency(Frequency::WEEKLY_ODD, $days);
180211

181-
$this->addPeriod($startTime, $endTime, null);
212+
return $this;
213+
}
214+
215+
/**
216+
* Set schedule as weekly recurring on odd weeks and add a time period.
217+
*/
218+
public function weekOddDays(array $days, string $startTime, string $endTime): self
219+
{
220+
$this->setWeeklyFrequency(Frequency::WEEKLY_ODD, $days, $startTime, $endTime);
221+
222+
return $this;
223+
}
224+
225+
/**
226+
* Set schedule as weekly recurring on even weeks.
227+
*/
228+
public function weeklyEven(array $days = []): self
229+
{
230+
$this->setWeeklyFrequency(Frequency::WEEKLY_EVEN, $days);
231+
232+
return $this;
233+
}
234+
235+
/**
236+
* Set schedule as weekly recurring on even weeks and add a time period.
237+
*/
238+
public function weekEvenDays(array $days, string $startTime, string $endTime): self
239+
{
240+
$this->setWeeklyFrequency(Frequency::WEEKLY_EVEN, $days, $startTime, $endTime);
182241

183242
return $this;
184243
}
@@ -311,6 +370,14 @@ public function noOverlap(): self
311370
return $this->withRule('no_overlap');
312371
}
313372

373+
/**
374+
* Add allow overlap rule.
375+
*/
376+
public function allowOverlap(): self
377+
{
378+
return $this->withRule('no_overlap', ['enabled' => false]);
379+
}
380+
314381
/**
315382
* Set schedule as availability type (allows overlaps).
316383
*/
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Zap\Data\WeeklyEvenOddFrequencyConfig;
4+
5+
use Zap\Data\FrequencyConfig;
6+
use Zap\Helper\DateHelper;
7+
use Zap\Models\Schedule;
8+
9+
/**
10+
* @property-read list<string> $daysOfWeek
11+
*/
12+
abstract class AbstractWeeklyOddEvenFrequencyConfig extends FrequencyConfig
13+
{
14+
public function __construct(
15+
public array $days = []
16+
) {}
17+
18+
abstract protected function isWeekTypeMatch(\Carbon\CarbonInterface $date): bool;
19+
20+
public function shouldCreateInstance(\Carbon\CarbonInterface $date): bool
21+
{
22+
return empty($this->days) || in_array(strtolower($date->format('l')), $this->days);
23+
}
24+
25+
public function shouldCreateRecurringInstance(Schedule $schedule, \Carbon\CarbonInterface $date): bool
26+
{
27+
$allowedDays = ! empty($this->days) ? $this->days : ['monday'];
28+
$allowedDayNumbers = DateHelper::getDayNumbers($allowedDays);
29+
30+
return $this->isWeekTypeMatch($date) && in_array($date->dayOfWeek, $allowedDayNumbers);
31+
}
32+
33+
public function getNextRecurrence(\Carbon\CarbonInterface $current): \Carbon\CarbonInterface
34+
{
35+
return $this->getNextWeeklyOccurrence($current, $this->days);
36+
}
37+
38+
protected function getNextWeeklyOccurrence(\Carbon\CarbonInterface $current, array $allowedDays): \Carbon\CarbonInterface
39+
{
40+
$next = $current->copy()->addDay();
41+
$allowedDayNumbers = DateHelper::getDayNumbers($allowedDays);
42+
43+
while (true) {
44+
$isAllowedDay = in_array($next->dayOfWeek, $allowedDayNumbers);
45+
46+
if ($this->isWeekTypeMatch($next) && $isAllowedDay) {
47+
break;
48+
}
49+
50+
$next->addDay();
51+
52+
if ($next->diffInDays($current) > 14) {
53+
break;
54+
}
55+
}
56+
57+
return $next;
58+
}
59+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Zap\Data\WeeklyEvenOddFrequencyConfig;
4+
5+
use Zap\Helper\DateHelper;
6+
7+
/**
8+
* @property-read list<string> $daysOfWeek
9+
*/
10+
class WeeklyEvenFrequencyConfig extends AbstractWeeklyOddEvenFrequencyConfig
11+
{
12+
public static function fromArray(array $data): self
13+
{
14+
if (! array_key_exists('days', $data)) {
15+
throw new \InvalidArgumentException("Missing 'days' key in WeeklyEvenFrequencyConfig data array.");
16+
}
17+
18+
return new self(
19+
days: $data['days'] ?? []
20+
);
21+
}
22+
23+
protected function isWeekTypeMatch(\Carbon\CarbonInterface $date): bool
24+
{
25+
return DateHelper::isDateInEvenIsoWeek($date);
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Zap\Data\WeeklyEvenOddFrequencyConfig;
4+
5+
use Zap\Helper\DateHelper;
6+
7+
/**
8+
* @property-read list<string> $daysOfWeek
9+
*/
10+
class WeeklyOddFrequencyConfig extends AbstractWeeklyOddEvenFrequencyConfig
11+
{
12+
public static function fromArray(array $data): self
13+
{
14+
if (! array_key_exists('days', $data)) {
15+
throw new \InvalidArgumentException("Missing 'days' key in WeeklyOddFrequencyConfig data array.");
16+
}
17+
18+
return new self(
19+
days: $data['days'] ?? []
20+
);
21+
}
22+
23+
protected function isWeekTypeMatch(\Carbon\CarbonInterface $date): bool
24+
{
25+
return DateHelper::isDateInOddIsoWeek($date);
26+
}
27+
}

src/Data/WeeklyFrequencyConfig.php

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Zap\Data;
44

5+
use Zap\Helper\DateHelper;
56
use Zap\Models\Schedule;
67

78
/**
@@ -37,18 +38,7 @@ public function shouldCreateInstance(\Carbon\CarbonInterface $date): bool
3738
public function shouldCreateRecurringInstance(Schedule $schedule, \Carbon\CarbonInterface $date): bool
3839
{
3940
$allowedDays = ! empty($this->days) ? $this->days : ['monday'];
40-
$allowedDayNumbers = array_map(function ($day) {
41-
return match (strtolower($day)) {
42-
'sunday' => 0,
43-
'monday' => 1,
44-
'tuesday' => 2,
45-
'wednesday' => 3,
46-
'thursday' => 4,
47-
'friday' => 5,
48-
'saturday' => 6,
49-
default => 1, // Default to Monday
50-
};
51-
}, $allowedDays);
41+
$allowedDayNumbers = DateHelper::getDayNumbers($allowedDays);
5242

5343
return in_array($date->dayOfWeek, $allowedDayNumbers);
5444
}
@@ -57,19 +47,7 @@ protected function getNextWeeklyOccurrence(\Carbon\CarbonInterface $current, arr
5747
{
5848
$next = $current->copy()->addDay();
5949

60-
// Convert day names to numbers (0 = Sunday, 1 = Monday, etc.)
61-
$allowedDayNumbers = array_map(function ($day) {
62-
return match (strtolower($day)) {
63-
'sunday' => 0,
64-
'monday' => 1,
65-
'tuesday' => 2,
66-
'wednesday' => 3,
67-
'thursday' => 4,
68-
'friday' => 5,
69-
'saturday' => 6,
70-
default => 1, // Default to Monday
71-
};
72-
}, $allowedDays);
50+
$allowedDayNumbers = DateHelper::getDayNumbers($allowedDays);
7351

7452
// Find the next allowed day
7553
while (! in_array($next->dayOfWeek, $allowedDayNumbers)) {

src/Enums/Frequency.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ enum Frequency: string
99
{
1010
case DAILY = 'daily';
1111
case WEEKLY = 'weekly';
12+
case WEEKLY_ODD = 'weekly_odd';
13+
case WEEKLY_EVEN = 'weekly_even';
1214
case BIWEEKLY = 'biweekly';
1315
case MONTHLY = 'monthly';
1416
case BIMONTHLY = 'bimonthly';
@@ -21,6 +23,8 @@ public function getNextRecurrence(CarbonInterface $current): CarbonInterface
2123
return match ($this) {
2224
self::DAILY => $current->copy()->addDay(),
2325
self::WEEKLY => $current->copy()->addWeek(),
26+
self::WEEKLY_ODD => \Zap\Helper\DateHelper::nextWeekOdd($current),
27+
self::WEEKLY_EVEN => \Zap\Helper\DateHelper::nextWeekEven($current),
2428
self::BIWEEKLY => $current->copy()->addWeeks(2),
2529
self::MONTHLY => $current->copy()->addMonth(),
2630
self::BIMONTHLY => $current->copy()->addMonths(2),
@@ -38,6 +42,8 @@ public function configClass(): string
3842
return match ($this) {
3943
self::DAILY => \Zap\Data\DailyFrequencyConfig::class,
4044
self::WEEKLY => \Zap\Data\WeeklyFrequencyConfig::class,
45+
self::WEEKLY_ODD => \Zap\Data\WeeklyEvenOddFrequencyConfig\WeeklyOddFrequencyConfig::class,
46+
self::WEEKLY_EVEN => \Zap\Data\WeeklyEvenOddFrequencyConfig\WeeklyEvenFrequencyConfig::class,
4147
self::BIWEEKLY => \Zap\Data\BiWeeklyFrequencyConfig::class,
4248
self::MONTHLY => \Zap\Data\MonthlyFrequencyConfig::class,
4349
self::BIMONTHLY => \Zap\Data\BiMonthlyFrequencyConfig::class,
@@ -47,6 +53,15 @@ public function configClass(): string
4753
};
4854
}
4955

56+
public static function weeklyFrequencies(): array
57+
{
58+
return [
59+
self::WEEKLY,
60+
self::WEEKLY_ODD,
61+
self::WEEKLY_EVEN,
62+
];
63+
}
64+
5065
public static function filteredByWeekday(): array
5166
{
5267
return [

0 commit comments

Comments
 (0)