diff --git a/rates/librates.go b/rates/librates.go index 4f267a052..f06266de6 100644 --- a/rates/librates.go +++ b/rates/librates.go @@ -25,19 +25,56 @@ import ( "github.com/cgrates/cgrates/engine" ) +func newRatesWithWinner(rt *engine.Rate) (rts *ratesWithWinner) { + rts = &ratesWithWinner{ + rts: make(map[string]*engine.Rate), + } + rts.add(rt) + return +} + +// ratesWithWinner computes always the winner based on highest Weight +type ratesWithWinner struct { + rts map[string]*engine.Rate + rt *engine.Rate +} + +//add will add the rate to the rates +func (rs *ratesWithWinner) add(rt *engine.Rate) { + rs.rts[rt.ID] = rt + if rs.rt == nil || rs.rt.Weight < rt.Weight { + rs.rt = rt + } +} + +// winner returns the rate with the highest Weight +func (rs *ratesWithWinner) winner() *engine.Rate { + return rs.rt +} + +// has tests if the rateID is present in rates +func (rs *ratesWithWinner) has(rtID string) (has bool) { + _, has = rs.rts[rtID] + return +} + // 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, usage time.Duration, isDuration bool) (ordRts []*engine.RateSInterval) { +func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, usage time.Duration, isDuration bool, aTimeVerbosity int) (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][]*engine.Rate) // map[ActivationTime][]*engine.Rate + rtIdx := make(map[time.Time]*ratesWithWinner) // map[ActivationTime]*ratesWithWinner for _, rt := range aRts { nextRunTime := rt.NextActivationTime(cronSTime) if nextRunTime.After(endTime) { continue } - rtIdx[nextRunTime] = append(rtIdx[nextRunTime], rt) + if _, hasKey := rtIdx[nextRunTime]; !hasKey { + rtIdx[nextRunTime] = newRatesWithWinner(rt) + continue + } + rtIdx[nextRunTime].add(rt) } // sort the activation times sortedATimes := make([]time.Time, len(rtIdx)) @@ -56,10 +93,94 @@ func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, usage time.Dura } sortedATimes = sortedATimes[i-1:] } - // only for duration we will have multiple activationTimes - if !isDuration { - sortedATimes = sortedATimes[:1] + // finalize the sortedATimes + if isDuration { + // add all the possible ActivationTimes from cron expressions + for i := 0; i < aTimeVerbosity; i++ { + //fmt.Printf("aTimeVerbosity run: %d, sortedATimes: %+v\n", i, sortedATimes) + var altered bool + for _, aTime := range sortedATimes { + //fmt.Printf("checking aTime: %+v\n", aTime) + // each of the rates will be checked against activationTime + for _, rt := range aRts { + nextRunTime := rt.NextActivationTime(aTime) + //fmt.Printf("rate: %s, rt.ActivationStart: %s, on aTime: %+v nextRunTime: %+v\n", rt.ID, rt.ActivationStart, aTime, nextRunTime) + if nextRunTime.After(endTime) { + //fmt.Println("after endTime") + continue + } + if rtMp, hasRunTime := rtIdx[nextRunTime]; hasRunTime { + if rtMp.has(rt.ID) { // rate was already captured + //fmt.Printf("already captured!\n") + continue + } + } + // try to see if it fits in the activation time list + for sTimeIdx, sATime := range sortedATimes { + //fmt.Printf("sTimeIdx: %d, sAtime: %+v\n", sTimeIdx, sATime) + if sTimeIdx == 0 { // ignore the first one + //fmt.Println("first interval") + continue + } + if nextRunTime.After(sATime) { + if sTimeIdx != len(sortedATimes)-1 { + //fmt.Println("not the last one and higher") + continue + } + // last one and higher + if rtIdx[sortedATimes[sTimeIdx]].winner().ID == rt.ID { // activated in last slot + //fmt.Println("activated in last slot") + continue + } + } + if rtIdx[sortedATimes[sTimeIdx-1]].has(rt.ID) { // activated in previous slot + //fmt.Println("activated in previous slot") + continue + } else { + //fmt.Printf("rtIdx: %+v, has no rt with id: %s\n", rtIdx, rt.ID) + } + // Anything passing here should be a winner + if nextRunTime == sATime { + //fmt.Println("already in time, adding to rates") + rtIdx[nextRunTime].add(rt) + altered = true + break + } + if _, has := rtIdx[nextRunTime]; !has { + rtIdx[nextRunTime] = newRatesWithWinner(rt) + } else { + rtIdx[nextRunTime].add(rt) + } + if sTimeIdx == len(sortedATimes)-1 { // last index, higher than the last ActivationTime + //fmt.Println("adding as last") + sortedATimes = append(sortedATimes, nextRunTime) + } else { // Insert before current index + //fmt.Printf("insert in betwee, have before: %+v", sortedATimes) + sortedATimes = append(sortedATimes, time.Time{}) + copy(sortedATimes[sTimeIdx+1:], sortedATimes[sTimeIdx:]) + sortedATimes[sTimeIdx] = nextRunTime + //fmt.Printf(" have after: %+v\n", sortedATimes) + } + altered = true + break + } + + } + if altered { // start again from first aTime + break + } + + } + if !altered { // no change was done in a complete iteration, no need of further lookups + break + } + } + + } else { // only for duration we will have multiple activationTimes + sortedATimes = sortedATimes[:1] // only first ordered activationTime is considered for units } + //fmt.Printf("sortedTimes: %+v\n", sortedATimes) + // add the Intervals and Increments var usageSIdx, usageEIdx time.Duration for i, at := range sortedATimes { if sTime.Before(at) { @@ -70,26 +191,21 @@ func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, usage time.Dura } 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 - }) + // append the valid increments 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}) + rt := rtIdx[at].winner() + 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}) } diff --git a/rates/librates_test.go b/rates/librates_test.go index c5cfe8da2..c0da42e76 100644 --- a/rates/librates_test.go +++ b/rates/librates_test.go @@ -60,12 +60,72 @@ func TestOrderRatesOnIntervals(t *testing.T) { }, }, } - // - // 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) { + time.Duration(2*time.Minute), true, 10); !reflect.DeepEqual(expOrdered, ordRts) { t.Errorf("expecting: %s\n, received: %s", utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) } + + expOrdered = []*engine.RateSInterval{ + { + Increments: []*engine.RateSIncrement{ + { + Rate: rt0, + IntervalRateIndex: 0, + }, + }, + }, + { + Increments: []*engine.RateSIncrement{ + { + Rate: rtChristmas, + IntervalRateIndex: 0, + }, + }, + }, + } + if ordRts := orderRatesOnIntervals( + allRts, time.Date(2020, time.December, 23, 23, 59, 05, 0, time.UTC), + time.Duration(2*time.Minute), true, 10); !reflect.DeepEqual(expOrdered, ordRts) { + t.Errorf("expecting: %s\n, received: %s", + utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) + } + expOrdered = []*engine.RateSInterval{ + { + Increments: []*engine.RateSIncrement{ + { + Rate: rt0, + IntervalRateIndex: 0, + }, + }, + }, + { + Increments: []*engine.RateSIncrement{ + { + Rate: rtChristmas, + IntervalRateIndex: 0, + }, + }, + }, + { + Increments: []*engine.RateSIncrement{ + { + Rate: rt0, + IntervalRateIndex: 0, + }, + }, + }, + } + /* + fmt.Println("Third test") + if ordRts := orderRatesOnIntervals( + allRts, time.Date(2020, time.December, 23, 23, 59, 05, 0, time.UTC), + time.Duration(2*time.Minute), true, 10); !reflect.DeepEqual(expOrdered, ordRts) { + t.Errorf("expecting: %s\n, received: %s", + utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) + } + */ + }