Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.squarespace.cldr.codegen.CalendarCodeGenerator Maven / Gradle / Ivy
package com.squarespace.cldr.codegen;
import static com.squarespace.cldr.codegen.Types.CALENDAR_FORMAT;
import static com.squarespace.cldr.codegen.Types.CALENDAR_FORMATTER;
import static com.squarespace.cldr.codegen.Types.CLDR_LOCALE_IF;
import static com.squarespace.cldr.codegen.Types.FIELD_VARIANTS;
import static com.squarespace.cldr.codegen.Types.HASHMAP;
import static com.squarespace.cldr.codegen.Types.MAP;
import static com.squarespace.cldr.codegen.Types.SKELETON;
import static com.squarespace.cldr.codegen.Types.STRING;
import static com.squarespace.cldr.codegen.Types.TIMEZONE_NAMES;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PROTECTED;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import com.google.common.base.Splitter;
import com.squarespace.cldr.codegen.reader.DataReader;
import com.squarespace.cldr.codegen.reader.DateTimeData;
import com.squarespace.cldr.codegen.reader.DateTimeData.Format;
import com.squarespace.cldr.codegen.reader.DateTimeData.Skeleton;
import com.squarespace.cldr.codegen.reader.DateTimeData.Variants;
import com.squarespace.cldr.codegen.reader.TimeZoneData;
import com.squarespace.cldr.codegen.reader.TimeZoneData.MetaZone;
import com.squarespace.cldr.codegen.reader.TimeZoneData.MetaZoneEntry;
import com.squarespace.cldr.codegen.reader.TimeZoneData.MetaZoneInfo;
import com.squarespace.cldr.codegen.reader.TimeZoneData.TimeZoneInfo;
import com.squarespace.cldr.dates.DateTimeField;
import com.squarespace.cldr.parse.DateTimePatternParser;
import com.squarespace.cldr.parse.FieldPattern.Field;
import com.squarespace.cldr.parse.FieldPattern.Node;
import com.squarespace.cldr.parse.FieldPattern.Text;
import com.squarespace.cldr.parse.WrapperPatternParser;
import com.squarespace.compiler.parse.Pair;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
/**
* Generates code to format dates and times using CLDR data.
*/
public class CalendarCodeGenerator {
private static final DateTimePatternParser DATETIME_PARSER = new DateTimePatternParser();
private static final WrapperPatternParser WRAPPER_PARSER = new WrapperPatternParser();
public static void main(String[] args) throws IOException {
DataReader reader = DataReader.get();
Path outputDir = Paths.get("/Users/phensley/dev/squarespace-cldr/runtime/src/generated/java");
new CalendarCodeGenerator().generate(outputDir, reader);
}
/**
* Generates the date-time classes into the given output directory.
*/
public Map generate(Path outputDir, DataReader reader)
throws IOException {
Map dateClasses = new TreeMap<>();
List dateTimeDataList = new ArrayList<>();
for (Map.Entry entry : reader.calendars().entrySet()) {
DateTimeData dateTimeData = entry.getValue();
LocaleID localeId = entry.getKey();
String className = "_CalendarFormatter_" + localeId.safe;
TimeZoneData timeZoneData = reader.timezones().get(localeId);
TypeSpec type = createFormatter(dateTimeData, timeZoneData, className);
CodeGenerator.saveClass(outputDir, Types.PACKAGE_CLDR_DATES, className, type);
ClassName cls = ClassName.get(Types.PACKAGE_CLDR_DATES, className);
dateClasses.put(localeId, cls);
dateTimeDataList.add(dateTimeData);
}
String className = "_CalendarUtils";
TypeSpec.Builder utilsType = TypeSpec.classBuilder(className)
.addModifiers(PUBLIC);
addSkeletonClassifierMethod(utilsType, dateTimeDataList);
addMetaZones(utilsType, reader.metazones());
buildTimeZoneAliases(utilsType, reader.timezoneAliases());
CodeGenerator.saveClass(outputDir, Types.PACKAGE_CLDR_DATES, "_CalendarUtils", utilsType.build());
return dateClasses;
}
/**
* Create a helper class to classify skeletons as either DATE or TIME.
*/
private void addSkeletonClassifierMethod(TypeSpec.Builder type, List dataList) {
Set dates = new LinkedHashSet<>();
Set times = new LinkedHashSet<>();
for (DateTimeData data : dataList) {
for (Skeleton skeleton : data.dateTimeSkeletons) {
if (isDateSkeleton(skeleton.skeleton)) {
dates.add(skeleton.skeleton);
} else {
times.add(skeleton.skeleton);
}
}
}
MethodSpec.Builder method = buildSkeletonType(dates, times);
type.addMethod(method.build());
}
/**
* Populate the metaZone mapping.
*/
private void addMetaZones(TypeSpec.Builder type, Map metazones) {
ClassName metazoneType = ClassName.get(Types.PACKAGE_CLDR_DATES, "MetaZone");
TypeName mapType = ParameterizedTypeName.get(MAP, STRING, metazoneType);
FieldSpec.Builder field = FieldSpec.builder(mapType, "metazones", PROTECTED, STATIC, FINAL);
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("new $T<$T, $T>() {", HashMap.class, String.class, metazoneType);
for (Map.Entry entry : metazones.entrySet()) {
String zoneId = entry.getKey();
MetaZone zone = entry.getValue();
code.beginControlFlow("\nput($S, new $T($S,\n new $T.Entry[] ", zoneId, metazoneType, zoneId, metazoneType);
int size = zone.metazones.size();
Collections.reverse(zone.metazones);
for (int i = 0; i < size; i++) {
MetaZoneEntry meta = zone.metazones.get(i);
if (i > 0) {
code.add(",\n");
}
code.add(" new $T.Entry($S, ", metazoneType, meta.metazone);
if (meta.from != null) {
code.add("/* $L */ $L, ", meta.fromString, meta.from.toEpochSecond());
} else {
code.add("-1, ");
}
if (meta.to != null) {
code.add("/* $L */ $L)", meta.toString, meta.to.toEpochSecond());
} else {
code.add("-1)");
}
}
code.endControlFlow("))");
}
code.endControlFlow("\n}");
field.initializer(code.build());
type.addField(field.build());
MethodSpec.Builder method = MethodSpec.methodBuilder("getMetazone")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "zoneId")
.addParameter(ZonedDateTime.class, "date")
.returns(String.class);
method.addStatement("$T zone = metazones.get(zoneId)", metazoneType);
method.addStatement("return zone == null ? null : zone.applies(date)");
type.addMethod(method.build());
}
/**
* Creates a switch table to resolve a retired time zone to a valid one.
*/
private void buildTimeZoneAliases(TypeSpec.Builder type, Map map) {
MethodSpec.Builder method = MethodSpec.methodBuilder("getTimeZoneAlias")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "zoneId")
.returns(String.class);
method.beginControlFlow("switch (zoneId)");
for (Map.Entry entry : map.entrySet()) {
method.addStatement("case $S: return $S", entry.getKey(), entry.getValue());
}
method.addStatement("default: return null");
method.endControlFlow();
type.addMethod(method.build());
}
/**
* Create a Java class that captures all data formats for a given locale.
*/
private TypeSpec createFormatter(DateTimeData dateTimeData, TimeZoneData timeZoneData, String className) {
LocaleID id = dateTimeData.id;
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC);
constructor.addStatement("this.bundleId = $T.$L", CLDR_LOCALE_IF, id.safe);
constructor.addStatement("this.firstDay = $L", dateTimeData.firstDay);
constructor.addStatement("this.minDays = $L", dateTimeData.minDays);
variantsFieldInit(constructor, "this.eras", dateTimeData.eras);
variantsFieldInit(constructor, "this.quartersFormat", dateTimeData.quartersFormat);
variantsFieldInit(constructor, "this.quartersStandalone", dateTimeData.quartersStandalone);
variantsFieldInit(constructor, "this.monthsFormat", dateTimeData.monthsFormat);
variantsFieldInit(constructor, "this.monthsStandalone", dateTimeData.monthsStandalone);
variantsFieldInit(constructor, "this.weekdaysFormat", dateTimeData.weekdaysFormat);
variantsFieldInit(constructor, "this.weekdaysStandalone", dateTimeData.weekdaysStandalone);
variantsFieldInit(constructor, "this.dayPeriodsFormat", dateTimeData.dayPeriodsFormat);
variantsFieldInit(constructor, "this.dayPeriodsStandalone", dateTimeData.dayPeriodsStandalone);
buildTimeZoneExemplarCities(constructor, timeZoneData);
buildTimeZoneNames(constructor, timeZoneData);
buildMetaZoneNames(constructor, timeZoneData);
TypeSpec.Builder type = TypeSpec.classBuilder(className)
.superclass(CALENDAR_FORMATTER)
.addModifiers(PUBLIC)
.addJavadoc(
"Locale \"" + dateTimeData.id + "\"\n" +
"See http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table\n")
.addMethod(constructor.build());
MethodSpec dateMethod = buildTypedPatternMethod("formatDate", CALENDAR_FORMAT, dateTimeData.dateFormats);
MethodSpec timeMethod = buildTypedPatternMethod("formatTime", CALENDAR_FORMAT, dateTimeData.timeFormats);
MethodSpec wrapperMethod = buildWrapperMethod(dateTimeData.dateTimeFormats);
MethodSpec skeletonMethod = buildSkeletonFormatter(dateTimeData.dateTimeSkeletons);
MethodSpec intervalMethod = buildIntervalMethod(dateTimeData.intervalFormats, dateTimeData.intervalFallbackFormat);
MethodSpec gmtMethod = buildWrapTimeZoneGMTMethod(timeZoneData);
MethodSpec regionFormatMethod = buildWrapTimeZoneRegionMethod(timeZoneData);
return type
.addMethod(dateMethod)
.addMethod(timeMethod)
.addMethod(wrapperMethod)
.addMethod(skeletonMethod)
.addMethod(intervalMethod)
.addMethod(gmtMethod)
.addMethod(regionFormatMethod)
.build();
}
/**
* The CLDR contains 4 standard pattern types for date and time: short, medium, long and full.
* This generates a switch statement to format patterns of this type.
* See CLDR "dateFormats" and "timeFormats" nodes.
*/
private MethodSpec buildTypedPatternMethod(String methodName, ClassName type, Format format) {
MethodSpec.Builder method = MethodSpec.methodBuilder(methodName)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(CALENDAR_FORMAT, "type")
.addParameter(ZonedDateTime.class, "d")
.addParameter(StringBuilder.class, "b");
method.beginControlFlow("if (type == null)");
method.addStatement("return");
method.endControlFlow();
method.beginControlFlow("switch (type)", type);
addTypedPattern(method, "SHORT", format.short_);
addTypedPattern(method, "MEDIUM", format.medium);
addTypedPattern(method, "LONG", format.long_);
addTypedPattern(method, "FULL", format.full);
method.endControlFlow();
return method.build();
}
/**
* Adds the pattern builder statements for a typed pattern.
*/
private void addTypedPattern(MethodSpec.Builder method, String formatType, String pattern) {
method.beginControlFlow("case $L:", formatType);
method.addComment("$S", pattern);
addPattern(method, pattern);
method.addStatement("break");
method.endControlFlow();
}
/**
* Add a named date-time pattern, adding statements to the formatting and indexing methods.
* Returns true if the pattern corresponds to a date; false if a time.
*/
private void addPattern(MethodSpec.Builder method, String pattern) {
// Parse the pattern and populate the method body with instructions to format the date.
for (Node node : DATETIME_PARSER.parse(pattern)) {
if (node instanceof Text) {
method.addStatement("b.append($S)", ((Text)node).text());
} else if (node instanceof Field) {
Field field = (Field)node;
method.addStatement("formatField(d, '$L', $L, b)", field.ch(), field.width());
}
}
}
/**
* Build a method that will format a date and time (named or skeleton) in a
* localized wrapper.
*/
private MethodSpec buildWrapperMethod(Format format) {
MethodSpec.Builder method = MethodSpec.methodBuilder("formatWrapped")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(CALENDAR_FORMAT, "wrapperType")
.addParameter(CALENDAR_FORMAT, "dateType")
.addParameter(CALENDAR_FORMAT, "timeType")
.addParameter(String.class, "dateSkel")
.addParameter(String.class, "timeSkel")
.addParameter(ZonedDateTime.class, "d")
.addParameter(StringBuilder.class, "b");
Map> map = deduplicateFormats(format);
method.beginControlFlow("switch (wrapperType)");
for (Map.Entry> entry : map.entrySet()) {
for (String type : entry.getValue()) {
method.addCode("case $L:\n", type);
}
method.beginControlFlow("");
addWrapper(method, entry.getKey());
method.addStatement("break");
method.endControlFlow();
}
method.endControlFlow();
return method.build();
}
/**
* Formats a date-time combination into a wrapper format, e.g. "{1} at {0}"
*/
private void addWrapper(MethodSpec.Builder method, String pattern) {
method.addComment("$S", pattern);
for (Node node : WRAPPER_PARSER.parseWrapper(pattern)) {
if (node instanceof Text) {
method.addStatement("b.append($S)", ((Text)node).text());
} else if (node instanceof Field) {
Field field = (Field)node;
switch (field.ch()) {
case '0':
method.beginControlFlow("if (timeType != null)");
method.addStatement("formatTime(timeType, d, b)");
method.nextControlFlow("else");
method.addStatement("formatSkeleton(timeSkel, d, b)");
method.endControlFlow();
break;
case '1':
method.beginControlFlow("if (dateType != null)");
method.addStatement("formatDate(dateType, d, b)");
method.nextControlFlow("else");
method.addStatement("formatSkeleton(dateSkel, d, b)");
method.endControlFlow();
break;
}
}
}
}
/**
* Implements the formatSkeleton method.
*/
private MethodSpec buildSkeletonFormatter(List skeletons) {
MethodSpec.Builder method = MethodSpec.methodBuilder("formatSkeleton")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(String.class, "skeleton")
.addParameter(ZonedDateTime.class, "d")
.addParameter(StringBuilder.class, "b")
.returns(boolean.class);
method.beginControlFlow("if (skeleton == null)");
method.addStatement("return false");
method.endControlFlow();
method.beginControlFlow("switch (skeleton)");
// Skeleton patterns.
for (Skeleton skeleton : skeletons) {
method.beginControlFlow("case $S:", skeleton.skeleton)
.addComment("Pattern: $S", skeleton.pattern);
addPattern(method, skeleton.pattern);
method.addStatement("break");
method.endControlFlow();
}
method.beginControlFlow("default:");
method.addStatement("return false");
method.endControlFlow();
method.endControlFlow();
method.addStatement("return true");
return method.build();
}
/**
* Build methods to format date time intervals using the field of greatest difference.
*/
private MethodSpec buildIntervalMethod(Map> intervalFormats, String fallback) {
MethodSpec.Builder method = MethodSpec.methodBuilder("formatInterval")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(ZonedDateTime.class, "s")
.addParameter(ZonedDateTime.class, "e")
.addParameter(String.class, "k")
.addParameter(DateTimeField.class, "f")
.addParameter(StringBuilder.class, "b");
// Only enter the switches if both params are non-null.
method.beginControlFlow("if (k != null && f != null)");
// BEGIN switch (k)
method.beginControlFlow("switch (k)");
for (Map.Entry> format : intervalFormats.entrySet()) {
String skeleton = format.getKey();
// BEGIN "case skeleton:"
method.beginControlFlow("case $S:", skeleton);
method.beginControlFlow("switch (f)");
for (Map.Entry entry : format.getValue().entrySet()) {
String field = entry.getKey();
// Split the interval pattern on the boundary. We end up with two patterns, one for
// start and end respectively.
Pair, List> patterns = DATETIME_PARSER.splitIntervalPattern(entry.getValue());
// BEGIN "case field:"
// Render this pair of patterns when the given field matches.
method.beginControlFlow("case $L:", DateTimeField.fromString(field));
method.addComment("$S", DateTimePatternParser.render(patterns._1));
addIntervalPattern(method, patterns._1, "s");
method.addComment("$S", DateTimePatternParser.render(patterns._2));
addIntervalPattern(method, patterns._2, "e");
method.addStatement("return");
method.endControlFlow();
// END "case field:"
}
method.addStatement("default: break");
method.endControlFlow(); // switch (f)
method.addStatement("break");
method.endControlFlow();
// END "case skeleton:"
}
method.addStatement("default: break");
method.endControlFlow();
// END switch (k)
// One of the parameters was null, or nothing matched, so render the
// fallback, e.g. format the start / end separately as "{0} - {1}"
addIntervalFallback(method, fallback);
method.endControlFlow();
return method.build();
}
/**
* Adds code to format a date time interval pattern, which may be the start or end
* date time, signified by the 'which' parameter.
*/
private void addIntervalPattern(MethodSpec.Builder method, List pattern, String which) {
for (Node node : pattern) {
if (node instanceof Text) {
method.addStatement("b.append($S)", ((Text)node).text());
} else if (node instanceof Field) {
Field field = (Field)node;
method.addStatement("formatField($L, '$L', $L, b)", which, field.ch(), field.width());
}
}
}
/**
* Adds code to render the fallback pattern.
*/
private void addIntervalFallback(MethodSpec.Builder method, String pattern) {
method.addComment("$S", pattern);
for (Node node : WRAPPER_PARSER.parseWrapper(pattern)) {
if (node instanceof Text) {
method.addStatement("b.append($S)", ((Text)node).text());
} else if (node instanceof Field) {
Field field = (Field)node;
String which = "s";
if (field.ch() == '1') {
which = "e";
}
method.addStatement("formatSkeleton(k, $L, b)", which);
}
}
}
/**
* Indicates a skeleton represents a date based on the fields it contains.
* Any time-related field will cause this to return false.
*/
private boolean isDateSkeleton(String skeleton) {
List parts = Splitter.on('-').splitToList(skeleton);
if (parts.size() > 1) {
skeleton = parts.get(0);
}
for (Node node : DATETIME_PARSER.parse(skeleton)) {
if (node instanceof Field) {
Field field = (Field) node;
switch (field.ch()) {
case 'H':
case 'h':
case 'm':
case 's':
return false;
}
}
}
return true;
}
/**
* Constructs a method to indicate if the skeleton is a DATE or TIME, or null if unsupported.
*/
private MethodSpec.Builder buildSkeletonType(Set dateSkeletons, Set timeSkeletons) {
MethodSpec.Builder method = MethodSpec.methodBuilder("skeletonType")
.addJavadoc("Indicates whether a given skeleton pattern is a DATE or TIME.\n")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "skeleton")
.returns(SKELETON);
method.beginControlFlow("switch (skeleton)");
for (String skel : dateSkeletons) {
method.addCode("case $S:\n", skel);
}
method.addStatement(" return $T.DATE", SKELETON);
for (String skel : timeSkeletons) {
method.addCode("case $S:\n", skel);
}
method.addStatement(" return $T.TIME", SKELETON);
method.addCode("default:\n");
method.addStatement(" return null");
method.endControlFlow();
return method;
}
/**
* Appends a statement that initializes a FieldVariants field in the superclass.
*/
private void variantsFieldInit(MethodSpec.Builder method, String fieldName, Variants v) {
Stmt b = new Stmt();
b.append("$L = new $T(", fieldName, FIELD_VARIANTS);
b.append("\n ").append(v.abbreviated).comma();
b.append("\n ").append(v.narrow).comma();
b.append("\n ").append(v.short_).comma();
b.append("\n ").append(v.wide);
b.append(")");
method.addStatement(b.format(), b.args());
}
/**
* Mapping locale identifiers to their localized exemplar cities.
*/
private void buildTimeZoneExemplarCities(MethodSpec.Builder method, TimeZoneData data) {
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("this.exemplarCities = new $T<$T, $T>() {", HashMap.class, String.class, String.class);
for (TimeZoneInfo info : data.timeZoneInfo) {
code.addStatement("put($S, $S)", info.zone, info.exemplarCity);
}
code.endControlFlow("}");
method.addCode(code.build());
}
/**
* Builds localized timezone name mapping.
*/
private void buildTimeZoneNames(MethodSpec.Builder method, TimeZoneData data) {
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("\nthis.$L = new $T<$T, $T>() {", "timezoneNames",
HASHMAP, STRING, TIMEZONE_NAMES);
for (TimeZoneInfo info : data.timeZoneInfo) {
if (info.nameLong == null && info.nameShort == null) {
continue;
}
code.add("\nput($S, new $T($S, ", info.zone, TIMEZONE_NAMES, info.zone);
if (info.nameLong == null) {
code.add(" null,");
} else {
code.add(" new $T.Name($S, $S, $S),", TIMEZONE_NAMES,
info.nameLong.generic, info.nameLong.standard, info.nameLong.daylight);
}
if (info.nameShort == null) {
code.add("\n null");
} else {
code.add("\n new $T.Name($S, $S, $S)", TIMEZONE_NAMES,
info.nameShort.generic, info.nameShort.standard, info.nameShort.daylight);
}
code.add("));\n");
}
code.endControlFlow("}");
method.addCode(code.build());
}
/**
* Builds localized metazone name mapping.
*/
private void buildMetaZoneNames(MethodSpec.Builder method, TimeZoneData data) {
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("\nthis.$L = new $T<$T, $T>() {", "metazoneNames",
HASHMAP, STRING, TIMEZONE_NAMES);
for (MetaZoneInfo info : data.metaZoneInfo) {
if (info.nameLong == null && info.nameShort == null) {
continue;
}
code.add("\nput($S, new $T($S, ", info.zone, TIMEZONE_NAMES, info.zone);
if (info.nameLong == null) {
code.add("\n null,");
} else {
code.add("\n new $T.Name($S, $S, $S),", TIMEZONE_NAMES,
info.nameLong.generic, info.nameLong.standard, info.nameLong.daylight);
}
if (info.nameShort == null) {
code.add("\n null");
} else {
code.add("\n new $T.Name($S, $S, $S)", TIMEZONE_NAMES,
info.nameShort.generic, info.nameShort.standard, info.nameShort.daylight);
}
code.add("));\n");
}
code.endControlFlow("}");
method.addCode(code.build());
}
/**
* Builds a method to format the timezone as hourFormat with a GMT wrapper.
*/
private MethodSpec buildWrapTimeZoneGMTMethod(TimeZoneData data) {
String[] hourFormat = data.hourFormat.split(";");
List positive = DATETIME_PARSER.parse(hourFormat[0]);
List negative = DATETIME_PARSER.parse(hourFormat[1]);
List format = WRAPPER_PARSER.parseWrapper(data.gmtFormat);
MethodSpec.Builder method = MethodSpec.methodBuilder("wrapTimeZoneGMT")
.addModifiers(PROTECTED)
.addParameter(StringBuilder.class, "b")
.addParameter(boolean.class, "neg")
.addParameter(int.class, "hours")
.addParameter(int.class, "mins")
.addParameter(boolean.class, "_short");
// Special format for zero
method.beginControlFlow("if (hours == 0 && mins == 0)");
method.addStatement("b.append($S)", data.gmtZeroFormat);
method.addStatement("return");
method.endControlFlow();
method.addStatement("boolean emitMins = !_short || mins > 0");
for (Node node : format) {
if (node instanceof Text) {
Text text = (Text) node;
method.addStatement("b.append($S)", text.text());
} else {
method.beginControlFlow("if (neg)");
appendHourFormat(method, negative);
method.endControlFlow();
method.beginControlFlow("else");
appendHourFormat(method, positive);
method.endControlFlow();
}
}
return method.build();
}
/**
* Build a method to wrap a region in the regionFormat.
*/
private MethodSpec buildWrapTimeZoneRegionMethod(TimeZoneData data) {
MethodSpec.Builder method = MethodSpec.methodBuilder("wrapTimeZoneRegion")
.addModifiers(PROTECTED)
.addParameter(StringBuilder.class, "b")
.addParameter(String.class, "region");
List format = WRAPPER_PARSER.parseWrapper(data.regionFormat);
for (Node node : format) {
if (node instanceof Text) {
Text text = (Text) node;
method.addStatement("b.append($S)", text.text());
} else {
method.addStatement("b.append(region)");
}
}
return method.build();
}
/**
* Appends code to emit the hourFormat for positive or negative.
*/
private void appendHourFormat(MethodSpec.Builder method, List fmt) {
for (Node n : fmt) {
if (n instanceof Text) {
String t = ((Text)n).text();
boolean minute = t.equals(":") || t.equals(".");
if (minute) {
method.beginControlFlow("if (emitMins)");
}
method.addStatement("b.append($S)", t);
if (minute) {
method.endControlFlow();
}
} else {
Field f = (Field)n;
if (f.ch() == 'H') {
if (f.width() == 1) {
method.addStatement("zeroPad2(b, hours, 1)");
} else {
method.addStatement("zeroPad2(b, hours, _short ? 1 : $L)", f.width());
}
} else {
method.beginControlFlow("if (emitMins)");
method.addStatement("zeroPad2(b, mins, $L)", f.width());
method.endControlFlow();
}
}
}
}
/**
* De-duplication of formats.
*/
public static Map> deduplicateFormats(Format format) {
Map> res = new LinkedHashMap<>();
mapAdd(res, format.short_, "SHORT");
mapAdd(res, format.medium, "MEDIUM");
mapAdd(res, format.long_, "LONG");
mapAdd(res, format.full, "FULL");
return res;
}
/**
* Adds a key and value to a Map>.
*/
private static void mapAdd(Map> map, String key, String val) {
List list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(val);
}
/**
* Helper to build up longer formatted statements.
*/
private static class Stmt {
private final ArrayList args = new ArrayList<>();
private final StringBuilder buf = new StringBuilder();
public Stmt append(String fmt, Object ...objects) {
buf.append(fmt);
for (Object o : objects) {
args.add(o);
}
return this;
}
public Stmt append(String[] values) {
buf.append("new $T[] {");
args.add(String.class);
for (int i= 0; i < values.length; i++) {
if (i > 0) {
buf.append(", ");
}
buf.append("$S");
args.add(values[i]);
}
buf.append("}");
return this;
}
public Stmt comma() {
buf.append(", ");
return this;
}
public Object[] args() {
return args.toArray();
}
public String format() {
return buf.toString();
}
}
}