Skip to content

Date.ParseStrategy parses two-digit Japanese and ROC era years in the wrong century #2061

Description

@vistar941

Describe the bug

Date.ParseStrategy, Date.VerbatimFormatStyle.parseStrategy, and Date.FormatStyle.parse(_:) can parse two-digit year-of-era values in Japanese and ROC calendars as dates 100 years too far in the future.

For example, 平成10年 is parsed as 2098 instead of 1998, and 民國01年 is parsed as 2012 instead of 1912.

To Reproduce

import Foundation

let timeZone = TimeZone(secondsFromGMT: 0)!
var gregorian = Calendar(identifier: .gregorian)
gregorian.timeZone = timeZone

func ymd(_ date: Date) -> String {
    let c = gregorian.dateComponents([.year, .month, .day], from: date)
    return String(format: "%04d-%02d-%02d", c.year!, c.month!, c.day!)
}

let japaneseLocale = Locale(identifier: "ja_JP@calendar=japanese")
var japaneseCalendar = Calendar(identifier: .japanese)
japaneseCalendar.locale = japaneseLocale
japaneseCalendar.timeZone = timeZone

let japaneseFormat: Date.FormatString =
    "\(era: .wide)\(year: .defaultDigits)\(month: .defaultDigits)\(day: .defaultDigits)"
let japaneseStrategy = Date.ParseStrategy(
    format: japaneseFormat,
    locale: japaneseLocale,
    timeZone: timeZone,
    calendar: japaneseCalendar,
    isLenient: false
)

print("平成09:", ymd(try japaneseStrategy.parse("平成09年1月1日")))
print("平成10:", ymd(try japaneseStrategy.parse("平成10年1月1日")))
print("令和01:", ymd(try japaneseStrategy.parse("令和01年5月1日")))

let rocLocale = Locale(identifier: "zh_TW@calendar=roc")
var rocCalendar = Calendar(identifier: .republicOfChina)
rocCalendar.locale = rocLocale
rocCalendar.timeZone = timeZone

let rocFormat: Date.FormatString =
    "\(era: .wide)\(year: .defaultDigits)\(month: .defaultDigits)\(day: .defaultDigits)"
let rocStrategy = Date.ParseStrategy(
    format: rocFormat,
    locale: rocLocale,
    timeZone: timeZone,
    calendar: rocCalendar,
    isLenient: false
)

print("民國01:", ymd(try rocStrategy.parse("民國01年1月1日")))
print("民國58:", ymd(try rocStrategy.parse("民國58年1月1日")))
print("民國59:", ymd(try rocStrategy.parse("民國59年1月1日")))

Actual output:

平成09: 2097-01-01
平成10: 2098-01-01
令和01: 2119-05-01
民國01: 2012-01-01
民國58: 2069-01-01
民國59: 1970-01-01

Expected behavior

The parsed year should remain scoped to the parsed era:

平成09: 1997-01-01
平成10: 1998-01-01
令和01: 2019-05-01
民國01: 1912-01-01
民國58: 1969-01-01
民國59: 1970-01-01

Configuration (please complete the following information):

  • Swift Version: Apple Swift version 6.3 (swiftlang-6.3.0.123.5)
  • OS: macOS
  • OS Version: 26.4.1

Regression information:

Not confirmed.

Additional context

Also reproduced against swift-foundation release/6.4.x.

The affected APIs all use ICUDateFormatter. ICUDateFormatter currently calls udat_set2DigitYearStart unconditionally, using the parse strategy's twoDigitStartDate default of 1970-01-01.

ICU applies the two-digit year window when a y pattern with width less than 3 parses exactly two digits. That is appropriate for two-digit Gregorian years, but it also affects explicit year-of-era values such as 平成10 and 民國01.

JapaneseCalendar normally disables default-century pivoting in ICU, but the unconditional udat_set2DigitYearStart call re-enables it for this formatter.

One possible fix is to avoid applying ICU's two-digit year window to explicit era-year parsing, for example by using a parse-only wider year field for Japanese and ROC patterns that contain an era field.

This behavior was also independently observed and discussed on X here: URL

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions