Skip to content

Conversation

Copy link

Copilot AI commented Dec 8, 2025

Adds mergeIfPossible() method to combine compatible recurrence rules, reducing redundancy in schedule representations. The method applies iteratively to reduce rules as far as possible (e.g., Yearly → Monthly → Weekly).

Changes

Schedule class - Added getter methods exposing RRule properties:

  • getFreq(), getCount(), getByDay(), getByMonth(), getByHour(), getByMinute(), getBySecond(), getUntil(), getInterval()
  • getUntil() normalizes DateTime objects to RFC 5545 format strings

ScheduleSet class - Implemented intelligent rule merging with iterative reduction:

  • Iterative reduction: Automatically applies merge logic repeatedly until no further reductions are possible, enabling multi-level simplifications (Yearly → Monthly → Weekly)
  • Weekly rules: Merges when COUNT matches or UNTIL dates are compatible (checks for occurrence conflicts between end dates), combining BYDAY values
  • Yearly rules: Combines BYMONTH values when other properties align
  • Monthly rules: Simplifies to weekly when all Nth occurrences of a weekday are covered
  • Proper end date compatibility: Verifies that a rule's pattern wouldn't have occurrences between its end date and a later end date, rather than just checking if dates fall in the same week
  • Incompatible rules: Preserves unchanged (different frequencies, mismatched end conditions)

ScheduleSet constructor - Extended to accept arrays of Schedule/RRule objects

Comprehensive unit tests - Added extensive test coverage:

  • 18 tests for Schedule.php covering all getter methods with various input formats (string, array, DateTime, null)
  • 21 additional tests for ScheduleSet.php covering:
    • End date compatibility scenarios with occurrence conflict checking
    • Constructor variations (array, RFC string, null, with dtstart)
    • Edge cases: different intervals, WKST, BYHOUR, mixed COUNT/UNTIL
    • Iterative reduction validation
    • Multiple merge scenarios and incompatible rule handling
  • Total: 126 tests, 274 assertions (expanded from 85 tests, 208 assertions)

Example

$set = new ScheduleSet();
$set->addRRule("FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10");
$set->addRRule("FREQ=WEEKLY;BYDAY=FR;COUNT=10");

$set->mergeIfPossible();

// Result: Single rule with BYDAY=MO,WE,FR and COUNT=20
count($set->getRRules()); // 1
$set->getRRules()[0]->getByDay(); // ['MO', 'WE', 'FR']

Iterative Reduction Example

$set = new ScheduleSet();
// Add monthly rules for 1st-5th Sunday at 9am
for ($n = 1; $n <= 5; $n++) {
    $set->addRRule("FREQ=MONTHLY;BYDAY={$n}SU;BYHOUR=9;BYMINUTE=0;BYSECOND=0");
}

$set->mergeIfPossible();

// Result: Single WEEKLY rule (all Sundays covered)
count($set->getRRules()); // 1
$set->getRRules()[0]->getFreq(); // 'WEEKLY'

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI and others added 3 commits December 8, 2025 17:12
Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Copy link
Author

Copilot AI commented Dec 8, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/gW2Kgy /usr/bin/composer install (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/gW2Kgy /usr/bin/composer install (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/nikic/PHP-Parser/zipball/51bd93cc741b7fc3d63d20b6bdcd99fdaa359837
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phpDocumentor/Reflection/zipball/447928a45710d6313e68774cf12b5f730b909baa
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/gW2Kgy /usr/bin/composer install (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/gW2Kgy /usr/bin/composer install (http block)
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/pronamic/wp-documentor/zipball/a81e7626f2dc5a3767bbce010584c7a08813ffeb
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/rlanvin/php-rrule/zipball/2a389a9fa67dda58bc5a569a3264555152db3c49
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/console/zipball/1b2813049506b39eb3d7e64aff033fd5ca26c97e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/6uQq7I /usr/bin/composer install --no-interaction (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Add logic to ScheduleSet:mergeIfPossible() for RRules Implement RRule merging logic for ScheduleSet Dec 8, 2025
Copilot AI requested a review from jkrrv December 8, 2025 17:20
Copy link
Member

@jkrrv jkrrv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things here to adjust.

$newRules = array_merge($newRules, $freqRules);
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that this is able to apply iteratively. That is, if a Yearly is able to be reduced to a Monthly, and the Monthly is able to be reduced to a weekly, then it should be reduced as far as possible.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented iterative reduction in mergeIfPossible(). The method now loops and reapplies merge logic until no further reductions are possible, allowing Yearly -> Monthly -> Weekly cascading reductions. See commit 81d4791.

* @param string|\DateTime $date Date string in RFC format or DateTime object
* @return string Week identifier (e.g., "2025-52")
*/
private function getWeekOfDate($date): string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation misses the point. For combining two series (weekly, monthly or yearly) it doesn't matter if they end the same week, but rather if there is an acceptable shared end date. That is, if series A's last occurrence is before series B's last occurrence, they can share an end date as long as the pattern of A wouldn't have another occurrence after the end of A and before the end of B.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the end date compatibility logic. Now properly checks if series A's pattern would have occurrences between A's end and B's end using the hasOccurrencesBetween() method, rather than just checking if they end in the same week. See commit 81d4791.

Copilot AI and others added 2 commits December 8, 2025 21:40
Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Copy link
Author

Copilot AI commented Dec 8, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repositories/138435860/zipball/bb603128bd6bf332020f14f0c0875ac875aeef4f
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/uvzYmx /usr/bin/composer install --no-interaction (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Implement RRule merging logic for ScheduleSet Implement RRule merging logic for ScheduleSet with iterative reduction Dec 8, 2025
Copilot AI requested a review from jkrrv December 8, 2025 21:44
@jkrrv
Copy link
Member

jkrrv commented Dec 8, 2025

@copilot Please expand unit tests to add test cases for the situations I described in the previous preview, and also for 100% coverage of the Schedule.php and ScheduleSet.php

Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com>
Copy link
Author

Copilot AI commented Dec 8, 2025

@copilot Please expand unit tests to add test cases for the situations I described in the previous preview, and also for 100% coverage of the Schedule.php and ScheduleSet.php

Expanded unit tests with comprehensive coverage:

Schedule.php tests (18 new tests):

  • All getter methods: getFreq, getCount, getUntil, getInterval, getByDay, getByMonth, getByHour, getByMinute, getBySecond
  • Edge cases: DateTime vs string handling, null values, various input formats

ScheduleSet.php tests (21 new tests):

  • End date compatibility scenarios from review comments (rules with/without occurrence conflicts)
  • Constructor variations (array, RFC string, null, with dtstart)
  • Different merge scenarios: same/different intervals, WKST, BYHOUR, mixed COUNT/UNTIL
  • Iterative reduction validation
  • Edge cases: non-standard frequencies, max iteration protection

Results: 126 total tests (was 85), 274 assertions (was 208), all passing.

See commit 576faae.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants