Skip to content

Commit

Permalink
Merge pull request #41 from daithihearn/variable-period-lengtha
Browse files Browse the repository at this point in the history
feat: variable period lengths
  • Loading branch information
daithihearn authored Oct 15, 2023
2 parents 04424da + f71a8b5 commit 9096887
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 172 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.12.4
1.13.0
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class PriceService(
val date = dateStr?.let { LocalDate.parse(it, dateFormatter) } ?: LocalDate.now()
val prices = getPrices(start = dateStr, end = dateStr)
if (prices.isEmpty()) return null
val cheapestPeriods = getTwoCheapestPeriods(prices, 3)
val expensivePeriod = getMostExpensivePeriod(prices, 3)
val cheapestPeriods = getTwoCheapestPeriods(prices)
val expensivePeriod = getMostExpensivePeriod(prices)

val dailyAverage = prices.map { it.price }.average()
val thirtyDayAverage: Double = getThirtyDayAverage(date.atStartOfDay())
Expand Down Expand Up @@ -108,14 +108,14 @@ class PriceService(
*/
fun syncEsiosData(day: LocalDate) {
// Get the prices for the day
val prices = getPrices(day)
val pricesToday = getPrices(day)

// Validate that we have prices for the day
if (!validatePricesForDay(prices, day)) {
if (!validatePricesForDay(pricesToday, day)) {
logger.info("Failed to validate prices for $day. Will attempt to sync with ESIOS")

// Clear the prices for the day
priceRepo.deleteAll(prices)
priceRepo.deleteAll(pricesToday)

// Get the prices from ESIOS
val dayStr = day.format(dateFormatter)
Expand Down
126 changes: 122 additions & 4 deletions src/main/kotlin/ie/daithi/electricityprices/utils/PriceUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ import kotlin.math.abs

const val VARIANCE = 0.02

/**
* Returns the cheapest period. The period is calculated by getting the cheapest hour and then selecting
* adjacent hours that fall within a price variance of 0.02.
*/
fun getCheapestPeriod(prices: List<Price>): List<Price> {
val cheapestHour = prices.minByOrNull { it.price } ?: return emptyList()

val cheapestPeriod = mutableListOf(cheapestHour)

var i = prices.indexOf(cheapestHour) + 1
while (i < prices.size && abs(prices[i].price - cheapestHour.price) < VARIANCE) {
cheapestPeriod.add(prices[i])
i++
}

i = prices.indexOf(cheapestHour) - 1
while (i >= 0 && abs(prices[i].price - cheapestHour.price) < VARIANCE) {
cheapestPeriod.add(prices[i])
i--
}

return cheapestPeriod.sortedBy { it.dateTime }
}

/**
* Returns the cheapest period of n hours.
* If there are multiple periods with the same price, the first is returned.
* If there are less than n prices, an empty list is returned.
*/
fun getCheapestPeriod(prices: List<Price>, n: Int): List<Price> {
if (prices.size < n) {
return emptyList()
Expand All @@ -29,6 +58,67 @@ fun getCheapestPeriod(prices: List<Price>, n: Int): List<Price> {
return minWindow
}

/**
* Returns the two cheapest periods.
* If there is only one period that falls within the predefined variance of 0.02, the second period is empty.
*/
fun getTwoCheapestPeriods(prices: List<Price>): Pair<List<Price>, List<Price>> {
if (prices.size < 2) {
return Pair(emptyList(), emptyList())
}

val firstPeriod = getCheapestPeriod(prices)

val remainingPricesBefore = prices.filter { it.dateTime.isBefore(firstPeriod.first().dateTime) }.dropLast(1)
val remainingPricesAfter = prices.filter { it.dateTime.isAfter(firstPeriod.last().dateTime) }.drop(1)

val firstPeriodBefore = getCheapestPeriod(remainingPricesBefore)
val firstPeriodAfter = getCheapestPeriod(remainingPricesAfter)

var secondPeriod: List<Price>

secondPeriod = if (firstPeriodBefore.isNotEmpty() && firstPeriodAfter.isNotEmpty()) {
val firstPeriodBeforeAverage = calculateAverage(firstPeriodBefore)
val firstPeriodAfterAverage = calculateAverage(firstPeriodAfter)

if (firstPeriodBeforeAverage < firstPeriodAfterAverage) firstPeriodBefore else firstPeriodAfter
} else if (firstPeriodBefore.isNotEmpty()) {
firstPeriodBefore
} else if (firstPeriodAfter.isNotEmpty()) {
firstPeriodAfter
} else {
emptyList()
}

if (abs(calculateAverage(firstPeriod) - calculateAverage(secondPeriod)) > VARIANCE) {
secondPeriod = emptyList()
}

return when {
secondPeriod.isEmpty() || abs(calculateAverage(firstPeriod) - calculateAverage(secondPeriod)) > VARIANCE -> Pair(
firstPeriod,
emptyList()
)

firstPeriod.last().dateTime == secondPeriod.first().dateTime.minusHours(1) -> Pair(
firstPeriod + secondPeriod,
emptyList()
)

firstPeriod.first().dateTime == secondPeriod.last().dateTime.plusHours(1) -> Pair(
secondPeriod + firstPeriod,
emptyList()
)

firstPeriod.first().dateTime.isBefore(secondPeriod.first().dateTime) -> Pair(firstPeriod, secondPeriod)
else -> Pair(secondPeriod, firstPeriod)
}
}

/**
* Returns the two cheapest periods of n hours.
* If there is only one period that falls within the predefined variance of 0.02, the second period is empty.
*/
fun getTwoCheapestPeriods(prices: List<Price>, n: Int): Pair<List<Price>, List<Price>> {
if (prices.size < n) {
return Pair(emptyList(), emptyList())
Expand All @@ -42,15 +132,15 @@ fun getTwoCheapestPeriods(prices: List<Price>, n: Int): Pair<List<Price>, List<P
val firstPeriodBefore = getCheapestPeriod(remainingPricesBefore, n)
val firstPeriodAfter = getCheapestPeriod(remainingPricesAfter, n)

var secondPeriod: List<Price> = emptyList()
var secondPeriod: List<Price>

if (firstPeriodBefore.size == n && firstPeriodAfter.size == n) {
secondPeriod = if (firstPeriodBefore.size == n && firstPeriodAfter.size == n) {
val firstPeriodBeforeAverage = calculateAverage(firstPeriodBefore)
val firstPeriodAfterAverage = calculateAverage(firstPeriodAfter)

secondPeriod = if (firstPeriodBeforeAverage < firstPeriodAfterAverage) firstPeriodBefore else firstPeriodAfter
if (firstPeriodBeforeAverage < firstPeriodAfterAverage) firstPeriodBefore else firstPeriodAfter
} else {
secondPeriod = if (firstPeriodBefore.size == n) firstPeriodBefore else firstPeriodAfter
if (firstPeriodBefore.size == n) firstPeriodBefore else firstPeriodAfter
}

if (abs(calculateAverage(firstPeriod) - calculateAverage(secondPeriod)) > VARIANCE) {
Expand Down Expand Up @@ -78,6 +168,34 @@ fun getTwoCheapestPeriods(prices: List<Price>, n: Int): Pair<List<Price>, List<P
}
}

/**
* Returns the most expensive period.
* The period is calculated by getting the most expensive hour and then selecting
* adjacent hours that fall within a price variance of 0.02.
*/
fun getMostExpensivePeriod(prices: List<Price>): List<Price> {
val mostExpensiveHour = prices.maxByOrNull { it.price } ?: return emptyList()

val mostExpensivePeriod = mutableListOf(mostExpensiveHour)

var i = prices.indexOf(mostExpensiveHour) + 1
while (i < prices.size && abs(prices[i].price - mostExpensiveHour.price) < VARIANCE) {
mostExpensivePeriod.add(prices[i])
i++
}

i = prices.indexOf(mostExpensiveHour) - 1
while (i >= 0 && abs(prices[i].price - mostExpensiveHour.price) < VARIANCE) {
mostExpensivePeriod.add(prices[i])
i--
}

return mostExpensivePeriod.sortedBy { it.dateTime }
}

/**
* Returns the most expensive period of n hours.
*/
fun getMostExpensivePeriod(prices: List<Price>, n: Int): List<Price> {
if (prices.size < n) {
return emptyList()
Expand Down
Loading

0 comments on commit 9096887

Please sign in to comment.