Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c711a077e | |||
| f307005c6d | |||
| 4f915afb06 |
23 changed files with 1070 additions and 100 deletions
|
|
@ -154,9 +154,11 @@ BigDecimal amount = new BigDecimal("10.123") // gültig oder nicht?
|
||||||
|
|
||||||
<div class="my-6"></div>
|
<div class="my-6"></div>
|
||||||
|
|
||||||
- CHF: 2 Nachkommastellen
|
<ul v-click>
|
||||||
- JPY: keine Nachkommastellen
|
<li>CHF: 2 Nachkommastellen</li>
|
||||||
- TND: 3 Nachkommastellen
|
<li>JPY: keine Nachkommastellen</li>
|
||||||
|
<li>TND: 3 Nachkommastellen</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -234,8 +236,7 @@ Was ist JSR-354?
|
||||||
- Spezifikation: Java Community Process (JCP)
|
- Spezifikation: Java Community Process (JCP)
|
||||||
- Referenzimplementierung: Moneta
|
- Referenzimplementierung: Moneta
|
||||||
- Entwickelt und gepflegt von der Java-Community
|
- Entwickelt und gepflegt von der Java-Community
|
||||||
- Spec Lead JSR-354: Werner Keil
|
- Spec Leads: Anatole Tresch, Werner Keil und Otavio Santana
|
||||||
- Lead der Referenzimplementierung (Moneta): Anatole Tresch
|
|
||||||
|
|
||||||
👉 Open Source und gemeinschaftlich weiterentwickelt
|
👉 Open Source und gemeinschaftlich weiterentwickelt
|
||||||
|
|
||||||
|
|
@ -676,7 +677,7 @@ MonetaryAmount amountCHF = Money.of(10, "CHF");
|
||||||
|
|
||||||
ConversionQuery query = ConversionQueryBuilder.of()
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
.setTermCurrency("EUR")
|
.setTermCurrency("EUR")
|
||||||
.set(LocalDateTime.class, LocalDateTime.of(2026, 3, 13, 14, 3, 27))
|
.set(LocalDate.class, LocalDate.of(2026, 3, 13))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
CurrencyConversion conversion = MonetaryConversions.getConversion(query);
|
CurrencyConversion conversion = MonetaryConversions.getConversion(query);
|
||||||
|
|
@ -992,6 +993,66 @@ Warenkorb / Bestellung
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Bonusaufgabe 1
|
||||||
|
|
||||||
|
Eigener Exchange Rate Provider
|
||||||
|
|
||||||
|
Package: `swiss.fihlon.workshop.money.part4`
|
||||||
|
|
||||||
|
- Implementiere einen eigenen `ExchangeRateProvider`
|
||||||
|
- Lade Wechselkurse aus `exchange-rates.csv`
|
||||||
|
- CSV enthält:
|
||||||
|
- Quellwährung
|
||||||
|
- Zielwährung
|
||||||
|
- Wechselkurs
|
||||||
|
- Stelle Umrechnungen zwischen den Währungen bereit
|
||||||
|
- Noch **ohne** Historisierung
|
||||||
|
|
||||||
|
Relevante Interfaces: `ExchangeRateProvider`
|
||||||
|
|
||||||
|
👉 Ziel: eigener Provider mit lokal geladenen Kursen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bonus 2
|
||||||
|
|
||||||
|
Historische Kurse
|
||||||
|
|
||||||
|
Package: `swiss.fihlon.workshop.money.part4`
|
||||||
|
|
||||||
|
- Erweitere deinen Provider aus Bonus 1
|
||||||
|
- Verwende `exchange-rates-hist.csv`
|
||||||
|
- CSV enthält zusätzlich:
|
||||||
|
- Datum pro Wechselkurs
|
||||||
|
- Wähle den passenden Kurs für ein angefragtes Datum
|
||||||
|
- Fallback: sinnvoller Standard (z. B. letzter verfügbarer Kurs)
|
||||||
|
|
||||||
|
Relevante Interfaces: `ExchangeRateProvider`
|
||||||
|
|
||||||
|
👉 Ziel: zeitabhängige Wechselkurse unterstützen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bonus 3
|
||||||
|
|
||||||
|
Eigene Währung
|
||||||
|
|
||||||
|
Package: `swiss.fihlon.workshop.money.part4`
|
||||||
|
|
||||||
|
- Implementiere die eigene Währung `DKP`
|
||||||
|
- Vorgegebener Umrechnungsfaktor:
|
||||||
|
- 10 CHF = 1 DKP
|
||||||
|
- Baue einen eigenen Converter
|
||||||
|
- Unterstütze:
|
||||||
|
- CHF → DKP
|
||||||
|
- DKP → CHF
|
||||||
|
|
||||||
|
Relevante Interfaces: `CurrencyUnit` und `ExchangeRateProvider`
|
||||||
|
|
||||||
|
👉 Ziel: eigene Währung ins Modell integrieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Voraussetzungen
|
# Voraussetzungen
|
||||||
|
|
||||||
Java-Versionen
|
Java-Versionen
|
||||||
|
|
@ -1058,4 +1119,4 @@ layout: center
|
||||||
layout: center
|
layout: center
|
||||||
---
|
---
|
||||||
|
|
||||||
# **Danke!**
|
# Danke!
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class ArithmeticExercises {
|
||||||
* @return sum of both amounts in the same currency
|
* @return sum of both amounts in the same currency
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount addAmounts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) {
|
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
|
* @return difference after subtraction in the same currency
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount subtractAmounts(MonetaryAmount minuend, MonetaryAmount subtrahend) {
|
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
|
* @return product amount in the same currency
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount multiplyAmount(MonetaryAmount amount, int factor) {
|
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}
|
* @return {@code true} if first amount is greater, otherwise {@code false}
|
||||||
*/
|
*/
|
||||||
public boolean compareAmounts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) {
|
public boolean compareAmounts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return firstAmount.isGreaterThan(secondAmount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package swiss.fihlon.workshop.money.part1;
|
package swiss.fihlon.workshop.money.part1;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.format.MonetaryAmountFormat;
|
||||||
|
import javax.money.format.MonetaryFormats;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 1 workshop exercises for formatting monetary amounts using JSR-354.</p>
|
* <p>Part 1 workshop exercises for formatting monetary amounts using JSR-354.</p>
|
||||||
|
|
@ -21,7 +24,7 @@ public class FormattingExercises {
|
||||||
* @return formatted amount string for Swiss German locale
|
* @return formatted amount string for Swiss German locale
|
||||||
*/
|
*/
|
||||||
public String formatSwissGerman(MonetaryAmount amount) {
|
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
|
* @return formatted amount string for German locale
|
||||||
*/
|
*/
|
||||||
public String formatGerman(MonetaryAmount amount) {
|
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
|
* @return formatted amount string for US locale
|
||||||
*/
|
*/
|
||||||
public String formatUs(MonetaryAmount amount) {
|
public String formatUs(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return format(amount, "en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Format the given amount using the specified locale.</p>
|
||||||
|
*
|
||||||
|
* <p>Use the provided language tag to obtain a locale-specific formatter.</p>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
package swiss.fihlon.workshop.money.part1;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 1 workshop exercises for combining parsing, arithmetic, rounding, and formatting with JSR-354.</p>
|
* <p>Part 1 workshop exercises for combining parsing, arithmetic, rounding, and formatting with JSR-354.</p>
|
||||||
*
|
*
|
||||||
|
|
@ -22,6 +30,24 @@ public class IntegrationExercises {
|
||||||
* @return formatted final price text
|
* @return formatted final price text
|
||||||
*/
|
*/
|
||||||
public String calculateFinalPrice(String priceText) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package swiss.fihlon.workshop.money.part1;
|
package swiss.fihlon.workshop.money.part1;
|
||||||
|
|
||||||
|
import org.javamoney.moneta.Money;
|
||||||
|
|
||||||
import javax.money.CurrencyUnit;
|
import javax.money.CurrencyUnit;
|
||||||
|
import javax.money.Monetary;
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,7 +22,7 @@ public class MoneyExercises {
|
||||||
* @return Swiss franc currency unit ({@code CHF})
|
* @return Swiss franc currency unit ({@code CHF})
|
||||||
*/
|
*/
|
||||||
public CurrencyUnit createSwissFranc() {
|
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}
|
* @return monetary amount {@code 19.95 CHF}
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount createSwissFrancAmount() {
|
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}
|
* @return monetary amount {@code 2500 JPY}
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount createJapaneseYenAmount() {
|
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}
|
* @return monetary amount {@code 12.345 TND}
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount createTunisianDinarAmount() {
|
public MonetaryAmount createTunisianDinarAmount() {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return Money.of(12.345, "TND");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package swiss.fihlon.workshop.money.part1;
|
package swiss.fihlon.workshop.money.part1;
|
||||||
|
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.format.MonetaryAmountFormat;
|
||||||
|
import javax.money.format.MonetaryFormats;
|
||||||
|
import javax.money.format.MonetaryParseException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,7 +27,7 @@ public class ParsingExercises {
|
||||||
* @return parsed monetary amount
|
* @return parsed monetary amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount parseSwissGerman(String text) {
|
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
|
* @return parsed monetary amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount parseGerman(String text) {
|
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
|
* @return parsed monetary amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount parseUs(String text) {
|
public MonetaryAmount parseUs(String text) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return parse(text, "en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Parse the given monetary amount text using the specified locale.</p>
|
||||||
|
*
|
||||||
|
* <p>Use the provided language tag to obtain a locale-specific parser.</p>
|
||||||
|
*
|
||||||
|
* @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}
|
* @return {@code true} if parsing succeeds, otherwise {@code false}
|
||||||
*/
|
*/
|
||||||
public boolean parseInvalidInput(String text, Locale locale) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package swiss.fihlon.workshop.money.part1;
|
package swiss.fihlon.workshop.money.part1;
|
||||||
|
|
||||||
|
import javax.money.Monetary;
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.MonetaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 1 workshop exercises for currency-aware rounding with JSR-354.</p>
|
* <p>Part 1 workshop exercises for currency-aware rounding with JSR-354.</p>
|
||||||
|
|
@ -20,7 +22,7 @@ public class RoundingExercises {
|
||||||
* @return rounded amount
|
* @return rounded amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyDefaultRounding(MonetaryAmount 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
|
* @return divided and rounded amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount divideAndRound(MonetaryAmount amount, int divisor) {
|
public MonetaryAmount divideAndRound(MonetaryAmount amount, int divisor) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return amount.divide(divisor)
|
||||||
|
.with(Monetary.getDefaultRounding());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package swiss.fihlon.workshop.money.part2;
|
package swiss.fihlon.workshop.money.part2;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
import javax.money.convert.CurrencyConversion;
|
import javax.money.convert.CurrencyConversion;
|
||||||
|
import javax.money.convert.MonetaryConversions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 2 workshop exercises for currency conversion using JSR-354.</p>
|
* <p>Part 2 workshop exercises for currency conversion using JSR-354.</p>
|
||||||
|
|
@ -21,7 +21,7 @@ public class CurrencyConversionExercises {
|
||||||
* @return currency conversion to EUR
|
* @return currency conversion to EUR
|
||||||
*/
|
*/
|
||||||
public CurrencyConversion getEurConversion() {
|
public CurrencyConversion getEurConversion() {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return MonetaryConversions.getConversion("EUR", "ECB");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,31 +33,7 @@ public class CurrencyConversionExercises {
|
||||||
* @return converted amount in EUR
|
* @return converted amount in EUR
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount convertChfToEur(MonetaryAmount amount) {
|
public MonetaryAmount convertChfToEur(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
CurrencyConversion conversion = getEurConversion();
|
||||||
}
|
return amount.with(conversion);
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Create a currency conversion to EUR using a timestamp-based conversion query.</p>
|
|
||||||
*
|
|
||||||
* <p>Include the given {@link LocalDateTime} in the query.</p>
|
|
||||||
*
|
|
||||||
* @param timestamp timestamp used in the conversion query
|
|
||||||
* @return currency conversion to EUR for the given timestamp
|
|
||||||
*/
|
|
||||||
public CurrencyConversion getEurConversionForTimestamp(LocalDateTime timestamp) {
|
|
||||||
throw new UnsupportedOperationException("TODO");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Convert a CHF amount to EUR using a timestamp-based conversion query.</p>
|
|
||||||
*
|
|
||||||
* <p>Build the conversion query with the given timestamp and return the converted amount.</p>
|
|
||||||
*
|
|
||||||
* @param amount amount in CHF
|
|
||||||
* @param timestamp timestamp used in the conversion query
|
|
||||||
* @return converted amount in EUR
|
|
||||||
*/
|
|
||||||
public MonetaryAmount convertChfToEurAt(MonetaryAmount amount, LocalDateTime timestamp) {
|
|
||||||
throw new UnsupportedOperationException("TODO");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public class MonetaryContextExercises {
|
||||||
* @return monetary context of the amount
|
* @return monetary context of the amount
|
||||||
*/
|
*/
|
||||||
public MonetaryContext getMonetaryContext(MonetaryAmount 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
|
* @return maximum scale from the monetary context
|
||||||
*/
|
*/
|
||||||
public int getMaxScale(MonetaryAmount amount) {
|
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}
|
* @return {@code true} if both contexts are equal, otherwise {@code false}
|
||||||
*/
|
*/
|
||||||
public boolean compareContexts(MonetaryAmount firstAmount, MonetaryAmount secondAmount) {
|
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
|
* @return formatted context description
|
||||||
*/
|
*/
|
||||||
public String describeContext(MonetaryAmount amount) {
|
public String describeContext(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryContext context = amount.getContext();
|
||||||
|
return "precision=" + context.getPrecision() + ", maxScale=" + context.getMaxScale();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package swiss.fihlon.workshop.money.part2;
|
package swiss.fihlon.workshop.money.part2;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
import javax.money.MonetaryOperator;
|
import javax.money.MonetaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 2 workshop exercises for encapsulating domain logic with MonetaryOperator.</p>
|
* <p>Part 2 workshop exercises for encapsulating domain logic with MonetaryOperator.</p>
|
||||||
*
|
*
|
||||||
* <p>The exercises focus on applying reusable monetary operations such as discounts and VAT.</p>
|
* <p>The examples focus on applying reusable monetary operations such as discounts and VAT.</p>
|
||||||
*
|
|
||||||
* <p>The methods are intentionally incomplete and should be implemented by making tests pass.</p>
|
|
||||||
*/
|
*/
|
||||||
public class MonetaryOperatorExercises {
|
public class MonetaryOperatorExercises {
|
||||||
|
|
||||||
|
|
@ -21,7 +20,8 @@ public class MonetaryOperatorExercises {
|
||||||
* @return discounted amount
|
* @return discounted amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyDiscount(MonetaryAmount 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
|
* @return amount after VAT
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyVat(MonetaryAmount amount) {
|
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
|
* @return amount after discount and VAT
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyDiscountThenVat(MonetaryAmount amount) {
|
public MonetaryAmount applyDiscountThenVat(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryOperator discount = createDiscountOperator();
|
||||||
|
MonetaryOperator vat = createVatOperator();
|
||||||
|
return amount.with(discount).with(vat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a reusable 7.7% VAT monetary operator.</p>
|
||||||
|
*
|
||||||
|
* @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
|
* @return reusable discount operator
|
||||||
*/
|
*/
|
||||||
public MonetaryOperator createDiscountOperator() {
|
public MonetaryOperator createDiscountOperator() {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return monetaryAmount -> monetaryAmount.multiply(new BigDecimal("0.90"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ import javax.money.MonetaryQuery;
|
||||||
/**
|
/**
|
||||||
* <p>Part 2 workshop exercises for extracting information from monetary amounts with MonetaryQuery.</p>
|
* <p>Part 2 workshop exercises for extracting information from monetary amounts with MonetaryQuery.</p>
|
||||||
*
|
*
|
||||||
* <p>The exercises focus on encapsulating reusable query logic for monetary values and currencies.</p>
|
* <p>The examples focus on encapsulating reusable query logic for monetary values and currencies.</p>
|
||||||
*
|
|
||||||
* <p>The methods are intentionally incomplete and should be implemented by making tests pass.</p>
|
|
||||||
*/
|
*/
|
||||||
public class MonetaryQueryExercises {
|
public class MonetaryQueryExercises {
|
||||||
|
|
||||||
|
|
@ -22,7 +20,7 @@ public class MonetaryQueryExercises {
|
||||||
* @return currency code
|
* @return currency code
|
||||||
*/
|
*/
|
||||||
public String queryCurrencyCode(MonetaryAmount amount) {
|
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
|
* @return default fraction digits of the currency
|
||||||
*/
|
*/
|
||||||
public int queryDefaultFractionDigits(MonetaryAmount amount) {
|
public int queryDefaultFractionDigits(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryQuery<Integer> query =
|
||||||
|
monetaryAmount -> monetaryAmount.getCurrency().getDefaultFractionDigits();
|
||||||
|
return amount.query(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,7 +46,9 @@ public class MonetaryQueryExercises {
|
||||||
* @return numeric value as BigDecimal
|
* @return numeric value as BigDecimal
|
||||||
*/
|
*/
|
||||||
public BigDecimal queryNumberAsBigDecimal(MonetaryAmount amount) {
|
public BigDecimal queryNumberAsBigDecimal(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryQuery<BigDecimal> 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
|
* @return reusable query returning a currency code
|
||||||
*/
|
*/
|
||||||
public MonetaryQuery<String> createCurrencyCodeQuery() {
|
public MonetaryQuery<String> createCurrencyCodeQuery() {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return monetaryAmount -> monetaryAmount.getCurrency().getCurrencyCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
package swiss.fihlon.workshop.money.part2;
|
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.MonetaryAmount;
|
||||||
import javax.money.MonetaryOperator;
|
import javax.money.MonetaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 2 workshop exercises for applying different rounding strategies with JSR-354.</p>
|
* <p>Part 2 workshop examples for applying different rounding strategies with JSR-354.</p>
|
||||||
*
|
*
|
||||||
* <p>The exercises focus on treating rounding as an explicit domain decision that depends on the use case.</p>
|
* <p>The examples focus on treating rounding as an explicit domain decision that depends on the use case.</p>
|
||||||
*
|
|
||||||
* <p>The methods are intentionally incomplete and should be implemented by making tests pass.</p>
|
|
||||||
*/
|
*/
|
||||||
public class RoundingStrategyExercises {
|
public class RoundingStrategyExercises {
|
||||||
|
|
||||||
|
|
@ -21,19 +22,8 @@ public class RoundingStrategyExercises {
|
||||||
* @return amount rounded with default currency rounding
|
* @return amount rounded with default currency rounding
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyDefaultRounding(MonetaryAmount amount) {
|
public MonetaryAmount applyDefaultRounding(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryOperator rounding = Monetary.getDefaultRounding();
|
||||||
}
|
return amount.with(rounding);
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Apply explicit default rounding to a VAT amount.</p>
|
|
||||||
*
|
|
||||||
* <p>Use rounding as a separate, explicit step.</p>
|
|
||||||
*
|
|
||||||
* @param taxAmount tax amount to round
|
|
||||||
* @return rounded tax amount
|
|
||||||
*/
|
|
||||||
public MonetaryAmount roundVatAmount(MonetaryAmount taxAmount) {
|
|
||||||
throw new UnsupportedOperationException("TODO");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,7 +35,8 @@ public class RoundingStrategyExercises {
|
||||||
* @return CHF amount rounded to the nearest 0.05
|
* @return CHF amount rounded to the nearest 0.05
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applySwissCashRounding(MonetaryAmount amount) {
|
public MonetaryAmount applySwissCashRounding(MonetaryAmount amount) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
MonetaryOperator swissCashRounding = createSwissCashRounding();
|
||||||
|
return amount.with(swissCashRounding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,6 +47,19 @@ public class RoundingStrategyExercises {
|
||||||
* @return reusable Swiss cash rounding operator
|
* @return reusable Swiss cash rounding operator
|
||||||
*/
|
*/
|
||||||
public MonetaryOperator createSwissCashRounding() {
|
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();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
package swiss.fihlon.workshop.money.part3;
|
package swiss.fihlon.workshop.money.part3;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Locale;
|
||||||
import javax.money.CurrencyUnit;
|
import javax.money.CurrencyUnit;
|
||||||
|
import javax.money.Monetary;
|
||||||
import javax.money.MonetaryAmount;
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.MonetaryOperator;
|
||||||
|
import javax.money.convert.MonetaryConversions;
|
||||||
|
import javax.money.format.MonetaryAmountFormat;
|
||||||
|
import javax.money.format.MonetaryFormats;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Part 3 workshop exercises for integrating monetary calculations into a small order workflow.</p>
|
* <p>Part 3 workshop examples for integrating monetary calculations into a small order workflow.</p>
|
||||||
*
|
*
|
||||||
* <p>The exercises focus on combining subtotal calculation, discount, VAT, rounding, and formatting.</p>
|
* <p>The examples focus on combining subtotal calculation, discount, VAT, rounding, and formatting.</p>
|
||||||
*
|
|
||||||
* <p>The methods are intentionally incomplete and should be implemented by making tests pass.</p>
|
|
||||||
*/
|
*/
|
||||||
public class OrderExercises {
|
public class OrderExercises {
|
||||||
|
|
||||||
|
|
@ -22,7 +27,7 @@ public class OrderExercises {
|
||||||
* @return subtotal amount
|
* @return subtotal amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount calculateSubtotal(MonetaryAmount unitPrice, int quantity) {
|
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
|
* @return discounted amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyDiscount(MonetaryAmount subtotal) {
|
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
|
* @return VAT-inclusive amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount applyVat(MonetaryAmount discountedAmount) {
|
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
|
* @return final total amount
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount calculateTotal(MonetaryAmount unitPrice, int quantity) {
|
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
|
* @return formatted total string
|
||||||
*/
|
*/
|
||||||
public String formatTotal(MonetaryAmount total) {
|
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
|
* @return amount converted to the target currency
|
||||||
*/
|
*/
|
||||||
public MonetaryAmount convertToCurrency(MonetaryAmount amount, CurrencyUnit targetCurrency) {
|
public MonetaryAmount convertToCurrency(MonetaryAmount amount, CurrencyUnit targetCurrency) {
|
||||||
throw new UnsupportedOperationException("TODO");
|
return amount.with(MonetaryConversions.getConversion(targetCurrency, "ECB"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Bonus exercise 1 for implementing a local CSV-based exchange rate provider with JSR-354.</p>
|
||||||
|
*
|
||||||
|
* <p>The provider is intended for workshop usage and should read rates from the classpath resource
|
||||||
|
* {@code exchange-rates.csv}.</p>
|
||||||
|
*/
|
||||||
|
public class CsvExchangeRateProvider implements ExchangeRateProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Name of the classpath CSV resource containing exchange rates.</p>
|
||||||
|
*/
|
||||||
|
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<CurrencyPair, BigDecimal> ratesByPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a new provider instance.</p>
|
||||||
|
*
|
||||||
|
* <p>The provider should load exchange rates from {@link #RESOURCE_NAME}.</p>
|
||||||
|
*/
|
||||||
|
public CsvExchangeRateProvider() {
|
||||||
|
this.ratesByPair = loadRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Return the provider context for this exchange rate provider.</p>
|
||||||
|
*
|
||||||
|
* @return provider context including a useful provider name
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProviderContext getContext() {
|
||||||
|
return CONTEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Look up an exchange rate for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery conversion query containing base and term currency
|
||||||
|
* @return matching exchange rate, or {@code null} if no matching rate exists
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a currency conversion for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* <p>The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery conversion query containing conversion details
|
||||||
|
* @return currency conversion for the query
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
|
||||||
|
if (getExchangeRate(conversionQuery) == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ConversionContext conversionContext = ConversionContextBuilder.create(getContext(), RateType.DEFERRED).build();
|
||||||
|
return new LazyBoundCurrencyConversion(conversionQuery, this, conversionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Load exchange rates from the classpath CSV resource into a lookup map.</p>
|
||||||
|
*
|
||||||
|
* <p>The CSV is expected to contain a header and records in the format
|
||||||
|
* {@code base,term,rate}.</p>
|
||||||
|
*
|
||||||
|
* @return map keyed by currency pair containing the configured exchange factor
|
||||||
|
*/
|
||||||
|
private static Map<CurrencyPair, BigDecimal> 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<CurrencyPair, BigDecimal> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Simple key for identifying an exchange rate by base and term currency code.</p>
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
/**
|
||||||
|
* <p>Create a currency-pair key from a conversion query.</p>
|
||||||
|
*
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Bonus exercise 2 for implementing a local CSV-based historical exchange rate provider with JSR-354.</p>
|
||||||
|
*
|
||||||
|
* <p>The provider is intended for workshop usage and should read rates from the classpath resource
|
||||||
|
* {@code exchange-rates-hist.csv}.</p>
|
||||||
|
*
|
||||||
|
* <p>The lookup should use base currency, term currency, and a date passed in the query context.</p>
|
||||||
|
*/
|
||||||
|
public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Name of the classpath CSV resource containing historical exchange rates.</p>
|
||||||
|
*/
|
||||||
|
public static final String RESOURCE_NAME = "exchange-rates-hist.csv";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Query key type for historical lookups.</p>
|
||||||
|
*/
|
||||||
|
public static final Class<LocalDate> DATE_QUERY_KEY = LocalDate.class;
|
||||||
|
private static final ProviderContext CONTEXT =
|
||||||
|
ProviderContextBuilder.of("csv-historical-exchange-rate-provider", RateType.HISTORIC).build();
|
||||||
|
|
||||||
|
private final Map<CurrencyPair, NavigableMap<LocalDate, BigDecimal>> ratesByPairAndDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a new provider instance.</p>
|
||||||
|
*
|
||||||
|
* <p>The provider should load exchange rates from {@link #RESOURCE_NAME}.</p>
|
||||||
|
*/
|
||||||
|
public CsvHistoricalExchangeRateProvider() {
|
||||||
|
this.ratesByPairAndDate = loadRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Return the provider context for this exchange rate provider.</p>
|
||||||
|
*
|
||||||
|
* @return provider context including a useful provider name
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProviderContext getContext() {
|
||||||
|
return CONTEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Look up a historical exchange rate for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* <p>The query is expected to contain a {@link LocalDate} under {@link #DATE_QUERY_KEY}.</p>
|
||||||
|
*
|
||||||
|
* <p>If no exact match exists for the requested date, the provider should use the last available
|
||||||
|
* rate before that date.</p>
|
||||||
|
*
|
||||||
|
* <p>The method should return {@code null} only when no suitable earlier rate exists.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery conversion query containing base currency, term currency, and date
|
||||||
|
* @return matching historical exchange rate with fallback to the latest earlier date, or {@code null}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
|
||||||
|
LocalDate requestedDate = conversionQuery.get(DATE_QUERY_KEY);
|
||||||
|
if (requestedDate == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrencyPair currencyPair = CurrencyPair.from(conversionQuery);
|
||||||
|
NavigableMap<LocalDate, BigDecimal> ratesByDate = ratesByPairAndDate.get(currencyPair);
|
||||||
|
if (ratesByDate == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.Entry<LocalDate, BigDecimal> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a currency conversion for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* <p>The conversion should use the same lookup rules as {@link #getExchangeRate(ConversionQuery)}:
|
||||||
|
* exact date first, otherwise the last available earlier rate.</p>
|
||||||
|
*
|
||||||
|
* <p>The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery conversion query containing conversion details
|
||||||
|
* @return currency conversion for the query
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
|
||||||
|
if (getExchangeRate(conversionQuery) == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ConversionContext conversionContext = ConversionContextBuilder.create(getContext(), RateType.HISTORIC).build();
|
||||||
|
return new LazyBoundCurrencyConversion(conversionQuery, this, conversionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Load historical exchange rates from the classpath CSV resource into a date-indexed lookup map.</p>
|
||||||
|
*
|
||||||
|
* <p>The CSV is expected to contain a header and records in the format
|
||||||
|
* {@code date,base,term,rate}.</p>
|
||||||
|
*
|
||||||
|
* @return map keyed by currency pair with a navigable date-to-rate mapping
|
||||||
|
*/
|
||||||
|
private static Map<CurrencyPair, NavigableMap<LocalDate, BigDecimal>> 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<CurrencyPair, NavigableMap<LocalDate, BigDecimal>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Simple key for identifying a historical exchange-rate series by base and term currency code.</p>
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
/**
|
||||||
|
* <p>Create a currency-pair key from a conversion query.</p>
|
||||||
|
*
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package swiss.fihlon.workshop.money.part4;
|
||||||
|
|
||||||
|
import javax.money.CurrencyUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Bonus exercise 3 helper for creating the custom DukePoints currency.</p>
|
||||||
|
*
|
||||||
|
* <p>The goal is to model {@code DKP} as a custom {@link CurrencyUnit} that can be used with monetary amounts.</p>
|
||||||
|
*/
|
||||||
|
public class DukePointsCurrencyFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Currency code for DukePoints.</p>
|
||||||
|
*/
|
||||||
|
public static final String DKP = "DKP";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create the custom DukePoints currency unit.</p>
|
||||||
|
*
|
||||||
|
* @return custom currency unit with currency code {@code DKP}
|
||||||
|
*/
|
||||||
|
public CurrencyUnit createDkpCurrency() {
|
||||||
|
return new DukePointsCurrencyUnit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package swiss.fihlon.workshop.money.part4;
|
||||||
|
|
||||||
|
import javax.money.CurrencyContext;
|
||||||
|
import javax.money.CurrencyContextBuilder;
|
||||||
|
import javax.money.CurrencyUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>This is a minimal custom CurrencyUnit implementation used for workshop purposes.</p>
|
||||||
|
*
|
||||||
|
* <p>The implementation is provided to focus on currency integration and conversion logic.</p>
|
||||||
|
*/
|
||||||
|
public final class DukePointsCurrencyUnit implements CurrencyUnit {
|
||||||
|
|
||||||
|
private static final String CURRENCY_CODE = DukePointsCurrencyFactory.DKP;
|
||||||
|
private static final int DEFAULT_FRACTION_DIGITS = 2;
|
||||||
|
private static final int NUMERIC_CODE = 999;
|
||||||
|
private static final CurrencyContext CONTEXT =
|
||||||
|
CurrencyContextBuilder.of(DukePointsCurrencyUnit.class.getSimpleName()).build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCurrencyCode() {
|
||||||
|
return CURRENCY_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNumericCode() {
|
||||||
|
return NUMERIC_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDefaultFractionDigits() {
|
||||||
|
return DEFAULT_FRACTION_DIGITS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CurrencyContext getContext() {
|
||||||
|
return CONTEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(CurrencyUnit otherCurrency) {
|
||||||
|
return getCurrencyCode().compareTo(otherCurrency.getCurrencyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getCurrencyCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof CurrencyUnit other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getCurrencyCode().equals(other.getCurrencyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getCurrencyCode().hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Bonus exercise 3 for implementing local exchange rates between CHF and DukePoints.</p>
|
||||||
|
*
|
||||||
|
* <p>The provider should support deterministic conversion for {@code CHF -> DKP} and {@code DKP -> CHF}.</p>
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a new exchange rate provider for DukePoints.</p>
|
||||||
|
*/
|
||||||
|
public DukePointsExchangeRateProvider() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Return provider context metadata for this provider.</p>
|
||||||
|
*
|
||||||
|
* @return provider context with a useful provider name
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProviderContext getContext() {
|
||||||
|
return CONTEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Return the exchange rate for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* <p>Supported pairs are {@code CHF -> DKP} and {@code DKP -> CHF}.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery query containing base and term currency
|
||||||
|
* @return matching exchange rate, or {@code null} for unsupported currency pairs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Create a currency conversion for the given conversion query.</p>
|
||||||
|
*
|
||||||
|
* <p>The returned conversion should allow direct conversion using {@code amount.with(conversion)}.</p>
|
||||||
|
*
|
||||||
|
* @param conversionQuery query containing base and term currency
|
||||||
|
* @return currency conversion based on the local DukePoints rules
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
|
||||||
|
if (getExchangeRate(conversionQuery) == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ConversionContext conversionContext = ConversionContextBuilder.create(getContext(), RateType.HISTORIC).build();
|
||||||
|
return new LazyBoundCurrencyConversion(conversionQuery, this, conversionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Resolve the conversion factor for a supported DukePoints currency pair.</p>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/main/resources/exchange-rates-hist.csv
Normal file
19
src/main/resources/exchange-rates-hist.csv
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
date,base,term,rate
|
||||||
|
2026-04-15,CHF,EUR,0.92
|
||||||
|
2026-04-15,CHF,USD,1.09
|
||||||
|
2026-04-15,EUR,CHF,1.09
|
||||||
|
2026-04-15,EUR,USD,1.18
|
||||||
|
2026-04-15,USD,CHF,0.92
|
||||||
|
2026-04-15,USD,EUR,0.85
|
||||||
|
2026-04-16,CHF,EUR,0.93
|
||||||
|
2026-04-16,CHF,USD,1.10
|
||||||
|
2026-04-16,EUR,CHF,1.08
|
||||||
|
2026-04-16,EUR,USD,1.19
|
||||||
|
2026-04-16,USD,CHF,0.91
|
||||||
|
2026-04-16,USD,EUR,0.84
|
||||||
|
2026-04-17,CHF,EUR,0.94
|
||||||
|
2026-04-17,CHF,USD,1.11
|
||||||
|
2026-04-17,EUR,CHF,1.06
|
||||||
|
2026-04-17,EUR,USD,1.20
|
||||||
|
2026-04-17,USD,CHF,0.90
|
||||||
|
2026-04-17,USD,EUR,0.83
|
||||||
|
9
src/main/resources/exchange-rates.csv
Normal file
9
src/main/resources/exchange-rates.csv
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
base,term,rate
|
||||||
|
CHF,EUR,0.93
|
||||||
|
CHF,USD,1.10
|
||||||
|
CHF,GBP,0.82
|
||||||
|
EUR,CHF,1.08
|
||||||
|
USD,CHF,0.91
|
||||||
|
GBP,CHF,1.22
|
||||||
|
EUR,USD,1.18
|
||||||
|
USD,EUR,0.85
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package swiss.fihlon.workshop.money.part4;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import javax.money.Monetary;
|
||||||
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.convert.ConversionQuery;
|
||||||
|
import javax.money.convert.ConversionQueryBuilder;
|
||||||
|
import javax.money.convert.CurrencyConversion;
|
||||||
|
import javax.money.convert.ExchangeRate;
|
||||||
|
import org.javamoney.moneta.Money;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CsvExchangeRateProviderTest {
|
||||||
|
|
||||||
|
private final CsvExchangeRateProvider provider = new CsvExchangeRateProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnProviderContextWithUsefulName() {
|
||||||
|
String providerName = provider.getContext().getProviderName();
|
||||||
|
|
||||||
|
assertThat(providerName).isNotBlank();
|
||||||
|
assertThat(providerName).containsIgnoringCase("csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExchangeRateForChfToEur() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.93");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExchangeRateForUsdToEur() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("USD"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.85");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNullForUnknownCurrencyPair() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("GBP"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConvertAmountUsingExchangeRate() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.build();
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class));
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("93.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateCurrencyConversion() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CurrencyConversion conversion = provider.getCurrencyConversion(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.with(conversion);
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("93.00");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package swiss.fihlon.workshop.money.part4;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import javax.money.Monetary;
|
||||||
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.convert.ConversionQuery;
|
||||||
|
import javax.money.convert.ConversionQueryBuilder;
|
||||||
|
import javax.money.convert.CurrencyConversion;
|
||||||
|
import javax.money.convert.ExchangeRate;
|
||||||
|
import org.javamoney.moneta.Money;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CsvHistoricalExchangeRateProviderTest {
|
||||||
|
|
||||||
|
private final CsvHistoricalExchangeRateProvider provider = new CsvHistoricalExchangeRateProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExchangeRateForChfToEurOn20260415() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 15))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.92");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExchangeRateForChfToEurOn20260417() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 17))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.94");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExchangeRateForUsdToChfOn20260416() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("USD"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 16))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.91");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnLastAvailableRateWhenDateIsMissing() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 18))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNotNull();
|
||||||
|
assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.94");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConvertAmountUsingHistoricalRate() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("USD"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 17))
|
||||||
|
.build();
|
||||||
|
ExchangeRate exchangeRate = provider.getExchangeRate(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class));
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("111.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateCurrencyConversionForHistoricalRate() {
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(Monetary.getCurrency("USD"))
|
||||||
|
.set(LocalDate.class, LocalDate.of(2026, 4, 17))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CurrencyConversion conversion = provider.getCurrencyConversion(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.with(conversion);
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("USD");
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("111.00");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package swiss.fihlon.workshop.money.part4;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import javax.money.CurrencyUnit;
|
||||||
|
import javax.money.Monetary;
|
||||||
|
import javax.money.MonetaryAmount;
|
||||||
|
import javax.money.convert.ConversionQuery;
|
||||||
|
import javax.money.convert.ConversionQueryBuilder;
|
||||||
|
import javax.money.convert.CurrencyConversion;
|
||||||
|
import javax.money.convert.ExchangeRate;
|
||||||
|
import org.javamoney.moneta.Money;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DukePointsExchangeRateProviderTest {
|
||||||
|
|
||||||
|
private final DukePointsCurrencyFactory currencyFactory = new DukePointsCurrencyFactory();
|
||||||
|
private final DukePointsExchangeRateProvider exchangeRateProvider = new DukePointsExchangeRateProvider();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateDkpCurrency() {
|
||||||
|
CurrencyUnit dkp = currencyFactory.createDkpCurrency();
|
||||||
|
|
||||||
|
assertThat(dkp).isNotNull();
|
||||||
|
assertThat(dkp.getCurrencyCode()).isEqualTo("DKP");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConvertChfToDkp() {
|
||||||
|
CurrencyUnit dkp = currencyFactory.createDkpCurrency();
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(dkp)
|
||||||
|
.build();
|
||||||
|
ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class));
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConvertDkpToChf() {
|
||||||
|
CurrencyUnit dkp = currencyFactory.createDkpCurrency();
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(dkp)
|
||||||
|
.setTermCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.build();
|
||||||
|
ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query);
|
||||||
|
MonetaryAmount dkpAmount = Money.of(5, dkp);
|
||||||
|
|
||||||
|
MonetaryAmount convertedAmount = dkpAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class));
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("50.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNullForUnsupportedCurrencyPair() {
|
||||||
|
CurrencyUnit dkp = currencyFactory.createDkpCurrency();
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("EUR"))
|
||||||
|
.setTermCurrency(dkp)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query);
|
||||||
|
|
||||||
|
assertThat(exchangeRate).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConvertUsingCurrencyConversion() {
|
||||||
|
CurrencyUnit dkp = currencyFactory.createDkpCurrency();
|
||||||
|
ConversionQuery query = ConversionQueryBuilder.of()
|
||||||
|
.setBaseCurrency(Monetary.getCurrency("CHF"))
|
||||||
|
.setTermCurrency(dkp)
|
||||||
|
.build();
|
||||||
|
CurrencyConversion conversion = exchangeRateProvider.getCurrencyConversion(query);
|
||||||
|
MonetaryAmount chfAmount = Money.of(100, "CHF");
|
||||||
|
|
||||||
|
MonetaryAmount convertedAmount = chfAmount.with(conversion);
|
||||||
|
|
||||||
|
assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("DKP");
|
||||||
|
assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.00");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue