Converting ROC (Taiwan) Dates in JavaScript
JavaScript's Date object has no ROC or Minguo calendar. To turn
115/07/29 into a real date you add 1911 to the year yourself, and remember
that Date's month argument is zero-indexed.
The conversion, as a function
Two things trip people up here: the +1911 offset, and the fact that new Date() takes January as
0, not 1. Both are easy to get wrong silently, because a wrong month just rolls into
the next one instead of throwing.
function rocToWestern(rocYear, month, day) {
if (rocYear <= 0) {
throw new RangeError("ROC year must be 1 or greater; there is no ROC year 0");
}
return new Date(rocYear + 1911, month - 1, day);
}
rocToWestern(115, 7, 29); // Wed Jul 29 2026 (local time)
rocToWestern(100, 1, 1); // Sat Jan 01 2011
Check any single conversion against the ROC year converter if you want a second source before shipping a fix.
Parsing the string formats you'll receive
ROC dates arrive as 115/07/29, 115-07-29, 115.7.29, or the compact
1150729 from fixed-width exports. Never hand any of these to new Date(string) or
Date.parse() — both interpret an ambiguous string using the browser's own heuristics, which
differ between engines and will silently treat 115 as a literal year:
function parseRocDate(text) {
const sep = text.trim().match(/^(\d{2,3})[/.\-](\d{1,2})[/.\-](\d{1,2})$/);
const compact = !sep && text.trim().match(/^(\d{3})(\d{2})(\d{2})$/);
const m = sep || compact;
if (!m) throw new Error(`unrecognised ROC date: ${text}`);
const [, rocYear, month, day] = m.map(Number);
return rocToWestern(rocYear, month, day);
}
parseRocDate("115/07/29"); // Wed Jul 29 2026
parseRocDate("1150729"); // Wed Jul 29 2026
Formatting a Date back into ROC form
For display, Intl.DateTimeFormat can format an existing Date object as an ROC year
directly, using the zh-TW-u-ca-roc locale extension — useful if you just need a label and
don't want to hand-build the string:
const d = new Date(2026, 6, 29); // 29 July 2026
new Intl.DateTimeFormat("zh-TW-u-ca-roc", {
year: "numeric", month: "2-digit", day: "2-digit"
}).format(d);
// "民國115/07/29" — note the 民國 era prefix, and that the exact
// output varies with the JS engine’s ICU data. Use the manual version below
// when you need a bare 115/07/29.
// Manual version if you need an exact, engine-independent format:
function westernToRocString(d) {
const rocYear = d.getFullYear() - 1911;
if (rocYear <= 0) throw new RangeError("date is before the ROC era (pre-1912)");
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${String(rocYear).padStart(3, "0")}/${mm}/${dd}`;
}
Intl.DateTimeFormat is read-only in this direction — it formats, it does not parse. There
is no built-in way to go from an ROC string back to a Date without writing the parser above.
Node.js and browser behave the same way
This is not a browser quirk or a Node quirk; V8, SpiderMonkey and JavaScriptCore all implement the same ECMA-262
Date semantics, so the +1911 conversion and the zero-indexed month apply identically whether the
code runs client-side or on a server. There is no separate server-side ROC date type to reach for.
Edge cases that will bite you in production
There is no ROC year 0. A calculation that produces rocYear <= 0 is a bug in
the input or the formula, not a valid pre-1912 date — before-ROC years need their own handling, not a pass
into rocToWestern.
Two-digit year fields truncate at ROC 100. Legacy exports or old form widgets that only kept
two digits show ROC 100 (2011) as 00. See the Y1C write-up
for how to detect and repair this in a dataset.
Timezones affect the day, not the year offset. new Date(rocYear + 1911, month - 1, day)
constructs a date in the browser's local timezone. If your users and your server are in different zones, a date
near midnight can shift by a day when serialised — that's an ordinary JavaScript date-handling issue, not
specific to ROC years, but it compounds with them because government forms rarely include a time component to
anchor the day.
Frequently asked questions
How do I convert an ROC year to a Western year in JavaScript?
Add 1911 to the ROC year, then remember JavaScript's Date constructor takes a zero-indexed month:
new Date(rocYear + 1911, month - 1, day).
Does Intl.DateTimeFormat support the ROC or Minguo calendar?
Yes, indirectly. Passing the locale extension zh-TW-u-ca-roc to Intl.DateTimeFormat
formats a JavaScript Date as an ROC year, but it only formats an existing Date —
it cannot parse a ROC-year string back into one, so you still need your own parsing function.
Why does new Date(115, 6, 29) give the wrong year?
Because JavaScript's Date constructor treats a two- or three-digit year as a literal year number,
not a ROC year — new Date(115, 6, 29) creates 29 July in the year 115 CE. You must add 1911
yourself before calling the constructor.
How do I parse a ROC date string like 115/07/29 in JavaScript?
Split the string on the separator, parse the first segment as the ROC year, add 1911, and build the date with
new Date(year, month - 1, day). Do not pass the raw string to new Date() or
Date.parse() — both assume a Western year.
Is there an npm package for ROC date conversion?
Several small unofficial packages exist on npm, but none is a widely-adopted standard, and this page has not
audited any of them for correctness. For most projects, the handful of lines in the parseRocDate()
example here is simpler and easier to audit than adding a dependency.
Related
- ROC year converter — convert any full date, with weekday and formal written form
- Validating ROC date input — regex patterns and the edge cases that break naive ones
- ROC dates in Python — the same conversion, server side
- Storing ROC dates in a database — do's and don'ts for schema design
- The Y1C problem — when ROC year 100 broke two-digit date fields in 2011
- FAQ — ROC years, lunar dates, zodiac and age
- ROC dates in Excel — formulas and the date-detection problem