blob: c15ac79617af9b80e638318592a563072d891d70 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.tools.cldr;
import com.google.gwt.codegen.server.StringGenerator;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.i18n.client.DateTimeFormatInfo;
import com.google.gwt.i18n.client.impl.cldr.DateTimeFormatInfoImpl;
import com.google.gwt.i18n.rebind.DateTimePatternGenerator;
import com.google.gwt.i18n.rebind.MessageFormatParser;
import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.DefaultTemplateChunkVisitor;
import com.google.gwt.i18n.rebind.MessageFormatParser.StringChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk;
import com.google.gwt.i18n.shared.GwtLocale;
import org.unicode.cldr.util.CLDRFile.Factory;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Loads data needed to produce DateTimeFormatInfo implementations.
*/
public class DateTimeFormatInfoProcessor extends Processor {
private static final String[] DAYS = new String[] {
"sun", "mon", "tue", "wed", "thu", "fri", "sat"};
/**
* Map of skeleton format patterns and the method name suffix that uses them.
*/
private static final Map<String, String> FORMATS;
/**
* Index of the formats, ordered by the method name.
*/
private static final SortedMap<String, String> FORMAT_BY_METHOD;
static {
FORMATS = new HashMap<String, String>();
FORMATS.put("d", "Day");
FORMATS.put("hmm", "Hour12Minute");
FORMATS.put("hmmss", "Hour12MinuteSecond");
FORMATS.put("Hmm", "Hour24Minute");
FORMATS.put("Hmmss", "Hour24MinuteSecond");
FORMATS.put("mss", "MinuteSecond");
FORMATS.put("MMM", "MonthAbbrev");
FORMATS.put("MMMd", "MonthAbbrevDay");
FORMATS.put("MMMM", "MonthFull");
FORMATS.put("MMMMd", "MonthFullDay");
FORMATS.put("MMMMEEEEd", "MonthFullWeekdayDay");
FORMATS.put("Md", "MonthNumDay");
FORMATS.put("y", "Year");
FORMATS.put("yMMM", "YearMonthAbbrev");
FORMATS.put("yMMMd", "YearMonthAbbrevDay");
FORMATS.put("yMMMM", "YearMonthFull");
FORMATS.put("yMMMMd", "YearMonthFullDay");
FORMATS.put("yM", "YearMonthNum");
FORMATS.put("yMd", "YearMonthNumDay");
FORMATS.put("yMMMEEEd", "YearMonthWeekdayDay");
FORMATS.put("yQQQQ", "YearQuarterFull");
FORMATS.put("yQ", "YearQuarterShort");
FORMAT_BY_METHOD = new TreeMap<String, String>();
for (Map.Entry<String, String> entry : FORMATS.entrySet()) {
FORMAT_BY_METHOD.put(entry.getValue(), entry.getKey());
}
}
/**
* Convert the unlocalized name of a day ("sun".."sat") into a day number of
* the week, ie 0-6.
*
* @param day abbreviated, unlocalized name of the day ("sun".."sat")
* @return the day number, 0-6
* @throws IllegalArgumentException if the day name is not found
*/
private static int getDayNumber(String day) {
for (int i = 0; i < DAYS.length; ++i) {
if (DAYS[i].equals(day)) {
return i;
}
}
throw new IllegalArgumentException();
}
private final RegionLanguageData regionLanguageData;
public DateTimeFormatInfoProcessor(File outputDir, Factory cldrFactory, LocaleData localeData) {
super(outputDir, cldrFactory, localeData);
regionLanguageData = new RegionLanguageData(cldrFactory);
}
@Override
protected void cleanupData() {
System.out.println("Removing duplicates from date/time formats");
localeData.copyLocaleData("en", "default", "era-wide", "era-abbrev", "quarter-wide",
"quarter-abbrev", "day-wide", "day-sa-wide", "day-narrow", "day-sa-narrow", "day-abbrev",
"day-sa-abbrev", "month-wide", "month-sa-wide", "month-narrow", "month-sa-narrow",
"month-abbrev", "month-sa-abbrev");
removeUnusedFormats();
localeData.removeDuplicates("predef");
localeData.removeDuplicates("weekdata");
localeData.removeDuplicates("date");
localeData.removeDuplicates("time");
localeData.removeDuplicates("dateTime");
localeData.removeCompleteDuplicates("dayPeriod-abbrev");
computePeriodRedirects("day");
computePeriodRedirects("month");
computePeriodRedirects("day");
removePeriodDuplicates("day");
removePeriodDuplicates("month");
removePeriodDuplicates("quarter");
removePeriodDuplicates("era");
}
/**
* Generate an override for a method which takes String arguments, which
* simply redirect to another method based on a default value.
*
* @param pw
* @param category
* @param locale
* @param method
* @param args
*/
protected void generateArgMethod(PrintWriter pw, String category, GwtLocale locale,
String method, String... args) {
String value = localeData.getEntry(category, locale, "default");
if (value != null && value.length() > 0) {
pw.println();
if (getOverrides()) {
pw.println(" @Override");
}
pw.print(" public String " + method + "(");
String prefix = "";
for (String arg : args) {
pw.print(prefix + "String " + arg);
prefix = ", ";
}
pw.println(") {");
pw.print(" return " + method + Character.toTitleCase(value.charAt(0)) + value.substring(1)
+ "(");
prefix = "";
for (String arg : args) {
pw.print(prefix + arg);
prefix = ", ";
}
pw.println(");");
pw.println(" }");
}
}
/**
* Generate an override for a method which takes String arguments.
*
* @param pw
* @param category
* @param locale
* @param key
* @param method
* @param args
*/
protected void generateArgMethodRedirect(PrintWriter pw, String category, GwtLocale locale,
String key, String method, final String... args) {
String value = localeData.getEntry(category, locale, key);
if (value != null) {
pw.println();
if (getOverrides()) {
pw.println(" @Override");
}
pw.print(" public String " + method + "(");
String prefix = "";
for (String arg : args) {
pw.print(prefix + "String " + arg);
prefix = ", ";
}
pw.println(") {");
final StringBuilder buf = new StringBuilder();
final StringGenerator gen = StringGenerator.create(buf, false);
try {
List<TemplateChunk> chunks = MessageFormatParser.parse(value);
for (TemplateChunk chunk : chunks) {
chunk.accept(new DefaultTemplateChunkVisitor() {
@Override
public void visit(ArgumentChunk argChunk) throws UnableToCompleteException {
gen.appendStringValuedExpression(args[argChunk.getArgumentNumber()]);
}
@Override
public void visit(StringChunk stringChunk) throws UnableToCompleteException {
gen.appendStringLiteral(stringChunk.getString());
}
});
}
} catch (ParseException e) {
throw new RuntimeException("Unable to parse pattern '" + value + "' for locale " + locale
+ " key " + category + "/" + key, e);
} catch (UnableToCompleteException e) {
throw new RuntimeException("Unable to parse pattern '" + value + "' for locale " + locale
+ " key " + category + "/" + key, e);
}
gen.completeString();
pw.println(" return " + buf.toString() + ";");
pw.println(" }");
}
}
/**
* Generate a method which returns a day number as an integer.
*
* @param pw
* @param locale
* @param key
* @param method
*/
protected void generateDayNumber(PrintWriter pw, GwtLocale locale, String key, String method) {
String day = localeData.getEntry("weekdata", locale, key);
if (day != null) {
int value = getDayNumber(day);
pw.println();
if (getOverrides()) {
pw.println(" @Override");
}
pw.println(" public int " + method + "() {");
pw.println(" return " + value + ";");
pw.println(" }");
}
}
/**
* Generate a method which returns a format string for a given predefined
* skeleton pattern.
*
* @param locale
* @param pw
* @param skeleton
* @param methodSuffix
*/
protected void generateFormat(GwtLocale locale, PrintWriter pw, String skeleton,
String methodSuffix) {
String pattern = localeData.getEntry("predef", locale, skeleton);
generateStringValue(pw, "format" + methodSuffix, pattern);
}
/**
* Generate a series of methods which returns names in wide, narrow, and
* abbreviated lengths plus their standalone versions.
*
* @param pw
* @param group
* @param locale
* @param methodPrefix
* @param keys
*/
protected void generateFullStringList(PrintWriter pw, String group, GwtLocale locale,
String methodPrefix, String... keys) {
generateStringListPair(pw, group, locale, methodPrefix, "Full", "wide", keys);
generateStringListPair(pw, group, locale, methodPrefix, "Narrow", "narrow", keys);
generateStringListPair(pw, group, locale, methodPrefix, "Short", "abbrev", keys);
}
/**
* Generate an override of a standalone names list that simply redirects to
* the non-standalone version.
*
* @param pw
* @param methodPrefix
*/
protected void generateStandaloneRedirect(PrintWriter pw, String methodPrefix) {
pw.println();
if (getOverrides()) {
pw.println(" @Override");
}
pw.println(" public String[] " + methodPrefix + "Standalone" + "() {");
pw.println(" return " + methodPrefix + "();");
pw.println(" }");
}
/**
* Generate a method which returns a list of strings.
*
* @param pw
* @param category
* @param fallbackCategory
* @param locale
* @param method
* @param keys
* @return true if the method was skipped as identical to its ancestor
*/
protected boolean generateStringList(PrintWriter pw, String category, String fallbackCategory,
GwtLocale locale, String method, String... keys) {
Map<String, String> map = localeData.getEntries(category, locale);
Map<String, String> fallback =
fallbackCategory == null ? Collections.<String, String> emptyMap() : localeData.getEntries(
fallbackCategory, locale);
if (map == null || map.isEmpty() && fallback != null && !fallback.isEmpty()) {
return true;
}
if (map != null && !map.isEmpty()) {
if (fallbackCategory != null) {
// see if the entry is the same as the fallback
boolean different = false;
for (String key : keys) {
String value = map.get(key);
if (value != null && !value.equals(fallback.get(key))) {
different = true;
break;
}
}
if (!different) {
return true;
}
}
pw.println();
if (getOverrides()) {
pw.println(" @Override");
}
pw.println(" public String[] " + method + "() {");
pw.print(" return new String[] {");
boolean first = true;
for (String key : keys) {
String value = map.get(key);
if (value == null) {
value = fallback.get(key);
}
if (value == null) {
System.err.println("Missing \"" + key + "\" in " + locale + "/" + category);
value = "";
}
if (first) {
first = false;
} else {
pw.print(",");
}
pw.print("\n \"" + value.replace("\"", "\\\"") + "\"");
}
pw.println("\n };");
pw.println(" }");
}
return false;
}
protected void generateStringListPair(PrintWriter pw, String group, GwtLocale locale,
String methodPrefix, String width, String categorySuffix, String... keys) {
generateStringList(pw, group + "-" + categorySuffix, null, locale, methodPrefix + width, keys);
String redirect =
localeData.getEntry(group + "-sa-" + categorySuffix + "-redirect", locale, "redirect");
if ("yes".equals(redirect)) {
generateStandaloneRedirect(pw, methodPrefix + width);
} else {
generateStringList(pw, group + "-sa-" + categorySuffix, group + "-" + categorySuffix, locale,
methodPrefix + width + "Standalone", keys);
}
}
@Override
protected void loadData() throws IOException {
System.out.println("Loading data for date/time formats");
localeData.addEntries("predef", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/"
+ "availableFormats", "dateFormatItem", "id");
localeData.addNameEntries("month", cldrFactory);
localeData.addNameEntries("day", cldrFactory);
localeData.addNameEntries("quarter", cldrFactory);
// only add the entries we will use to avoid overriding a parent for
// differences that don't matter.
localeData.addEntries("dayPeriod-abbrev", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/"
+ "dayPeriodContext[@type=\"format\"]/"
+ "dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod[@type=\"am\"]", "dayPeriod", "type");
localeData.addEntries("dayPeriod-abbrev", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/"
+ "dayPeriodContext[@type=\"format\"]/"
+ "dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod[@type=\"pm\"]", "dayPeriod", "type");
localeData.addEntries("era-abbrev", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr", "era", "type");
localeData.addEntries("era-wide", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames", "era", "type");
localeData.addDateTimeFormatEntries("date", cldrFactory);
localeData.addDateTimeFormatEntries("time", cldrFactory);
localeData.addDateTimeFormatEntries("dateTime", cldrFactory);
loadWeekData();
loadFormatPatterns();
}
@Override
protected void writeOutputFiles() throws IOException {
// TODO(jat): make uz_UZ inherit from uz_Cyrl rather than uz, for example
System.out.println("Writing output for date/time formats");
for (GwtLocale locale : localeData.getNonEmptyLocales()) {
String myClass;
String path = "client/";
String pathSuffix = "";
if (locale.isDefault()) {
myClass = "DefaultDateTimeFormatInfo";
} else {
myClass = "DateTimeFormatInfoImpl" + localeSuffix(locale);
pathSuffix = "impl/cldr/";
}
GwtLocale parent = localeData.inheritsFrom(locale);
PrintWriter pw = createOutputFile(path + pathSuffix + myClass + ".java");
printHeader(pw);
pw.print("package com.google.gwt.i18n.client");
if (!locale.isDefault()) {
pw.print(".impl.cldr");
setOverrides(true);
} else {
setOverrides(false);
}
pw.println(";");
pw.println();
pw.println("// DO NOT EDIT - GENERATED FROM CLDR AND ICU DATA");
pw.println();
if (locale.isDefault()) {
pw.println("/**");
pw.println(" * Default implementation of DateTimeFormatInfo interface, "
+ "using values from");
pw.println(" * the CLDR root locale.");
pw.println(" * <p>");
pw.println(" * Users who need to create their own DateTimeFormatInfo "
+ "implementation are");
pw.println(" * encouraged to extend this class so their implementation "
+ "won't break when ");
pw.println(" * new methods are added.");
pw.println(" */");
} else {
pw.println("/**");
pw.println(" * Implementation of DateTimeFormatInfo for the \"" + locale + "\" locale.");
pw.println(" */");
}
pw.print("public class " + myClass);
if (locale.isDefault()) {
pw.print(" implements " + DateTimeFormatInfo.class.getSimpleName());
} else {
pw.print(" extends ");
pw.print(DateTimeFormatInfoImpl.class.getSimpleName());
if (!parent.isDefault()) {
pw.print('_');
pw.print(parent.getAsString());
}
}
pw.println(" {");
// write AM/PM names
generateStringList(pw, "dayPeriod-abbrev", null, locale, "ampms", "am", "pm");
// write standard date formats
generateArgMethod(pw, "date", locale, "dateFormat");
generateStringMethod(pw, "date", locale, "full", "dateFormatFull");
generateStringMethod(pw, "date", locale, "long", "dateFormatLong");
generateStringMethod(pw, "date", locale, "medium", "dateFormatMedium");
generateStringMethod(pw, "date", locale, "short", "dateFormatShort");
// write methods for assembling date/time formats
generateArgMethod(pw, "dateTime", locale, "dateTime", "timePattern", "datePattern");
generateArgMethodRedirect(pw, "dateTime", locale, "full", "dateTimeFull", "timePattern",
"datePattern");
generateArgMethodRedirect(pw, "dateTime", locale, "long", "dateTimeLong", "timePattern",
"datePattern");
generateArgMethodRedirect(pw, "dateTime", locale, "medium", "dateTimeMedium", "timePattern",
"datePattern");
generateArgMethodRedirect(pw, "dateTime", locale, "short", "dateTimeShort", "timePattern",
"datePattern");
// write era names
generateStringList(pw, "era-wide", null, locale, "erasFull", "0", "1");
generateStringList(pw, "era-abbrev", null, locale, "erasShort", "0", "1");
// write firstDayOfTheWeek
generateDayNumber(pw, locale, "firstDay", "firstDayOfTheWeek");
// write predefined date/time formats
for (Map.Entry<String, String> entry : FORMAT_BY_METHOD.entrySet()) {
generateFormat(locale, pw, entry.getValue(), entry.getKey());
}
// write month names
generateFullStringList(pw, "month", locale, "months", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "10", "11", "12");
// write quarter names
generateStringList(pw, "quarter-wide", null, locale, "quartersFull", "1", "2", "3", "4");
generateStringList(pw, "quarter-abbrev", null, locale, "quartersShort", "1", "2", "3", "4");
// write standard time formats
generateArgMethod(pw, "time", locale, "timeFormat");
generateStringMethod(pw, "time", locale, "full", "timeFormatFull");
generateStringMethod(pw, "time", locale, "long", "timeFormatLong");
generateStringMethod(pw, "time", locale, "medium", "timeFormatMedium");
generateStringMethod(pw, "time", locale, "short", "timeFormatShort");
// write weekday names
generateFullStringList(pw, "day", locale, "weekdays", DAYS);
// write weekend boundaries
generateDayNumber(pw, locale, "weekendEnd", "weekendEnd");
generateDayNumber(pw, locale, "weekendStart", "weekendStart");
pw.println("}");
pw.close();
}
}
/**
* @param period
*/
private void computePeriodRedirects(String period) {
localeData.computeRedirects(period + "-abbrev", period + "-sa-abbrev");
localeData.computeRedirects(period + "-narrow", period + "-sa-narrow");
localeData.computeRedirects(period + "-wide", period + "-sa-wide");
}
private void loadFormatPatterns() {
localeData.addEntries("predef", cldrFactory,
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/"
+ "availableFormats", "dateFormatItem", "id");
for (GwtLocale locale : localeData.getAllLocales()) {
DateTimePatternGenerator dtpg = new DateTimePatternGenerator(locale);
for (Map.Entry<String, String> entry : FORMATS.entrySet()) {
String skeleton = entry.getKey();
String cldrPattern = localeData.getEntry("predef", locale, skeleton);
String pattern = dtpg.getBestPattern(skeleton);
if (cldrPattern != null && !cldrPattern.equals(pattern)) {
System.err.println("Mismatch on skeleton pattern in locale " + locale + " for skeleton '"
+ skeleton + "': icu='" + pattern + "', cldr='" + cldrPattern + "'");
}
localeData.addEntry("predef", locale, skeleton, pattern);
}
}
}
/**
* Load the week start and weekend range values from CLDR.
*/
private void loadWeekData() {
localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData,
"//supplementalData/weekData/firstDay", "firstDay", "day");
localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData,
"//supplementalData/weekData/weekendStart", "weekendStart", "day");
localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData,
"//supplementalData/weekData/weekendEnd", "weekendEnd", "day");
localeData.addTerritoryEntries("weekdata", cldrFactory, regionLanguageData,
"//supplementalData/weekData/minDays", "minDays", "count");
}
/**
* Remove duplicates from period names.
*
* @param group
*/
private void removePeriodDuplicates(String group) {
removePeriodWidthDuplicates(group, "wide");
removePeriodWidthDuplicates(group, "abbrev");
removePeriodWidthDuplicates(group, "narrow");
}
private void removePeriodWidthDuplicates(String group, String width) {
localeData.removeCompleteDuplicates(group + "-" + width);
localeData.removeCompleteDuplicates(group + "-sa-" + width);
localeData.removeCompleteDuplicates(group + "-sa-" + width + "-redirect");
}
private void removeUnusedFormats() {
for (GwtLocale locale : localeData.getAllLocales()) {
Set<String> toRemove = new HashSet<String>();
Map<String, String> map = localeData.getEntries("predef", locale);
for (Entry<String, String> entry : map.entrySet()) {
if (!FORMATS.containsKey(entry.getKey())) {
toRemove.add(entry.getKey());
}
}
localeData.removeEntries("predef", locale, toRemove);
}
}
}