Skip to content

Commit 0892404

Browse files
authored
Merge pull request #14 from mroth/v2
v2: conversion to utilize go1.18 generics
2 parents c32b594 + 54fe109 commit 0892404

File tree

8 files changed

+76
-66
lines changed

8 files changed

+76
-66
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ jobs:
77
strategy:
88
matrix:
99
go:
10-
- "1.10"
11-
- "1.11"
12-
- "1.12"
13-
- "1.13"
14-
- "1.14"
15-
- "1.15"
16-
- "1.16"
17-
- "1.17"
1810
- "1.18"
1911
- "1.19"
2012
name: Go ${{ matrix.go }} test

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,24 @@ element to be selected are not equal, but rather defined by relative "weights"
1616
```go
1717
import (
1818
/* ...snip... */
19-
wr "github.com/mroth/weightedrand"
19+
"github.com/mroth/weightedrand/v2"
2020
)
2121

2222
func main() {
2323
rand.Seed(time.Now().UTC().UnixNano()) // always seed random!
2424

25-
chooser, _ := wr.NewChooser(
26-
wr.Choice{Item: "🍒", Weight: 0},
27-
wr.Choice{Item: "🍋", Weight: 1},
28-
wr.Choice{Item: "🍊", Weight: 1},
29-
wr.Choice{Item: "🍉", Weight: 3},
30-
wr.Choice{Item: "🥑", Weight: 5},
25+
chooser, _ := weightedrand.NewChooser(
26+
weightedrand.NewChoice('🍒', 0),
27+
weightedrand.NewChoice('🍋', 1),
28+
weightedrand.NewChoice('🍊', 1),
29+
weightedrand.NewChoice('🍉', 3),
30+
weightedrand.NewChoice('🥑', 5),
3131
)
32-
/* The following will print 🍋 and 🍊 with 0.1 probability, 🍉 with 0.3
33-
probability, and 🥑 with 0.5 probability. 🍒 will never be printed. (Note
34-
the weights don't have to add up to 10, that was just done here to make the
35-
example easier to read.) */
36-
result := chooser.Pick().(string)
32+
// The following will print 🍋 and 🍊 with 0.1 probability, 🍉 with 0.3
33+
// probability, and 🥑 with 0.5 probability. 🍒 will never be printed. (Note
34+
// the weights don't have to add up to 10, that was just done here to make
35+
// the example easier to read.)
36+
result := chooser.Pick()
3737
fmt.Println(result)
3838
}
3939
```
@@ -73,6 +73,11 @@ right choice! If you are only picking from the same distribution once,
7373
`randutil` will be faster. `weightedrand` optimizes for repeated calls at the
7474
expense of some initialization time and memory storage.
7575

76+
## Requirements
77+
78+
weightedrand >= v2 requires go1.18 or greater. For support on earlier versions
79+
of go, use weightedrand [v1](https://github.com/mroth/weightedrand/tree/v1).
80+
7681
## Credits
7782

7883
To better understand the algorithm used in this library (as well as the one used

examples/compbench/bench_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"time"
1010

1111
"github.com/jmcvetta/randutil"
12-
"github.com/mroth/weightedrand"
12+
"github.com/mroth/weightedrand/v2"
1313
)
1414

1515
const BMMinChoices = 10
@@ -98,9 +98,9 @@ func BenchmarkSingle(b *testing.B) {
9898
})
9999
}
100100

101-
func mockChoices(tb testing.TB, n int) []weightedrand.Choice {
101+
func mockChoices(tb testing.TB, n int) []weightedrand.Choice[rune, uint] {
102102
tb.Helper()
103-
choices := make([]weightedrand.Choice, 0, n)
103+
choices := make([]weightedrand.Choice[rune, uint], 0, n)
104104
for i := 0; i < n; i++ {
105105
s := '🥑'
106106
w := rand.Intn(10)
@@ -110,7 +110,7 @@ func mockChoices(tb testing.TB, n int) []weightedrand.Choice {
110110
return choices
111111
}
112112

113-
func convertChoices(tb testing.TB, cs []weightedrand.Choice) []randutil.Choice {
113+
func convertChoices(tb testing.TB, cs []weightedrand.Choice[rune, uint]) []randutil.Choice {
114114
tb.Helper()
115115
res := make([]randutil.Choice, len(cs))
116116
for i, c := range cs {

examples/compbench/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module github.com/mroth/weightedrand/examples/compbench
22

3-
go 1.15
3+
go 1.18
44

55
require (
66
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
7-
github.com/mroth/weightedrand v0.0.0
7+
github.com/mroth/weightedrand/v2 v2.0.0
88
)
99

10-
replace github.com/mroth/weightedrand => ../..
10+
replace github.com/mroth/weightedrand/v2 => ../..

examples/frequency/main.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ import (
66
"math/rand"
77
"time"
88

9-
wr "github.com/mroth/weightedrand"
9+
"github.com/mroth/weightedrand/v2"
1010
)
1111

1212
func main() {
1313
rand.Seed(time.Now().UTC().UnixNano()) // always seed random!
1414

15-
c, err := wr.NewChooser(
16-
wr.Choice{Item: '🍒', Weight: 0}, // alternatively: wr.NewChoice('🍒', 0)
17-
wr.Choice{Item: '🍋', Weight: 1},
18-
wr.Choice{Item: '🍊', Weight: 1},
19-
wr.Choice{Item: '🍉', Weight: 3},
20-
wr.Choice{Item: '🥑', Weight: 5},
15+
c, err := weightedrand.NewChooser(
16+
weightedrand.NewChoice('🍒', 0),
17+
weightedrand.NewChoice('🍋', 1),
18+
weightedrand.NewChoice('🍊', 1),
19+
weightedrand.NewChoice('🍉', 3),
20+
weightedrand.NewChoice('🥑', 5),
2121
)
2222
if err != nil {
2323
log.Fatal(err)
@@ -26,7 +26,7 @@ func main() {
2626
/* Let's pick a bunch of fruits so we can see the distribution in action! */
2727
fruits := make([]rune, 40*18)
2828
for i := 0; i < len(fruits); i++ {
29-
fruits[i] = c.Pick().(rune)
29+
fruits[i] = c.Pick()
3030
}
3131
fmt.Println(string(fruits))
3232

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
module github.com/mroth/weightedrand
1+
module github.com/mroth/weightedrand/v2
22

3-
go 1.10
3+
go 1.18

weightedrand.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,30 @@ import (
1717
)
1818

1919
// Choice is a generic wrapper that can be used to add weights for any item.
20-
type Choice struct {
21-
Item interface{}
22-
Weight uint
20+
type Choice[T any, W integer] struct {
21+
Item T
22+
Weight W
23+
}
24+
25+
type integer interface {
26+
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
2327
}
2428

2529
// NewChoice creates a new Choice with specified item and weight.
26-
func NewChoice(item interface{}, weight uint) Choice {
27-
return Choice{Item: item, Weight: weight}
30+
func NewChoice[T any, W integer](item T, weight W) Choice[T, W] {
31+
return Choice[T, W]{Item: item, Weight: weight}
2832
}
2933

3034
// A Chooser caches many possible Choices in a structure designed to improve
3135
// performance on repeated calls for weighted random selection.
32-
type Chooser struct {
33-
data []Choice
36+
type Chooser[T any, W integer] struct {
37+
data []Choice[T, W]
3438
totals []int
3539
max int
3640
}
3741

3842
// NewChooser initializes a new Chooser for picking from the provided choices.
39-
func NewChooser(choices ...Choice) (*Chooser, error) {
43+
func NewChooser[T any, W integer](choices ...Choice[T, W]) (*Chooser[T, W], error) {
4044
sort.Slice(choices, func(i, j int) bool {
4145
return choices[i].Weight < choices[j].Weight
4246
})
@@ -45,6 +49,10 @@ func NewChooser(choices ...Choice) (*Chooser, error) {
4549
runningTotal := 0
4650
for i, c := range choices {
4751
weight := int(c.Weight)
52+
if weight < 0 {
53+
continue // ignore negative weights, can never be picked
54+
}
55+
4856
if (maxInt - runningTotal) <= weight {
4957
return nil, errWeightOverflow
5058
}
@@ -56,7 +64,7 @@ func NewChooser(choices ...Choice) (*Chooser, error) {
5664
return nil, errNoValidChoices
5765
}
5866

59-
return &Chooser{data: choices, totals: totals, max: runningTotal}, nil
67+
return &Chooser[T, W]{data: choices, totals: totals, max: runningTotal}, nil
6068
}
6169

6270
const (
@@ -80,7 +88,7 @@ var (
8088
// Pick returns a single weighted random Choice.Item from the Chooser.
8189
//
8290
// Utilizes global rand as the source of randomness.
83-
func (c Chooser) Pick() interface{} {
91+
func (c Chooser[T, W]) Pick() T {
8492
r := rand.Intn(c.max) + 1
8593
i := searchInts(c.totals, r)
8694
return c.data[i].Item
@@ -95,7 +103,7 @@ func (c Chooser) Pick() interface{} {
95103
//
96104
// It is the responsibility of the caller to ensure the provided rand.Source is
97105
// free from thread safety issues.
98-
func (c Chooser) PickSource(rs *rand.Rand) interface{} {
106+
func (c Chooser[T, W]) PickSource(rs *rand.Rand) T {
99107
r := rs.Intn(c.max) + 1
100108
i := searchInts(c.totals, r)
101109
return c.data[i].Item

weightedrand_test.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func Example() {
2424
NewChoice('🍉', 0),
2525
NewChoice('🥑', 42),
2626
)
27-
fruit := chooser.Pick().(rune)
27+
fruit := chooser.Pick()
2828
fmt.Printf("%c", fruit)
2929
//Output: 🥑
3030
}
@@ -45,32 +45,37 @@ const (
4545
func TestNewChooser(t *testing.T) {
4646
tests := []struct {
4747
name string
48-
cs []Choice
48+
cs []Choice[rune, int]
4949
wantErr error
5050
}{
5151
{
5252
name: "zero choices",
53-
cs: []Choice{},
53+
cs: []Choice[rune, int]{},
5454
wantErr: errNoValidChoices,
5555
},
5656
{
5757
name: "no choices with positive weight",
58-
cs: []Choice{{Item: 'a', Weight: 0}, {Item: 'b', Weight: 0}},
58+
cs: []Choice[rune, int]{{Item: 'a', Weight: 0}, {Item: 'b', Weight: 0}},
5959
wantErr: errNoValidChoices,
6060
},
6161
{
6262
name: "choice with weight equals 1",
63-
cs: []Choice{{Item: 'a', Weight: 1}},
63+
cs: []Choice[rune, int]{{Item: 'a', Weight: 1}},
6464
wantErr: nil,
6565
},
6666
{
6767
name: "weight overflow",
68-
cs: []Choice{{Item: 'a', Weight: maxInt/2 + 1}, {Item: 'b', Weight: maxInt/2 + 1}},
68+
cs: []Choice[rune, int]{{Item: 'a', Weight: maxInt/2 + 1}, {Item: 'b', Weight: maxInt/2 + 1}},
6969
wantErr: errWeightOverflow,
7070
},
7171
{
7272
name: "nominal case",
73-
cs: []Choice{{Item: 'a', Weight: 1}, {Item: 'b', Weight: 2}},
73+
cs: []Choice[rune, int]{{Item: 'a', Weight: 1}, {Item: 'b', Weight: 2}},
74+
wantErr: nil,
75+
},
76+
{
77+
name: "negative weight case",
78+
cs: []Choice[rune, int]{{Item: 'a', Weight: 3}, {Item: 'b', Weight: -2}},
7479
wantErr: nil,
7580
},
7681
}
@@ -100,7 +105,7 @@ func TestChooser_Pick(t *testing.T) {
100105
counts := make(map[int]int)
101106
for i := 0; i < testIterations; i++ {
102107
c := chooser.Pick()
103-
counts[c.(int)]++
108+
counts[c]++
104109
}
105110

106111
verifyFrequencyCounts(t, counts, choices)
@@ -127,7 +132,7 @@ func TestChooser_PickSource(t *testing.T) {
127132
rs := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
128133
for i := 0; i < testIterations/2; i++ {
129134
c := chooser.PickSource(rs)
130-
counts[c.(int)]++
135+
counts[c]++
131136
}
132137
}
133138
go checker(counts1)
@@ -140,19 +145,19 @@ func TestChooser_PickSource(t *testing.T) {
140145

141146
// Similar to what is used in randutil test, but in randomized order to avoid
142147
// any issues with algorithms that are accidentally dependant on presorted data.
143-
func mockFrequencyChoices(t *testing.T, n int) []Choice {
148+
func mockFrequencyChoices(t *testing.T, n int) []Choice[int, int] {
144149
t.Helper()
145-
choices := make([]Choice, 0, n)
150+
choices := make([]Choice[int, int], 0, n)
146151
list := rand.Perm(n)
147152
for _, v := range list {
148-
c := NewChoice(v, uint(v))
153+
c := NewChoice(v, v)
149154
choices = append(choices, c)
150155
}
151156
t.Log("mocked choices of", choices)
152157
return choices
153158
}
154159

155-
func verifyFrequencyCounts(t *testing.T, counts map[int]int, choices []Choice) {
160+
func verifyFrequencyCounts(t *testing.T, counts map[int]int, choices []Choice[int, int]) {
156161
t.Helper()
157162

158163
// Ensure weight 0 results in no results
@@ -202,7 +207,7 @@ func BenchmarkPick(b *testing.B) {
202207
b.ResetTimer()
203208

204209
for i := 0; i < b.N; i++ {
205-
_ = chooser.Pick().(rune)
210+
_ = chooser.Pick()
206211
}
207212
})
208213
}
@@ -220,19 +225,19 @@ func BenchmarkPickParallel(b *testing.B) {
220225
b.RunParallel(func(pb *testing.PB) {
221226
rs := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
222227
for pb.Next() {
223-
_ = chooser.PickSource(rs).(rune)
228+
_ = chooser.PickSource(rs)
224229
}
225230
})
226231
})
227232
}
228233
}
229234

230-
func mockChoices(n int) []Choice {
231-
choices := make([]Choice, 0, n)
235+
func mockChoices(n int) []Choice[rune, int] {
236+
choices := make([]Choice[rune, int], 0, n)
232237
for i := 0; i < n; i++ {
233238
s := '🥑'
234239
w := rand.Intn(10)
235-
c := NewChoice(s, uint(w))
240+
c := NewChoice(s, w)
236241
choices = append(choices, c)
237242
}
238243
return choices

0 commit comments

Comments
 (0)