diff --git a/icu4c/source/i18n/smpdtfmt.cpp b/icu4c/source/i18n/smpdtfmt.cpp index b691944689c..414dfffab8f 100644 --- a/icu4c/source/i18n/smpdtfmt.cpp +++ b/icu4c/source/i18n/smpdtfmt.cpp @@ -856,6 +856,17 @@ SimpleDateFormat::initialize(const Locale& locale, { if (U_FAILURE(status)) return; + parsePattern(); // Need this before initNumberFormatters(), to set fHasHanYearChar + + // Simple-minded hack to force Gannen year numbering for ja@calendar=japanese + // if format is non-numeric (includes 年) and fDateOverride is not already specified. + // This does not update if applyPattern subsequently changes the pattern type. + if (fDateOverride.isBogus() && fHasHanYearChar && + fCalendar != nullptr && uprv_strcmp(fCalendar->getType(),"japanese") == 0 && + uprv_strcmp(fLocale.getLanguage(),"ja") == 0) { + fDateOverride.setTo(u"y=jpanyear", -1); + } + // We don't need to check that the row count is >= 1, since all 2d arrays have at // least one row fNumberFormat = NumberFormat::createInstance(locale, status); @@ -872,8 +883,6 @@ SimpleDateFormat::initialize(const Locale& locale, { status = U_MISSING_RESOURCE_ERROR; } - - parsePattern(); } /* Initialize the fields we use to disambiguate ambiguous years. Separate @@ -4216,6 +4225,9 @@ void SimpleDateFormat::parsePattern() { if (ch == QUOTE) { inQuote = !inQuote; } + if (ch == 0x5E74) { // don't care whether this is inside quotes + fHasHanYearChar = TRUE; + } if (!inQuote) { if (ch == 0x6D) { // 0x6D == 'm' fHasMinute = TRUE; diff --git a/icu4c/source/i18n/unicode/smpdtfmt.h b/icu4c/source/i18n/unicode/smpdtfmt.h index f1e7c96ee44..a015c5be5c8 100644 --- a/icu4c/source/i18n/unicode/smpdtfmt.h +++ b/icu4c/source/i18n/unicode/smpdtfmt.h @@ -1599,6 +1599,7 @@ private: UBool fHasMinute; UBool fHasSecond; + UBool fHasHanYearChar; // pattern contains the Han year character \u5E74 /** * Sets fHasMinutes and fHasSeconds. diff --git a/icu4c/source/test/cintltst/cdattst.c b/icu4c/source/test/cintltst/cdattst.c index 72dbb94a874..d5e3ba2ccd9 100644 --- a/icu4c/source/test/cintltst/cdattst.c +++ b/icu4c/source/test/cintltst/cdattst.c @@ -42,6 +42,7 @@ static void TestContext(void); static void TestCalendarDateParse(void); static void TestParseErrorReturnValue(void); static void TestFormatForFields(void); +static void TestForceGannenNumbering(void); void addDateForTest(TestNode** root); @@ -61,6 +62,7 @@ void addDateForTest(TestNode** root) TESTCASE(TestOverrideNumberFormat); TESTCASE(TestParseErrorReturnValue); TESTCASE(TestFormatForFields); + TESTCASE(TestForceGannenNumbering); } /* Testing the DateFormat API */ static void TestDateFormat() @@ -1867,4 +1869,40 @@ static void TestFormatForFields(void) { } } +static void TestForceGannenNumbering(void) { + UErrorCode status; + const char* locID = "ja_JP@calendar=japanese"; + UDate refDate = 600336000000.0; // 1989 Jan 9 Monday = Heisei 1 + const UChar* testSkeleton = u"yMMMd"; + + // Test Gannen year forcing + status = U_ZERO_ERROR; + UDateTimePatternGenerator* dtpgen = udatpg_open(locID, &status); + if (U_FAILURE(status)) { + log_data_err("Fail in udatpg_open locale %s: %s", locID, u_errorName(status)); + } else { + UChar pattern[kUbufMax]; + int32_t patlen = udatpg_getBestPattern(dtpgen, testSkeleton, -1, pattern, kUbufMax, &status); + if (U_FAILURE(status)) { + log_data_err("Fail in udatpg_getBestPattern locale %s: %s", locID, u_errorName(status)); + } else { + UDateFormat *testFmt = udat_open(UDAT_PATTERN, UDAT_PATTERN, locID, NULL, 0, pattern, patlen, &status); + if (U_FAILURE(status)) { + log_data_err("Fail in udat_open locale %s: %s", locID, u_errorName(status)); + } else { + UChar testString[kUbufMax]; + int32_t testStrLen = udat_format(testFmt, refDate, testString, kUbufMax, NULL, &status); + if (U_FAILURE(status)) { + log_err("Fail in udat_format locale %s: %s", locID, u_errorName(status)); + } else if (testStrLen < 3 || testString[2] != 0x5143) { + char bbuf[kBbufMax]; + u_austrncpy(bbuf, testString, testStrLen); + log_err("Formatting year 1 as Gannen, got%s but expected 3rd char to be 0x5143", bbuf); + } + udat_close(testFmt); + } + } + udatpg_close(dtpgen); + } +} #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/dtifmtts.cpp b/icu4c/source/test/intltest/dtifmtts.cpp index a2e7019d3f4..4b38a3dc60e 100644 --- a/icu4c/source/test/intltest/dtifmtts.cpp +++ b/icu4c/source/test/intltest/dtifmtts.cpp @@ -1062,9 +1062,9 @@ void DateIntervalFormatTest::testFormat() { "ja-u-ca-japanese", "H 31 03 15 09:00:00", "H 31 04 15 09:00:00", "GGGGGyMd", "H31/03/15\\uFF5E31/04/15", - "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GyMMMd", "\\u662D\\u548C64\\u5E741\\u67085\\u65E5\\uFF5E\\u5E73\\u62101\\u5E741\\u670815\\u65E5", + "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GyMMMd", "\\u662D\\u548C64\\u5E741\\u67085\\u65E5\\uFF5E\\u5E73\\u6210\\u5143\\u5E741\\u670815\\u65E5", - "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GGGGGyMd", "S64/1/5\\uFF5EH1/1/15", // The GGGGG/G forces inheritance from a different pattern, no padding + "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GGGGGyMd", "S64/1/5\\uFF5EH\\u5143/1/15", // The GGGGG/G forces inheritance from a different pattern, no padding "ja-u-ca-japanese", "H 31 04 15 09:00:00", JP_ERA_2019_NARROW " 1 05 15 09:00:00", "GGGGGyMd", "H31/4/15\\uFF5E" JP_ERA_2019_NARROW "1/5/15", diff --git a/icu4c/source/test/intltest/incaltst.cpp b/icu4c/source/test/intltest/incaltst.cpp index bf0624e04a6..3acb8af0b6f 100644 --- a/icu4c/source/test/intltest/incaltst.cpp +++ b/icu4c/source/test/intltest/incaltst.cpp @@ -12,6 +12,10 @@ #include "string.h" #include "unicode/locid.h" #include "japancal.h" +#include "unicode/localpointer.h" +#include "unicode/datefmt.h" +#include "unicode/smpdtfmt.h" +#include "unicode/dtptngen.h" #if !UCONFIG_NO_FORMATTING @@ -74,9 +78,10 @@ void IntlCalendarTest::runIndexedTest( int32_t index, UBool exec, const char* &n CASE(4,TestBuddhistFormat); CASE(5,TestJapaneseFormat); CASE(6,TestJapanese3860); - CASE(7,TestPersian); - CASE(8,TestPersianFormat); - CASE(9,TestTaiwan); + CASE(7,TestForceGannenNumbering); + CASE(8,TestPersian); + CASE(9,TestPersianFormat); + CASE(10,TestTaiwan); default: name = ""; break; } } @@ -636,20 +641,20 @@ void IntlCalendarTest::TestJapanese3860() // Test parse with missing era (should default to current era, heisei) // Test parse with incomplete information logln("Testing parse w/ missing era..."); - SimpleDateFormat *fmt = new SimpleDateFormat(UnicodeString("y.M.d"), Locale("ja_JP@calendar=japanese"), status); + SimpleDateFormat *fmt = new SimpleDateFormat(UnicodeString("y/M/d"), Locale("ja_JP@calendar=japanese"), status); CHECK(status, "creating date format instance"); if(!fmt) { errln("Couldn't create en_US instance"); } else { UErrorCode s2 = U_ZERO_ERROR; cal2->clear(); - UnicodeString samplestr("1.1.9"); + UnicodeString samplestr("1/5/9"); logln(UnicodeString() + "Test Year: " + samplestr); aDate = fmt->parse(samplestr, s2); ParsePosition pp=0; fmt->parse(samplestr, *cal2, pp); - CHECK(s2, "parsing the 1.1.9 string"); - logln("*cal2 after 119 parse:"); + CHECK(s2, "parsing the 1/5/9 string"); + logln("*cal2 after 159 parse:"); str.remove(); fmt2->format(aDate, str); logln(UnicodeString() + "as Gregorian Calendar: " + str); @@ -660,7 +665,7 @@ void IntlCalendarTest::TestJapanese3860() int32_t expectYear = 1; int32_t expectEra = JapaneseCalendar::getCurrentEra(); if((gotYear!=1) || (gotEra != expectEra)) { - errln(UnicodeString("parse "+samplestr+" of 'y.m.d' as Japanese Calendar, expected year ") + expectYear + + errln(UnicodeString("parse "+samplestr+" of 'y/M/d' as Japanese Calendar, expected year ") + expectYear + UnicodeString(" and era ") + expectEra +", but got year " + gotYear + " and era " + gotEra + " (Gregorian:" + str +")"); } else { logln(UnicodeString() + " year: " + gotYear + ", era: " + gotEra); @@ -714,8 +719,50 @@ void IntlCalendarTest::TestJapanese3860() delete fmt2; } +void IntlCalendarTest::TestForceGannenNumbering() +{ + UErrorCode status; + const char* locID = "ja_JP@calendar=japanese"; + Locale loc(locID); + UDate refDate = 600336000000.0; // 1989 Jan 9 Monday = Heisei 1 + UnicodeString testSkeleton("yMMMd"); + // Test Gannen year forcing + status = U_ZERO_ERROR; + LocalPointer testFmt1(DateFormat::createInstanceForSkeleton(testSkeleton, loc, status)); + if (U_FAILURE(status)) { + dataerrln("Fail in DateFormat::createInstanceForSkeleton locale %s: %s", locID, u_errorName(status)); + } else { + UnicodeString testString1; + testString1 = testFmt1->format(refDate, testString1); + if (testString1.length() < 3 || testString1.charAt(2) != 0x5143) { + errln(UnicodeString("Formatting year 1 as Gannen, got " + testString1 + " but expected 3rd char to be 0x5143")); + } + } + // Test disabling of Gannen year forcing + status = U_ZERO_ERROR; + LocalPointer dtpgen(DateTimePatternGenerator::createInstance(loc, status)); + if (U_FAILURE(status)) { + dataerrln("Fail in DateTimePatternGenerator::createInstance locale %s: %s", locID, u_errorName(status)); + } else { + UnicodeString pattern = dtpgen->getBestPattern(testSkeleton, status); + if (U_FAILURE(status)) { + dataerrln("Fail in DateTimePatternGenerator::getBestPattern locale %s: %s", locID, u_errorName(status)); + } else { + LocalPointer testFmt2(new SimpleDateFormat(pattern, UnicodeString(""), loc, status)); + if (U_FAILURE(status)) { + dataerrln("Fail in new SimpleDateFormat locale %s: %s", locID, u_errorName(status)); + } else { + UnicodeString testString2; + testString2 = testFmt2->format(refDate, testString2); + if (testString2.length() < 3 || testString2.charAt(2) != 0x0031) { + errln(UnicodeString("Formatting year 1 with Gannen disabled, got " + testString2 + " but expected 3rd char to be 1")); + } + } + } + } +} /** * Verify the Persian Calendar. diff --git a/icu4c/source/test/intltest/incaltst.h b/icu4c/source/test/intltest/incaltst.h index 628b6e4cd40..2d42bcc817f 100644 --- a/icu4c/source/test/intltest/incaltst.h +++ b/icu4c/source/test/intltest/incaltst.h @@ -34,6 +34,7 @@ public: void TestJapanese(void); void TestJapaneseFormat(void); void TestJapanese3860(void); + void TestForceGannenNumbering(void); void TestPersian(void); void TestPersianFormat(void); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java index c14c2388119..737a127744a 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java @@ -939,6 +939,11 @@ public class SimpleDateFormat extends DateFormat { */ private transient boolean hasSecond; + /** + * DateFormat pattern contains the Han year character \u5E74=年, => non-numeric E Asian format. + */ + private transient boolean hasHanYearChar; + /* * Capitalization setting, introduced in ICU 50 * Special serialization, see writeObject & readObject below @@ -1136,11 +1141,20 @@ public class SimpleDateFormat extends DateFormat { setLocale(calendar.getLocale(ULocale.VALID_LOCALE ), calendar.getLocale(ULocale.ACTUAL_LOCALE)); initLocalZeroPaddingNumberFormat(); + parsePattern(); // Need this before initNumberFormatters(), to set hasHanYearChar + + // Simple-minded hack to force Gannen year numbering for ja@calendar=japanese + // if format is non-numeric (includes 年) and overrides are not already specified. + // This does not update if applyPattern subsequently changes the pattern type. + if (override == null && hasHanYearChar && + calendar != null && calendar.getType().equals("japanese") && + locale != null && locale.getLanguage().equals("ja")) { + override = "y=jpanyear"; + } + if (override != null) { initNumberFormatters(locale); } - - parsePattern(); } /** @@ -4555,6 +4569,7 @@ public class SimpleDateFormat extends DateFormat { private void parsePattern() { hasMinute = false; hasSecond = false; + hasHanYearChar = false; boolean inQuote = false; for (int i = 0; i < pattern.length(); ++i) { @@ -4562,6 +4577,9 @@ public class SimpleDateFormat extends DateFormat { if (ch == '\'') { inQuote = !inQuote; } + if (ch == '\u5E74') { // don't care whether this is inside quotes + hasHanYearChar = true; + } if (!inQuote) { if (ch == 'm') { hasMinute = true; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/JapaneseTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/JapaneseTest.java index 590a84751a8..7b9739abefb 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/JapaneseTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/JapaneseTest.java @@ -19,6 +19,7 @@ import org.junit.runners.JUnit4; import com.ibm.icu.impl.LocaleUtility; import com.ibm.icu.text.DateFormat; +import com.ibm.icu.text.DateTimePatternGenerator; import com.ibm.icu.text.SimpleDateFormat; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.JapaneseCalendar; @@ -150,13 +151,17 @@ public class JapaneseTest extends CalendarTestFmwk { @Test public void Test3860() { + final String jCalShortPattern = "y/M/d"; // Note: just 'y' doesn't work here. + final String jCalGannenDate = "1/5/9"; // A date in the above format after the accession date for Heisei era (Heisei year 1 Jan 8) + // or the new era in Gregorian 2019 (new era year 1 May 1). If before the accession date, + // the year will be in the previous era. ULocale loc = new ULocale("ja_JP@calendar=japanese"); Calendar cal = new JapaneseCalendar(loc); DateFormat enjformat = cal.getDateTimeFormat(DateFormat.FULL,DateFormat.FULL,new ULocale("en_JP@calendar=japanese")); - DateFormat format = cal.getDateTimeFormat(DateFormat.SHORT,DateFormat.SHORT,loc); // SHORT => no jpanyear since we apply "y.M.d" anyway - ((SimpleDateFormat)format).applyPattern("y.M.d"); // Note: just 'y' doesn't work here. + DateFormat format = cal.getDateTimeFormat(DateFormat.SHORT,DateFormat.SHORT,loc); // SHORT => no jpanyear since we will apply a short pattern + ((SimpleDateFormat)format).applyPattern(jCalShortPattern); ParsePosition pos = new ParsePosition(0); - Date aDate = format.parse("1.5.9", pos); // after accession (Heisei or next). Before accession in the same year would format with the previous era. + Date aDate = format.parse(jCalGannenDate, pos); String inEn = enjformat.format(aDate); cal.clear(); @@ -207,7 +212,7 @@ public class JapaneseTest extends CalendarTestFmwk { // Tests for formats with gannen numbering Gy年 pos.setIndex(0); - aDate = format.parse("1.5.9", pos); // reset + aDate = format.parse(jCalGannenDate, pos); // reset DateFormat fmtWithGannen = DateFormat.getDateInstance(cal, DateFormat.MEDIUM, loc); String aString = fmtWithGannen.format(aDate); if (aString.charAt(2) != '\u5143') { // 元 @@ -227,6 +232,36 @@ public class JapaneseTest extends CalendarTestFmwk { } } + @Test + public void TestForceGannenNumbering() { + final String jCalShortPattern = "y/M/d"; // Note: just 'y' doesn't work here. + final String jCalGannenDate = "1/5/9"; // A date in the above format after the accession date for Heisei era (Heisei year 1 Jan 8) + // or the new era in Gregorian 2019 (new era year 1 May 1). If before the accession date, + // the year will be in the previous era. + ULocale loc = new ULocale("ja_JP@calendar=japanese"); + DateFormat refFmt = DateFormat.getDateInstance(DateFormat.SHORT, loc); + ((SimpleDateFormat)refFmt).applyPattern(jCalShortPattern); + ParsePosition pos = new ParsePosition(0); + Date refDate = refFmt.parse(jCalGannenDate, pos); + final String testSkeleton = "yMMMd"; + + // Test Gannen year forcing + DateFormat testFmt1 = DateFormat.getInstanceForSkeleton(testSkeleton, loc); + String testString1 = testFmt1.format(refDate); + if (testString1.length() < 3 || testString1.charAt(2) != '\u5143') { // 元 + errln("Formatting year 1 as Gannen, got " + testString1 + " but expected 3rd char to be \u5143"); + } + + // Test disabling of Gannen year forcing + DateTimePatternGenerator dtpgen = DateTimePatternGenerator.getInstance(loc); + String pattern = dtpgen.getBestPattern(testSkeleton); + SimpleDateFormat testFmt2 = new SimpleDateFormat(pattern, "", loc); // empty override string to disable Gannen year numbering + String testString2 = testFmt2.format(refDate); + if (testString2.length() < 3 || testString2.charAt(2) != '1') { + errln("Formatting year 1 with Gannen disabled, got " + testString2 + " but expected 3rd char to be 1"); + } + } + @Test public void Test5345parse() { // Test parse with incomplete information diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java index 7c7a8a45be3..8fd76a414c7 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java @@ -710,11 +710,11 @@ public class DateIntervalFormatTest extends TestFmwk { "ja-u-ca-japanese", "H 31 03 15 09:00:00", "H 31 04 15 09:00:00", "GGGGGyMd", "H31/03/15\uFF5E31/04/15", - "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GyMMMd", "\u662D\u548C64\u5E741\u67085\u65E5\uFF5E\u5E73\u62101\u5E741\u670815\u65E5", + "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GyMMMd", "\u662D\u548C64\u5E741\u67085\u65E5\uFF5E\u5E73\u6210\u5143\u5E741\u670815\u65E5", - "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GGGGGyMd", "S64\u5E741\u67085\u65E5\uFF5EH1\u5E741\u670815\u65E5", // The GGGGG/G forces inheritance from a different pattern, non-numeric-only + "ja-u-ca-japanese", "S 64 01 05 09:00:00", "H 1 01 15 09:00:00", "GGGGGyMd", "S64\u5E741\u67085\u65E5\uFF5EH\u5143\u5E741\u670815\u65E5", // The GGGGG/G forces inheritance from a different pattern, non-numeric-only - "ja-u-ca-japanese", "H 31 04 15 09:00:00", DateFormat.JP_ERA_2019_NARROW+" 1 05 15 09:00:00", "GGGGGyMd", "H31\u5E744\u670815\u65E5\uFF5E"+DateFormat.JP_ERA_2019_NARROW+"1\u5E745\u670815\u65E5", + "ja-u-ca-japanese", "H 31 04 15 09:00:00", DateFormat.JP_ERA_2019_NARROW+" 1 05 15 09:00:00", "GGGGGyMd", "H31\u5E744\u670815\u65E5\uFF5E"+DateFormat.JP_ERA_2019_NARROW+"\u5143\u5E745\u670815\u65E5", }; expect(DATA, DATA.length);