Skip to content

Commit

Permalink
feat: support relative weighting for fractional evaluation
Browse files Browse the repository at this point in the history
Signed-off-by: Florian Bacher <[email protected]>
  • Loading branch information
bacherfl committed May 21, 2024
1 parent 34756fe commit 6792e7b
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 25 deletions.
56 changes: 33 additions & 23 deletions core/pkg/evaluator/fractional.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,21 @@ type Fractional struct {
}

type fractionalEvaluationDistribution struct {
variant string
percentage int
totalWeight int
weightedVariants []fractionalEvaluationVariant
}

type fractionalEvaluationVariant struct {
variant string
weight int
}

func (v fractionalEvaluationVariant) getPercentage(totalWeight int) float64 {
if totalWeight == 0 {
return 0
}

return 100 * float64(v.weight) / float64(totalWeight)
}

func NewFractional(logger *logger.Logger) *Fractional {
Expand All @@ -34,7 +47,7 @@ func (fe *Fractional) Evaluate(values, data any) any {
return distributeValue(valueToDistribute, feDistributions)
}

func parseFractionalEvaluationData(values, data any) (string, []fractionalEvaluationDistribution, error) {
func parseFractionalEvaluationData(values, data any) (string, *fractionalEvaluationDistribution, error) {
valuesArray, ok := values.([]any)
if !ok {
return "", nil, errors.New("fractional evaluation data is not an array")
Expand Down Expand Up @@ -77,9 +90,11 @@ func parseFractionalEvaluationData(values, data any) (string, []fractionalEvalua
return bucketBy, feDistributions, nil
}

func parseFractionalEvaluationDistributions(values []any) ([]fractionalEvaluationDistribution, error) {
sumOfPercentages := 0
var feDistributions []fractionalEvaluationDistribution
func parseFractionalEvaluationDistributions(values []any) (*fractionalEvaluationDistribution, error) {
feDistributions := &fractionalEvaluationDistribution{
totalWeight: 0,
weightedVariants: make([]fractionalEvaluationVariant, len(values)),
}
for i := 0; i < len(values); i++ {
distributionArray, ok := values[i].([]any)
if !ok {
Expand All @@ -96,37 +111,32 @@ func parseFractionalEvaluationDistributions(values []any) ([]fractionalEvaluatio
return nil, errors.New("first element of distribution element isn't string")
}

percentage, ok := distributionArray[1].(float64)
weight, ok := distributionArray[1].(float64)
if !ok {
return nil, errors.New("second element of distribution element isn't float")
}

sumOfPercentages += int(percentage)

feDistributions = append(feDistributions, fractionalEvaluationDistribution{
variant: variant,
percentage: int(percentage),
})
}

if sumOfPercentages != 100 {
return nil, fmt.Errorf("percentages must sum to 100, got: %d", sumOfPercentages)
feDistributions.totalWeight += int(weight)
feDistributions.weightedVariants[i] = fractionalEvaluationVariant{
variant: variant,
weight: int(weight),
}
}

return feDistributions, nil
}

// distributeValue calculate hash for given hash key and find the bucket distributions belongs to
func distributeValue(value string, feDistribution []fractionalEvaluationDistribution) string {
func distributeValue(value string, feDistribution *fractionalEvaluationDistribution) string {
hashValue := int32(murmur3.StringSum32(value))
hashRatio := math.Abs(float64(hashValue)) / math.MaxInt32
bucket := int(hashRatio * 100) // in range [0, 100]
bucket := hashRatio * 100 // in range [0, 100]

rangeEnd := 0
for _, dist := range feDistribution {
rangeEnd += dist.percentage
rangeEnd := float64(0)
for _, weightedVariant := range feDistribution.weightedVariants {
rangeEnd += weightedVariant.getPercentage(feDistribution.totalWeight)
if bucket < rangeEnd {
return dist.variant
return weightedVariant.variant
}
}

Expand Down
4 changes: 2 additions & 2 deletions core/pkg/evaluator/fractional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func TestFractionalEvaluation(t *testing.T) {
expectedValue: "#FF0000",
expectedReason: model.DefaultReason,
},
"fallback to default variant if percentages don't sum to 100": {
"get variant for non-percentage weight values": {
flags: Flags{
Flags: map[string]model.Flag{
"headerColor": {
Expand Down Expand Up @@ -352,7 +352,7 @@ func TestFractionalEvaluation(t *testing.T) {
},
expectedVariant: "red",
expectedValue: "#FF0000",
expectedReason: model.DefaultReason,
expectedReason: model.TargetingMatchReason,
},
"default to targetingKey if no bucket key provided": {
flags: Flags{
Expand Down

0 comments on commit 6792e7b

Please sign in to comment.