phacility/phabricator · error · Exception
RRULE BYDAY value "%s" is invalid: rule part must be in the
Error message
RRULE BYDAY value "%s" is invalid: rule part must be in the expected form (like "MO", "-3TH", or "+2SU").
What it means
setByDay() validates each token against an optional ordinal (1-99, optionally signed) followed by a two-letter weekday constant - forms like 'MO', '-3TH', '+2SU'. Anything else (full names, 'MON', '+0MO', ordinal 0, unsplit comma lists) fails the regex and throws. Ordinals that pass but exceed 53 fail a separate magnitude check immediately after.
Source
Thrown at src/applications/calendar/parser/data/PhutilCalendarRecurrenceRule.php:470
public function setByHour(array $by_hour) {
$this->assertByRange('BYHOUR', $by_hour, 0, 23);
$this->byHour = array_fuse($by_hour);
return $this;
}
public function getByHour() {
return $this->byHour;
}
public function setByDay(array $by_day) {
$constants = self::getAllWeekdayConstants();
$constants = implode('|', $constants);
$pattern = '/^(?:[+-]?([1-9]\d?))?('.$constants.')\z/';
foreach ($by_day as $key => $value) {
$matches = null;
if (!preg_match($pattern, $value, $matches)) {
throw new Exception(
pht(
'RRULE BYDAY value "%s" is invalid: rule part must be in the '.
'expected form (like "MO", "-3TH", or "+2SU").',
$value));
}
// The maximum allowed value is 53, which corresponds to "the 53rd
// Monday every year" or similar when evaluated against a YEARLY rule.
$maximum = 53;
$magnitude = (int)$matches[1];
if ($magnitude > $maximum) {
throw new Exception(
pht(
'RRULE BYDAY value "%s" has an offset with magnitude "%s", but '.
'the maximum permitted value is "%s".',
$value,
$magnitude,View on GitHub (pinned to 5720a38cfe)
Solutions
- Split comma lists before calling: array_map('trim', explode(',', $byday)).
- Use bare two-letter uppercase tokens, or ordinal forms like -1SU / +2MO with the ordinal between 1 and 53.
- Uppercase and pre-validate tokens with the production regex: /^(?:[+-]?[1-9]\d?)?(SU|MO|TU|WE|TH|FR|SA)$/
Example fix
// before
$rule->setByDay(array('MO,FR', 'WED'));
// after
$rule->setByDay(array('MO', 'FR')); Defensive patterns
Strategy: type-guard
Validate before calling
// Split and normalize a raw BYDAY list before calling setByDay():
$tokens = array_map('trim', explode(',', (string)$byday_raw));
$pattern = '/^(?:[+-]?[1-9]\d?)?(SU|MO|TU|WE|TH|FR|SA)$/';
$by_day = array();
foreach ($tokens as $t) {
$t = strtoupper($t);
if (preg_match($pattern, $t, $m) && (!isset($m[1][0]) || abs((int)$m[1]) <= 53)) {
$by_day[] = $t;
}
}
if (!$by_day) {
// reject the BYDAY part; do not call setByDay()
} Type guard
function isByDayToken($token) {
return (bool) preg_match(
'/^(?:[+-]?[1-9]\d?)?(SU|MO|TU|WE|TH|FR|SA)$/',
strtoupper(trim((string)$token))
);
} Prevention
- Explode comma-separated BYDAY strings before passing them as arrays.
- Uppercase and trim every token; reject full weekday names after normalization.
- Keep ordinals within 1..53 - the regex alone allows up to 99 but the setter rejects more than 53.
When it happens
Trigger: setByDay(array('MON')) ('MON' not matched); '+0FR' or '0MO' (ordinal must start with 1-9); passing the raw string 'MO,WE' instead of an array; lowercase 'mo'.
Common situations: Producers emitting full weekday names; forgetting to explode comma-separated BYDAY lists; edge-case exports with 0 or >53 ordinals; case normalization skipped.
Related errors
- RRULE dictionary includes unknown key "%s". Expected keys ar
- Unexpected value "%s" in "%s" RULE property: expected an int
- Unexpected value "%s" in "%s" RRULE property: expected only
- Weekday "%s" is not a valid weekday constant. Valid constant
- RRULE COUNT value "%s" is invalid: count must be at least 1.
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/f121fdd0b3d1fcaf.
Report an issue: GitHub.