Now you want to show the number of dollars of fuel delivered along with liters deliv- ered, and you’ll factor the logic for this out into a new class called Fill. Figure 4.9 shows the conceptual view of Fill. Here are the inputs:
■ sClearAccumulator—The mechanism for clearing the accumulator
■ sFuelPulses—Pulses from the flow meter
■ calibration—The conversion from pulse count to liters
■ price1, price2, price3—The prices for the three fuels
■ sFuelPulses—An event for the start of the fill, identifying the fuel that was selected
Most of Fill’s work is done by accumulate(), which we gave in the last section, and capturePrice(), which we’ll describe next. Finally, you multiply litersDelivered by price to give dollarsDelivered.
Displays liters delivered
Zeroes the pulse count on sClearAccumulator Accumulates
pulses
Multiplies by calibration to give liters
sClearAccumulator sFuelPulses calibration
price1
price price2
price3 sStart
capturePrice accumulate
lift
litersDelivered
dollarsDelivered
Figure 4.9 Conceptual view of Fill that translates delivery pulses into dollars and liters
Listing 4.7 gives the code for the Fill class. When a group of FRP statements corre- sponds to something at a conceptual level, it’s both easy and a good idea to separate them out into a module, which can be implemented as a method or a class. capture- Price() is an example of a module implemented as a method. In Java, it’s easiest to use a static method if there’s one output or a class if more than one. We’ll return to the subject of modules shortly.
NOTE Making the parameters distinct types will help you avoid making mis- takes. For example, you could create a Dollars type instead of using Double, although we haven’t done this here. In some languages, you can use named parameters. You can also use simple container objects to group things together, as we’ve done with Inputs and Outputs.
package chapter4.section7;
import pump.*;
import chapter4.section6.AccumulatePulsesPump;
import nz.sodium.*;
import java.util.Optional;
public class Fill {
public final Cell<Double> price;
public final Cell<Double> dollarsDelivered;
public final Cell<Double> litersDelivered;
public Fill(
Stream<Unit> sClearAccumulator, Stream<Integer> sFuelPulses, Cell<Double> calibration, Cell<Double> price1,
Cell<Double> price2, Cell<Double> price3, Stream<Fuel> sStart) {
price = capturePrice(sStart, price1, price2, price3);
litersDelivered = AccumulatePulsesPump.accumulate(
sClearAccumulator, sFuelPulses, calibration);
dollarsDelivered = litersDelivered.lift(price, (liters, price_) -> liters * price_);
}
public static Cell<Double> capturePrice(
Stream<Fuel> sStart,
Cell<Double> price1, Cell<Double> price2, Cell<Double> price3) {
Stream<Double> sPrice1 = Stream.filterOptional(
sStart.snapshot(price1,
(f, p) -> f == Fuel.ONE ? Optional.of(p) : Optional.empty()));
Stream<Double> sPrice2 = Stream.filterOptional(
sStart.snapshot(price2,
(f, p) -> f == Fuel.TWO ? Optional.of(p) : Optional.empty()));
Listing 4.7 State and logic for the fill
Price as it was when the fill started
Multiplies liters by price to give dollars
Captures price1 if Fuel.ONE was selected
81 Showing dollars of fuel delivered
Stream<Double> sPrice3 = Stream.filterOptional(
sStart.snapshot(price3,
(f, p) -> f == Fuel.THREE ? Optional.of(p) : Optional.empty()));
return sPrice1.orElse(sPrice2.orElse(sPrice3)) .hold(0.0);
} }
The first thing you do in Fill is call capturePrice() to capture the price as it was when the fuel was selected. In capturePrice(), you snapshot each price, taking the value only where you match the right fuel selected. You merge these together with orElse() and then filterOptional so you get the Optional value only when it has a value. You must maintain state here: you need to know the price throughout the fill, so you hold the captured price in a cell.
Finally, as discussed, you calculate and return dollars spent. This is the same idea as in the previous section, where you multiplied pulses by calibration to give liters delivered.
The next listing shows the pump implementation that uses the new Fill class.
package chapter4.section7;
import pump.*;
import chapter4.section4.LifeCycle;
import nz.sodium.*;
import java.util.Optional;
public class ShowDollarsPump implements Pump { public Outputs create(Inputs inputs) {
LifeCycle lc = new LifeCycle(inputs.sNozzle1, inputs.sNozzle2, inputs.sNozzle3);
Fill fi = new Fill(lc.sStart.map(u -> Unit.UNIT), inputs.sFuelPulses, inputs.calibration, inputs.price1, inputs.price2, inputs.price3, lc.sStart);
return new Outputs()
.setDelivery(lc.fillActive.map(
of ->
of.equals(Optional.of(Fuel.ONE)) ? Delivery.FAST1 : of.equals(Optional.of(Fuel.TWO)) ? Delivery.FAST2 : of.equals(Optional.of(Fuel.THREE)) ? Delivery.FAST3 : Delivery.OFF)) .setSaleCostLCD(fi.dollarsDelivered.map(
q -> Formatters.formatSaleCost(q))) .setSaleQuantityLCD(fi.litersDelivered.map(
q -> Formatters.formatSaleQuantity(q)))
.setPriceLCD1(priceLCD(lc.fillActive, fi.price, Fuel.ONE, inputs))
Listing 4.8 Petrol pump that shows dollars
Constructs the new Fill class (previous listing)
.setPriceLCD2(priceLCD(lc.fillActive, fi.price, Fuel.TWO, inputs))
.setPriceLCD3(priceLCD(lc.fillActive, fi.price, Fuel.THREE, inputs));
}
public static Cell<String> priceLCD(
Cell<Optional<Fuel>> fillActive,
Cell<Double> fillPrice, Fuel fuel,
Inputs inputs) {
Cell<Double> idlePrice;
switch (fuel) {
case ONE: idlePrice = inputs.price1; break;
case TWO: idlePrice = inputs.price2; break;
case THREE: idlePrice = inputs.price3; break;
default: idlePrice = null;
}
return fillActive.lift(fillPrice, idlePrice, (oFuelSelected, fillPrice_, idlePrice_) ->
oFuelSelected.isPresent()
? oFuelSelected.get() == fuel
? Formatters.formatPrice(fillPrice_) : ""
: Formatters.formatPrice(idlePrice_));
} }
As always, please try this example out.