MenoData / Time4J

Advanced date, time and interval library for Java with sun/moon-astronomy and calendars like Chinese, Coptic, Ethiopian, French Republican, Hebrew, Hijri, Historic Christian, Indian National, Japanese, Julian, Korean, Minguo, Persian, Thai, Vietnamese
GNU Lesser General Public License v2.1
438 stars 64 forks source link

Can ChronoFormatter display Chinese Zodiac(鼠牛虎龙...and so on)? #982

Closed G-Fantastic closed 7 months ago

G-Fantastic commented 9 months ago
ChronoFormatter<ChineseCalendar> formatter =
                ChronoFormatter.setUp(ChineseCalendar.axis(), Locale.CHINESE)
                        .addPattern("r(U) MMMM dd. (EEE) ", PatternType.CLDR_DATE)
                        .addText(ChineseCalendar.SOLAR_TERM)    
                        .build();
ChineseCalendar chineseCalendar = ChineseCalendar.nowInSystemTime();
System.out.println(formatter.format(chineseCalendar));  

and could display Month alias like: 一月(alias: 正月),十一月(alias: 冬月),十二月(alias: 腊月).
and display Day alias like: 1(alias: 初一),2(alias: 初二),3(alias: 初三),4(alias: 初四),5(alias: 初五)... 21(alias: 廿一)... and so on.
example: 2023-11-05 10:00:05,expect format: 2023(癸卯)年冬月初五(兔) 10:00:05 and parse it to ChineseCalendar: 2023(癸卯)年冬月初五(兔) 10:00:05 to 2023-11-05 10:00:05

MenoData commented 9 months ago

Sorry for late reply but it is also not so easy for me to analyze text partly written in Chinese ;-)

Let's first say three things about the formatter you define above:

a) If you apply a CLDR pattern then you indirectly use text resources defined / overtaken by CLDR group. The actual chinese resources (for the Chinese calendar, not for the gregorian one) are like this for the (lunar) month (without leap symbol) and are actually the same for the output style using the pattern letter L:

M(w)_1=正月 M(w)_2=二月 M(w)_3=三月 M(w)_4=四月 M(w)_5=五月 M(w)_6=六月 M(w)_7=七月 M(w)_8=八月 M(w)_9=九月 M(w)_10=十月 M(w)_11=十一月 M(w)_12=腊月

For example: Given your formatter definition above, the gregorian date 2017-07-23 will produce the output

2017(丁酉) 閏六月 01. (周日) 大暑

The month part here (閏六月) first uses the leap indicator 閏 (for the leap month) and then the text resource from CLDR while the day part just uses common arab digits (0-9). And to complete the analysis for your formatter: It produces for the date 2023-11-05 the output (pay attention the transformation of month and day numbers - explanation later under c)):

2023(癸卯) 九月 22. (周日) 霜降

The last part is the solar term which has only meaning in Chinese calendar chronology. It is NOT the zodiac.

b) Looking at your desired text resources for the month, it seems to me that you really want the month numbers written in Taiwanese style (traditional instead of simplified Chinese using the locale zh-TW corresponding to the constant Locale.TRADITIONAL_CHINESE). Well, CLDR defines the resources for the month in this Taiwanese locale (for the Chinese calendar, not for the gregorian one) like:

M(w)_1=正月 M(w)_2=二月 M(w)_3=三月 M(w)_4=四月 M(w)_5=五月 M(w)_6=六月 M(w)_7=七月 M(w)_8=八月 M(w)_9=九月 M(w)_10=十月 M(w)_11=冬月 M(w)_12=臘月

c) You seem to be confused about the difference between language and chronology. The formatter you define above uses the Chinese language AND the old rural Chinese calendar. It is not gregorian. Therefore, you have different month and day numbers for any given year. Given the gregorian date 2023-11-05, you seem to expect the month 11 and the day 5, but in the Chinese calendar those numbers will be transformed to the 9th month and 22. day. Furthermore, the Chinese calendar chronology knows leap months while the gregorian calendar has no leap months.

Obviously you wish another format with customized text resources so defining a CLDR pattern using standardized text mappings is not necessarily the best way, especially for the day text (the formatter would simply use arab digits). Furthermore: You seem to need a formatter for gregorian dates in Chinese language including elements of Chinese calendar like cyclic year and zodiac (as partial property of cyclic year).

Fortunately there is a way to define custom text resources. Let's look at this formatter which will use the Taiwanese locale, a customized day element, the gregorian chronology and two embedded elements of the Chinese cyclic year and zodiac. It even uses a customized month element because the month text resources defined by CLDR are unfortunately different for the Chinese (as shown above) and the gregorian calendar chronology, given the same locale for Taiwan.

        Map<Month, String> monthMap = new EnumMap<>(Month.class); // using net.time4j.Month
        monthMap.put(Month.JANUARY, "正月");
        // complete the mappings as desired
        monthMap.put(Month.FEBRUARY, "");
        monthMap.put(Month.MARCH, "");
        monthMap.put(Month.APRIL, "");
        monthMap.put(Month.MAY, "");
        monthMap.put(Month.JUNE, "");
        monthMap.put(Month.JULY, "");
        monthMap.put(Month.AUGUST, "");
        monthMap.put(Month.SEPTEMBER, "");
        monthMap.put(Month.OCTOBER, "");
        monthMap.put(Month.NOVEMBER, "冬月");
        monthMap.put(Month.DECEMBER, "");        

        Map<Integer, String> dayMap = new HashMap<>();
        dayMap.put(1, "初一");
        dayMap.put(2, "初二");
        dayMap.put(3, "初三");
        dayMap.put(4, "初四");
        dayMap.put(5, "初五");
        // define more mappings until day 31

        ChronoPrinter<Integer> yearPrinter = 
                (Integer gregorianYear, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(ChineseCalendar.ofNewYear(gregorianYear).getYear().getDisplayName(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };
        ChronoPrinter<PlainDate> zodiacPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(gregorianDate.transform(ChineseCalendar.axis()).getYear().getZodiac(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };
        ChronoParser<Integer> yearParser =
                (CharSequence text, ParseLog status, AttributeQuery attributes) -> {
                    throw new UnsupportedOperationException("Parsing not used.");
        };
        ChronoParser<PlainDate> zodiacParser =
                (CharSequence text, ParseLog status, AttributeQuery attributes) -> {
                    throw new UnsupportedOperationException("Parsing not used.");
        };

        ChronoFormatter<PlainDate> gf = ChronoFormatter.setUp(PlainDate.axis(), Locale.TRADITIONAL_CHINESE)
                .addPattern("y(", PatternType.CLDR)
                .addCustomized(PlainDate.YEAR, yearPrinter, yearParser)
                .addLiteral(")年")
                .addText(PlainDate.MONTH_OF_YEAR, monthMap)
                .addText(PlainDate.DAY_OF_MONTH, dayMap)
                .addLiteral('(')
                .addCustomized(PlainDate.COMPONENT, zodiacPrinter, zodiacParser)
                .addLiteral(')')
                .build();

        String s = gf.print(PlainDate.of(2023, 11, 5));
        System.out.println(s);

Output (like your expectation): 2023(癸卯)年冬月初五(兔)

MenoData commented 9 months ago

A small correction...

The presented solution is not fully consistent because the expression ChineseCalendar.ofNewYear(gregorianYear) uses the related gregorian year which can be different from actual calendar year especially in January. You should probably better use following year printer:

        ChronoPrinter<PlainDate> yearPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(
                gregorianDate.transform(ChineseCalendar.axis()).getYear().getDisplayName(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };

and then adjust following two lines...

ChronoParser<PlainDate> yearParser = ...;

.addCustomized(PlainDate.COMPONENT, yearPrinter, yearParser)

MenoData commented 9 months ago

Just for completeness, look at the documentation of Chinese number system regarding day numbers which could be an elegant way to avoid lengthy day mappings: NumberSystem.CHINESE_MANDARIN

Unfortunately, I have not yet defined a number system for Taiwan...

Edit on 2024-02-09: The last sentence is obsolete. Instead, the already existing number system CHINESE_MANDARIN can also use the right characters for Taiwan.

MenoData commented 7 months ago

Using the next version 5.9.4 of Time4J which will soon be published you can do following to combine gregorian dates with Chinese numbers normally only used in the old Chinese rural calendar (something I do not recommend):

        Map<Month, String> monthMap = new EnumMap<>(Month.class); // using gregorian net.time4j.Month
        CalendarText ct = CalendarText.getInstance(ChineseCalendar.axis(), Locale.TAIWAN);
        List<String> monthNames = ct.getStdMonths(TextWidth.WIDE, OutputContext.STANDALONE).getTextForms();
        for (int m = 1; m <= 12; m++) {
            monthMap.put(Month.valueOf(m), monthNames.get(m - 1));
        }

        Map<Integer, String> dayMap = new HashMap<>();
        for (int d = 1; d <= 31; d++) {
            dayMap.put(d, NumberSystem.CHINESE_LUNAR_DAYS.toNumeral(d) + "日");
        }

        ChronoPrinter<PlainDate> yearPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(
                gregorianDate.transform(ChineseCalendar.axis()).getYear().getDisplayName(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };

        ChronoPrinter<PlainDate> zodiacPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(gregorianDate.transform(ChineseCalendar.axis()).getYear().getZodiac(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };

        ChronoFormatter<PlainDate> gf = ChronoFormatter.setUp(PlainDate.axis(), Locale.TRADITIONAL_CHINESE)
                .addPattern("y(", PatternType.CLDR)
                .addCustomized(PlainDate.COMPONENT, yearPrinter, ChronoParser.unsupported())
                .addLiteral(")年")
                .addText(PlainDate.MONTH_OF_YEAR, monthMap)
                .addText(PlainDate.DAY_OF_MONTH, dayMap)
                .addLiteral('(')
                .addCustomized(PlainDate.COMPONENT, zodiacPrinter, ChronoParser.unsupported())
                .addLiteral(')')
                .build();

        String s = gf.print(PlainDate.of(2023, 11, 5));
        System.out.println(s); // 2023(癸卯)年冬月初五日(兔)

That is obviously what you wished at the start of your question - a gregorian date with two extra informations representing the Chinese year and zodiac in brackets.

But keep in mind that Chinese people usually use gregorian dates with arabic digits or at least simplified Chinese numbers and not with numerals only used in lunisolar calendar. For example: The sign 冬月 rather denotes the 11th lunisolar month (as shown in Google translator or on Wikipedia about months in the old Chinese calendar) and not "November" in gregorian dates.

Anyway, it is your freedom to combine elements from different calendars.

And here is my recommendation for using the standard numerals for gregorian dates in Chinese language:

        ChronoPrinter<PlainDate> yearPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(
                gregorianDate.transform(ChineseCalendar.axis()).getYear().getDisplayName(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };

        ChronoPrinter<PlainDate> zodiacPrinter = 
                (PlainDate gregorianDate, StringBuilder buffer, AttributeQuery attributes) -> {
            buffer.append(gregorianDate.transform(ChineseCalendar.axis()).getYear().getZodiac(Locale.TRADITIONAL_CHINESE));
            return Collections.emptySet();
        };

        ChronoFormatter<PlainDate> gf = ChronoFormatter.setUp(PlainDate.axis(), Locale.TRADITIONAL_CHINESE)
                .addPattern("y(", PatternType.CLDR)
                .addCustomized(PlainDate.COMPONENT, yearPrinter, ChronoParser.unsupported())
                //.startSection(Attributes.NUMBER_SYSTEM, NumberSystem.CHINESE_MANDARIN)
                .addPattern(")年M月d日(", PatternType.CLDR)
                //.endSection()
                .addCustomized(PlainDate.COMPONENT, zodiacPrinter, ChronoParser.unsupported())
                .addLiteral(')')
                .build();

        String s = gf.print(PlainDate.of(2023, 11, 5));
        System.out.println(s); // 2023(癸卯)年11月5日(兔)

If you activate the two commented lines for the sectional attribute choosing the number system MANDARIN then you would get the output:

2023(癸卯)年十一月五日(兔)

And if you finally also encapsulate the first addPattern-line representing the gregorian year in the formatter definition with the number system attribute CHINESE_DECIMAL then you would obtain the output:

二零二三(癸卯)年十一月五日(兔)