Ctrl K
ComunidadGuías…

tariff/embed.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"errors"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"reflect"
	"slices"
	"time"

	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/plugin/golang/stdlib"
	"github.com/evcc-io/evcc/tariff/fixed"
	"github.com/traefik/yaegi/interp"
)

type embed struct {
	Features_ []api.Feature `mapstructure:"features"`

	Charges       float64             `mapstructure:"charges"`
	ChargesZones_ []chargesZoneConfig `mapstructure:"chargesZones"`
	Tax           float64             `mapstructure:"tax"`
	Formula       string              `mapstructure:"formula"`

	chargesZones fixed.Zones
	calc         func(price, charges float64, ts time.Time) (float64, error)
}

type chargesZoneConfig struct {
	Charges             float64
	Days, Hours, Months string
}

func (t *embed) init() (err error) {
	defer func() {
		if r := recover(); r != nil && err == nil {
			err = fmt.Errorf("panic: %v", r)
		}
	}()

	specs := make([]fixed.ZoneSpec, len(t.ChargesZones_))
	for i, c := range t.ChargesZones_ {
		specs[i] = fixed.ZoneSpec{Price: c.Charges, Days: c.Days, Hours: c.Hours, Months: c.Months}
	}
	zones, err := fixed.ParseZones(specs)
	if err != nil {
		return err
	}
	t.chargesZones = zones

	if t.Formula == "" {
		return nil
	}

	vm := interp.New(interp.Options{})
	if err := vm.Use(stdlib.Symbols); err != nil {
		return err
	}
	vm.ImportUsed()

	body, err := formulaBody(t.Formula)
	if err != nil {
		return err
	}

	// Compile the formula into a callable function, avoiding any per-call parsing
	src := fmt.Sprintf("var calc = func(price, charges, tax float64, ts time.Time) float64 {\n%s\n}", body)
	if _, err := vm.Eval(src); err != nil {
		return err
	}

	calcFn := vm.Globals()["calc"]

	t.calc = func(price, charges float64, ts time.Time) (float64, error) {
		var (
			res []reflect.Value
			err error
		)
		func() {
			defer func() {
				if r := recover(); r != nil {
					err = fmt.Errorf("panic: %v", r)
				}
			}()
			res = calcFn.Call([]reflect.Value{
				reflect.ValueOf(price),
				reflect.ValueOf(charges),
				reflect.ValueOf(t.Tax),
				reflect.ValueOf(time.Unix(ts.Unix(), 0).Local()),
			})
		}()

		if err != nil {
			return 0, err
		}
		return res[0].Float(), nil
	}

	// test the formula
	_, err = t.calc(0, t.Charges, time.Now())

	return err
}

// formulaBody turns the formula's trailing expression into the return statement,
// allowing multi-statement formulas like `x := 1; if ... { x = 2 }; price + x`
func formulaBody(formula string) (string, error) {
	const prefix = "package p; func _() {\n"

	f, err := parser.ParseFile(token.NewFileSet(), "", prefix+formula+"\n}", 0)
	if err != nil {
		return "", err
	}

	stmts := f.Decls[0].(*ast.FuncDecl).Body.List
	if len(stmts) == 0 {
		return "", errors.New("empty formula")
	}

	last, ok := stmts[len(stmts)-1].(*ast.ExprStmt)
	if !ok {
		return "", errors.New("formula must end with an expression")
	}

	// file base is 1, offsets are relative to the formula
	start, end := int(last.Pos())-1-len(prefix), int(last.End())-1-len(prefix)

	return formula[:start] + "return float64(" + formula[start:end] + ")" + formula[end:], nil
}

// effectiveCharges resolves the charge for ts in local time; later zones win.
func (t *embed) effectiveCharges(ts time.Time) float64 {
	if len(t.chargesZones) == 0 {
		return t.Charges
	}

	ts = ts.Local()
	day := fixed.Day(int(ts.Weekday()))
	month := fixed.Month(ts.Month() - 1)
	hm := fixed.HourMin{Hour: ts.Hour(), Min: ts.Minute()}

	zones := t.chargesZones.ForDayAndMonth(day, month)
	for _, z := range slices.Backward(zones) {
		if z.Hours.Contains(hm) {
			return z.Price
		}
	}
	return t.Charges
}

func (t *embed) totalPrice(price float64, ts time.Time) float64 {
	charges := t.effectiveCharges(ts)
	if t.calc != nil {
		res, _ := t.calc(price, charges, ts)
		return res
	}
	return (price + charges) * (1 + t.Tax)
}

var _ api.FeatureDescriber = (*embed)(nil)

func (t *embed) Features() []api.Feature {
	return t.Features_
}