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.text.DateFormatSymbols; 65 import android.icu.util.ULocale; 66 67 import static java.time.temporal.ChronoField.AMPM_OF_DAY; 68 import static java.time.temporal.ChronoField.DAY_OF_WEEK; 69 import static java.time.temporal.ChronoField.ERA; 70 import static java.time.temporal.ChronoField.MONTH_OF_YEAR; 71 72 import com.android.icu.text.ExtendedDateFormatSymbols; 73 74 import java.time.chrono.Chronology; 75 import java.time.chrono.IsoChronology; 76 import java.time.chrono.JapaneseChronology; 77 import java.time.temporal.ChronoField; 78 import java.time.temporal.IsoFields; 79 import java.time.temporal.TemporalField; 80 import java.util.AbstractMap.SimpleImmutableEntry; 81 import java.util.ArrayList; 82 import java.util.Calendar; 83 import java.util.Collections; 84 import java.util.Comparator; 85 import java.util.HashMap; 86 import java.util.Iterator; 87 import java.util.List; 88 import java.util.Locale; 89 import java.util.Map; 90 import java.util.Map.Entry; 91 import java.util.concurrent.ConcurrentHashMap; 92 import java.util.concurrent.ConcurrentMap; 93 94 import sun.util.locale.provider.CalendarDataUtility; 95 96 /** 97 * A provider to obtain the textual form of a date-time field. 98 * 99 * @implSpec 100 * Implementations must be thread-safe. 101 * Implementations should cache the textual information. 102 * 103 * @since 1.8 104 */ 105 class DateTimeTextProvider { 106 107 /** Cache. */ 108 private static final ConcurrentMap<Entry<TemporalField, Locale>, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2); 109 /** Comparator. */ 110 private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() { 111 @Override 112 public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) { 113 return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest 114 } 115 }; 116 DateTimeTextProvider()117 DateTimeTextProvider() {} 118 119 /** 120 * Gets the provider of text. 121 * 122 * @return the provider, not null 123 */ getInstance()124 static DateTimeTextProvider getInstance() { 125 return new DateTimeTextProvider(); 126 } 127 128 /** 129 * Gets the text for the specified field, locale and style 130 * for the purpose of formatting. 131 * <p> 132 * The text associated with the value is returned. 133 * The null return value should be used if there is no applicable text, or 134 * if the text would be a numeric representation of the value. 135 * 136 * @param field the field to get text for, not null 137 * @param value the field value to get text for, not null 138 * @param style the style to get text for, not null 139 * @param locale the locale to get text for, not null 140 * @return the text for the field value, null if no text found 141 */ getText(TemporalField field, long value, TextStyle style, Locale locale)142 public String getText(TemporalField field, long value, TextStyle style, Locale locale) { 143 Object store = findStore(field, locale); 144 if (store instanceof LocaleStore) { 145 return ((LocaleStore) store).getText(value, style); 146 } 147 return null; 148 } 149 150 /** 151 * Gets the text for the specified chrono, field, locale and style 152 * for the purpose of formatting. 153 * <p> 154 * The text associated with the value is returned. 155 * The null return value should be used if there is no applicable text, or 156 * if the text would be a numeric representation of the value. 157 * 158 * @param chrono the Chronology to get text for, not null 159 * @param field the field to get text for, not null 160 * @param value the field value to get text for, not null 161 * @param style the style to get text for, not null 162 * @param locale the locale to get text for, not null 163 * @return the text for the field value, null if no text found 164 */ getText(Chronology chrono, TemporalField field, long value, TextStyle style, Locale locale)165 public String getText(Chronology chrono, TemporalField field, long value, 166 TextStyle style, Locale locale) { 167 if (chrono == IsoChronology.INSTANCE 168 || !(field instanceof ChronoField)) { 169 return getText(field, value, style, locale); 170 } 171 172 int fieldIndex; 173 int fieldValue; 174 if (field == ERA) { 175 fieldIndex = Calendar.ERA; 176 if (chrono == JapaneseChronology.INSTANCE) { 177 if (value == -999) { 178 fieldValue = 0; 179 } else { 180 fieldValue = (int) value + 2; 181 } 182 } else { 183 fieldValue = (int) value; 184 } 185 } else if (field == MONTH_OF_YEAR) { 186 fieldIndex = Calendar.MONTH; 187 fieldValue = (int) value - 1; 188 } else if (field == DAY_OF_WEEK) { 189 fieldIndex = Calendar.DAY_OF_WEEK; 190 fieldValue = (int) value + 1; 191 if (fieldValue > 7) { 192 fieldValue = Calendar.SUNDAY; 193 } 194 } else if (field == AMPM_OF_DAY) { 195 fieldIndex = Calendar.AM_PM; 196 fieldValue = (int) value; 197 } else { 198 return null; 199 } 200 return CalendarDataUtility.retrieveJavaTimeFieldValueName( 201 chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale); 202 } 203 204 /** 205 * Gets an iterator of text to field for the specified field, locale and style 206 * for the purpose of parsing. 207 * <p> 208 * The iterator must be returned in order from the longest text to the shortest. 209 * <p> 210 * The null return value should be used if there is no applicable parsable text, or 211 * if the text would be a numeric representation of the value. 212 * Text can only be parsed if all the values for that field-style-locale combination are unique. 213 * 214 * @param field the field to get text for, not null 215 * @param style the style to get text for, null for all parsable text 216 * @param locale the locale to get text for, not null 217 * @return the iterator of text to field pairs, in order from longest text to shortest text, 218 * null if the field or style is not parsable 219 */ getTextIterator(TemporalField field, TextStyle style, Locale locale)220 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) { 221 Object store = findStore(field, locale); 222 if (store instanceof LocaleStore) { 223 return ((LocaleStore) store).getTextIterator(style); 224 } 225 return null; 226 } 227 228 /** 229 * Gets an iterator of text to field for the specified chrono, field, locale and style 230 * for the purpose of parsing. 231 * <p> 232 * The iterator must be returned in order from the longest text to the shortest. 233 * <p> 234 * The null return value should be used if there is no applicable parsable text, or 235 * if the text would be a numeric representation of the value. 236 * Text can only be parsed if all the values for that field-style-locale combination are unique. 237 * 238 * @param chrono the Chronology to get text for, not null 239 * @param field the field to get text for, not null 240 * @param style the style to get text for, null for all parsable text 241 * @param locale the locale to get text for, not null 242 * @return the iterator of text to field pairs, in order from longest text to shortest text, 243 * null if the field or style is not parsable 244 */ getTextIterator(Chronology chrono, TemporalField field, TextStyle style, Locale locale)245 public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, TemporalField field, 246 TextStyle style, Locale locale) { 247 if (chrono == IsoChronology.INSTANCE 248 || !(field instanceof ChronoField)) { 249 return getTextIterator(field, style, locale); 250 } 251 252 int fieldIndex; 253 switch ((ChronoField)field) { 254 case ERA: 255 fieldIndex = Calendar.ERA; 256 break; 257 case MONTH_OF_YEAR: 258 fieldIndex = Calendar.MONTH; 259 break; 260 case DAY_OF_WEEK: 261 fieldIndex = Calendar.DAY_OF_WEEK; 262 break; 263 case AMPM_OF_DAY: 264 fieldIndex = Calendar.AM_PM; 265 break; 266 default: 267 return null; 268 } 269 270 int calendarStyle = (style == null) ? Calendar.ALL_STYLES : style.toCalendarStyle(); 271 Map<String, Integer> map = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 272 chrono.getCalendarType(), fieldIndex, calendarStyle, locale); 273 if (map == null) { 274 return null; 275 } 276 List<Entry<String, Long>> list = new ArrayList<>(map.size()); 277 switch (fieldIndex) { 278 case Calendar.ERA: 279 for (Map.Entry<String, Integer> entry : map.entrySet()) { 280 int era = entry.getValue(); 281 if (chrono == JapaneseChronology.INSTANCE) { 282 if (era == 0) { 283 era = -999; 284 } else { 285 era -= 2; 286 } 287 } 288 list.add(createEntry(entry.getKey(), (long)era)); 289 } 290 break; 291 case Calendar.MONTH: 292 for (Map.Entry<String, Integer> entry : map.entrySet()) { 293 list.add(createEntry(entry.getKey(), (long)(entry.getValue() + 1))); 294 } 295 break; 296 case Calendar.DAY_OF_WEEK: 297 for (Map.Entry<String, Integer> entry : map.entrySet()) { 298 list.add(createEntry(entry.getKey(), (long)toWeekDay(entry.getValue()))); 299 } 300 break; 301 default: 302 for (Map.Entry<String, Integer> entry : map.entrySet()) { 303 list.add(createEntry(entry.getKey(), (long)entry.getValue())); 304 } 305 break; 306 } 307 return list.iterator(); 308 } 309 findStore(TemporalField field, Locale locale)310 private Object findStore(TemporalField field, Locale locale) { 311 Entry<TemporalField, Locale> key = createEntry(field, locale); 312 Object store = CACHE.get(key); 313 if (store == null) { 314 store = createStore(field, locale); 315 CACHE.putIfAbsent(key, store); 316 store = CACHE.get(key); 317 } 318 return store; 319 } 320 toWeekDay(int calWeekDay)321 private static int toWeekDay(int calWeekDay) { 322 if (calWeekDay == Calendar.SUNDAY) { 323 return 7; 324 } else { 325 return calWeekDay - 1; 326 } 327 } 328 createStore(TemporalField field, Locale locale)329 private Object createStore(TemporalField field, Locale locale) { 330 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>(); 331 if (field == ERA) { 332 for (TextStyle textStyle : TextStyle.values()) { 333 if (textStyle.isStandalone()) { 334 // Stand-alone isn't applicable to era names. 335 continue; 336 } 337 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 338 "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale); 339 if (displayNames != null) { 340 Map<Long, String> map = new HashMap<>(); 341 for (Entry<String, Integer> entry : displayNames.entrySet()) { 342 map.put((long) entry.getValue(), entry.getKey()); 343 } 344 if (!map.isEmpty()) { 345 styleMap.put(textStyle, map); 346 } 347 } 348 } 349 return new LocaleStore(styleMap); 350 } 351 352 if (field == MONTH_OF_YEAR) { 353 for (TextStyle textStyle : TextStyle.values()) { 354 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 355 "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale); 356 Map<Long, String> map = new HashMap<>(); 357 if (displayNames != null) { 358 for (Entry<String, Integer> entry : displayNames.entrySet()) { 359 map.put((long) (entry.getValue() + 1), entry.getKey()); 360 } 361 362 } else { 363 // Narrow names may have duplicated names, such as "J" for January, Jun, July. 364 // Get names one by one in that case. 365 for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 366 String name; 367 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 368 "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale); 369 if (name == null) { 370 break; 371 } 372 map.put((long) (month + 1), name); 373 } 374 } 375 if (!map.isEmpty()) { 376 styleMap.put(textStyle, map); 377 } 378 } 379 return new LocaleStore(styleMap); 380 } 381 382 if (field == DAY_OF_WEEK) { 383 for (TextStyle textStyle : TextStyle.values()) { 384 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 385 "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale); 386 Map<Long, String> map = new HashMap<>(); 387 if (displayNames != null) { 388 for (Entry<String, Integer> entry : displayNames.entrySet()) { 389 map.put((long)toWeekDay(entry.getValue()), entry.getKey()); 390 } 391 392 } else { 393 // Narrow names may have duplicated names, such as "S" for Sunday and Saturday. 394 // Get names one by one in that case. 395 for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) { 396 String name; 397 name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 398 "gregory", Calendar.DAY_OF_WEEK, wday, textStyle.toCalendarStyle(), locale); 399 if (name == null) { 400 break; 401 } 402 map.put((long)toWeekDay(wday), name); 403 } 404 } 405 if (!map.isEmpty()) { 406 styleMap.put(textStyle, map); 407 } 408 } 409 return new LocaleStore(styleMap); 410 } 411 412 if (field == AMPM_OF_DAY) { 413 for (TextStyle textStyle : TextStyle.values()) { 414 if (textStyle.isStandalone()) { 415 // Stand-alone isn't applicable to AM/PM. 416 continue; 417 } 418 Map<String, Integer> displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 419 "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale); 420 if (displayNames != null) { 421 Map<Long, String> map = new HashMap<>(); 422 for (Entry<String, Integer> entry : displayNames.entrySet()) { 423 map.put((long) entry.getValue(), entry.getKey()); 424 } 425 if (!map.isEmpty()) { 426 styleMap.put(textStyle, map); 427 } 428 } 429 } 430 return new LocaleStore(styleMap); 431 } 432 433 if (field == IsoFields.QUARTER_OF_YEAR) { 434 // BEGIN Android-changed: Use ICU resources. 435 /* 436 // The order of keys must correspond to the TextStyle.values() order. 437 final String[] keys = { 438 "QuarterNames", 439 "standalone.QuarterNames", 440 "QuarterAbbreviations", 441 "standalone.QuarterAbbreviations", 442 "QuarterNarrows", 443 "standalone.QuarterNarrows", 444 }; 445 for (int i = 0; i < keys.length; i++) { 446 String[] names = getLocalizedResource(keys[i], locale); 447 if (names != null) { 448 Map<Long, String> map = new HashMap<>(); 449 for (int q = 0; q < names.length; q++) { 450 map.put((long) (q + 1), names[q]); 451 } 452 styleMap.put(TextStyle.values()[i], map); 453 } 454 } 455 */ 456 ULocale uLocale = ULocale.forLocale(locale); 457 // TODO: Figure why we forced Gregorian calendar in the first patch in 458 // https://r.android.com/311224 459 uLocale.setKeywordValue("calendar", "gregorian"); 460 ExtendedDateFormatSymbols extendedDfs = ExtendedDateFormatSymbols.getInstance(uLocale); 461 DateFormatSymbols dfs = extendedDfs.getDateFormatSymbols(); 462 styleMap.put(TextStyle.FULL, extractQuarters( 463 dfs.getQuarters(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE))); 464 styleMap.put(TextStyle.FULL_STANDALONE, extractQuarters( 465 dfs.getQuarters(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE))); 466 styleMap.put(TextStyle.SHORT, extractQuarters( 467 dfs.getQuarters(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED))); 468 styleMap.put(TextStyle.SHORT_STANDALONE, extractQuarters( 469 dfs.getQuarters(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED))); 470 styleMap.put(TextStyle.NARROW, extractQuarters( 471 extendedDfs.getNarrowQuarters(DateFormatSymbols.FORMAT))); 472 styleMap.put(TextStyle.NARROW_STANDALONE, extractQuarters( 473 extendedDfs.getNarrowQuarters(DateFormatSymbols.STANDALONE))); 474 475 // END Android-changed: Use ICU resources. 476 return new LocaleStore(styleMap); 477 } 478 479 return ""; // null marker for map 480 } 481 482 // BEGIN Android-added: Extracts a Map of quarter names. extractQuarters(String[] quarters)483 private static Map<Long, String> extractQuarters(String[] quarters) { 484 Map<Long, String> map = new HashMap<>(); 485 for (int q = 0; q < quarters.length; q++) { 486 map.put((long) (q + 1), quarters[q]); 487 } 488 return map; 489 } 490 // END Android-added: Extracts a Map of quarter names. 491 492 /** 493 * Helper method to create an immutable entry. 494 * 495 * @param text the text, not null 496 * @param field the field, not null 497 * @return the entry, not null 498 */ createEntry(A text, B field)499 private static <A, B> Entry<A, B> createEntry(A text, B field) { 500 return new SimpleImmutableEntry<>(text, field); 501 } 502 503 // BEGIN Android-removed: Android uses ICU resources and has no LocaleProviderAdapter. 504 /** 505 * Returns the localized resource of the given key and locale, or null 506 * if no localized resource is available. 507 * 508 * @param key the key of the localized resource, not null 509 * @param locale the locale, not null 510 * @return the localized resource, or null if not available 511 * @throws NullPointerException if key or locale is null 512 */ 513 // @SuppressWarnings("unchecked") 514 // static <T> T getLocalizedResource(String key, Locale locale) { 515 // LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased() 516 // .getLocaleResources(locale); 517 // ResourceBundle rb = lr.getJavaTimeFormatData(); 518 // return rb.containsKey(key) ? (T) rb.getObject(key) : null; 519 // } 520 // END Android-removed: Android uses ICU resources and has no LocaleProviderAdapter. 521 522 /** 523 * Stores the text for a single locale. 524 * <p> 525 * Some fields have a textual representation, such as day-of-week or month-of-year. 526 * These textual representations can be captured in this class for printing 527 * and parsing. 528 * <p> 529 * This class is immutable and thread-safe. 530 */ 531 static final class LocaleStore { 532 /** 533 * Map of value to text. 534 */ 535 private final Map<TextStyle, Map<Long, String>> valueTextMap; 536 /** 537 * Parsable data. 538 */ 539 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 540 541 /** 542 * Constructor. 543 * 544 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 545 */ LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)546 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 547 this.valueTextMap = valueTextMap; 548 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<>(); 549 List<Entry<String, Long>> allList = new ArrayList<>(); 550 for (Map.Entry<TextStyle, Map<Long, String>> vtmEntry : valueTextMap.entrySet()) { 551 Map<String, Entry<String, Long>> reverse = new HashMap<>(); 552 for (Map.Entry<Long, String> entry : vtmEntry.getValue().entrySet()) { 553 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 554 // TODO: BUG: this has no effect 555 continue; // not parsable, try next style 556 } 557 } 558 List<Entry<String, Long>> list = new ArrayList<>(reverse.values()); 559 Collections.sort(list, COMPARATOR); 560 map.put(vtmEntry.getKey(), list); 561 allList.addAll(list); 562 map.put(null, allList); 563 } 564 Collections.sort(allList, COMPARATOR); 565 this.parsable = map; 566 } 567 568 /** 569 * Gets the text for the specified field value, locale and style 570 * for the purpose of printing. 571 * 572 * @param value the value to get text for, not null 573 * @param style the style to get text for, not null 574 * @return the text for the field value, null if no text found 575 */ getText(long value, TextStyle style)576 String getText(long value, TextStyle style) { 577 Map<Long, String> map = valueTextMap.get(style); 578 return map != null ? map.get(value) : null; 579 } 580 581 /** 582 * Gets an iterator of text to field for the specified style for the purpose of parsing. 583 * <p> 584 * The iterator must be returned in order from the longest text to the shortest. 585 * 586 * @param style the style to get text for, null for all parsable text 587 * @return the iterator of text to field pairs, in order from longest text to shortest text, 588 * null if the style is not parsable 589 */ getTextIterator(TextStyle style)590 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 591 List<Entry<String, Long>> list = parsable.get(style); 592 return list != null ? list.iterator() : null; 593 } 594 } 595 } 596