001/** 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2015 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 char ch = s.charAt(i++); 161 final int digit; 162 if (ch >= '0' & ch <= '9') { 163 digit = (int) (ch - '0'); 164 } else { 165 throw newNumberFormatExceptionFor(arith, s); 166 } 167 value = value * 10 + digit; 168 } 169 final int scale = arith.getScale(); 170 if (len < scale) { 171 final ScaleMetrics diffScale = Scales.getScaleMetrics(scale - len); 172 return diffScale.multiplyByScaleFactor(value); 173 } 174 return value; 175 } 176 return 0; 177 } 178 179 private static final TruncatedPart parseTruncatedPart(DecimalArithmetic arith, CharSequence s, int start, int end) { 180 if (start < end) { 181 final char firstChar = s.charAt(start); 182 TruncatedPart truncatedPart; 183 if (firstChar == '0') { 184 truncatedPart = TruncatedPart.ZERO; 185 } else if (firstChar == '5') { 186 truncatedPart = TruncatedPart.EQUAL_TO_HALF; 187 } else if (firstChar > '0' & firstChar < '5') { 188 truncatedPart = TruncatedPart.LESS_THAN_HALF_BUT_NOT_ZERO; 189 } else if (firstChar > '5' & firstChar <= '9') { 190 truncatedPart = TruncatedPart.GREATER_THAN_HALF; 191 } else { 192 throw newNumberFormatExceptionFor(arith, s); 193 } 194 int i = start + 1; 195 while (i < end) { 196 final char ch = s.charAt(i++); 197 if (ch > '0' & ch <= '9') { 198 if (truncatedPart == TruncatedPart.ZERO) { 199 truncatedPart = TruncatedPart.LESS_THAN_HALF_BUT_NOT_ZERO; 200 } else if (truncatedPart == TruncatedPart.EQUAL_TO_HALF) { 201 truncatedPart = TruncatedPart.GREATER_THAN_HALF; 202 } 203 } else if (ch != '0') { 204 throw newNumberFormatExceptionFor(arith, s); 205 } 206 } 207 return truncatedPart; 208 } 209 return TruncatedPart.ZERO; 210 } 211 212 private static final int indexOfDecimalPoint(CharSequence s, int start, int end) { 213 for (int i = start; i < end; i++) { 214 if (s.charAt(i) == '.') { 215 return i; 216 } 217 } 218 return -1; 219 } 220 221 // copied from Long.parseLong(String, int) but for fixed radix 10 222 private static final long parseIntegralPart(DecimalArithmetic arith, CharSequence s, int start, int end, ParseMode mode) { 223 long result = 0; 224 boolean negative = false; 225 int i = start; 226 long limit = -Long.MAX_VALUE; 227 long multmin; 228 229 if (end > start) { 230 char firstChar = s.charAt(start); 231 if (firstChar < '0') { // Possible leading "+" or "-" 232 if (firstChar == '-') { 233 negative = true; 234 limit = Long.MIN_VALUE; 235 } else if (firstChar != '+') { 236 // invalid first character 237 throw newNumberFormatExceptionFor(arith, s); 238 } 239 240 if (end - start == 1) { 241 if (mode == ParseMode.IntegralPart) { 242 // we allow something like "-.75" or "+.75" 243 return 0; 244 } 245 // Cannot have lone "+" or "-" 246 throw newNumberFormatExceptionFor(arith, s); 247 } 248 i++; 249 } 250 multmin = limit / 10; 251 while (i < end) { 252 // Accumulating negatively avoids surprises near MAX_VALUE 253 final char ch = s.charAt(i++); 254 final int digit; 255 if (ch >= '0' & ch <= '9') { 256 digit = (int) (ch - '0'); 257 } else { 258 throw newNumberFormatExceptionFor(arith, s); 259 } 260 if (result < multmin) { 261 throw newNumberFormatExceptionFor(arith, s); 262 } 263 result *= 10; 264 if (result < limit + digit) { 265 throw newNumberFormatExceptionFor(arith, s); 266 } 267 result -= digit; 268 } 269 } else { 270 throw newNumberFormatExceptionFor(arith, s); 271 } 272 return negative ? result : -result; 273 } 274 275 /** 276 * Returns a {@code String} object representing the specified {@code long}. The argument is converted to signed 277 * decimal representation and returned as a string, exactly as if passed to {@link Long#toString(long)}. 278 * 279 * @param value 280 * a {@code long} to be converted. 281 * @return a string representation of the argument in base 10. 282 */ 283 static final String longToString(long value) { 284 return Long.toString(value); 285 } 286 287 /** 288 * Creates a {@code String} object representing the specified {@code long} and appends it to the given 289 * {@code appendable}. 290 * 291 * @param value 292 * a {@code long} to be converted. 293 * @param appendable 294 * t the appendable to which the string is to be appended 295 * @throws IOException 296 * If an I/O error occurs when appending to {@code appendable} 297 */ 298 static final void longToString(long value, Appendable appendable) throws IOException { 299 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 300 sb.setLength(0); 301 sb.append(value); 302 appendable.append(sb); 303 } 304 305 /** 306 * Returns a {@code String} object representing the specified unscaled Decimal value {@code uDecimal}. The argument 307 * is converted to signed decimal representation and returned as a string with {@code scale} decimal places event if 308 * trailing fraction digits are zero. 309 * 310 * @param uDecimal 311 * a unscaled Decimal to be converted 312 * @param arith 313 * the decimal arithmetics providing the scale to apply 314 * @return a string representation of the argument 315 */ 316 static final String unscaledToString(DecimalArithmetic arith, long uDecimal) { 317 return unscaledToStringBuilder(arith, uDecimal).toString(); 318 } 319 320 /** 321 * Constructs a {@code String} object representing the specified unscaled Decimal value {@code uDecimal} and appends 322 * the constructed string to the given appendable argument. The value is converted to signed decimal representation 323 * and converted to a string with {@code scale} decimal places event if trailing fraction digits are zero. 324 * 325 * @param uDecimal 326 * a unscaled Decimal to be converted to a string 327 * @param arith 328 * the decimal arithmetics providing the scale to apply 329 * @param appendable 330 * t the appendable to which the string is to be appended 331 * @throws IOException 332 * If an I/O error occurs when appending to {@code appendable} 333 */ 334 static final void unscaledToString(DecimalArithmetic arith, long uDecimal, Appendable appendable) throws IOException { 335 final StringBuilder sb = unscaledToStringBuilder(arith, uDecimal); 336 appendable.append(sb); 337 } 338 339 private static final StringBuilder unscaledToStringBuilder(DecimalArithmetic arith, long uDecimal) { 340 final StringBuilder sb = STRING_BUILDER_THREAD_LOCAL.get(); 341 sb.setLength(0); 342 343 final int scale = arith.getScale(); 344 sb.append(uDecimal); 345 final int len = sb.length(); 346 final int negativeOffset = uDecimal < 0 ? 1 : 0; 347 if (len <= scale + negativeOffset) { 348 // Long.MAX_VALUE = 9,223,372,036,854,775,807 349 sb.insert(negativeOffset, "0.00000000000000000000", 0, 2 + scale - len + negativeOffset); 350 } else { 351 sb.insert(len - scale, '.'); 352 } 353 return sb; 354 } 355 356 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s) { 357 return new NumberFormatException( 358 "Cannot parse Decimal value with scale " + arith.getScale() + " for input string: \"" + s + "\""); 359 } 360 361 private static final NumberFormatException newNumberFormatExceptionFor(DecimalArithmetic arith, CharSequence s, Exception cause) { 362 final NumberFormatException ex = newNumberFormatExceptionFor(arith, s); 363 ex.initCause(cause); 364 return ex; 365 } 366 367 // no instances 368 private StringConversion() { 369 super(); 370 } 371}