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 }