diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java index 005f3e4..d508689 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java +++ b/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java @@ -1,18 +1,30 @@ package swiss.fihlon.workshop.money.part4; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionContext; +import javax.money.convert.ConversionContextBuilder; import javax.money.convert.CurrencyConversion; import javax.money.convert.ExchangeRate; import javax.money.convert.ExchangeRateProvider; import javax.money.convert.ProviderContext; +import javax.money.convert.ProviderContextBuilder; +import javax.money.convert.RateType; +import org.javamoney.moneta.convert.ExchangeRateBuilder; +import org.javamoney.moneta.spi.DefaultNumberValue; +import org.javamoney.moneta.spi.LazyBoundCurrencyConversion; /** *
Bonus exercise 1 for implementing a local CSV-based exchange rate provider with JSR-354.
* *The provider is intended for workshop usage and should read rates from the classpath resource * {@code exchange-rates.csv}.
- * - *The implementation is intentionally incomplete and should be finished by making tests pass.
*/ public class CsvExchangeRateProvider implements ExchangeRateProvider { @@ -20,6 +32,10 @@ public class CsvExchangeRateProvider implements ExchangeRateProvider { *Name of the classpath CSV resource containing exchange rates.
*/ public static final String RESOURCE_NAME = "exchange-rates.csv"; + private static final ProviderContext CONTEXT = + ProviderContextBuilder.of("csv-exchange-rate-provider", RateType.HISTORIC).build(); + + private final MapCreate a new provider instance.
@@ -27,6 +43,7 @@ public class CsvExchangeRateProvider implements ExchangeRateProvider { *The provider should load exchange rates from {@link #RESOURCE_NAME}.
*/ public CsvExchangeRateProvider() { + this.ratesByPair = loadRates(); } /** @@ -36,7 +53,7 @@ public class CsvExchangeRateProvider implements ExchangeRateProvider { */ @Override public ProviderContext getContext() { - throw new UnsupportedOperationException("TODO"); + return CONTEXT; } /** @@ -47,7 +64,16 @@ public class CsvExchangeRateProvider implements ExchangeRateProvider { */ @Override public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { - throw new UnsupportedOperationException("TODO"); + CurrencyPair currencyPair = CurrencyPair.from(conversionQuery); + BigDecimal factor = ratesByPair.get(currencyPair); + if (factor == null) { + return null; + } + return new ExchangeRateBuilder(getContext().getProviderName(), RateType.DEFERRED) + .setBase(conversionQuery.getBaseCurrency()) + .setTerm(conversionQuery.getCurrency()) + .setFactor(DefaultNumberValue.of(factor)) + .build(); } /** @@ -60,6 +86,60 @@ public class CsvExchangeRateProvider implements ExchangeRateProvider { */ @Override public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) { - throw new UnsupportedOperationException("TODO"); + if (getExchangeRate(conversionQuery) == null) { + return null; + } + ConversionContext conversionContext = ConversionContextBuilder.create(getContext(), RateType.DEFERRED).build(); + return new LazyBoundCurrencyConversion(conversionQuery, this, conversionContext); + } + + /** + *Load exchange rates from the classpath CSV resource into a lookup map.
+ * + *The CSV is expected to contain a header and records in the format + * {@code base,term,rate}.
+ * + * @return map keyed by currency pair containing the configured exchange factor + */ + private static MapSimple key for identifying an exchange rate by base and term currency code.
+ * + * @param baseCurrencyCode ISO currency code of the base currency + * @param termCurrencyCode ISO currency code of the term currency + */ + private record CurrencyPair(String baseCurrencyCode, String termCurrencyCode) { + /** + *Create a currency-pair key from a conversion query.
+ * + * @param conversionQuery query containing base and term currency + * @return key for map-based rate lookup + */ + private static CurrencyPair from(ConversionQuery conversionQuery) { + return new CurrencyPair( + conversionQuery.getBaseCurrency().getCurrencyCode(), + conversionQuery.getCurrency().getCurrencyCode()); + } } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java index 320bbaa..802bd1f 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java +++ b/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java @@ -1,11 +1,27 @@ package swiss.fihlon.workshop.money.part4; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigDecimal; import java.time.LocalDate; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionContext; +import javax.money.convert.ConversionContextBuilder; import javax.money.convert.CurrencyConversion; import javax.money.convert.ExchangeRate; import javax.money.convert.ExchangeRateProvider; import javax.money.convert.ProviderContext; +import javax.money.convert.ProviderContextBuilder; +import javax.money.convert.RateType; +import org.javamoney.moneta.convert.ExchangeRateBuilder; +import org.javamoney.moneta.spi.DefaultNumberValue; +import org.javamoney.moneta.spi.LazyBoundCurrencyConversion; /** *Bonus exercise 2 for implementing a local CSV-based historical exchange rate provider with JSR-354.
@@ -14,8 +30,6 @@ import javax.money.convert.ProviderContext; * {@code exchange-rates-hist.csv}. * *The lookup should use base currency, term currency, and a date passed in the query context.
- * - *The implementation is intentionally incomplete and should be finished by making tests pass.
*/ public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { @@ -28,6 +42,10 @@ public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { *Query key type for historical lookups.
*/ public static final ClassCreate a new provider instance.
@@ -35,6 +53,7 @@ public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { *The provider should load exchange rates from {@link #RESOURCE_NAME}.
*/ public CsvHistoricalExchangeRateProvider() { + this.ratesByPairAndDate = loadRates(); } /** @@ -44,7 +63,7 @@ public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { */ @Override public ProviderContext getContext() { - throw new UnsupportedOperationException("TODO"); + return CONTEXT; } /** @@ -62,7 +81,27 @@ public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { */ @Override public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { - throw new UnsupportedOperationException("TODO"); + LocalDate requestedDate = conversionQuery.get(DATE_QUERY_KEY); + if (requestedDate == null) { + return null; + } + + CurrencyPair currencyPair = CurrencyPair.from(conversionQuery); + NavigableMapLoad historical exchange rates from the classpath CSV resource into a date-indexed lookup map.
+ * + *The CSV is expected to contain a header and records in the format + * {@code date,base,term,rate}.
+ * + * @return map keyed by currency pair with a navigable date-to-rate mapping + */ + private static MapSimple key for identifying a historical exchange-rate series by base and term currency code.
+ * + * @param baseCurrencyCode ISO currency code of the base currency + * @param termCurrencyCode ISO currency code of the term currency + */ + private record CurrencyPair(String baseCurrencyCode, String termCurrencyCode) { + /** + *Create a currency-pair key from a conversion query.
+ * + * @param conversionQuery query containing base and term currency + * @return key for map-based historical rate lookup + */ + private static CurrencyPair from(ConversionQuery conversionQuery) { + return new CurrencyPair( + conversionQuery.getBaseCurrency().getCurrencyCode(), + conversionQuery.getCurrency().getCurrencyCode()); + } } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java index 0dadb80..2d4c7c6 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java +++ b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java @@ -1,20 +1,32 @@ package swiss.fihlon.workshop.money.part4; +import java.math.BigDecimal; import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionContext; +import javax.money.convert.ConversionContextBuilder; import javax.money.convert.CurrencyConversion; import javax.money.convert.ExchangeRate; import javax.money.convert.ExchangeRateProvider; import javax.money.convert.ProviderContext; +import javax.money.convert.ProviderContextBuilder; +import javax.money.convert.RateType; +import org.javamoney.moneta.convert.ExchangeRateBuilder; +import org.javamoney.moneta.spi.DefaultNumberValue; +import org.javamoney.moneta.spi.LazyBoundCurrencyConversion; /** *Bonus exercise 3 for implementing local exchange rates between CHF and DukePoints.
* *The provider should support deterministic conversion for {@code CHF -> DKP} and {@code DKP -> CHF}.
- * - *The implementation is intentionally incomplete and should be finished by making tests pass.
*/ public class DukePointsExchangeRateProvider implements ExchangeRateProvider { + private static final BigDecimal CHF_TO_DKP_FACTOR = new BigDecimal("0.1"); + private static final BigDecimal DKP_TO_CHF_FACTOR = new BigDecimal("10"); + private static final ProviderContext CONTEXT = + ProviderContextBuilder.of("dkp-exchange-rate-provider", RateType.HISTORIC).build(); + private static final String CHF = "CHF"; + /** *Create a new exchange rate provider for DukePoints.
*/ @@ -28,7 +40,7 @@ public class DukePointsExchangeRateProvider implements ExchangeRateProvider { */ @Override public ProviderContext getContext() { - throw new UnsupportedOperationException("TODO"); + return CONTEXT; } /** @@ -41,7 +53,19 @@ public class DukePointsExchangeRateProvider implements ExchangeRateProvider { */ @Override public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { - throw new UnsupportedOperationException("TODO"); + String baseCurrencyCode = conversionQuery.getBaseCurrency().getCurrencyCode(); + String termCurrencyCode = conversionQuery.getCurrency().getCurrencyCode(); + + BigDecimal factor = resolveFactor(baseCurrencyCode, termCurrencyCode); + if (factor == null) { + return null; + } + + return new ExchangeRateBuilder(getContext().getProviderName(), RateType.HISTORIC) + .setBase(conversionQuery.getBaseCurrency()) + .setTerm(conversionQuery.getCurrency()) + .setFactor(DefaultNumberValue.of(factor)) + .build(); } /** @@ -54,6 +78,27 @@ public class DukePointsExchangeRateProvider implements ExchangeRateProvider { */ @Override public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) { - throw new UnsupportedOperationException("TODO"); + if (getExchangeRate(conversionQuery) == null) { + return null; + } + ConversionContext conversionContext = ConversionContextBuilder.create(getContext(), RateType.HISTORIC).build(); + return new LazyBoundCurrencyConversion(conversionQuery, this, conversionContext); + } + + /** + *Resolve the conversion factor for a supported DukePoints currency pair.
+ * + * @param baseCurrencyCode currency code of the base currency + * @param termCurrencyCode currency code of the target currency + * @return conversion factor for supported pairs, otherwise {@code null} + */ + private BigDecimal resolveFactor(String baseCurrencyCode, String termCurrencyCode) { + if (CHF.equals(baseCurrencyCode) && DukePointsCurrencyFactory.DKP.equals(termCurrencyCode)) { + return CHF_TO_DKP_FACTOR; + } + if (DukePointsCurrencyFactory.DKP.equals(baseCurrencyCode) && CHF.equals(termCurrencyCode)) { + return DKP_TO_CHF_FACTOR; + } + return null; } }