1 module hunt.time.format.OffsetIdPrinterParser; 2 3 import hunt.time.Exceptions; 4 5 import hunt.time.format.DateTimeParseContext; 6 import hunt.time.format.DateTimePrinterParser; 7 import hunt.time.format.DateTimePrintContext; 8 import hunt.time.temporal.ChronoField; 9 import hunt.time.temporal.TemporalField; 10 import hunt.time.util.Common; 11 import hunt.util.StringBuilder; 12 13 import hunt.Exceptions; 14 import hunt.Long; 15 import hunt.math.Helper; 16 17 import std.conv; 18 19 //----------------------------------------------------------------------- 20 /** 21 * Prints or parses an offset ID. 22 */ 23 static final class OffsetIdPrinterParser : DateTimePrinterParser 24 { 25 enum string[] PATTERNS = [ 26 "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", 27 "+HHmmss", "+HH:mm:ss", "+H", "+Hmm", "+H:mm", "+HMM", "+H:MM", "+HMMss", 28 "+H:MM:ss", "+HMMSS", "+H:MM:SS", "+Hmmss", "+H:mm:ss", 29 ]; // order used _in pattern builder 30 // __gshared OffsetIdPrinterParser INSTANCE_ID_Z; 31 // __gshared OffsetIdPrinterParser INSTANCE_ID_ZERO; 32 33 private string noOffsetText; 34 private int type; 35 private int style; 36 37 // shared static this() 38 // { 39 // INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z"); 40 mixin(MakeGlobalVar!(OffsetIdPrinterParser)("INSTANCE_ID_Z",`new OffsetIdPrinterParser("+HH:MM:ss", "Z")`)); 41 // INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0"); 42 mixin(MakeGlobalVar!(OffsetIdPrinterParser)("INSTANCE_ID_ZERO",`new OffsetIdPrinterParser("+HH:MM:ss", "0")`)); 43 44 // } 45 /** 46 * Constructor. 47 * 48 * @param pattern the pattern 49 * @param noOffsetText the text to use for UTC, not null 50 */ 51 this(string pattern, string noOffsetText) 52 { 53 assert(pattern, "pattern"); 54 assert(noOffsetText, "noOffsetText"); 55 this.type = checkPattern(pattern); 56 this.style = type % 11; 57 this.noOffsetText = noOffsetText; 58 } 59 60 private int checkPattern(string pattern) 61 { 62 for (int i = 0; i < PATTERNS.length; i++) 63 { 64 if (PATTERNS[i] == (pattern)) 65 { 66 return i; 67 } 68 } 69 throw new IllegalArgumentException("Invalid zone offset pattern: " ~ pattern); 70 } 71 72 private bool isPaddedHour() 73 { 74 return type < 11; 75 } 76 77 private bool isColon() 78 { 79 return style > 0 && (style % 2) == 0; 80 } 81 82 override public bool format(DateTimePrintContext context, StringBuilder buf) 83 { 84 Long offsetSecs = context.getValue(ChronoField.OFFSET_SECONDS); 85 if (offsetSecs is null) 86 { 87 return false; 88 } 89 int totalSecs = MathHelper.toIntExact(offsetSecs.longValue()); 90 if (totalSecs == 0) 91 { 92 buf.append(noOffsetText); 93 } 94 else 95 { 96 int absHours = MathHelper.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped 97 int absMinutes = MathHelper.abs((totalSecs / 60) % 60); 98 int absSeconds = MathHelper.abs(totalSecs % 60); 99 int bufPos = buf.length(); 100 int output = absHours; 101 buf.append(totalSecs < 0 ? "-" : "+"); 102 if (isPaddedHour() || absHours >= 10) 103 { 104 formatZeroPad(false, absHours, buf); 105 } 106 else 107 { 108 buf.append( /* cast(char) */ (absHours.to!string ~ '0')); 109 } 110 if ((style >= 3 && style <= 8) || (style >= 9 && absSeconds > 0) 111 || (style >= 1 && absMinutes > 0)) 112 { 113 formatZeroPad(isColon(), absMinutes, buf); 114 output += absMinutes; 115 if (style == 7 || style == 8 || (style >= 5 && absSeconds > 0)) 116 { 117 formatZeroPad(isColon(), absSeconds, buf); 118 output += absSeconds; 119 } 120 } 121 if (output == 0) 122 { 123 buf.setLength(bufPos); 124 buf.append(noOffsetText); 125 } 126 } 127 return true; 128 } 129 130 private void formatZeroPad(bool colon, int value, StringBuilder buf) 131 { 132 buf.append(colon ? ":" : "").append( /* cast(char) */ ((value / 10) 133 .to!string ~ '0')).append( /* cast(char) */ ((value % 10).to!string ~ '0')); 134 } 135 136 override public int parse(DateTimeParseContext context, string text, int position) 137 { 138 int length = cast(int)(text.length); 139 int noOffsetLen = cast(int)(noOffsetText.length); 140 if (noOffsetLen == 0) 141 { 142 if (position == length) 143 { 144 return context.setParsedField(ChronoField.OFFSET_SECONDS, 145 0, position, position); 146 } 147 } 148 else 149 { 150 if (position == length) 151 { 152 return ~position; 153 } 154 if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) 155 { 156 return context.setParsedField(ChronoField.OFFSET_SECONDS, 157 0, position, position + noOffsetLen); 158 } 159 } 160 161 // parse normal plus/minus offset 162 char sign = text[position]; // IOOBE if invalid position 163 if (sign == '+' || sign == '-') 164 { 165 // starts 166 int negative = (sign == '-' ? -1 : 1); 167 bool isColon = isColon(); 168 bool paddedHour = isPaddedHour(); 169 int[] array = new int[4]; 170 array[0] = position + 1; 171 int parseType = type; 172 // select parse type when lenient 173 if (!context.isStrict()) 174 { 175 if (paddedHour) 176 { 177 if (isColon || (parseType == 0 && length > position + 3 178 && text[position + 3] == ':')) 179 { 180 isColon = true; // needed _in cases like ("+HH", "+01:01") 181 parseType = 10; 182 } 183 else 184 { 185 parseType = 9; 186 } 187 } 188 else 189 { 190 if (isColon || (parseType == 11 && length > position + 3 191 && (text[position + 2] == ':' 192 || text[position + 3] == ':'))) 193 { 194 isColon = true; 195 parseType = 21; // needed _in cases like ("+H", "+1:01") 196 } 197 else 198 { 199 parseType = 20; 200 } 201 } 202 } 203 // parse according to the selected pattern 204 switch (parseType) 205 { 206 case 0: // +HH 207 case 11: // +H 208 parseHour(text, paddedHour, array); 209 break; 210 case 1: // +HHmm 211 case 2: // +HH:mm 212 case 13: // +H:mm 213 parseHour(text, paddedHour, array); 214 parseMinute(text, isColon, false, array); 215 break; 216 case 3: // +HHMM 217 case 4: // +HH:MM 218 case 15: // +H:MM 219 parseHour(text, paddedHour, array); 220 parseMinute(text, isColon, true, array); 221 break; 222 case 5: // +HHMMss 223 case 6: // +HH:MM:ss 224 case 17: // +H:MM:ss 225 parseHour(text, paddedHour, array); 226 parseMinute(text, isColon, true, array); 227 parseSecond(text, isColon, false, array); 228 break; 229 case 7: // +HHMMSS 230 case 8: // +HH:MM:SS 231 case 19: // +H:MM:SS 232 parseHour(text, paddedHour, array); 233 parseMinute(text, isColon, true, array); 234 parseSecond(text, isColon, true, array); 235 break; 236 case 9: // +HHmmss 237 case 10: // +HH:mm:ss 238 case 21: // +H:mm:ss 239 parseHour(text, paddedHour, array); 240 parseOptionalMinuteSecond(text, isColon, array); 241 break; 242 case 12: // +Hmm 243 parseVariableWidthDigits(text, 1, 4, array); 244 break; 245 case 14: // +HMM 246 parseVariableWidthDigits(text, 3, 4, array); 247 break; 248 case 16: // +HMMss 249 parseVariableWidthDigits(text, 3, 6, array); 250 break; 251 case 18: // +HMMSS 252 parseVariableWidthDigits(text, 5, 6, array); 253 break; 254 case 20: // +Hmmss 255 parseVariableWidthDigits(text, 1, 6, array); 256 break; 257 default: 258 break; 259 } 260 if (array[0] > 0) 261 { 262 if (array[1] > 23 || array[2] > 59 || array[3] > 59) 263 { 264 throw new DateTimeException( 265 "Value _out of range: Hour[0-23], Minute[0-59], Second[0-59]"); 266 } 267 long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]); 268 return context.setParsedField(ChronoField.OFFSET_SECONDS, 269 offsetSecs, position, array[0]); 270 } 271 } 272 // handle special case of empty no offset text 273 if (noOffsetLen == 0) 274 { 275 return context.setParsedField(ChronoField.OFFSET_SECONDS, 276 0, position, position); 277 } 278 return ~position; 279 } 280 281 private void parseHour(string parseText, bool paddedHour, int[] array) 282 { 283 if (paddedHour) 284 { 285 // parse two digits 286 if (!parseDigits(parseText, false, 1, array)) 287 { 288 array[0] = ~array[0]; 289 } 290 } 291 else 292 { 293 // parse one or two digits 294 parseVariableWidthDigits(parseText, 1, 2, array); 295 } 296 } 297 298 private void parseMinute(string parseText, bool isColon, bool mandatory, int[] array) 299 { 300 if (!parseDigits(parseText, isColon, 2, array)) 301 { 302 if (mandatory) 303 { 304 array[0] = ~array[0]; 305 } 306 } 307 } 308 309 private void parseSecond(string parseText, bool isColon, bool mandatory, int[] array) 310 { 311 if (!parseDigits(parseText, isColon, 3, array)) 312 { 313 if (mandatory) 314 { 315 array[0] = ~array[0]; 316 } 317 } 318 } 319 320 private void parseOptionalMinuteSecond(string parseText, bool isColon, int[] array) 321 { 322 if (parseDigits(parseText, isColon, 2, array)) 323 { 324 parseDigits(parseText, isColon, 3, array); 325 } 326 } 327 328 private bool parseDigits(string parseText, bool isColon, int arrayIndex, int[] array) 329 { 330 int pos = array[0]; 331 if (pos < 0) 332 { 333 return true; 334 } 335 if (isColon && arrayIndex != 1) 336 { // ':' will precede only _in case of minute/second 337 if (pos + 1 > parseText.length || parseText[pos] != ':') 338 { 339 return false; 340 } 341 pos++; 342 } 343 if (pos + 2 > parseText.length) 344 { 345 return false; 346 } 347 char ch1 = parseText[pos++]; 348 char ch2 = parseText[pos++]; 349 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') 350 { 351 return false; 352 } 353 int value = (ch1 - 48) * 10 + (ch2 - 48); 354 if (value < 0 || value > 59) 355 { 356 return false; 357 } 358 array[arrayIndex] = value; 359 array[0] = pos; 360 return true; 361 } 362 363 private void parseVariableWidthDigits(string parseText, 364 int minDigits, int maxDigits, int[] array) 365 { 366 // scan the text to find the available number of digits up to maxDigits 367 // so long as the number available is minDigits or more, the input is valid 368 // then parse the number of available digits 369 int pos = array[0]; 370 int available = 0; 371 char[] chars = new char[maxDigits]; 372 for (int i = 0; i < maxDigits; i++) 373 { 374 if (pos + 1 > parseText.length) 375 { 376 break; 377 } 378 char ch = parseText[pos++]; 379 if (ch < '0' || ch > '9') 380 { 381 pos--; 382 break; 383 } 384 chars[i] = ch; 385 available++; 386 } 387 if (available < minDigits) 388 { 389 array[0] = ~array[0]; 390 return; 391 } 392 switch (available) 393 { 394 case 1: 395 array[1] = (chars[0] - 48); 396 break; 397 case 2: 398 array[1] = ((chars[0] - 48) * 10 + (chars[1] - 48)); 399 break; 400 case 3: 401 array[1] = (chars[0] - 48); 402 array[2] = ((chars[1] - 48) * 10 + (chars[2] - 48)); 403 break; 404 case 4: 405 array[1] = ((chars[0] - 48) * 10 + (chars[1] - 48)); 406 array[2] = ((chars[2] - 48) * 10 + (chars[3] - 48)); 407 break; 408 case 5: 409 array[1] = (chars[0] - 48); 410 array[2] = ((chars[1] - 48) * 10 + (chars[2] - 48)); 411 array[3] = ((chars[3] - 48) * 10 + (chars[4] - 48)); 412 break; 413 case 6: 414 array[1] = ((chars[0] - 48) * 10 + (chars[1] - 48)); 415 array[2] = ((chars[2] - 48) * 10 + (chars[3] - 48)); 416 array[3] = ((chars[4] - 48) * 10 + (chars[5] - 48)); 417 break; 418 default: 419 break; 420 } 421 array[0] = pos; 422 } 423 424 override public string toString() 425 { 426 string converted = noOffsetText.replace("'", "''"); 427 return "Offset(" ~ PATTERNS[type] ~ ",'" ~ converted ~ "')"; 428 } 429 }