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
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ as well as the logic to recommend the cheapest price plan for a particular house

>Unfortunately, as the codebase has evolved, it has gathered tech debt in the form of a number of code smells and some
questionable design decisions. Our goal for the upcoming exercise would be to deliver value by implementing a new
feature using _Test Driven Development_ (TDD), while refactoring away the code smells we see.
feature using _[Test Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html)_ (TDD), while refactoring away the code smells we see.
>
>In preparation for this, please take some time to go through the code and identify any improvements, big or small,
that would improve its maintainability, testability, and design.
Expand Down Expand Up @@ -46,7 +46,9 @@ The project requires [Java 21](https://adoptium.net/) or higher.

## Useful commands

Compile the project, run the tests and creates an executable JAR file
### Build the project

Compile the project, run the tests and creates an executable JAR file:

```console
./gradlew build
Expand All @@ -67,11 +69,10 @@ You can run it with the following command:

## API Documentation

The codebase contains two service classes, _MeterReadingManager_ and _PricePlanComparator_, that serve as entry points to
the implemented features.
The codebase contains two service classes, `MeterReadingManager` and `PricePlanComparator` that serve as entry points to the implemented features.

### MeterReadingManager
Provides methods to store and fetch the energy consumption readings from a given Smart Meter
Provides methods to store and fetch the energy consumption readings from a given Smart Meter.

> #### _public void_ storeReadings(_String smartMeterId, List<ElectricityReading> electricityReadings_)
Stores the provided _ElectricityReading_ collection in the indicated _SmartMeter_. If no
Expand All @@ -91,13 +92,13 @@ An _ElectricityReading_ record consists of the following fields:

Example readings

| Date (`GMT`) | Epoch timestamp | Reading (kWh) |
|-------------------|----------------:|--------------:|
| `2020-11-29 8:00` | 1606636800 | 600.05 |
| `2020-11-29 9:00` | 1606640400 | 602.06 |
| `2020-11-30 7:30` | 1606721400 | 610.09 |
| `2020-12-01 8:30` | 1606811400 | 627.12 |
| `2020-12-02 8:30` | 1606897800 | 635.14 |
| Date (`GMT`) | Epoch timestamp (seconds) | Reading (kWh) |
|-------------------|--------------------------:|--------------:|
| `2020-11-29 8:00` | 1606636800 | 600.05 |
| `2020-11-29 9:00` | 1606640400 | 602.06 |
| `2020-11-30 7:30` | 1606721400 | 610.09 |
| `2020-12-01 8:30` | 1606811400 | 627.12 |
| `2020-12-02 8:30` | 1606897800 | 635.14 |

Thee above table shows some readings sampled by a smart meter over multiple days. Note that since the smart
meter is reporting the total energy consumed up to that point in time, a reading's value will always be higher or the same as
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ application {
mainClass = "tw.joi.energy.App"
}

tasks.withType<JavaCompile>() {
tasks.withType<JavaCompile> {
this.options.isDeprecation = true
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/tw/joi/energy/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ 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...");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
package tw.joi.energy.config;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.time.Clock;
import java.time.temporal.ChronoUnit;
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);
public static final double AVG_HOURLY_USAGE = 0.3;
public static final double VARIANCE = 0.2;
public static final double MIN_HOURLY_USAGE = AVG_HOURLY_USAGE - VARIANCE;
public static final double MAX_HOURLY_USAGE = AVG_HOURLY_USAGE + VARIANCE;

Random readingRandomiser = new Random();
private ElectricityReadingsGenerator() {}

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

// 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

readings.sort(Comparator.comparing(ElectricityReading::time));
return readings;
// the assumed starting point is the time on the clock, the ending point 24 hours later - so for 1 day, we'll get 25
// readings
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 * 24L, ChronoUnit.HOURS);
return Stream.iterate(seed, er -> !er.time().isAfter(lastTimeToBeSupplied), er -> {
var hoursWorthOfEnergy =
BigDecimal.valueOf(readingRandomiser.nextDouble(MIN_HOURLY_USAGE, MAX_HOURLY_USAGE));
return new ElectricityReading(
er.time().plus(1, ChronoUnit.HOURS), er.readingInKwH().add(hoursWorthOfEnergy));
});
}
}
30 changes: 23 additions & 7 deletions src/main/java/tw/joi/energy/config/TestData.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,39 @@
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);
private static final PricePlan RENEWABLES_PRICE_PLAN =
new PricePlan("price-plan-1", "The Green Eco", BigDecimal.valueOf(2), null);
new PricePlan("price-plan-1", "The Green Eco", BigDecimal.valueOf(2));
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);

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

Expand Down
11 changes: 9 additions & 2 deletions src/main/java/tw/joi/energy/domain/ElectricityReading.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package tw.joi.energy.domain;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;

/**
* @param reading kWh
* @param time point in time
* @param readingInKwH energy consumed in total to this point in time in kWh
*/
public record ElectricityReading(Instant time, BigDecimal reading) {}
public record ElectricityReading(Instant time, BigDecimal readingInKwH) {

public ElectricityReading(Clock clock, double readingInKwH) {

Choose a reason for hiding this comment

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

Is this constructor meant to be for testing purposes (faking the current time, lighter syntax for the reading) or also for production usage?

this(clock.instant(), BigDecimal.valueOf(readingInKwH));
}
}
6 changes: 6 additions & 0 deletions src/main/java/tw/joi/energy/domain/PeakTimeMultiplier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tw.joi.energy.domain;

import java.math.BigDecimal;
import java.time.DayOfWeek;

public record PeakTimeMultiplier(DayOfWeek dayOfWeek, BigDecimal multiplier) {}
49 changes: 20 additions & 29 deletions src/main/java/tw/joi/energy/domain/PricePlan.java
Original file line number Diff line number Diff line change
@@ -1,64 +1,55 @@
package tw.joi.energy.domain;

import java.io.*;
import java.math.BigDecimal;
import java.text.*;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.util.List;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class PricePlan {

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

public PricePlan(String planName, String energySupplier, BigDecimal unitRate) {
this.planName = planName;
this.energySupplier = energySupplier;
this.unitRate = unitRate;
this.peakTimeMultipliers = Collections.emptyMap();
}

public PricePlan(
String planName, String energySupplier, BigDecimal unitRate, List<PeakTimeMultiplier> peakTimeMultipliers) {
String planName,
String energySupplier,
BigDecimal unitRate,
Map<DayOfWeek, BigDecimal> peakTimeMultipliers) {
this.planName = planName;
this.energySupplier = energySupplier;
this.unitRate = unitRate;
this.peakTimeMultipliers = peakTimeMultipliers;
this.peakTimeMultipliers = Collections.unmodifiableMap(new HashMap<>(peakTimeMultipliers));

Choose a reason for hiding this comment

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

Suggested change
this.peakTimeMultipliers = Collections.unmodifiableMap(new HashMap<>(peakTimeMultipliers));
this.peakTimeMultipliers = Map.copyOf(peakTimeMultipliers);

Credit goes to IntelliJ for that suggestion...

}

public String getEnergySupplier() {
return energySupplier;
}

public void setEnergySupplier(String supplierName) {
this.energySupplier = supplierName;
}

public String getPlanName() {
return planName;
}

public void setPlanName(String name) {
this.planName = name;
}

public BigDecimal getUnitRate() {
return unitRate;
}

public BigDecimal getPrice(LocalDateTime dateTime) {
public BigDecimal getPrice(ZonedDateTime dateTime) {
return unitRate;
}

@Override
public String toString() {
return "Name: '" + planName + "', Unit Rate: " + unitRate + ", Supplier: '" + energySupplier + "'";
}

static class PeakTimeMultiplier {

DayOfWeek dayOfWeek;
BigDecimal multiplier;

public PeakTimeMultiplier(DayOfWeek dayOfWeek, BigDecimal multiplier) {
this.dayOfWeek = dayOfWeek;
this.multiplier = multiplier;
}
}
}
1 change: 1 addition & 0 deletions src/main/java/tw/joi/energy/domain/SmartMeter.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.stream.Collectors;

public class SmartMeter {

private final PricePlan pricePlan;
private final List<ElectricityReading> electricityReadings;

Expand Down
11 changes: 5 additions & 6 deletions src/main/java/tw/joi/energy/repository/PricePlanRepository.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package tw.joi.energy.repository;

import static java.util.Comparator.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toMap;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.*;
import tw.joi.energy.domain.ElectricityReading;
import tw.joi.energy.domain.PricePlan;
import tw.joi.energy.domain.SmartMeter;
Expand All @@ -34,8 +33,8 @@ private BigDecimal calculateCost(Collection<ElectricityReading> electricityReadi
.max(comparing(ElectricityReading::time))
.get();

BigDecimal energyConsumed = latest.reading().subtract(oldest.reading());
return energyConsumed.multiply(pricePlan.getPrice(LocalDateTime.now()));
BigDecimal energyConsumed = latest.readingInKwH().subtract(oldest.readingInKwH());
return energyConsumed.multiply(pricePlan.getPrice(ZonedDateTime.now()));
}

public List<PricePlan> getAllPricePlans() {
Expand Down
Loading