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