phacility/phabricator · error · Exception

Expected ISO8601 duration in the format "P12DT3H4M5S", found

Error message

Expected ISO8601 duration in the format "P12DT3H4M5S", found "%s".

What it means

PhutilCalendarDuration's ISO 8601 duration parser accepts an optional sign, a literal 'P', then either a weeks component (nW) or days and/or time components (nD, T nH nM nS). Values missing the P, missing every component, or using unsupported units (Y years, M months) fail the regex and throw.

Source

Thrown at src/applications/calendar/parser/data/PhutilCalendarDuration.php:75

  public static function newFromISO8601($value) {
    $pattern =
      '/^'.
      '(?P<sign>[+-])?'.
      'P'.
      '(?:'.
        '(?P<W>\d+)W'.
        '|'.
        '(?:(?:(?P<D>\d+)D)?'.
          '(?:T(?:(?P<H>\d+)H)?(?:(?P<M>\d+)M)?(?:(?P<S>\d+)S)?)?'.
        ')'.
      ')'.
      '\z/';

    $matches = null;
    $ok = preg_match($pattern, $value, $matches);
    if (!$ok) {
      throw new Exception(
        pht(
          'Expected ISO8601 duration in the format "P12DT3H4M5S", found '.
          '"%s".',
          $value));
    }

    $is_negative = (idx($matches, 'sign') == '-');

    return id(new self())
      ->setIsNegative($is_negative)
      ->setWeeks((int)idx($matches, 'W', 0))
      ->setDays((int)idx($matches, 'D', 0))
      ->setHours((int)idx($matches, 'H', 0))
      ->setMinutes((int)idx($matches, 'M', 0))
      ->setSeconds((int)idx($matches, 'S', 0));
  }

  public function toISO8601() {

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Ensure the value has the leading P (with optional +/- sign) and at least one component: 'PT30M', 'P1D', 'P2W', '-P1D'.
  2. Convert unsupported units before parsing: years/months to days/weeks (e.g. P1Y -> P52W or P365D per your business rule).
  3. Insert the required T before hour/minute/second parts when a date part is also present: 'P1DT2H'.
  4. Validate with the production regex before feeding external data in.

Example fix

// before
$dur = PhutilCalendarDuration::newFromISO8601('30M');
// after
$dur = PhutilCalendarDuration::newFromISO8601('PT30M');
Defensive patterns

Strategy: type-guard

Validate before calling

$value = strtoupper(trim($value));
if (!preg_match('/^[+-]?P(\d+W|(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)$/', $value)
    || preg_match('/^[+-]?P[^WDT]/', $value)) {
  // rewrite unsupported units (Y/M) to days/weeks per your rules first
  $value = preg_replace('/(\d+)Y/', '${1}365D', $value);
  $value = preg_replace('/(\d+)M(?!\d*S)/', '${1}30D', $value);
}

Type guard

function isIso8601Duration($value) {
  return (bool) preg_match(
    '/^[+-]?P(\d+W|\d+D(T(\d+H)?(\d+M)?(\d+S)?)?|T(\d+H)?(\d+M)?(\d+S)?)$/',
    strtoupper(trim((string)$value))
  );
}

Try / catch

try {
  $dur = PhutilCalendarDuration::newFromISO8601($value);
} catch (Exception $ex) {
  phlog('Bad ISO8601 duration: '.$value);
  continue; // skip this property, keep importing
}

Prevention

When it happens

Trigger: newFromISO8601('3H') (missing P), 'PT' or 'P' (no components), 'P1Y6M' (years/months unsupported), 'P1.5H' (fraction), 'P1D2H' (T missing before time parts).

Common situations: ICS DURATION values from producers that emit years/months; human-written shorthand like '30m'; forgetting the T separator between date and time parts.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/2d310d32a6529fee. Report an issue: GitHub.