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 = " "; 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