001/* 002 * Copyright 2009-2011 Stephen Colebourne 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.joda.money; 017 018import java.io.InvalidObjectException; 019import java.io.ObjectInputStream; 020import java.io.Serializable; 021import java.math.BigDecimal; 022import java.math.RoundingMode; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026/** 027 * Represents an exchange rate that allows {@link BigMoneyProvider} instances to be converted between amounts in 028 * different {@link CurrencyUnit}s. Exchange rate is an object that holds data about two {@link CurrencyUnit}s and a 029 * conversion rate, and basically mirrors the following equation: 030 * 031 * <p> 032 * 1 major unit of <strong><code>target currency</code></strong> = <strong><code>rate</code></strong> major units of 033 * <strong><code>source currency</code></strong>. 034 * </p> 035 * 036 * Exchange rates provide a few simple operations that can be performed with them through {@link ExchangeRateOperations} 037 * . A default implementation can be acquired using {@link #operations(int, RoundingMode)}. 038 * 039 * <p> 040 * Instances of this class are immutable. 041 * </p> 042 * 043 * <p> 044 * Two instances of this class are considered equal when both target and source currencies are the same and the rates 045 * compare equal according to {@link BigDecimal#compareTo(BigDecimal)}. 046 * </p> 047 * 048 * Please be aware that creating an exchange rate less or equal to 0 is considered an error. 049 * 050 * @author Tom Pasierb 051 */ 052public final class ExchangeRate implements Serializable { 053 054 private static final long serialVersionUID = 1L; 055 056 private static final Pattern PARSE_REGEX = Pattern.compile("^1\\s+([A-Z]{3})\\s+=\\s([-+]?\\d+(.\\d+)?)\\s+([A-Z]{3})$"); 057 058 /** 059 * Conversion rate between source and target currency. 060 */ 061 private final BigDecimal rate; 062 063 /** 064 * The source currency. 065 */ 066 private final CurrencyUnit source; 067 068 /** 069 * The target currency. 070 */ 071 private final CurrencyUnit target; 072 073 public static ExchangeRate parse(String exchangeRate) { 074 Utils.notNull(exchangeRate, "Exchange rate cannot be null"); 075 076 String exr = exchangeRate.trim(); 077 Matcher matcher = PARSE_REGEX.matcher(exr); 078 if (!matcher.matches()) { 079 throw new IllegalArgumentException(String.format("Exchange rate '%s' cannot be parsed", exchangeRate)); 080 } 081 082 String source, target, rate; 083 target = matcher.group(1); 084 rate = matcher.group(2); 085 source = matcher.groupCount() == 4 ? matcher.group(4) : matcher.group(3); 086 return ExchangeRate.of(new BigDecimal(rate), CurrencyUnit.getInstance(source), CurrencyUnit.getInstance(target)); 087 } 088 089 /** 090 * Creates an identity rate for the given {@link CurrencyUnit}. The rate will be equal to 1.00 and both source and 091 * target currency units will be the same as the given one. 092 * 093 * @param currency to be set for source and target currency units. 094 * @throws NullPointerException if the given parameter is <code>null</code> 095 * @return identity exchange rate having rate 1 and the same source and target currencies 096 */ 097 public static ExchangeRate identity(CurrencyUnit currency) { 098 return new ExchangeRate(BigDecimal.ONE, currency, currency); 099 } 100 101 /** 102 * Creates an Exchange rate with the given parameters. 103 * 104 * @param rate the conversion rate 105 * @param source source {@link CurrencyUnit} 106 * @param target target {@link CurrencyUnit} 107 * @throws NullPointerException if any of the given parameters is <code>null</code> 108 * @throws IllegalArgumentException if the given rate is less or equal to 0 or when source and currency units are 109 * equal and rate is not equal to 1 110 * @return an instance of exchange rate parameterized as instructed 111 */ 112 public static ExchangeRate of(BigDecimal rate, CurrencyUnit source, CurrencyUnit target) { 113 return new ExchangeRate(rate, source, target); 114 } 115 116 /** 117 * Constructs an instance of ExchangeRate with the given parameters. 118 * 119 * @param rate the conversion rate 120 * @param source source {@link CurrencyUnit} 121 * @param target target {@link CurrencyUnit} 122 * @throws NullPointerException if any of the given parameters is <code>null</code> 123 * @throws IllegalArgumentException if the given rate is less or equal to 0 or when source and currency units are 124 * equal and rate is not equal to 1 125 * @return an instance of exchange rate parameterized as instructed 126 */ 127 ExchangeRate(BigDecimal rate, CurrencyUnit source, CurrencyUnit target) { 128 Utils.notNull(rate, "Rate must not be null"); 129 Utils.notNull(source, "Source currency must not be null"); 130 Utils.notNull(target, "Target currency must not be null"); 131 Utils.isTrue(rate.compareTo(BigDecimal.ZERO) > 0, "Rate must be greater than 0"); 132 if (source == target) { 133 Utils.isTrue(rate.compareTo(BigDecimal.ONE) == 0, 134 "Rate must be 1 if source and target currencies are the same, got rate=%f, source=%s, target=%s", rate, source, target); 135 } 136 this.rate = rate; 137 this.source = source; 138 this.target = target; 139 } 140 141 /** 142 * Creates an ExchangeRate like this one with the given rate. 143 * 144 * @param rate the rate that the new ExchangeRate should have 145 * @return a copy of this with the given rate and all other settings as this one 146 * @see #of(BigDecimal, CurrencyUnit, CurrencyUnit) 147 */ 148 public ExchangeRate withRate(BigDecimal rate) { 149 return new ExchangeRate(rate, source, target); 150 } 151 152 /** 153 * Returns the conversion rate. 154 * 155 * @return the conversion rate between this exchange rate's source and target currencies 156 */ 157 public BigDecimal getRate() { 158 return rate; 159 } 160 161 /** 162 * Returns the source currency. 163 * 164 * @return the source currency 165 */ 166 public CurrencyUnit getSource() { 167 return source; 168 } 169 170 /** 171 * Returns the target currency. 172 * 173 * @return the target currency 174 */ 175 public CurrencyUnit getTarget() { 176 return target; 177 } 178 179 /** 180 * Creates an instance of an object that knows how to perform operations on {@link ExchangeRate}s. 181 * 182 * @param scale scale that will be used for calculations 183 * @param roundingMode rounding mode to use if rounding is necessary 184 * @return an instance of {@link ExchangeRateOperations} 185 */ 186 public ExchangeRateOperations operations(int scale, RoundingMode roundingMode) { 187 return new DefaultExchangeRateOperations(this, scale, roundingMode); 188 } 189 190 /** 191 * Creates an instance of an object that knows how to perform operations on {@link ExchangeRate}s with default 192 * values for <code>scale</code> and <code>roundingMode</code> that will be used for calculations. The values used 193 * as defaults are: 194 * <ul> 195 * <li>scale = 16</li> 196 * <li>roundingMode = {@link RoundingMode#HALF_EVEN}</li> 197 * </ul> 198 * 199 * @return an instance of {@link ExchangeRateOperations} 200 */ 201 public ExchangeRateOperations operations() { 202 return new DefaultExchangeRateOperations(this, 16, RoundingMode.HALF_EVEN); 203 } 204 205 @Override 206 public int hashCode() { 207 final int prime = 31; 208 int result = 1; 209 result = prime * result + ((rate == null) ? 0 : rate.hashCode()); 210 result = prime * result + ((source == null) ? 0 : source.hashCode()); 211 result = prime * result + ((target == null) ? 0 : target.hashCode()); 212 return result; 213 } 214 215 @Override 216 public boolean equals(Object obj) { 217 if (this == obj) { 218 return true; 219 } 220 if (!(obj instanceof ExchangeRate)) { 221 return false; 222 } 223 ExchangeRate other = (ExchangeRate) obj; 224 boolean equal = true; 225 equal &= rate == null ? other.rate == null : rate.compareTo(other.rate) == 0; 226 if (!equal) { 227 return equal; 228 } 229 equal &= source == null ? other.source == null : source.equals(other.source); 230 if (!equal) { 231 return equal; 232 } 233 equal &= target == null ? other.target == null : target.equals(other.target); 234 return equal; 235 } 236 237 @Override 238 public String toString() { 239 return "ExchangeRate[1 " + target + " = " + rate + " " + source + "]"; 240 } 241 242 /** 243 * Block malicious data streams. 244 * 245 * @param ois the input stream, not null 246 * @throws InvalidObjectException 247 */ 248 private void readObject(ObjectInputStream ois) throws InvalidObjectException { 249 throw new InvalidObjectException("Serialization delegate required"); 250 } 251 252 /** 253 * Uses a serialization delegate. 254 * 255 * @return the replacing object, never null 256 */ 257 private Object writeReplace() { 258 return new Ser(Ser.EXCHANGE_RATE, this); 259 } 260}