Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config

import (
"encoding/json"
"math"
"strings"
"time"

Expand Down Expand Up @@ -276,16 +277,22 @@ var PresetRampSchedules = map[string]RampSchedule{
},
}

// CumulativePercent is the share of the base (0..100) a ramp has bought once
// step steps are complete: step*PercentPerStep capped at 100, and 100 for an
// immediate schedule. It is the one definition of cumulative ramp progress.
func (r *RampSchedule) CumulativePercent(step int) float64 {
if r.Type == "immediate" {
return 100
}
return math.Min(100, float64(step)*r.PercentPerStep)
}

// GetCurrentCoverage calculates the current effective coverage based on ramp progress.
func (r *RampSchedule) GetCurrentCoverage(baseCoverage float64) float64 {
if r.Type == "immediate" {
return baseCoverage
}
completedPercent := float64(r.CurrentStep) * r.PercentPerStep
if completedPercent > 100 {
completedPercent = 100
return baseCoverage // base*100/100 is not exact in floating point
}
return baseCoverage * completedPercent / 100
return baseCoverage * r.CumulativePercent(r.CurrentStep) / 100
}

// GetNextPurchaseDate calculates when the next purchase step should occur.
Expand Down
41 changes: 41 additions & 0 deletions internal/config/types_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"math"
"testing"
"time"

Expand Down Expand Up @@ -93,6 +94,46 @@ func TestRampSchedule_GetCurrentCoverage(t *testing.T) {
}
}

func TestRampSchedule_CumulativePercent(t *testing.T) {
weekly := RampSchedule{Type: "weekly", PercentPerStep: 25, TotalSteps: 4}
custom := RampSchedule{Type: "custom", PercentPerStep: 40, TotalSteps: 3}
tests := []struct {
name string
schedule RampSchedule
step int
expected float64
}{
{"immediate is always 100", RampSchedule{Type: "immediate"}, 0, 100},
{"weekly step 0", weekly, 0, 0},
{"weekly step 1", weekly, 1, 25},
{"weekly step 4", weekly, 4, 100},
{"custom 3x40 step 2", custom, 2, 80},
{"custom 3x40 step 3 clamps to 100", custom, 3, 100},
{"step beyond the ramp stays at 100", weekly, 9, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.schedule.CumulativePercent(tt.step))
})
}
}

// GetCurrentCoverage must stay the base scaled by CumulativePercent at the
// schedule's own CurrentStep, so the two cannot drift.
func TestRampSchedule_GetCurrentCoverageUsesCumulativePercent(t *testing.T) {
s := RampSchedule{Type: "custom", PercentPerStep: 40, TotalSteps: 3, CurrentStep: 2}
assert.Equal(t, 80*s.CumulativePercent(2)/100, s.GetCurrentCoverage(80))
}

// An immediate schedule returns the base untouched: base*100/100 would turn
// 0.1694 into 0.16939999999999997 and math.MaxFloat64 into +Inf.
func TestRampSchedule_GetCurrentCoverageImmediateIsExact(t *testing.T) {
s := RampSchedule{Type: "immediate"}
for _, base := range []float64{0.1694, 80, math.MaxFloat64} {
assert.Equal(t, base, s.GetCurrentCoverage(base))
}
}

func TestRampSchedule_GetNextPurchaseDate(t *testing.T) {
now := time.Now()
startDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
Expand Down
Loading