Tariffs: add month in fixed tariff zones to allow for seasonal tariffs (#20484)
This commit is contained in:
parent
651e9b8932
commit
6a87f3727b
5 changed files with 306 additions and 18 deletions
|
|
@ -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
133
tariff/fixed/month.go
Normal 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
|
||||
}
|
||||
118
tariff/fixed/month_enumer.go
Normal file
118
tariff/fixed/month_enumer.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue