Minguo.tw
Home / ROC dates in Python

Converting ROC (Taiwan) Dates in Python

Python's standard library has no ROC or Minguo calendar type. Converting a date like 115/07/29 means parsing the ROC year yourself and adding 1911 before it ever touches datetime.date.

The conversion, as a function

There is no shortcut import for this. datetime, date and calendar all assume the Gregorian year, so the ROC offset has to happen in your own code, once, in one place:

from datetime import date

def roc_to_western(roc_year: int, month: int, day: int) -> date:
    if roc_year <= 0:
        raise ValueError("ROC year must be 1 or greater; there is no ROC year 0")
    return date(roc_year + 1911, month, day)

roc_to_western(115, 7, 29)   # date(2026, 7, 29)
roc_to_western(100, 1, 1)    # date(2011, 1, 1)

Keep this as the single conversion point in a codebase. Every bug report about Taiwanese dates in production Python traces back to someone adding 1911 inline in three different places and getting one of them wrong. Check a date quickly with the ROC year converter before you commit to a fix.

Advertisement

Parsing the formats you'll actually receive

Government exports, bank CSVs and scanned-form OCR produce ROC dates in several shapes: 115/07/29, 115.7.29, 115-07-29, and the compact 1150729 used in some fixed-width files. One function with two regular expressions covers all of them:

import re
from datetime import date

_SEP = re.compile(r"^(\d{2,3})[/.\-](\d{1,2})[/.\-](\d{1,2})$")
_COMPACT = re.compile(r"^(\d{3})(\d{2})(\d{2})$")

def parse_roc_date(text: str) -> date:
    text = text.strip()
    m = _SEP.match(text) or _COMPACT.match(text)
    if not m:
        raise ValueError(f"unrecognised ROC date: {text!r}")
    roc_year, month, day = (int(g) for g in m.groups())
    return roc_to_western(roc_year, month, day)

parse_roc_date("115/07/29")   # date(2026, 7, 29)
parse_roc_date("115.7.29")    # date(2026, 7, 29)
parse_roc_date("1150729")     # date(2026, 7, 29)

The compact form is genuinely ambiguous — a 7-digit string could also be a phone extension or an internal ID. Only apply _COMPACT in a field you already know is a date column; don't run it over free text.

Formatting a Python date back into ROC form

The reverse direction is just as short, and worth zero-padding to three digits so the output survives past ROC year 999 without a format change and matches how Taiwanese documents actually print the year:

def western_to_roc_string(d: date) -> str:
    roc_year = d.year - 1911
    if roc_year <= 0:
        raise ValueError("date is before the ROC era (pre-1912)")
    return f"{roc_year:03d}/{d.month:02d}/{d.day:02d}"

western_to_roc_string(date(2026, 7, 29))  # '115/07/29'

pandas and other libraries don't know about ROC years

pandas.to_datetime(), dateutil.parser.parse() and every general-purpose date library available for Python treat a bare year like 115 as the year 115 CE, not as a ROC year. If you have a DataFrame column of ROC dates, convert the year before you call to_datetime, rather than trying to coax the parser into doing it:

import pandas as pd

df["roc_year"] = df["date_str"].str.slice(0, 3).astype(int)
df["western_date"] = pd.to_datetime(
    (df["roc_year"] + 1911).astype(str) + df["date_str"].str.slice(3)
)

This is the same rule as the single-function approach above: isolate the +1911 step, apply it once, and let every downstream call work in ordinary Gregorian years.

Edge cases that will bite you in production

There is no ROC year 0. The calendar runs ROC 1 (1912) forward and switches to "before ROC N" going backward, so a naive roc_year - 1911 that produces 0 or a negative number is a bug, not a valid date — raise instead of silently constructing a date in year 1911 or earlier.

Two-digit year fields truncate at ROC 100. A field that only stores two digits shows ROC 100 (2011) as 00, which is indistinguishable from a genuinely malformed record. This is the Y1C bug; see the full write-up if you're auditing a legacy import for it.

Don't guess the calendar from field width alone. A three-digit year in the 90–130 range is almost always ROC, but a system migrated from a Western-dated predecessor can leave a mixed column. Validate against a known document type rather than inferring purely from the number of digits.

Frequently asked questions

How do I convert an ROC year to a Western year in Python?

Add 1911 to the ROC year. There is no library call for this in the standard library — date(roc_year + 1911, month, day) using datetime.date is the whole conversion.

Does Python's datetime module support the ROC calendar natively?

No. The standard library has no Minguo or ROC calendar type, unlike Java's java.time.chrono.MinguoDate or C#'s TaiwanCalendar. Every ROC date must be converted to a Gregorian year before it becomes a datetime.date or datetime.datetime.

How do I parse a ROC date like 115/07/29 in Python?

Split on the separator, take the first field as the ROC year, add 1911, and pass the result to date(). A small regex handles the slash, dot and compact digit variants in one function; see the parse_roc_date() example on this page.

What if the ROC year is 0 or negative?

Raise an error. There is no ROC year 0 — the calendar goes from before-ROC years straight to ROC year 1 (1912). Dates before 1912 are conventionally written as negative or "before ROC N" and need their own branch, not a silent pass into date().

Can pandas parse a column of ROC-year dates directly?

No. pandas.to_datetime has no ROC calendar option, so it will silently misread a year like 115 as the year 115 CE. Add 1911 to the year column before calling to_datetime, or write a small apply() that reuses the same conversion function.

Advertisement

Related