# HG changeset patch # User japod@localhost # Date 1223331577 -7200 # Node ID 3a18aae85c9e96707f75d6b2de9446079c1bd712 # Parent 80f2d8751f356e0d3a4057a01a46f8f8e8d0889b adding solution02 for task2 diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/src/org/apidesign/apifest08/currency/CompositeConvertor.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/task2/solution02/src/org/apidesign/apifest08/currency/CompositeConvertor.java Tue Oct 07 00:19:37 2008 +0200 @@ -0,0 +1,75 @@ +package org.apidesign.apifest08.currency; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Currency; + +/** + * Convertor that is composed from other convertors. + * @author lukas + * + */ +class CompositeConvertor implements ExtendedConvertor, Serializable { + + private static final long serialVersionUID = -2702026568249130055L; + + private final ExtendedConvertor[] convertors; + + + public CompositeConvertor(ExtendedConvertor... convertors) { + this.convertors = convertors.clone(); + for (ExtendedConvertor convertor: this.convertors) + { + if (convertor==null) + { + throw new NullPointerException("One of the convertors to be merged is null"); + } + } + } + + /** + * Returns true if the composite contains convertor that supports given conversion. + */ + public boolean isConversionSupported(Currency from, Currency to) { + return findConvertorFor(from, to)!=null; + } + + + /** + * If the composite contains convertor that supports given conversion, delegates to its convert method. + * Throws {@link IllegalArgumentException} convertor supporting given conversion is not found. + */ + public Money convert(Money amount, Currency destinationCurrency) throws IllegalArgumentException { + ExtendedConvertor convertor = findConvertorFor(amount.getCurrency(), destinationCurrency); + if (convertor!=null) + { + return convertor.convert(amount, destinationCurrency); + } + throw new IllegalArgumentException("Conversion not supported. Can not convert to "+destinationCurrency+"."); + } + + /** + * Finds convertor for given currencies. If not found, returns null. + * @param from + * @param to + * @return + */ + ExtendedConvertor findConvertorFor(Currency from, Currency to) { + //does linear search, in the future can cache the results. + for (ExtendedConvertor convertor:convertors) + { + if (convertor.isConversionSupported(from, to)) + { + return convertor; + } + } + return null; + } + + + @Override + public String toString() { + return getClass().getName()+" converts "+Arrays.toString(convertors); + } + +} diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/src/org/apidesign/apifest08/currency/ConvertorFactory.java --- a/task2/solution02/src/org/apidesign/apifest08/currency/ConvertorFactory.java Tue Oct 07 00:17:07 2008 +0200 +++ b/task2/solution02/src/org/apidesign/apifest08/currency/ConvertorFactory.java Tue Oct 07 00:19:37 2008 +0200 @@ -22,9 +22,24 @@ * @param destinationEquivalent * @return */ - public static final Convertor createConvertor(Money sourceEquivalent, Money destinationEquivalent) + public static final ExtendedConvertor createConvertor(Money sourceEquivalent, Money destinationEquivalent) { - return new DefaultConvertor(sourceEquivalent.getAmount(), destinationEquivalent.getAmount(), sourceEquivalent.getCurrency(), destinationEquivalent.getCurrency()); + return mergeConvertors( + new DefaultConvertor(sourceEquivalent, destinationEquivalent), + new DefaultConvertor(destinationEquivalent ,sourceEquivalent) + ); + } + + /** + * Merges convertors. The resulting convertor has ability to do all conversions that its underlying + * convertors could do. No consistency validation is done, inconsistent input will result in a convertor with + * inconsistent behavior. + * @param convertors + * @return + */ + public static final ExtendedConvertor mergeConvertors(ExtendedConvertor... convertors) + { + return new CompositeConvertor(convertors); } diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/src/org/apidesign/apifest08/currency/DefaultConvertor.java --- a/task2/solution02/src/org/apidesign/apifest08/currency/DefaultConvertor.java Tue Oct 07 00:17:07 2008 +0200 +++ b/task2/solution02/src/org/apidesign/apifest08/currency/DefaultConvertor.java Tue Oct 07 00:19:37 2008 +0200 @@ -11,9 +11,9 @@ * @author lukas * */ -class DefaultConvertor implements Convertor, Serializable { +class DefaultConvertor implements ExtendedConvertor, Serializable { - private static final long serialVersionUID = -1754789142402148099L; + private static final long serialVersionUID = 6660014633685135034L; /** * Equivalent in source currency. @@ -29,20 +29,20 @@ private final Currency destinationCurrency; - public DefaultConvertor(BigDecimal sourceEquivalent, BigDecimal destinationEquivalent, Currency sourceCurrency, Currency destinationCurrency) { + public DefaultConvertor(Money sourceEquivalent, Money destinationEquivalent) { super(); - if (BigDecimal.ZERO.compareTo(sourceEquivalent)==0) + if (BigDecimal.ZERO.compareTo(sourceEquivalent.getAmount())==0) { throw new IllegalArgumentException("Source equivalent amount can not be 0."); } - if (BigDecimal.ZERO.compareTo(destinationEquivalent)==0) + if (BigDecimal.ZERO.compareTo(destinationEquivalent.getAmount())==0) { throw new IllegalArgumentException("Destination equivalent amount can not be 0."); } - this.sourceEquivalent = sourceEquivalent; - this.destinationEquivalent = destinationEquivalent; - this.sourceCurrency = sourceCurrency; - this.destinationCurrency = destinationCurrency; + this.sourceEquivalent = sourceEquivalent.getAmount(); + this.destinationEquivalent = destinationEquivalent.getAmount(); + this.sourceCurrency = sourceEquivalent.getCurrency(); + this.destinationCurrency = destinationEquivalent.getCurrency(); } public Money convert(Money amount, Currency destinationCurrency) { @@ -54,10 +54,6 @@ { throw new NullPointerException("destionationCurrency is null"); } - if (isConversionInOpositeDirection(amount, destinationCurrency)) - { - return revert().convert(amount, destinationCurrency); - } if (!amount.getCurrency().equals(getSourceCurrency())) { throw new IllegalArgumentException("Can not convert from "+amount.getCurrency()+". Converts between "+getSourceCurrency()+" and "+getDestinationCurrency()); @@ -70,20 +66,9 @@ BigDecimal destinationAmount = sourceAmount.multiply(destinationEquivalent).divide(sourceEquivalent, 2, RoundingMode.HALF_UP); return new MoneyImpl(destinationAmount, getDestinationCurrency()); } - - /** - * Returns true, if the conversion is in oposit direction. - * @param amount - * @param destinationCurrency - * @return - */ - private boolean isConversionInOpositeDirection(Money amount, Currency destinationCurrency) { - return amount.getCurrency().equals(getDestinationCurrency()) && destinationCurrency.equals(getSourceCurrency()); - } - - - public Convertor revert() { - return new DefaultConvertor(destinationEquivalent, sourceEquivalent, destinationCurrency, sourceCurrency); + + public boolean isConversionSupported(Currency from, Currency to) { + return sourceCurrency.equals(from) && destinationCurrency.equals(to); } public BigDecimal getSourceEquivalent() { @@ -107,58 +92,4 @@ return getClass().getName()+" converts "+getSourceCurrency()+" to "+getDestinationCurrency()+" " +getSourceCurrency()+getSourceEquivalent()+"="+getDestinationCurrency()+getDestinationEquivalent(); } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime - * result - + ((destinationCurrency == null) ? 0 : destinationCurrency - .hashCode()); - result = prime - * result - + ((destinationEquivalent == null) ? 0 : destinationEquivalent - .hashCode()); - result = prime * result - + ((sourceCurrency == null) ? 0 : sourceCurrency.hashCode()); - result = prime - * result - + ((sourceEquivalent == null) ? 0 : sourceEquivalent.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!(obj instanceof DefaultConvertor)) - return false; - DefaultConvertor other = (DefaultConvertor) obj; - if (destinationCurrency == null) { - if (other.destinationCurrency != null) - return false; - } else if (!destinationCurrency.equals(other.destinationCurrency)) - return false; - if (destinationEquivalent == null) { - if (other.destinationEquivalent != null) - return false; - } else if (!destinationEquivalent.equals(other.destinationEquivalent)) - return false; - if (sourceCurrency == null) { - if (other.sourceCurrency != null) - return false; - } else if (!sourceCurrency.equals(other.sourceCurrency)) - return false; - if (sourceEquivalent == null) { - if (other.sourceEquivalent != null) - return false; - } else if (!sourceEquivalent.equals(other.sourceEquivalent)) - return false; - return true; - } - - } diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/src/org/apidesign/apifest08/currency/ExtendedConvertor.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/task2/solution02/src/org/apidesign/apifest08/currency/ExtendedConvertor.java Tue Oct 07 00:19:37 2008 +0200 @@ -0,0 +1,18 @@ +package org.apidesign.apifest08.currency; + +import java.util.Currency; + +/** + * Extended convertor interface. + * @author lukas + * + */ +public interface ExtendedConvertor extends Convertor { + /** + * Returns true if given conversion is supported. + * @param from + * @param to + * @return + */ + public boolean isConversionSupported(Currency from, Currency to); +} diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/src/org/apidesign/apifest08/currency/MoneyImpl.java --- a/task2/solution02/src/org/apidesign/apifest08/currency/MoneyImpl.java Tue Oct 07 00:17:07 2008 +0200 +++ b/task2/solution02/src/org/apidesign/apifest08/currency/MoneyImpl.java Tue Oct 07 00:19:37 2008 +0200 @@ -15,7 +15,7 @@ private final BigDecimal amount; private final Currency currency; - + public MoneyImpl(BigDecimal amount, Currency currency) { if (amount==null) throw new NullPointerException("Amount is null"); if (currency==null) throw new NullPointerException("Currency is null"+currency); @@ -23,6 +23,8 @@ this.currency = currency; } + + public MoneyImpl(long amount, Currency currency) { this(BigDecimal.valueOf(amount), currency); } @@ -30,6 +32,37 @@ public MoneyImpl(double amount, Currency currency) { this(BigDecimal.valueOf(amount), currency); } + + /** + * Factory method. + * @param amount + * @param currency + * @return + */ + public static final Money money(BigDecimal amount, Currency currency) + { + return new MoneyImpl(amount, currency); + } + /** + * Factory method. + * @param amount + * @param currency + * @return + */ + public static final Money money(long amount, Currency currency) + { + return new MoneyImpl(amount, currency); + } + /** + * Factory method. + * @param amount + * @param currency + * @return + */ + public static final Money money(double amount, Currency currency) + { + return new MoneyImpl(amount, currency); + } /** * Returns amount. diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/test/org/apidesign/apifest08/currency/CompositeConvertorTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/task2/solution02/test/org/apidesign/apifest08/currency/CompositeConvertorTest.java Tue Oct 07 00:19:37 2008 +0200 @@ -0,0 +1,56 @@ +package org.apidesign.apifest08.currency; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; +import static org.apidesign.apifest08.test.Task1Test.CZK; +import static org.apidesign.apifest08.test.Task1Test.SKK; +import static org.apidesign.apifest08.test.Task1Test.USD; + +import org.junit.Test; + +public class CompositeConvertorTest { + private static final ExtendedConvertor USD_CZK_CONVERTOR = ConvertorFactory.createConvertor(new MoneyImpl(1,USD), new MoneyImpl(17,CZK)); + private static final ExtendedConvertor SKK_CZK_CONVERTOR = ConvertorFactory.createConvertor(new MoneyImpl(100,SKK), new MoneyImpl(80,CZK)); + + @Test + public void testCompose() + { + ExtendedConvertor convertor = new CompositeConvertor(USD_CZK_CONVERTOR, SKK_CZK_CONVERTOR); + assertTrue(convertor.isConversionSupported(CZK, SKK)); + assertTrue(convertor.isConversionSupported(CZK, USD)); + assertFalse(convertor.isConversionSupported(SKK, USD)); + assertEquals(new MoneyImpl(10,SKK), convertor.convert(new MoneyImpl(8,CZK), SKK)); + assertEquals(new MoneyImpl(2,USD), convertor.convert(new MoneyImpl(34,CZK), USD)); + try + { + convertor.convert(new MoneyImpl(34,SKK), USD); + fail("Exception expected"); + } + catch(IllegalArgumentException e) + { + assertTrue("Ok", true); + } + } + @Test + public void testEmpty() + { + ExtendedConvertor convertor = new CompositeConvertor(); + assertFalse(convertor.isConversionSupported(SKK, USD)); + try + { + convertor.convert(new MoneyImpl(34,SKK), USD); + fail("Exception expected"); + } + catch(IllegalArgumentException e) + { + assertTrue("Ok", true); + } + } + @Test(expected=NullPointerException.class) + public void testCreateNull() + { + new CompositeConvertor(USD_CZK_CONVERTOR, null); + } +} diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/test/org/apidesign/apifest08/test/ConvertorTest.java --- a/task2/solution02/test/org/apidesign/apifest08/test/ConvertorTest.java Tue Oct 07 00:17:07 2008 +0200 +++ b/task2/solution02/test/org/apidesign/apifest08/test/ConvertorTest.java Tue Oct 07 00:19:37 2008 +0200 @@ -1,13 +1,16 @@ package org.apidesign.apifest08.test; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; import static org.apidesign.apifest08.test.Task1Test.CZK; +import static org.apidesign.apifest08.test.Task1Test.SKK; import static org.apidesign.apifest08.test.Task1Test.USD; -import static org.junit.Assert.assertEquals; import java.math.BigDecimal; -import org.apidesign.apifest08.currency.Convertor; import org.apidesign.apifest08.currency.ConvertorFactory; +import org.apidesign.apifest08.currency.ExtendedConvertor; import org.apidesign.apifest08.currency.Money; import org.apidesign.apifest08.currency.MoneyImpl; import org.junit.Test; @@ -15,7 +18,7 @@ public class ConvertorTest { - private static final Convertor CZK_TO_USD_CONVERTOR = ConvertorFactory.createConvertor(new MoneyImpl(17,CZK), new MoneyImpl(1,USD)); + private static final ExtendedConvertor CZK_TO_USD_CONVERTOR = ConvertorFactory.createConvertor(new MoneyImpl(17,CZK), new MoneyImpl(1,USD)); @Test public void testConvertSmall() { @@ -29,4 +32,13 @@ Money converted = CZK_TO_USD_CONVERTOR.convert(new MoneyImpl(0.01,USD),CZK); assertEquals(new MoneyImpl(new BigDecimal("0.17"),CZK),converted); } + @Test + public void testSupports() + { + assertTrue(CZK_TO_USD_CONVERTOR.isConversionSupported(USD,CZK)); + assertTrue(CZK_TO_USD_CONVERTOR.isConversionSupported(CZK,USD)); + assertFalse(CZK_TO_USD_CONVERTOR.isConversionSupported(CZK,CZK)); + assertFalse(CZK_TO_USD_CONVERTOR.isConversionSupported(CZK,SKK)); + + } } diff -r 80f2d8751f35 -r 3a18aae85c9e task2/solution02/test/org/apidesign/apifest08/test/Task2Test.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/task2/solution02/test/org/apidesign/apifest08/test/Task2Test.java Tue Oct 07 00:19:37 2008 +0200 @@ -0,0 +1,119 @@ +package org.apidesign.apifest08.test; + +import static org.apidesign.apifest08.currency.ConvertorFactory.createConvertor; +import static org.apidesign.apifest08.currency.ConvertorFactory.mergeConvertors; +import static org.apidesign.apifest08.currency.MoneyImpl.money; +import static org.apidesign.apifest08.test.Task1Test.CZK; +import static org.apidesign.apifest08.test.Task1Test.SKK; +import static org.apidesign.apifest08.test.Task1Test.USD; +import junit.framework.TestCase; + +import org.apidesign.apifest08.currency.Convertor; +import org.apidesign.apifest08.currency.ExtendedConvertor; + +/** There are many currencies around the world and many banks manipulate + * with more than one or two at the same time. As banks are usually the + * best paying clients, which is true even in case of your Convertor API, + * it is reasonable to listen to their requests. + *

+ * The quest for today is to enhance your existing convertor API to hold + * information about many currencies and allow conversions between any of them. + * Also, as conversion rates for diferent currencies usually arise from various + * bank departments, there is another important need. There is a need to + * compose two convertors into one by merging all the information about + * currencies they know about. + */ +public class Task2Test extends TestCase { + public Task2Test(String testName) { + super(testName); + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + // As in Task1Test, keep in mind, that there are three parts + // of the whole system: + // 1. there is someone who knows the current exchange rate + // 2. there is someone who wants to do the conversion + // 3. there is the API between 1. and 2. which allows them to communicate + // + // Please backward compatibly enhance your existing API to support following + // usecases: + // + + /** Create convertor that understands two currencies, CZK and + * SKK. Make 100 SKK == 75 CZK. This is method for the group of users that + * knows the exchange rate, and needs to use the API to create objects + * with the exchange rate. Anyone shall be ready to call this method without + * any other method being called previously. The API itself shall know + * nothing about any rates, before this method is called. + */ + public static Convertor createTripleConvertor() { + // Rates: 1USD = 15CZK + // Rates: 1USD = 20SKK + // Rates: 75CZK = 100SKK + return mergeConvertors( + createConvertor(money(1, USD), money(15, CZK)), + createConvertor(money(1, USD), money(20, SKK)), + createConvertor(money(75, CZK), money(100, SKK)) + ); + } + + /** Define convertor that understands three currencies. Use it. + */ + public void testConvertorForUSDandCZKandSKK() throws Exception { + Convertor c = createTripleConvertor(); + + // convert $5 to CZK using c: + assertEquals("Result is 75 CZK", money(75, CZK),c.convert(money(5,USD), CZK)); + + // convert $5 to SKK using c: + assertEquals("Result is 100 SKK", money(100, SKK),c.convert(money(5,USD), SKK)); + + // convert 200SKK to CZK using c: + assertEquals("Result is 150 CZK", money(150, CZK),c.convert(money(200,SKK), CZK)); + + // convert 200SKK to USK using c: + assertEquals("Result is 10 USD", money(10, USD),c.convert(money(200,SKK), USD)); + } + + /** Merge all currency rates of convertor 1 with convertor 2. + * Implement this using your API, preferably this method just delegates + * into some API method which does the actual work, without requiring + * API clients to code anything complex. + */ + public static Convertor merge(Convertor one, Convertor two) { + return mergeConvertors((ExtendedConvertor)one, (ExtendedConvertor)two); + } + + /** Join the convertors from previous task, Task1Test and show that it + * can be used to do reasonable conversions. + */ + public void testConvertorComposition() throws Exception { + Convertor c = merge( + Task1Test.createCZKtoUSD(), + Task1Test.createSKKtoCZK() + ); + + // convert $5 to CZK using c: + assertEquals("Result is 85 CZK", money(85, CZK),c.convert(money(5,USD), CZK)); + + // convert $8 to CZK using c: + assertEquals("Result is 136 CZK", money(136, CZK),c.convert(money(8,USD), CZK)); + + // convert 1003CZK to USD using c: + assertEquals("Result is 59 USD", money(59, USD),c.convert(money(1003,CZK), USD)); + + // convert 16CZK using c: + assertEquals("Result is 20 SKK", money(20, SKK),c.convert(money(16,CZK), SKK)); + + // convert 500SKK to CZK using c: + assertEquals("Result is 400 CZK", money(400, CZK),c.convert(money(500,SKK), CZK)); + + } +}