From: Joe Mooring Date: Sun, 16 Nov 2025 19:23:14 +0000 (-0800) Subject: tpl/collections: Improve collections.D X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=c6b6910a7914356f9bfa2283d32d55fed58d6b69;p=brevno-suite%2Fhugo tpl/collections: Improve collections.D 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 --- diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 6b580b31c..f10466fd9 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -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 { diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index 75623d1fd..2d0a6c7df 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -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) } }) }