1 /* 2 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * * Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * * Neither the name of JSR-310 nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package org.threeten.bp.format; 33 34 import static org.threeten.bp.temporal.ChronoField.AMPM_OF_DAY; 35 import static org.threeten.bp.temporal.ChronoField.DAY_OF_WEEK; 36 import static org.threeten.bp.temporal.ChronoField.ERA; 37 import static org.threeten.bp.temporal.ChronoField.MONTH_OF_YEAR; 38 39 import java.text.DateFormatSymbols; 40 import java.util.AbstractMap.SimpleImmutableEntry; 41 import java.util.ArrayList; 42 import java.util.Calendar; 43 import java.util.Collections; 44 import java.util.Comparator; 45 import java.util.GregorianCalendar; 46 import java.util.HashMap; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Map; 51 import java.util.Map.Entry; 52 import java.util.concurrent.ConcurrentHashMap; 53 import java.util.concurrent.ConcurrentMap; 54 55 import org.threeten.bp.temporal.IsoFields; 56 import org.threeten.bp.temporal.TemporalField; 57 58 /** 59 * The Service Provider Implementation to obtain date-time text for a field. 60 * <p> 61 * This implementation is based on extraction of data from a {@link DateFormatSymbols}. 62 * 63 * <h3>Specification for implementors</h3> 64 * This class is immutable and thread-safe. 65 */ 66 final class SimpleDateTimeTextProvider extends DateTimeTextProvider { 67 // TODO: Better implementation based on CLDR 68 69 /** Comparator. */ 70 private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() { 71 @Override 72 public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) { 73 return obj2.getKey().length() - obj1.getKey().length(); // longest to shortest 74 } 75 }; 76 77 /** Cache. */ 78 private final ConcurrentMap<Entry<TemporalField, Locale>, Object> cache = 79 new ConcurrentHashMap<Entry<TemporalField, Locale>, Object>(16, 0.75f, 2); 80 81 //----------------------------------------------------------------------- 82 @Override getText(TemporalField field, long value, TextStyle style, Locale locale)83 public String getText(TemporalField field, long value, TextStyle style, Locale locale) { 84 Object store = findStore(field, locale); 85 if (store instanceof LocaleStore) { 86 return ((LocaleStore) store).getText(value, style); 87 } 88 return null; 89 } 90 91 @Override getTextIterator(TemporalField field, TextStyle style, Locale locale)92 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) { 93 Object store = findStore(field, locale); 94 if (store instanceof LocaleStore) { 95 return ((LocaleStore) store).getTextIterator(style); 96 } 97 return null; 98 } 99 100 //----------------------------------------------------------------------- findStore(TemporalField field, Locale locale)101 private Object findStore(TemporalField field, Locale locale) { 102 Entry<TemporalField, Locale> key = createEntry(field, locale); 103 Object store = cache.get(key); 104 if (store == null) { 105 store = createStore(field, locale); 106 cache.putIfAbsent(key, store); 107 store = cache.get(key); 108 } 109 return store; 110 } 111 createStore(TemporalField field, Locale locale)112 private Object createStore(TemporalField field, Locale locale) { 113 if (field == MONTH_OF_YEAR) { 114 DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale); 115 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>(); 116 Long f1 = 1L; 117 Long f2 = 2L; 118 Long f3 = 3L; 119 Long f4 = 4L; 120 Long f5 = 5L; 121 Long f6 = 6L; 122 Long f7 = 7L; 123 Long f8 = 8L; 124 Long f9 = 9L; 125 Long f10 = 10L; 126 Long f11 = 11L; 127 Long f12 = 12L; 128 String[] array = oldSymbols.getMonths(); 129 Map<Long, String> map = new HashMap<Long, String>(); 130 map.put(f1, array[Calendar.JANUARY]); 131 map.put(f2, array[Calendar.FEBRUARY]); 132 map.put(f3, array[Calendar.MARCH]); 133 map.put(f4, array[Calendar.APRIL]); 134 map.put(f5, array[Calendar.MAY]); 135 map.put(f6, array[Calendar.JUNE]); 136 map.put(f7, array[Calendar.JULY]); 137 map.put(f8, array[Calendar.AUGUST]); 138 map.put(f9, array[Calendar.SEPTEMBER]); 139 map.put(f10, array[Calendar.OCTOBER]); 140 map.put(f11, array[Calendar.NOVEMBER]); 141 map.put(f12, array[Calendar.DECEMBER]); 142 styleMap.put(TextStyle.FULL, map); 143 144 map = new HashMap<Long, String>(); 145 map.put(f1, narrowMonth(1, array[Calendar.JANUARY], locale)); 146 map.put(f2, narrowMonth(2, array[Calendar.FEBRUARY], locale)); 147 map.put(f3, narrowMonth(3, array[Calendar.MARCH], locale)); 148 map.put(f4, narrowMonth(4, array[Calendar.APRIL], locale)); 149 map.put(f5, narrowMonth(5, array[Calendar.MAY], locale)); 150 map.put(f6, narrowMonth(6, array[Calendar.JUNE], locale)); 151 map.put(f7, narrowMonth(7, array[Calendar.JULY], locale)); 152 map.put(f8, narrowMonth(8, array[Calendar.AUGUST], locale)); 153 map.put(f9, narrowMonth(9, array[Calendar.SEPTEMBER], locale)); 154 map.put(f10, narrowMonth(10, array[Calendar.OCTOBER], locale)); 155 map.put(f11, narrowMonth(11, array[Calendar.NOVEMBER], locale)); 156 map.put(f12, narrowMonth(12, array[Calendar.DECEMBER], locale)); 157 styleMap.put(TextStyle.NARROW, map); 158 159 array = oldSymbols.getShortMonths(); 160 map = new HashMap<Long, String>(); 161 map.put(f1, array[Calendar.JANUARY]); 162 map.put(f2, array[Calendar.FEBRUARY]); 163 map.put(f3, array[Calendar.MARCH]); 164 map.put(f4, array[Calendar.APRIL]); 165 map.put(f5, array[Calendar.MAY]); 166 map.put(f6, array[Calendar.JUNE]); 167 map.put(f7, array[Calendar.JULY]); 168 map.put(f8, array[Calendar.AUGUST]); 169 map.put(f9, array[Calendar.SEPTEMBER]); 170 map.put(f10, array[Calendar.OCTOBER]); 171 map.put(f11, array[Calendar.NOVEMBER]); 172 map.put(f12, array[Calendar.DECEMBER]); 173 styleMap.put(TextStyle.SHORT, map); 174 return createLocaleStore(styleMap); 175 } 176 if (field == DAY_OF_WEEK) { 177 DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale); 178 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>(); 179 Long f1 = 1L; 180 Long f2 = 2L; 181 Long f3 = 3L; 182 Long f4 = 4L; 183 Long f5 = 5L; 184 Long f6 = 6L; 185 Long f7 = 7L; 186 String[] array = oldSymbols.getWeekdays(); 187 Map<Long, String> map = new HashMap<Long, String>(); 188 map.put(f1, array[Calendar.MONDAY]); 189 map.put(f2, array[Calendar.TUESDAY]); 190 map.put(f3, array[Calendar.WEDNESDAY]); 191 map.put(f4, array[Calendar.THURSDAY]); 192 map.put(f5, array[Calendar.FRIDAY]); 193 map.put(f6, array[Calendar.SATURDAY]); 194 map.put(f7, array[Calendar.SUNDAY]); 195 styleMap.put(TextStyle.FULL, map); 196 197 map = new HashMap<Long, String>(); 198 map.put(f1, narrowDayOfWeek(1, array[Calendar.MONDAY], locale)); 199 map.put(f2, narrowDayOfWeek(2, array[Calendar.TUESDAY], locale)); 200 map.put(f3, narrowDayOfWeek(3, array[Calendar.WEDNESDAY], locale)); 201 map.put(f4, narrowDayOfWeek(4, array[Calendar.THURSDAY], locale)); 202 map.put(f5, narrowDayOfWeek(5, array[Calendar.FRIDAY], locale)); 203 map.put(f6, narrowDayOfWeek(6, array[Calendar.SATURDAY], locale)); 204 map.put(f7, narrowDayOfWeek(7, array[Calendar.SUNDAY], locale)); 205 styleMap.put(TextStyle.NARROW, map); 206 207 array = oldSymbols.getShortWeekdays(); 208 map = new HashMap<Long, String>(); 209 map.put(f1, array[Calendar.MONDAY]); 210 map.put(f2, array[Calendar.TUESDAY]); 211 map.put(f3, array[Calendar.WEDNESDAY]); 212 map.put(f4, array[Calendar.THURSDAY]); 213 map.put(f5, array[Calendar.FRIDAY]); 214 map.put(f6, array[Calendar.SATURDAY]); 215 map.put(f7, array[Calendar.SUNDAY]); 216 styleMap.put(TextStyle.SHORT, map); 217 return createLocaleStore(styleMap); 218 } 219 if (field == AMPM_OF_DAY) { 220 DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale); 221 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>(); 222 String[] array = oldSymbols.getAmPmStrings(); 223 Map<Long, String> map = new HashMap<Long, String>(); 224 map.put(0L, array[Calendar.AM]); 225 map.put(1L, array[Calendar.PM]); 226 styleMap.put(TextStyle.FULL, map); 227 styleMap.put(TextStyle.SHORT, map); // re-use, as we don't have different data 228 return createLocaleStore(styleMap); 229 } 230 if (field == ERA) { 231 DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale); 232 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>(); 233 String[] array = oldSymbols.getEras(); 234 Map<Long, String> map = new HashMap<Long, String>(); 235 map.put(0L, array[GregorianCalendar.BC]); 236 map.put(1L, array[GregorianCalendar.AD]); 237 styleMap.put(TextStyle.SHORT, map); 238 if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { 239 map = new HashMap<Long, String>(); 240 map.put(0L, "Before Christ"); 241 map.put(1L, "Anno Domini"); 242 styleMap.put(TextStyle.FULL, map); 243 } else { 244 // re-use, as we don't have different data 245 styleMap.put(TextStyle.FULL, map); 246 } 247 map = new HashMap<Long, String>(); 248 map.put(0L, array[GregorianCalendar.BC].substring(0, 1)); 249 map.put(1L, array[GregorianCalendar.AD].substring(0, 1)); 250 styleMap.put(TextStyle.NARROW, map); 251 return createLocaleStore(styleMap); 252 } 253 // hard code English quarter text 254 if (field == IsoFields.QUARTER_OF_YEAR) { 255 Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>(); 256 Map<Long, String> map = new HashMap<Long, String>(); 257 map.put(1L, "Q1"); 258 map.put(2L, "Q2"); 259 map.put(3L, "Q3"); 260 map.put(4L, "Q4"); 261 styleMap.put(TextStyle.SHORT, map); 262 map = new HashMap<Long, String>(); 263 map.put(1L, "1st quarter"); 264 map.put(2L, "2nd quarter"); 265 map.put(3L, "3rd quarter"); 266 map.put(4L, "4th quarter"); 267 styleMap.put(TextStyle.FULL, map); 268 return createLocaleStore(styleMap); 269 } 270 return ""; // null marker for map 271 } 272 273 // for China/Japan we need special behaviour narrowMonth(int month, String text, Locale locale)274 private String narrowMonth(int month, String text, Locale locale) { 275 if (locale.getLanguage().equals("zh") && locale.getCountry().equals("CN")) { 276 switch (month) { 277 case 1: 278 return "\u4e00"; 279 case 2: 280 return "\u4e8c"; 281 case 3: 282 return "\u4e09"; 283 case 4: 284 return "\u56db"; 285 case 5: 286 return "\u4e94"; 287 case 6: 288 return "\u516d"; 289 case 7: 290 return "\u4e03"; 291 case 8: 292 return "\u516b"; 293 case 9: 294 return "\u4e5d"; 295 case 10: 296 return "\u5341"; 297 case 11: 298 return "\u5341\u4e00"; 299 case 12: 300 return "\u5341\u4e8c"; 301 } 302 } 303 if (locale.getLanguage().equals("ar")) { 304 switch (month) { 305 case 1: 306 return "\u064a"; 307 case 2: 308 return "\u0641"; 309 case 3: 310 return "\u0645"; 311 case 4: 312 return "\u0623"; 313 case 5: 314 return "\u0648"; 315 case 6: 316 return "\u0646"; 317 case 7: 318 return "\u0644"; 319 case 8: 320 return "\u063a"; 321 case 9: 322 return "\u0633"; 323 case 10: 324 return "\u0643"; 325 case 11: 326 return "\u0628"; 327 case 12: 328 return "\u062f"; 329 } 330 } 331 if (locale.getLanguage().equals("ja") && locale.getCountry().equals("JP")) { 332 return Integer.toString(month); 333 } 334 return text.substring(0, 1); 335 } 336 337 // for China we need to select the last character narrowDayOfWeek(int dow, String text, Locale locale)338 private String narrowDayOfWeek(int dow, String text, Locale locale) { 339 if (locale.getLanguage().equals("zh") && locale.getCountry().equals("CN")) { 340 switch (dow) { 341 case 1: 342 return "\u4e00"; 343 case 2: 344 return "\u4e8c"; 345 case 3: 346 return "\u4e09"; 347 case 4: 348 return "\u56db"; 349 case 5: 350 return "\u4e94"; 351 case 6: 352 return "\u516d"; 353 case 7: 354 return "\u65e5"; 355 } 356 } 357 if (locale.getLanguage().equals("ar")) { 358 switch (dow) { 359 case 1: 360 return "\u0646"; 361 case 2: 362 return "\u062b"; 363 case 3: 364 return "\u0631"; 365 case 4: 366 return "\u062e"; 367 case 5: 368 return "\u062c"; 369 case 6: 370 return "\u0633"; 371 case 7: 372 return "\u062d"; 373 } 374 } 375 return text.substring(0, 1); 376 } 377 378 //----------------------------------------------------------------------- 379 /** 380 * Helper method to create an immutable entry. 381 * 382 * @param text the text, not null 383 * @param field the field, not null 384 * @return the entry, not null 385 */ createEntry(A text, B field)386 private static <A, B> Entry<A, B> createEntry(A text, B field) { 387 return new SimpleImmutableEntry<A, B>(text, field); 388 } 389 390 //----------------------------------------------------------------------- createLocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)391 private static LocaleStore createLocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 392 valueTextMap.put(TextStyle.FULL_STANDALONE, valueTextMap.get(TextStyle.FULL)); 393 valueTextMap.put(TextStyle.SHORT_STANDALONE, valueTextMap.get(TextStyle.SHORT)); 394 if (valueTextMap.containsKey(TextStyle.NARROW) && valueTextMap.containsKey(TextStyle.NARROW_STANDALONE) == false) { 395 valueTextMap.put(TextStyle.NARROW_STANDALONE, valueTextMap.get(TextStyle.NARROW)); 396 } 397 return new LocaleStore(valueTextMap); 398 } 399 400 /** 401 * Stores the text for a single locale. 402 * <p> 403 * Some fields have a textual representation, such as day-of-week or month-of-year. 404 * These textual representations can be captured in this class for printing 405 * and parsing. 406 * <p> 407 * This class is immutable and thread-safe. 408 */ 409 static final class LocaleStore { 410 /** 411 * Map of value to text. 412 */ 413 private final Map<TextStyle, Map<Long, String>> valueTextMap; 414 /** 415 * Parsable data. 416 */ 417 private final Map<TextStyle, List<Entry<String, Long>>> parsable; 418 419 //----------------------------------------------------------------------- 420 /** 421 * Constructor. 422 * 423 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 424 */ LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)425 LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) { 426 this.valueTextMap = valueTextMap; 427 Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<TextStyle, List<Entry<String,Long>>>(); 428 List<Entry<String, Long>> allList = new ArrayList<Map.Entry<String,Long>>(); 429 for (TextStyle style : valueTextMap.keySet()) { 430 Map<String, Entry<String, Long>> reverse = new HashMap<String, Map.Entry<String,Long>>(); 431 for (Map.Entry<Long, String> entry : valueTextMap.get(style).entrySet()) { 432 if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) { 433 continue; // not parsable, try next style 434 } 435 } 436 List<Entry<String, Long>> list = new ArrayList<Map.Entry<String,Long>>(reverse.values()); 437 Collections.sort(list, COMPARATOR); 438 map.put(style, list); 439 allList.addAll(list); 440 map.put(null, allList); 441 } 442 Collections.sort(allList, COMPARATOR); 443 this.parsable = map; 444 } 445 446 //----------------------------------------------------------------------- 447 /** 448 * Gets the text for the specified field value, locale and style 449 * for the purpose of printing. 450 * 451 * @param value the value to get text for, not null 452 * @param style the style to get text for, not null 453 * @return the text for the field value, null if no text found 454 */ getText(long value, TextStyle style)455 String getText(long value, TextStyle style) { 456 Map<Long, String> map = valueTextMap.get(style); 457 return map != null ? map.get(value) : null; 458 } 459 460 /** 461 * Gets an iterator of text to field for the specified style for the purpose of parsing. 462 * <p> 463 * The iterator must be returned in order from the longest text to the shortest. 464 * 465 * @param style the style to get text for, null for all parsable text 466 * @return the iterator of text to field pairs, in order from longest text to shortest text, 467 * null if the style is not parsable 468 */ getTextIterator(TextStyle style)469 Iterator<Entry<String, Long>> getTextIterator(TextStyle style) { 470 List<Entry<String, Long>> list = parsable.get(style); 471 return list != null ? list.iterator() : null; 472 } 473 } 474 475 } 476