1 module hunt.time.format.NumberPrinterParser;
2 
3 import hunt.time.Exceptions;
4 import hunt.time.format.DateTimeParseContext;
5 import hunt.time.format.DateTimePrinterParser;
6 import hunt.time.format.DateTimePrintContext;
7 import hunt.time.format.DecimalStyle;
8 import hunt.time.format.SignStyle;
9 import hunt.time.temporal.TemporalField;
10 import hunt.util.StringBuilder;
11 
12 import hunt.Long;
13 import hunt.math.BigInteger;
14 import hunt.math.Helper;
15 
16 import std.conv;
17 
18 
19 //-----------------------------------------------------------------------
20 /**
21  * Prints and parses a numeric date-time field with optional padding.
22  */
23 class NumberPrinterParser : DateTimePrinterParser
24 {
25 
26     /**
27      * Array of 10 to the power of n.
28      */
29     enum long[] EXCEED_POINTS = [
30         0L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L,
31         100000000L, 1000000000L, 10000000000L,
32     ];
33 
34     TemporalField field;
35     int minWidth;
36     int maxWidth;
37     package SignStyle signStyle;
38     int subsequentWidth;
39 
40     /**
41      * Constructor.
42      *
43      * @param field  the field to format, not null
44      * @param minWidth  the minimum field width, from 1 to 19
45      * @param maxWidth  the maximum field width, from minWidth to 19
46      * @param signStyle  the positive/negative sign style, not null
47      */
48     this(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle)
49     {
50         // validated by caller
51         this.field = field;
52         this.minWidth = minWidth;
53         this.maxWidth = maxWidth;
54         this.signStyle = signStyle;
55         this.subsequentWidth = 0;
56     }
57 
58     /**
59      * Constructor.
60      *
61      * @param field  the field to format, not null
62      * @param minWidth  the minimum field width, from 1 to 19
63      * @param maxWidth  the maximum field width, from minWidth to 19
64      * @param signStyle  the positive/negative sign style, not null
65      * @param subsequentWidth  the width of subsequent non-negative numbers, 0 or greater,
66      *  -1 if fixed width due to active adjacent parsing
67      */
68     package this(TemporalField field, int minWidth, int maxWidth,
69             SignStyle signStyle, int subsequentWidth)
70     {
71         // validated by caller
72         this.field = field;
73         this.minWidth = minWidth;
74         this.maxWidth = maxWidth;
75         this.signStyle = signStyle;
76         this.subsequentWidth = subsequentWidth;
77     }
78 
79     /**
80      * Returns a new instance with fixed width flag set.
81      *
82      * @return a new updated printer-parser, not null
83      */
84     NumberPrinterParser withFixedWidth()
85     {
86         if (subsequentWidth == -1)
87         {
88             return this;
89         }
90         return new NumberPrinterParser(field, minWidth, maxWidth, signStyle, -1);
91     }
92 
93     /**
94      * Returns a new instance with an updated subsequent width.
95      *
96      * @param subsequentWidth  the width of subsequent non-negative numbers, 0 or greater
97      * @return a new updated printer-parser, not null
98      */
99     NumberPrinterParser withSubsequentWidth(int subsequentWidth)
100     {
101         return new NumberPrinterParser(field, minWidth, maxWidth,
102                 signStyle, this.subsequentWidth + subsequentWidth);
103     }
104 
105     override public bool format(DateTimePrintContext context, StringBuilder buf)
106     {
107         Long valueLong = context.getValue(field);
108         if (valueLong is null)
109         {
110             return false;
111         }
112         long value = getValue(context, valueLong.longValue());
113         DecimalStyle decimalStyle = context.getDecimalStyle();
114         string str = (value == Long.MIN_VALUE ? "9223372036854775808"
115                 : to!string(MathHelper.abs(value)));
116         if (str.length > maxWidth)
117         {
118             throw new DateTimeException("Field " ~ typeid(field)
119                     .name ~ " cannot be printed as the value " ~ value.to!string
120                     ~ " exceeds the maximum print width of " ~ maxWidth.to!string);
121         }
122         str = decimalStyle.convertNumberToI18N(str);
123 
124         if (value >= 0)
125         {
126             auto name = signStyle.name();
127             {
128                 if (name == SignStyle.EXCEEDS_PAD.name())
129                 {
130                     if (minWidth < 19 && value >= EXCEED_POINTS[minWidth])
131                     {
132                         buf.append(decimalStyle.getPositiveSign());
133                     }
134                 }
135 
136                 if (name == SignStyle.ALWAYS.name())
137                 {
138                     buf.append(decimalStyle.getPositiveSign());
139                 }
140 
141             }
142         }
143         else
144         {
145             auto name = signStyle.name();
146             {
147                 if (name == SignStyle.NORMAL.name()
148                         || name == SignStyle.EXCEEDS_PAD.name()
149                         || name == SignStyle.ALWAYS.name())
150                 {
151                     buf.append(decimalStyle.getNegativeSign());
152                 }
153                 if (name == SignStyle.NOT_NEGATIVE.name())
154                 {
155                     throw new DateTimeException("Field " ~ typeid(field)
156                             .name ~ " cannot be printed as the value " ~ value.to!string
157                             ~ " cannot be negative according to the SignStyle");
158                 }
159             }
160         }
161         for (int i = 0; i < minWidth - str.length; i++)
162         {
163             buf.append(decimalStyle.getZeroDigit());
164         }
165         buf.append(str);
166         return true;
167     }
168 
169     /**
170      * Gets the value to output.
171      *
172      * @param context  the context
173      * @param value  the value of the field, not null
174      * @return the value
175      */
176     long getValue(DateTimePrintContext context, long value)
177     {
178         return value;
179     }
180 
181     /**
182      * For NumberPrinterParser, the width is fixed depending on the
183      * minWidth, maxWidth, signStyle and whether subsequent fields are fixed.
184      * @param context the context
185      * @return true if the field is fixed width
186      * @see DateTimeFormatterBuilder#appendValue(hunt.time.temporal.TemporalField, int)
187      */
188     bool isFixedWidth(DateTimeParseContext context)
189     {
190         return subsequentWidth == -1 || (subsequentWidth > 0
191                 && minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE);
192     }
193 
194     override public int parse(DateTimeParseContext context, string text, int position)
195     {
196         int length = cast(int)(text.length);
197         if (position == length)
198         {
199             return ~position;
200         }
201         char sign = text[position]; // IOOBE if invalid position
202         bool negative = false;
203         bool positive = false;
204         if (sign == context.getDecimalStyle().getPositiveSign())
205         {
206             if (signStyle.parse(true, context.isStrict(), minWidth == maxWidth) == false)
207             {
208                 return ~position;
209             }
210             positive = true;
211             position++;
212         }
213         else if (sign == context.getDecimalStyle().getNegativeSign())
214         {
215             if (signStyle.parse(false, context.isStrict(), minWidth == maxWidth) == false)
216             {
217                 return ~position;
218             }
219             negative = true;
220             position++;
221         }
222         else
223         {
224             if (signStyle == SignStyle.ALWAYS && context.isStrict())
225             {
226                 return ~position;
227             }
228         }
229         int effMinWidth = (context.isStrict() || isFixedWidth(context) ? minWidth : 1);
230         int minEndPos = position + effMinWidth;
231         if (minEndPos > length)
232         {
233             return ~position;
234         }
235         int effMaxWidth = (context.isStrict() || isFixedWidth(context) ? maxWidth : 9) + MathHelper.max(subsequentWidth,
236                 0);
237         long total = 0;
238         BigInteger totalBig = null;
239         int pos = position;
240         for (int pass = 0; pass < 2; pass++)
241         {
242             int maxEndPos = MathHelper.min(pos + effMaxWidth, length);
243             while (pos < maxEndPos)
244             {
245                 char ch = text[pos++];
246                 int digit = context.getDecimalStyle().convertToDigit(ch);
247                 if (digit < 0)
248                 {
249                     pos--;
250                     if (pos < minEndPos)
251                     {
252                         return ~position; // need at least min width digits
253                     }
254                     break;
255                 }
256                 if ((pos - position) > 18)
257                 {
258                     if (totalBig is null)
259                     {
260                         totalBig = BigInteger.valueOf(total);
261                     }
262                     totalBig = totalBig.multiply(BigInteger.TEN).add(BigInteger.valueOf(digit));
263                 }
264                 else
265                 {
266                     total = total * 10 + digit;
267                 }
268             }
269             if (subsequentWidth > 0 && pass == 0)
270             {
271                 // re-parse now we know the correct width
272                 int parseLen = pos - position;
273                 effMaxWidth = MathHelper.max(effMinWidth, parseLen - subsequentWidth);
274                 pos = position;
275                 total = 0;
276                 totalBig = null;
277             }
278             else
279             {
280                 break;
281             }
282         }
283         if (negative)
284         {
285             if (totalBig !is null)
286             {
287                 if (totalBig.equals(BigInteger.ZERO) && context.isStrict())
288                 {
289                     return ~(position - 1); // minus zero not allowed
290                 }
291                 totalBig = totalBig.negate();
292             }
293             else
294             {
295                 if (total == 0 && context.isStrict())
296                 {
297                     return ~(position - 1); // minus zero not allowed
298                 }
299                 total = -total;
300             }
301         }
302         else if (signStyle == SignStyle.EXCEEDS_PAD && context.isStrict())
303         {
304             int parseLen = pos - position;
305             if (positive)
306             {
307                 if (parseLen <= minWidth)
308                 {
309                     return ~(position - 1); // '+' only parsed if minWidth exceeded
310                 }
311             }
312             else
313             {
314                 if (parseLen > minWidth)
315                 {
316                     return ~position; // '+' must be parsed if minWidth exceeded
317                 }
318             }
319         }
320         if (totalBig !is null)
321         {
322             if (totalBig.bitLength() > 63)
323             {
324                 // overflow, parse 1 less digit
325                 totalBig = totalBig.divide(BigInteger.TEN);
326                 pos--;
327             }
328             return setValue(context, totalBig.longValue(), position, pos);
329         }
330         return setValue(context, total, position, pos);
331     }
332 
333     /**
334      * Stores the value.
335      *
336      * @param context  the context to store into, not null
337      * @param value  the value
338      * @param errorPos  the position of the field being parsed
339      * @param successPos  the position after the field being parsed
340      * @return the new position
341      */
342     int setValue(DateTimeParseContext context, long value, int errorPos, int successPos)
343     {
344         return context.setParsedField(field, value, errorPos, successPos);
345     }
346 
347     override public string toString()
348     {
349         if (minWidth == 1 && maxWidth == 19 && signStyle.name() == SignStyle.NORMAL.name())
350         {
351             return "Value(" ~ typeid(field).name ~ ")";
352         }
353         if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE)
354         {
355             return "Value(" ~ typeid(field).name ~ "," ~ minWidth.to!string ~ ")";
356         }
357         return "Value(" ~ typeid(field)
358             .name ~ "," ~ minWidth.to!string ~ "," ~ maxWidth.to!string ~ "," ~ signStyle.name()
359             ~ ")";
360     }
361 }