1/* 2 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26/* 27 * This file is available under and governed by the GNU General Public 28 * License version 2 only, as published by the Free Software Foundation. 29 * However, the following notice accompanied the original version of this 30 * file: 31 * 32 * Copyright (c) 2011-2012, Stephen Colebourne & Michael Nascimento Santos 33 * 34 * All rights reserved. 35 * 36 * Redistribution and use in source and binary forms, with or without 37 * modification, are permitted provided that the following conditions are met: 38 * 39 * * Redistributions of source code must retain the above copyright notice, 40 * this list of conditions and the following disclaimer. 41 * 42 * * Redistributions in binary form must reproduce the above copyright notice, 43 * this list of conditions and the following disclaimer in the documentation 44 * and/or other materials provided with the distribution. 45 * 46 * * Neither the name of JSR-310 nor the names of its contributors 47 * may be used to endorse or promote products derived from this software 48 * without specific prior written permission. 49 * 50 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 51 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 52 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 53 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 54 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 55 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 56 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 57 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 58 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 59 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 60 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 61 */ 62package java.time.format; 63 64import android.icu.impl.ICUData; 65import android.icu.impl.ICUResourceBundle; 66import android.icu.util.UResourceBundle; 67 68import static java.time.temporal.ChronoField.AMPM_OF_DAY; 69import static java.time.temporal.ChronoField.DAY_OF_WEEK; 70import static java.time.temporal.ChronoField.ERA; 71import static java.time.temporal.ChronoField.MONTH_OF_YEAR; 72 73import java.time.chrono.Chronology; 74import java.time.chrono.IsoChronology; 75import java.time.chrono.JapaneseChronology; 76import java.time.temporal.ChronoField; 77import java.time.temporal.IsoFields; 78import java.time.temporal.TemporalField; 79import java.util.AbstractMap.SimpleImmutableEntry; 80import java.util.ArrayList; 81import java.util.Calendar; 82import java.util.Collections; 83import java.util.Comparator; 84import java.util.HashMap; 85import java.util.Iterator; 86import java.util.List; 87import java.util.Locale; 88import java.util.Map; 89import java.util.Map.Entry; 90import java.util.concurrent.ConcurrentHashMap; 91import java.util.concurrent.ConcurrentMap; 92 93import sun.util.locale.provider.CalendarDataUtility; 94 95/** 96 * A provider to obtain the textual form of a date-time field. 97 * 98 * @implSpec 99 * Implementations must be thread-safe. 100 * Implementations should cache the textual information. 101 * 102 * @since 1.8 103 */ 104class DateTimeTextProvider { 105 106 /** Cache. */ 107 private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2); 108 /** Comparator. */ 109 private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() { 110 @Override 111 public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) { 112 return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest 113 } 114 }; 115 116 DateTimeTextProvider() {} 117 118 /** 119 * Gets the provider of text. 120 * 121 * @return the provider, not null 122 */ 123 static DateTimeTextProvider getInstance() { 124 return new DateTimeTextProvider(); 125 } 126 127 /** 128 * Gets the text for the specified field, locale and style 129 * for the purpose of formatting. 130 * <p> 131 * The text associated with the value is returned. 132 * The null return value should be used if there is no applicable text, or 133 * if the text would be a numeric representation of the value. 134 * 135 * @param field the field to get text for, not null 136 * @param value the field value to get text for, not null 137 * @param style the style to get text for, not null 138 * @param locale the locale to get text for, not null 139 * @return the text for the field value, null if no text found 140 */ 141 public String getText(TemporalField field, long value, TextStyle style, Locale locale) { 142 Object store = findStore(field, locale); 143 if (store instanceof LocaleStore) { 144 return ((LocaleStore) store).getText(value, style); 145 } 146 return null; 147 } 148 149 /** 150 * Gets the text for the specified chrono, field, locale and style 151 * for the purpose of formatting. 152 * <p> 153 * The text associated with the value is returned. 154 * The null return value should be used if there is no applicable text, or 155 * if the text would be a numeric representation of the value. 156 * 157 * @param chrono the Chronology to get text for, not null 158 * @param field the field to get text for, not null 159 * @param value the field value to get text for, not null 160 * @param style the style to get text for, not null 161 * @param locale the locale to get text for, not null 162 * @return the text for the field value, null if no text found 163 */ 164 public String getText(Chronology chrono, TemporalField field, long value, 165 TextStyle style, Locale locale) { 166 if (chrono == IsoChronology.INSTANCE 167 || !(field instanceof ChronoField)) { 168 return getText(field, value, style, locale); 169 } 170 171 int fieldIndex; 172 int fieldValue; 173 if (field == ERA) { 174 fieldIndex = Calendar.ERA; 175 if (chrono == JapaneseChronology.INSTANCE) { 176 if (value == -999) { 177 fieldValue = 0; 178 } else { 179 fieldValue = (int) value + 2; 180 } 181 } else { 182 fieldValue = (int) value; 183 } 184 } else if (field == MONTH_OF_YEAR) { 185 fieldIndex = Calendar.MONTH; 186 fieldValue = (int) value - 1; 187 } else if (field == DAY_OF_WEEK) { 188 fieldIndex = Calendar.DAY_OF_WEEK; 189 fieldValue = (int) value + 1; 190 if (fieldValue > 7) { 191 fieldValue = Calendar.SUNDAY; 192 } 193 } else if (field == AMPM_OF_DAY) { 194 fieldIndex = Calendar.AM_PM; 195 fieldValue = (int) value; 196 } else { 197 return null; 198 } 199 return CalendarDataUtility.retrieveJavaTimeFieldValueName( 200 chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale); 201 } 202 203 /** 204 * Gets an iterator of text to field for the specified field, locale and style 205 * for the purpose of parsing. 206 * <p> 207 * The iterator must be returned in order from the longest text to the shortest. 208 * <p> 209 * The null return value should be used if there is no applicable parsable text, or 210 * if the text would be a numeric representation of the value. 211 * Text can only be parsed if all the values for that field-style-locale combination are unique. 212 * 213 * @param field the field to get text for, not null 214 * @param style the style to get text for, null for all parsable text 215 * @param locale the locale to get text for, not null 216 * @return the iterator of text to field pairs, in order from longest text to shortest text, 217 * null if the field or style is not parsable 218 */ 219 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) { 220 Object store = findStore(field, locale); 221 if (store instanceof LocaleStore) { 222 return ((LocaleStore) store).getTextIterator(style); 223 } 224 return null; 225 } 226 227 /** 228 * Gets an iterator of text to field for the specified chrono, field, locale and style 229 * for the purpose of parsing. 230 * <p> 231 * The iterator must be returned in order from the longest text to the shortest. 232 * <p> 233 * The null return value should be used if there is no applicable parsable text, or 234 * if the text would be a numeric representation of the value. 235 * Text can only be parsed if all the values for that field-style-locale combination are unique. 236 * 237 * @param chrono the Chronology to get text for, not null 238 * @param field the field to get text for, not null 239 * @param style the style to get text for, null for all parsable text 240 * @param locale the locale to get text for, not null 241 * @return the iterator of text to field pairs, in order from longest text to shortest text, 242 * null if the field or style is not parsable 243 */ 244 public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field, 245 TextStyle style, Locale locale) { 246 if (chrono == IsoChronology.INSTANCE 247 || !(field instanceof ChronoField)) { 248 return getTextIterator(field, style, locale); 249 } 250 251 int fieldIndex; 252 switch ((ChronoField)field) { 253 case ERA: 254 fieldIndex = Calendar.ERA; 255 break; 256 case MONTH_OF_YEAR: 257 fieldIndex = Calendar.MONTH; 258 break; 259 case DAY_OF_WEEK: 260 fieldIndex = Calendar.DAY_OF_WEEK; 261 break; 262 case AMPM_OF_DAY: 263 fieldIndex = Calendar.AM_PM; 264 break; 265 default: 266 return null; 267 } 268 269 int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle(); 270 Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 271 chrono.getCalendarType(), fieldIndex, calendarStyle, locale); 272 if (map == null) { 273 return null; 274 } 275 List<Entry<String, Long>> list = new ArrayList<>(map.size()); 276 switch (fieldIndex) { 277 case Calendar.ERA: 278 for (Map.Entry<String, Integer> entry : map.entrySet()) { 279 int era = entry.getValue(); 280 if (chrono == JapaneseChronology.INSTANCE) { 281 if (era == 0) { 282 era = -999; 283 } else { 284 era -= 2; 285 } 286 } 287 list.add(createEntry(entry.getKey(), (long)era)); 288 } 289 break; 290 case Calendar.MONTH: 291 for (Map.Entry<String, Integer> entry : map.entrySet()) { 292 list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1))); 293 } 294 break; 295 case Calendar.DAY_OF_WEEK: 296 for (Map.Entry<String, Integer> entry : map.entrySet()) { 297 list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue()))); 298 } 299 break; 300 default: 301 for (Map.Entry<String, Integer> entry : map.entrySet()) { 302 list.add(createEntry(entry.getKey(), (long)entry.getValue())); 303 } 304 break; 305 } 306 return list.iterator(); 307 } 308 309 private Object findStore(TemporalField field, Locale locale) { 310 Entry<TemporalField, Locale> key = createEntry(field, locale); 311 Object store = CACHE.get(key); 312 if (store == null) { 313 store = createStore(field, locale); 314 CACHE.putIfAbsent(key, store); 315 store = CACHE.get(key); 316 } 317 return store; 318 } 319 320 private static int toWeekDay(int calWeekDay) { 321 if (calWeekDay == Calendar.SUNDAY) { 322 return 7; 323 } else { 324 return calWeekDay - 1; 325 } 326 } 327 328 private Object createStore(TemporalField field, Locale locale) { 329 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>(); 330 if (field == ERA) { 331 for (TextStyle textStyle : TextStyle.values()) { 332 if (textStyle.isStandalone()) { 333 // Stand-alone isn't applicable to era names. 334 continue; 335 } 336 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 337 "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale); 338 if (displayNames != null) { 339 Map<Long, String> map = new HashMap<>(); 340 for (Entry<String, Integer> entry : displayNames.entrySet()) { 341 map.put((long) entry.getValue(), entry.getKey()); 342 } 343 if (!map.isEmpty()) { 344 styleMap.put(textStyle, map); 345 } 346 } 347 } 348 return new LocaleStore(styleMap); 349 } 350 351 if (field == MONTH_OF_YEAR) { 352 for (TextStyle textStyle : TextStyle.values()) { 353 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 354 "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale); 355 Map<Long, String> map = new HashMap<>(); 356 if (displayNames != null) { 357 for (Entry<String, Integer> entry : displayNames.entrySet()) { 358 map.put((long) (entry.getValue() + 1), entry.getKey()); 359 } 360 361 } else { 362 // Narrow names may have duplicated names, such as "J" for January, Jun, July. 363 // Get names one by one in that case. 364 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 365 String name; 366 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 367 "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale); 368 if (name == null) { 369 break; 370 } 371 map.put((long) (month + 1), name); 372 } 373 } 374 if (!map.isEmpty()) { 375 styleMap.put(textStyle, map); 376 } 377 } 378 return new LocaleStore(styleMap); 379 } 380 381 if (field == DAY_OF_WEEK) { 382 for (TextStyle textStyle : TextStyle.values()) { 383 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 384 "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale); 385 Map<Long, String> map = new HashMap<>(); 386 if (displayNames != null) { 387 for (Entry<String, Integer> entry : displayNames.entrySet()) { 388 map.put((long)toWeekDay(entry.getValue()), entry.getKey()); 389 } 390 391 } else { 392 // Narrow names may have duplicated names, such as "S" for Sunday and Saturday. 393 // Get names one by one in that case. 394 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) { 395 String name; 396 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 397 "gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale); 398 if (name == null) { 399 break; 400 } 401 map.put((long)toWeekDay(wday), name); 402 } 403 } 404 if (!map.isEmpty()) { 405 styleMap.put(textStyle, map); 406 } 407 } 408 return new LocaleStore(styleMap); 409 } 410 411 if (field == AMPM_OF_DAY) { 412 for (TextStyle textStyle : TextStyle.values()) { 413 if (textStyle.isStandalone()) { 414 // Stand-alone isn't applicable to AM/PM. 415 continue; 416 } 417 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 418 "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale); 419 if (displayNames != null) { 420 Map<Long, String> map = new HashMap<>(); 421 for (Entry<String, Integer> entry : displayNames.entrySet()) { 422 map.put((long) entry.getValue(), entry.getKey()); 423 } 424 if (!map.isEmpty()) { 425 styleMap.put(textStyle, map); 426 } 427 } 428 } 429 return new LocaleStore(styleMap); 430 } 431 432 if (field == IsoFields.QUARTER_OF_YEAR) { 433 // Android-changed: Use ICU resources. 434 ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle 435 .getBundleInstance(ICUData.ICU_BASE_NAME, locale); 436 ICUResourceBundle quartersRb = rb.getWithFallback("calendar/gregorian/quarters"); 437 ICUResourceBundle formatRb = quartersRb.getWithFallback("format"); 438 ICUResourceBundle standaloneRb = quartersRb.getWithFallback("stand-alone"); 439 styleMap.put(TextStyle.FULL, extractQuarters(formatRb, "wide")); 440 styleMap.put(TextStyle.FULL_STANDALONE, extractQuarters(standaloneRb, "wide")); 441 styleMap.put(TextStyle.SHORT, extractQuarters(formatRb, "abbreviated")); 442 styleMap.put(TextStyle.SHORT_STANDALONE, extractQuarters(standaloneRb, "abbreviated")); 443 styleMap.put(TextStyle.NARROW, extractQuarters(formatRb, "narrow")); 444 styleMap.put(TextStyle.NARROW_STANDALONE, extractQuarters(standaloneRb, "narrow")); 445 return new LocaleStore(styleMap); 446 } 447 448 return ""; // null marker for map 449 } 450 451 // Android-added: extractQuarters to extract Map of quarter names from ICU resource bundle. 452 private static Map<Long, String> extractQuarters(ICUResourceBundle rb, String key) { 453 String[] names = rb.getWithFallback(key).getStringArray(); 454 Map<Long, String> map = new HashMap<>(); 455 for (int q = 0; q < names.length; q++) { 456 map.put((long) (q + 1), names[q]); 457 } 458 return map; 459 } 460 461 /** 462 * Helper method to create an immutable entry. 463 * 464 * @param text the text, not null 465 * @param field the field, not null 466 * @return the entry, not null 467 */ 468 private static <A, B> Entry<A, B> createEntry(A text, B field) { 469 return new SimpleImmutableEntry<>(text, field); 470 } 471 472 // Android-removed: unused helper method getLocalizedResource. 473 474 /** 475 * Stores the text for a single locale. 476 * <p> 477 * Some fields have a textual representation, such as day-of-week or month-of-year. 478 * These textual representations can be captured in this class for printing 479 * and parsing. 480 * <p> 481 * This class is immutable and thread-safe. 482 */ 483 static final class LocaleStore { 484 /** 485 * Map of value to text. 486 */ 487 private final Map<TextStyle, Map<Long, String>> valueTextMap; 488 /** 489 * Parsable data. 490 */ 491 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 492 493 /** 494 * Constructor. 495 * 496 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 497 */ 498 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 499 this.valueTextMap = valueTextMap; 500 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>(); 501 List<Entry<String, Long>> allList = new ArrayList<>(); 502 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) { 503 Map<String, Entry<String, Long>> reverse = new HashMap<>(); 504 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) { 505 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 506 // TODO: BUG: this has no effect 507 continue; // not parsable, try next style 508 } 509 } 510 List<Entry<String, Long>> list = new ArrayList<>(reverse.values()); 511 Collections.sort(list, COMPARATOR); 512 map.put(vtmEntry.getKey(), list); 513 allList.addAll(list); 514 map.put(null, allList); 515 } 516 Collections.sort(allList, COMPARATOR); 517 this.parsable = map; 518 } 519 520 /** 521 * Gets the text for the specified field value, locale and style 522 * for the purpose of printing. 523 * 524 * @param value the value to get text for, not null 525 * @param style the style to get text for, not null 526 * @return the text for the field value, null if no text found 527 */ 528 String getText(long value, TextStyle style) { 529 Map<Long, String> map = valueTextMap.get(style); 530 return map != null ? map.get(value) : null; 531 } 532 533 /** 534 * Gets an iterator of text to field for the specified style for the purpose of parsing. 535 * <p> 536 * The iterator must be returned in order from the longest text to the shortest. 537 * 538 * @param style the style to get text for, null for all parsable text 539 * @return the iterator of text to field pairs, in order from longest text to shortest text, 540 * null if the style is not parsable 541 */ 542 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 543 List<Entry<String, Long>> list = parsable.get(style); 544 return list != null ? list.iterator() : null; 545 } 546 } 547} 548