diff --git a/rater/activationperiod_test.go b/rater/activationperiod_test.go index 0d993f47e..64a0b1f59 100644 --- a/rater/activationperiod_test.go +++ b/rater/activationperiod_test.go @@ -49,7 +49,7 @@ func TestApStoreRestoreJson(t *testing.T) { ap := &ActivationPeriod{ActivationTime: d} ap.AddInterval(i) result, _ := json.Marshal(ap) - expected := "{\"ActivationTime\":\"2012-02-01T14:30:01Z\",\"Intervals\":[{\"Years\":null,\"Months\":[2],\"MonthDays\":[1],\"WeekDays\":[3,4],\"StartTime\":\"14:30:00\",\"EndTime\":\"15:00:00\",\"Weight\":0,\"ConnectFee\":0,\"Price\":0,\"PricedUnits\":0,\"RateIncrements\":0}]}" + expected := "{\"ActivationTime\":\"2012-02-01T14:30:01Z\",\"Intervals\":[{\"Years\":null,\"Months\":[2],\"MonthDays\":[1],\"WeekDays\":[3,4],\"StartTime\":\"14:30:00\",\"EndTime\":\"15:00:00\",\"Weight\":0,\"ConnectFee\":0,\"Price\":0,\"PricedUnits\":0,\"RateIncrements\":0,\"RoundingMethod\":\"\",\"RoundingDecimals\":0}]}" if string(result) != expected { t.Errorf("Expected %q was %q", expected, result) } @@ -66,7 +66,7 @@ func TestApStoreRestoreBlank(t *testing.T) { ap := &ActivationPeriod{ActivationTime: d} ap.AddInterval(i) result, _ := json.Marshal(ap) - expected := "{\"ActivationTime\":\"2012-02-01T14:30:01Z\",\"Intervals\":[{\"Years\":null,\"Months\":null,\"MonthDays\":null,\"WeekDays\":null,\"StartTime\":\"\",\"EndTime\":\"\",\"Weight\":0,\"ConnectFee\":0,\"Price\":0,\"PricedUnits\":0,\"RateIncrements\":0}]}" + expected := "{\"ActivationTime\":\"2012-02-01T14:30:01Z\",\"Intervals\":[{\"Years\":null,\"Months\":null,\"MonthDays\":null,\"WeekDays\":null,\"StartTime\":\"\",\"EndTime\":\"\",\"Weight\":0,\"ConnectFee\":0,\"Price\":0,\"PricedUnits\":0,\"RateIncrements\":0,\"RoundingMethod\":\"\",\"RoundingDecimals\":0}]}" if string(result) != expected { t.Errorf("Expected %q was %q", expected, result) } diff --git a/rater/interval.go b/rater/interval.go index 05f2aa0ad..d6062c325 100644 --- a/rater/interval.go +++ b/rater/interval.go @@ -20,6 +20,8 @@ package rater import ( "fmt" + "github.com/cgrates/cgrates/utils" + "math" "reflect" "strconv" "strings" @@ -37,6 +39,8 @@ type Interval struct { WeekDays WeekDays StartTime, EndTime string // ##:##:## format Weight, ConnectFee, Price, PricedUnits, RateIncrements float64 + RoundingMethod string + RoundingDecimals int } /* @@ -132,3 +136,13 @@ func (i *Interval) Equal(o *Interval) bool { i.StartTime == o.StartTime && i.EndTime == o.EndTime } + +func (i *Interval) GetCost(duration float64) (cost float64) { + + if i.PricedUnits != 0 { + cost = math.Ceil(duration/i.RateIncrements) * i.RateIncrements * (i.Price / i.PricedUnits) + } else { + cost = math.Ceil(duration/i.RateIncrements) * i.RateIncrements * i.Price + } + return utils.Round(cost, i.RoundingDecimals, i.RoundingMethod) +} diff --git a/rater/simple_serializer.go b/rater/simple_serializer.go index c1139bfc8..bec2c9077 100644 --- a/rater/simple_serializer.go +++ b/rater/simple_serializer.go @@ -424,13 +424,15 @@ func (i *Interval) Store() (result string, err error) { result += strconv.FormatFloat(i.ConnectFee, 'f', -1, 64) + ";" result += strconv.FormatFloat(i.Price, 'f', -1, 64) + ";" result += strconv.FormatFloat(i.PricedUnits, 'f', -1, 64) + ";" - result += strconv.FormatFloat(i.RateIncrements, 'f', -1, 64) + result += strconv.FormatFloat(i.RateIncrements, 'f', -1, 64) + ";" + result += i.RoundingMethod + ";" + result += strconv.Itoa(i.RoundingDecimals) return } func (i *Interval) Restore(input string) error { is := strings.Split(input, ";") - if len(is) != 11 { + if len(is) != 13 { return notEnoughElements } if err := i.Years.Restore(is[0]); err != nil { @@ -452,6 +454,8 @@ func (i *Interval) Restore(input string) error { i.Price, _ = strconv.ParseFloat(is[8], 64) i.PricedUnits, _ = strconv.ParseFloat(is[9], 64) i.RateIncrements, _ = strconv.ParseFloat(is[10], 64) + i.RoundingMethod = is[11] + i.RoundingDecimals, _ = strconv.Atoi(is[12]) return nil } diff --git a/rater/simple_serializer_test.go b/rater/simple_serializer_test.go index 1ca0f3229..1d95e73cc 100644 --- a/rater/simple_serializer_test.go +++ b/rater/simple_serializer_test.go @@ -35,7 +35,7 @@ func TestSimpleMarshallerApStoreRestore(t *testing.T) { ap := &ActivationPeriod{ActivationTime: d} ap.AddInterval(i) result, err := ap.Store() - expected := "1328106601000000000|;2;1;3,4;14:30:00;15:00:00;0;0;0;0;0" + expected := "1328106601000000000|;2;1;3,4;14:30:00;15:00:00;0;0;0;0;0;;0" if err != nil || !reflect.DeepEqual(result, expected) { t.Errorf("Expected %q was %q", expected, result) } @@ -47,7 +47,7 @@ func TestSimpleMarshallerApStoreRestore(t *testing.T) { } func TestSimpleMarshallerApRestoreFromString(t *testing.T) { - s := "1325376000000000000|;1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5,6,0;00:00:00;;10;0;0.2;60;1\n" + s := "1325376000000000000|;1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5,6,0;00:00:00;;10;0;0.2;60;1;;0\n" ap := &ActivationPeriod{} err := ap.Restore(s) if err != nil || len(ap.Intervals) != 1 { @@ -68,7 +68,7 @@ func TestRpStoreRestore(t *testing.T) { rp := &RatingProfile{FallbackKey: "test"} rp.AddActivationPeriodIfNotPresent("0723", ap) result, err := rp.Store() - expected := "test>0723=1328106601000000000|;2;1;3,4;14:30:00;15:00:00;0;0;0;0;0" + expected := "test>0723=1328106601000000000|;2;1;3,4;14:30:00;15:00:00;0;0;0;0;0;;0" if err != nil || !reflect.DeepEqual(result, expected) { t.Errorf("Expected %q was %q", expected, result) } @@ -101,7 +101,7 @@ func TestActionTimingStoreRestore(t *testing.T) { ActionsId: "Commando", } r, err := at.Store() - if err != nil || r != "some uuid|test|one,two,three|;1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5;18:00:00;00:00:00;10;0;1;60;1|10|Commando" { + if err != nil || r != "some uuid|test|one,two,three|;1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5;18:00:00;00:00:00;10;0;1;60;1;;0|10|Commando" { t.Errorf("Error serializing action timing: %v", string(r)) } o := &ActionTiming{} @@ -146,7 +146,7 @@ func TestIntervalStoreRestore(t *testing.T) { RateIncrements: 1, } r, err := i.Store() - if err != nil || r != ";1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5;18:00:00;00:00:00;10;0;1;60;1" { + if err != nil || r != ";1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5;18:00:00;00:00:00;10;0;1;60;1;;0" { t.Errorf("Error serializing interval: %v", string(r)) } o := &Interval{} @@ -157,7 +157,7 @@ func TestIntervalStoreRestore(t *testing.T) { } func TestIntervalRestoreFromString(t *testing.T) { - s := ";1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5,6,0;00:00:00;;10;0;0.2;60;1" + s := ";1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5,6,0;00:00:00;;10;0;0.2;60;0;;1" i := Interval{} err := i.Restore(s) if err != nil || i.Price != 0.2 { diff --git a/rater/timespans.go b/rater/timespans.go index 61ef8655f..5ecee0e56 100644 --- a/rater/timespans.go +++ b/rater/timespans.go @@ -20,7 +20,6 @@ package rater import ( "fmt" - "math" "time" ) @@ -64,11 +63,7 @@ func (ts *TimeSpan) getCost(cd *CallDescriptor) (cost float64) { if i.RateIncrements == 0 { i.RateIncrements = 1 } - if i.PricedUnits != 0 { - cost = math.Ceil(duration/i.RateIncrements) * i.RateIncrements * (i.Price / i.PricedUnits) - } else { - cost = math.Ceil(duration/i.RateIncrements) * i.RateIncrements * i.Price - } + cost = i.GetCost(duration) // if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { // userBalance.mux.RLock() // if percentageDiscount, err := userBalance.getVolumeDiscount(cd.Destination, INBOUND); err == nil && percentageDiscount > 0 { diff --git a/utils/coreutils.go b/utils/coreutils.go index dadc6b3e7..defc797c0 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -23,6 +23,7 @@ import ( "crypto/sha1" "encoding/hex" "fmt" + "math" "strconv" "time" ) @@ -63,3 +64,33 @@ func GenUUID() string { return hex.EncodeToString(uuid) } + +// Round return rounded version of x with prec precision. +// +// Special cases are: +// Round(±0) = ±0 +// Round(±Inf) = ±Inf +// Round(NaN) = NaN +func Round(x float64, prec int, method string) float64 { + var rounder float64 + pow := math.Pow(10, float64(prec)) + intermed := x * pow + _, frac := math.Modf(intermed) + + switch method { + case "*up": + rounder = math.Ceil(intermed) + case "*down": + rounder = math.Floor(intermed) + case "*middle": + if frac >= 0.5 { + rounder = math.Ceil(intermed) + } else { + rounder = math.Floor(intermed) + } + default: + rounder = intermed + } + + return rounder / pow +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 6b7ec3fcd..fc9a2f2ed 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -39,3 +39,83 @@ func TestUUID(t *testing.T) { t.Fatalf("GenUUID error %s", uuid) } } + +func TestRoundUp(t *testing.T) { + result := Round(12.52, 0, "*middle") + expected := 13.0 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundUpMiddle(t *testing.T) { + result := Round(12.5, 0, "*middle") + expected := 13.0 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundDown(t *testing.T) { + result := Round(12.49, 0, "*middle") + expected := 12.0 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundPrec(t *testing.T) { + result := Round(12.49, 1, "*middle") + expected := 12.5 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundPrecNothing(t *testing.T) { + result := Round(12.49, 2, "*middle") + expected := 12.49 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundPrecNoTouch(t *testing.T) { + result := Round(12.49, 2, "") + expected := 12.49 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundByMethodUp1(t *testing.T) { + result := Round(12.49, 1, "*up") + expected := 12.5 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundByMethodUp2(t *testing.T) { + result := Round(12.21, 1, "*up") + expected := 12.3 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundByMethodDown1(t *testing.T) { + result := Round(12.49, 1, "*down") + expected := 12.4 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + +func TestRoundByMethodDown2(t *testing.T) { + result := Round(12.21, 1, "*down") + expected := 12.2 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +}