1 module hunt.time.format.FractionPrinterParser;
2 
3 import hunt.time.format.DateTimeParseContext;
4 import hunt.time.format.DateTimePrintContext;
5 import hunt.time.format.DecimalStyle;
6 import hunt.time.format.NumberPrinterParser;
7 import hunt.time.format.SignStyle;
8 import hunt.time.temporal.TemporalField;
9 import hunt.util.StringBuilder;
10 
11 import hunt.Exceptions;
12 import hunt.Long;
13 import hunt.math.BigDecimal;
14 import hunt.math.Helper;
15 import hunt.text.Common;
16 
17 import std.conv;
18 
19 
20 //-----------------------------------------------------------------------
21 /**
22 * Prints and parses a numeric date-time field with optional padding.
23 */
24 static final class FractionPrinterParser : NumberPrinterParser
25 {
26     private bool decimalPoint;
27 
28     /**
29  * Constructor.
30  *
31  * @param field  the field to output, not null
32  * @param minWidth  the minimum width to output, from 0 to 9
33  * @param maxWidth  the maximum width to output, from 0 to 9
34  * @param decimalPoint  whether to output the localized decimal point symbol
35  */
36     this(TemporalField field, int minWidth, int maxWidth, bool decimalPoint)
37     {
38         this(field, minWidth, maxWidth, decimalPoint, 0);
39         assert(field, "field");
40         if (field.range().isFixed() == false)
41         {
42             throw new IllegalArgumentException(
43                     "Field must have a fixed set of values: " ~ typeid(field).name);
44         }
45         if (minWidth < 0 || minWidth > 9)
46         {
47             throw new IllegalArgumentException(
48                     "Minimum width must be from 0 to 9 inclusive but was "
49                     ~ minWidth.to!string);
50         }
51         if (maxWidth < 1 || maxWidth > 9)
52         {
53             throw new IllegalArgumentException(
54                     "Maximum width must be from 1 to 9 inclusive but was "
55                     ~ maxWidth.to!string);
56         }
57         if (maxWidth < minWidth)
58         {
59             throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but "
60                     ~ maxWidth.to!string ~ " < " ~ minWidth.to!string);
61         }
62     }
63 
64     /**
65  * Constructor.
66  *
67  * @param field  the field to output, not null
68  * @param minWidth  the minimum width to output, from 0 to 9
69  * @param maxWidth  the maximum width to output, from 0 to 9
70  * @param decimalPoint  whether to output the localized decimal point symbol
71  * @param subsequentWidth the subsequentWidth for this instance
72  */
73     this(TemporalField field, int minWidth, int maxWidth,
74             bool decimalPoint, int subsequentWidth)
75     {
76         super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
77         this.decimalPoint = decimalPoint;
78     }
79 
80     /**
81  * Returns a new instance with fixed width flag set.
82  *
83  * @return a new updated printer-parser, not null
84  */
85     override FractionPrinterParser withFixedWidth()
86     {
87         if (subsequentWidth == -1)
88         {
89             return this;
90         }
91         return new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint, -1);
92     }
93 
94     /**
95  * Returns a new instance with an updated subsequent width.
96  *
97  * @param subsequentWidth  the width of subsequent non-negative numbers, 0 or greater
98  * @return a new updated printer-parser, not null
99  */
100     override FractionPrinterParser withSubsequentWidth(int subsequentWidth)
101     {
102         return new FractionPrinterParser(field, minWidth, maxWidth,
103                 decimalPoint, this.subsequentWidth + subsequentWidth);
104     }
105 
106     /**
107  * For FractionPrinterPrinterParser, the width is fixed if context is sttrict,
108  * minWidth equal to maxWidth and decimalpoint is absent.
109  * @param context the context
110  * @return if the field is fixed width
111  * @see DateTimeFormatterBuilder#appendValueFraction(hunt.time.temporal.TemporalField, int, int, bool)
112  */
113     override bool isFixedWidth(DateTimeParseContext context)
114     {
115         if (context.isStrict() && minWidth == maxWidth && decimalPoint == false)
116         {
117             return true;
118         }
119         return false;
120     }
121 
122     override public bool format(DateTimePrintContext context, StringBuilder buf)
123     {
124         Long value = context.getValue(field);
125         if (value is null)
126         {
127             return false;
128         }
129         DecimalStyle decimalStyle = context.getDecimalStyle();
130         BigDecimal fraction = convertToFraction(value.longValue());
131         if(fraction !is null )
132         {
133             if (fraction.scale() == 0)
134         { // scale is zero if value is zero
135             if (minWidth > 0)
136             {
137                 if (decimalPoint)
138                 {
139                     buf.append(decimalStyle.getDecimalSeparator());
140                 }
141                 for (int i = 0; i < minWidth; i++)
142                 {
143                     buf.append(decimalStyle.getZeroDigit());
144                 }
145             }
146         }
147         else
148         {
149             int outputScale = MathHelper.min(MathHelper.max(fraction.scale(), minWidth), maxWidth);
150             fraction = fraction.setScale(outputScale, RoundingMode.FLOOR.mode());
151             string str = fraction.toPlainString().substring(2);
152             str = decimalStyle.convertNumberToI18N(str);
153             if (decimalPoint)
154             {
155                 buf.append(decimalStyle.getDecimalSeparator());
156             }
157             buf.append(str);
158             }
159         }
160         return true;
161     }
162 
163     override public int parse(DateTimeParseContext context, string text, int position)
164     {
165         int effectiveMin = (context.isStrict() || isFixedWidth(context) ? minWidth : 0);
166         int effectiveMax = (context.isStrict() || isFixedWidth(context) ? maxWidth : 9);
167         int length = cast(int)(text.length);
168         if (position == length)
169         {
170             // valid if whole field is optional, invalid if minimum width
171             return (effectiveMin > 0 ? ~position : position);
172         }
173         if (decimalPoint)
174         {
175             if (text[position] != context.getDecimalStyle().getDecimalSeparator())
176             {
177                 // valid if whole field is optional, invalid if minimum width
178                 return (effectiveMin > 0 ? ~position : position);
179             }
180             position++;
181         }
182         int minEndPos = position + effectiveMin;
183         if (minEndPos > length)
184         {
185             return ~position; // need at least min width digits
186         }
187         int maxEndPos = MathHelper.min(position + effectiveMax, length);
188         int total = 0; // can use int because we are only parsing up to 9 digits
189         int pos = position;
190         while (pos < maxEndPos)
191         {
192             char ch = text[pos++];
193             int digit = context.getDecimalStyle().convertToDigit(ch);
194             if (digit < 0)
195             {
196                 if (pos < minEndPos)
197                 {
198                     return ~position; // need at least min width digits
199                 }
200                 pos--;
201                 break;
202             }
203             total = total * 10 + digit;
204         }
205         BigDecimal fraction = new BigDecimal(total).movePointLeft(pos - position);
206         long value = convertFromFraction(fraction);
207         return context.setParsedField(field, value, position, pos);
208     }
209 
210     /**
211  * Converts a value for this field to a fraction between 0 and 1.
212  * !(p)
213  * The fractional value is between 0 (inclusive) and 1 (exclusive).
214  * It can only be returned if the {@link hunt.time.temporal.TemporalField#range() value range} is fixed.
215  * The fraction is obtained by calculation from the field range using 9 decimal
216  * places and a rounding mode of {@link RoundingMode#FLOOR FLOOR}.
217  * The calculation is inaccurate if the values do not run continuously from smallest to largest.
218  * !(p)
219  * For example, the second-of-minute value of 15 would be returned as 0.25,
220  * assuming the standard definition of 60 seconds _in a minute.
221  *
222  * @param value  the value to convert, must be valid for this rule
223  * @return the value as a fraction within the range, from 0 to 1, not null
224  * @throws DateTimeException if the value cannot be converted to a fraction
225  */
226     private BigDecimal convertToFraction(long value)
227     {
228         ///@gxc
229         // ValueRange range = field.range();
230         // range.checkValidValue(value, field);
231         // BigDecimal minBD = BigDecimal.valueOf(range.getMinimum());
232         // BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE);
233         // BigDecimal valueBD = BigDecimal.valueOf(value).subtract(minBD);
234         // BigDecimal fraction = valueBD.divide(rangeBD, 9, RoundingMode.FLOOR);
235         // // stripTrailingZeros bug
236         // return fraction.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : fraction.stripTrailingZeros();
237         implementationMissing();
238         return null;
239     }
240 
241     /**
242  * Converts a fraction from 0 to 1 for this field to a value.
243  * !(p)
244  * The fractional value must be between 0 (inclusive) and 1 (exclusive).
245  * It can only be returned if the {@link hunt.time.temporal.TemporalField#range() value range} is fixed.
246  * The value is obtained by calculation from the field range and a rounding
247  * mode of {@link RoundingMode#FLOOR FLOOR}.
248  * The calculation is inaccurate if the values do not run continuously from smallest to largest.
249  * !(p)
250  * For example, the fractional second-of-minute of 0.25 would be converted to 15,
251  * assuming the standard definition of 60 seconds _in a minute.
252  *
253  * @param fraction  the fraction to convert, not null
254  * @return the value of the field, valid for this rule
255  * @throws DateTimeException if the value cannot be converted
256  */
257     private long convertFromFraction(BigDecimal fraction)
258     {
259         // ValueRange range = field.range();
260         // BigDecimal minBD = BigDecimal.valueOf(range.getMinimum());
261         // BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE);
262         // BigDecimal valueBD = fraction.multiply(rangeBD).setScale(0, RoundingMode.FLOOR.mode()).add(minBD);
263         // return valueBD.longValueExact();
264         implementationMissing();
265         return long.init;
266     }
267 
268     override public string toString()
269     {
270         string decimal = (decimalPoint ? ",DecimalPoint" : "");
271         return "Fraction(" ~ typeid(field)
272             .name ~ "," ~ minWidth.to!string ~ "," ~ maxWidth.to!string ~ decimal ~ ")";
273     }
274 }