← All articles
Data & Analytics

DAX time intelligence when your periods are text, not dates

Health and program exports hand you periods like "2026Q2" — strings, not dates. Every built-in time-intelligence function quietly fails on that. Here's the pattern for prior-period comparisons that actually works when your calendar is text.

Sometimes the smaller tool is the right one.

From this article
By Enkop Intelligence · August 13, 2026 · 4 min read
Power BIDAXData modelingDHIS2

Power BI’s time-intelligence functions — TOTALYTD, SAMEPERIODLASTYEAR, DATEADD, PREVIOUSQUARTER — are genuinely useful, and they all share one requirement that program data routinely violates: they need a real date column, in a table marked as a date table, with contiguous dates. What you actually get from DHIS2, aggregate reporting tools, and most donor systems is a text period: 2026Q2, 2026Q2, Jan-26, FY26 Q3. A string that looks like a period but carries none of the ordering or arithmetic a date has.

Point PREVIOUSQUARTER at that and it returns blank, or worse, something plausible-but-wrong. The fix isn’t to fight the functions — it’s to stop needing them for the one comparison you make most: this period versus the one before it.

The problem: “the previous quarter” isn’t obvious to a string

Ask a human for the quarter before 2026Q2 and they say 2026Q1 instantly. Ask them for the one before 2026Q1 and they say 2025Q4 — the year rolls back and the quarter wraps to 4. That wrap is the whole difficulty. A naive “subtract one” breaks at every year boundary, and string sorting alone won’t save you because 2025Q4 sorts after 2025Q1 correctly but the arithmetic of the wrap still has to happen somewhere.

So you compute the prior period explicitly: parse the year and quarter out of the string, handle the Q1→Q4 rollover, reassemble the prior period as a string, and look up the measure at that period. Here’s the pattern, written against a period table whose key is a QuarterID like 2026Q2:

Clients currently in care – Previous Quarter =
VAR CurrentPeriod =
    SELECTEDVALUE('period'[QuarterID])

VAR CurrentYr =
    VALUE( LEFT( CurrentPeriod, 4 ) )

VAR CurrentQtr =
    VALUE( RIGHT( CurrentPeriod, 1 ) )

VAR PriorYr =
    IF( CurrentQtr = 1, CurrentYr - 1, CurrentYr )

VAR PriorQtr =
    IF( CurrentQtr = 1, 4, CurrentQtr - 1 )

VAR PriorPeriod =
    FORMAT( PriorYr, "0000" ) & "Q" & FORMAT( PriorQtr, "0" )

RETURN
    IF(
        ISBLANK( CurrentPeriod ),
        BLANK(),
        CALCULATE(
            [Clients currently in care],
            REMOVEFILTERS( 'period' ),
            'period'[QuarterID] = PriorPeriod
        )
    )

Why each piece is there

SELECTEDVALUE guards the grain. Prior-period logic only makes sense when a single period is in context — one column of a matrix, one point on a line. SELECTEDVALUE returns the period when exactly one is selected and blank otherwise, which is exactly when you want the measure to stand down rather than compute nonsense across a range.

LEFT / RIGHT / VALUE parse the string into numbers. LEFT(…,4) takes the year, RIGHT(…,1) the quarter, and VALUE turns those substrings into integers you can do arithmetic on. This is the step that replaces the date column you don’t have — you’re extracting ordering from the text yourself.

The two IFs handle the wrap. If the current quarter is 1, the prior quarter is 4 of the previous year; otherwise it’s the same year, one quarter back. Two lines, and every year-boundary bug disappears.

FORMAT reassembles the key in the exact shape of your period column. The "0000" and "0" format strings matter — they rebuild 2026Q1 with the same zero-padding and structure as the real QuarterID, so the lookup matches. If your periods look like 2026-Q1 or Q1 2026, this is the one line you adjust.

REMOVEFILTERS('period') then re-filters to the prior period. This is the quiet heart of it. The current filter context is pinned to 2026Q2; you strip that, then set the context to the computed prior period, and evaluate the base measure there. Without the REMOVEFILTERS, you’d be intersecting “this quarter” with “last quarter” and getting nothing.

What it unlocks

Once you have a reliable prior-period measure, the things stakeholders actually ask for become one subtraction away: net change quarter-over-quarter, percentage change, directional indicators for a matrix. The quarterly net-change view below — a cumulative “currently in care” line over per-quarter net change, disaggregated by age and sex — is built entirely on this pattern. Every net-change figure is current-period minus prior-period, computed with the logic above, and it holds up across every year boundary in the series.

A Power BI dashboard showing patients currently in care and quarterly net change across twenty text-based quarter periods, with KPI cards, a cumulative line over net-change bars disaggregated by sex, and an age-group net-change matrix. The net-change figures depend on a reliable previous-quarter measure.
Quarterly net change built on the prior-period pattern — figures and indicator names here are synthetic.

The honest caveat

This pattern is deliberately narrow: it solves prior single period, not the full sweep of time intelligence. If you need rolling twelve-month windows, year-to-date across fiscal calendars, or same-period-last-year at scale, the right move is still to build a proper date table and map your text periods onto it once — then the native functions work as designed. But for the single most common ask in program reporting — “how does this quarter compare to last quarter?” — computing the prior period from the string is direct, transparent, and unbreakable at the year boundary. Sometimes the smaller tool is the right one.


This is a field note from our practice. For the modelling groundwork that makes measures like this clean to write, see Modeling disaggregated DHIS2 data in Power BI.

The Enkop dispatch

Field notes and tutorials, twice a month.

Practical data & AI methods for mission-driven organizations — the same rigor we bring to engagements, written to be used.

Twice a month at most. Unsubscribe any time via the link in every email.

Not sure which engagement fits?

Book thirty minutes. You will leave with a clearer view of your options and an honest recommendation on the right next step.

Book a 30-minute call →