]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tpl/collections: Improve collections.D
authorJoe Mooring <joe.mooring@veriphor.com>
Sun, 16 Nov 2025 19:23:14 +0000 (11:23 -0800)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 18 Nov 2025 10:42:33 +0000 (11:42 +0100)
Changes:

- If n > hi, return the full, sorted range [0, hi) of size hi.
- Throw errors when seed, n, or hi are < 0
- Improve error messages

Closes #14143

tpl/collections/collections.go
tpl/collections/collections_test.go

index 6b580b31c3b95ddec3dc0aa42e21692498ea3867..f10466fd94156a7976826185b7997bc4e8d64b6d 100644 (file)
@@ -537,27 +537,62 @@ type dKey struct {
        hi   int
 }
 
-// D returns a slice of n unique random numbers in the range [0, hi) using the provded seed,
-// using  J. S. Vitter's Method D for sequential random sampling, from Vitter, J.S.
-// - An Efficient Algorithm for Sequential Random Sampling - ACM Trans. Math. Software 11 (1985), 37-57.
-// See  https://getkerf.wordpress.com/2016/03/30/the-best-algorithm-no-one-knows-about/
-func (ns *Namespace) D(seed, n, hi int) []int {
-       key := dKey{seed: cast.ToUint64(seed), n: n, hi: hi}
-       if key.n <= 0 || key.hi <= 0 || key.n > key.hi {
-               return nil
-       }
-       if key.n > maxSeqSize {
-               panic(errSeqSizeExceedsLimit)
-       }
-       v, _ := ns.dCache.GetOrCreate(key, func() ([]int, error) {
+// D returns a sorted slice of unique random integers in the half-open interval
+// [0, hi) using the provided seed value. The number of elements in the
+// resulting slice is n or hi, whichever is less.
+//
+// If n <= hi, it returns a sorted random sample of size n using J. S. Vitter’s
+// Method D for sequential random sampling.
+//
+// If n > hi, it returns the full, sorted range [0, hi) of size hi.
+//
+// If n == 0 or hi == 0, it returns an empty slice.
+//
+// Reference:
+//
+//     J. S. Vitter, "An efficient algorithm for sequential random sampling," ACM Trans. Math. Softw., vol. 11, no. 1, pp. 37–57, 1985.
+//     See also: https://getkerf.wordpress.com/2016/03/30/the-best-algorithm-no-one-knows-about/
+func (ns *Namespace) D(seed, n, hi any) ([]int, error) {
+       seedInt, err := cast.ToInt64E(seed)
+       if err != nil || seedInt < 0 {
+               return nil, fmt.Errorf("the seed value (%v) must be a non-negative integer", seed)
+       }
+
+       nInt, err := cast.ToIntE(n)
+       if err != nil || nInt < 0 || nInt > maxSeqSize {
+               return nil, fmt.Errorf("the number of requested values (%v) must be a non-negative integer <= %d", n, maxSeqSize)
+       }
+
+       hiInt, err := cast.ToIntE(hi)
+       if err != nil || hiInt < 0 || hiInt > maxSeqSize {
+               return nil, fmt.Errorf("the maximum requested value (%v) must be a non-negative integer <= %d", hi, maxSeqSize)
+       }
+
+       if nInt == 0 || hiInt == 0 {
+               return []int{}, nil
+       }
+
+       key := dKey{seed: uint64(seedInt), n: nInt, hi: hiInt}
+
+       v, err := ns.dCache.GetOrCreate(key, func() ([]int, error) {
+               if key.n > key.hi {
+                       result := make([]int, key.hi)
+                       for i := 0; i < key.hi; i++ {
+                               result[i] = i
+                       }
+                       return result, nil
+               }
+
                prng := rand.New(rand.NewPCG(key.seed, 0))
                result := make([]int, 0, key.n)
                _d(prng, key.n, key.hi, func(i int) {
                        result = append(result, i)
                })
+
                return result, nil
        })
-       return v
+
+       return v, err
 }
 
 type intersector struct {
index 75623d1fdaaca27419bed4027061302f5752c729..2d0a6c7df5b0962b0c212f1e0a1b87f7d2a47ac2 100644 (file)
@@ -788,30 +788,73 @@ func TestUniq(t *testing.T) {
 
 func TestD(t *testing.T) {
        t.Parallel()
-       c := qt.New(t)
        ns := newNs()
 
-       c.Assert(ns.D(42, 5, 100), qt.DeepEquals, []int{24, 34, 66, 82, 96})
-       c.Assert(ns.D(31, 5, 100), qt.DeepEquals, []int{12, 37, 38, 69, 98})
-       c.Assert(ns.D(42, 9, 10), qt.DeepEquals, []int{0, 1, 2, 3, 4, 6, 7, 8, 9})
-       c.Assert(ns.D(42, 10, 10), qt.DeepEquals, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
-       c.Assert(ns.D(42, 11, 10), qt.IsNil) // n > hi
-       c.Assert(ns.D(42, -5, 100), qt.IsNil)
-       c.Assert(ns.D(42, 0, 100), qt.IsNil)
-       c.Assert(ns.D(42, 5, 0), qt.IsNil)
-       c.Assert(ns.D(42, 5, -10), qt.IsNil)
-       c.Assert(ns.D(42, 5, 3000000), qt.DeepEquals, []int{720363, 1041693, 2009179, 2489106, 2873969})
-       c.Assert(func() { ns.D(31, 2000000, 3000000) }, qt.PanicMatches, "size of result exceeds limit")
+       const (
+               errNumberOfRequestedValuesRegexPattern = `.*the number of requested values.*`
+               errMaximumRequestedValueRegexPattern   = `.*the maximum requested value.*`
+               errSeedValueRegexPattern               = `.*the seed value.*`
+       )
+
+       tests := []struct {
+               name        string
+               seed        any
+               n           any
+               hi          any
+               wantResult  []int
+               wantErrText string
+       }{
+               // n <= hi
+               {"seed_eq_42", 42, 5, 100, []int{24, 34, 66, 82, 96}, ""},
+               {"seed_eq_31", 31, 5, 100, []int{12, 37, 38, 69, 98}, ""},
+               {"n_lt_hi", 42, 9, 10, []int{0, 1, 2, 3, 4, 6, 7, 8, 9}, ""},
+               {"n_eq_hi", 42, 10, 10, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, ""},
+               {"hi_eq_max_size", 42, 5, maxSeqSize, []int{240121, 347230, 669726, 829701, 957989}, ""},
+               // n > hi
+               {"n_gt_hi", 42, 11, 10, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, ""},
+               // zero values
+               {"seed_eq_0", 0, 5, 100, []int{0, 2, 29, 50, 72}, ""},
+               {"n_eq_0", 42, 0, 100, []int{}, ""},
+               {"hi_eq_0", 42, 5, 0, []int{}, ""},
+               // errors: values < 0
+               {"seed_lt_0", -42, 5, 100, nil, errSeedValueRegexPattern},
+               {"n_lt_0", 42, -1, 100, nil, errNumberOfRequestedValuesRegexPattern},
+               {"hi_lt_0", 42, 5, -100, nil, errMaximumRequestedValueRegexPattern},
+               // errors: values that can't be cast to int
+               {"seed_invalid_type", "foo", 5, 100, nil, errSeedValueRegexPattern},
+               {"n_invalid_type", 42, "bar", 100, nil, errNumberOfRequestedValuesRegexPattern},
+               {"hi_invalid_type", 42, 5, "baz", nil, errMaximumRequestedValueRegexPattern},
+               // errors: values that exceed maxSeqSize
+               {"n_gt_max_size", 42, maxSeqSize + 1, 10, nil, errNumberOfRequestedValuesRegexPattern},
+               {"hi_gt_max_size", 42, 5, maxSeqSize + 1, nil, errMaximumRequestedValueRegexPattern},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+                       c := qt.New(t)
+
+                       got, err := ns.D(tt.seed, tt.n, tt.hi)
+
+                       if tt.wantErrText != "" {
+                               c.Assert(err, qt.ErrorMatches, tt.wantErrText, qt.Commentf("n=%d, hi=%d", tt.n, tt.hi))
+                               c.Assert(got, qt.IsNil, qt.Commentf("Expected nil result on error"))
+                               return
+                       }
+
+                       c.Assert(err, qt.IsNil, qt.Commentf("Did not expect an error, but got: %v", err))
+                       c.Assert(got, qt.DeepEquals, tt.wantResult)
+               })
+       }
 }
 
 func BenchmarkD2(b *testing.B) {
        ns := newNs()
-
-       runBenchmark := func(seed, n, max int) {
+       runBenchmark := func(seed, n, max any) {
                name := fmt.Sprintf("n=%d,max=%d", n, max)
                b.Run(name, func(b *testing.B) {
                        for b.Loop() {
-                               ns.D(seed, n, max)
+                               _, _ = ns.D(seed, n, max)
                        }
                })
        }