From f9cc4b4db51a7259c6551d6bd5bb19381205a710 Mon Sep 17 00:00:00 2001 From: DanB Date: Thu, 2 Jul 2020 18:37:56 +0200 Subject: [PATCH] RateS with initial OrderRatesOnIntervals --- engine/rateprofile.go | 36 ++++++++-- rates/librates.go | 93 +++++++++++++++--------- rates/librates_test.go | 158 ++++++++++------------------------------- 3 files changed, 127 insertions(+), 160 deletions(-) diff --git a/engine/rateprofile.go b/engine/rateprofile.go index 6e5fd7f2d..2a7cb2f50 100644 --- a/engine/rateprofile.go +++ b/engine/rateprofile.go @@ -45,8 +45,20 @@ type RateProfile struct { maxCost *utils.Decimal } -func (rpp *RateProfile) TenantID() string { - return utils.ConcatenatedKey(rpp.Tenant, rpp.ID) +func (rp *RateProfile) TenantID() string { + return utils.ConcatenatedKey(rp.Tenant, rp.ID) +} + +func (rp *RateProfile) Compile() (err error) { + rp.connFee = utils.NewDecimalFromFloat64(rp.ConnectFee) + rp.minCost = utils.NewDecimalFromFloat64(rp.MinCost) + rp.minCost = utils.NewDecimalFromFloat64(rp.MaxCost) + for _, rtP := range rp.Rates { + if err = rtP.Compile(); err != nil { + return + } + } + return } // Route defines rate related information used within a RateProfile @@ -61,6 +73,21 @@ type Rate struct { aTime cron.Schedule // compiled version of activation time as cron.Schedule interface } +func (rt *Rate) Compile() (err error) { + aTime := rt.ActivationStart + if aTime == utils.EmptyString { + aTime = "* * * * *" + } + if rt.aTime, err = cron.ParseStandard(aTime); err != nil { + return + } + return +} + +func (rt *Rate) NextActivationTime(t time.Time) time.Time { + return rt.aTime.Next(t) +} + type IntervalRate struct { IntervalStart time.Duration // Starting point when the Rate kicks in Unit time.Duration // RateUnit @@ -83,6 +110,7 @@ type RateSInterval struct { } type RateSIncrement struct { - UsageStart time.Duration - Rate *Rate + Rate *Rate + IntervalRateIndex int + Usage time.Duration } diff --git a/rates/librates.go b/rates/librates.go index 4e6e81a70..4f267a052 100644 --- a/rates/librates.go +++ b/rates/librates.go @@ -18,46 +18,27 @@ along with this program. If not, see package rates -/* +import ( + "sort" + "time" + + "github.com/cgrates/cgrates/engine" +) + // orderRatesOnIntervals will order the rates based on ActivationInterval and intervalStart of each Rate // there can be only one winning Rate for each interval, prioritized by the Weight -func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, isDuration bool) (ordRts []*engine.RateSInterval) { +func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, usage time.Duration, isDuration bool) (ordRts []*engine.RateSInterval) { + cronSTime := sTime.Add(time.Duration(-1 * time.Minute)) // cron min verbosity is minute + endTime := sTime.Add(usage) // cover also the last unit used // index the received rates - rtIdx := make(map[time.Time]map[time.Duration][]*engine.Rate) // map[ActivationTime]map[IntervalStart][]*engine.Rate + rtIdx := make(map[time.Time][]*engine.Rate) // map[ActivationTime][]*engine.Rate for _, rt := range aRts { - var rtATimesKey time.Time - if rt.ActivationInterval != nil { - rtATimesKey = rt.ActivationInterval.ActivationTime + nextRunTime := rt.NextActivationTime(cronSTime) + if nextRunTime.After(endTime) { + continue } - if _, has := rtIdx[rtATimesKey]; !has { - rtIdx[rtATimesKey] = make(map[time.Duration][]*engine.Rate) - } - rtIdx[rtATimesKey][rt.IntervalStart] = append(rtIdx[rtATimesKey][rt.IntervalStart], rt) + rtIdx[nextRunTime] = append(rtIdx[nextRunTime], rt) } - - // sort the rates within the duration map - for _, durMp := range rtIdx { - for _, rts := range durMp { - sort.Slice(rts, func(i, j int) bool { - return rts[i].Weight > rts[j].Weight - }) - } - } - - // sort the IntervalStarts - sortedStarts := make(map[time.Time][]time.Duration) // map[ActivationTime] - for aTime, durMp := range rtIdx { - sortedStarts[aTime] = make([]time.Duration, len(durMp)) - idxDur := 0 - for dur := range durMp { - sortedStarts[aTime] = append(sortedStarts[aTime], dur) - idxDur++ - } - sort.Slice(sortedStarts[aTime], func(i, j int) bool { - return sortedStarts[aTime][i] < sortedStarts[aTime][j] - }) - } - // sort the activation times sortedATimes := make([]time.Time, len(rtIdx)) idxATimes := 0 @@ -68,7 +49,49 @@ func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, isDuration bool sort.Slice(sortedATimes, func(i, j int) bool { return sortedATimes[i].Before(sortedATimes[j]) }) + // start with most recent activationTime lower or equal to sTime + for i, aT := range sortedATimes { + if !aT.After(sTime) || i == 0 { + continue + } + sortedATimes = sortedATimes[i-1:] + } + // only for duration we will have multiple activationTimes + if !isDuration { + sortedATimes = sortedATimes[:1] + } + var usageSIdx, usageEIdx time.Duration + for i, at := range sortedATimes { + if sTime.Before(at) { + usageSIdx = at.Sub(sTime) + } + if i != len(sortedATimes)-1 { // not the last one + usageEIdx = sortedATimes[i+1].Sub(sTime) + } else { + usageEIdx = usage + } + // Sort the rates based on their Weight + sort.Slice(rtIdx[at], func(i, j int) bool { + return rtIdx[at][i].Weight < rtIdx[at][j].Weight + }) + var rtIcmts []*engine.RateSIncrement + for _, rt := range rtIdx[at] { + for j, ivlRt := range rt.IntervalRates { + if ivlRt.IntervalStart > usageEIdx { + break + } + if j != len(rt.IntervalRates)-1 && + rt.IntervalRates[j+1].IntervalStart <= usageSIdx { // not the last one + // the next intervalStat is still good for usageSIdx, no need of adding the current one + continue + } + // ready to add the increment + rtIcmts = append(rtIcmts, + &engine.RateSIncrement{Rate: rt, IntervalRateIndex: j}) + } + } + ordRts = append(ordRts, &engine.RateSInterval{Increments: rtIcmts}) + } return } -*/ diff --git a/rates/librates_test.go b/rates/librates_test.go index 465dae10d..ca342f282 100644 --- a/rates/librates_test.go +++ b/rates/librates_test.go @@ -18,138 +18,54 @@ along with this program. If not, see package rates -/* +import ( + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + func TestOrderRatesOnIntervals(t *testing.T) { - allRts := map[time.Duration][]*engine.Rate{ - time.Duration(0): { - &engine.Rate{ - ID: "RATE0", + rt0 := &engine.Rate{ + ID: "RATE0", + Weight: 0, + IntervalRates: []*engine.IntervalRate{ + { IntervalStart: time.Duration(0), }, - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - &engine.Rate{ - ID: "RATE50", - IntervalStart: time.Duration(0), - Weight: 50, - }, }, } - expOrdered := []*engine.Rate{ - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - } - if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { - t.Errorf("expecting: %s\n, received: %s", - utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) - } - allRts = map[time.Duration][]*engine.Rate{ - time.Duration(1 * time.Minute): { - &engine.Rate{ - ID: "RATE30", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 30, - }, - &engine.Rate{ - ID: "RATE70", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 70, - }, - }, - time.Duration(0): { - &engine.Rate{ - ID: "RATE0", + rt0.Compile() + rtChristmas := &engine.Rate{ + ID: "RT_CHRISTMAS", + ActivationStart: "* * 24 12 *", + Weight: 50, + IntervalRates: []*engine.IntervalRate{ + { IntervalStart: time.Duration(0), }, - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - &engine.Rate{ - ID: "RATE50", - IntervalStart: time.Duration(0), - Weight: 50, + }, + } + rtChristmas.Compile() + allRts := []*engine.Rate{rt0, rtChristmas} + expOrdered := []*engine.RateSInterval{ + { + Increments: []*engine.RateSIncrement{ + { + Rate: rt0, + IntervalRateIndex: 0, + }, }, }, } - expOrdered = []*engine.Rate{ - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - &engine.Rate{ - ID: "RATE70", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 70, - }, - } - if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { - t.Errorf("expecting: %s\n, received: %s", - utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) - } - allRts = map[time.Duration][]*engine.Rate{ - time.Duration(1 * time.Minute): { - &engine.Rate{ - ID: "RATE30", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 30, - }, - &engine.Rate{ - ID: "RATE70", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 70, - }, - }, - time.Duration(2 * time.Minute): { - &engine.Rate{ - ID: "RATE0", - IntervalStart: time.Duration(2 * time.Minute), - }, - }, - time.Duration(0): { - &engine.Rate{ - ID: "RATE0", - IntervalStart: time.Duration(0), - }, - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - &engine.Rate{ - ID: "RATE50", - IntervalStart: time.Duration(0), - Weight: 50, - }, - }, - } - expOrdered = []*engine.Rate{ - &engine.Rate{ - ID: "RATE100", - IntervalStart: time.Duration(0), - Weight: 100, - }, - &engine.Rate{ - ID: "RATE70", - IntervalStart: time.Duration(1 * time.Minute), - Weight: 70, - }, - &engine.Rate{ - ID: "RATE0", - IntervalStart: time.Duration(2 * time.Minute), - }, - } - if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { + // + // time.Date(2020, time.December, 23, 23, 59, 05, 0, time.UTC), + if ordRts := orderRatesOnIntervals( + allRts, time.Date(2020, time.June, 28, 18, 56, 05, 0, time.UTC), + time.Duration(2*time.Minute), true); !reflect.DeepEqual(expOrdered, ordRts) { t.Errorf("expecting: %s\n, received: %s", utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) } } -*/