Tariffs: add month in fixed tariff zones to allow for seasonal tariffs (#20484)

This commit is contained in:
Jeffrey Borg 2025-04-09 17:36:54 +10:00 • committed by GitHub
parent 651e9b8932
commit 6a87f3727b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 306 additions and 18 deletions

View file

@ -28,8 +28,8 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
var cc struct {
Price float64
Zones []struct {
Price float64
Days, Hours string
Price float64
Days, Hours, Months string
}
}
@ -48,6 +48,11 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
return nil, err
}
months, err := fixed.ParseMonths(z.Months)
if err != nil {
return nil, err
}
hours, err := fixed.ParseTimeRanges(z.Hours)
if err != nil && z.Hours != "" {
return nil, err
@ -55,17 +60,19 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
if len(hours) == 0 {
t.zones = append(t.zones, fixed.Zone{
Price: z.Price,
Days: days,
Price: z.Price,
Days: days,
Months: months,
})
continue
}
for _, h := range hours {
t.zones = append(t.zones, fixed.Zone{
Price: z.Price,
Days: days,
Hours: h,
Price: z.Price,
Days: days,
Months: months,
Hours: h,
})
}
}
@ -86,14 +93,15 @@ func (t *Fixed) Rates() (api.Rates, error) {
start := now.With(t.clock.Now().Local()).BeginningOfDay()
for i := range 7 {
dayStart := start.AddDate(0, 0, i)
dow := fixed.Day((int(start.Weekday()) + i) % 7)
month := fixed.Month(dayStart.Month() - 1)
zones := t.zones.ForDay(dow)
zones := t.zones.ForDayAndMonth(dow, month)
if len(zones) == 0 {
return nil, fmt.Errorf("no zones for weekday %d", dow)
}
dayStart := start.AddDate(0, 0, i)
markers := zones.TimeTableMarkers()
for i, m := range markers {

133
tariff/fixed/month.go Normal file
View file

@ -0,0 +1,133 @@
package fixed
import (
"errors"
"fmt"
"slices"
"strconv"
"strings"
)
//go:generate go tool enumer -type Month
type Month int
const (
January Month = iota
February
March
April
May
June
July
August
September
October
November
December
)
var Year = []Month{January, February, March, April, May, June, July, August, September, October, November, December}
var shortMonths = map[string]Month{
// english
"jan": January,
"feb": February,
"mar": March,
"apr": April,
"may": May,
"jun": June,
"jul": July,
"aug": August,
"sep": September,
"oct": October,
"nov": November,
"dec": December,
// german
// "jan": January,
// "feb": February,
"mär": March,
// "apr": April,
"mai": May,
// "jun": June,
// "jul": July,
// "aug": August,
// "sep": September,
"okt": October,
// "nov": November,
"dez": December,
}
// ParseMonth parses a single month
func ParseMonth(s string) (Month, error) {
s = strings.ToLower(strings.TrimSpace(s))
// full string
if m, err := MonthString(s); err == nil {
return m, nil
}
// short string
if m, ok := shortMonths[s]; ok {
return m, nil
}
m, err := strconv.Atoi(s)
if m < 1 || m > 12 || err != nil {
return 0, fmt.Errorf("invalid month: %s", s)
}
return Month(m - 1), nil
}
// ParseMonths converts a months string into a slice of individual months
// Months format:
//
// month[-month][, ...]
func ParseMonths(s string) ([]Month, error) {
var res []Month
for _, segment := range strings.Split(s, ",") {
fromto := strings.SplitN(segment, "-", 2)
if len(fromto) == 0 {
return nil, fmt.Errorf("invalid month range: %s", segment)
}
fromToFrom := fromto[0]
// single empty segment
if len(fromto) == 1 && strings.TrimSpace(fromToFrom) == "" {
return slices.Clone(Year), nil
}
from, err := ParseMonth(fromToFrom)
if err != nil {
return nil, err
}
res = append(res, from)
if len(fromto) == 2 {
to, err := ParseMonth(fromto[1])
if err != nil {
return nil, err
}
if to < from {
to += 12
}
for m := from + 1; m <= to; m++ {
res = append(res, m%12)
}
}
}
if len(res) > 12 {
return nil, errors.New("too many months")
}
if len(slices.Compact(slices.Sorted(slices.Values(res)))) < len(res) {
return nil, errors.New("duplicate months")
}
return res, nil
}

View file

@ -0,0 +1,118 @@
// Code generated by "enumer -type Month"; DO NOT EDIT.
package fixed
import (
"fmt"
"strings"
)
const _MonthName = "JanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember"
var _MonthIndex = [...]uint8{0, 7, 15, 20, 25, 28, 32, 36, 42, 51, 58, 66, 74}
const _MonthLowerName = "januaryfebruarymarchaprilmayjunejulyaugustseptemberoctobernovemberdecember"
func (i Month) String() string {
if i < 0 || i >= Month(len(_MonthIndex)-1) {
return fmt.Sprintf("Month(%d)", i)
}
return _MonthName[_MonthIndex[i]:_MonthIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _MonthNoOp() {
var x [1]struct{}
_ = x[January-(0)]
_ = x[February-(1)]
_ = x[March-(2)]
_ = x[April-(3)]
_ = x[May-(4)]
_ = x[June-(5)]
_ = x[July-(6)]
_ = x[August-(7)]
_ = x[September-(8)]
_ = x[October-(9)]
_ = x[November-(10)]
_ = x[December-(11)]
}
var _MonthValues = []Month{January, February, March, April, May, June, July, August, September, October, November, December}
var _MonthNameToValueMap = map[string]Month{
_MonthName[0:7]: January,
_MonthLowerName[0:7]: January,
_MonthName[7:15]: February,
_MonthLowerName[7:15]: February,
_MonthName[15:20]: March,
_MonthLowerName[15:20]: March,
_MonthName[20:25]: April,
_MonthLowerName[20:25]: April,
_MonthName[25:28]: May,
_MonthLowerName[25:28]: May,
_MonthName[28:32]: June,
_MonthLowerName[28:32]: June,
_MonthName[32:36]: July,
_MonthLowerName[32:36]: July,
_MonthName[36:42]: August,
_MonthLowerName[36:42]: August,
_MonthName[42:51]: September,
_MonthLowerName[42:51]: September,
_MonthName[51:58]: October,
_MonthLowerName[51:58]: October,
_MonthName[58:66]: November,
_MonthLowerName[58:66]: November,
_MonthName[66:74]: December,
_MonthLowerName[66:74]: December,
}
var _MonthNames = []string{
_MonthName[0:7],
_MonthName[7:15],
_MonthName[15:20],
_MonthName[20:25],
_MonthName[25:28],
_MonthName[28:32],
_MonthName[32:36],
_MonthName[36:42],
_MonthName[42:51],
_MonthName[51:58],
_MonthName[58:66],
_MonthName[66:74],
}
// MonthString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func MonthString(s string) (Month, error) {
if val, ok := _MonthNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _MonthNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Month values", s)
}
// MonthValues returns all values of the enum
func MonthValues() []Month {
return _MonthValues
}
// MonthStrings returns a slice of all String values of the enum
func MonthStrings() []string {
strs := make([]string, len(_MonthNames))
copy(strs, _MonthNames)
return strs
}
// IsAMonth returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Month) IsAMonth() bool {
for _, v := range _MonthValues {
if i == v {
return true
}
}
return false
}

View file

@ -5,9 +5,10 @@ import (
)
type Zone struct {
Price float64
Days []Day
Hours TimeRange
Price float64
Days []Day
Hours TimeRange
Months []Month
}
type Zones []Zone
@ -29,10 +30,10 @@ func (r Zones) Swap(i, j int) {
}
// ForDay returns the zones for given day in ascending order
func (r Zones) ForDay(day Day) Zones {
func (r Zones) ForDayAndMonth(day Day, month Month) Zones {
var zones Zones
for _, z := range r {
if slices.Contains(z.Days, day) || len(z.Days) == 0 {
if (slices.Contains(z.Days, day) || len(z.Days) == 0) && (slices.Contains(z.Months, month) || len(z.Months) == 0) {
zones = append(zones, z)
}
}

View file

@ -13,10 +13,38 @@ func TestZonesForDay(t *testing.T) {
{Days: []Day{Wednesday, Thursday, Friday}},
}
assert.Len(t, zones.ForDay(Sunday), 1)
assert.Len(t, zones.ForDay(Monday), 2)
assert.Len(t, zones.ForDay(Wednesday), 3)
assert.Len(t, zones.ForDay(Thursday), 2)
assert.Len(t, zones.ForDayAndMonth(Sunday, April), 1)
assert.Len(t, zones.ForDayAndMonth(Monday, April), 2)
assert.Len(t, zones.ForDayAndMonth(Wednesday, April), 3)
assert.Len(t, zones.ForDayAndMonth(Thursday, April), 2)
}
func TestZonesForDayAndMonth(t *testing.T) {
zones := Zones{
{Days: nil, Months: nil}, // Applies to all days and months
{Days: []Day{Monday, Tuesday, Wednesday}, Months: []Month{January, February, March, April}},
{Days: []Day{Wednesday, Thursday, Friday}, Months: []Month{April, May, June}},
}
// Test for specific day and month combinations
assert.Len(t, zones.ForDayAndMonth(Sunday, April), 1)
assert.Len(t, zones.ForDayAndMonth(Monday, January), 2)
assert.Len(t, zones.ForDayAndMonth(Wednesday, April), 3)
assert.Len(t, zones.ForDayAndMonth(Thursday, May), 2)
assert.Len(t, zones.ForDayAndMonth(Saturday, July), 1)
}
func TestZonesForMonth(t *testing.T) {
zones := Zones{
{Months: nil},
{Months: []Month{January, February, March, April}},
{Months: []Month{April, May, June}},
}
// Test for specific months
assert.Len(t, zones.ForDayAndMonth(Sunday, January), 2)
assert.Len(t, zones.ForDayAndMonth(Sunday, April), 3)
assert.Len(t, zones.ForDayAndMonth(Sunday, July), 1)
}
func TestZonesTimeTableMarkers(t *testing.T) {