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 */ 62 package java.time.format; 63 64 import android.icu.impl.ICUData; 65 import android.icu.impl.ICUResourceBundle; 66 import android.icu.util.UResourceBundle; 67 68 import static java.time.temporal.ChronoField.AMPM_OF_DAY; 69 import static java.time.temporal.ChronoField.DAY_OF_WEEK; 70 import static java.time.temporal.ChronoField.ERA; 71 import static java.time.temporal.ChronoField.MONTH_OF_YEAR; 72 73 import java.time.chrono.Chronology; 74 import java.time.chrono.IsoChronology; 75 import java.time.chrono.JapaneseChronology; 76 import java.time.temporal.ChronoField; 77 import java.time.temporal.IsoFields; 78 import java.time.temporal.TemporalField; 79 import java.util.AbstractMap.SimpleImmutableEntry; 80 import java.util.ArrayList; 81 import java.util.Calendar; 82 import java.util.Collections; 83 import java.util.Comparator; 84 import java.util.HashMap; 85 import java.util.Iterator; 86 import java.util.List; 87 import java.util.Locale; 88 import java.util.Map; 89 import java.util.Map.Entry; 90 import java.util.concurrent.ConcurrentHashMap; 91 import java.util.concurrent.ConcurrentMap; 92 93 import 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 */ 104 class 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 DateTimeTextProvider()116 DateTimeTextProvider() {} 117 118 /** 119 * Gets the provider of text. 120 * 121 * @return the provider, not null 122 */ getInstance()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 */ getText(TemporalField field, long value, TextStyle style, Locale locale)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 */ getText(Chronology chrono, TemporalField field, long value, TextStyle style, Locale locale)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 */ getTextIterator(TemporalField field, TextStyle style, Locale locale)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 */ getTextIterator(Chronology chrono, TemporalField field, TextStyle style, Locale locale)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 findStore(TemporalField field, Locale locale)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 toWeekDay(int calWeekDay)320 private static int toWeekDay(int calWeekDay) { 321 if (calWeekDay == Calendar.SUNDAY) { 322 return 7; 323 } else { 324 return calWeekDay - 1; 325 } 326 } 327 createStore(TemporalField field, Locale locale)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 extractQuarters(ICUResourceBundle rb, String key)451 private static Map<Long, String> extractQuarters(ICUResourceBundle rb, String key) { 452 String[] names = rb.getWithFallback(key).getStringArray(); 453 Map<Long, String> map = new HashMap<>(); 454 for (int q = 0; q < names.length; q++) { 455 map.put((long) (q + 1), names[q]); 456 } 457 return map; 458 } 459 460 /** 461 * Helper method to create an immutable entry. 462 * 463 * @param text the text, not null 464 * @param field the field, not null 465 * @return the entry, not null 466 */ createEntry(A text, B field)467 private static <A, B> Entry<A, B> createEntry(A text, B field) { 468 return new SimpleImmutableEntry<>(text, field); 469 } 470 471 // Android-changed: removed getLocalizedResource. 472 473 /** 474 * Stores the text for a single locale. 475 * <p> 476 * Some fields have a textual representation, such as day-of-week or month-of-year. 477 * These textual representations can be captured in this class for printing 478 * and parsing. 479 * <p> 480 * This class is immutable and thread-safe. 481 */ 482 static final class LocaleStore { 483 /** 484 * Map of value to text. 485 */ 486 private final Map<TextStyle, Map<Long, String>> valueTextMap; 487 /** 488 * Parsable data. 489 */ 490 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 491 492 /** 493 * Constructor. 494 * 495 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 496 */ LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)497 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 498 this.valueTextMap = valueTextMap; 499 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>(); 500 List<Entry<String, Long>> allList = new ArrayList<>(); 501 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) { 502 Map<String, Entry<String, Long>> reverse = new HashMap<>(); 503 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) { 504 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 505 // TODO: BUG: this has no effect 506 continue; // not parsable, try next style 507 } 508 } 509 List<Entry<String, Long>> list = new ArrayList<>(reverse.values()); 510 Collections.sort(list, COMPARATOR); 511 map.put(vtmEntry.getKey(), list); 512 allList.addAll(list); 513 map.put(null, allList); 514 } 515 Collections.sort(allList, COMPARATOR); 516 this.parsable = map; 517 } 518 519 /** 520 * Gets the text for the specified field value, locale and style 521 * for the purpose of printing. 522 * 523 * @param value the value to get text for, not null 524 * @param style the style to get text for, not null 525 * @return the text for the field value, null if no text found 526 */ getText(long value, TextStyle style)527 String getText(long value, TextStyle style) { 528 Map<Long, String> map = valueTextMap.get(style); 529 return map != null ? map.get(value) : null; 530 } 531 532 /** 533 * Gets an iterator of text to field for the specified style for the purpose of parsing. 534 * <p> 535 * The iterator must be returned in order from the longest text to the shortest. 536 * 537 * @param style the style to get text for, null for all parsable text 538 * @return the iterator of text to field pairs, in order from longest text to shortest text, 539 * null if the style is not parsable 540 */ getTextIterator(TextStyle style)541 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 542 List<Entry<String, Long>> list = parsable.get(style); 543 return list != null ? list.iterator() : null; 544 } 545 } 546 } 547