Again, some code suggestions for “Chapter 5”, similar to “Chapter 3”, in the form JUnit tests.
This includes the rewritten FinanceData
using URI
instead of URL
package chapter5;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
// ---
// Originally "designing/fpij/Asset.java" on p.84 but written as a record.
// In order to cut down on syntactic noise in callers, we also add two
// methods isStock(), isBond().
// ---
record Asset(AssetType type, int value) {
public enum AssetType {BOND, STOCK}
public Asset {
Objects.requireNonNull(type);
}
public boolean isStock() {
return type() == AssetType.STOCK;
}
public boolean isBond() {
return type() == AssetType.BOND;
}
}
// ---
// Originally "designing/fpij/FinanceData.java" on page 91
// But: "java.net.URL" has been "broken" since inception, it is actually deprecated in Java 20.
// Suggesting using URI instead, as coded here.
// ---
class FinanceData {
public static BigDecimal getPrice(final String ticker) {
HttpResponse<String> response;
try {
final String scheme = "https";
final String authority = "eodhistoricaldata.com";
final String path = String.format("/api/eod/%s.US", ticker);
// two ways of writing this:
/*
final String query =
List.of("fmt=json", "filter=last_close", "api_token=OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX")
.stream()
.collect(Collectors.joining("&"));
*/
final String query =
String.join("&", "fmt=json", "filter=last_close", "api_token=OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX");
final String fragment = null;
final URI uri = new URI(scheme, authority, path, query, fragment);
System.out.println("Connecting to URI: " + uri);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(uri).build();
response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
System.out.println("Received status code: " + response.statusCode());
if (response.statusCode() == 200) {
System.out.println("Received body: " + response.body());
// parsing BigDecimal will throw on bad syntax
return new BigDecimal(response.body());
} else {
throw new IllegalStateException("Status code was: " + response.statusCode());
}
}
}
// ---
// "designing/fpij/CalculateNAV.java" on p.89.
// Should we make it a "record"? It feels more like class.
// Made the "priceFinder" final.
// ---
class CalculateNAV {
private final Function<String, BigDecimal> priceFinder;
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
priceFinder = aPriceFinder;
}
public BigDecimal computeStockWorth(final String ticker, final int shares) {
return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}
//... other methods that use the priceFinder ...
}
public class DesigningWithLambdaExpressions {
// "designing/fpij/AssetUtil.java" on p. 84
// Example portfolio.
final static List<Asset> assets = List.of(
new Asset(Asset.AssetType.BOND, 1000),
new Asset(Asset.AssetType.BOND, 2000),
new Asset(Asset.AssetType.STOCK, 3000),
new Asset(Asset.AssetType.STOCK, 4000)
);
// "designing/fpij/AssetUtil.java" on p. 84
private static int totalAssetValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(Asset::value)
.sum();
}
// "designing/fpij/AssetUtil.java" on p. 85
// The comparison "== BOND" has been replaced by "isBond()"
private static int totalBondValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(asset -> asset.isBond() ? asset.value() : 0)
.sum();
}
// "designing/fpij/AssetUtil.java" on p. 85
// The comparison "== STOCK" has been replaced by "isStock()"
private static int totalStockValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(asset -> asset.isStock() ? asset.value() : 0)
.sum();
}
// "designing/fpij/AssetUtilRefactored.java" on p. 86
// But renamed from "totalAssetValues" to "totalSelectableValues"
private static int totalSelectableValues(final List<Asset> assets, final Predicate<Asset> assetSelector) {
return assets.stream()
.filter(assetSelector)
.mapToInt(Asset::value)
.sum();
}
// "designing/fpij/AssetUtil.java"
// part of this shown on p.84, p.85
@Test
void totalsOfAssetsBondsStocks() {
System.out.println("Total of all assets: " + totalAssetValues(assets));
System.out.println("Total of all bonds: " + totalBondValues(assets));
System.out.println("Total of all stocks: " + totalStockValues(assets));
}
// "designing/fpij/AssetUtilRefactored.java"
// part of this is shown on p.87
@Test
void totalsUsingUsingSelectingLambda() {
System.out.println("Total of all assets: " + totalSelectableValues(assets, asset -> true));
System.out.println("Total of all bonds: " + totalSelectableValues(assets, asset -> asset.isBond()));
System.out.println("Total of all stocks: " + totalSelectableValues(assets, asset -> asset.isStock()));
}
// The above can be simplified to
@Test
void totalsUsingSelectingLambdaAndMethodReferences() {
System.out.println("Total of all assets: " + totalSelectableValues(assets, asset -> true));
System.out.println("Total of all bonds: " + totalSelectableValues(assets, Asset::isBond));
System.out.println("Total of all stocks: " + totalSelectableValues(assets, Asset::isStock));
}
// Actually running a test, not just displaying
@Test
void actuallyTestTotalsUsingSelectingLambda() {
int totalAssets = totalSelectableValues(assets, asset -> true);
int totalBonds = totalSelectableValues(assets, asset -> asset.isBond());
int totalStocks = totalSelectableValues(assets, asset -> asset.isStock());
assertEquals(10000, totalAssets);
assertEquals(3000, totalBonds);
assertEquals(7000, totalStocks);
}
// Actually running a test, using JUnit5 lambda support
@Test
void actuallyTestTotalAssetsBondsStocksUsingSelectingLambda() {
int totalAssets = totalSelectableValues(assets, asset -> true);
int totalBonds = totalSelectableValues(assets, asset -> asset.isBond());
int totalStocks = totalSelectableValues(assets, asset -> asset.isStock());
assertAll("totals",
() -> assertEquals(10000, totalAssets),
() -> assertEquals(3000, totalBonds),
() -> assertEquals(7000, totalStocks)
);
}
// "designing/fpij/CalculateNAVTest.java" on p.90
// ORIGINAL CODE HAD A BUG:
// assertEquals(0, calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected), 0.001);
// but compareTo() returns 0,1,+1, not something in the range [0,0.001]
@Test
void computeStockWorthPricefinderReturningConstant() {
final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
final BigDecimal expected = new BigDecimal("6010.00");
final BigDecimal actual = calculateNAV.computeStockWorth("GOOG", 1000);
final BigDecimal delta = actual.subtract(expected);
assertEquals(delta.doubleValue(), 0, 0.001);
}
// "designing/fpij/CalculateNAV.java" on p. 91
// But constants have been moved to their own lines and printing is separate from computing.
// The call to "String.format" is redundant: "System.out.println(String.format())"
// can be replaced by "System.out.printf()" , since Java 1.5 (I didn't even know)
// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/PrintStream.html#printf(java.lang.String,java.lang.Object...)
@Test
void computeStockWorthWithPriceObtainedFromInternet() {
final CalculateNAV calculateNav = new CalculateNAV(FinanceData::getPrice);
final int n = 100;
final BigDecimal worth = calculateNav.computeStockWorth("AAPL", n);
System.out.printf("%d shares of Apple worth: $%.2f%n", n, worth);
}
}