8c3d06c0a1
Composes Score over a candidate slice, sorts descending, truncates. Pure — no IO. Liked-rank-higher and high-skip-ranks-last behaviors are stable across RNG seeds (jitter band is smaller than LikeBoost and SkipPenalty).
49 lines
1.0 KiB
Go
49 lines
1.0 KiB
Go
package recommendation
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
// Candidate pairs a track with the inputs needed to score it.
|
|
type Candidate struct {
|
|
Track dbq.Track
|
|
Inputs ScoringInputs
|
|
}
|
|
|
|
// Shuffle scores each candidate, sorts descending by score, and returns
|
|
// the top `limit` candidates. limit <= 0 returns nil; nil input returns
|
|
// nil. Pure — no IO, no global state beyond the rng callback.
|
|
func Shuffle(
|
|
candidates []Candidate,
|
|
weights ScoringWeights,
|
|
now time.Time,
|
|
rng func() float64,
|
|
limit int,
|
|
) []Candidate {
|
|
if len(candidates) == 0 || limit <= 0 {
|
|
return nil
|
|
}
|
|
scored := make([]struct {
|
|
c Candidate
|
|
score float64
|
|
}, len(candidates))
|
|
for i, c := range candidates {
|
|
scored[i].c = c
|
|
scored[i].score = Score(c.Inputs, weights, now, rng)
|
|
}
|
|
sort.Slice(scored, func(i, j int) bool {
|
|
return scored[i].score > scored[j].score
|
|
})
|
|
if limit > len(scored) {
|
|
limit = len(scored)
|
|
}
|
|
out := make([]Candidate, limit)
|
|
for i := 0; i < limit; i++ {
|
|
out[i] = scored[i].c
|
|
}
|
|
return out
|
|
}
|