1 /* 2 * hunt-time: A time library for D programming language. 3 * 4 * Copyright (C) 2015-2018 HuntLabs 5 * 6 * Website: https://www.huntlabs.net/ 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.time.format.DateTimeTextProvider; 13 14 import hunt.time.format.TextStyle; 15 import hunt.time.chrono.Chronology; 16 import hunt.time.chrono.IsoChronology; 17 import hunt.time.chrono.JapaneseChronology; 18 import hunt.time.temporal.ChronoField; 19 import hunt.time.temporal.IsoFields; 20 import hunt.time.temporal.TemporalField; 21 import hunt.time.util.Calendar; 22 import hunt.time.util.Common; 23 24 25 import hunt.collection; 26 import hunt.Integer; 27 import hunt.Long; 28 import hunt.Exceptions; 29 30 import hunt.util.Comparator; 31 import hunt.util.Common; 32 import hunt.util.Locale; 33 34 import std.concurrency : initOnce; 35 36 37 /** 38 * Helper method to create an immutable entry. 39 * 40 * @param text the text, not null 41 * @param field the field, not null 42 * @return the entry, not null 43 */ 44 private static MapEntry!(A, B) createEntry(A, B)(A text, B field) { 45 return new SimpleImmutableEntry!(A, B)(text, field); 46 } 47 48 49 /** 50 * A provider to obtain the textual form of a date-time field. 51 * 52 * @implSpec 53 * Implementations must be thread-safe. 54 * Implementations should cache the textual information. 55 * 56 * @since 1.8 57 */ 58 class DateTimeTextProvider { 59 // TODO: Tasks pending completion -@zxp at 3/19/2019, 8:16:45 PM 60 // 61 /** Cache. */ 62 // static Map!(MapEntry!(TemporalField, Locale), Object) CACHE() { 63 // __gshared Map!(MapEntry!(TemporalField, Locale), Object) d ; 64 // return initOnce!(d)(new HashMap!(MapEntry!(TemporalField, Locale), Object)(16, 0.75f)); 65 // } 66 // private static final ConcurrentMap!(MapEntry!(TemporalField, Locale), Object) CACHE = new ConcurrentHashMap!()(16, 0.75f, 2); 67 68 69 // Singleton instance 70 static DateTimeTextProvider INSTANCE() { 71 __gshared DateTimeTextProvider d ; 72 return initOnce!(d)(new DateTimeTextProvider()); 73 } 74 75 76 this() {} 77 78 /** 79 * Gets the provider of text. 80 * 81 * @return the provider, not null 82 */ 83 static DateTimeTextProvider getInstance() { 84 return INSTANCE; 85 } 86 87 /** 88 * Gets the text for the specified field, locale and style 89 * for the purpose of formatting. 90 * !(p) 91 * The text associated with the value is returned. 92 * The null return value should be used if there is no applicable text, or 93 * if the text would be a numeric representation of the value. 94 * 95 * @param field the field to get text for, not null 96 * @param value the field value to get text for, not null 97 * @param style the style to get text for, not null 98 * @param locale the locale to get text for, not null 99 * @return the text for the field value, null if no text found 100 */ 101 string getText(TemporalField field, long value, TextStyle style, Locale locale) { 102 Object store = findStore(field, locale); 103 LocaleStore ls = cast(LocaleStore)(store); 104 if (ls !is null) { 105 return ls.getText(value, style); 106 } 107 return null; 108 } 109 110 /** 111 * Gets the text for the specified chrono, field, locale and style 112 * for the purpose of formatting. 113 * !(p) 114 * The text associated with the value is returned. 115 * The null return value should be used if there is no applicable text, or 116 * if the text would be a numeric representation of the value. 117 * 118 * @param chrono the Chronology to get text for, not null 119 * @param field the field to get text for, not null 120 * @param value the field value to get text for, not null 121 * @param style the style to get text for, not null 122 * @param locale the locale to get text for, not null 123 * @return the text for the field value, null if no text found 124 */ 125 string getText(Chronology chrono, TemporalField field, long value, 126 TextStyle style, Locale locale) { 127 if (chrono == IsoChronology.INSTANCE 128 || !(cast(ChronoField)(field) !is null)) { 129 return getText(field, value, style, locale); 130 } 131 132 int fieldIndex; 133 int fieldValue; 134 if (field == ChronoField.ERA) { 135 fieldIndex = Calendar.ERA; 136 // TODO: Tasks pending completion -@zxp at 3/19/2019, 8:17:55 PM 137 // 138 /* if (chrono == JapaneseChronology.INSTANCE) { 139 if (value == -999) { 140 fieldValue = 0; 141 } else { 142 fieldValue = cast(int) value + 2; 143 } 144 } else */ { 145 fieldValue = cast(int) value; 146 } 147 } else if (field == ChronoField.MONTH_OF_YEAR) { 148 fieldIndex = Calendar.MONTH; 149 fieldValue = cast(int) value - 1; 150 } else if (field == ChronoField.DAY_OF_WEEK) { 151 fieldIndex = Calendar.DAY_OF_WEEK; 152 fieldValue = cast(int) value + 1; 153 if (fieldValue > 7) { 154 fieldValue = Calendar.SUNDAY; 155 } 156 } else if (field == ChronoField.AMPM_OF_DAY) { 157 fieldIndex = Calendar.AM_PM; 158 fieldValue = cast(int) value; 159 } else { 160 return null; 161 } 162 // return CalendarDataUtility.retrieveJavaTimeFieldValueName( 163 // chrono.getCalendarType(), fieldIndex, fieldValue, style.toCalendarStyle(), locale); ///@gxc 164 return null; 165 } 166 167 /** 168 * Gets an iterator of text to field for the specified field, locale and style 169 * for the purpose of parsing. 170 * !(p) 171 * The iterator must be returned _in order from the longest text to the shortest. 172 * !(p) 173 * The null return value should be used if there is no applicable parsable text, or 174 * if the text would be a numeric representation of the value. 175 * Text can only be parsed if all the values for that field-style-locale combination are unique. 176 * 177 * @param field the field to get text for, not null 178 * @param style the style to get text for, null for all parsable text 179 * @param locale the locale to get text for, not null 180 * @return the iterator of text to field pairs, _in order from longest text to shortest text, 181 * null if the field or style is not parsable 182 */ 183 Iterable!(MapEntry!(string, Long)) getTextIterator(TemporalField field, TextStyle style, Locale locale) { 184 Object store = findStore(field, locale); 185 if (cast(LocaleStore)(store) !is null) { 186 return (cast(LocaleStore) store).getTextIterator(style); 187 } 188 return null; 189 } 190 191 /** 192 * Gets an iterator of text to field for the specified chrono, field, locale and style 193 * for the purpose of parsing. 194 * !(p) 195 * The iterator must be returned _in order from the longest text to the shortest. 196 * !(p) 197 * The null return value should be used if there is no applicable parsable text, or 198 * if the text would be a numeric representation of the value. 199 * Text can only be parsed if all the values for that field-style-locale combination are unique. 200 * 201 * @param chrono the Chronology to get text for, not null 202 * @param field the field to get text for, not null 203 * @param style the style to get text for, null for all parsable text 204 * @param locale the locale to get text for, not null 205 * @return the iterator of text to field pairs, _in order from longest text to shortest text, 206 * null if the field or style is not parsable 207 */ 208 Iterable!(MapEntry!(string, Long)) getTextIterator(Chronology chrono, TemporalField field, 209 TextStyle style, Locale locale) { 210 if (chrono == IsoChronology.INSTANCE 211 || !(cast(ChronoField)(field) !is null)) { 212 return getTextIterator(field, style, locale); 213 } 214 215 int fieldIndex; 216 auto f = cast(ChronoField)field; 217 { 218 if( f == ChronoField.ERA) 219 { 220 fieldIndex = Calendar.ERA; 221 } 222 223 if( f == ChronoField.MONTH_OF_YEAR) 224 { 225 fieldIndex = Calendar.MONTH; 226 } 227 228 if( f == ChronoField.DAY_OF_WEEK) 229 { 230 fieldIndex = Calendar.DAY_OF_WEEK; 231 } 232 233 if( f == ChronoField.AMPM_OF_DAY) 234 { 235 fieldIndex = Calendar.AM_PM; 236 } 237 } 238 239 int calendarStyle = (style is null) ? Calendar.ALL_STYLES : style.toCalendarStyle(); 240 Map!(string, Integer) map = null/* CalendarDataUtility.retrieveJavaTimeFieldValueNames( ///@gxc 241 chrono.getCalendarType(), fieldIndex, calendarStyle, locale) */; 242 if (map is null) { 243 return null; 244 } 245 // List!(MapEntry!(string, Long)) list = new ArrayList!(MapEntry!(string, Long))(map.size()); 246 // switch (fieldIndex) { 247 // case Calendar.ERA: 248 // foreach(string k , Integer v ; map) { 249 // int era = v.intValue(); 250 // if (chrono == JapaneseChronology.INSTANCE) { 251 // if (era == 0) { 252 // era = -999; 253 // } else { 254 // era -= 2; 255 // } 256 // } 257 // list.add(createEntry(k, cast(long)era)); 258 // } 259 // break; 260 // case Calendar.MONTH: 261 // foreach(string k , Integer v ; map) { 262 // list.add(createEntry(k, cast(long)(v.intValue() + 1))); 263 // } 264 // break; 265 // case Calendar.DAY_OF_WEEK: 266 // foreach(string k , Integer v ; map) { 267 // list.add(createEntry(k, cast(long)toWeekDay(v.intValue))); 268 // } 269 // break; 270 // default: 271 // foreach(string k , Integer v ; map) { 272 // list.add(createEntry(k, cast(long)v.intValue)); 273 // } 274 // break; 275 // } 276 // return list; 277 return null; 278 } 279 280 private Object findStore(TemporalField field, Locale locale) { 281 MapEntry!(TemporalField, Locale) key = createEntry(field, locale); 282 // Object store = CACHE.get(key); 283 // if (store is null) { 284 // store = createStore(field, locale); 285 // CACHE.putIfAbsent(key, store); 286 // store = CACHE.get(key); 287 // } 288 // return store; 289 return null; 290 } 291 292 private static int toWeekDay(int calWeekDay) { 293 if (calWeekDay == Calendar.SUNDAY) { 294 return 7; 295 } else { 296 return calWeekDay - 1; 297 } 298 } 299 300 private Object createStore(TemporalField field, Locale locale) { 301 // Map!(TextStyle, Map!(Long, string)) styleMap = new HashMap!(TextStyle, Map!(Long, string))(); 302 // if (field == ChronoField.ERA) { 303 // foreach(TextStyle textStyle ; TextStyle.values()) { 304 // if (textStyle.isStandalone()) { 305 // // Stand-alone isn't applicable to era names. 306 // continue; 307 // } 308 // Map!(string, Integer) displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 309 // "gregory", Calendar.ERA, textStyle.toCalendarStyle(), locale); 310 // if (displayNames !is null) { 311 // Map!(Long, string) map = new HashMap!()(); 312 // foreach(MapEntry!(string, Integer) entry ; displayNames.entrySet()) { 313 // map.put(cast(long) entry.getValue(), entry.getKey()); 314 // } 315 // if (!map.isEmpty()) { 316 // styleMap.put(textStyle, map); 317 // } 318 // } 319 // } 320 // return new LocaleStore(styleMap); 321 // } 322 323 // if (field == ChronoField.MONTH_OF_YEAR) { 324 // foreach(TextStyle textStyle ; TextStyle.values()) { 325 // Map!(Long, string) map = new HashMap!()(); 326 // // Narrow names may have duplicated names, such as "J" for January, June, July. 327 // // Get names one by one _in that case. 328 // if ((textStyle.equals(TextStyle.NARROW) || 329 // textStyle.equals(TextStyle.NARROW_STANDALONE))) { 330 // for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 331 // string name; 332 // name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 333 // "gregory", Calendar.MONTH, 334 // month, textStyle.toCalendarStyle(), locale); 335 // if (name is null) { 336 // break; 337 // } 338 // map.put((month + 1L), name); 339 // } 340 // } else { 341 // Map!(string, Integer) displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 342 // "gregory", Calendar.MONTH, textStyle.toCalendarStyle(), locale); 343 // if (displayNames !is null) { 344 // foreach(MapEntry!(string, Integer) entry ; displayNames.entrySet()) { 345 // map.put(cast(long)(entry.getValue() + 1), entry.getKey()); 346 // } 347 // } else { 348 // // Although probability is very less, but if other styles have duplicate names. 349 // // Get names one by one _in that case. 350 // for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { 351 // string name; 352 // name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 353 // "gregory", Calendar.MONTH, month, textStyle.toCalendarStyle(), locale); 354 // if (name is null) { 355 // break; 356 // } 357 // map.put((month + 1L), name); 358 // } 359 // } 360 // } 361 // if (!map.isEmpty()) { 362 // styleMap.put(textStyle, map); 363 // } 364 // } 365 // return new LocaleStore(styleMap); 366 // } 367 368 // if (field == ChronoField.DAY_OF_WEEK) { 369 // foreach(TextStyle textStyle ; TextStyle.values()) { 370 // Map!(Long, string) map = new HashMap!()(); 371 // // Narrow names may have duplicated names, such as "S" for Sunday and Saturday. 372 // // Get names one by one _in that case. 373 // if ((textStyle.equals(TextStyle.NARROW) || 374 // textStyle.equals(TextStyle.NARROW_STANDALONE))) { 375 // for (int wday = Calendar.SUNDAY; wday <= Calendar.SATURDAY; wday++) { 376 // string name; 377 // name = CalendarDataUtility.retrieveJavaTimeFieldValueName( 378 // "gregory", Calendar.DAY_OF_WEEK, 379 // wday, textStyle.toCalendarStyle(), locale); 380 // if (name is null) { 381 // break; 382 // } 383 // map.put(cast(long)toWeekDay(wday), name); 384 // } 385 // } else { 386 // Map!(string, Integer) displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 387 // "gregory", Calendar.DAY_OF_WEEK, textStyle.toCalendarStyle(), locale); 388 // if (displayNames !is null) { 389 // foreach(MapEntry!(string, Integer) entry ; displayNames.entrySet()) { 390 // map.put(cast(long)toWeekDay(entry.getValue()), entry.getKey()); 391 // } 392 // } else { 393 // // Although probability is very less, but if other styles have duplicate names. 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 is null) { 400 // break; 401 // } 402 // map.put(cast(long)toWeekDay(wday), name); 403 // } 404 // } 405 // } 406 // if (!map.isEmpty()) { 407 // styleMap.put(textStyle, map); 408 // } 409 // } 410 // return new LocaleStore(styleMap); 411 // } 412 413 // if (field == ChronoField.AMPM_OF_DAY) { 414 // foreach(TextStyle textStyle ; TextStyle.values()) { 415 // if (textStyle.isStandalone()) { 416 // // Stand-alone isn't applicable to AM/PM. 417 // continue; 418 // } 419 // Map!(string, Integer) displayNames = CalendarDataUtility.retrieveJavaTimeFieldValueNames( 420 // "gregory", Calendar.AM_PM, textStyle.toCalendarStyle(), locale); 421 // if (displayNames !is null) { 422 // Map!(Long, string) map = new HashMap!()(); 423 // foreach(MapEntry!(string, Integer) entry ; displayNames.entrySet()) { 424 // map.put(cast(long) entry.getValue(), entry.getKey()); 425 // } 426 // if (!map.isEmpty()) { 427 // styleMap.put(textStyle, map); 428 // } 429 // } 430 // } 431 // return new LocaleStore(styleMap); 432 // } 433 434 // if (field == IsoFields.QUARTER_OF_YEAR) { 435 // // The order of keys must correspond to the TextStyle.values() order. 436 // final string[] keys = { 437 // "QuarterNames", 438 // "standalone.QuarterNames", 439 // "QuarterAbbreviations", 440 // "standalone.QuarterAbbreviations", 441 // "QuarterNarrows", 442 // "standalone.QuarterNarrows", 443 // }; 444 // for (int i = 0; i < keys.length; i++) { 445 // string[] names = getLocalizedResource(keys[i], locale); 446 // if (names !is null) { 447 // Map!(Long, string) map = new HashMap!()(); 448 // for (int q = 0; q < names.length; q++) { 449 // map.put(cast(long) (q + 1), names[q]); 450 // } 451 // styleMap.put(TextStyle.values()[i], map); 452 // } 453 // } 454 // return new LocaleStore(styleMap); 455 // } 456 457 return null; // null marker for map 458 } 459 460 /** 461 * Returns the localized resource of the given key and locale, or null 462 * if no localized resource is available. 463 * 464 * @param key the key of the localized resource, not null 465 * @param locale the locale, not null 466 * @return the localized resource, or null if not available 467 * @throws NullPointerException if key or locale is null 468 */ 469 /*@SuppressWarnings("unchecked")*/ 470 static T getLocalizedResource(T)(string key, Locale locale) { 471 ///@gxc 472 // LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased() 473 // .getLocaleResources( 474 // CalendarDataUtility.findRegionOverride(locale)); 475 // ResourceBundle rb = lr.getJavaTimeFormatData(); 476 // return rb.containsKey(key) ? cast(T) rb.getObject(key) : null; 477 implementationMissing(false); 478 return T.init; 479 } 480 481 } 482 483 484 485 /** 486 * Stores the text for a single locale. 487 * !(p) 488 * Some fields have a textual representation, such as day-of-week or month-of-year. 489 * These textual representations can be captured _in this class for printing 490 * and parsing. 491 * !(p) 492 * This class is immutable and thread-safe. 493 */ 494 final class LocaleStore { 495 496 /** Comparator. */ 497 private static Comparator!(MapEntry!(string, Long)) COMPARATOR() { 498 __gshared Comparator!(MapEntry!(string, Long)) c; 499 return initOnce!(c)(createComparator()); 500 } 501 502 private static Comparator!(MapEntry!(string, Long)) createComparator() { 503 return new class Comparator!(MapEntry!(string, Long)) { 504 override 505 int compare(MapEntry!(string, Long) obj1, MapEntry!(string, Long) obj2) nothrow { 506 try { 507 return cast(int)(obj2.getKey().length - obj1.getKey().length); // longest to shortest 508 } catch(Exception) { 509 return 0; 510 } 511 } 512 }; 513 } 514 515 /** 516 * Map of value to text. 517 */ 518 private Map!(TextStyle, Map!(Long, string)) valueTextMap; 519 /** 520 * Parsable data. 521 */ 522 private Map!(TextStyle, List!(MapEntry!(string, Long))) parsable; 523 524 525 /** 526 * Constructor. 527 * 528 * @param valueTextMap the map of values to text to store, assigned and not altered, not null 529 */ 530 this(Map!(TextStyle, Map!(Long, string)) valueTextMap) { 531 this.valueTextMap = valueTextMap; 532 Map!(TextStyle, List!(MapEntry!(string, Long))) map = new HashMap!(TextStyle, List!(MapEntry!(string, Long)))(); 533 List!(MapEntry!(string, Long)) allList = new ArrayList!(MapEntry!(string, Long))(); 534 535 foreach(TextStyle k , Map!(Long, string) vtmValue ; valueTextMap) { 536 Map!(string, MapEntry!(string, Long)) reverse = new HashMap!(string, MapEntry!(string, Long))(); 537 foreach(Long k2 , string v2 ; vtmValue) { 538 if (reverse.put(v2, createEntry(v2, k2)) !is null) { 539 // TODO: BUG: this has no effect 540 continue; // not parsable, try next style 541 } 542 } 543 List!(MapEntry!(string, Long)) list = new ArrayList!(MapEntry!(string, Long))(reverse.values()); 544 list.sort(COMPARATOR()); 545 map.put(k, list); 546 allList.addAll(list); 547 map.put(null, allList); 548 } 549 allList.sort(COMPARATOR()); 550 this.parsable = map; 551 } 552 553 /** 554 * Gets the text for the specified field value, locale and style 555 * for the purpose of printing. 556 * 557 * @param value the value to get text for, not null 558 * @param style the style to get text for, not null 559 * @return the text for the field value, null if no text found 560 */ 561 string getText(long value, TextStyle style) { 562 Map!(Long, string) map = valueTextMap.get(style); 563 return map !is null ? map.get(new Long(value)) : null; 564 } 565 566 /** 567 * Gets an iterator of text to field for the specified style for the purpose of parsing. 568 * !(p) 569 * The iterator must be returned _in order from the longest text to the shortest. 570 * 571 * @param style the style to get text for, null for all parsable text 572 * @return the iterator of text to field pairs, _in order from longest text to shortest text, 573 * null if the style is not parsable 574 */ 575 Iterable!(MapEntry!(string, Long)) getTextIterator(TextStyle style) { 576 List!(MapEntry!(string, Long)) list = parsable.get(style); 577 return list !is null ? list : null; 578 } 579 }