• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.util;
2 
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.Arrays;
6 import java.util.Date;
7 import java.util.EnumSet;
8 import java.util.LinkedHashSet;
9 import java.util.List;
10 import java.util.Map;
11 import java.util.Map.Entry;
12 import java.util.Set;
13 import java.util.TreeMap;
14 import java.util.regex.Matcher;
15 import java.util.regex.Pattern;
16 
17 import org.unicode.cldr.draft.FileUtilities;
18 import org.unicode.cldr.tool.Option;
19 import org.unicode.cldr.tool.Option.Options;
20 import org.unicode.cldr.util.ICUServiceBuilder.Context;
21 import org.unicode.cldr.util.ICUServiceBuilder.Width;
22 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
23 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
24 
25 import com.ibm.icu.impl.Row.R3;
26 import com.ibm.icu.text.DateFormat;
27 import com.ibm.icu.text.DateIntervalFormat;
28 import com.ibm.icu.text.DateIntervalInfo;
29 import com.ibm.icu.text.DateTimePatternGenerator;
30 import com.ibm.icu.text.DateTimePatternGenerator.FormatParser;
31 import com.ibm.icu.text.DateTimePatternGenerator.PatternInfo;
32 import com.ibm.icu.text.DateTimePatternGenerator.VariableField;
33 import com.ibm.icu.text.DecimalFormat;
34 import com.ibm.icu.text.MessageFormat;
35 import com.ibm.icu.text.SimpleDateFormat;
36 import com.ibm.icu.util.Calendar;
37 import com.ibm.icu.util.DateInterval;
38 import com.ibm.icu.util.ICUUncheckedIOException;
39 import com.ibm.icu.util.Output;
40 import com.ibm.icu.util.TimeZone;
41 import com.ibm.icu.util.ULocale;
42 
43 public class DateTimeFormats {
44     private static final String DIR = CLDRPaths.CHART_DIRECTORY + "/verify/dates/";
45     private static SupplementalDataInfo sdi = SupplementalDataInfo.getInstance();
46     private static Map<String, PreferredAndAllowedHour> timeData = sdi.getTimeData();
47 
48     final static Options myOptions = new Options();
49 
50     enum MyOptions {
51         organization(".*", "CLDR", "organization"), filter(".*", ".*", "locale filter (regex)");
52         // boilerplate
53         final Option option;
54 
MyOptions(String argumentPattern, String defaultArgument, String helpText)55         MyOptions(String argumentPattern, String defaultArgument, String helpText) {
56             option = myOptions.add(this, argumentPattern, defaultArgument, helpText);
57         }
58     }
59 
60     private static final String TIMES_24H_TITLE = "Times 24h";
61     private static final boolean DEBUG = false;
62     private static final String DEBUG_SKELETON = "y";
63     private static final ULocale DEBUG_LIST_PATTERNS = ULocale.JAPANESE; // or null;
64 
65     private static final String FIELDS_TITLE = "Fields";
66 
67     private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
68 
69     private static final String[] STOCK = { "short", "medium", "long", "full" };
70     private static final String[] CALENDAR_FIELD_TO_PATTERN_LETTER = {
71         "G", "y", "M",
72         "w", "W", "d",
73         "D", "E", "F",
74         "a", "h", "H",
75         "m",
76     };
77     private static final Date SAMPLE_DATE = new Date(2012 - 1900, 0, 13, 14, 45, 59);
78 
79     private static final String SAMPLE_DATE_STRING = CldrUtility.isoFormat(SAMPLE_DATE);
80 
81     private static final Date[] SAMPLE_DATE_END = {
82         // "G", "y", "M",
83         null, new Date(2013 - 1900, 0, 13, 14, 45, 59), new Date(2012 - 1900, 1, 13, 14, 45, 59),
84         // "w", "W", "d",
85         null, null, new Date(2012 - 1900, 0, 14, 14, 45, 59),
86         // "D", "E", "F",
87         null, new Date(2012 - 1900, 0, 14, 14, 45, 59), null,
88         // "a", "h", "H",
89         new Date(2012 - 1900, 0, 13, 2, 45, 59), new Date(2012 - 1900, 0, 13, 15, 45, 59),
90         new Date(2012 - 1900, 0, 13, 15, 45, 59),
91         // "m",
92         new Date(2012 - 1900, 0, 13, 14, 46, 59)
93     };;
94 
95     private DateTimePatternGenerator generator;
96     private ULocale locale;
97     private ICUServiceBuilder icuServiceBuilder;
98     private ICUServiceBuilder icuServiceBuilderEnglish = new ICUServiceBuilder().setCldrFile(CLDRConfig.getInstance().getEnglish());
99 
100     private DateIntervalInfo dateIntervalInfo = new DateIntervalInfo();
101     private String calendarID;
102     private CLDRFile file;
103 
104     private static String surveyUrl = CLDRConfig.getInstance().getProperty("CLDR_SURVEY_URL",
105         "http://st.unicode.org/cldr-apps/survey");
106 
107     /**
108      * Set a CLDRFile and calendar. Must be done before calling addTable.
109      *
110      * @param file
111      * @param calendarID
112      * @return
113      */
set(CLDRFile file, String calendarID)114     public DateTimeFormats set(CLDRFile file, String calendarID) {
115         return set(file, calendarID, true);
116     }
117 
118     /**
119      * Set a CLDRFile and calendar. Must be done before calling addTable.
120      *
121      * @param file
122      * @param calendarID
123      * @return
124      */
set(CLDRFile file, String calendarID, boolean useStock)125     public DateTimeFormats set(CLDRFile file, String calendarID, boolean useStock) {
126         this.file = file;
127         locale = new ULocale(file.getLocaleID());
128         if (useStock) {
129             icuServiceBuilder = new ICUServiceBuilder().setCldrFile(file);
130         }
131         PatternInfo returnInfo = new PatternInfo();
132         XPathParts parts = new XPathParts();
133         generator = DateTimePatternGenerator.getEmptyInstance();
134         this.calendarID = calendarID;
135         boolean haveDefaultHourChar = false;
136 
137         for (String stock : STOCK) {
138             String path = "//ldml/dates/calendars/calendar[@type=\"" + calendarID
139                 + "\"]/dateFormats/dateFormatLength[@type=\"" +
140                 stock +
141                 "\"]/dateFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
142             String dateTimePattern = file.getStringValue(path);
143             if (useStock) {
144                 generator.addPattern(dateTimePattern, true, returnInfo);
145             }
146             path = "//ldml/dates/calendars/calendar[@type=\"" + calendarID
147                 + "\"]/timeFormats/timeFormatLength[@type=\"" +
148                 stock +
149                 "\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
150             dateTimePattern = file.getStringValue(path);
151             if (useStock) {
152                 generator.addPattern(dateTimePattern, true, returnInfo);
153             }
154             if (DEBUG
155                 && DEBUG_LIST_PATTERNS.equals(locale)) {
156                 System.out.println("* Adding: " + locale + "\t" + dateTimePattern);
157             }
158             if (!haveDefaultHourChar) {
159                 // use hour style in SHORT time pattern as the default
160                 // hour style for the locale
161                 FormatParser fp = new FormatParser();
162                 fp.set(dateTimePattern);
163                 List<Object> items = fp.getItems();
164                 for (int idx = 0; idx < items.size(); idx++) {
165                     Object item = items.get(idx);
166                     if (item instanceof VariableField) {
167                         VariableField fld = (VariableField) item;
168                         if (fld.getType() == DateTimePatternGenerator.HOUR) {
169                             generator.setDefaultHourFormatChar(fld.toString().charAt(0));
170                             haveDefaultHourChar = true;
171                             break;
172                         }
173                     }
174                 }
175             }
176         }
177 
178         // appendItems result.setAppendItemFormat(getAppendFormatNumber(formatName), value);
179         for (String path : With.in(file.iterator("//ldml/dates/calendars/calendar[@type=\"" + calendarID
180             + "\"]/dateTimeFormats/appendItems/appendItem"))) {
181             String request = parts.set(path).getAttributeValue(-1, "request");
182             int requestNumber = DateTimePatternGenerator.getAppendFormatNumber(request);
183             String value = file.getStringValue(path);
184             generator.setAppendItemFormat(requestNumber, value);
185             if (DEBUG
186                 && DEBUG_LIST_PATTERNS.equals(locale)) {
187                 System.out.println("* Adding: " + locale + "\t" + request + "\t" + value);
188             }
189         }
190 
191         // field names result.setAppendItemName(i, value);
192         // ldml/dates/fields/field[@type="day"]/displayName
193         for (String path : With.in(file.iterator("//ldml/dates/fields/field"))) {
194             if (!path.contains("displayName")) {
195                 continue;
196             }
197             String type = parts.set(path).getAttributeValue(-2, "type");
198             int requestNumber = find(FIELD_NAMES, type);
199 
200             String value = file.getStringValue(path);
201             generator.setAppendItemName(requestNumber, value);
202             if (DEBUG
203                 && DEBUG_LIST_PATTERNS.equals(locale)) {
204                 System.out.println("* Adding: " + locale + "\t" + type + "\t" + value);
205             }
206         }
207 
208         for (String path : With.in(file.iterator("//ldml/dates/calendars/calendar[@type=\"" + calendarID
209             + "\"]/dateTimeFormats/availableFormats/dateFormatItem"))) {
210             String key = parts.set(path).getAttributeValue(-1, "id");
211             String value = file.getStringValue(path);
212             if (key.equals(DEBUG_SKELETON)) {
213                 int debug = 0;
214             }
215             generator.addPatternWithSkeleton(value, key, true, returnInfo);
216             if (DEBUG
217                 && DEBUG_LIST_PATTERNS.equals(locale)) {
218                 System.out.println("* Adding: " + locale + "\t" + key + "\t" + value);
219             }
220         }
221 
222         generator
223             .setDateTimeFormat(Calendar.getDateTimePattern(Calendar.getInstance(locale), locale, DateFormat.MEDIUM));
224 
225         // ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/intervalFormats/intervalFormatItem[@id=\"yMMMEd\"]/greatestDifference[@id=\"d\"]
226         for (String path : With.in(file.iterator("//ldml/dates/calendars/calendar[@type=\"" + calendarID
227             + "\"]/dateTimeFormats/intervalFormats/intervalFormatItem"))) {
228             String skeleton = parts.set(path).getAttributeValue(-2, "id");
229             String diff = parts.set(path).getAttributeValue(-1, "id");
230             int diffNumber = find(CALENDAR_FIELD_TO_PATTERN_LETTER, diff);
231             String intervalPattern = file.getStringValue(path);
232             dateIntervalInfo.setIntervalPattern(skeleton, diffNumber, intervalPattern);
233         }
234         if (useStock) {
235             dateIntervalInfo.setFallbackIntervalPattern(
236                 file.getStringValue("//ldml/dates/calendars/calendar[@type=\""
237                     + calendarID + "\"]/dateTimeFormats/intervalFormats/intervalFormatFallback"));
238         }
239         return this;
240     }
241 
242     private static final String[] FIELD_NAMES = {
243         "era", "year", "quarter", "month", "week", "week_of_month",
244         "weekday", "day", "day_of_year", "day_of_week_in_month",
245         "dayperiod", "hour", "minute", "second", "fractional_second", "zone"
246     };
247 
248     static {
249         if (FIELD_NAMES.length != DateTimePatternGenerator.TYPE_LIMIT) {
250             throw new IllegalArgumentException("Internal error " + FIELD_NAMES.length + "\t"
251                 + DateTimePatternGenerator.TYPE_LIMIT);
252         }
253     }
254 
find(T[] array, T item)255     private <T> int find(T[] array, T item) {
256         for (int i = 0; i < array.length; ++i) {
257             if (array[i].equals(item)) {
258                 return i;
259             }
260         }
261         return 0;
262     }
263 
264     private static final String[][] NAME_AND_PATTERN = {
265         { "-", "Full Month" },
266         { "year month", "yMMMM" },
267         { " to  month+1", "yMMMM/M" },
268         { " to  year+1", "yMMMM/y" },
269         { "year month day", "yMMMMd" },
270         { " to  day+1", "yMMMMd/d" },
271         { " to  month+1", "yMMMMd/M" },
272         { " to  year+1", "yMMMMd/y" },
273         { "year month day weekday", "yMMMMEEEEd" },
274         { " to  day+1", "yMMMMEEEEd/d" },
275         { " to  month+1", "yMMMMEEEEd/M" },
276         { " to  year+1", "yMMMMEEEEd/y" },
277         { "month day", "MMMMd" },
278         { " to  day+1", "MMMMd/d" },
279         { " to  month+1", "MMMMd/M" },
280         { "month day weekday", "MMMMEEEEd" },
281         { " to  day+1", "MMMMEEEEd/d" },
282         { " to  month+1", "MMMMEEEEd/M" },
283 
284         { "-", "Abbreviated Month" },
285         { "year month<sub>a</sub>", "yMMM" },
286         { " to  month+1", "yMMM/M" },
287         { " to  year+1", "yMMM/y" },
288         { "year month<sub>a</sub> day", "yMMMd" },
289         { " to  day+1", "yMMMd/d" },
290         { " to  month+1", "yMMMd/M" },
291         { " to  year+1", "yMMMd/y" },
292         { "year month<sub>a</sub> day weekday", "yMMMEd" },
293         { " to  day+1", "yMMMEd/d" },
294         { " to  month+1", "yMMMEd/M" },
295         { " to  year+1", "yMMMEd/y" },
296         { "month<sub>a</sub> day", "MMMd" },
297         { " to  day+1", "MMMd/d" },
298         { " to  month+1", "MMMd/M" },
299         { "month<sub>a</sub> day weekday", "MMMEd" },
300         { " to  day+1", "MMMEd/d" },
301         { " to  month+1", "MMMEd/M" },
302 
303         { "-", "Numeric Month" },
304         { "year month<sub>n</sub>", "yM" },
305         { " to  month+1", "yM/M" },
306         { " to  year+1", "yM/y" },
307         { "year month<sub>n</sub> day", "yMd" },
308         { " to  day+1", "yMd/d" },
309         { " to  month+1", "yMd/M" },
310         { " to  year+1", "yMd/y" },
311         { "year month<sub>n</sub> day weekday", "yMEd" },
312         { " to  day+1", "yMEd/d" },
313         { " to  month+1", "yMEd/M" },
314         { " to  year+1", "yMEd/y" },
315         { "month<sub>n</sub> day", "Md" },
316         { " to  day+1", "Md/d" },
317         { " to  month+1", "Md/M" },
318         { "month<sub>n</sub> day weekday", "MEd" },
319         { " to  day+1", "MEd/d" },
320         { " to  month+1", "MEd/M" },
321 
322         { "-", "Other Dates" },
323         { "year", "y" },
324         { " to  year+1", "y/y" },
325         { "year quarter", "yQQQQ" },
326         { "year quarter<sub>a</sub>", "yQQQ" },
327         { "quarter", "QQQQ" },
328         { "quarter<sub>a</sub>", "QQQ" },
329         { "month", "MMMM" },
330         { " to  month+1", "MMMM/M" },
331         { "month<sub>a</sub>", "MMM" },
332         { " to  month+1", "MMM/M" },
333         { "month<sub>n</sub>", "M" },
334         { " to  month+1", "M/M" },
335         { "day", "d" },
336         { " to  day+1", "d/d" },
337         { "day weekday", "Ed" },
338         { " to  day+1", "Ed/d" },
339         { "weekday", "EEEE" },
340         { " to  weekday+1", "EEEE/E" },
341         { "weekday<sub>a</sub>", "E" },
342         { " to  weekday+1", "E/E" },
343 
344         { "-", "Times" },
345         { "hour", "j" },
346         { " to  hour+1", "j/j" },
347         { "hour minute", "jm" },
348         { " to  minute+1", "jm/m" },
349         { " to  hour+1", "jm/j" },
350         { "hour minute second", "jms" },
351         { "minute second", "ms" },
352         { "minute", "m" },
353         { "second", "s" },
354 
355         { "-", TIMES_24H_TITLE },
356         { "hour<sub>24</sub>", "H" },
357         { " to  hour+1", "H/H" },
358         { "hour<sub>24</sub> minute", "Hm" },
359         { " to  minute+1", "Hm/m" },
360         { " to  hour+1", "Hm/H" },
361         { "hour<sub>24</sub> minute second", "Hms" },
362 
363         { "-", "Dates and Times" },
364         { "month, day, hour, minute", "Mdjm" },
365         { "month, day, hour, minute", "MMMdjm" },
366         { "month, day, hour, minute", "MMMMdjm" },
367         { "year month, day, hour, minute", "yMdjms" },
368         { "year month, day, hour, minute", "yMMMdjms" },
369         { "year month, day, hour, minute", "yMMMMdjms" },
370         { "year month, day, hour, minute, zone", "yMMMMdjmsv" },
371         { "year month, day, hour, minute, zone (long)", "yMMMMdjmsvvvv" },
372 
373         { "-", "Relative Dates" },
374         { "3 years ago", "®year-past-long-3" },
375         { "2 years ago", "®year-past-long-2" },
376         { "Last year", "®year-1" },
377         { "This year", "®year0" },
378         { "Next year", "®year1" },
379         { "2 years from now", "®year-future-long-2" },
380         { "3 years from now", "®year-future-long-3" },
381 
382         { "3 months ago", "®month-past-long-3" },
383         { "Last month", "®month-1" },
384         { "This month", "®month0" },
385         { "Next month", "®month1" },
386         { "3 months from now", "®month-future-long-3" },
387 
388         { "6 weeks ago", "®week-past-long-3" },
389         { "Last week", "®week-1" },
390         { "This week", "®week0" },
391         { "Next week", "®week1" },
392         { "6 weeks from now", "®week-future-long-3" },
393 
394         { "Last Sunday", "®sun-1" },
395         { "This Sunday", "®sun0" },
396         { "Next Sunday", "®sun1" },
397 
398         { "Last Sunday + time", "®sun-1jm" },
399         { "This Sunday + time", "®sun0jm" },
400         { "Next Sunday + time", "®sun1jm" },
401 
402         { "3 days ago", "®day-past-long-3" },
403         { "Yesterday", "®day-1" },
404         { "This day", "®day0" },
405         { "Tomorrow", "®day1" },
406         { "3 days from now", "®day-future-long-3" },
407 
408         { "3 days ago + time", "®day-past-long-3jm" },
409         { "Last day + time", "®day-1jm" },
410         { "This day + time", "®day0jm" },
411         { "Next day + time", "®day1jm" },
412         { "3 days from now + time", "®day-future-long-3jm" },
413     };
414 
415     private class Diff {
416         Set<String> availablePatterns = generator.getBaseSkeletons(new LinkedHashSet<String>());
417         {
418             for (Entry<String, Set<String>> pat : dateIntervalInfo.getPatterns().entrySet()) {
419                 for (String patDiff : pat.getValue()) {
420                     availablePatterns.add(pat.getKey() + "/" + patDiff);
421                 }
422             }
423         }
424 
isPresent(String skeleton)425         public boolean isPresent(String skeleton) {
426             return availablePatterns.remove(skeleton.replace('j', generator.getDefaultHourFormatChar()));
427         }
428     }
429 
430     /**
431      * Generate a table of date examples.
432      *
433      * @param comparison
434      * @param output
435      */
addTable(DateTimeFormats comparison, Appendable output)436     public void addTable(DateTimeFormats comparison, Appendable output) {
437         try {
438             output.append("<h2>" + hackDoubleLinked("Patterns") + "</h2>\n<table class='dtf-table'>");
439             Diff diff = new Diff();
440             boolean is24h = generator.getDefaultHourFormatChar() == 'H';
441             showRow(output, RowStyle.header, FIELDS_TITLE, "Skeleton", "English Example", "Native Example", false);
442             for (String[] nameAndSkeleton : NAME_AND_PATTERN) {
443                 String name = nameAndSkeleton[0];
444                 String skeleton = nameAndSkeleton[1];
445                 if (skeleton.equals(DEBUG_SKELETON)) {
446                     int debug = 0;
447                 }
448                 if (name.equals("-")) {
449                     if (is24h && skeleton.equals(TIMES_24H_TITLE)) {
450                         continue;
451                     }
452                     showRow(output, RowStyle.separator, skeleton, null, null, null, false);
453                 } else {
454                     if (is24h && skeleton.contains("H")) {
455                         continue;
456                     }
457                     showRow(output, RowStyle.normal, name, skeleton, comparison.getExample(skeleton), getExample(skeleton), diff.isPresent(skeleton));
458                 }
459             }
460             if (!diff.availablePatterns.isEmpty()) {
461                 showRow(output, RowStyle.separator, "Additional Patterns in Locale data", null, null, null, false);
462                 for (String skeleton : diff.availablePatterns) {
463                     if (skeleton.equals(DEBUG_SKELETON)) {
464                         int debug = 0;
465                     }
466                     if (is24h && (skeleton.contains("h") || skeleton.contains("a"))) {
467                         continue;
468                     }
469                     // skip zones, day_of_year, Day of Week in Month, numeric quarter, week in month, week in year,
470                     // frac.sec
471                     if (skeleton.contains("v") || skeleton.contains("z")
472                         || skeleton.contains("Q") && !skeleton.contains("QQ")
473                         || skeleton.equals("D") || skeleton.equals("F")
474                         || skeleton.equals("S")
475                         || skeleton.equals("W") || skeleton.equals("w")) {
476                         continue;
477                     }
478                     showRow(output, RowStyle.normal, skeleton, skeleton, comparison.getExample(skeleton), getExample(skeleton), true);
479                 }
480             }
481             output.append("</table>");
482         } catch (IOException e) {
483             throw new ICUUncheckedIOException(e);
484         }
485     }
486 
487     /**
488      * Get an example from the "enhanced" skeleton.
489      *
490      * @param skeleton
491      * @return
492      */
getExample(String skeleton)493     private String getExample(String skeleton) {
494         String example;
495         if (skeleton.contains("®")) {
496             return getRelativeExampleFromSkeleton(skeleton);
497         } else {
498             int slashPos = skeleton.indexOf('/');
499             if (slashPos >= 0) {
500                 String mainSkeleton = skeleton.substring(0, slashPos);
501                 DateIntervalFormat dateIntervalFormat = new DateIntervalFormat(mainSkeleton, dateIntervalInfo,
502                     icuServiceBuilder.getDateFormat(calendarID, generator.getBestPattern(mainSkeleton)));
503                 String diffString = skeleton.substring(slashPos + 1).replace('j', 'H');
504                 int diffNumber = find(CALENDAR_FIELD_TO_PATTERN_LETTER, diffString);
505                 Date endDate = SAMPLE_DATE_END[diffNumber];
506                 try {
507                     example = dateIntervalFormat.format(new DateInterval(SAMPLE_DATE.getTime(), endDate.getTime()));
508                 } catch (Exception e) {
509                     throw new IllegalArgumentException(skeleton + ", " + endDate, e);
510                 }
511             } else {
512                 if (skeleton.equals(DEBUG_SKELETON)) {
513                     int debug = 0;
514                 }
515                 SimpleDateFormat format = getDateFormatFromSkeleton(skeleton);
516                 format.setTimeZone(TimeZone.getTimeZone("Europe/Paris"));
517                 example = format.format(SAMPLE_DATE);
518             }
519         }
520         return TransliteratorUtilities.toHTML.transform(example);
521     }
522 
523     static final Pattern RELATIVE_DATE = PatternCache.get("®([a-z]+(?:-[a-z]+)?)+(-[a-z]+)?([+-]?\\d+)([a-zA-Z]+)?");
524 
525     class RelativePattern {
526         private static final String UNIT_PREFIX = "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"duration-";
527         final String type;
528         final int offset;
529         final String time;
530         final String path;
531         final String value;
532 
RelativePattern(CLDRFile file, String skeleton)533         public RelativePattern(CLDRFile file, String skeleton) {
534             Matcher m = RELATIVE_DATE.matcher(skeleton);
535             if (m.matches()) {
536                 type = m.group(1);
537                 String length = m.group(2);
538                 offset = Integer.parseInt(m.group(3));
539                 String temp = m.group(4);
540                 time = temp == null ? null : temp.replace('j', generator.getDefaultHourFormatChar());
541 
542                 if (-1 <= offset && offset <= 1) {
543                     //ldml/dates/fields/field[@type="year"]/relative[@type="-1"]
544                     path = "//ldml/dates/fields/field[@type=\"" + type + "\"]/relative[@type=\"" + offset + "\"]";
545                     value = file.getStringValue(path);
546                 } else {
547                     // //ldml/units/unit[@type="hour"]/unitPattern[@count="other"]
548                     PluralInfo plurals = sdi.getPlurals(file.getLocaleID());
549                     String base = UNIT_PREFIX + type + "\"]/unitPattern[@count=\"";
550                     String tempPath = base + plurals.getCount(offset) + "\"]";
551                     String tempValue = file.getStringValue(tempPath);
552                     if (tempValue == null) {
553                         tempPath = base + Count.other + "\"]";
554                         tempValue = file.getStringValue(tempPath);
555                     }
556                     path = tempPath;
557                     value = tempValue;
558                 }
559             } else {
560                 throw new IllegalArgumentException(skeleton);
561             }
562         }
563     }
564 
getRelativeExampleFromSkeleton(String skeleton)565     private String getRelativeExampleFromSkeleton(String skeleton) {
566         RelativePattern rp = new RelativePattern(file, skeleton);
567         String value = rp.value;
568         if (value == null) {
569             value = "ⓜⓘⓢⓢⓘⓝⓖ";
570         } else {
571             DecimalFormat format = icuServiceBuilder.getNumberFormat(0);
572             value = value.replace("{0}", format.format(Math.abs(rp.offset)).replace("'", "''"));
573         }
574         if (rp.time == null) {
575             return value;
576         } else {
577             SimpleDateFormat format2 = getDateFormatFromSkeleton(rp.time);
578             format2.setTimeZone(GMT);
579             String formattedTime = format2.format(SAMPLE_DATE);
580             //                String length = skeleton.contains("MMMM") ? skeleton.contains("E") ? "full" : "long"
581             //                    : skeleton.contains("MMM") ? "medium" : "short";
582             String path2 = getDTSeparator("full");
583             String datetimePattern = file.getStringValue(path2).replace("'", "");
584             return MessageFormat.format(datetimePattern, formattedTime, value);
585         }
586     }
587 
getDTSeparator(String length)588     private String getDTSeparator(String length) {
589         String path = "//ldml/dates/calendars/calendar[@type=\"" +
590             calendarID +
591             "\"]/dateTimeFormats/dateTimeFormatLength[@type=\"" +
592             length +
593             "\"]/dateTimeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
594         return path;
595     }
596 
getDateFormatFromSkeleton(String skeleton)597     public SimpleDateFormat getDateFormatFromSkeleton(String skeleton) {
598         String pattern = getBestPattern(skeleton);
599         return getDateFormat(pattern);
600     }
601 
getDateFormat(String pattern)602     private SimpleDateFormat getDateFormat(String pattern) {
603         SimpleDateFormat format = icuServiceBuilder.getDateFormat(calendarID, pattern);
604         format.setTimeZone(GMT);
605         return format;
606     }
607 
getBestPattern(String skeleton)608     public String getBestPattern(String skeleton) {
609         String pattern = generator.getBestPattern(skeleton);
610         return pattern;
611     }
612 
613     enum RowStyle {
614         header, separator, normal
615     }
616 
617     /**
618      * Show a single row
619      *
620      * @param output
621      * @param rowStyle
622      * @param name
623      * @param skeleton
624      * @param english
625      * @param example
626      * @param isPresent
627      * @throws IOException
628      */
showRow(Appendable output, RowStyle rowStyle, String name, String skeleton, String english, String example, boolean isPresent)629     private void showRow(Appendable output, RowStyle rowStyle, String name, String skeleton, String english,
630         String example, boolean isPresent)
631         throws IOException {
632         output.append("<tr>");
633         switch (rowStyle) {
634         case separator:
635             String link = name.replace(' ', '_');
636             output.append("<th colSpan='3' class='dtf-sep'>")
637                 .append(hackDoubleLinked(link, name))
638                 .append("</th>");
639             break;
640         case header:
641         case normal:
642             String startCell = rowStyle == RowStyle.header ? "<th class='dtf-h'>" : "<td class='dtf-s'>";
643             String endCell = rowStyle == RowStyle.header ? "</th>" : "</td>";
644             if (name.equals(FIELDS_TITLE)) {
645                 output.append("<th class='dtf-th'>").append(name).append("</a></th>");
646             } else {
647                 String indent = "";
648                 if (name.startsWith(" ")) {
649                     indent = "&nbsp;&nbsp;&nbsp;";
650                     name = name.trim();
651                 }
652                 output.append("<th class='dtf-left'>" + indent + hackDoubleLinked(skeleton, name) + "</th>");
653             }
654             // .append(startCell).append(skeleton).append(endCell)
655             output.append(startCell).append(english).append(endCell)
656                 .append(startCell).append(example).append(endCell)
657             //.append(startCell).append(isPresent ? " " : "c").append(endCell)
658             ;
659             if (rowStyle != RowStyle.header) {
660                 String fix = getFix(skeleton);
661                 if (fix != null) {
662                     output.append(startCell).append(fix).append(endCell);
663                 }
664             }
665         }
666         output.append("</tr>\n");
667     }
668 
getFix(String skeleton)669     private String getFix(String skeleton) {
670         String path;
671         String value;
672         if (skeleton.contains("®")) {
673             RelativePattern rp = new RelativePattern(file, skeleton);
674             path = rp.path;
675             value = rp.value;
676         } else {
677             skeleton = skeleton.replace('j', generator.getDefaultHourFormatChar());
678             int slashPos = skeleton.indexOf('/');
679             if (slashPos >= 0) {
680                 String mainSkeleton = skeleton.substring(0, slashPos);
681                 String diff = skeleton.substring(slashPos + 1);
682                 path = "//ldml/dates/calendars/calendar[@type=\"" + calendarID +
683                     "\"]/dateTimeFormats/intervalFormats/intervalFormatItem[@id=\"" + mainSkeleton +
684                     "\"]/greatestDifference[@id=\"" + diff +
685                     "\"]";
686             } else {
687                 path = getAvailableFormatPath(skeleton);
688             }
689             value = file.getStringValue(path);
690         }
691         if (value == null) {
692             String skeleton2 = skeleton.replace("MMMM", "MMM").replace("EEEE", "E").replace("QQQQ", "QQQ");
693             if (!skeleton.equals(skeleton2)) {
694                 return getFix(skeleton2);
695             }
696             if (DEBUG) {
697                 System.out.println("No pattern for " + skeleton + ", " + path);
698             }
699             return null;
700         }
701         return getFixFromPath(path);
702     }
703 
getAvailableFormatPath(String skeleton)704     private String getAvailableFormatPath(String skeleton) {
705         String path = "//ldml/dates/calendars/calendar[@type=\"" + calendarID +
706             "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" + skeleton +
707             "\"]";
708         return path;
709     }
710 
getFixFromPath(String path)711     public String getFixFromPath(String path) {
712         String result = PathHeader.getLinkedView(surveyUrl, file, path);
713         return result == null ? "" : result;
714     }
715 
716     /**
717      * Add a table of date comparisons
718      *
719      * @param english
720      * @param output
721      */
addDateTable(CLDRFile english, Appendable output)722     public void addDateTable(CLDRFile english, Appendable output) {
723         // ldml/dates/calendars/calendar[@type="gregorian"]/months/monthContext[@type="format"]/monthWidth[@type="abbreviated"]/month[@type="1"]
724         // ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="stand-alone"]/quarterWidth[@type="wide"]/quarter[@type="1"]
725         // ldml/dates/calendars/calendar[@type="gregorian"]/days/dayContext[@type="stand-alone"]/dayWidth[@type="abbreviated"]/day[@type="sun"]
726         try {
727             output.append("<h2>" + hackDoubleLinked("Weekdays") + "</h2>\n");
728             addDateSubtable(
729                 "//ldml/dates/calendars/calendar[@type=\"CALENDAR\"]/days/dayContext[@type=\"FORMAT\"]/dayWidth[@type=\"WIDTH\"]/day[@type=\"TYPE\"]",
730                 english, output, "sun", "mon", "tue", "wed", "thu", "fri", "sat");
731             output.append("<h2>" + hackDoubleLinked("Months") + "</h2>\n");
732             addDateSubtable(
733                 "//ldml/dates/calendars/calendar[@type=\"CALENDAR\"]/months/monthContext[@type=\"FORMAT\"]/monthWidth[@type=\"WIDTH\"]/month[@type=\"TYPE\"]",
734                 english, output, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");
735             output.append("<h2>" + hackDoubleLinked("Quarters") + "</h2>\n");
736             addDateSubtable(
737                 "//ldml/dates/calendars/calendar[@type=\"CALENDAR\"]/quarters/quarterContext[@type=\"FORMAT\"]/quarterWidth[@type=\"WIDTH\"]/quarter[@type=\"TYPE\"]",
738                 english, output, "1", "2", "3", "4");
739             //            add24HourInfo();
740         } catch (IOException e) {
741             throw new ICUUncheckedIOException(e);
742         }
743     }
744 
745     //    private void add24HourInfo() {
746     //        PreferredAndAllowedHour timeInfo = timeData.get(locale);
747     //
748     //        for (String loc : fac)
749     //    }
750 
addDateSubtable(String path, CLDRFile english, Appendable output, String... types)751     private void addDateSubtable(String path, CLDRFile english, Appendable output, String... types) throws IOException {
752         path = path.replace("CALENDAR", calendarID);
753         output
754             .append("<table class='dtf-table'>\n"
755                 +
756                 "<tr><th class='dtf-th'>English</th><th class='dtf-th'>Wide</th><th class='dtf-th'>Abbr.</th><th class='dtf-th'>Narrow</th></tr>"
757                 +
758                 "\n");
759         for (String type : types) {
760             String path1 = path.replace("TYPE", type);
761             output.append("<tr>");
762             boolean first = true;
763             for (String width : Arrays.asList("wide", "abbreviated", "narrow")) {
764                 String path2 = path1.replace("WIDTH", width);
765                 String last = null;
766                 String lastPath = null;
767                 for (String format : Arrays.asList("format", "stand-alone")) {
768                     String path3 = path2.replace("FORMAT", format);
769                     if (first) {
770                         String value = english.getStringValue(path3);
771                         output.append("<th class='dtf-left'>").append(TransliteratorUtilities.toHTML.transform(value))
772                             .append("</th>");
773                         first = false;
774                     }
775                     String value = file.getStringValue(path3);
776                     if (last == null) {
777                         last = value;
778                         lastPath = path3;
779                     } else {
780                         String lastFix = getFixFromPath(lastPath);
781                         output.append("<td class='dtf-nopad'><table class='dtf-int'><tr><td>").append(
782                             TransliteratorUtilities.toHTML.transform(last));
783                         if (lastFix != null) {
784                             output.append("</td><td class='dtf-fix'>").append(lastFix);
785                         }
786                         if (!value.equals(last)) {
787                             String fix = getFixFromPath(path3);
788                             output.append("</td></tr><tr><td>").append(TransliteratorUtilities.toHTML.transform(value));
789                             if (fix != null) {
790                                 output.append("</td><td class='dtf-fix'>").append(fix);
791                             }
792                         }
793                         output.append("</td></tr></table></td>");
794                     }
795                 }
796             }
797             output.append("</tr>\n");
798         }
799         output.append("</table>\n");
800     }
801 
802     private static final boolean RETIRE = false;
803     private static final String LOCALES = ".*"; // "da|zh|de|ta";
804 
805     /**
806      * Produce a set of static tables from the vxml data. Only a stopgap until the above is integrated into ST.
807      *
808      * @param args
809      * @throws IOException
810      */
main(String[] args)811     public static void main(String[] args) throws IOException {
812         myOptions.parse(MyOptions.organization, args, true);
813 
814         String organization = MyOptions.organization.option.getValue();
815         String filter = MyOptions.filter.option.getValue();
816 
817         Factory englishFactory = Factory.make(CLDRPaths.MAIN_DIRECTORY, filter);
818         CLDRFile englishFile = englishFactory.make("en", true);
819 
820         Factory factory = Factory.make(CLDRPaths.MAIN_DIRECTORY, LOCALES);
821         System.out.println("Total locales: " + factory.getAvailableLanguages().size());
822         DateTimeFormats english = new DateTimeFormats().set(englishFile, "gregorian");
823         PrintWriter index = openIndex(DIR, "Date/Time");
824 
825         Map<String, String> sorted = new TreeMap<String, String>();
826         SupplementalDataInfo sdi = SupplementalDataInfo.getInstance();
827         Set<String> defaultContent = sdi.getDefaultContentLocales();
828         for (String localeID : factory.getAvailableLanguages()) {
829             Level level = StandardCodes.make().getLocaleCoverageLevel(organization, localeID);
830             if (Level.MODERN.compareTo(level) > 0) {
831                 continue;
832             }
833             if (defaultContent.contains(localeID)) {
834                 System.out.println("Skipping default content: " + localeID);
835                 continue;
836             }
837             sorted.put(englishFile.getName(localeID, true), localeID);
838         }
839 
840         writeCss(DIR);
841         PrintWriter out;
842         // http://st.unicode.org/cldr-apps/survey?_=LOCALE&x=r_datetime&calendar=gregorian
843         int oldFirst = 0;
844         for (Entry<String, String> nameAndLocale : sorted.entrySet()) {
845             String name = nameAndLocale.getKey();
846             String localeID = nameAndLocale.getValue();
847             DateTimeFormats formats = new DateTimeFormats().set(factory.make(localeID, true), "gregorian");
848             String filename = localeID + ".html";
849             out = FileUtilities.openUTF8Writer(DIR, filename);
850             String redirect = "http://st.unicode.org/cldr-apps/survey?_=" + localeID
851                 + "&x=r_datetime&calendar=gregorian";
852             out.println(
853                 "<!doctype HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'><html><head>\n"
854                     +
855                     (RETIRE ? "<meta http-equiv='REFRESH' content='0;url=" + redirect + "'>\n" : "")
856                     +
857                     "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>\n"
858                     +
859                     "<title>Date/Time Charts: "
860                     + name
861                     + "</title>\n"
862                     +
863                     "<link rel='stylesheet' type='text/css' href='index.css'>\n"
864                     +
865                     "</head><body><h1>Date/Time Charts: "
866                     + name
867                     + "</h1>"
868                     +
869                     "<p><a href='index.html'>Index</a></p>\n"
870                     +
871                     "<p>The following chart shows typical usage of date and time formatting with the Gregorian calendar. "
872                     +
873                     "<i>There is important information on <a target='CLDR_ST_DOCS' href='http://cldr.unicode.org/translation/date-time-review'>Date/Time Review</a>, "
874                     +
875                     "so please read that page before starting!</i></p>\n");
876             formats.addTable(english, out);
877             formats.addDateTable(englishFile, out);
878             formats.addDayPeriods(englishFile, out);
879             out.println(
880                 "<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>"
881                     +
882                     "<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>");
883             out.println("</body></html>");
884             out.close();
885             int first = name.codePointAt(0);
886             if (oldFirst != first) {
887                 index.append("<hr>");
888                 oldFirst = first;
889             } else {
890                 index.append("  ");
891             }
892             index.append("<a href='").append(filename).append("'>").append(name).append("</a>\n");
893             index.flush();
894         }
895         index.println("</div></body></html>");
896         index.close();
897     }
898 
openIndex(String directory, String title)899     public static PrintWriter openIndex(String directory, String title) throws IOException {
900         String dateString = CldrUtility.isoFormatDateOnly(new Date());
901         PrintWriter index = FileUtilities.openUTF8Writer(directory, "index.html");
902         index
903             .println(
904                 "<!doctype HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'><html><head>\n"
905                     +
906                     "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>\n"
907                     +
908                     "<title>"
909                     + title
910                     + " Charts</title>\n"
911                     +
912                     "</head><body><h1>"
913                     + title
914                     + " Charts</h1>"
915                     +
916                     "<p style='float:left; text-align:left'><a href='../index.html'>Index</a></p>\n"
917                     +
918                     // "<p style='float:left; text-align:left'><a href='index.html'>Index</a></p>\n" +
919                     "<p style='float:right; text-align:right'>"
920                     + dateString
921                     + "</p>\n"
922                     + "<div style='clear:both; margin:2em'>");
923         return index;
924     }
925 
writeCss(String directory)926     public static void writeCss(String directory) throws IOException {
927         PrintWriter out = FileUtilities.openUTF8Writer(directory, "index.css");
928         out.println(".dtf-table, .dtf-int {margin-left:auto; margin-right:auto; border-collapse:collapse;}\n"
929             +
930             ".dtf-table, .dtf-s, .dtf-nopad, .dtf-fix, .dtf-th, .dtf-h, .dtf-sep, .dtf-left, .dtf-int {border:1px solid gray;}\n"
931             +
932             ".dtf-th {background-color:#EEE; padding:4px}\n" +
933             ".dtf-s, .dtf-nopad, .dtf-fix {padding:3px; text-align:center}\n" +
934             ".dtf-sep {background-color:#EEF; text-align:center}\n" +
935             ".dtf-s {text-align:center;}\n" +
936             ".dtf-int {width:100%; height:100%}\n" +
937             ".dtf-fix {width:1px}\n" +
938             ".dtf-left {text-align:left;}\n" +
939             ".dtf-nopad {padding:0px; align:top}\n" +
940             ".dtf-gray {background-color:#EEF}\n");
941         out.close();
942     }
943 
addDayPeriods(CLDRFile englishFile, Appendable output)944     public void addDayPeriods(CLDRFile englishFile, Appendable output) {
945         try {
946             output.append("<h2>" + hackDoubleLinked("Day Periods") + "</h2>\n");
947             output
948                 .append("<p>Please review these and correct if needed. The Wide fields are the most important. "
949                     + "To correct them, go to "
950                     + getFixFromPath(ICUServiceBuilder.getDayPeriodPath(DayPeriodInfo.DayPeriod.am, Context.format, Width.wide))
951                     + " and following. "
952                     + "<b>Note: </b>Day Periods can be a bit tricky; "
953                     + "for more information, see <a target='CLDR-ST-DOCS' href='http://cldr.unicode.org/translation/date-time-names#TOC-Day-Periods-AM-and-PM-'>Day Periods</a>.</p>\n");
954             output
955                 .append("<table class='dtf-table'>\n"
956                     + "<tr>"
957                     + "<th class='dtf-th' rowSpan='3'>DayPeriodID</th>"
958                     + "<th class='dtf-th' rowSpan='3'>Time Span(s)</th>"
959                     + "<th class='dtf-th' colSpan='4'>Format</th>"
960                     + "<th class='dtf-th' colSpan='4'>Standalone</th>"
961 
962                     + "</tr>\n"
963                     + "<tr>"
964                     + "<th class='dtf-th' colSpan='2'>Wide</th>"
965                     + "<th class='dtf-th'>Abbreviated</th>"
966                     + "<th class='dtf-th'>Narrow</th>"
967                     + "<th class='dtf-th' colSpan='2'>Wide</th>"
968                     + "<th class='dtf-th'>Abbreviated</th>"
969                     + "<th class='dtf-th'>Narrow</th>"
970                     + "</tr>\n"
971                     + "<tr>"
972                     + "<th class='dtf-th'>English</th>"
973                     + "<th class='dtf-th'>Native</th>"
974                     + "<th class='dtf-th'>Native</th>"
975                     + "<th class='dtf-th'>Native</th>"
976                     + "<th class='dtf-th'>English</th>"
977                     + "<th class='dtf-th'>Native</th>"
978                     + "<th class='dtf-th'>Native</th>"
979                     + "<th class='dtf-th'>Native</th>"
980                     + "</tr>\n");
981             DayPeriodInfo dayPeriodInfo = sdi.getDayPeriods(DayPeriodInfo.Type.format, file.getLocaleID());
982             Set<DayPeriodInfo.DayPeriod> dayPeriods = new LinkedHashSet<>(dayPeriodInfo.getPeriods());
983             DayPeriodInfo dayPeriodInfo2 = sdi.getDayPeriods(DayPeriodInfo.Type.format, "en");
984             Set<DayPeriodInfo.DayPeriod> eDayPeriods = EnumSet.copyOf(dayPeriodInfo2.getPeriods());
985             Output<Boolean> real = new Output<>();
986             Output<Boolean> realEnglish = new Output<>();
987 
988             for (DayPeriodInfo.DayPeriod period : dayPeriods) {
989                 R3<Integer, Integer, Boolean> first = dayPeriodInfo.getFirstDayPeriodInfo(period);
990                 int midPoint = (first.get0() + first.get1()) / 2;
991                 output.append("<tr>");
992                 output.append("<th class='dtf-left'>").append(TransliteratorUtilities.toHTML.transform(period.toString()))
993                     .append("</th>\n");
994                 String periods = dayPeriodInfo.toString(period);
995                 output.append("<th class='dtf-left'>").append(TransliteratorUtilities.toHTML.transform(periods))
996                     .append("</th>\n");
997                 for (Context context : Context.values()) {
998                     for (Width width : Width.values()) {
999                         final String dayPeriodPath = ICUServiceBuilder.getDayPeriodPath(period, context, width);
1000                         if (width == Width.wide) {
1001                             String englishValue;
1002                             if (context == Context.format) {
1003                                 englishValue = icuServiceBuilderEnglish.formatDayPeriod(midPoint, context, width);
1004                                 realEnglish.value = true;
1005                             } else {
1006                                 englishValue = icuServiceBuilderEnglish.getDayPeriodValue(dayPeriodPath, null, realEnglish);
1007                             }
1008                             output.append("<th class='dtf-left" + (realEnglish.value ? "" : " dtf-gray") + "'" + ">")
1009                                 .append(getCleanValue(englishValue, width, "<i>unused</i>"))
1010                                 .append("</th>\n");
1011                         }
1012                         String nativeValue = icuServiceBuilder.getDayPeriodValue(dayPeriodPath, "�", real);
1013                         if (context == Context.format) {
1014                             nativeValue = icuServiceBuilder.formatDayPeriod(midPoint, nativeValue);
1015                         }
1016                         output.append("<td class='dtf-left" + (real.value ? "" : " dtf-gray") + "'>")
1017                             .append(getCleanValue(nativeValue, width, "<i>missing</i>"))
1018                             .append("</td>\n");
1019                     }
1020                 }
1021                 output.append("</tr>\n");
1022             }
1023             output.append("</table>\n");
1024         } catch (IOException e) {
1025             throw new ICUUncheckedIOException(e);
1026         }
1027     }
1028 
getCleanValue(String evalue, Width width, String fallback)1029     private String getCleanValue(String evalue, Width width, String fallback) {
1030         String replacement = width == Width.wide ? fallback : "<i>optional</i>";
1031         String qevalue = evalue != null ? TransliteratorUtilities.toHTML.transform(evalue) : replacement;
1032         return qevalue.replace("�", replacement);
1033     }
1034 
1035 //    static final String SHORT_PATH = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/timeFormats/timeFormatLength[@type=\"short\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
1036 //    static final String HM_PATH = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"hm\"]";
1037 //
1038 //    private String format(CLDRFile file, String evalue, int timeInDay) {
1039 //        String pattern = file.getStringValue(HM_PATH);
1040 //        if (pattern == null) {
1041 //            pattern = "h:mm \uE000";
1042 //        } else {
1043 //            pattern = pattern.replace('a', '\uE000');
1044 //        }
1045 //        SimpleDateFormat df = icuServiceBuilder.getDateFormat("gregorian", pattern);
1046 //        String formatted = df.format(timeInDay);
1047 //        String result = formatted.replace("\uE000", evalue);
1048 //        return result;
1049 //    }
1050 
hackDoubleLinked(String link, String name)1051     private String hackDoubleLinked(String link, String name) {
1052         return name;
1053     }
1054 
hackDoubleLinked(String string)1055     private String hackDoubleLinked(String string) {
1056         return string;
1057     }
1058 
writeIndexMap(Map<String, String> nameToFile, PrintWriter index)1059     static void writeIndexMap(Map<String, String> nameToFile, PrintWriter index) {
1060         int oldFirst = 0;
1061         for (Entry<String, String> entry : nameToFile.entrySet()) {
1062             String name = entry.getKey();
1063             String file = entry.getValue();
1064             int first = name.codePointAt(0);
1065             if (oldFirst != first) {
1066                 index.append("<hr>");
1067                 oldFirst = first;
1068             } else {
1069                 index.append("  ");
1070             }
1071             index.append("<a href='").append(file).append("'>").append(name).append("</a>\n");
1072             index.flush();
1073         }
1074         index.println("</div></body></html>");
1075     }
1076 }
1077