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&nbsp;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}