Convert a Gregorian Date to a Japanese Era Date

To convert a Gregorian date into a Japanese-era date, attach the japanese calendar to a Temporal.PlainDate with .withCalendar('japanese'), then read its .era (reiwa, heisei, showa) and .eraYear properties — or format the whole thing with Intl.DateTimeFormat using calendar: 'japanese'. This page is part of Calendar Systems and Era Handling.

Why This Scenario Is Tricky

The Japanese imperial calendar counts years within named eras (Reiwa, Heisei, Shōwa…) that reset to year 1 each time the era changes, and the era changes are set by imperial succession rather than by any regular cycle. That is what makes conversion tricky: there is no formula from Gregorian year to era year, because the mapping depends on knowing the exact date each era began and ended. Reiwa started on 1 May 2019, so a date in April 2019 is Heisei 31 while a date one day later is Reiwa 1 — the boundary falls in the middle of a Gregorian year, not at January 1. Any hand-rolled conversion that assumes era boundaries align with calendar years will be wrong for every date near a transition.

Temporal sidesteps the formula entirely by delegating to the CLDR/ICU calendar data that ships with the platform. withCalendar('japanese') re-interprets a date in the Japanese calendar and exposes era (a CLDR era id like 'reiwa') and eraYear (the year within that era), while year still returns the ISO year for any code that needs it. Because the era table lives in the internationalization data rather than in your code, it stays correct across the exact mid-year boundaries and updates when a future era is added, which a hard-coded lookup never would.

The Japanese imperial calendar counts years from the start of the current emperor's era (元号, gengō), not from a fixed epoch. "2019" is not a single Japanese year: January through April 30 was Heisei 31, and May 1 onward was Reiwa 1 (年号 written as 令和元年, "Reiwa first year"). The era changed mid-year, so any naive gregorianYear - offset formula is wrong for dates straddling that boundary.

Legacy Date cannot help here at all: it has no era concept and assumes the proleptic Gregorian calendar. You would have to hard-code the era transition table yourself — and keep it current whenever a new emperor accedes. The Temporal API and Intl both defer to the CLDR/ICU era data shipped with the runtime, so the boundaries stay correct without bespoke tables.

The diagram below shows the most recent era transitions on the Gregorian timeline and where the mid-year Heisei→Reiwa boundary falls.

Japanese era transitions on the Gregorian timelineShowa ends 7 January 1989, Heisei runs to 30 April 2019, Reiwa begins 1 May 2019 — a transition that occurs in the middle of the Gregorian year 2019.Showa (昭和)Heisei (平成)Reiwa (令和)1 May 2019mid-year boundary…–19891989–20192019–

Minimal Working Solution

The shortest correct conversion reads the era fields straight off a japanese-calendar PlainDate:

import { Temporal } from '@js-temporal/polyfill';

// Start from an ISO/Gregorian date, then re-interpret it in the Japanese calendar
const jp = Temporal.PlainDate.from('2024-05-01').withCalendar('japanese');

console.log(jp.era);     // 'reiwa' — CLDR era id, not the Gregorian year
console.log(jp.eraYear); // 6  → Reiwa 6
console.log(jp.year);    // 2024 — the ISO year is always still available

.withCalendar() does not move the day; it re-labels the same instant in a different calendar system. The underlying ISO date is unchanged, so .year still returns 2024 while .eraYear returns the era-relative count.

Gregorian date to Japanese erawithCalendar('japanese') exposes era and eraYearGregorian date to Japanese eraPlainDate2019-05-01withCalendar('japanese')era 'reiwa'eraYear 1

Full Production Version

A production converter usually needs to go both directions and to render the result the way Japanese users expect. Reading era and eraYear off a withCalendar('japanese') date gives you the components; formatting them for display is a job for Intl.DateTimeFormat with the japanese calendar, which produces the correct localized era name (Reiwa in Latin script, or 令和 in Japanese) and handles the convention that era year 1 is often written 元年 ("first year") rather than "1". Letting the formatter render the era name keeps you out of the business of maintaining a name table, which is both tedious and a localization hazard.

Constructing a date from an era and era-year is the reverse operation, and Temporal supports it by accepting { era, eraYear, month, day, calendar: 'japanese' } in PlainDate.from. This is what you need when the input arrives as "Reiwa 6, May 1" — a form on a Japanese government or banking site, say — and you must turn it into an ISO date for storage. Store the ISO form as the canonical value and derive the era presentation on demand, so your database holds one unambiguous representation and the era formatting is purely a display concern. That separation keeps the era complexity at the edges of the system rather than in its core.

A real converter should accept either an ISO string or a PlainDate, validate input, and return both the structured era data and a localized display string. Caching the Intl.DateTimeFormat instance matters because constructing one is expensive.

import { Temporal } from '@js-temporal/polyfill';

interface JapaneseEraDate {
  era: string;        // 'reiwa' | 'heisei' | 'showa' | …
  eraYear: number;    // year within the era (1-based)
  isoYear: number;    // ISO/Gregorian year
  display: string;    // localized, e.g. '令和6年5月1日'
}

// Construct the formatter once and reuse it — building it per call is costly
const jaFormatter = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
  era: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  timeZone: 'UTC', // PlainDate has no time/zone; pin UTC to avoid host-zone day shifts
});

export function toJapaneseEra(input: string | Temporal.PlainDate): JapaneseEraDate {
  const iso =
    typeof input === 'string' ? Temporal.PlainDate.from(input) : input;

  if (iso.calendarId !== 'iso8601' && iso.calendarId !== 'gregory') {
    throw new RangeError(`Expected an ISO/Gregorian date, got ${iso.calendarId}`);
  }

  const jp = iso.withCalendar('japanese');

  // Intl.format() takes a Date, not a Temporal value — build a UTC-midnight Date
  // so the calendar day cannot drift backward in a negative-offset host zone
  const asDate = new Date(Date.UTC(iso.year, iso.month - 1, iso.day));

  return {
    era: jp.era ?? 'unknown',
    eraYear: jp.eraYear ?? jp.year,
    isoYear: iso.year,
    display: jaFormatter.format(asDate),
  };
}

console.log(toJapaneseEra('2024-05-01'));
// { era: 'reiwa', eraYear: 6, isoYear: 2024, display: '令和6年5月1日' }

For an English rendering, swap the locale and the era/month token widths:

const enFormatter = new Intl.DateTimeFormat('en-US-u-ca-japanese', {
  era: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  timeZone: 'UTC', // same UTC pin — keep the displayed day stable across servers
});

const may1 = Temporal.PlainDate.from('2024-05-01');
const asDate = new Date(Date.UTC(may1.year, may1.month - 1, may1.day));
console.log(enFormatter.format(asDate)); // 'May 1, 6 Reiwa'

Robust era converterValidate input, read era fields, format with IntlRobust era converterGregorian datewithCalendarera/eraYearIntl 令和

Verifying the Mid-Year Era Boundary

The whole point of using calendar data instead of arithmetic is the 2019 Heisei→Reiwa switch on May 1. This assertion block proves both sides of that boundary resolve correctly:

import { Temporal } from '@js-temporal/polyfill';

const lastHeisei = Temporal.PlainDate.from('2019-04-30').withCalendar('japanese');
const firstReiwa = Temporal.PlainDate.from('2019-05-01').withCalendar('japanese');

// 30 April 2019 is the final day of Heisei (Heisei 31)
console.assert(lastHeisei.era === 'heisei', 'Apr 30 2019 should be Heisei');
console.assert(lastHeisei.eraYear === 31, 'Apr 30 2019 should be Heisei 31');

// 1 May 2019 is the first day of Reiwa (Reiwa 1 — 元年, the "gan-nen" first year)
console.assert(firstReiwa.era === 'reiwa', 'May 1 2019 should be Reiwa');
console.assert(firstReiwa.eraYear === 1, 'May 1 2019 should be Reiwa 1');

// Both dates share the same ISO year despite different eras
console.assert(lastHeisei.year === firstReiwa.year, 'Both are ISO year 2019');

Era-boundary assertionsThe 2019-04-30 / 05-01 change flips era mid-yearAssertions that prove the edge case2019-04-30Heisei 312019-05-01Reiwa 1eraYear resetsto 1Intl ja-JP令和元年

Common Pitfalls

The headline pitfall is assuming era boundaries coincide with Gregorian year boundaries. They do not — Reiwa began on 1 May 2019 — so any conversion that derives the era year by subtracting a fixed offset from the Gregorian year is wrong for dates in the transition year and near the boundary. Delegate to the calendar data via withCalendar('japanese') rather than computing it. The second pitfall is hard-coding era names or their start dates in application code, which silently goes stale when a new era is proclaimed; the CLDR data is updated centrally, so reading names through Intl.DateTimeFormat keeps you current.

A third mistake is dropping the ISO year once you have the era components. Always keep the ISO year available (Temporal does) as the canonical, unambiguous value for storage and comparison, and treat the era pair as a derived display form. A fourth is forgetting the 元年 convention — that the first year of an era is conventionally written "gannen" rather than "1" — which the formatter handles but a manual string build would miss. Finally, pin the time zone when the era boundary matters, because a date near midnight at a boundary could fall on either side depending on the zone it is interpreted in.

Japanese-era pitfallsHard-coding era rangesWrongRightmap year → era by tablebreaks on new erawithCalendar('japanese')era supplied by Temporalassume era = calendar yearresets mid-yearread eraYearboundary-correct

Frequently Asked Questions

Why does .eraYear return 1 instead of 2019 for May 1, 2019?

Because the Japanese calendar counts years within the current era, not from a fixed epoch. May 1, 2019 is the first day of Reiwa, so .eraYear is 1 (written 令和元年). The ISO year 2019 is still available on the separate .year property.

Will Temporal know about a future era before the runtime is updated?

No. Era boundaries come from the CLDR/ICU data bundled with the JavaScript engine (or the @js-temporal/polyfill build). A new imperial era is only recognized once that data is updated, so keep your runtime and polyfill current for forward-dated calculations.

How do I convert a Gregorian date to a Japanese era date in JavaScript?

Start from an ISO/Gregorian Temporal.PlainDate and call withCalendar('japanese'), then read era (a CLDR id like 'reiwa'), eraYear (the year within the era), month, and day. The ISO year remains available via .year. Because Temporal delegates to the platform's calendar data, the mapping is correct even across mid-year era boundaries like Reiwa beginning on 1 May 2019, which a fixed offset would get wrong.

Why can't I compute the Japanese era year with a formula?

Because eras reset to year 1 on dates set by imperial succession, not on any regular cycle or on January 1. Reiwa started on 1 May 2019, so the boundary falls mid-year and there is no offset from the Gregorian year that holds for every date. The correct mapping requires knowing each era's exact start date, which lives in the CLDR calendar data that Temporal's withCalendar('japanese') consults for you.

How do I build a date from a Japanese era and year?

Pass { era, eraYear, month, day, calendar: 'japanese' } to Temporal.PlainDate.from — for example { era: 'reiwa', eraYear: 6, month: 5, day: 1 }. This is the reverse of reading the era components and is what you need when input arrives as 'Reiwa 6, May 1'. Convert it to the ISO form for canonical storage and derive the era presentation on demand for display.