task4/solution13/src/org/apidesign/apifest08/currency/Convertor.java
author Jaroslav Tulach <jtulach@netbeans.org>
Sat, 25 Oct 2008 20:53:00 +0200
changeset 84 2ae6e4aa7aef
parent 61 58ec6da75f6f
permissions -rw-r--r--
Solutions by Petr Smid
     1 package org.apidesign.apifest08.currency;
     2 
     3 import java.math.BigDecimal;
     4 import java.math.MathContext;
     5 import java.math.RoundingMode;
     6 import java.util.Date;
     7 
     8 /** Convertor able to convert amount from one currency to other currency.
     9  * <p>
    10  * Conversion method are:
    11  * <ul>
    12  * <li>{@link #convert(ConvertorCurrency, ConvertorCurrency, BigDecimal)} - convert
    13  * using exchange rate specified in exchange rate provide. This method is not try 
    14  * use reverted excahnage rate. 
    15  * <li>{@link #convertWithReversibleRates(ConvertorCurrency, ConvertorCurrency, BigDecimal)} - 
    16  * convert using exchange rate specified in exchange rate provide. This method
    17  * is not trying to use reverted exchange rate.
    18  * </ul>
    19  * 
    20  * Exchange rate is provided by {@link ExchangeRateProvider}.
    21  */
    22 public class Convertor {
    23     private Convertor[] convertors;
    24     private IDateProviderEngine dateProvider;
    25     boolean remainderAllowed = true; //if false, remained is not allowed (should be true ideally, but can't handle it now)
    26     private ExchangeRateProvider exchangeRateProvider;
    27     private Date fromDate;
    28     private Date toDate;
    29 
    30     /** Create new <code>Convertor</code> as merge of provided convertors. Merged convertor will use
    31      * provided convertors to convert between currencies.
    32      * <p>
    33      * Only one should be able to provide conversion between currencies. If more than one convertos
    34      * are able to convert currency, one of conversions will be used (it is not defined which).
    35      * 
    36      * @since version 2
    37      * @param convertors Convertor used to create merge-convertor.
    38      * @return Returns new convertor instance.
    39      */
    40     public static Convertor createConvertorAsMerge(Convertor[] convertors) {
    41         for (int i=0;i<convertors.length;i++) {
    42             if (convertors[i]==null) {
    43                 throw new NullPointerException("Convertor at index "+i+" can't be null");
    44             }
    45         }
    46         return new Convertor(convertors);
    47     }
    48     
    49     
    50     /** Create simle convertor.
    51      */
    52     private Convertor() {
    53         setDateProvider(DateProvider.createCurrentDateProvider());
    54         this.convertors=new Convertor[0];
    55     }
    56     
    57     /** Create merge convertor.
    58      */            
    59     private Convertor(Convertor[] convertors) {
    60         this();
    61         this.convertors = convertors;       
    62     }
    63     
    64     /**
    65      * Create new <code>Convertor</code> using <code>ExchangeRateProvider</code>.
    66      * 
    67      * @param exchangeRateProvider {@link ExchangeRateProvider} used to get exchange rate.
    68      * 
    69      * @return Returns <code>Convertor</code> which can be used to convert money.
    70      * @since version1
    71      */
    72     public static Convertor createConvertor(ExchangeRateProvider exchangeRateProvider) {
    73         Convertor c = new Convertor();
    74 
    75         c.exchangeRateProvider = exchangeRateProvider;
    76         return c;
    77     }
    78     
    79     /**
    80      * Convert <code>amount</code> from <code>fromCurrency</code> to <code>toCurrency</code> as specified
    81      * in <code>ExchangeRateProvider</code>.
    82      * 
    83      * @param amount Amount which should be converted. Can't be negative value (can be zero or positive).
    84      * @return Return <code>ConversionResult</code> which holds conversion result.
    85      * @since version1
    86      * @deprecated since version2. Use {@link #convert(ConvertorCurrency, ConvertorCurrency, BigDecimal) } - explicitly specify conversion currencies.
    87      */
    88     public ConversionResult convert(BigDecimal amount) {
    89         return convertValue(exchangeRateProvider.getFromCurrency(), exchangeRateProvider.getToCurrency(),amount, false,false,null);
    90     }
    91     
    92     /**
    93      * Convert <code>amount</code> from <code>toCurrency</code> to <code>fromCurrency</code> as specified
    94      * in <code>ExchangeRateProvider</code>. This is <em>reverted</em> order than suggested by  names of currency fields in <code>ExchangeRate</code>.
    95      * 
    96      * @param amount Amount which should be converted. Can't be negative value (can be zero or positive).
    97      * @return Return <code>ConversionResult</code> which holds conversion result.
    98      * @since version1
    99      * @deprecated since version2. Use {@link #convert(ConvertorCurrency, ConvertorCurrency, BigDecimal) } - explicitly specify conversion currencies.
   100      */
   101     public ConversionResult convertBack(BigDecimal amount) {
   102         return convertValue(exchangeRateProvider.getFromCurrency(), exchangeRateProvider.getToCurrency(),amount, true,false,null);
   103     }
   104 
   105     private ConversionResult convertUsingSimpleConvertor(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency, boolean reversibleExRate, BigDecimal amount, boolean convertBack,Date date) throws ConversionNotSupportedException, RuntimeException {
   106         if (date == null) {
   107             date = dateProvider.getCurrentDate();
   108         }
   109 
   110         ExchangeRate rate;
   111         if (reversibleExRate) {
   112             rate = exchangeRateProvider.getReversibleExchangeRate(fromCurrency, toCurrency, date);
   113         } else {
   114             rate = exchangeRateProvider.getExchangeRate(fromCurrency, toCurrency, date);
   115         }
   116         if (rate == null) {
   117             return null;
   118         }
   119         ConversionResult result = new ConversionResult();
   120         
   121         int fromFranctionDigits = fromCurrency.getDefaultFractionDigits();
   122         int toFractionDigits = toCurrency.getDefaultFractionDigits();
   123         
   124         int usedFractionDigits = Math.max(fromFranctionDigits, toFractionDigits);
   125 
   126         //if (toFractionDigits != 2) {
   127         //    throw new ConvertorException("Can't process currency with defaultFractionDigits!=2, " + toCurrency + " has " + toFractionDigits + " defaultFractionDigits");
   128         //}
   129         //if (fromFranctionDigits != 2) {
   130         //    throw new ConvertorException("Can't process currency with defaultFractionDigits!=2, " + fromCurrency + " has " + fromFranctionDigits + " defaultFractionDigits");
   131         //}
   132 
   133         if (amount.signum() == -1) {
   134             throw new RuntimeException("Can convert only non-negative value, current value is " + amount);
   135         }
   136 
   137 
   138         MathContext context = new MathContext(0, RoundingMode.DOWN);
   139 
   140         BigDecimal from;
   141         BigDecimal to;
   142         if (convertBack) {
   143             //converting in reverted way
   144             to = rate.getFromValue();
   145             from = rate.getToValue();
   146         } else {
   147             //converting in normal way
   148             from = rate.getFromValue();
   149             to = rate.getToValue();
   150         }
   151 
   152         BigDecimal amountCent = amount.movePointRight(usedFractionDigits);
   153 
   154         final BigDecimal multiplied = amountCent.multiply(to, context);
   155         BigDecimal[] division = multiplied.divideAndRemainder(from, context);
   156 
   157         if (!remainderAllowed && !(BigDecimal.ZERO.equals(division[1]))) {
   158             throw new RuntimeException("Remained is not allowed - remaining amount is " + division[1] + " cents");
   159         } else {
   160             result.setRemainder(BigDecimal.ZERO.setScale(fromFranctionDigits));
   161         }
   162 
   163         BigDecimal converted = division[0].movePointLeft(usedFractionDigits);
   164         converted = converted.setScale(toFractionDigits,RoundingMode.DOWN); //XXX ugly
   165         result.setConverted(converted);
   166         BigDecimal[] convertedInBase = converted.multiply(from).divideAndRemainder(to);
   167         BigDecimal x2 = convertedInBase[0].add(convertedInBase[1]);
   168         BigDecimal remainder = amount.subtract(x2);
   169         result.setRemainder(remainder);
   170         //System.out.println("ConvertedInBase="+Arrays.asList(convertedInBase)+":"+x2 +" ("+amount+") ["+converted+"] <"+from+","+to+"> ->"+remainder);
   171         return result;
   172     }
   173     
   174 
   175     private ConversionResult convertValue(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency,BigDecimal amount, boolean convertBack,boolean reversibleExRate,Date date) throws RuntimeException {
   176         //result.setRemainder(...);
   177         if (!dateIsInLimit(date)) {
   178             return null;
   179         }        
   180         if (convertors.length==0) {
   181             return convertUsingSimpleConvertor(fromCurrency, toCurrency, reversibleExRate, amount, convertBack,date);
   182         } else {
   183             ConversionResult result = null;
   184             for (int i = 0;i<convertors.length;i++) {
   185                 Convertor subConvertor = convertors[i];
   186                 result = subConvertor.convertValue(fromCurrency, toCurrency, amount, convertBack, reversibleExRate,date);
   187                 if (result!=null) {
   188                     break;
   189                 }
   190             }
   191             return result;
   192         }
   193     }
   194     
   195     /**
   196      * Convert <code>value</code> from <code>fromCurrency</code> to <code>toCurrency</code>.
   197      * <p>
   198      * Exchange rate is provided by exchange rate provider which was specified when Convertor was created.
   199      * This method is using only exchange rate from->to and not trying to use reverted excange rate to->from.
   200      * <p>
   201      * This method is using date from <code>IDateProviderEngine</code> to get exchange rate (see {@link #getDateProvider() }.
   202      * 
   203      * @param fromCurrency Source currency to convert from.
   204      * @param toCurrency Target currency to convert to.
   205      * @param value Value in source currency which should be converted.
   206      * @return Return conversion result.
   207      * @since version2
   208      * @throws ConversionNotSupportedException If conversion from <code>fromCurrency</code> to <code>toCurrency</code> is not supported.
   209      */
   210     public ConversionResult convert(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency, BigDecimal value) {
   211         Date defaultDate = dateProvider.getCurrentDate();
   212         ConversionResult result = convertValue(fromCurrency, toCurrency, value, false,false,defaultDate);
   213         if (result==null) {
   214             //throw new ConversionNotSupportedException("Conversion from " + fromCurrency + " to " + toCurrency + " is not supported");
   215             throw new ConversionNotSupportedException(fromCurrency.getCurrencyCode(),toCurrency.getCurrencyCode(),false);
   216         }
   217         return result;
   218     }
   219     /**
   220      * Same as {@link #convert(ConvertorCurrency, ConvertorCurrency, java.math.BigDecimal)} but using provided rate instead of
   221      * <code>IDateProviderEngine</code>.
   222      * 
   223      * @param fromCurrency Source currency to convert from.
   224      * @param toCurrency Target currency to convert to.
   225      * @param value Value in source currency which should be converted.
   226      * @return Return conversion result.
   227      * @param date Conversion date
   228      * @return Return conversion result.
   229      */
   230     public ConversionResult convert(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency, BigDecimal value, Date date) {
   231         ConversionResult result = convertValue(fromCurrency, toCurrency, value, false,false,date);
   232         if (result==null) {
   233             throw new ConversionNotSupportedException(fromCurrency.getCurrencyCode(),toCurrency.getCurrencyCode(),false);
   234         }
   235         return result;
   236     }    
   237     
   238     /**
   239      * Convert <code>value</code> from <code>fromCurrency</code> to <code>toCurrency</code>.
   240      * Exchange rate is provided by exchange rate provider which was specified when Convertor was created.
   241      * <p>
   242      *  This method is using only exchange rate from->to and if not found, it is trying to use reverted excange rate to->from.
   243      * <p>
   244      * This method is using date from <code>IDateProviderEngine</code> to get exchange rate (see {@link #getDateProvider() }.
   245      * 
   246      * @param fromCurrency Source currency to convert from.
   247      * @param toCurrency Target currency to convert to.
   248      * @param value Value in source currency which should be converted.
   249      * @return Return conversion result.
   250      * @since version2
   251      * @throws ConversionNotSupportedException If conversion from <code>fromCurrency</code> to <code>toCurrency</code> 
   252      *         is not supported and neither conversion from <code>toCurrency</code> to <code>fromCurrency</code> is not supported.
   253      */
   254     public ConversionResult convertWithReversibleRates(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency, BigDecimal value) {
   255         Date defaultDate = dateProvider.getCurrentDate();
   256         ConversionResult result = convertValue(fromCurrency, toCurrency, value, false,true,defaultDate);
   257         if (result==null) {
   258             //throw new ConversionNotSupportedException("Neither onversion nor reverted conversion from " + fromCurrency + " to " + toCurrency + "  is not supported,");
   259             throw new ConversionNotSupportedException(fromCurrency.getCurrencyCode(),toCurrency.getCurrencyCode(),true);
   260         }
   261         return result;
   262     }
   263     
   264     /**
   265      * Same as {@link #convertWithReversibleRates(ConvertorCurrency, ConvertorCurrency, java.math.BigDecimal)} but using provided rate instead of
   266      * <code>IDateProviderEngine</code>.
   267      * 
   268      * @param fromCurrency Source currency to convert from.
   269      * @param toCurrency Target currency to convert to.
   270      * @param value Value in source currency which should be converted.
   271      * @return Return conversion result.
   272      * @param date Conversion date
   273      * @return Return conversion result.
   274      */
   275     public ConversionResult convertWithReversibleRates(ConvertorCurrency fromCurrency, ConvertorCurrency toCurrency, BigDecimal value, Date date) {
   276         ConversionResult result = convertValue(fromCurrency, toCurrency, value, false,true,date);
   277         if (result==null) {
   278             throw new ConversionNotSupportedException(fromCurrency.getCurrencyCode(),toCurrency.getCurrencyCode(),true);
   279         }
   280         return result;
   281     }
   282     
   283     private boolean dateIsInLimit(Date date) {
   284         boolean result;
   285 //        if (date==null) {
   286 //          result = true;  
   287 //        } else 
   288         if (fromDate == null && toDate == null) {
   289             result = true;
   290         } else if (fromDate.getTime()<=date.getTime() && date.getTime()<=toDate.getTime()) {
   291             result = true;
   292         } else {
   293             result = false;
   294         }
   295         
   296         return result;
   297     }
   298     
   299     
   300     public void limitAllowedDates(Date from, Date till) {
   301         if (from==null) {
   302             throw new NullPointerException("from Date can't be null");
   303         }
   304         if (till==null) {
   305             throw new NullPointerException("till Date can't be null");
   306         }
   307         if (from.getTime()>till.getTime()) {
   308             throw new IllegalArgumentException("From date "+from+" must be before tii date "+till);
   309         }
   310         this.fromDate = from;
   311         this.toDate = till;
   312     }
   313     
   314 
   315     /** Return current date provider.
   316      * @return Returns current date provider.
   317      */
   318     public IDateProviderEngine getDateProvider() {
   319         return dateProvider;
   320     }
   321 
   322     
   323     /**
   324      * Set date provider. Date provider is used to get "current date". Current date 
   325      * is used by methods {@link #convert(ConvertorCurrency, ConvertorCurrency, java.math.BigDecimal)} and  
   326      * {@link #convertWithReversibleRates(ConvertorCurrency, ConvertorCurrency, java.math.BigDecimal)}.
   327      * 
   328      * @param dateProvider Date provider which should be used by Convertor.
   329      */
   330     public void setDateProvider(IDateProviderEngine dateProvider) {
   331         this.dateProvider = dateProvider;
   332     }
   333     
   334     
   335 }