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 }