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 }