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 }