001/* 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2015-2023 decimal4j (tools4j), Marco Terzer 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024package org.decimal4j.arithmetic; 025 026import org.decimal4j.api.DecimalArithmetic; 027import org.decimal4j.scale.ScaleMetrics; 028import org.decimal4j.scale.Scales; 029import org.decimal4j.truncate.DecimalRounding; 030import org.decimal4j.truncate.TruncatedPart; 031 032import java.io.IOException; 033 034/** 035 * Contains methods to convert from and to String. 036 */ 037final class StringConversion { 038 039 /** 040 * Thread-local used to build Decimal strings. Allocated big enough to avoid growth. 041 */ 042 static final ThreadLocal<StringBuilder> STRING_BUILDER_THREAD_LOCAL = new ThreadLocal<StringBuilder>() { 043 @Override 044 protected StringBuilder initialValue() { 045 return new StringBuilder(19 + 1 + 2);// unsigned long: 19 digits, 046 // sign: 1, decimal point 047 // and leading 0: 2 048 } 049 }; 050 051 private static enum ParseMode { 052 Long, IntegralPart; 053 } 054 055 /** 056 * Parses the given string into a long and returns it, rounding extra digits if necessary. 057 * 058 * @param arith 059 * the arithmetic of the target value 060 * @param rounding 061 * the rounding to apply if a fraction is present 062 * @param s 063 * the string to parse 064 * @param start 065 * the start index to read characters in {@code s}, inclusive 066 * @param end 067 * the end index where to stop reading in characters in {@code s}, exclusive 068 * @return the parsed value 069 * @throws IndexOutOfBoundsException 070 * if {@code start < 0} or {@code end > s.length()} 071 * @throws NumberFormatException 072 * if {@code value} does not represent a valid {@code Decimal} or if the value is too large to be 073 * represented as a long 074 */ 075 static final long parseLong(DecimalArithmetic arith, DecimalRounding rounding, CharSequence s, int start, int end) { 076 return parseUnscaledDecimal(arith, rounding, s, start, end); 077 } 078 079 /** 080 * Parses the given string into an unscaled decimal and returns it, rounding extra digits if necessary. 081 * 082 * @param arith 083 * the arithmetic of the target value 084 * @param rounding 085 * the rounding to apply if extra fraction digits are present 086 * @param s 087 * the string to parse 088 * @param start 089 * the start index to read characters in {@code s}, inclusive 090 * @param end 091 * the end index where to stop reading in characters in {@code s}, exclusive 092 * @return the parsed value 093 * @throws IndexOutOfBoundsException 094 * if {@code start < 0} or {@code end > s.length()} 095 * @throws NumberFormatException 096 * if {@code value} does not represent a valid {@code Decimal} or if the value is too large to be 097 * represented as a Decimal with the scale of the given arithmetic 098 */ 099 static final long parseUnscaledDecimal(DecimalArithmetic arith, DecimalRounding rounding, CharSequence s, int start, int end) { 100 if (start < 0 | end > s.length()) { 101 throw new IndexOutOfBoundsException("Start or end index is out of bounds: [" + start + ", " + end 102 + " must be <= [0, " + s.length() + "]"); 103 } 104 final ScaleMetrics scaleMetrics = arith.getScaleMetrics(); 105 final int scale = scaleMetrics.getScale(); 106 final int indexOfDecimalPoint = indexOfDecimalPoint(s, start, end); 107 if (indexOfDecimalPoint == end & scale > 0) { 108 throw newNumberFormatExceptionFor(arith, s, start, end); 109 } 110 111 // parse a decimal number 112 final long integralPart;// unscaled 113 final long fractionalPart;// scaled 114 final TruncatedPart truncatedPart; 115 final boolean negative; 116 if (indexOfDecimalPoint < 0) { 117 integralPart = parseIntegralPart(arith, s, start, end, ParseMode.Long); 118 fractionalPart = 0; 119 truncatedPart = TruncatedPart.ZERO; 120 negative = integralPart < 0; 121 } else { 122 final int fractionalEnd = Math.min(end, indexOfDecimalPoint + 1 + scale); 123 if (indexOfDecimalPoint == start) { 124 // allowed format .45 125 integralPart = 0; 126 fractionalPart = parseFractionalPart(arith, s, start + 1, fractionalEnd); 127 truncatedPart = parseTruncatedPart(arith, s, fractionalEnd, end); 128 negative = false; 129 } else { 130 // allowed formats: "0.45", "+0.45", "-0.45", ".45", "+.45", 131 // "-.45" 132 integralPart = parseIntegralPart(arith, s, start, indexOfDecimalPoint, ParseMode.IntegralPart); 133 fractionalPart = parseFractionalPart(arith, s, indexOfDecimalPoint + 1, fractionalEnd); 134 truncatedPart = parseTruncatedPart(arith, s, fractionalEnd, end); 135 negative = integralPart < 0 | (integralPart == 0 && s.charAt(start) == '-'); 136 } 137 } 138 if (truncatedPart.isGreaterThanZero() & rounding == DecimalRounding.UNNECESSARY) { 139 throw Exceptions.newRoundingNecessaryArithmeticException(); 140 } 141 try { 142 final long unscaledIntegeral = scaleMetrics.multiplyByScaleFactorExact(integralPart); 143 final long unscaledFractional = negative ? -fractionalPart : fractionalPart;// < Scale18.SCALE_FACTOR hence 144 // no overflow 145 final long truncatedValue = Checked.add(arith, unscaledIntegeral, unscaledFractional); 146 final int roundingIncrement = rounding.calculateRoundingIncrement(negative ? -1 : 1, truncatedValue, 147 truncatedPart); 148 return roundingIncrement == 0 ? truncatedValue : Checked.add(arith, truncatedValue, roundingIncrement); 149 } catch (ArithmeticException e) { 150 throw newNumberFormatExceptionFor(arith, s, start, end, e); 151 } 152 } 153 154 private static final long parseFractionalPart(DecimalArithmetic arith, CharSequence s, int start, int end) { 155 final int len = end - start; 156 if (len > 0) { 157 int i = start; 158 long value = 0; 159 while (i < end) { 160 final int digit = getDigit(arith, s, start, end, s.charAt(i++)); 161 value = value * 10 + digit; 162 } 163 final int scale = arith.getScale(); 164 if (len < scale) { 165 final ScaleMetrics diffScale = Scales.getScaleMetrics(scale - len); 166 return diffScale.multiplyByScaleFactor(value); 167 } 168 return value; 169 } 170 return 0; 171 } 172 173 private static final TruncatedPart parseTruncatedPart(DecimalArithmetic arith, CharSequence s, int start, int end) { 174 if (start < end) { 175 final char firstChar = s.charAt(start); 176 TruncatedPart truncatedPart; 177 if (firstChar == '0') { 178 truncatedPart = TruncatedPart.ZERO; 179 } else if (firstChar == '5') { 180 truncatedPart = TruncatedPart.EQUAL_TO_HALF; 181 } else if (firstChar > '0' & firstChar < '5') { 182 truncatedPart = TruncatedPart.LESS_THAN_HALF_BUT_NOT_ZERO; 183 } else if (firstChar > '5' & firstChar <= '9') { 184 truncatedPart = TruncatedPart.GREATER_THAN_HALF; 185 } else { 186 throw newNumberFormatExceptionFor(arith, s, start, end); 187 } 188 int i = start + 1; 189 while (i < end) { 190 final char ch = s.charAt(i++); 191 if (ch > '0' & ch <= '9') { 192 if (truncatedPart == TruncatedPart.ZERO) { 193 truncatedPart = TruncatedPart.LESS_THAN_HALF_BUT_NOT_ZERO; 194 } else if (truncatedPart == TruncatedPart.EQUAL_TO_HALF) { 195 truncatedPart = TruncatedPart.GREATER_THAN_HALF; 196 } 197 } else if (ch != '0') { 198 throw newNumberFormatExceptionFor(arith, s, start, end); 199 } 200 } 201 return truncatedPart; 202 } 203 return TruncatedPart.ZERO; 204 } 205 206 private static final int indexOfDecimalPoint(CharSequence s, int start, int end) { 207 for (int i = start; i < end; i++) { 208 if (s.charAt(i) == '.') { 209 return i; 210 } 211 } 212 return -1; 213 } 214 215 // copied from Long.parseLong(String, int) but for fixed radix 10 216 private static final long parseIntegralPart(DecimalArithmetic arith, CharSequence s, int start, int end, ParseMode mode) { 217 long result = 0; 218 boolean negative = false; 219 int i = start; 220 long limit = -Long.MAX_VALUE; 221 222 if (end > start) { 223 char firstChar = s.charAt(start); 224 if (firstChar < '0') { // Possible leading "+" or "-" 225 if (firstChar == '-') { 226 negative = true; 227 limit = Long.MIN_VALUE; 228 } else { 229 if (firstChar != '+') { 230 // invalid first character 231 throw newNumberFormatExceptionFor(arith, s, start, end); 232 } 233 } 234 235 if (end - start == 1) { 236 if (mode == ParseMode.IntegralPart) { 237 // we allow something like "-.75" or "+.75" 238 return 0; 239 } 240 // Cannot have lone "+" or "-" 241 throw newNumberFormatExceptionFor(arith, s, start, end); 242 } 243 i++; 244 } 245 246 final int end2 = end - 1; 247 while (i < end2) { 248 final int digit0 = getDigit(arith, s, start, end, s.charAt(i++)); 249 final int digit1 = getDigit(arith, s, start, end, s.charAt(i++)); 250 final int inc = TENS[digit0] + digit1; 251 if (result < (-Long.MAX_VALUE / 100)) {//same limit with Long.MIN_VALUE 252 throw newNumberFormatExceptionFor(arith, s, start, end); 253 } 254 result *= 100; 255 if (result < limit + inc) { 256 throw newNumberFormatExceptionFor(arith, s, start, end); 257 } 258 result -= inc; 259 } 260 if (i < end) { 261 final int digit = getDigit(arith, s, start, end, s.charAt(i++)); 262 if (result < (-Long.MAX_VALUE / 10)) {//same limit with Long.MIN_VALUE 263 throw newNumberFormatExceptionFor(arith, s, start, end); 264 } 265 result *= 10; 266 if (result < limit + digit) { 267 throw newNumberFormatExceptionFor(arith, s, start, end); 268 } 269 result -= digit; 270 } 271 } else { 272 throw newNumberFormatExceptionFor(arith, s, start, end); 273 } 274 return negative ? result : -result; 275 } 276 277 private static final int getDigit(final DecimalArithmetic arith, final CharSequence s, 278 final int start, final int end, final char ch) { 279 if (ch >= '0' & ch <= '9') { 280 return (int) (ch - '0'); 281 } else { 282 throw newNumberFormatExceptionFor(arith, s, start, end); 283 } 284 } 285 286 private static final int[] TENS = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90}; 287 288 /** 289 * Returns a {@code String} object representing the specified {@code long}. The argument is converted to signed 290 * decimal representation and returned as a string, exactly as if passed to {@link Long#toString(long)}. 291 * 292 * @param value 293 * a {@code long} to be converted. 294 * @return a string representation of the argument in base 10. 295 */ 296 static final String longToString(long value) { 297 return Long.toString(value); 298 } 299 300 /** 301 * Creates a {@code String} object representing the specified {@code long} and appends it to the given 302 * {@code appendable}. 303 * 304 * @param value 305 * a {@code long} to be converted. 306 * @param appendable 307 * t the appendable to which the string is to be appended 308 * @throws IOException 309 * If an I/O error occurs when appending to {@code appendable} 310 */ 311 static final void longToString(long value, Appendable appendable) throws IOException { 312 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 313 sb.setLength(0); 314 sb.append(value); 315 appendable.append(sb); 316 } 317 318 /** 319 * Returns a {@code String} object representing the specified unscaled Decimal value {@code uDecimal}. The argument 320 * is converted to signed decimal representation and returned as a string with {@code scale} decimal places event if 321 * trailing fraction digits are zero. 322 * 323 * @param uDecimal 324 * a unscaled Decimal to be converted 325 * @param arith 326 * the decimal arithmetics providing the scale to apply 327 * @return a string representation of the argument 328 */ 329 static final String unscaledToString(DecimalArithmetic arith, long uDecimal) { 330 return unscaledToStringBuilder(arith, uDecimal).toString(); 331 } 332 333 /** 334 * Constructs a {@code String} object representing the specified unscaled Decimal value {@code uDecimal} and appends 335 * the constructed string to the given appendable argument. The value is converted to signed decimal representation 336 * and converted to a string with {@code scale} decimal places event if trailing fraction digits are zero. 337 * 338 * @param uDecimal 339 * a unscaled Decimal to be converted to a string 340 * @param arith 341 * the decimal arithmetics providing the scale to apply 342 * @param appendable 343 * t the appendable to which the string is to be appended 344 * @throws IOException 345 * If an I/O error occurs when appending to {@code appendable} 346 */ 347 static final void unscaledToString(DecimalArithmetic arith, long uDecimal, Appendable appendable) throws IOException { 348 final StringBuilder sb = unscaledToStringBuilder(arith, uDecimal); 349 appendable.append(sb); 350 } 351 352 private static final StringBuilder unscaledToStringBuilder(DecimalArithmetic arith, long uDecimal) { 353 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 354 sb.setLength(0); 355 356 final int scale = arith.getScale(); 357 sb.append(uDecimal); 358 final int len = sb.length(); 359 final int negativeOffset = uDecimal < 0 ? 1 : 0; 360 if (len <= scale + negativeOffset) { 361 // Long.MAX_VALUE = 9,223,372,036,854,775,807 362 sb.insert(negativeOffset, "0.00000000000000000000", 0, 2 + scale - len + negativeOffset); 363 } else { 364 sb.insert(len - scale, '.'); 365 } 366 return sb; 367 } 368 369 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s, int start, int end) { 370 return new NumberFormatException( 371 "Cannot parse Decimal value with scale " + arith.getScale() + " for input string: \"" + s.subSequence(start, end) + "\""); 372 } 373 374 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s, int start, int end, Exception cause) { 375 final NumberFormatException ex = newNumberFormatExceptionFor(arith, s, start, end); 376 ex.initCause(cause); 377 return ex; 378 } 379 380 // no instances 381 private StringConversion() { 382 super(); 383 } 384}