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 }