diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/ArithmeticExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/ArithmeticExercises.java index 1886e7e..c1c0b07 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/ArithmeticExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/ArithmeticExercises.java @@ -21,7 +21,7 @@ public class ArithmeticExercises { * @return sum of both amounts in the same currency */ public MonetaryAmount addAmounts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) { - throw new UnsupportedOperationException("TODO"); + return firstAmount.add(secondAmount); } /** @@ -34,7 +34,7 @@ public class ArithmeticExercises { * @return difference after subtraction in the same currency */ public MonetaryAmount subtractAmounts(MonetaryAmount minuend, MonetaryAmount subtrahend) { - throw new UnsupportedOperationException("TODO"); + return minuend.subtract(subtrahend); } /** @@ -47,7 +47,7 @@ public class ArithmeticExercises { * @return product amount in the same currency */ public MonetaryAmount multiplyAmount(MonetaryAmount amount, int factor) { - throw new UnsupportedOperationException("TODO"); + return amount.multiply(factor); } /** @@ -60,6 +60,6 @@ public class ArithmeticExercises { * @return {@code true} if first amount is greater, otherwise {@code false} */ public boolean compareAmounts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) { - throw new UnsupportedOperationException("TODO"); + return firstAmount.isGreaterThan(secondAmount); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/FormattingExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/FormattingExercises.java index b1fa413..202dc81 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/FormattingExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/FormattingExercises.java @@ -1,6 +1,9 @@ package swiss.fihlon.workshop.money.part1; +import java.util.Locale; import javax.money.MonetaryAmount; +import javax.money.format.MonetaryAmountFormat; +import javax.money.format.MonetaryFormats; /** *

Part 1 workshop exercises for formatting monetary amounts using JSR-354.

@@ -21,7 +24,7 @@ public class FormattingExercises { * @return formatted amount string for Swiss German locale */ public String formatSwissGerman(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return format(amount, "de-CH"); } /** @@ -33,7 +36,7 @@ public class FormattingExercises { * @return formatted amount string for German locale */ public String formatGerman(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return format(amount, "de-DE"); } /** @@ -45,6 +48,20 @@ public class FormattingExercises { * @return formatted amount string for US locale */ public String formatUs(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return format(amount, "en-US"); + } + + /** + *

Format the given amount using the specified locale.

+ * + *

Use the provided language tag to obtain a locale-specific formatter.

+ * + * @param amount the amount to format + * @param languageTag IETF BCP 47 language tag (e.g. {@code de-CH}) + * @return formatted amount string + */ + private String format(MonetaryAmount amount, String languageTag) { + MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.forLanguageTag(languageTag)); + return format.format(amount); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/IntegrationExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/IntegrationExercises.java index 9036e31..4d09217 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/IntegrationExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/IntegrationExercises.java @@ -1,5 +1,13 @@ package swiss.fihlon.workshop.money.part1; +import java.util.Locale; +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import javax.money.MonetaryOperator; +import javax.money.format.MonetaryAmountFormat; +import javax.money.format.MonetaryFormats; +import org.javamoney.moneta.Money; + /** *

Part 1 workshop exercises for combining parsing, arithmetic, rounding, and formatting with JSR-354.

* @@ -22,6 +30,24 @@ public class IntegrationExercises { * @return formatted final price text */ public String calculateFinalPrice(String priceText) { - throw new UnsupportedOperationException("TODO"); + // setup locale and formatter + Locale swissGerman = Locale.forLanguageTag("de-CH"); + MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(swissGerman); + + // parse + MonetaryAmount price = format.parse(priceText); + + // constants + MonetaryAmount shipping = Money.of(4.95, "CHF"); + MonetaryAmount discount = Money.of(2.00, "CHF"); + + // calculate + MonetaryAmount total = price.add(shipping).subtract(discount); + + // round + MonetaryAmount roundedTotal = total.with(Monetary.getDefaultRounding()); + + // format + return format.format(roundedTotal); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/MoneyExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/MoneyExercises.java index e63510e..28d6ddf 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/MoneyExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/MoneyExercises.java @@ -1,6 +1,9 @@ package swiss.fihlon.workshop.money.part1; +import org.javamoney.moneta.Money; + import javax.money.CurrencyUnit; +import javax.money.Monetary; import javax.money.MonetaryAmount; /** @@ -19,7 +22,7 @@ public class MoneyExercises { * @return Swiss franc currency unit ({@code CHF}) */ public CurrencyUnit createSwissFranc() { - throw new UnsupportedOperationException("TODO"); + return Monetary.getCurrency("CHF"); } /** @@ -29,7 +32,7 @@ public class MoneyExercises { * @return monetary amount {@code 19.95 CHF} */ public MonetaryAmount createSwissFrancAmount() { - throw new UnsupportedOperationException("TODO"); + return Money.of(19.95, "CHF"); } /** @@ -39,7 +42,7 @@ public class MoneyExercises { * @return monetary amount {@code 2500 JPY} */ public MonetaryAmount createJapaneseYenAmount() { - throw new UnsupportedOperationException("TODO"); + return Money.of(2500, "JPY"); } /** @@ -49,6 +52,6 @@ public class MoneyExercises { * @return monetary amount {@code 12.345 TND} */ public MonetaryAmount createTunisianDinarAmount() { - throw new UnsupportedOperationException("TODO"); + return Money.of(12.345, "TND"); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/ParsingExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/ParsingExercises.java index 301c76a..cb77428 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/ParsingExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/ParsingExercises.java @@ -1,6 +1,9 @@ package swiss.fihlon.workshop.money.part1; import javax.money.MonetaryAmount; +import javax.money.format.MonetaryAmountFormat; +import javax.money.format.MonetaryFormats; +import javax.money.format.MonetaryParseException; import java.util.Locale; /** @@ -24,7 +27,7 @@ public class ParsingExercises { * @return parsed monetary amount */ public MonetaryAmount parseSwissGerman(String text) { - throw new UnsupportedOperationException("TODO"); + return parse(text, "de-CH"); } /** @@ -36,7 +39,7 @@ public class ParsingExercises { * @return parsed monetary amount */ public MonetaryAmount parseGerman(String text) { - throw new UnsupportedOperationException("TODO"); + return parse(text, "de-DE"); } /** @@ -48,7 +51,22 @@ public class ParsingExercises { * @return parsed monetary amount */ public MonetaryAmount parseUs(String text) { - throw new UnsupportedOperationException("TODO"); + return parse(text, "en-US"); + } + + /** + *

Parse the given monetary amount text using the specified locale.

+ * + *

Use the provided language tag to obtain a locale-specific parser.

+ * + * @param text the input text to parse + * @param languageTag IETF BCP 47 language tag (e.g. {@code de-CH}) + * @return parsed monetary amount + */ + private MonetaryAmount parse(String text, String languageTag) { + MonetaryAmountFormat format = + MonetaryFormats.getAmountFormat(Locale.forLanguageTag(languageTag)); + return format.parse(text); } /** @@ -61,6 +79,12 @@ public class ParsingExercises { * @return {@code true} if parsing succeeds, otherwise {@code false} */ public boolean parseInvalidInput(String text, Locale locale) { - throw new UnsupportedOperationException("TODO"); + MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale); + try { + format.parse(text); + return true; + } catch (MonetaryParseException exception) { + return false; + } } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part1/RoundingExercises.java b/src/main/java/swiss/fihlon/workshop/money/part1/RoundingExercises.java index ac299e8..3e81ccf 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part1/RoundingExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part1/RoundingExercises.java @@ -1,6 +1,8 @@ package swiss.fihlon.workshop.money.part1; +import javax.money.Monetary; import javax.money.MonetaryAmount; +import javax.money.MonetaryOperator; /** *

Part 1 workshop exercises for currency-aware rounding with JSR-354.

@@ -20,7 +22,7 @@ public class RoundingExercises { * @return rounded amount */ public MonetaryAmount applyDefaultRounding(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return amount.with(Monetary.getDefaultRounding()); } /** @@ -33,6 +35,7 @@ public class RoundingExercises { * @return divided and rounded amount */ public MonetaryAmount divideAndRound(MonetaryAmount amount, int divisor) { - throw new UnsupportedOperationException("TODO"); + return amount.divide(divisor) + .with(Monetary.getDefaultRounding()); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java index 945040b..2173271 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java @@ -2,6 +2,7 @@ package swiss.fihlon.workshop.money.part2; import javax.money.MonetaryAmount; import javax.money.convert.CurrencyConversion; +import javax.money.convert.MonetaryConversions; /** *

Part 2 workshop exercises for currency conversion using JSR-354.

@@ -20,7 +21,7 @@ public class CurrencyConversionExercises { * @return currency conversion to EUR */ public CurrencyConversion getEurConversion() { - throw new UnsupportedOperationException("TODO"); + return MonetaryConversions.getConversion("EUR", "ECB"); } /** @@ -32,6 +33,7 @@ public class CurrencyConversionExercises { * @return converted amount in EUR */ public MonetaryAmount convertChfToEur(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + CurrencyConversion conversion = getEurConversion(); + return amount.with(conversion); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryContextExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryContextExercises.java index 395a7be..eef2f2e 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryContextExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryContextExercises.java @@ -22,7 +22,7 @@ public class MonetaryContextExercises { * @return monetary context of the amount */ public MonetaryContext getMonetaryContext(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return amount.getContext(); } /** @@ -34,7 +34,7 @@ public class MonetaryContextExercises { * @return maximum scale from the monetary context */ public int getMaxScale(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return amount.getContext().getMaxScale(); } /** @@ -47,7 +47,7 @@ public class MonetaryContextExercises { * @return {@code true} if both contexts are equal, otherwise {@code false} */ public boolean compareContexts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) { - throw new UnsupportedOperationException("TODO"); + return firstAmount.getContext().equals(secondAmount.getContext()); } /** @@ -59,6 +59,7 @@ public class MonetaryContextExercises { * @return formatted context description */ public String describeContext(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryContext context = amount.getContext(); + return "precision=" + context.getPrecision() + ", maxScale=" + context.getMaxScale(); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryOperatorExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryOperatorExercises.java index 3147e8f..c2b9e30 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryOperatorExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryOperatorExercises.java @@ -1,14 +1,13 @@ package swiss.fihlon.workshop.money.part2; +import java.math.BigDecimal; import javax.money.MonetaryAmount; import javax.money.MonetaryOperator; /** *

Part 2 workshop exercises for encapsulating domain logic with MonetaryOperator.

* - *

The exercises focus on applying reusable monetary operations such as discounts and VAT.

- * - *

The methods are intentionally incomplete and should be implemented by making tests pass.

+ *

The examples focus on applying reusable monetary operations such as discounts and VAT.

*/ public class MonetaryOperatorExercises { @@ -21,7 +20,8 @@ public class MonetaryOperatorExercises { * @return discounted amount */ public MonetaryAmount applyDiscount(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator discount = createDiscountOperator(); + return amount.with(discount); } /** @@ -33,7 +33,8 @@ public class MonetaryOperatorExercises { * @return amount after VAT */ public MonetaryAmount applyVat(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator vat = createVatOperator(); + return amount.with(vat); } /** @@ -45,7 +46,18 @@ public class MonetaryOperatorExercises { * @return amount after discount and VAT */ public MonetaryAmount applyDiscountThenVat(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator discount = createDiscountOperator(); + MonetaryOperator vat = createVatOperator(); + return amount.with(discount).with(vat); + } + + /** + *

Create a reusable 7.7% VAT monetary operator.

+ * + * @return a reusable VAT operator + */ + public MonetaryOperator createVatOperator() { + return monetaryAmount -> monetaryAmount.multiply(new BigDecimal("1.077")); } /** @@ -56,6 +68,6 @@ public class MonetaryOperatorExercises { * @return reusable discount operator */ public MonetaryOperator createDiscountOperator() { - throw new UnsupportedOperationException("TODO"); + return monetaryAmount -> monetaryAmount.multiply(new BigDecimal("0.90")); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryQueryExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryQueryExercises.java index 79d3d76..3a89380 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryQueryExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/MonetaryQueryExercises.java @@ -7,9 +7,7 @@ import javax.money.MonetaryQuery; /** *

Part 2 workshop exercises for extracting information from monetary amounts with MonetaryQuery.

* - *

The exercises focus on encapsulating reusable query logic for monetary values and currencies.

- * - *

The methods are intentionally incomplete and should be implemented by making tests pass.

+ *

The examples focus on encapsulating reusable query logic for monetary values and currencies.

*/ public class MonetaryQueryExercises { @@ -22,7 +20,7 @@ public class MonetaryQueryExercises { * @return currency code */ public String queryCurrencyCode(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + return amount.query(createCurrencyCodeQuery()); } /** @@ -34,7 +32,9 @@ public class MonetaryQueryExercises { * @return default fraction digits of the currency */ public int queryDefaultFractionDigits(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryQuery query = + monetaryAmount -> monetaryAmount.getCurrency().getDefaultFractionDigits(); + return amount.query(query); } /** @@ -46,7 +46,9 @@ public class MonetaryQueryExercises { * @return numeric value as BigDecimal */ public BigDecimal queryNumberAsBigDecimal(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryQuery query = + monetaryAmount -> monetaryAmount.getNumber().numberValueExact(BigDecimal.class); + return amount.query(query); } /** @@ -57,6 +59,6 @@ public class MonetaryQueryExercises { * @return reusable query returning a currency code */ public MonetaryQuery createCurrencyCodeQuery() { - throw new UnsupportedOperationException("TODO"); + return monetaryAmount -> monetaryAmount.getCurrency().getCurrencyCode(); } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java index 03d3b2f..e3bb88b 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java @@ -1,14 +1,15 @@ package swiss.fihlon.workshop.money.part2; +import java.math.BigDecimal; +import java.math.RoundingMode; +import javax.money.Monetary; import javax.money.MonetaryAmount; import javax.money.MonetaryOperator; /** - *

Part 2 workshop exercises for applying different rounding strategies with JSR-354.

+ *

Part 2 workshop examples for applying different rounding strategies with JSR-354.

* - *

The exercises focus on treating rounding as an explicit domain decision that depends on the use case.

- * - *

The methods are intentionally incomplete and should be implemented by making tests pass.

+ *

The examples focus on treating rounding as an explicit domain decision that depends on the use case.

*/ public class RoundingStrategyExercises { @@ -21,7 +22,8 @@ public class RoundingStrategyExercises { * @return amount rounded with default currency rounding */ public MonetaryAmount applyDefaultRounding(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator rounding = Monetary.getDefaultRounding(); + return amount.with(rounding); } /** @@ -33,7 +35,8 @@ public class RoundingStrategyExercises { * @return CHF amount rounded to the nearest 0.05 */ public MonetaryAmount applySwissCashRounding(MonetaryAmount amount) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator swissCashRounding = createSwissCashRounding(); + return amount.with(swissCashRounding); } /** @@ -44,6 +47,19 @@ public class RoundingStrategyExercises { * @return reusable Swiss cash rounding operator */ public MonetaryOperator createSwissCashRounding() { - throw new UnsupportedOperationException("TODO"); + return monetaryAmount -> { + if (!"CHF".equals(monetaryAmount.getCurrency().getCurrencyCode())) { + throw new IllegalArgumentException("Swiss cash rounding only supports CHF."); + } + + BigDecimal increment = new BigDecimal("0.05"); + BigDecimal value = monetaryAmount.getNumber().numberValueExact(BigDecimal.class); + BigDecimal rounded = value.divide(increment, 0, RoundingMode.HALF_UP).multiply(increment); + + return monetaryAmount.getFactory() + .setCurrency(monetaryAmount.getCurrency()) + .setNumber(rounded) + .create(); + }; } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part3/OrderExercises.java b/src/main/java/swiss/fihlon/workshop/money/part3/OrderExercises.java index 3ada081..549cb6f 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part3/OrderExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part3/OrderExercises.java @@ -1,14 +1,19 @@ package swiss.fihlon.workshop.money.part3; +import java.math.BigDecimal; +import java.util.Locale; import javax.money.CurrencyUnit; +import javax.money.Monetary; import javax.money.MonetaryAmount; +import javax.money.MonetaryOperator; +import javax.money.convert.MonetaryConversions; +import javax.money.format.MonetaryAmountFormat; +import javax.money.format.MonetaryFormats; /** - *

Part 3 workshop exercises for integrating monetary calculations into a small order workflow.

+ *

Part 3 workshop examples for integrating monetary calculations into a small order workflow.

* - *

The exercises focus on combining subtotal calculation, discount, VAT, rounding, and formatting.

- * - *

The methods are intentionally incomplete and should be implemented by making tests pass.

+ *

The examples focus on combining subtotal calculation, discount, VAT, rounding, and formatting.

*/ public class OrderExercises { @@ -22,7 +27,7 @@ public class OrderExercises { * @return subtotal amount */ public MonetaryAmount calculateSubtotal(MonetaryAmount unitPrice, int quantity) { - throw new UnsupportedOperationException("TODO"); + return unitPrice.multiply(quantity); } /** @@ -34,7 +39,8 @@ public class OrderExercises { * @return discounted amount */ public MonetaryAmount applyDiscount(MonetaryAmount subtotal) { - throw new UnsupportedOperationException("TODO"); + MonetaryOperator discount = amount -> amount.multiply(new BigDecimal("0.90")); + return subtotal.with(discount); } /** @@ -46,7 +52,9 @@ public class OrderExercises { * @return VAT-inclusive amount */ public MonetaryAmount applyVat(MonetaryAmount discountedAmount) { - throw new UnsupportedOperationException("TODO"); + MonetaryAmount vatInclusiveAmount = discountedAmount.multiply(new BigDecimal("1.077")); + MonetaryOperator rounding = Monetary.getDefaultRounding(); + return vatInclusiveAmount.with(rounding); } /** @@ -59,7 +67,9 @@ public class OrderExercises { * @return final total amount */ public MonetaryAmount calculateTotal(MonetaryAmount unitPrice, int quantity) { - throw new UnsupportedOperationException("TODO"); + MonetaryAmount subtotal = calculateSubtotal(unitPrice, quantity); + MonetaryAmount discountedAmount = applyDiscount(subtotal); + return applyVat(discountedAmount); } /** @@ -71,7 +81,8 @@ public class OrderExercises { * @return formatted total string */ public String formatTotal(MonetaryAmount total) { - throw new UnsupportedOperationException("TODO"); + MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.forLanguageTag("de-CH")); + return format.format(total); } /** @@ -84,6 +95,6 @@ public class OrderExercises { * @return amount converted to the target currency */ public MonetaryAmount convertToCurrency(MonetaryAmount amount, CurrencyUnit targetCurrency) { - throw new UnsupportedOperationException("TODO"); + return amount.with(MonetaryConversions.getConversion(targetCurrency, "ECB")); } } 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 Map ratesByPair; /** *

Create 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 Map loadRates() { + InputStream inputStream = CsvExchangeRateProvider.class.getClassLoader().getResourceAsStream(RESOURCE_NAME); + if (inputStream == null) { + throw new IllegalStateException("Could not load classpath resource: " + RESOURCE_NAME); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + Map rates = new HashMap<>(); + reader.readLine(); + String line; + while ((line = reader.readLine()) != null) { + String[] values = line.split(","); + CurrencyPair currencyPair = new CurrencyPair(values[0], values[1]); + BigDecimal factor = new BigDecimal(values[2]); + rates.put(currencyPair, factor); + } + return rates; + } catch (Exception exception) { + throw new IllegalStateException("Failed to parse exchange rates from: " + RESOURCE_NAME, exception); + } + } + + /** + *

Simple 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 Class DATE_QUERY_KEY = LocalDate.class; + private static final ProviderContext CONTEXT = + ProviderContextBuilder.of("csv-historical-exchange-rate-provider", RateType.HISTORIC).build(); + + private final Map> ratesByPairAndDate; /** *

Create 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); + NavigableMap ratesByDate = ratesByPairAndDate.get(currencyPair); + if (ratesByDate == null) { + return null; + } + + Map.Entry bestMatch = ratesByDate.floorEntry(requestedDate); + if (bestMatch == null) { + return null; + } + + return new ExchangeRateBuilder(getContext().getProviderName(), RateType.HISTORIC) + .setBase(conversionQuery.getBaseCurrency()) + .setTerm(conversionQuery.getCurrency()) + .setFactor(DefaultNumberValue.of(bestMatch.getValue())) + .build(); } /** @@ -78,6 +117,61 @@ public class CsvHistoricalExchangeRateProvider 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); + } + + /** + *

Load 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 Map> loadRates() { + InputStream inputStream = CsvHistoricalExchangeRateProvider.class.getClassLoader().getResourceAsStream(RESOURCE_NAME); + if (inputStream == null) { + throw new IllegalStateException("Could not load classpath resource: " + RESOURCE_NAME); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + Map> rates = new HashMap<>(); + reader.readLine(); + String line; + while ((line = reader.readLine()) != null) { + String[] values = line.split(","); + LocalDate date = LocalDate.parse(values[0]); + CurrencyPair currencyPair = new CurrencyPair(values[1], values[2]); + BigDecimal factor = new BigDecimal(values[3]); + rates.computeIfAbsent(currencyPair, key -> new TreeMap<>()).put(date, factor); + } + return rates; + } catch (Exception exception) { + throw new IllegalStateException("Failed to parse historical exchange rates from: " + RESOURCE_NAME, exception); + } + } + + /** + *

Simple 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; } } diff --git a/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java b/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java index f563807..1ad9a00 100644 --- a/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java +++ b/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java @@ -1,12 +1,12 @@ package swiss.fihlon.workshop.money.part2; -import org.javamoney.moneta.Money; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.time.LocalDateTime; import javax.money.MonetaryAmount; import javax.money.convert.CurrencyConversion; - -import static org.assertj.core.api.Assertions.assertThat; +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; class CurrencyConversionExercisesTest { @@ -29,4 +29,25 @@ class CurrencyConversionExercisesTest { assertThat(result).isNotNull(); assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("EUR"); } + + @Test + void shouldCreateEurConversionForTimestamp() { + LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30); + + CurrencyConversion conversion = exercises.getEurConversionForTimestamp(timestamp); + + assertThat(conversion).isNotNull(); + assertThat(conversion.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + } + + @Test + void shouldConvertChfToEurAtTimestamp() { + MonetaryAmount chfAmount = Money.of(10, "CHF"); + LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30); + + MonetaryAmount result = exercises.convertChfToEurAt(chfAmount, timestamp); + + assertThat(result).isNotNull(); + assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + } } diff --git a/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java b/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java index 7e2c60e..6904009 100644 --- a/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java +++ b/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java @@ -1,11 +1,10 @@ package swiss.fihlon.workshop.money.part2; -import org.javamoney.moneta.Money; -import org.junit.jupiter.api.Test; - +import java.math.BigDecimal; import javax.money.MonetaryAmount; import javax.money.MonetaryOperator; -import java.math.BigDecimal; +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -24,6 +23,17 @@ class RoundingStrategyExercisesTest { assertThat(result.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.02"); } + @Test + void shouldRoundVatAmount() { + MonetaryAmount taxAmount = Money.of(1.537, "CHF"); + + MonetaryAmount result = exercises.roundVatAmount(taxAmount); + + assertThat(result).isNotNull(); + assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("CHF"); + assertThat(result.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("1.54"); + } + @Test void shouldApplySwissCashRounding() { MonetaryAmount firstAmount = Money.of(10.02, "CHF");