1 module hunt.time.format.ReducedPrinterParser;
2 
3 import hunt.time.Exceptions;
4 import hunt.time.LocalDate;
5 
6 import hunt.time.chrono.Chronology;
7 import hunt.time.chrono.ChronoLocalDate;
8 
9 import hunt.time.format.DateTimeParseContext;
10 import hunt.time.format.DateTimePrinterParser;
11 import hunt.time.format.DateTimePrintContext;
12 import hunt.time.format.NumberPrinterParser;
13 import hunt.time.format.SignStyle;
14 import hunt.time.temporal.TemporalField;
15 import hunt.time.util.Common;
16 import hunt.util.StringBuilder;
17 
18 import hunt.Exceptions;
19 import hunt.Integer;
20 import hunt.math.Helper;
21 import hunt.Functions;
22 
23 import std.conv;
24 
25 //-----------------------------------------------------------------------
26 /**
27  * Prints and parses a reduced numeric date-time field.
28  */
29 final class ReducedPrinterParser : NumberPrinterParser
30 {
31     /**
32      * The base date for reduced value parsing.
33      */
34     // __gshared LocalDate BASE_DATE;
35 
36     private int baseValue;
37     private ChronoLocalDate baseDate;
38 
39     // shared static this()
40     // {
41     //     BASE_DATE = LocalDate.of(2000, 1, 1);
42         mixin(MakeGlobalVar!(LocalDate)("BASE_DATE",`LocalDate.of(2000, 1, 1)`));
43     // }
44 
45     /**
46      * Constructor.
47      *
48      * @param field  the field to format, validated not null
49      * @param minWidth  the minimum field width, from 1 to 10
50      * @param maxWidth  the maximum field width, from 1 to 10
51      * @param baseValue  the base value
52      * @param baseDate  the base date
53      */
54     this(TemporalField field, int minWidth, int maxWidth, int baseValue,
55             ChronoLocalDate baseDate)
56     {
57         this(field, minWidth, maxWidth, baseValue, baseDate, 0);
58         if (minWidth < 1 || minWidth > 10)
59         {
60             throw new IllegalArgumentException(
61                     "The minWidth must be from 1 to 10 inclusive but was " ~ minWidth
62                     .to!string);
63         }
64         if (maxWidth < 1 || maxWidth > 10)
65         {
66             throw new IllegalArgumentException(
67                     "The maxWidth must be from 1 to 10 inclusive but was " ~ minWidth
68                     .to!string);
69         }
70         if (maxWidth < minWidth)
71         {
72             throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but "
73                     ~ maxWidth.to!string ~ " < " ~ minWidth.to!string);
74         }
75         if (baseDate is null)
76         {
77             if (field.range().isValidValue(baseValue) == false)
78             {
79                 throw new IllegalArgumentException(
80                         "The base value must be within the range of the field");
81             }
82             if (((cast(long) baseValue) + EXCEED_POINTS[maxWidth]) > Integer.MAX_VALUE)
83             {
84                 throw new DateTimeException(
85                         "Unable to add printer-parser as the range exceeds the capacity of an int");
86             }
87         }
88     }
89 
90     /**
91      * Constructor.
92      * The arguments have already been checked.
93      *
94      * @param field  the field to format, validated not null
95      * @param minWidth  the minimum field width, from 1 to 10
96      * @param maxWidth  the maximum field width, from 1 to 10
97      * @param baseValue  the base value
98      * @param baseDate  the base date
99      * @param subsequentWidth the subsequentWidth for this instance
100      */
101     package this(TemporalField field, int minWidth, int maxWidth,
102             int baseValue, ChronoLocalDate baseDate, int subsequentWidth)
103     {
104         super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
105         this.baseValue = baseValue;
106         this.baseDate = baseDate;
107     }
108 
109     override long getValue(DateTimePrintContext context, long value)
110     {
111         long absValue = MathHelper.abs(value);
112         int baseValue = this.baseValue;
113         if (baseDate !is null)
114         {
115             Chronology chrono = Chronology.from(context.getTemporal());
116             baseValue = chrono.date(baseDate).get(field);
117         }
118         if (value >= baseValue && value < baseValue + EXCEED_POINTS[minWidth])
119         {
120             // Use the reduced value if it fits _in minWidth
121             return absValue % EXCEED_POINTS[minWidth];
122         }
123         // Otherwise truncate to fit _in maxWidth
124         return absValue % EXCEED_POINTS[maxWidth];
125     }
126 
127     override int setValue(DateTimeParseContext context, long value, int errorPos, int successPos)
128     {
129         int baseValue = this.baseValue;
130         if (baseDate !is null)
131         {
132             Chronology chrono = context.getEffectiveChronology();
133             baseValue = chrono.date(baseDate).get(field);
134 
135             // In case the Chronology is changed later, add a callback when/if it changes
136             long initialValue = value;
137             context.addChronoChangedListener( (Chronology t) {
138                 /* Repeat the set of the field using the current Chronology
139                  * The success/error position is ignored because the value is
140                  * intentionally being overwritten.
141                  */
142                 setValue(context, initialValue, errorPos, successPos);
143             });
144         }
145         int parseLen = successPos - errorPos;
146         if (parseLen == minWidth && value >= 0)
147         {
148             long range = EXCEED_POINTS[minWidth];
149             long lastPart = baseValue % range;
150             long basePart = baseValue - lastPart;
151             if (baseValue > 0)
152             {
153                 value = basePart + value;
154             }
155             else
156             {
157                 value = basePart - value;
158             }
159             if (value < baseValue)
160             {
161                 value += range;
162             }
163         }
164         return context.setParsedField(field, value, errorPos, successPos);
165     }
166 
167     /**
168  * Returns a new instance with fixed width flag set.
169  *
170  * @return a new updated printer-parser, not null
171  */
172     override ReducedPrinterParser withFixedWidth()
173     {
174         if (subsequentWidth == -1)
175         {
176             return this;
177         }
178         return new ReducedPrinterParser(field, minWidth, maxWidth,
179                 baseValue, baseDate, -1);
180     }
181 
182     /**
183  * Returns a new instance with an updated subsequent width.
184  *
185  * @param subsequentWidth  the width of subsequent non-negative numbers, 0 or greater
186  * @return a new updated printer-parser, not null
187  */
188     override ReducedPrinterParser withSubsequentWidth(int subsequentWidth)
189     {
190         return new ReducedPrinterParser(field, minWidth, maxWidth,
191                 baseValue, baseDate, this.subsequentWidth + subsequentWidth);
192     }
193 
194     /**
195  * For a ReducedPrinterParser, fixed width is false if the mode is strict,
196  * otherwise it is set as for NumberPrinterParser.
197  * @param context the context
198  * @return if the field is fixed width
199  * @see DateTimeFormatterBuilder#appendValueReduced(hunt.time.temporal.TemporalField, int, int, int)
200  */
201     override bool isFixedWidth(DateTimeParseContext context)
202     {
203         if (context.isStrict() == false)
204         {
205             return false;
206         }
207         return super.isFixedWidth(context);
208     }
209 
210     override public string toString()
211     {
212         return "ReducedValue(" ~ typeid(field)
213             .name ~ "," ~ minWidth.to!string ~ "," ~ maxWidth.to!string ~ "," ~ typeid(baseDate)
214             .name ~ ")";
215     }
216 }