EventCost.Trim in middle with tests, libeventcost

This commit is contained in:
DanB
2017-05-25 19:16:15 +02:00
parent 2341ea95aa
commit 8421d74d85
4 changed files with 802 additions and 566 deletions

View File

@@ -18,141 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
package engine
import (
"errors"
"time"
"github.com/cgrates/cgrates/utils"
)
type RatingFilters map[string]RatingMatchedFilters // so we can define search methods
// GetWithSet attempts to retrieve the UUID of a matching data or create a new one
func (rfs RatingFilters) GetUUIDWithSet(rmf RatingMatchedFilters) string {
if rmf == nil || len(rmf) == 0 {
return ""
}
for k, v := range rfs {
if v.Equals(rmf) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
rfs[uuid] = rmf
return uuid
}
func (rfs RatingFilters) Clone() (cln RatingFilters) {
cln = make(RatingFilters, len(rfs))
for k, v := range rfs {
cln[k] = v.Clone()
}
return
}
type Rating map[string]*RatingUnit
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crus Rating) GetUUIDWithSet(cru *RatingUnit) string {
if cru == nil {
return ""
}
for k, v := range crus {
if v.Equals(cru) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crus[uuid] = cru
return uuid
}
func (crus Rating) Clone() (cln Rating) {
cln = make(Rating, len(crus))
for k, v := range crus {
cln[k] = v.Clone()
}
return
}
type ChargedRates map[string]RateGroups
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crs ChargedRates) GetUUIDWithSet(rg RateGroups) string {
if rg == nil || len(rg) == 0 {
return ""
}
for k, v := range crs {
if v.Equals(rg) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crs[uuid] = rg
return uuid
}
func (crs ChargedRates) Clone() (cln ChargedRates) {
cln = make(ChargedRates, len(crs))
for k, v := range crs {
cln[k] = v.Clone()
}
return
}
type ChargedTimings map[string]*ChargedTiming
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cts ChargedTimings) GetUUIDWithSet(ct *ChargedTiming) string {
if ct == nil {
return ""
}
for k, v := range cts {
if v.Equals(ct) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cts[uuid] = ct
return uuid
}
func (cts ChargedTimings) Clone() (cln ChargedTimings) {
cln = make(ChargedTimings, len(cts))
for k, v := range cts {
cln[k] = v.Clone()
}
return
}
type Accounting map[string]*BalanceCharge
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cbs Accounting) GetUUIDWithSet(cb *BalanceCharge) string {
if cb == nil {
return ""
}
for k, v := range cbs {
if v.Equals(cb) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cbs[uuid] = cb
return uuid
}
func (cbs Accounting) Clone() (cln Accounting) {
cln = make(Accounting, len(cbs))
for k, v := range cbs {
cln[k] = v.Clone()
}
return
}
func NewBareEventCost() *EventCost {
return &EventCost{
Rating: make(Rating),
@@ -433,6 +304,9 @@ func (ec *EventCost) AsCallCost() *CallCost {
// ratingGetUUIDFomEventCost retrieves UUID based on data from another EventCost
func (ec *EventCost) ratingGetUUIDFomEventCost(oEC *EventCost, oRatingUUID string) string {
if oRatingUUID == "" {
return ""
}
oCIlRating := oEC.Rating[oRatingUUID].Clone() // clone so we don't influence the original data
oCIlRating.TimingUUID = ec.Timings.GetUUIDWithSet(oEC.Timings[oCIlRating.TimingUUID])
oCIlRating.RatingFiltersUUID = ec.RatingFilters.GetUUIDWithSet(oEC.RatingFilters[oCIlRating.RatingFiltersUUID])
@@ -442,11 +316,12 @@ func (ec *EventCost) ratingGetUUIDFomEventCost(oEC *EventCost, oRatingUUID strin
// accountingGetUUIDFromEventCost retrieves UUID based on data from another EventCost
func (ec *EventCost) accountingGetUUIDFromEventCost(oEC *EventCost, oBalanceChargeUUID string) string {
if oBalanceChargeUUID == "" {
return ""
}
oBC := oEC.Accounting[oBalanceChargeUUID].Clone()
oBC.RatingUUID = ec.ratingGetUUIDFomEventCost(oEC, oBC.RatingUUID)
if oBC.ExtraChargeUUID != "" {
oBC.ExtraChargeUUID = ec.accountingGetUUIDFromEventCost(oEC, oBC.ExtraChargeUUID)
}
oBC.ExtraChargeUUID = ec.accountingGetUUIDFromEventCost(oEC, oBC.ExtraChargeUUID)
return ec.Accounting.GetUUIDWithSet(oBC)
}
@@ -488,6 +363,7 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error
if ec.Usage == nil {
ec.ComputeUsage()
}
origECUsage := ec.ComputeUsage()
if atUsage >= *ec.Usage {
return // no trim
}
@@ -500,295 +376,141 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error
ec.AccountSummary = srplusEC.AccountSummary.Clone()
return // trim all, fresh EC with 0 usage
}
/*
var lastActiveCIlIdx *int // marks last index which should stay with ec
for i, cIl := range ec.Charges {
if cIl.ecUsageIdx == nil {
ec.ComputeEventCostUsageIndexes()
}
if *cIl.ecUsageIdx >= atUsage {
lastActiveCIlIdx = utils.IntPointer(i - 1)
srplusEC = NewBareEventCost()
srplusEC.CGRID = ec.CGRID
srplusEC.RunID = ec.RunID
srplusEC.StartTime = ec.StartTime
srplusEC.AccountSummary = ec.AccountSummary.Clone()
var lastActiveCIlIdx *int // marks last index which should stay with ec
for i, cIl := range ec.Charges {
if cIl.ecUsageIdx == nil {
ec.ComputeEventCostUsageIndexes()
}
if cIl.usage == nil {
ec.ComputeUsage()
}
if *cIl.ecUsageIdx+*cIl.TotalUsage() >= atUsage {
lastActiveCIlIdx = utils.IntPointer(i)
break
}
}
if lastActiveCIlIdx == nil {
return nil, errors.New("cannot find last active ChargingInterval")
}
lastActiveCIl := ec.Charges[*lastActiveCIlIdx]
if *lastActiveCIl.ecUsageIdx >= atUsage {
return nil, errors.New("failed detecting last active ChargingInterval")
} else if lastActiveCIl.CompressFactor == 0 {
return nil, errors.New("ChargingInterval with 0 compressFactor")
}
srplusEC.Charges = ec.Charges[*lastActiveCIlIdx+1:]
ec.Charges = ec.Charges[:*lastActiveCIlIdx+1]
if lastActiveCIl.CompressFactor != 1 { // Split based on compress factor if needed
var laCF int
for ciCnt := 1; ciCnt <= lastActiveCIl.CompressFactor; ciCnt++ {
if *lastActiveCIl.ecUsageIdx+
time.Duration(lastActiveCIl.usage.Nanoseconds()*int64(ciCnt)) > atUsage {
laCF = ciCnt
break
}
}
if lastActiveCIlIdx == nil {
return
if laCF == 0 {
return nil, errors.New("cannot detect last active CompressFactor in ChargingInterval")
}
if *lastActiveCIlIdx == -1 { // trim full usage
srplusEC = NewBareEventCost()
*srplusEC = *ec // no need of cloning since we will not keep info in ec
ec = NewBareEventCost()
ec.CGRID = srplusEC.CGRID
ec.RunID = srplusEC.RunID
return
if laCF != lastActiveCIl.CompressFactor {
srplsCIl := lastActiveCIl.Clone()
srplsCIl.CompressFactor = lastActiveCIl.CompressFactor - laCF
srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...) // prepend surplus CIl
lastActiveCIl.CompressFactor = laCF // correct compress factor
}
/*
lastActiveCIl := ec.Charges[lastActiveCIlIdx]
if lastActiveCI.ecUsageIdx >= atUsage {
return nil, errors.New("failed detecting last active ChargingInterval")
} else if lastActiveCI.CompressFactor == 0 {
return nil, errors.New("ChargingInterval with 0 compressFactor")
}
srplsCIl := new(ChargingInterval)
srplsCIl.RatingUUID = lastActiveCIl.RatingUUID
if lastActiveCI.CompressFactor != 1 {
var laCF int
for ciCnt := 1; ciCnt <= lastActiveCI.CompressFactor; ciCnt++ {
if *lastActiveCI.ecUsageIdx.Add(
time.Duration(lastActiveCI.Usage.Nanoseconds() * int64(ciCnt))) > atUsage {
laCF = ciCnt
break
}
}
if laCF == 0 {
return nil, errors.New("cannot detect last active CompressFactor in ChargingInterval")
}
lastActiveCIl.CompressFactor = laCF // this factor will stay uncompressed
}
var lastActiveCItIdx *int
cIlUIdx := ec.Charges[lastActiveCIlIdx].ecUsageIdx
for i, cIt := range ec.Charges[lastActiveCIlIdx].Increments {
if cIlUIdx.Add(cIt.Usage) > atUsage {
lastActiveCItIdx = utils.IntPointer(i)
break
}
}
if lastActiveCItIdx == nil { // bug in increments
return nil, errors.New("no active increment found")
}
ec.ResetCounters() // avoid stale counters
var ciUncompressed bool // marks whether we needed to uncomrpess the last ChargingInterval
if ec.Charges[lastActiveCIlIdx].
srplusEC = NewBareEventCost()
srplusEC.CGRID = ec.CGRID
srplusEC.RunID = ec.RunID
var laCIlIncrmts []*ChargingIncrement // surplus increments in the last active ChargingInterval
for _, cIl := range ec.Charges[lastActiveCIlIdx].Increments[*lastActiveCItIdx+1:] {
laCIlIncrmts = append(laCIlIncrmts, cIl)
}
srplusEC.Charges = []*ChargingInterval{}
ec.Charges[lastActiveCIlIdx].Increments = ec.Charges[lastActiveCIlIdx].Increments[:*lastActiveCItIdx+1] // remove srplusEC increments out of last active CIl
for _, cIl := range ec.Charges[lastActiveCIlIdx+1:] {
}
*/
return
}
// ChargingInterval represents one interval out of Usage providing charging info
// eg: PEAK vs OFFPEAK
type ChargingInterval struct {
RatingUUID string // reference to RatingUnit
Increments []*ChargingIncrement // specific increments applied to this interval
CompressFactor int
usage *time.Duration // cache usage computation for this interval
ecUsageIdx *time.Duration // computed value of totalUsage at the starting of the interval
cost *float64 // cache cost calculation on this interval
}
// PartiallyEquals does not compare CompressFactor, usefull for Merge
func (cIl *ChargingInterval) PartiallyEquals(oCIl *ChargingInterval) (equals bool) {
if equals = cIl.RatingUUID == oCIl.RatingUUID &&
len(cIl.Increments) == len(oCIl.Increments); !equals {
return
}
for i := range cIl.Increments {
if !cIl.Increments[i].Equals(oCIl.Increments[i]) {
equals = false
atUsage = atUsage - time.Duration(lastActiveCIl.ecUsageIdx.Nanoseconds()*int64(lastActiveCIl.CompressFactor)) // remaining duration to cover in increments
// find out last increment covering duration
var lastActiveCItIdx *int
var incrementsUsage time.Duration
for i, cIt := range lastActiveCIl.Increments {
incrementsUsage += cIt.TotalUsage()
if incrementsUsage > atUsage {
lastActiveCItIdx = utils.IntPointer(i)
break
}
}
return
}
if lastActiveCItIdx == nil { // bug in increments
return nil, errors.New("no active increment found")
}
lastActiveCIts := lastActiveCIl.Increments // so we can modify the reference in case we have surplus
lastIncrement := lastActiveCIts[*lastActiveCItIdx]
// Usage computes the total usage of this ChargingInterval, ignoring CompressFactor
func (cIl *ChargingInterval) Usage() *time.Duration {
if cIl.usage == nil {
var usage time.Duration
if lastIncrement.CompressFactor == 0 {
return nil, errors.New("empty compress factor in increment")
}
var srplsIncrements []*ChargingIncrement
if *lastActiveCItIdx < len(lastActiveCIl.Increments)-1 { // less that complete increments, have surplus
srplsIncrements = lastActiveCIts[*lastActiveCItIdx+1:]
lastActiveCIts = lastActiveCIts[:*lastActiveCItIdx+1]
}
var laItCF int
if lastIncrement.CompressFactor != 1 { // detect the increment covering the last part of usage
incrementsUsage -= lastIncrement.TotalUsage()
for cnt := 1; cnt <= lastIncrement.CompressFactor; cnt++ {
incrementsUsage += lastIncrement.Usage
if incrementsUsage >= atUsage {
laItCF = cnt
break
}
}
if laItCF == 0 {
return nil, errors.New("cannot detect last active CompressFactor in ChargingIncrement")
}
if laItCF != lastIncrement.CompressFactor {
srplsIncrement := lastIncrement.Clone()
srplsIncrement.CompressFactor = srplsIncrement.CompressFactor - laItCF
srplsIncrements = append([]*ChargingIncrement{srplsIncrement}, srplsIncrements...) // prepend the surplus out of compress
//lastIncrement.CompressFactor = laItCF
}
}
if len(srplsIncrements) != 0 { // partially covering, need trim
if lastActiveCIl.CompressFactor > 1 { // ChargingInterval not covering in full, need to split it
lastActiveCIl.CompressFactor -= 1
ec.Charges = append(ec.Charges, lastActiveCIl.Clone())
lastActiveCIl = ec.Charges[len(ec.Charges)-1]
lastActiveCIl.CompressFactor = 1
}
srplsCIl := lastActiveCIl.Clone()
srplsCIl.Increments = srplsIncrements
srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...)
lastActiveCIl.Increments = make([]*ChargingIncrement, len(lastActiveCIts))
for i, incr := range lastActiveCIts {
lastActiveCIl.Increments[i] = incr.Clone() // avoid pointer references to the other interval
}
if laItCF != 0 {
lastActiveCIl.Increments[len(lastActiveCIl.Increments)-1].CompressFactor = laItCF // correct the compressFactor for the last increment
}
}
ec.ResetCounters()
if usage := ec.ComputeUsage(); usage < atUsage {
return nil, errors.New("usage of EventCost smaller than requested")
}
srplusEC.ResetCounters()
srplusEC.StartTime = ec.StartTime.Add(ec.ComputeUsage())
if srplsUsage := srplusEC.ComputeUsage(); srplsUsage > origECUsage-atUsage {
return nil, errors.New("surplus EventCost too big")
}
// close surplus with missing cache
for _, cIl := range srplusEC.Charges {
cIl.RatingUUID = srplusEC.ratingGetUUIDFomEventCost(ec, cIl.RatingUUID)
for _, incr := range cIl.Increments {
usage += time.Duration(incr.Usage.Nanoseconds() * int64(incr.CompressFactor))
}
cIl.usage = &usage
}
return cIl.usage
}
// TotalUsage returns the total usage of this interval, considering compress factor
func (cIl *ChargingInterval) TotalUsage() (tu *time.Duration) {
usage := cIl.Usage()
if usage == nil {
return
}
tu = new(time.Duration)
*tu = time.Duration(usage.Nanoseconds() * int64(cIl.CompressFactor))
return
}
// EventCostUsageIndex publishes the value of ecUsageIdx
func (cIl *ChargingInterval) EventCostUsageIndex() *time.Duration {
return cIl.ecUsageIdx
}
// StartTime computes a StartTime based on EventCost.Start time and ecUsageIdx
func (cIl *ChargingInterval) StartTime(ecST time.Time) (st time.Time) {
if cIl.ecUsageIdx != nil {
st = ecST.Add(*cIl.ecUsageIdx)
}
return
}
// EndTime computes an EndTime based on ChargingInterval StartTime value and usage
func (cIl *ChargingInterval) EndTime(cIlST time.Time) (et time.Time) {
return cIlST.Add(time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor)))
}
// Cost computes the total cost on this ChargingInterval
func (cIl *ChargingInterval) Cost() float64 {
if cIl.cost == nil {
var cost float64
for _, incr := range cIl.Increments {
cost += incr.Cost * float64(incr.CompressFactor)
}
cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE)
cIl.cost = &cost
}
return *cIl.cost
}
// Clone returns a new instance of ChargingInterval with independent data
func (cIl *ChargingInterval) Clone() (cln *ChargingInterval) {
cln = new(ChargingInterval)
cln.RatingUUID = cIl.RatingUUID
cln.CompressFactor = cIl.CompressFactor
cln.Increments = make([]*ChargingIncrement, len(cIl.Increments))
for i, cIt := range cIl.Increments {
cln.Increments[i] = cIt.Clone()
}
return
}
// ChargingIncrement represents one unit charged inside an interval
type ChargingIncrement struct {
Usage time.Duration
Cost float64
BalanceChargeUUID string
CompressFactor int
}
func (cIt *ChargingIncrement) Equals(oCIt *ChargingIncrement) bool {
return cIt.Usage == oCIt.Usage &&
cIt.Cost == oCIt.Cost &&
cIt.BalanceChargeUUID == oCIt.BalanceChargeUUID &&
cIt.CompressFactor == oCIt.CompressFactor
}
func (cIt *ChargingIncrement) Clone() (cln *ChargingIncrement) {
cln = new(ChargingIncrement)
*cln = *cIt
return
}
// TotalUsage returns the total usage of the increment, considering compress factor
func (cIt *ChargingIncrement) TotalUsage() time.Duration {
return time.Duration(cIt.Usage.Nanoseconds() * int64(cIt.CompressFactor))
}
// BalanceCharge represents one unit charged to a balance
type BalanceCharge struct {
AccountID string // keep reference for shared balances
BalanceUUID string // balance charged
RatingUUID string // special price applied on this balance
Units float64 // number of units charged
ExtraChargeUUID string // used in cases when paying *voice with *monetary
}
func (bc *BalanceCharge) Equals(oBC *BalanceCharge) bool {
return bc.AccountID == oBC.AccountID &&
bc.BalanceUUID == oBC.BalanceUUID &&
bc.RatingUUID == oBC.RatingUUID &&
bc.Units == oBC.Units &&
bc.ExtraChargeUUID == oBC.ExtraChargeUUID
}
func (bc *BalanceCharge) Clone() *BalanceCharge {
clnBC := new(BalanceCharge)
*clnBC = *bc
return clnBC
}
type RatingMatchedFilters map[string]interface{}
func (rf RatingMatchedFilters) Equals(oRF RatingMatchedFilters) (equals bool) {
equals = true
for k := range rf {
if rf[k] != oRF[k] {
equals = false
break
incr.BalanceChargeUUID = srplusEC.accountingGetUUIDFromEventCost(ec, incr.BalanceChargeUUID)
}
}
return
}
func (rf RatingMatchedFilters) Clone() (cln map[string]interface{}) {
cln = make(map[string]interface{})
utils.Clone(rf, &cln)
return
}
// ChargedTiming represents one timing attached to a charge
type ChargedTiming struct {
Years utils.Years
Months utils.Months
MonthDays utils.MonthDays
WeekDays utils.WeekDays
StartTime string
}
func (ct *ChargedTiming) Equals(oCT *ChargedTiming) bool {
return ct.Years.Equals(oCT.Years) &&
ct.Months.Equals(oCT.Months) &&
ct.MonthDays.Equals(oCT.MonthDays) &&
ct.WeekDays.Equals(oCT.WeekDays) &&
ct.StartTime == oCT.StartTime
}
func (ct *ChargedTiming) Clone() (cln *ChargedTiming) {
cln = new(ChargedTiming)
*cln = *ct
return
}
// RatingUnit represents one unit out of RatingPlan matching for an event
type RatingUnit struct {
ConnectFee float64
RoundingMethod string
RoundingDecimals int
MaxCost float64
MaxCostStrategy string
TimingUUID string // This RatingUnit is bounded to specific timing profile
RatesUUID string
RatingFiltersUUID string
}
func (ru *RatingUnit) Equals(oRU *RatingUnit) bool {
return ru.ConnectFee == oRU.ConnectFee &&
ru.RoundingMethod == oRU.RoundingMethod &&
ru.RoundingDecimals == oRU.RoundingDecimals &&
ru.MaxCost == oRU.MaxCost &&
ru.MaxCostStrategy == oRU.MaxCostStrategy &&
ru.TimingUUID == oRU.TimingUUID &&
ru.RatesUUID == oRU.RatesUUID &&
ru.RatingFiltersUUID == oRU.RatingFiltersUUID
}
func (ru *RatingUnit) Clone() (cln *RatingUnit) {
cln = new(RatingUnit)
*cln = *ru
return
}

View File

@@ -25,6 +25,189 @@ import (
"github.com/cgrates/cgrates/utils"
)
// testEC is used as sample through various tests
var testEC = &EventCost{
CGRID: "164b0422fdc6a5117031b427439482c6a4f90e41",
RunID: utils.META_DEFAULT,
StartTime: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC),
Charges: []*ChargingInterval{
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(0),
Cost: 0.1,
BalanceChargeUUID: "9bdad10",
CompressFactor: 1,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0,
BalanceChargeUUID: "3455b83",
CompressFactor: 10,
},
&ChargingIncrement{
Usage: time.Duration(10 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 2,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.005,
BalanceChargeUUID: "44d6c02",
CompressFactor: 30,
},
},
CompressFactor: 1,
},
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 60,
},
},
CompressFactor: 4,
},
},
AccountSummary: &AccountSummary{
Tenant: "cgrates.org",
ID: "dan",
BalanceSummaries: []*BalanceSummary{
&BalanceSummary{
Type: "*monetary",
Value: 50,
Disabled: false},
&BalanceSummary{
ID: "4b8b53d7-c1a1-4159-b845-4623a00a0165",
Type: "*monetary",
Value: 25,
Disabled: false},
&BalanceSummary{
Type: "*voice",
Value: 200,
Disabled: false,
},
},
AllowNegative: false,
Disabled: false,
},
Rating: Rating{
"3cd6425": &RatingUnit{
RoundingMethod: "*up",
RoundingDecimals: 5,
TimingUUID: "7f324ab",
RatesUUID: "4910ecf",
RatingFiltersUUID: "43e77dc",
},
"c1a5ab9": &RatingUnit{
ConnectFee: 0.1,
RoundingMethod: "*up",
RoundingDecimals: 5,
TimingUUID: "7f324ab",
RatesUUID: "ec1a177",
RatingFiltersUUID: "43e77dc",
},
},
Accounting: Accounting{
"a012888": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.01,
},
"188bfa6": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.005,
},
"9bdad10": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.1,
},
"44d6c02": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "7a54a9e9-d610-4c82-bcb5-a315b9a65010",
RatingUUID: "3cd6425",
Units: 1,
ExtraChargeUUID: "188bfa6",
},
"3455b83": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "9d54a9e9-d610-4c82-bcb5-a315b9a65089",
Units: 1,
ExtraChargeUUID: "*none",
},
},
RatingFilters: RatingFilters{
"43e77dc": RatingMatchedFilters{
"DestinationID": "GERMANY",
"DestinationPrefix": "+49",
"RatingPlanID": "RPL_RETAIL1",
"Subject": "*out:cgrates.org:call:*any",
},
},
Rates: ChargedRates{
"ec1a177": RateGroups{
&Rate{
GroupIntervalStart: time.Duration(0),
Value: 0.01,
RateIncrement: time.Duration(1 * time.Minute),
RateUnit: time.Duration(1 * time.Second)},
},
"4910ecf": RateGroups{
&Rate{
GroupIntervalStart: time.Duration(0),
Value: 0.005,
RateIncrement: time.Duration(1 * time.Second),
RateUnit: time.Duration(1 * time.Second)},
&Rate{
GroupIntervalStart: time.Duration(60 * time.Second),
Value: 0.005,
RateIncrement: time.Duration(1 * time.Second),
RateUnit: time.Duration(1 * time.Second)},
},
},
Timings: ChargedTimings{
"7f324ab": &ChargedTiming{
StartTime: "00:00:00",
},
},
}
func TestECClone(t *testing.T) {
ec := testEC.Clone()
if !reflect.DeepEqual(testEC, ec) {
t.Errorf("Expecting: %s, received: %s",
utils.ToJSON(testEC), utils.ToJSON(ec))
}
}
func TestECComputeAndReset(t *testing.T) {
ec := testEC.Clone()
eEc := testEC.Clone()
eEc.Usage = utils.DurationPointer(time.Duration(5 * time.Minute))
eEc.Cost = utils.Float64Pointer(2.67)
eEc.Charges[0].ecUsageIdx = utils.DurationPointer(time.Duration(0))
eEc.Charges[0].usage = utils.DurationPointer(time.Duration(1 * time.Minute))
eEc.Charges[0].cost = utils.Float64Pointer(0.27)
eEc.Charges[1].ecUsageIdx = utils.DurationPointer(time.Duration(1 * time.Minute))
eEc.Charges[1].usage = utils.DurationPointer(time.Duration(1 * time.Minute))
eEc.Charges[1].cost = utils.Float64Pointer(0.6)
ec.Compute()
if !reflect.DeepEqual(eEc, ec) {
t.Errorf("Expecting: %+v, received: %+v", eEc, ec)
}
ec.ResetCounters()
if !reflect.DeepEqual(testEC, ec) {
t.Errorf("Expecting: %+v, received: %+v", testEC, ec)
}
}
func TestNewEventCostFromCallCost(t *testing.T) {
acntSummary := &AccountSummary{
Tenant: "cgrates.org",
@@ -423,7 +606,7 @@ func TestNewEventCostFromCallCost(t *testing.T) {
}
}
func TestEventCostAsCallCost(t *testing.T) {
func TestECAsCallCost(t *testing.T) {
acntSummary := &AccountSummary{
Tenant: "cgrates.org",
ID: "dan",
@@ -718,165 +901,129 @@ func TestEventCostAsCallCost(t *testing.T) {
}
}
func TestEventCostTrim(t *testing.T) {
acntSummary := &AccountSummary{
Tenant: "cgrates.org",
ID: "dan",
BalanceSummaries: []*BalanceSummary{
&BalanceSummary{
Type: "*monetary",
Value: 50,
Disabled: false},
&BalanceSummary{
ID: "4b8b53d7-c1a1-4159-b845-4623a00a0165",
Type: "*monetary",
Value: 25,
Disabled: false},
&BalanceSummary{
Type: "*voice",
Value: 200,
Disabled: false,
},
},
AllowNegative: false,
Disabled: false,
}
ec := &EventCost{
CGRID: "164b0422fdc6a5117031b427439482c6a4f90e41",
RunID: utils.META_DEFAULT,
StartTime: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC),
Cost: utils.Float64Pointer(2.05),
Usage: utils.DurationPointer(time.Duration(4 * time.Minute)),
Charges: []*ChargingInterval{
&ChargingInterval{
RatingUUID: "f2518464-68b8-42f4-acec-aef23d714314",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(0),
Cost: 0.1,
BalanceChargeUUID: "44e97dec-8a7e-43d0-8b0a-736d46b5613e",
CompressFactor: 1,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0,
BalanceChargeUUID: "a555cde8-4bd0-408a-afbc-c3ba64888927",
CompressFactor: 30,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.005,
BalanceChargeUUID: "906bfd0f-035c-40a3-93a8-46f71627983e",
CompressFactor: 30,
},
},
CompressFactor: 1,
},
&ChargingInterval{
RatingUUID: "f2518464-68b8-42f4-acec-aef23d714314",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "c890a899-df43-497a-9979-38492713f57b",
CompressFactor: 60,
},
},
CompressFactor: 3,
},
},
AccountSummary: acntSummary,
Rating: Rating{
"4607d907-02c3-4f2b-bc08-95a0dcc7222c": &RatingUnit{
RoundingMethod: "*up",
RoundingDecimals: 5,
TimingUUID: "27f1e5f8-05bb-4f1c-a596-bf1010ad296c",
RatesUUID: "e5eb0f1c-3612-4e8c-b749-7f8f41dd90d4",
RatingFiltersUUID: "7e73a00d-be53-4083-a1ee-8ee0b546c62a",
},
"f2518464-68b8-42f4-acec-aef23d714314": &RatingUnit{
ConnectFee: 0.1,
RoundingMethod: "*up",
RoundingDecimals: 5,
TimingUUID: "27f1e5f8-05bb-4f1c-a596-bf1010ad296c",
RatesUUID: "6504fb84-6b27-47a8-a1c6-c0d843959f89",
RatingFiltersUUID: "7e73a00d-be53-4083-a1ee-8ee0b546c62a",
},
},
Accounting: Accounting{
"c890a899-df43-497a-9979-38492713f57b": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.01,
},
"a894f8f1-206a-4457-99ce-df21a0c7fedc": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.005,
},
"44e97dec-8a7e-43d0-8b0a-736d46b5613e": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
Units: 0.1,
},
"906bfd0f-035c-40a3-93a8-46f71627983e": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "7a54a9e9-d610-4c82-bcb5-a315b9a65010",
RatingUUID: "4607d907-02c3-4f2b-bc08-95a0dcc7222c",
Units: 1,
ExtraChargeUUID: "a894f8f1-206a-4457-99ce-df21a0c7fedc",
},
"a555cde8-4bd0-408a-afbc-c3ba64888927": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "9d54a9e9-d610-4c82-bcb5-a315b9a65089",
Units: 1,
ExtraChargeUUID: "*none",
},
},
RatingFilters: RatingFilters{
"7e73a00d-be53-4083-a1ee-8ee0b546c62a": RatingMatchedFilters{
"DestinationID": "GERMANY",
"DestinationPrefix": "+49",
"RatingPlanID": "RPL_RETAIL1",
"Subject": "*out:cgrates.org:call:*any",
},
},
Rates: ChargedRates{
"6504fb84-6b27-47a8-a1c6-c0d843959f89": RateGroups{
&Rate{
GroupIntervalStart: time.Duration(0),
Value: 0.01,
RateIncrement: time.Duration(1 * time.Minute),
RateUnit: time.Duration(1 * time.Second)},
},
"e5eb0f1c-3612-4e8c-b749-7f8f41dd90d4": RateGroups{
&Rate{
GroupIntervalStart: time.Duration(0),
Value: 0.005,
RateIncrement: time.Duration(1 * time.Second),
RateUnit: time.Duration(1 * time.Second)},
&Rate{
GroupIntervalStart: time.Duration(60 * time.Second),
Value: 0.005,
RateIncrement: time.Duration(1 * time.Second),
RateUnit: time.Duration(1 * time.Second)},
},
},
Timings: ChargedTimings{
"27f1e5f8-05bb-4f1c-a596-bf1010ad296c": &ChargedTiming{
StartTime: "00:00:00",
},
},
}
origEC := ec.Clone()
if srplsEC, err := ec.Trim(time.Duration(4 * time.Minute)); err != nil {
func TestECTrimZeroAndFull(t *testing.T) {
ec := testEC.Clone()
if srplsEC, err := ec.Trim(time.Duration(5 * time.Minute)); err != nil {
t.Error(err)
} else if srplsEC != nil {
t.Errorf("Expecting nil, got: %+v", srplsEC)
}
eFullSrpls := testEC.Clone()
eFullSrpls.Usage = utils.DurationPointer(time.Duration(5 * time.Minute))
eFullSrpls.Charges[0].usage = utils.DurationPointer(time.Duration(1 * time.Minute))
eFullSrpls.Charges[1].usage = utils.DurationPointer(time.Duration(1 * time.Minute))
if srplsEC, err := ec.Trim(time.Duration(0)); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(origEC, srplsEC) {
t.Errorf("Expecting: %s,\n received: %s", utils.ToJSON(origEC), utils.ToJSON(srplsEC))
} else if !reflect.DeepEqual(eFullSrpls, srplsEC) {
t.Errorf("\tExpecting: %s,\n\treceived: %s",
utils.ToJSON(eFullSrpls), utils.ToJSON(srplsEC))
}
}
func TestECTrimMiddle(t *testing.T) {
// trim in the middle of increments
ec := testEC.Clone()
eEC := testEC.Clone()
eEC.Charges = []*ChargingInterval{
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(0),
Cost: 0.1,
BalanceChargeUUID: "9bdad10",
CompressFactor: 1,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0,
BalanceChargeUUID: "3455b83",
CompressFactor: 10,
},
&ChargingIncrement{
Usage: time.Duration(10 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 2,
},
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.005,
BalanceChargeUUID: "44d6c02",
CompressFactor: 30,
},
},
CompressFactor: 1,
},
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 60,
},
},
CompressFactor: 2,
},
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 10,
},
},
CompressFactor: 1,
},
}
eSrplsEC := testEC.Clone()
eSrplsEC.Charges = []*ChargingInterval{
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 50,
},
},
CompressFactor: 1,
},
&ChargingInterval{
RatingUUID: "c1a5ab9",
Increments: []*ChargingIncrement{
&ChargingIncrement{
Usage: time.Duration(1 * time.Second),
Cost: 0.01,
BalanceChargeUUID: "a012888",
CompressFactor: 60,
},
},
CompressFactor: 1,
},
}
reqDuration := time.Duration(190 * time.Second)
initDur := ec.ComputeUsage()
srplsEC, err := ec.Trim(reqDuration)
if err != nil {
t.Error(err)
}
if reqDuration != *ec.Usage {
t.Logf("\teEC: %s\n\tEC: %s\n\torigEC: %s\n", utils.ToJSON(eEC), utils.ToJSON(ec), utils.ToJSON(testEC))
t.Errorf("Expecting request duration: %v, received: %v", reqDuration, *ec.Usage)
}
if srplsUsage := srplsEC.ComputeUsage(); srplsUsage != time.Duration(110*time.Second) {
t.Errorf("Expecting surplus duration: %v, received: %v", initDur-reqDuration, srplsUsage)
}
if !reflect.DeepEqual(eEC, ec) {
//t.Errorf("Expecting: %s, received: %s", utils.ToJSON(eEC), utils.ToJSON(ec))
} else if !reflect.DeepEqual(eSrplsEC, srplsEC) {
//t.Errorf("Expecting: %s, received: %s", utils.ToJSON(eSrplsEC), utils.ToJSON(srplsEC))
}
}

367
engine/libeventcost.go Normal file
View File

@@ -0,0 +1,367 @@
/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
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 (
"time"
"github.com/cgrates/cgrates/utils"
)
// ChargingInterval represents one interval out of Usage providing charging info
// eg: PEAK vs OFFPEAK
type ChargingInterval struct {
RatingUUID string // reference to RatingUnit
Increments []*ChargingIncrement // specific increments applied to this interval
CompressFactor int
usage *time.Duration // cache usage computation for this interval
ecUsageIdx *time.Duration // computed value of totalUsage at the starting of the interval
cost *float64 // cache cost calculation on this interval
}
// PartiallyEquals does not compare CompressFactor, usefull for Merge
func (cIl *ChargingInterval) PartiallyEquals(oCIl *ChargingInterval) (equals bool) {
if equals = cIl.RatingUUID == oCIl.RatingUUID &&
len(cIl.Increments) == len(oCIl.Increments); !equals {
return
}
for i := range cIl.Increments {
if !cIl.Increments[i].Equals(oCIl.Increments[i]) {
equals = false
break
}
}
return
}
// Usage computes the total usage of this ChargingInterval, ignoring CompressFactor
func (cIl *ChargingInterval) Usage() *time.Duration {
if cIl.usage == nil {
var usage time.Duration
for _, incr := range cIl.Increments {
usage += incr.TotalUsage()
}
cIl.usage = &usage
}
return cIl.usage
}
// TotalUsage returns the total usage of this interval, considering compress factor
func (cIl *ChargingInterval) TotalUsage() (tu *time.Duration) {
usage := cIl.Usage()
if usage == nil {
return
}
tu = new(time.Duration)
*tu = time.Duration(usage.Nanoseconds() * int64(cIl.CompressFactor))
return
}
// EventCostUsageIndex publishes the value of ecUsageIdx
func (cIl *ChargingInterval) EventCostUsageIndex() *time.Duration {
return cIl.ecUsageIdx
}
// StartTime computes a StartTime based on EventCost.Start time and ecUsageIdx
func (cIl *ChargingInterval) StartTime(ecST time.Time) (st time.Time) {
if cIl.ecUsageIdx != nil {
st = ecST.Add(*cIl.ecUsageIdx)
}
return
}
// EndTime computes an EndTime based on ChargingInterval StartTime value and usage
func (cIl *ChargingInterval) EndTime(cIlST time.Time) (et time.Time) {
return cIlST.Add(time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor)))
}
// Cost computes the total cost on this ChargingInterval
func (cIl *ChargingInterval) Cost() float64 {
if cIl.cost == nil {
var cost float64
for _, incr := range cIl.Increments {
cost += incr.Cost * float64(incr.CompressFactor)
}
cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE)
cIl.cost = &cost
}
return *cIl.cost
}
// Clone returns a new instance of ChargingInterval with independent data
func (cIl *ChargingInterval) Clone() (cln *ChargingInterval) {
cln = new(ChargingInterval)
cln.RatingUUID = cIl.RatingUUID
cln.CompressFactor = cIl.CompressFactor
cln.Increments = make([]*ChargingIncrement, len(cIl.Increments))
for i, cIt := range cIl.Increments {
cln.Increments[i] = cIt.Clone()
}
return
}
// ChargingIncrement represents one unit charged inside an interval
type ChargingIncrement struct {
Usage time.Duration
Cost float64
BalanceChargeUUID string
CompressFactor int
}
func (cIt *ChargingIncrement) Equals(oCIt *ChargingIncrement) bool {
return cIt.Usage == oCIt.Usage &&
cIt.Cost == oCIt.Cost &&
cIt.BalanceChargeUUID == oCIt.BalanceChargeUUID &&
cIt.CompressFactor == oCIt.CompressFactor
}
func (cIt *ChargingIncrement) Clone() (cln *ChargingIncrement) {
cln = new(ChargingIncrement)
*cln = *cIt
return
}
// TotalUsage returns the total usage of the increment, considering compress factor
func (cIt *ChargingIncrement) TotalUsage() time.Duration {
return time.Duration(cIt.Usage.Nanoseconds() * int64(cIt.CompressFactor))
}
// BalanceCharge represents one unit charged to a balance
type BalanceCharge struct {
AccountID string // keep reference for shared balances
BalanceUUID string // balance charged
RatingUUID string // special price applied on this balance
Units float64 // number of units charged
ExtraChargeUUID string // used in cases when paying *voice with *monetary
}
func (bc *BalanceCharge) Equals(oBC *BalanceCharge) bool {
return bc.AccountID == oBC.AccountID &&
bc.BalanceUUID == oBC.BalanceUUID &&
bc.RatingUUID == oBC.RatingUUID &&
bc.Units == oBC.Units &&
bc.ExtraChargeUUID == oBC.ExtraChargeUUID
}
func (bc *BalanceCharge) Clone() *BalanceCharge {
clnBC := new(BalanceCharge)
*clnBC = *bc
return clnBC
}
type RatingMatchedFilters map[string]interface{}
func (rf RatingMatchedFilters) Equals(oRF RatingMatchedFilters) (equals bool) {
equals = true
for k := range rf {
if rf[k] != oRF[k] {
equals = false
break
}
}
return
}
func (rf RatingMatchedFilters) Clone() (cln map[string]interface{}) {
cln = make(map[string]interface{})
utils.Clone(rf, &cln)
return
}
// ChargedTiming represents one timing attached to a charge
type ChargedTiming struct {
Years utils.Years
Months utils.Months
MonthDays utils.MonthDays
WeekDays utils.WeekDays
StartTime string
}
func (ct *ChargedTiming) Equals(oCT *ChargedTiming) bool {
return ct.Years.Equals(oCT.Years) &&
ct.Months.Equals(oCT.Months) &&
ct.MonthDays.Equals(oCT.MonthDays) &&
ct.WeekDays.Equals(oCT.WeekDays) &&
ct.StartTime == oCT.StartTime
}
func (ct *ChargedTiming) Clone() (cln *ChargedTiming) {
cln = new(ChargedTiming)
*cln = *ct
return
}
// RatingUnit represents one unit out of RatingPlan matching for an event
type RatingUnit struct {
ConnectFee float64
RoundingMethod string
RoundingDecimals int
MaxCost float64
MaxCostStrategy string
TimingUUID string // This RatingUnit is bounded to specific timing profile
RatesUUID string
RatingFiltersUUID string
}
func (ru *RatingUnit) Equals(oRU *RatingUnit) bool {
return ru.ConnectFee == oRU.ConnectFee &&
ru.RoundingMethod == oRU.RoundingMethod &&
ru.RoundingDecimals == oRU.RoundingDecimals &&
ru.MaxCost == oRU.MaxCost &&
ru.MaxCostStrategy == oRU.MaxCostStrategy &&
ru.TimingUUID == oRU.TimingUUID &&
ru.RatesUUID == oRU.RatesUUID &&
ru.RatingFiltersUUID == oRU.RatingFiltersUUID
}
func (ru *RatingUnit) Clone() (cln *RatingUnit) {
cln = new(RatingUnit)
*cln = *ru
return
}
type RatingFilters map[string]RatingMatchedFilters // so we can define search methods
// GetWithSet attempts to retrieve the UUID of a matching data or create a new one
func (rfs RatingFilters) GetUUIDWithSet(rmf RatingMatchedFilters) string {
if rmf == nil || len(rmf) == 0 {
return ""
}
for k, v := range rfs {
if v.Equals(rmf) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
rfs[uuid] = rmf
return uuid
}
func (rfs RatingFilters) Clone() (cln RatingFilters) {
cln = make(RatingFilters, len(rfs))
for k, v := range rfs {
cln[k] = v.Clone()
}
return
}
type Rating map[string]*RatingUnit
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crus Rating) GetUUIDWithSet(cru *RatingUnit) string {
if cru == nil {
return ""
}
for k, v := range crus {
if v.Equals(cru) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crus[uuid] = cru
return uuid
}
func (crus Rating) Clone() (cln Rating) {
cln = make(Rating, len(crus))
for k, v := range crus {
cln[k] = v.Clone()
}
return
}
type ChargedRates map[string]RateGroups
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crs ChargedRates) GetUUIDWithSet(rg RateGroups) string {
if rg == nil || len(rg) == 0 {
return ""
}
for k, v := range crs {
if v.Equals(rg) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crs[uuid] = rg
return uuid
}
func (crs ChargedRates) Clone() (cln ChargedRates) {
cln = make(ChargedRates, len(crs))
for k, v := range crs {
cln[k] = v.Clone()
}
return
}
type ChargedTimings map[string]*ChargedTiming
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cts ChargedTimings) GetUUIDWithSet(ct *ChargedTiming) string {
if ct == nil {
return ""
}
for k, v := range cts {
if v.Equals(ct) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cts[uuid] = ct
return uuid
}
func (cts ChargedTimings) Clone() (cln ChargedTimings) {
cln = make(ChargedTimings, len(cts))
for k, v := range cts {
cln[k] = v.Clone()
}
return
}
type Accounting map[string]*BalanceCharge
// GetUUIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cbs Accounting) GetUUIDWithSet(cb *BalanceCharge) string {
if cb == nil {
return ""
}
for k, v := range cbs {
if v.Equals(cb) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cbs[uuid] = cb
return uuid
}
func (cbs Accounting) Clone() (cln Accounting) {
cln = make(Accounting, len(cbs))
for k, v := range cbs {
cln[k] = v.Clone()
}
return
}

View File

@@ -108,7 +108,7 @@ func GenUUID() string {
// UUIDSha1Prefix generates a prefix of the sha1 applied to an UUID
// prefix 8 is chosen since the probability of colision starts being minimal after 7 characters (see git commits)
func UUIDSha1Prefix() string {
return Sha1(GenUUID())[:8]
return Sha1(GenUUID())[:7]
}
// Round return rounded version of x with prec precision.