public void canAddEntryWithNoCommonCurrencyIfSuppliedBySubsequentEntries() { FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(CHF, AUD, 1.6) // Cannot be added as nothing to tie it to USD or GBP .addRate(EUR, CHF, 1.2) // Again cannot be added .addRate(EUR, USD, 1.4) // Now everything can be tied together .build(); }
/** * Obtains an instance containing a single FX rate. * <p> * This is most likely to be used in testing. * <p> * An invocation of the method with {@code FxMatrix.of(GBP, USD, 1.6)} * indicates that 1 pound sterling is worth 1.6 US dollars. * The matrix can also be queried for the reverse rate, from USD to GBP. * * @param ccy1 the first currency of the pair * @param ccy2 the second currency of the pair * @param rate the FX rate between the first currency and the second currency. * The rate indicates the value of one unit of the first currency in terms * of the second currency. * @return a matrix containing the single FX rate */ public static FxMatrix of(Currency ccy1, Currency ccy2, double rate) { return new FxMatrixBuilder().addRate(ccy1, ccy2, rate).build(); }
FxMatrixBuilder merge(FxMatrixBuilder other) { // Find the common currencies Optional<Currency> common = currencies.keySet() .stream() .filter(other.currencies::containsKey) .findFirst(); Currency commonCurrency = common.orElseThrow(() -> new IllegalArgumentException( "There are no currencies in common between " + currencies.keySet() + " and " + other.currencies.keySet())); // Add in all currencies that we don't already have MapStream.of(other.currencies) .filterKeys(ccy -> !ccy.equals(commonCurrency) && !currencies.containsKey(ccy)) .forEach((ccy, idx) -> addCurrencyPair(commonCurrency, ccy, other.getRate(commonCurrency, ccy))); return this; }
private void addCurrencyPair(Currency ccy1, Currency ccy2, double rate) { // Only resize if there's a danger we can't fit a new currency in if (rates.length < currencies.size() + 1) { ensureCapacity(Stream.of(ccy1, ccy2)); } if (!currencies.containsKey(ccy1) && !currencies.containsKey(ccy2)) { // Neither currency present - add to disjoint set disjointRates.put(CurrencyPair.of(ccy1, ccy2), rate); } else if (currencies.containsKey(ccy1) && currencies.containsKey(ccy2)) { // We already have a rate for this currency pair updateRate(ccy1, ccy2, rate); } else { // We have exactly one of the currencies already addNewRate(ccy1, ccy2, rate); // With a new rate added we may be able to handle the disjoint retryDisjoints(); } }
public void addMultipleRatesContainingEntryWithNoCommonCurrency() { LinkedHashMap<CurrencyPair, Double> rates = new LinkedHashMap<>(); rates.put(CurrencyPair.of(GBP, USD), 1.6); rates.put(CurrencyPair.of(EUR, USD), 1.4); rates.put(CurrencyPair.of(JPY, CAD), 0.01); // Neither currency linked to one of the others assertThrows( () -> FxMatrix.builder().addRates(rates).build(), IllegalStateException.class); }
/** * Creates a {@code Collector} that allows a {@code Map.Entry} of currency pair to rate * to be streamed and collected into a new {@code FxMatrix}. * * @return a collector for creating an {@code FxMatrix} from a stream */ public static Collector<? super Map.Entry<CurrencyPair, Double>, FxMatrixBuilder, FxMatrix> entriesToFxMatrix() { return collector((builder, entry) -> builder.addRate(entry.getKey(), entry.getValue())); }
/** * Merges the entries from the other matrix into this one. * <p> * The other matrix should have at least one currency in common with this one. * The additional currencies from the other matrix are added one by one and * the exchange rate data created is coherent with some data in this matrix. * <p> * Note that if the other matrix has more than one currency in common with * this one, and the rates for pairs of those currencies are different to * the equivalents in this matrix, then the rates between the additional * currencies is this matrix will differ from those in the original. * * @param other the matrix to be merged into this one * @return a new matrix containing the rates from this matrix * plus any rates for additional currencies from the other matrix */ public FxMatrix merge(FxMatrix other) { return toBuilder().merge(other.toBuilder()).build(); }
/** * Adds a collection of new rates for currency pairs to the builder. * Pairs that are already in the builder are treated as updates to the * existing rates -> !e.getKey().equals(commonCurrency) && !currencies.containsKey(e.getKey()) * * @param rates the currency pairs and rates to be added * @return the builder updated with the new rates */ public FxMatrixBuilder addRates(Map<CurrencyPair, Double> rates) { ArgChecker.notNull(rates, "rates"); if (!rates.isEmpty()) { ensureCapacity(rates.keySet().stream() .flatMap(cp -> Stream.of(cp.getBase(), cp.getCounter()))); MapStream.of(rates).forEach((pair, rate) -> addRate(pair, rate)); } return this; }
public void emptyMatrixCannotDoConversion() { FxMatrix matrix = FxMatrix.builder().build(); assertThat(matrix.getCurrencies()).isEmpty(); assertThrowsIllegalArg(() -> matrix.fxRate(USD, EUR)); }
addInitialCurrencyPair(ccy1, ccy2, rate); } else { addCurrencyPair(ccy1, ccy2, rate);
private void retryDisjoints() { ensureCapacity(disjointRates.keySet() .stream() .flatMap(cp -> Stream.of(cp.getBase(), cp.getCounter()))); while (true) { int initialSize = disjointRates.size(); ImmutableMap<CurrencyPair, Double> addable = MapStream.of(disjointRates) .filterKeys(pair -> currencies.containsKey(pair.getBase()) || currencies.containsKey(pair.getCounter())) .toMap(); MapStream.of(addable).forEach((pair, rate) -> addNewRate(pair.getBase(), pair.getCounter(), rate)); addable.keySet().stream().forEach(disjointRates::remove); if (disjointRates.size() == initialSize) { // No effect so break out break; } } }
/** * Creates a builder that can be used to build instances of {@code FxMatrix}. * * @return a new builder */ public static FxMatrixBuilder builder() { return new FxMatrixBuilder(); }
private void ensureCapacity(Stream<Currency> potentialCurrencies) { // If adding the currencies would mean we have more // currencies than matrix size, create an expanded array int requiredOrder = (int) Stream.concat(currencies.keySet().stream(), potentialCurrencies) .distinct() .count(); ensureCapacity(requiredOrder); }
/** * Build a new {@code FxMatrix} from the data in the builder. * * @return a new {@code FxMatrix} * @throws IllegalStateException if an attempt was made to add currencies * which have no currency in common with other rates */ public FxMatrix build() { if (!disjointRates.isEmpty()) { throw new IllegalStateException("Received rates with no currencies in common with other: " + disjointRates); } // Trim array down to the correct size - we have to copy the array // anyway to ensure immutability, so we may as well remove any // unused rows return new FxMatrix(ImmutableMap.copyOf(currencies), DoubleMatrix.ofUnsafe(copyArray(rates, currencies.size()))); }
public void addSimpleMultipleRates() { // Use linked to force the order of evaluation // want to see that builder recovers when // encountering a currency pair for 2 unknown // currencies but which will appear later LinkedHashMap<CurrencyPair, Double> rates = new LinkedHashMap<>(); rates.put(CurrencyPair.of(GBP, USD), 1.6); rates.put(CurrencyPair.of(EUR, USD), 1.4); FxMatrix matrix = FxMatrix.builder() .addRates(rates) .build(); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(1 / 1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4); assertThat(matrix.fxRate(USD, EUR)).isEqualTo(1 / 1.4); assertThat(matrix.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.6, TOL); assertThat(matrix.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); }
/** * Creates a {@code Collector} that allows a collection of pairs each containing * a currency pair and a rate to be streamed and collected into a new {@code FxMatrix}. * * @return a collector for creating an {@code FxMatrix} from a stream */ public static Collector<? super Pair<CurrencyPair, Double>, FxMatrixBuilder, FxMatrix> pairsToFxMatrix() { return collector((builder, pair) -> builder.addRate(pair.getFirst(), pair.getSecond())); }
/** * Creates a new builder using the data from this matrix to * create a set of initial entries. * * @return a new builder containing the data from this matrix */ public FxMatrixBuilder toBuilder() { return new FxMatrixBuilder(currencies, rates.toArray()); }
public void mergeIgnoresDuplicateCurrencies() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .build(); FxMatrix matrix2 = FxMatrix.builder() .addRate(GBP, USD, 1.7) .addRate(EUR, USD, 1.5) .addRate(EUR, CHF, 1.3) .build(); FxMatrix result = matrix1.merge(matrix2); assertThat(result).isEqualTo(matrix1); }