/* * Copyright 2014 Mikhail Vorontsov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package info.javaperformance.money; import java.math.BigDecimal; import java.math.BigInteger; /** * Converter from String/double/float/integer types into Money instances. * Requires the precision to be specified for double to Money conversion. Precision is usually based * on the smallest tick in your exchange data. */ public class MoneyFactory { static final int MAX_LONG_LENGTH = Long.toString( Long.MAX_VALUE ).length(); public static final int MAX_ALLOWED_PRECISION = 15; //needed for overflow checking during conversion private static final long MAX_LONG_DIVIDED_BY_10 = Long.MAX_VALUE / 10; /** Non-negative powers of 10 */ static final long[] MULTIPLIERS = new long[ MoneyFactory.MAX_ALLOWED_PRECISION + 1 ]; /** Non-positive powers of 10 */ static final double[] MULTIPLIERS_NEG = new double[ MoneyFactory.MAX_ALLOWED_PRECISION + 1 ]; static { long val = 1; for ( int i = 0; i <= MoneyFactory.MAX_ALLOWED_PRECISION; ++i ) { MULTIPLIERS[ i ] = val; MULTIPLIERS_NEG[ i ] = 1.0 / val; val *= 10; } } static void checkPrecision(int precision) { if ( precision < 0 || precision > MAX_ALLOWED_PRECISION ) throw new IllegalArgumentException( "Precision must be between 0 and " + MAX_ALLOWED_PRECISION ); } /** * Convert from currency units and their precision into Money object. * @param units Currency units (cents, for example) * @param precision Number of digits after decimal point in your smallest possible currency unit. Should be between * 0 and <code>MAX_ALLOWED_PRECISION</code> (inclusive). * @return Money object * @throws java.lang.IllegalArgumentException In case of invalid precision */ public static Money fromUnits( final long units, final int precision ) { checkPrecision( precision ); return new MoneyLong( units, precision ).normalize(); } /** * Same as <code>fromString</code>, but characters are extracted from the given part of char array * @param chars Char array * @param offset Start position * @param length number of characters to process * @return Money object */ public static Money fromCharArray( final char[] chars, final int offset, final int length ) { return fromCharSequence( new CharArraySeq( chars, offset, length ) ); } private static class CharArraySeq implements CharSequence { private final char[] chars; private final int offset; private final int length; private CharArraySeq(char[] chars, int offset, int length) { this.chars = chars; this.offset = offset; this.length = length; } public int length() { return length; } public char charAt( final int index ) { return chars[ offset + index ]; } public CharSequence subSequence(int start, int end) { //not needed for this implemenration return null; } } /** * Same as <code>fromString</code>, but characters are extracted from the given part of byte array. * We expect the data to be ASCII-encoded in the given part of the array. * @param bytes Byte array * @param offset Start position * @param length number of characters to process * @return Money object */ public static Money fromByteArray( final byte[] bytes, final int offset, final int length ) { return fromCharSequence( new ByteArraySeq( bytes, offset, length)); } private static class ByteArraySeq implements CharSequence { private final byte[] bytes; private final int offset; private final int length; private ByteArraySeq(byte[] bytes, int offset, int length) { this.bytes = bytes; this.offset = offset; this.length = length; } public int length() { return length; } public char charAt(int index) { return (char) bytes[offset + index]; } public CharSequence subSequence(int start, int end) { return null; //not needed for this implementation } } /** * Same as <code>fromString</code>, but characters are extracted from the <code>CharSequence</code> * @param seq <code>CharSequence</code> object, may be more convenient in some situations. * @return Money object */ public static Money fromCharSequence( final CharSequence seq ) { final Money fast = parseFast( seq ); if ( fast != null ) return fast; return fromString0( seq.toString() ); //slow path, convert to String } /** * Convert a String monetary value into a Money object. We support a first format - dot-separated decimal part without * the scientific notation support ( valid example - 355.56 ). * @param value Value to parse * @return Money * @throws java.lang.IllegalArgumentException In case of any conversion errors */ public static Money fromString( final String value ) { //fast first pass parser first final Money fast = parseFast( value ); if ( fast != null ) return fast; return fromString0(value); } private static Money fromString0( final String value ) { final int dotPos = value.indexOf('.'); final int precision = dotPos == -1 ? 0 : value.length() - dotPos - 1; if ( precision > MAX_ALLOWED_PRECISION ) //too high precision return new MoneyBigDecimal( value ); if ( dotPos != -1 && value.indexOf( '.', dotPos + 1 ) != -1 ) throw new IllegalArgumentException( "Unparseable String value has more than 1 decimal point: " + value ); try { final long units = Long.parseLong( value.replace(".", "") ); return new MoneyLong( units, precision ); //actual precision, not the maximal one } catch ( NumberFormatException ex ) { try { return new MoneyBigDecimal( value ); } catch ( NumberFormatException ex2 ) { throw new IllegalArgumentException( "Unparseable value provided: " + value, ex2 ); } } } /** * Fast first pass parser, should correctly process most of positive/negative values fitting into <code>long</code> * @param str Floating point number to parse * @return Money object or null (if can't parse) * @throws java.lang.IllegalArgumentException If a value has more than one decimal digit */ private static Money parseFast( final CharSequence str ) { if ( str.length() >= MAX_LONG_LENGTH ) return null; long res = 0; int start = 0; long sign = 1; int precision = 0; if ( str.charAt( 0 ) == '-' ) { sign = -1; start = 1; } else if ( str.charAt( 0 ) == '+' ) { sign = 1; start = 1; } for ( int i = start; i < str.length(); ++i ) { final char c = str.charAt( i ); if ( c == '.' ) { if ( precision > 0 ) throw new IllegalArgumentException( "Unparseable String value has more than 1 decimal point: " + str ); precision = str.length() - i - 1; } else if ( c >= '0' && c <= '9' ) res = res * 10 + ( c - '0' ); else //unsupported char, handle in the caller return null; } if ( precision >= 0 && precision <= MAX_ALLOWED_PRECISION ) return new MoneyLong( res * sign, precision ).normalize(); else return new MoneyBigDecimal( str.toString() ); } /** * <p> * Convert a double monetary value into a Money object. You will end up with the most efficient Money type * if you have no more than <code>MAX_ALLOWED_PRECISION</code> decimal digits in your value. * </p> * <p> * This method will attempt to look by ulp in both directions during conversions to cater for already slightly * incorrect values - results of <code>double</code> operations outside this library. * </p> * @param value Double monetary value * @return Money object */ public static Money fromDouble( final double value ) { return fromDouble( value, MAX_ALLOWED_PRECISION ); } /** * <p> * Convert a double monetary value into a Money object. You will end up with the most efficient Money type * if you have no more than <code>precision</code> decimal digits in your value. * </p> * <p> * This method will attempt to look by ulp in both directions during conversions to cater for already slightly * incorrect values - results of <code>double</code> operations outside this library. * </p> * <p> * Do not try to set too high precision for this method - it may prevent you from correcting a slightly * incorrect value (off by ulp) into a correct one. As a result, you will end up with BigDecimal-based * implementation, which requires more memory and which is much slower to calculate. * </p> * @param value Double monetary value * @param precision Number of digits after decimal point in your smallest possible currency unit. * Should be between 0 and <code>MAX_ALLOWED_PRECISION</code> (inclusive). * This parameter is a hint only for more efficient conversion. It does not truncate the result. * @return Money object with a value as close as possible to a provided value (first parameter) */ public static Money fromDouble( final double value, final int precision ) { checkPrecision( precision ); //attempt direct final Money direct = fromDoubleNoFallback( value, precision ); if ( direct != null ) return direct; return new MoneyBigDecimal( value ); } static MoneyLong fromDoubleNoFallback( final double value, final int precision ) { //attempt direct final MoneyLong direct = fromDouble0( value, precision ); if ( direct != null ) return direct; //ulp down final MoneyLong down = fromDouble0( Math.nextAfter( value, -Double.MAX_VALUE ), precision ); if ( down != null ) return down; //ulp up final MoneyLong up = fromDouble0( Math.nextAfter( value, Double.MAX_VALUE ), precision ); if ( up != null ) return up; return null; } private static MoneyLong fromDouble0( final double value, final int precision ) { final double multiplied = value * MULTIPLIERS[ precision ]; final long converted = (long) multiplied; if ( multiplied == converted ) //here is an implicit conversion from long to double return new MoneyLong( converted, precision ).normalize(); return null; } /** * <p> * Convert a given BigDecimal value into money. Conversion is similar to <code>toDouble</code>, * though this method does not attempt to make any corrections: it assumes that BigDecimal is a result * of exact calculations. * </p> * <p> * This method will try to use the most efficient representation if possible. * </p> * @param value BigDecimal value to convert * @return Money object */ public static Money fromBigDecimal( final BigDecimal value ) { final BigDecimal cleaned = value.stripTrailingZeros(); //try to convert to double using a fixed precision = 3, which will cover most of currencies //it is required to get rid of rounding issues final double dbl = value.doubleValue(); final Money res = fromDoubleNoFallback( dbl, 3 ); if ( res != null ) return res; final int scale = cleaned.scale(); if ( scale > MAX_ALLOWED_PRECISION || scale < -MAX_ALLOWED_PRECISION ) return new MoneyBigDecimal( cleaned ); //we may not fit into the Long, but we should try //this value may be truncated! final BigInteger unscaledBigInt = cleaned.unscaledValue(); final long unscaledUnits = unscaledBigInt.longValue(); //check that it was not if ( !BigInteger.valueOf(unscaledUnits).equals( unscaledBigInt ) ) return new MoneyBigDecimal( cleaned ); //scale could be negative here - we must multiply in that case if ( scale >= 0 ) return new MoneyLong( unscaledUnits, scale ); //multiply by 10 and each time check that sign did not change //scale is negative long units = unscaledUnits; for ( int i = 0; i < -scale; ++i ) { units *= 10; if ( units >= MAX_LONG_DIVIDED_BY_10 ) return new MoneyBigDecimal( value ); } return new MoneyLong( units, 0 ); } }