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.DateTimePrintContext;
13 
14 import hunt.time.temporal.ChronoField;
15 
16 import hunt.time.Exceptions;
17 import hunt.time.Instant;
18 import hunt.time.ZoneId;
19 import hunt.time.ZoneOffset;
20 import hunt.time.chrono.ChronoLocalDate;
21 import hunt.time.chrono.Chronology;
22 import hunt.time.chrono.IsoChronology;
23 import hunt.time.temporal.ChronoField;
24 import hunt.time.temporal.TemporalAccessor;
25 import hunt.time.temporal.TemporalField;
26 import hunt.time.temporal.TemporalQueries;
27 import hunt.time.temporal.TemporalQuery;
28 import hunt.time.Exceptions;
29 import hunt.time.util.QueryHelper;
30 
31 import hunt.time.temporal.ValueRange;
32 import hunt.time.format.DateTimeFormatter;
33 import hunt.time.format.DecimalStyle;
34 
35 import hunt.Long;
36 import hunt.util.Locale;
37 import std.conv;
38 
39 /**
40  * Context object used during date and time printing.
41  * !(p)
42  * This class provides a single wrapper to items used _in the format.
43  *
44  * @implSpec
45  * This class is a mutable context intended for use from a single thread.
46  * Usage of the class is thread-safe within standard printing as the framework creates
47  * a new instance of the class for each format and printing is single-threaded.
48  *
49  * @since 1.8
50  */
51 final class DateTimePrintContext
52 {
53 
54     /**
55      * The temporal being output.
56      */
57     private TemporalAccessor temporal;
58     /**
59      * The formatter, not null.
60      */
61     private DateTimeFormatter formatter;
62     /**
63      * Whether the current formatter is optional.
64      */
65     private int optional;
66 
67     /**
68      * Creates a new instance of the context.
69      *
70      * @param temporal  the temporal object being output, not null
71      * @param formatter  the formatter controlling the format, not null
72      */
73     this(TemporalAccessor temporal, DateTimeFormatter formatter)
74     {
75         // super();
76         this.temporal = adjust(temporal, formatter);
77         this.formatter = formatter;
78     }
79 
80     private static TemporalAccessor adjust(TemporalAccessor temporal, DateTimeFormatter formatter)
81     {
82         // normal case first (early return is an optimization)
83         Chronology overrideChrono = formatter.getChronology();
84         ZoneId overrideZone = formatter.getZone();
85         if (overrideChrono is null && overrideZone is null)
86         {
87             return temporal;
88         }
89 
90         // ensure minimal change (early return is an optimization)
91         Chronology temporalChrono = QueryHelper.query!Chronology(temporal,TemporalQueries.chronology());
92         ZoneId temporalZone = QueryHelper.query!ZoneId(temporal,TemporalQueries.zoneId());
93         if (overrideChrono == temporalChrono)
94         {
95             overrideChrono = null;
96         }
97         if ((overrideZone == temporalZone))
98         {
99             overrideZone = null;
100         }
101         if (overrideChrono is null && overrideZone is null)
102         {
103             return temporal;
104         }
105 
106         // make adjustment
107         Chronology effectiveChrono = (overrideChrono !is null ? overrideChrono : temporalChrono);
108         if (overrideZone !is null)
109         {
110             // if have zone and instant, calculation is simple, defaulting chrono if necessary
111             if (temporal.isSupported(ChronoField.INSTANT_SECONDS))
112             {
113                 Chronology chrono = effectiveChrono !is null ? effectiveChrono
114                     : IsoChronology.INSTANCE;
115                 return chrono.zonedDateTime(Instant.from(temporal), overrideZone);
116             }
117             // block changing zone on OffsetTime, and similar problem cases
118             if ((cast(ZoneOffset)(overrideZone.normalized)) !is null
119                     && temporal.isSupported(ChronoField.OFFSET_SECONDS)
120                     && temporal.get(ChronoField.OFFSET_SECONDS) != overrideZone.getRules()
121                     .getOffset(Instant.EPOCH).getTotalSeconds())
122             {
123                 throw new DateTimeException("Unable to apply override zone '" ~ typeid(overrideZone)
124                         .name ~ "' because the temporal object being formatted has a different offset but"
125                         ~ " does not represent an instant: " ~ typeid(temporal).name);
126             }
127         }
128         ZoneId effectiveZone = (overrideZone !is null ? overrideZone : temporalZone);
129         ChronoLocalDate effectiveDate;
130         if (overrideChrono !is null)
131         {
132             if (temporal.isSupported(ChronoField.EPOCH_DAY))
133             {
134                 effectiveDate = effectiveChrono.date(temporal);
135             }
136             else
137             {
138                 // check for date fields other than epoch-day, ignoring case of converting null to ISO
139                 if (!(overrideChrono == IsoChronology.INSTANCE && temporalChrono is null))
140                 {
141                     foreach (ChronoField f; ChronoField.values())
142                     {
143                         if (f.isDateBased() && temporal.isSupported(f))
144                         {
145                             throw new DateTimeException("Unable to apply override chronology '" ~ typeid(overrideChrono)
146                                     .name ~ "' because the temporal object being formatted contains date fields but"
147                                     ~ " does not represent a whole date: " ~ typeid(temporal).name);
148                         }
149                     }
150                 }
151                 effectiveDate = null;
152             }
153         }
154         else
155         {
156             effectiveDate = null;
157         }
158 
159         // combine available data
160         // this is a non-standard temporal that is almost a pure delegate
161         // this better handles map-like underlying temporal instances
162         return new AnonymousClass2(effectiveDate,temporal,effectiveChrono,effectiveZone);
163     }
164 
165     //-----------------------------------------------------------------------
166     /**
167      * Gets the temporal object being output.
168      *
169      * @return the temporal object, not null
170      */
171     TemporalAccessor getTemporal()
172     {
173         return temporal;
174     }
175 
176     /**
177      * Gets the locale.
178      * !(p)
179      * This locale is used to control localization _in the format output except
180      * where localization is controlled by the DecimalStyle.
181      *
182      * @return the locale, not null
183      */
184     Locale getLocale()
185     {
186         return formatter.getLocale();
187     }
188 
189     /**
190      * Gets the DecimalStyle.
191      * !(p)
192      * The DecimalStyle controls the localization of numeric output.
193      *
194      * @return the DecimalStyle, not null
195      */
196     DecimalStyle getDecimalStyle()
197     {
198         return formatter.getDecimalStyle();
199     }
200 
201     //-----------------------------------------------------------------------
202     /**
203      * Starts the printing of an optional segment of the input.
204      */
205     void startOptional()
206     {
207         this.optional++;
208     }
209 
210     /**
211      * Ends the printing of an optional segment of the input.
212      */
213     void endOptional()
214     {
215         this.optional--;
216     }
217 
218     /**
219      * Gets a value using a query.
220      *
221      * @param query  the query to use, not null
222      * @return the result, null if not found and optional is true
223      * @throws DateTimeException if the type is not available and the section is not optional
224      */
225     R getValue(R)(TemporalQuery!(R) query)
226     {
227         R result = QueryHelper.query!R(temporal , query);
228         if (result is null && optional == 0)
229         {
230             throw new DateTimeException("Unable to extract " ~ typeid(query)
231                     .name ~ " from temporal " ~ typeid(temporal).name);
232         }
233         return result;
234     }
235 
236     /**
237      * Gets the value of the specified field.
238      * !(p)
239      * This will return the value for the specified field.
240      *
241      * @param field  the field to find, not null
242      * @return the value, null if not found and optional is true
243      * @throws DateTimeException if the field is not available and the section is not optional
244      */
245     Long getValue(TemporalField field)
246     {
247         if (optional > 0 && !temporal.isSupported(field))
248         {
249             return null;
250         }
251         return new Long(temporal.getLong(field));
252     }
253 
254     //-----------------------------------------------------------------------
255     /**
256      * Returns a string version of the context for debugging.
257      *
258      * @return a string representation of the context, not null
259      */
260     override public string toString()
261     {
262         return temporal.toString();
263     }
264 
265 }