PCT 30/31 Month Deadline Calculators: Architecture & Implementation Guide
The transition from the international Patent Cooperation Treaty (PCT) phase to national or regional prosecution is one of the highest-risk inflection points in patent portfolio management, and a PCT 30/31 month deadline calculator exists to close the specific gap between a WIPO priority date and the jurisdiction-specific filing window it implies. This page specifies how to build that calculator as a deterministic component: given an earliest valid priority date and a set of designated offices, it must return an auditable due date for each office, correctly applying the 30-month baseline of PCT Article 22 (and Article 39 for offices where a demand alters the timeline), the 31-month window used by the European Patent Office (EPO), calendar rollover rules, and business-day shifts. Miscalculation directly triggers application abandonment, so month-based arithmetic, version-pinned rule mapping, and strict input validation are non-negotiable. This component slots into the wider Automated Deadline Calculation & Rule Engines pipeline and hands its output to the National Phase Entry Date Logic module for downstream reminder scheduling.
Compliance & Scope Boundaries
A deadline calculator produces computational outputs; it does not constitute legal advice, and every deployment must encode that boundary explicitly. The 30-month baseline of PCT Article 22(1) is a treaty floor that contracting states may extend, restrict, or condition through reservations and national law. The calculator is therefore permitted to compute and surface a candidate deadline, flag applications approaching a statutory limit, and record the exact rule version applied — but it must not auto-file, auto-pay national fees, or suppress a paralegal review gate. Restoration of the priority right (PCT Rule 26bis.3 and Rule 49ter) and discretionary late-entry petitions require substantive attorney judgment and must never be resolved by arithmetic alone.
Scope is deliberately narrow: this component resolves the entry deadline only. Translation-filing windows, national fee schedules, and post-entry examination clocks are separate obligations owned by adjacent modules — for example, the PCT National Phase Entry Rules framework governs the office-by-office documentary requirements that follow entry. When the calculator ingests upstream office data, it must respect the rate limits and robots.txt obligations of the source portals; bulk WIPO status queries should route through the sanctioned polling patterns rather than scraping, and any cached contracting-states data must carry a retrieval timestamp so stale reservations are never silently trusted.
Operational Action: Encode a hard “advice boundary” flag on every response payload and route any application inside a restoration or late-entry window to a mandatory human review queue. Never let the calculator emit a deadline that has bypassed the review gate for a flagged matter.
Prerequisites & Dependency Map
Before the first deadline is computed, the following upstream data sources, libraries, and configuration artifacts must be in place. Treat each as a pinned dependency, not an ambient assumption.
- Priority data source. A structured priority claim list per application (priority number, country, date, withdrawal status), sourced from your docketing system of record or synced from WIPO PATENTSCOPE. The earliest valid, non-withdrawn claim is the anchor date.
- Contracting-states rule set. The current 30-vs-31-month window per designated office, derived from the WIPO PCT Contracting States table. Store this as version-controlled configuration, never as inline constants.
- Holiday calendars. Per-jurisdiction business-day and office-closure data (for the USPTO, U.S. federal holidays under 37 CFR § 1.7; for the EPO, closure days published in the Official Journal). Pin these to a dated release.
- Library versions. Python 3.11+ (for the stdlib
zoneinfomodule anddatetimeimprovements),python-dateutil2.8+ for calendar-awarerelativedelta, andpydantic2.x for request/response validation. - Rule-version identifier. A monotonic version string (e.g.
pct-rules-2026.02) that stamps every computation so a deadline can be forensically reconstructed against the exact rule snapshot that produced it.
Operational Action: Fail closed. If the contracting-states configuration cannot be loaded, or its retrieval timestamp is older than your staleness threshold, the calculator must refuse to compute rather than fall back to a hardcoded default window.
Step-by-Step Implementation
The calculation proceeds as a short, verifiable pipeline. Each step is independently testable, and no step may mutate state owned by a prior one.
Step 1 — Resolve the priority anchor date
Multi-priority applications require selecting the earliest valid claim while discarding withdrawn or malformed predecessors. Resolve this before any month arithmetic so the entire family tree collapses to a single defensible baseline.
from dataclasses import dataclass
from datetime import date
@dataclass(frozen=True)
class PriorityClaim:
number: str
country: str # ISO 3166-1 alpha-2
claimed_date: date
withdrawn: bool = False
def resolve_anchor(claims: list[PriorityClaim]) -> date:
"""Return the earliest non-withdrawn priority date.
Raises ValueError when no valid claim exists, so downstream
arithmetic can never run against a missing anchor.
"""
valid = [c.claimed_date for c in claims if not c.withdrawn]
if not valid:
raise ValueError("No valid (non-withdrawn) priority claim to anchor on")
return min(valid)
Step 2 — Apply corresponding-date month arithmetic
Naive addition with datetime.timedelta fails here because it operates on fixed day counts, not calendar months. PCT deadlines follow the “corresponding date” rule: if the target month lacks the original day (for example, 31 August + 6 months lands in a month with no 31st), the deadline defaults to the last valid day of that month. dateutil.relativedelta clamps to month-end automatically.
from datetime import date
from dateutil.relativedelta import relativedelta
def add_months(anchor: date, months: int) -> date:
"""Add calendar months with corresponding-date rollover.
relativedelta clamps to month-end when the source day exceeds the
target month's length (e.g. 31 Aug + 6 months -> 28/29 Feb).
Reference: https://dateutil.readthedocs.io/en/stable/relativedelta.html
"""
return anchor + relativedelta(months=months)
Step 3 — Look up the jurisdictional window
Hardcoding thresholds creates compliance drift. Externalize per-office rules into declarative configuration keyed by ISO 3166-1 alpha-2 (plus the EP regional code), each entry carrying a source citation.
# pct_jurisdiction_rules.yaml
# Source: WIPO PCT Contracting States table (verify retrieval date before deploy)
# https://www.wipo.int/pct/en/pct_contracting_states.html
rule_version: "pct-rules-2026.02"
retrieved_utc: "2026-02-01T00:00:00Z"
jurisdictions:
US:
base_months: 30 # 35 U.S.C. 371(b); PCT Art. 22(1)
restoration_available: true
business_day_shift: "following"
EP:
base_months: 31 # PCT Art. 39(1)(b) as applied under the EPC
restoration_available: true
business_day_shift: "following"
JP:
base_months: 30 # PCT Art. 22(1); JPO national entry
restoration_available: true
business_day_shift: "following"
CN:
base_months: 30 # PCT Art. 22(1); no reservation on file
restoration_available: false
business_day_shift: "following"
Step 4 — Apply the calendar (business-day) shift
A statutory deadline that lands on a weekend or an office-closure day rolls forward to the next business day. Keep this logic separate from the month math so the holiday dataset can be updated independently and the raw statutory date remains inspectable in the audit record.
from datetime import date, timedelta
def shift_to_business_day(target: date, closures: frozenset[date]) -> date:
"""Roll forward to the next open day, skipping weekends and office closures.
'closures' is a jurisdiction-specific set of non-working dates
(federal holidays, EPO Official-Journal closure days, etc.).
"""
while target.weekday() >= 5 or target in closures: # 5=Sat, 6=Sun
target += timedelta(days=1)
return target
Because the corresponding-date rule (Step 2) and the business-day shift (Step 4) are distinct operations, the calculator retains both the raw statutory date and the adjusted date — a distinction that malpractice reviewers rely on when reconstructing why a given date was docketed.
API Contract & Schema
High-throughput docketing systems expect a stateless, idempotent endpoint that returns an audit-ready payload. The Pydantic v2 contract below enforces strict input validation, supports multiple calculation modes, and stamps every result with a rule version and an audit hash so downstream compliance dashboards can trace exactly which configuration snapshot produced a deadline. The same audit-hash discipline underpins the append-only audit trail that governs access to finalized deadline records.
import hashlib
from datetime import date, datetime, timezone
from typing import Literal
from pydantic import BaseModel, Field, field_validator
class PCTDeadlineRequest(BaseModel):
priority_date: date
jurisdictions: list[str] = Field(..., min_length=1, max_length=50)
calculation_mode: Literal["standard", "restoration", "grace_period"] = "standard"
idempotency_key: str = Field(..., min_length=8, max_length=64)
@field_validator("jurisdictions")
@classmethod
def validate_iso_codes(cls, v: list[str]) -> list[str]:
if not all(len(code) == 2 and code.isalpha() for code in v):
raise ValueError("Jurisdictions must be ISO 3166-1 alpha-2 codes")
return [code.upper() for code in v]
class PCTDeadlineResult(BaseModel):
jurisdiction: str
statutory_deadline: date # raw corresponding-date result
docketed_deadline: date # after business-day shift
applied_months: int
calculation_mode: str
rule_version: str
audit_hash: str
computed_at_utc: str
def build_audit_hash(
priority_date: date, jurisdiction: str, docketed: date, rule_version: str
) -> str:
payload = f"{priority_date.isoformat()}|{jurisdiction}|{docketed.isoformat()}|{rule_version}"
return hashlib.sha256(payload.encode()).hexdigest()
def now_utc_iso() -> str:
return datetime.now(timezone.utc).isoformat()
The idempotency_key prevents duplicate docketing during network retries: a repeated request with the same key must return the previously computed result rather than recomputing (and re-emitting) a deadline. The audit_hash is a SHA-256 digest over the priority date, jurisdiction, docketed deadline, and rule version — a change in any input yields a different hash, so silent rule drift becomes detectable.
Edge Cases & Failure Modes
- Month-end rollover. A 31 August priority date plus 30 months resolves to 28/29 February;
relativedeltaclamps correctly, but a naivetimedelta(days=30*months)silently overshoots. Guard this with an explicit regression test (below). - Weekend and holiday collisions. A statutory deadline on a Saturday, Sunday, or office-closure day rolls forward. Note that consecutive closures (a holiday adjoining a weekend) may push the date several days — the loop in Step 4 handles this, but the holiday dataset must be complete for the shift to be correct.
- Timezone off-by-one. A deadline expiring at 17:00 in Geneva can already be the next calendar day in Tokyo. Normalize priority dates using
zoneinfoat the boundary and treat the date as the office-local civil date; never derive it from a machine-localdatetime.now(). - Stale contracting-states data. A contracting state may file or withdraw a reservation that changes its effective window. Trusting a cached rule set past its staleness threshold can silently produce a wrong month count — fail closed instead (see Prerequisites).
- Article 39 vs Article 22. Filing a demand for international preliminary examination historically affected certain offices’ timelines; most now apply a uniform 30/31-month window regardless, but the calculator must key on the current per-office rule, not a blanket assumption.
- Retry storms and circuit breaking. When the calculator pulls upstream office data, wrap remote calls with exponential backoff and a circuit breaker so a portal outage degrades gracefully rather than hammering the source or emitting partial results.
from zoneinfo import ZoneInfo
from datetime import datetime
def office_local_date(instant_utc: datetime, office_tz: str) -> date:
"""Project a UTC instant onto an office's civil date.
Prevents off-by-one docketing when the priority instant straddles
midnight in the designated office's timezone.
"""
return instant_utc.astimezone(ZoneInfo(office_tz)).date()
Verification & Regression Testing
Every rule change must pass a regression suite anchored to known dates. These assertions pin the two behaviours most likely to regress silently: corresponding-date rollover and the EPO 31-month divergence.
from datetime import date
def test_epo_uses_31_months() -> None:
anchor = date(2023, 1, 15)
assert add_months(anchor, 31) == date(2025, 8, 15) # EP window
def test_us_uses_30_months() -> None:
anchor = date(2023, 1, 15)
assert add_months(anchor, 30) == date(2025, 7, 15) # US window
def test_month_end_rollover_clamps() -> None:
# 31 Aug 2022 + 30 months has no 31st in the target month.
assert add_months(date(2022, 8, 31), 30) == date(2025, 2, 28)
def test_business_day_shift_skips_weekend() -> None:
# 15 Jul 2023 is a Saturday -> rolls to Monday 17 Jul.
assert shift_to_business_day(date(2023, 7, 15), frozenset()) == date(2023, 7, 17)
def test_anchor_ignores_withdrawn_claims() -> None:
claims = [
PriorityClaim("P1", "US", date(2022, 1, 1), withdrawn=True),
PriorityClaim("P2", "US", date(2022, 3, 1)),
]
assert resolve_anchor(claims) == date(2022, 3, 1)
Run this suite nightly against historical prosecution data as well as the synthetic fixtures above; a diff between a recomputed deadline and its stored audit_hash is the earliest signal of rule drift or a holiday-dataset regression.
Operational Action Summary
Before promoting the calculator to production, confirm each deployment decision:
- Pin the rule set. Every response carries a
rule_version, and that version maps to an immutable, committed configuration snapshot. No inline window constants. - Fail closed. Missing, malformed, or stale contracting-states configuration blocks computation instead of defaulting.
- Log immutably. Persist inputs, rule version, statutory date, docketed date, and audit hash to append-only storage for forensic reconstruction.
- Gate on humans. Restoration and late-entry matters route to a mandatory paralegal review queue before any deadline is treated as final.
- CI/CD for rules. Treat jurisdictional configuration as code — peer review by patent counsel, automated regression run, and version bump on every merge.
- Degrade gracefully. Backoff plus circuit breaker on all upstream portal calls; never emit a partial batch as if it were complete.
Frequently Asked Questions
What happens if the 30-month PCT deadline falls on a weekend in the US?
Why does the EPO use 31 months when most offices use 30?
Can the calculator handle applications with multiple priority claims?
Should the calculator ever file or pay national fees automatically?
How do I keep jurisdiction windows current?
Related
- ← Automated Deadline Calculation & Rule Engines — the parent pipeline this calculator plugs into
- National Phase Entry Date Logic — downstream module that schedules reminders from these deadlines
- PCT National Phase Entry Rules — office-by-office documentary requirements after entry
- How to Map PCT Rule 47 Deadlines to National Phase — seeding national windows from ISR/EISR publication dates
- Security & Access Control Boundaries — audit-trail and access model for finalized deadline records