001/** 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2015-2016 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 java.io.IOException; 027 028import org.decimal4j.api.DecimalArithmetic; 029import org.decimal4j.scale.ScaleMetrics; 030import org.decimal4j.scale.Scales; 031import org.decimal4j.truncate.DecimalRounding; 032import org.decimal4j.truncate.TruncatedPart; 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); 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, 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, 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); 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); 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); 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); 242 } 243 i++; 244 } 245 246 final int end2 = end - 1; 247 while (i < end2) { 248 final int digit0 = getDigit(arith, s, s.charAt(i++)); 249 final int digit1 = getDigit(arith, s, 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); 253 } 254 result *= 100; 255 if (result < limit + inc) { 256 throw newNumberFormatExceptionFor(arith, s); 257 } 258 result -= inc; 259 } 260 if (i < end) { 261 final int digit = getDigit(arith, s, s.charAt(i++)); 262 if (result < (-Long.MAX_VALUE / 10)) {//same limit with Long.MIN_VALUE 263 throw newNumberFormatExceptionFor(arith, s); 264 } 265 result *= 10; 266 if (result < limit + digit) { 267 throw newNumberFormatExceptionFor(arith, s); 268 } 269 result -= digit; 270 } 271 } else { 272 throw newNumberFormatExceptionFor(arith, s); 273 } 274 return negative ? result : -result; 275 } 276 277 private static final int getDigit(final DecimalArithmetic arith, final CharSequence s, final char ch) { 278 if (ch >= '0' & ch <= '9') { 279 return (int) (ch - '0'); 280 } else { 281 throw newNumberFormatExceptionFor(arith, s); 282 } 283 } 284 285 private static final int[] TENS = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90}; 286 287 /** 288 * Returns a {@code String} object representing the specified {@code long}. The argument is converted to signed 289 * decimal representation and returned as a string, exactly as if passed to {@link Long#toString(long)}. 290 * 291 * @param value 292 * a {@code long} to be converted. 293 * @return a string representation of the argument in base 10. 294 */ 295 static final String longToString(long value) { 296 return Long.toString(value); 297 } 298 299 /** 300 * Creates a {@code String} object representing the specified {@code long} and appends it to the given 301 * {@code appendable}. 302 * 303 * @param value 304 * a {@code long} to be converted. 305 * @param appendable 306 * t the appendable to which the string is to be appended 307 * @throws IOException 308 * If an I/O error occurs when appending to {@code appendable} 309 */ 310 static final void longToString(long value, Appendable appendable) throws IOException { 311 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 312 sb.setLength(0); 313 sb.append(value); 314 appendable.append(sb); 315 } 316 317 /** 318 * Returns a {@code String} object representing the specified unscaled Decimal value {@code uDecimal}. The argument 319 * is converted to signed decimal representation and returned as a string with {@code scale} decimal places event if 320 * trailing fraction digits are zero. 321 * 322 * @param uDecimal 323 * a unscaled Decimal to be converted 324 * @param arith 325 * the decimal arithmetics providing the scale to apply 326 * @return a string representation of the argument 327 */ 328 static final String unscaledToString(DecimalArithmetic arith, long uDecimal) { 329 return unscaledToStringBuilder(arith, uDecimal).toString(); 330 } 331 332 /** 333 * Constructs a {@code String} object representing the specified unscaled Decimal value {@code uDecimal} and appends 334 * the constructed string to the given appendable argument. The value is converted to signed decimal representation 335 * and converted to a string with {@code scale} decimal places event if trailing fraction digits are zero. 336 * 337 * @param uDecimal 338 * a unscaled Decimal to be converted to a string 339 * @param arith 340 * the decimal arithmetics providing the scale to apply 341 * @param appendable 342 * t the appendable to which the string is to be appended 343 * @throws IOException 344 * If an I/O error occurs when appending to {@code appendable} 345 */ 346 static final void unscaledToString(DecimalArithmetic arith, long uDecimal, Appendable appendable) throws IOException { 347 final StringBuilder sb = unscaledToStringBuilder(arith, uDecimal); 348 appendable.append(sb); 349 } 350 351 private static final StringBuilder unscaledToStringBuilder(DecimalArithmetic arith, long uDecimal) { 352 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 353 sb.setLength(0); 354 355 final int scale = arith.getScale(); 356 sb.append(uDecimal); 357 final int len = sb.length(); 358 final int negativeOffset = uDecimal < 0 ? 1 : 0; 359 if (len <= scale + negativeOffset) { 360 // Long.MAX_VALUE = 9,223,372,036,854,775,807 361 sb.insert(negativeOffset, "0.00000000000000000000", 0, 2 + scale - len + negativeOffset); 362 } else { 363 sb.insert(len - scale, '.'); 364 } 365 return sb; 366 } 367 368 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s) { 369 return new NumberFormatException( 370 "Cannot parse Decimal value with scale " + arith.getScale() + " for input string: \"" + s + "\""); 371 } 372 373 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s, Exception cause) { 374 final NumberFormatException ex = newNumberFormatExceptionFor(arith, s); 375 ex.initCause(cause); 376 return ex; 377 } 378 379 // no instances 380 private StringConversion() { 381 super(); 382 } 383}