Files
cgrates/engine/timespans.go
2014-02-27 16:54:11 +02:00

457 lines
13 KiB
Go

/*
Rating system designed to be used in VoIP Carriers World
Copyright (C) 2013 ITsysCOM
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package engine
import (
//"fmt"
"time"
"github.com/cgrates/cgrates/utils"
)
/*
A unit in which a call will be split that has a specific price related interval attached to it.
*/
type TimeSpan struct {
TimeStart, TimeEnd time.Time
Cost float64
ratingInfo *RatingInfo
RateInterval *RateInterval
CallDuration time.Duration // the call duration so far till TimeEnd
Increments Increments
MatchedSubject, MatchedPrefix string
}
type Increment struct {
Duration time.Duration
Cost float64
BalanceInfo *BalanceInfo // need more than one for minutes with cost
BalanceRateInterval *RateInterval
MinuteInfo *MinuteInfo
paid bool
}
// Holds the minute information related to a specified timespan
type MinuteInfo struct {
DestinationId string
Quantity float64
//Price float64
}
// Holds information about the balance that made a specific payment
type BalanceInfo struct {
MinuteBalanceUuid string
MoneyBalanceUuid string
AccountId string // used when debited from shared balance
}
type TimeSpans []*TimeSpan
func (timespans *TimeSpans) RemoveOverlapedFromIndex(index int) {
tss := *timespans
ts := tss[index]
endOverlapIndex := index
for i := index + 1; i < len(tss); i++ {
if tss[i].TimeEnd.Before(ts.TimeEnd) || tss[i].TimeEnd.Equal(ts.TimeEnd) {
endOverlapIndex = i
} else if tss[i].TimeStart.Before(ts.TimeEnd) {
tss[i].TimeStart = ts.TimeEnd
break
}
}
if endOverlapIndex > index {
newSliceEnd := len(tss) - (endOverlapIndex - index)
// delete overlapped
copy(tss[index+1:], tss[endOverlapIndex+1:])
for i := newSliceEnd; i < len(tss); i++ {
tss[i] = nil
}
*timespans = tss[:newSliceEnd]
return
}
*timespans = tss
}
// The paidTs will replace the timespans that are exactly under them from the reciver list
func (timespans *TimeSpans) OverlapWithTimeSpans(paidTs TimeSpans, newTs *TimeSpan, index int) bool {
tss := *timespans
// calculate overlaped timespans
var paidDuration time.Duration
for _, pts := range paidTs {
paidDuration += pts.GetDuration()
}
if paidDuration > 0 {
// we must add the rest of the current ts to the remaingTs
var remainingTs []*TimeSpan
overlapStartIndex := index
if newTs != nil {
remainingTs = append(remainingTs, newTs)
overlapStartIndex += 1
}
for tsi := overlapStartIndex; tsi < len(tss); tsi++ {
remainingTs = append(remainingTs, tss[tsi])
}
overlapEndIndex := 0
for i, rts := range remainingTs {
if paidDuration >= rts.GetDuration() {
paidDuration -= rts.GetDuration()
} else {
if paidDuration > 0 {
// this ts was not fully paid
fragment := rts.SplitByDuration(paidDuration)
paidTs = append(paidTs, fragment)
}
// find the end position in tss
overlapEndIndex = overlapStartIndex + i
break
}
// find the end position in tss
overlapEndIndex = overlapStartIndex + i
}
// delete from index to current
if overlapEndIndex == len(tss)-1 {
tss = tss[:overlapStartIndex]
} else {
if overlapEndIndex+1 < len(tss) {
tss = append(tss[:overlapStartIndex], tss[overlapEndIndex+1:]...)
}
}
// append the timespans to outer tss
for i, pts := range paidTs {
tss = append(tss, nil)
copy(tss[overlapStartIndex+i+1:], tss[overlapStartIndex+i:])
tss[overlapStartIndex+i] = pts
}
*timespans = tss
return true
}
*timespans = tss
return false
}
func (incr *Increment) Clone() *Increment {
nIncr := &Increment{
Duration: incr.Duration,
Cost: incr.Cost,
BalanceRateInterval: incr.BalanceRateInterval,
MinuteInfo: incr.MinuteInfo,
BalanceInfo: incr.BalanceInfo,
}
return nIncr
}
type Increments []*Increment
func (incs Increments) GetTotalCost() float64 {
cost := 0.0
for _, increment := range incs {
cost += increment.Cost
}
return cost
}
// Returns the duration of the timespan
func (ts *TimeSpan) GetDuration() time.Duration {
return ts.TimeEnd.Sub(ts.TimeStart)
}
// Returns true if the given time is inside timespan range.
func (ts *TimeSpan) Contains(t time.Time) bool {
return t.After(ts.TimeStart) && t.Before(ts.TimeEnd)
}
/*
Will set the interval as spans's interval if new Weight is lower then span's interval Weight
or if the Weights are equal and new price is lower then spans's interval price
*/
func (ts *TimeSpan) SetRateInterval(i *RateInterval) {
if ts.RateInterval == nil || ts.RateInterval.Weight < i.Weight {
ts.RateInterval = i
return
}
iPrice, _, _ := i.GetRateParameters(ts.GetGroupStart())
tsPrice, _, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart())
if ts.RateInterval.Weight == i.Weight && iPrice < tsPrice {
ts.RateInterval = i
}
}
// Returns the cost of the timespan according to the relevant cost interval.
// It also sets the Cost field of this timespan (used for refund on session
// manager debit loop where the cost cannot be recalculated)
func (ts *TimeSpan) getCost() float64 {
if len(ts.Increments) == 0 {
if ts.RateInterval == nil {
return 0
}
cost := ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart())
ts.Cost = utils.Round(cost, ts.RateInterval.Rating.RoundingDecimals, ts.RateInterval.Rating.RoundingMethod)
return ts.Cost
} else {
return ts.Increments[0].Cost * float64(len(ts.Increments))
}
}
func (ts *TimeSpan) createIncrementsSlice() {
if ts.RateInterval == nil {
return
}
ts.Increments = make([]*Increment, 0)
// create rated units series
_, rateIncrement, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart())
// we will use the cost calculated cost and devide by nb of increments
// because ts cost is rounded
//incrementCost := rate / rateUnit.Seconds() * rateIncrement.Seconds()
nbIncrements := int(ts.GetDuration() / rateIncrement)
incrementCost := ts.getCost() / float64(nbIncrements)
incrementCost = utils.Round(incrementCost, roundingDecimals, utils.ROUNDING_MIDDLE) // just get rid of the extra decimals
for s := 0; s < nbIncrements; s++ {
inc := &Increment{
Duration: rateIncrement,
Cost: incrementCost,
BalanceInfo: &BalanceInfo{},
}
ts.Increments = append(ts.Increments, inc)
}
// put the rounded cost back in timespan
ts.Cost = incrementCost * float64(nbIncrements)
}
// returns whether the timespan has all increments marked as paid and if not
// it also returns the first unpaied increment
func (ts *TimeSpan) IsPaid() (bool, int) {
if len(ts.Increments) == 0 {
return false, 0
}
for incrementIndex, increment := range ts.Increments {
if !increment.paid {
return false, incrementIndex
}
}
return true, len(ts.Increments)
}
/*
Splits the given timespan according to how it relates to the interval.
It will modify the endtime of the received timespan and it will return
a new timespan starting from the end of the received one.
The interval will attach itself to the timespan that overlaps the interval.
*/
func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) {
//Logger.Debug("here: ", ts, " +++ ", i)
// if the span is not in interval return nil
if !(i.Contains(ts.TimeStart, false) || i.Contains(ts.TimeEnd, true)) {
//Logger.Debug("Not in interval")
return
}
//Logger.Debug(fmt.Sprintf("TS: %+v", ts))
// split by GroupStart
if i.Rating != nil {
i.Rating.Rates.Sort()
for _, rate := range i.Rating.Rates {
// Logger.Debug(fmt.Sprintf("Rate: %+v", rate))
if ts.GetGroupStart() < rate.GroupIntervalStart && ts.GetGroupEnd() > rate.GroupIntervalStart {
// Logger.Debug(fmt.Sprintf("Splitting"))
ts.SetRateInterval(i)
splitTime := ts.TimeStart.Add(rate.GroupIntervalStart - ts.GetGroupStart())
nts = &TimeSpan{
TimeStart: splitTime,
TimeEnd: ts.TimeEnd,
}
nts.copyRatingInfo(ts)
ts.TimeEnd = splitTime
nts.SetRateInterval(i)
nts.CallDuration = ts.CallDuration
ts.SetNewCallDuration(nts)
// Logger.Debug(fmt.Sprintf("Group splitting: %+v %+v", ts, nts))
return
}
}
}
// if the span is enclosed in the interval try to set as new interval and return nil
if i.Contains(ts.TimeStart, false) && i.Contains(ts.TimeEnd, true) {
//Logger.Debug("All in interval")
ts.SetRateInterval(i)
return
}
// if only the start time is in the interval split the interval to the right
if i.Contains(ts.TimeStart, false) {
//Logger.Debug("Start in interval")
splitTime := i.getRightMargin(ts.TimeStart)
ts.SetRateInterval(i)
if splitTime == ts.TimeStart || splitTime.Equal(ts.TimeEnd) {
return
}
nts = &TimeSpan{
TimeStart: splitTime,
TimeEnd: ts.TimeEnd,
}
nts.copyRatingInfo(ts)
ts.TimeEnd = splitTime
nts.CallDuration = ts.CallDuration
ts.SetNewCallDuration(nts)
// Logger.Debug(fmt.Sprintf("right: %+v %+v", ts, nts))
return
}
// if only the end time is in the interval split the interval to the left
if i.Contains(ts.TimeEnd, true) {
//Logger.Debug("End in interval")
//tmpTime := time.Date(ts.TimeStart.)
splitTime := i.getLeftMargin(ts.TimeEnd)
splitTime = utils.CopyHour(splitTime, ts.TimeStart)
if splitTime.Equal(ts.TimeEnd) {
return
}
nts = &TimeSpan{
TimeStart: splitTime,
TimeEnd: ts.TimeEnd,
}
nts.copyRatingInfo(ts)
ts.TimeEnd = splitTime
nts.SetRateInterval(i)
nts.CallDuration = ts.CallDuration
ts.SetNewCallDuration(nts)
// Logger.Debug(fmt.Sprintf("left: %+v %+v", ts, nts))
return
}
return
}
// Split the timespan at the given increment start
func (ts *TimeSpan) SplitByIncrement(index int) *TimeSpan {
if index <= 0 || index >= len(ts.Increments) {
return nil
}
timeStart := ts.GetTimeStartForIncrement(index)
newTs := &TimeSpan{
RateInterval: ts.RateInterval,
TimeStart: timeStart,
TimeEnd: ts.TimeEnd,
}
newTs.copyRatingInfo(ts)
newTs.CallDuration = ts.CallDuration
ts.TimeEnd = timeStart
newTs.Increments = ts.Increments[index:]
ts.Increments = ts.Increments[:index]
ts.SetNewCallDuration(newTs)
return newTs
}
// Split the timespan at the given second
func (ts *TimeSpan) SplitByDuration(duration time.Duration) *TimeSpan {
if duration <= 0 || duration >= ts.GetDuration() {
return nil
}
timeStart := ts.TimeStart.Add(duration)
newTs := &TimeSpan{
RateInterval: ts.RateInterval,
TimeStart: timeStart,
TimeEnd: ts.TimeEnd,
}
newTs.copyRatingInfo(ts)
newTs.CallDuration = ts.CallDuration
ts.TimeEnd = timeStart
// split the increment
for incrIndex, incr := range ts.Increments {
if duration-incr.Duration >= 0 {
duration -= incr.Duration
} else {
splitIncrement := ts.Increments[incrIndex].Clone()
splitIncrement.Duration -= duration
ts.Increments[incrIndex].Duration = duration
newTs.Increments = Increments{splitIncrement}
if incrIndex < len(ts.Increments)-1 {
newTs.Increments = append(newTs.Increments, ts.Increments[incrIndex+1:]...)
}
ts.Increments = ts.Increments[:incrIndex+1]
break
}
}
ts.SetNewCallDuration(newTs)
return newTs
}
// Splits the given timespan on activation period's activation time.
func (ts *TimeSpan) SplitByRatingPlan(rp *RatingInfo) (newTs *TimeSpan) {
if !ts.Contains(rp.ActivationTime) {
return nil
}
newTs = &TimeSpan{
TimeStart: rp.ActivationTime,
TimeEnd: ts.TimeEnd,
}
newTs.copyRatingInfo(ts)
newTs.CallDuration = ts.CallDuration
ts.TimeEnd = rp.ActivationTime
ts.SetNewCallDuration(newTs)
// Logger.Debug(fmt.Sprintf("RP SPLITTING: %+v %+v", ts, newTs))
return
}
// Returns the starting time of this timespan
func (ts *TimeSpan) GetGroupStart() time.Duration {
s := ts.CallDuration - ts.GetDuration()
if s < 0 {
s = 0
}
return s
}
func (ts *TimeSpan) GetGroupEnd() time.Duration {
return ts.CallDuration
}
// sets the CallDuration attribute to reflect new timespan
func (ts *TimeSpan) SetNewCallDuration(nts *TimeSpan) {
d := ts.CallDuration - nts.GetDuration()
if d < 0 {
d = 0
}
ts.CallDuration = d
}
func (nts *TimeSpan) copyRatingInfo(ts *TimeSpan) {
if ts.ratingInfo == nil {
return
}
nts.ratingInfo = ts.ratingInfo
nts.MatchedSubject = ts.ratingInfo.MatchedSubject
nts.MatchedPrefix = ts.ratingInfo.MatchedPrefix
}
// returns a time for the specified second in the time span
func (ts *TimeSpan) GetTimeStartForIncrement(index int) time.Time {
return ts.TimeStart.Add(time.Duration(int64(index) * ts.Increments[0].Duration.Nanoseconds()))
}
func (ts *TimeSpan) RoundToDuration(duration time.Duration) {
if duration < ts.GetDuration() {
duration = utils.RoundTo(duration, ts.GetDuration())
}
if duration > ts.GetDuration() {
initialDuration := ts.GetDuration()
ts.TimeEnd = ts.TimeStart.Add(duration)
ts.CallDuration = ts.CallDuration + (duration - initialDuration)
}
}