# XBRL context period mismatch: quarter versus year validation error

## TL;DR
Context period mismatches happen because XBRL facts carry contexts declaring instant versus duration and specific dates, and quarterly facts sitting in annual contexts (or vice versa) fail validation. The fix is reading each fact's context before using its value: filter duration contexts for flow metrics like revenue and instant contexts for stock metrics like cash. When building period series, key facts by end date and period type, never by tag name alone.

## The error
```text
XBRL validation error: context period mismatch
fact [us-gaap Revenues] context duration 2026-01-01 to 2026-12-31 used as quarterly value
```

## When this helps
- XBRL validation fails on context period mismatches
- quarterly and annual values get mixed in a series
- revenue shows the full-year number where a quarter was expected
- building period time series from instance documents

## When it doesn't
- the instance failed to parse entirely; fix the parse first
- you need segment breakdowns; those are dimensional contexts, a separate concern
- the filing is HTML-era with no XBRL; there are no contexts to mismatch

## Works with
python 3.8+ with re; arelle exposes contexts natively for heavier use. Context semantics are XBRL 2.1 standard.

## Steps
### 1. List the contexts in the instance with their period types
```python
from arelle import Cntlr
cntlr = Cntlr.Cntlr()
model = cntlr.modelManager.modelXbrl
model.load("instance.xml")
print("contexts:", len(model.contexts))
for cid, ctx in list(model.contexts.items())[:5]:
    kind = "instant" if ctx.isInstantPeriod else "duration"
    per = ctx.instantDatetime if ctx.isInstantPeriod else (ctx.startDatetime, ctx.endDatetime)
    print(cid, kind, per)
```
Expected: Context counts split by instant versus duration. Annual filings lean on durations; balance-sheet-heavy extracts lean on instants.

### 2. Map each fact to its context period
```python
from arelle import Cntlr
cntlr = Cntlr.Cntlr()
model = cntlr.modelManager.modelXbrl
model.load("instance.xml")
want = "2026-09-30"
for f in model.facts:
    if f.qname.localName == "Revenues" and "us-gaap" in str(f.qname.namespaceURI):
        ctx = f.context
        end = str(ctx.instantDatetime or ctx.endDatetime)[:10]
        if end == want:
            print("Q3 revenue:", f.value, "context:", ctx.id)
```
Expected: A context id to period map. Every fact lookup goes through this map before the value is trusted.

### 3. Filter facts to the period you actually want
```python
from arelle import Cntlr
from collections import Counter
cntlr = Cntlr.Cntlr()
model = cntlr.modelManager.modelXbrl
model.load("instance.xml")
ends = Counter()
for f in model.facts:
    if f.qname.localName == "Revenues" and "us-gaap" in str(f.qname.namespaceURI):
        ctx = f.context
        ends[str(ctx.instantDatetime or ctx.endDatetime)[:10]] += 1
for end, n in sorted(ends.items()):
    print(end, n)
print("each end date is a distinct period; pick the want date")
```
Expected: Revenue facts with their context refs. The context map from step 2 decides which ones are Q3 versus full year.

### 4. Key stored facts by tag plus end date plus period type
```python
import json
rec = {"tag": "us-gaap:Revenues", "end": "2026-09-30", "period": "duration", "value": 1234567}
fp = (rec["tag"], rec["end"], rec["period"])
store = {str(fp): rec["value"]}
print("stored under:", str(fp))
```
Expected: A composite key. Tag-only keys collide across quarters and years; the composite key keeps periods distinct forever.

## Other ways people phrase this
### xbrl context period mismatch quarter year
The classic mix-up. Annual duration contexts look like big quarters to naive parsers.

### xbrl instant vs duration validation failed
Stock versus flow confusion. Balance sheet items are instants; income items are durations.

### duplicate xbrl facts different contexts
Same tag, different contexts is normal, not duplication. Key by context.

## Why it happens
XBRL separates the what (tag) from the when (context). A single tag like Revenues appears many times with different contexts: quarters, years, segments. Code that reads values by tag alone grabs whichever context comes first, which is how annual numbers end up labeled quarterly. The context is part of the fact's identity.

## Edge cases
- Some filers use 53-week years; duration end dates will not match calendar quarters exactly.
- Restated periods create new contexts with the same dates; prefer the latest amendment's instance.
- Segment dimensions multiply contexts; filter dimensions before periods.
- The companyfacts API pre-resolves periods into start/end fields, which avoids this class of bug.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_tPFqeKBOuIEuYbmLzXx9Aw
