Functional Programming in Java, Second Edition: All the code changes for Chapter 5, 85-93, in one file

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);
    }

}