Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoned date time #3

Open
wants to merge 11 commits into
base: option-0-no-api
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WIP - changes readings generator to use a stream, more use of zoned d…
…ate time
jejking-tw committed Jul 15, 2024
commit 89e4c02d9bfe2cd3d4e65f53cb44467060828a1c
2 changes: 1 addition & 1 deletion src/main/java/tw/joi/energy/App.java
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ public static void main(String[] args) {
printAllAvailablePricePlans(pricePlanRepository);

printSmartMeterInformation(smartMeterRepository, "Before storing readings...");
var readingsToSave = ElectricityReadingsGenerator.generate(3);
var readingsToSave = ElectricityReadingsGenerator.generateElectricityReadingStream(3).toList();
meterReadingManager.storeReadings(TEST_SMART_METER, readingsToSave);
printSmartMeterInformation(smartMeterRepository, "After storing readings...");

Original file line number Diff line number Diff line change
@@ -2,34 +2,37 @@

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;
import tw.joi.energy.domain.ElectricityReading;

public class ElectricityReadingsGenerator {

public static List<ElectricityReading> generate(int number) {
List<ElectricityReading> readings = new ArrayList<>();
Instant now = Instant.now();
BigDecimal previousReading = BigDecimal.ONE;
Instant previousReadingTime = now.minusSeconds(2 * number * 60L);
private ElectricityReadingsGenerator() {}

Random readingRandomiser = new Random();

for (int i = 0; i < number; i++) {
double positiveIncrement = Math.abs(readingRandomiser.nextGaussian());
BigDecimal currentReading =
previousReading.add(BigDecimal.valueOf(positiveIncrement)).setScale(4, RoundingMode.CEILING);
ElectricityReading electricityReading =
new ElectricityReading(previousReadingTime.plusSeconds(i * 60L), currentReading);
readings.add(electricityReading);
previousReading = currentReading;
}
public static Stream<ElectricityReading> generateElectricityReadingStream(int days) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I buy returning a stream here. The implementation doesn't look much simpler, and every single caller wants a List instead of a Stream.

The name also adds a lot of stutter (unless every caller switches to a static import).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend the static import 😄 which was sort of my assumption anyway.

Lets think a bit more about streams - they're trivially convertible to lists anyway. To be honest, the functions also need some more possible parameters - eg the interval between the readings to generate and the function to generate the values. I hadn't quite finished removing all dependencies on hidden state such as Random...

return generateElectricityReadingStream(Clock.systemDefaultZone(), BigDecimal.ZERO, days);
}

readings.sort(Comparator.comparing(ElectricityReading::time));
return readings;
// we'll provide hourly readings for the specified number of days assuming 24 hours a day
// we'll assume that a house consumes ca 2700 kWh a year, so about 0.3 kWh per hour
public static Stream<ElectricityReading> generateElectricityReadingStream(Clock clock, BigDecimal initialReading, int days) {
var now = clock.instant();
var readingRandomiser = new Random();
var seed = new ElectricityReading(now, initialReading);
var lastTimeToBeSupplied = now.plus(days * 24, ChronoUnit.HOURS);
return Stream.iterate(seed, er -> er.time().equals(lastTimeToBeSupplied) || er.time().isAfter(lastTimeToBeSupplied),
er -> {
var hoursWorthOfEnergy = BigDecimal.valueOf(readingRandomiser.nextDouble(0.3 - 0.2, 0.3 + 0.2));
return new ElectricityReading(er.time().plus(1, ChronoUnit.HOURS), er.readingInKwH().add(hoursWorthOfEnergy));
});
}
}
15 changes: 8 additions & 7 deletions src/main/java/tw/joi/energy/config/TestData.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tw.joi.energy.config;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;

import java.math.BigDecimal;
import java.util.List;
@@ -12,23 +13,23 @@
public final class TestData {

private static final PricePlan MOST_EVIL_PRICE_PLAN =
new PricePlan("price-plan-0", "Dr Evil's Dark Energy", BigDecimal.TEN, emptyList());
new PricePlan("price-plan-0", "Dr Evil's Dark Energy", BigDecimal.TEN, emptySet());
private static final PricePlan RENEWABLES_PRICE_PLAN =
new PricePlan("price-plan-1", "The Green Eco", BigDecimal.valueOf(2), emptyList());
new PricePlan("price-plan-1", "The Green Eco", BigDecimal.valueOf(2), emptySet());
private static final PricePlan STANDARD_PRICE_PLAN =
new PricePlan("price-plan-2", "Power for Everyone", BigDecimal.ONE, emptyList());
new PricePlan("price-plan-2", "Power for Everyone", BigDecimal.ONE, emptySet());

public static SmartMeterRepository smartMeterRepository() {
var smartMeterRepository = new SmartMeterRepository();
smartMeterRepository.save("smart-meter-0", new SmartMeter(MOST_EVIL_PRICE_PLAN, emptyList()));
smartMeterRepository.save(
"smart-meter-1", new SmartMeter(RENEWABLES_PRICE_PLAN, ElectricityReadingsGenerator.generate(7)));
"smart-meter-1", new SmartMeter(RENEWABLES_PRICE_PLAN, ElectricityReadingsGenerator.generateElectricityReadingStream(7).toList()));
smartMeterRepository.save(
"smart-meter-2", new SmartMeter(MOST_EVIL_PRICE_PLAN, ElectricityReadingsGenerator.generate(20)));
"smart-meter-2", new SmartMeter(MOST_EVIL_PRICE_PLAN, ElectricityReadingsGenerator.generateElectricityReadingStream(20).toList()));
smartMeterRepository.save(
"smart-meter-3", new SmartMeter(STANDARD_PRICE_PLAN, ElectricityReadingsGenerator.generate(12)));
"smart-meter-3", new SmartMeter(STANDARD_PRICE_PLAN, ElectricityReadingsGenerator.generateElectricityReadingStream(12).toList()));
smartMeterRepository.save(
"smart-meter-4", new SmartMeter(RENEWABLES_PRICE_PLAN, ElectricityReadingsGenerator.generate(3)));
"smart-meter-4", new SmartMeter(RENEWABLES_PRICE_PLAN, ElectricityReadingsGenerator.generateElectricityReadingStream(3).toList()));
return smartMeterRepository;
}

6 changes: 3 additions & 3 deletions src/main/java/tw/joi/energy/domain/PricePlan.java
Original file line number Diff line number Diff line change
@@ -2,19 +2,19 @@

import java.math.BigDecimal;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Set;

public class PricePlan {

private final String energySupplier;
private final String planName;
private final BigDecimal unitRate; // unit price per kWh
private final List<PeakTimeMultiplier> peakTimeMultipliers;
private final Set<PeakTimeMultiplier> peakTimeMultipliers;

public PricePlan(
String planName, String energySupplier, BigDecimal unitRate, List<PeakTimeMultiplier> peakTimeMultipliers) {
String planName, String energySupplier, BigDecimal unitRate, Set<PeakTimeMultiplier> peakTimeMultipliers) {
this.planName = planName;
this.energySupplier = energySupplier;
this.unitRate = unitRate;
11 changes: 4 additions & 7 deletions src/test/java/tw/joi/energy/domain/PricePlanTest.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package tw.joi.energy.domain;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.TimeZone;
import org.assertj.core.data.Percentage;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@@ -19,7 +16,7 @@ public class PricePlanTest {
@Test
@DisplayName("Get energy supplier should return supplier if not null")
public void get_energy_supplier_should_return_the_energy_supplier_given_supplier_is_existent() {
PricePlan pricePlan = new PricePlan("Test Plan Name", "Energy Supplier Name", BigDecimal.ONE, emptyList());
PricePlan pricePlan = new PricePlan("Test Plan Name", "Energy Supplier Name", BigDecimal.ONE, emptySet());

assertThat(pricePlan.getEnergySupplier()).isEqualTo("Energy Supplier Name");
}
@@ -30,7 +27,7 @@ public void get_price_should_return_the_base_price_given_an_ordinary_date_time()
ZonedDateTime nonPeakDateTime = ZonedDateTime.of(LocalDateTime.of(2017, Month.AUGUST, 31, 12, 0, 0),
ZoneId.of("GMT"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't thought this through completely, but what's the value in using a time zone? I presume:

  • the local date time is in the time zone of the meter that produced the reading
  • only the local date/time matters when determining the day and therefore the zone isn't necessary
  • we'll never compare dates from two different meters in meaningful ways

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following. In the model, there is no time zone associated with a meter, rather the readings are captured as Instant - as in 'absolute' moments in time and not associated with a time zone.

So, such a reading could be in different days depending on where we are in the world. As the Javadoc says, there's no way to convert to an instant with an offset or timezone. Likewise, there's no way to map an Instant to a day of the week without an offset or timezone.

To make that conversion, you'd need to supply a Zone whichever way, and in the worst case you'd arbitrarily choose whatever the system property gave you, making yourself dependent on hidden global variables.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, huh, yeah. Then it seems most correct to associate a zone with the measurements (or with the smart meter). But that'd be a bigger change.

// the price plan has no peak days....
PricePlan pricePlan = new PricePlan("test plan", "test supplier", BigDecimal.ONE, emptyList());
PricePlan pricePlan = new PricePlan("test plan", "test supplier", BigDecimal.ONE, emptySet());

BigDecimal price = pricePlan.getPrice(nonPeakDateTime);

@@ -40,7 +37,7 @@ public void get_price_should_return_the_base_price_given_an_ordinary_date_time()
@Test
@DisplayName("Get unit rate should return unit rate if no null")
public void get_unit_rate_should_return_unit_rate_given_unit_rate_is_present() {
PricePlan pricePlan = new PricePlan("test-price-plan", "test-energy-supplier", BigDecimal.TWO, emptyList());
PricePlan pricePlan = new PricePlan("test-price-plan", "test-energy-supplier", BigDecimal.TWO, emptySet());

BigDecimal rate = pricePlan.getUnitRate();

5 changes: 3 additions & 2 deletions src/test/java/tw/joi/energy/domain/SmartMeterTest.java
Original file line number Diff line number Diff line change
@@ -2,16 +2,17 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collections;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static java.util.Collections.emptyList;

class SmartMeterTest {

@Test
@DisplayName("Price plan should be null if none has been supplied")
void price_plan_id_should_be_null_given_no_price_plan_has_been_provided() {
var smartMeter = new SmartMeter(null, Collections.emptyList());
var smartMeter = new SmartMeter(null, emptyList());

var pricePlanId = smartMeter.getPricePlanId();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still being used by CostComparisonTest, which no longer compiles.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works on my machine ... That's possibly because it seems to have disappeared. I'll double check what's happening there.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually see that test in the branch option-0-no-api, so I'm a bit puzzled. Could you take another look?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, somehow that one was sitting around from another branch 🤷

Original file line number Diff line number Diff line change
@@ -4,15 +4,16 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import tw.joi.energy.domain.ElectricityReading;

public class ElectricityReadingFixture {
public static ElectricityReading createReading(LocalDateTime timeToRead, Double reading) {
public static ElectricityReading createReading(ZonedDateTime timeToRead, Double reading) {
return new ElectricityReading(
timeToRead.atZone(ZoneId.systemDefault()).toInstant(), BigDecimal.valueOf(reading));
timeToRead.toInstant(), BigDecimal.valueOf(reading));
}

public static ElectricityReading createReading(LocalDate dateToRead, Double reading) {
return createReading(dateToRead.atStartOfDay(), reading);
public static ElectricityReading createReading(LocalDate dateToRead, ZoneId zoneId, Double reading) {
return createReading(ZonedDateTime.of(dateToRead.atStartOfDay(), zoneId), reading);
}
}
9 changes: 5 additions & 4 deletions src/test/java/tw/joi/energy/fixture/PricePlanFixture.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package tw.joi.energy.fixture;

import java.math.BigDecimal;
import java.util.Collections;
import tw.joi.energy.domain.PricePlan;

import static java.util.Collections.emptySet;

public class PricePlanFixture {

public static final String WORST_PLAN_ID = "worst-supplier";
public static final String BEST_PLAN_ID = "best-supplier";
public static final String SECOND_BEST_PLAN_ID = "second-best-supplier";

public static final PricePlan DEFAULT_PRICE_PLAN =
new PricePlan(SECOND_BEST_PLAN_ID, "energy-supplier", BigDecimal.TWO, Collections.emptyList());
new PricePlan(SECOND_BEST_PLAN_ID, "energy-supplier", BigDecimal.TWO, emptySet());

public static final PricePlan WORST_PRICE_PLAN =
new PricePlan(WORST_PLAN_ID, null, BigDecimal.TEN, Collections.emptyList());
new PricePlan(WORST_PLAN_ID, null, BigDecimal.TEN, emptySet());

public static final PricePlan BEST_PRICE_PLAN =
new PricePlan(BEST_PLAN_ID, null, BigDecimal.ONE, Collections.emptyList());
new PricePlan(BEST_PLAN_ID, null, BigDecimal.ONE, emptySet());
}
15 changes: 9 additions & 6 deletions src/test/java/tw/joi/energy/service/MeterReadingManagerTest.java
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
import static tw.joi.energy.fixture.ElectricityReadingFixture.createReading;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
@@ -15,6 +17,7 @@

public class MeterReadingManagerTest {

private static final ZoneId GMT = ZoneId.of("GMT");
private static final String SMART_METER_ID = "10101010";
private final SmartMeterRepository smartMeterRepository = new SmartMeterRepository();
private final MeterReadingManager meterReadingManager = new MeterReadingManager(smartMeterRepository);
@@ -54,7 +57,7 @@ public void store_readings_should_throw_exception_given_readings_is_empty() {
@Test
@DisplayName("storeReadings should succeed given non-empty list of readings")
public void store_readings_should_succeed_given_meter_readings() {
var readingsToStore = List.of(createReading(LocalDate.now(), 1.0));
var readingsToStore = List.of(createReading(LocalDate.now(), GMT, 1.0));

meterReadingManager.storeReadings(SMART_METER_ID, readingsToStore);

@@ -65,8 +68,8 @@ public void store_readings_should_succeed_given_meter_readings() {
@Test
@DisplayName("storeReadings should succeed when called multiple times")
public void store_readings_should_succeed_given_multiple_batches_of_meter_readings() {
var meterReadings = List.of(createReading(LocalDate.now(), 1.0));
var otherMeterReadings = List.of(createReading(LocalDate.now(), 2.0));
var meterReadings = List.of(createReading(LocalDate.now(), GMT, 1.0));
var otherMeterReadings = List.of(createReading(LocalDate.now(), GMT, 2.0));

meterReadingManager.storeReadings(SMART_METER_ID, meterReadings);
meterReadingManager.storeReadings(SMART_METER_ID, otherMeterReadings);
@@ -82,8 +85,8 @@ public void store_readings_should_succeed_given_multiple_batches_of_meter_readin
@Test
@DisplayName("storeReadings should write supplied readings to correct meter")
public void store_readings_should_store_to_correct_meter_given_multiple_meters_exist() {
var meterReadings = List.of(createReading(LocalDate.now(), 1.0));
var otherMeterReadings = List.of(createReading(LocalDate.now(), 2.0));
var meterReadings = List.of(createReading(ZonedDateTime.now(), 1.0));
var otherMeterReadings = List.of(createReading(ZonedDateTime.now(), 2.0));

meterReadingManager.storeReadings(SMART_METER_ID, meterReadings);
meterReadingManager.storeReadings("00001", otherMeterReadings);
@@ -104,7 +107,7 @@ public void read_readings_should_throw_exception_given_meter_id_is_not_persisted
@DisplayName("readReadings should return previously supplied readings for a known meterId")
public void read_readings_should_return_readings_given_readings_are_existent() {
// given
var meterReadings = List.of(createReading(LocalDate.now(), 1.0));
var meterReadings = List.of(createReading(ZonedDateTime.now(), 1.0));
meterReadingManager.storeReadings(SMART_METER_ID, meterReadings);
// expect
assertThat(meterReadingManager.readReadings(SMART_METER_ID)).isEqualTo(meterReadings);
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
@@ -26,8 +27,8 @@ public class PricePlanComparatorTest {
private static final String SMART_METER_ID = "smart-meter-id";
private PricePlanComparator comparator;
private SmartMeterRepository smartMeterRepository;
private final LocalDateTime today = LocalDateTime.now();
private final LocalDateTime tenDaysAgo = today.minusDays(10);
private final ZonedDateTime today = ZonedDateTime.now();
private final ZonedDateTime tenDaysAgo = today.minusDays(10);

@BeforeEach
public void setUp() {