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