├── .github
├── DOCS.md
├── codecov.yml
├── dependabot.yml
└── workflows
│ ├── check.yml
│ ├── scheduled.yml
│ └── test.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── benches
└── zipf_bench.rs
└── src
└── lib.rs
/.github/DOCS.md:
--------------------------------------------------------------------------------
1 | # Github config and workflows
2 |
3 | In this folder there is configuration for codecoverage, dependabot, and ci
4 | workflows that check the library more deeply than the default configurations.
5 |
6 | This folder can be or was merged using a --allow-unrelated-histories merge
7 | strategy from which provides a
8 | reasonably sensible base for writing your own ci on. By using this strategy
9 | the history of the CI repo is included in your repo, and future updates to
10 | the CI can be merged later.
11 |
12 | To perform this merge run:
13 |
14 | ```shell
15 | git remote add ci https://github.com/jonhoo/rust-ci-conf.git
16 | git fetch ci
17 | git merge --allow-unrelated-histories ci/main
18 | ```
19 |
20 | An overview of the files in this project is available at:
21 | , which contains some
22 | rationale for decisions and runs through an example of solving minimal version
23 | and OpenSSL issues.
24 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | # ref: https://docs.codecov.com/docs/codecovyml-reference
2 | coverage:
3 | # Hold ourselves to a high bar
4 | range: 85..100
5 | round: down
6 | precision: 1
7 | status:
8 | # ref: https://docs.codecov.com/docs/commit-status
9 | project:
10 | default:
11 | # Avoid false negatives
12 | threshold: 1%
13 |
14 | # Test files aren't important for coverage
15 | ignore:
16 | - "tests"
17 |
18 | # Make comments less noisy
19 | comment:
20 | layout: "files"
21 | require_changes: true
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: cargo
8 | directory: /
9 | schedule:
10 | interval: daily
11 | ignore:
12 | - dependency-name: "*"
13 | # patch and minor updates don't matter for libraries as consumers of this library build
14 | # with their own lockfile, rather than the version specified in this library's lockfile
15 | # remove this ignore rule if your package has binaries to ensure that the binaries are
16 | # built with the exact set of dependencies and those are up to date.
17 | update-types:
18 | - "version-update:semver-patch"
19 | - "version-update:semver-minor"
20 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | # This workflow runs whenever a PR is opened or updated, or a commit is pushed to main. It runs
2 | # several checks:
3 | # - fmt: checks that the code is formatted according to rustfmt
4 | # - clippy: checks that the code does not contain any clippy warnings
5 | # - doc: checks that the code can be documented without errors
6 | # - hack: check combinations of feature flags
7 | # - msrv: check that the msrv specified in the crate is correct
8 | permissions:
9 | contents: read
10 | # This configuration allows maintainers of this repo to create a branch and pull request based on
11 | # the new branch. Restricting the push trigger to the main branch ensures that the PR only gets
12 | # built once.
13 | on:
14 | push:
15 | branches: [main]
16 | pull_request:
17 | # If new code is pushed to a PR branch, then cancel in progress workflows for that PR. Ensures that
18 | # we don't waste CI time, and returns results quicker https://github.com/jonhoo/rust-ci-conf/pull/5
19 | concurrency:
20 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
21 | cancel-in-progress: true
22 | name: check
23 | jobs:
24 | fmt:
25 | runs-on: ubuntu-latest
26 | name: stable / fmt
27 | steps:
28 | - uses: actions/checkout@v4
29 | with:
30 | submodules: true
31 | - name: Install stable
32 | uses: dtolnay/rust-toolchain@stable
33 | with:
34 | components: rustfmt
35 | - name: cargo fmt --check
36 | run: cargo fmt --check
37 | clippy:
38 | runs-on: ubuntu-latest
39 | name: ${{ matrix.toolchain }} / clippy
40 | permissions:
41 | contents: read
42 | checks: write
43 | strategy:
44 | fail-fast: false
45 | matrix:
46 | # Get early warning of new lints which are regularly introduced in beta channels.
47 | toolchain: [stable, beta]
48 | steps:
49 | - uses: actions/checkout@v4
50 | with:
51 | submodules: true
52 | - name: Install ${{ matrix.toolchain }}
53 | uses: dtolnay/rust-toolchain@master
54 | with:
55 | toolchain: ${{ matrix.toolchain }}
56 | components: clippy
57 | - name: cargo clippy
58 | uses: giraffate/clippy-action@v1
59 | with:
60 | reporter: 'github-pr-check'
61 | github_token: ${{ secrets.GITHUB_TOKEN }}
62 | semver:
63 | runs-on: ubuntu-latest
64 | name: semver
65 | steps:
66 | - uses: actions/checkout@v4
67 | with:
68 | submodules: true
69 | - name: Install stable
70 | uses: dtolnay/rust-toolchain@stable
71 | with:
72 | components: rustfmt
73 | - name: cargo-semver-checks
74 | uses: obi1kenobi/cargo-semver-checks-action@v2
75 | doc:
76 | # run docs generation on nightly rather than stable. This enables features like
77 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an
78 | # API be documented as only available in some specific platforms.
79 | runs-on: ubuntu-latest
80 | name: nightly / doc
81 | steps:
82 | - uses: actions/checkout@v4
83 | with:
84 | submodules: true
85 | - name: Install nightly
86 | uses: dtolnay/rust-toolchain@nightly
87 | - name: Install cargo-docs-rs
88 | uses: dtolnay/install@cargo-docs-rs
89 | - name: cargo docs-rs
90 | run: cargo docs-rs
91 | hack:
92 | # cargo-hack checks combinations of feature flags to ensure that features are all additive
93 | # which is required for feature unification
94 | runs-on: ubuntu-latest
95 | name: ubuntu / stable / features
96 | steps:
97 | - uses: actions/checkout@v4
98 | with:
99 | submodules: true
100 | - name: Install stable
101 | uses: dtolnay/rust-toolchain@stable
102 | - name: cargo install cargo-hack
103 | uses: taiki-e/install-action@cargo-hack
104 | # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4
105 | # --feature-powerset runs for every combination of features
106 | - name: cargo hack
107 | run: cargo hack --feature-powerset check
108 | msrv:
109 | # check that we can build using the minimal rust version that is specified by this crate
110 | runs-on: ubuntu-latest
111 | # we use a matrix here just because env can't be used in job names
112 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
113 | strategy:
114 | matrix:
115 | msrv: ["1.63.0"] # libc
116 | name: ubuntu / ${{ matrix.msrv }}
117 | steps:
118 | - uses: actions/checkout@v4
119 | with:
120 | submodules: true
121 | - name: Install ${{ matrix.msrv }}
122 | uses: dtolnay/rust-toolchain@master
123 | with:
124 | toolchain: ${{ matrix.msrv }}
125 | - name: cargo +${{ matrix.msrv }} check
126 | run: cargo check
127 |
--------------------------------------------------------------------------------
/.github/workflows/scheduled.yml:
--------------------------------------------------------------------------------
1 | # Run scheduled (rolling) jobs on a nightly basis, as your crate may break independently of any
2 | # given PR. E.g., updates to rust nightly and updates to this crates dependencies. See check.yml for
3 | # information about how the concurrency cancellation and workflow triggering works
4 | permissions:
5 | contents: read
6 | on:
7 | push:
8 | branches: [main]
9 | pull_request:
10 | schedule:
11 | - cron: '7 7 * * *'
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
14 | cancel-in-progress: true
15 | name: rolling
16 | jobs:
17 | # https://twitter.com/mycoliza/status/1571295690063753218
18 | nightly:
19 | runs-on: ubuntu-latest
20 | name: ubuntu / nightly
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | submodules: true
25 | - name: Install nightly
26 | uses: dtolnay/rust-toolchain@nightly
27 | - name: cargo generate-lockfile
28 | if: hashFiles('Cargo.lock') == ''
29 | run: cargo generate-lockfile
30 | - name: cargo test --locked
31 | run: cargo test --locked --all-features --all-targets
32 | # https://twitter.com/alcuadrado/status/1571291687837732873
33 | update:
34 | # This action checks that updating the dependencies of this crate to the latest available that
35 | # satisfy the versions in Cargo.toml does not break this crate. This is important as consumers
36 | # of this crate will generally use the latest available crates. This is subject to the standard
37 | # Cargo semver rules (i.e cargo does not update to a new major version unless explicitly told
38 | # to).
39 | runs-on: ubuntu-latest
40 | name: ubuntu / beta / updated
41 | # There's no point running this if no Cargo.lock was checked in in the first place, since we'd
42 | # just redo what happened in the regular test job. Unfortunately, hashFiles only works in if on
43 | # steps, so we repeat it.
44 | steps:
45 | - uses: actions/checkout@v4
46 | with:
47 | submodules: true
48 | - name: Install beta
49 | if: hashFiles('Cargo.lock') != ''
50 | uses: dtolnay/rust-toolchain@beta
51 | - name: cargo update
52 | if: hashFiles('Cargo.lock') != ''
53 | run: cargo update
54 | - name: cargo test
55 | if: hashFiles('Cargo.lock') != ''
56 | run: cargo test --locked --all-features --all-targets
57 | env:
58 | RUSTFLAGS: -D deprecated
59 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This is the main CI workflow that runs the test suite on all pushes to main and all pull requests.
2 | # It runs the following jobs:
3 | # - required: runs the test suite on ubuntu with stable and beta rust toolchains
4 | # - minimal: runs the test suite with the minimal versions of the dependencies that satisfy the
5 | # requirements of this crate, and its dependencies
6 | # - os-check: runs the test suite on mac and windows
7 | # - coverage: runs the test suite and collects coverage information
8 | # See check.yml for information about how the concurrency cancellation and workflow triggering works
9 | permissions:
10 | contents: read
11 | on:
12 | push:
13 | branches: [main]
14 | pull_request:
15 | concurrency:
16 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
17 | cancel-in-progress: true
18 | name: test
19 | jobs:
20 | required:
21 | runs-on: ubuntu-latest
22 | name: ubuntu / ${{ matrix.toolchain }}
23 | strategy:
24 | matrix:
25 | # run on stable and beta to ensure that tests won't break on the next version of the rust
26 | # toolchain
27 | toolchain: [stable, beta]
28 | steps:
29 | - uses: actions/checkout@v4
30 | with:
31 | submodules: true
32 | - name: Install ${{ matrix.toolchain }}
33 | uses: dtolnay/rust-toolchain@master
34 | with:
35 | toolchain: ${{ matrix.toolchain }}
36 | - name: cargo generate-lockfile
37 | # enable this ci template to run regardless of whether the lockfile is checked in or not
38 | if: hashFiles('Cargo.lock') == ''
39 | run: cargo generate-lockfile
40 | # https://twitter.com/jonhoo/status/1571290371124260865
41 | - name: cargo test --locked
42 | run: cargo test --locked --all-features
43 | minimal:
44 | # This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure
45 | # that this crate is compatible with the minimal version that this crate and its dependencies
46 | # require. This will pickup issues where this create relies on functionality that was introduced
47 | # later than the actual version specified (e.g., when we choose just a major version, but a
48 | # method was added after this version).
49 | #
50 | # This particular check can be difficult to get to succeed as often transitive dependencies may
51 | # be incorrectly specified (e.g., a dependency specifies 1.0 but really requires 1.1.5). There
52 | # is an alternative flag available -Zdirect-minimal-versions that uses the minimal versions for
53 | # direct dependencies of this crate, while selecting the maximal versions for the transitive
54 | # dependencies. Alternatively, you can add a line in your Cargo.toml to artificially increase
55 | # the minimal dependency, which you do with e.g.:
56 | # ```toml
57 | # # for minimal-versions
58 | # [target.'cfg(any())'.dependencies]
59 | # openssl = { version = "0.10.55", optional = true } # needed to allow foo to build with -Zminimal-versions
60 | # ```
61 | # The optional = true is necessary in case that dependency isn't otherwise transitively required
62 | # by your library, and the target bit is so that this dependency edge never actually affects
63 | # Cargo build order. See also
64 | # https://github.com/jonhoo/fantoccini/blob/fde336472b712bc7ebf5b4e772023a7ba71b2262/Cargo.toml#L47-L49.
65 | # This action is run on ubuntu with the stable toolchain, as it is not expected to fail
66 | runs-on: ubuntu-latest
67 | name: ubuntu / stable / minimal-versions
68 | steps:
69 | - uses: actions/checkout@v4
70 | with:
71 | submodules: true
72 | - name: Install stable
73 | uses: dtolnay/rust-toolchain@stable
74 | - name: Install nightly for -Zminimal-versions
75 | uses: dtolnay/rust-toolchain@nightly
76 | - name: rustup default stable
77 | run: rustup default stable
78 | - name: cargo update -Zminimal-versions
79 | run: cargo +nightly update -Zminimal-versions
80 | - name: cargo test
81 | run: cargo test --locked --all-features
82 | os-check:
83 | # run cargo test on mac and windows
84 | runs-on: ${{ matrix.os }}
85 | name: ${{ matrix.os }} / stable
86 | strategy:
87 | fail-fast: false
88 | matrix:
89 | os: [macos-latest, windows-latest]
90 | steps:
91 | # if your project needs OpenSSL, uncomment this to fix Windows builds.
92 | # it's commented out by default as the install command takes 5-10m.
93 | # - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
94 | # if: runner.os == 'Windows'
95 | # - run: vcpkg install openssl:x64-windows-static-md
96 | # if: runner.os == 'Windows'
97 | - uses: actions/checkout@v4
98 | with:
99 | submodules: true
100 | - name: Install stable
101 | uses: dtolnay/rust-toolchain@stable
102 | - name: cargo generate-lockfile
103 | if: hashFiles('Cargo.lock') == ''
104 | run: cargo generate-lockfile
105 | - name: cargo test
106 | run: cargo test --locked --all-features
107 | coverage:
108 | # use llvm-cov to build and collect coverage and outputs in a format that
109 | # is compatible with codecov.io
110 | #
111 | # note that codecov as of v4 requires that CODECOV_TOKEN from
112 | #
113 | # https://app.codecov.io/gh///settings
114 | #
115 | # is set in two places on your repo:
116 | #
117 | # - https://github.com/jonhoo/guardian/settings/secrets/actions
118 | # - https://github.com/jonhoo/guardian/settings/secrets/dependabot
119 | #
120 | # (the former is needed for codecov uploads to work with Dependabot PRs)
121 | #
122 | # PRs coming from forks of your repo will not have access to the token, but
123 | # for those, codecov allows uploading coverage reports without a token.
124 | # it's all a little weird and inconvenient. see
125 | #
126 | # https://github.com/codecov/feedback/issues/112
127 | #
128 | # for lots of more discussion
129 | runs-on: ubuntu-latest
130 | name: ubuntu / stable / coverage
131 | steps:
132 | - uses: actions/checkout@v4
133 | with:
134 | submodules: true
135 | - name: Install stable
136 | uses: dtolnay/rust-toolchain@stable
137 | with:
138 | components: llvm-tools-preview
139 | - name: cargo install cargo-llvm-cov
140 | uses: taiki-e/install-action@cargo-llvm-cov
141 | - name: cargo generate-lockfile
142 | if: hashFiles('Cargo.lock') == ''
143 | run: cargo generate-lockfile
144 | - name: cargo llvm-cov
145 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info
146 | - name: Record Rust version
147 | run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV"
148 | - name: Upload to codecov.io
149 | uses: codecov/codecov-action@v5
150 | with:
151 | fail_ci_if_error: true
152 | token: ${{ secrets.CODECOV_TOKEN }}
153 | env_vars: OS,RUST
154 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "byteorder"
7 | version = "1.5.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
10 |
11 | [[package]]
12 | name = "cfg-if"
13 | version = "1.0.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
16 |
17 | [[package]]
18 | name = "getrandom"
19 | version = "0.2.15"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
22 | dependencies = [
23 | "cfg-if",
24 | "libc",
25 | "wasi",
26 | ]
27 |
28 | [[package]]
29 | name = "libc"
30 | version = "0.2.169"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
33 |
34 | [[package]]
35 | name = "ppv-lite86"
36 | version = "0.2.20"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
39 | dependencies = [
40 | "zerocopy",
41 | ]
42 |
43 | [[package]]
44 | name = "proc-macro2"
45 | version = "1.0.92"
46 | source = "registry+https://github.com/rust-lang/crates.io-index"
47 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
48 | dependencies = [
49 | "unicode-ident",
50 | ]
51 |
52 | [[package]]
53 | name = "quote"
54 | version = "1.0.38"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
57 | dependencies = [
58 | "proc-macro2",
59 | ]
60 |
61 | [[package]]
62 | name = "rand"
63 | version = "0.8.5"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
66 | dependencies = [
67 | "libc",
68 | "rand_chacha",
69 | "rand_core",
70 | ]
71 |
72 | [[package]]
73 | name = "rand_chacha"
74 | version = "0.3.1"
75 | source = "registry+https://github.com/rust-lang/crates.io-index"
76 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
77 | dependencies = [
78 | "ppv-lite86",
79 | "rand_core",
80 | ]
81 |
82 | [[package]]
83 | name = "rand_core"
84 | version = "0.6.4"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
87 | dependencies = [
88 | "getrandom",
89 | ]
90 |
91 | [[package]]
92 | name = "syn"
93 | version = "2.0.93"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
96 | dependencies = [
97 | "proc-macro2",
98 | "quote",
99 | "unicode-ident",
100 | ]
101 |
102 | [[package]]
103 | name = "unicode-ident"
104 | version = "1.0.14"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
107 |
108 | [[package]]
109 | name = "wasi"
110 | version = "0.11.0+wasi-snapshot-preview1"
111 | source = "registry+https://github.com/rust-lang/crates.io-index"
112 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
113 |
114 | [[package]]
115 | name = "zerocopy"
116 | version = "0.7.35"
117 | source = "registry+https://github.com/rust-lang/crates.io-index"
118 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
119 | dependencies = [
120 | "byteorder",
121 | "zerocopy-derive",
122 | ]
123 |
124 | [[package]]
125 | name = "zerocopy-derive"
126 | version = "0.7.35"
127 | source = "registry+https://github.com/rust-lang/crates.io-index"
128 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
129 | dependencies = [
130 | "proc-macro2",
131 | "quote",
132 | "syn",
133 | ]
134 |
135 | [[package]]
136 | name = "zipf"
137 | version = "7.0.2"
138 | dependencies = [
139 | "rand",
140 | ]
141 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "zipf"
3 | version = "7.0.2"
4 | readme = "./README.md"
5 | description = "A fast generator of discrete, bounded Zipf-distributed random numbers"
6 | authors = ["Jon Gjengset "]
7 | edition = "2018"
8 |
9 | license = "Apache-2.0"
10 | keywords = ["random", "distribution", "zipf"]
11 | categories = ["algorithms"]
12 |
13 | repository = "https://github.com/jonhoo/rust-zipf"
14 | documentation = "https://docs.rs/zipf"
15 |
16 | [dependencies]
17 | rand = { version = "0.8.0", default-features = false }
18 |
19 | [dev-dependencies]
20 | rand = "0.8.0"
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!CAUTION]
2 | > This crate is deprecated. Prefer
3 | > [`rand_distr::Zipf`](https://docs.rs/rand_distr/latest/rand_distr/struct.Zipf.html).
4 |
5 | ---
6 |
7 | # rust-zipf
8 |
9 | [](https://crates.io/crates/zipf)
10 | [](https://docs.rs/zipf/)
11 | [](https://codecov.io/gh/jonhoo/rust-zipf)
12 | [](https://deps.rs/repo/github/jonhoo/rust-zipf)
13 |
14 | Rust implementation of a fast, discrete, bounded,
15 | [Zipf-distributed](https://en.wikipedia.org/wiki/Zipf's_law) random
16 | number generator. Compared to the implementation provided by
17 | [`randomkit`](https://github.com/stygstra/rust-randomkit) (which binds
18 | to NumPy's fork of RandomKit), this crate is approximately twice as
19 | fast:
20 |
21 | ```console
22 | $ cargo +nightly bench
23 | test tests::bench_randomkit ... bench: 339 ns/iter (+/- 18)
24 | test tests::bench_us ... bench: 68 ns/iter (+/- 1)
25 | test tests::bench_threadrng ... bench: 11 ns/iter (+/- 0)
26 | ```
27 |
28 | It is also both driven by, and provides, a [Rust random number
29 | generator](https://doc.rust-lang.org/rand/rand/trait.Rng.html).
30 |
31 | This implementation is effectively a direct port of Apache Common's
32 | [RejectionInversionZipfSampler](https://github.com/apache/commons-rng/blob/6a1b0c16090912e8fc5de2c1fb5bd8490ac14699/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/RejectionInversionZipfSampler.java),
33 | written in Java. It is based on the method described by Wolfgang Hörmann and Gerhard Derflinger
34 | in [*Rejection-inversion to generate variates from monotone discrete
35 | distributions*](https://dl.acm.org/citation.cfm?id=235029) from *ACM Transactions on Modeling
36 | and Computer Simulation (TOMACS) 6.3 (1996)*.
37 |
--------------------------------------------------------------------------------
/benches/zipf_bench.rs:
--------------------------------------------------------------------------------
1 | #![feature(test)]
2 | extern crate rand;
3 | extern crate test;
4 | extern crate zipf;
5 |
6 | use test::Bencher;
7 |
8 | #[bench]
9 | fn bench_us(b: &mut Bencher) {
10 | use rand;
11 | use rand::distributions::Distribution;
12 | use zipf::ZipfDistribution;
13 | let mut rng = rand::thread_rng();
14 | let us = ZipfDistribution::new(1000000, 1.07).unwrap();
15 | b.iter(|| us.sample(&mut rng));
16 | }
17 |
18 | #[bench]
19 | fn bench_threadrng(b: &mut Bencher) {
20 | use rand::{self, Rng};
21 | let mut rng = rand::thread_rng();
22 | b.iter(|| rng.gen::());
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! A fast generator of discrete, bounded
2 | //! [Zipf-distributed](https://en.wikipedia.org/wiki/Zipf's_law) random numbers.
3 | //!
4 | //! For a random variable `X` whose values are distributed according to this distribution, the
5 | //! probability mass function is given by
6 | //!
7 | //! ```ignore
8 | //! P(X = k) = H(N,s) * 1 / k^s for k = 1,2,...,N
9 | //! ```
10 | //!
11 | //! `H(N,s)` is the normalizing constant which corresponds to the generalized harmonic number
12 | //! of order `N` of `s`.
13 | //!
14 | //! # Example
15 | //!
16 | //! ```
17 | //! use rand::distributions::Distribution;
18 | //!
19 | //! let mut rng = rand::thread_rng();
20 | //! let mut zipf = zipf::ZipfDistribution::new(1000, 1.03).unwrap();
21 | //! let sample = zipf.sample(&mut rng);
22 | //! ```
23 | //!
24 | //! This implementation is effectively a direct port of Apache Common's
25 | //! [RejectionInversionZipfSampler][ref], written in Java. It is based on the method described by
26 | //! Wolfgang Hörmann and Gerhard Derflinger in [*Rejection-inversion to generate variates from
27 | //! monotone discrete distributions*](https://dl.acm.org/citation.cfm?id=235029) from *ACM
28 | //! Transactions on Modeling and Computer Simulation (TOMACS) 6.3 (1996)*.
29 | //!
30 | //! [ref]: https://github.com/apache/commons-rng/blob/6a1b0c16090912e8fc5de2c1fb5bd8490ac14699/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/RejectionInversionZipfSampler.java
31 |
32 | #![warn(rust_2018_idioms)]
33 |
34 | use rand::Rng;
35 |
36 | /// Random number generator that generates Zipf-distributed random numbers using rejection
37 | /// inversion.
38 | #[derive(Clone, Copy)]
39 | #[deprecated = "prefer rand_distr::Zipf"]
40 | pub struct ZipfDistribution {
41 | /// Number of elements
42 | num_elements: f64,
43 | /// Exponent parameter of the distribution
44 | exponent: f64,
45 | /// `hIntegral(1.5) - 1}`
46 | h_integral_x1: f64,
47 | /// `hIntegral(num_elements + 0.5)}`
48 | h_integral_num_elements: f64,
49 | /// `2 - hIntegralInverse(hIntegral(2.5) - h(2)}`
50 | s: f64,
51 | }
52 |
53 | impl ZipfDistribution {
54 | /// Creates a new [Zipf-distributed](https://en.wikipedia.org/wiki/Zipf's_law)
55 | /// random number generator.
56 | ///
57 | /// Note that both the number of elements and the exponent must be greater than 0.
58 | pub fn new(num_elements: usize, exponent: f64) -> Result {
59 | if num_elements == 0 {
60 | return Err(());
61 | }
62 | if exponent <= 0f64 {
63 | return Err(());
64 | }
65 |
66 | let z = ZipfDistribution {
67 | num_elements: num_elements as f64,
68 | exponent,
69 | h_integral_x1: ZipfDistribution::h_integral(1.5, exponent) - 1f64,
70 | h_integral_num_elements: ZipfDistribution::h_integral(
71 | num_elements as f64 + 0.5,
72 | exponent,
73 | ),
74 | s: 2f64
75 | - ZipfDistribution::h_integral_inv(
76 | ZipfDistribution::h_integral(2.5, exponent)
77 | - ZipfDistribution::h(2f64, exponent),
78 | exponent,
79 | ),
80 | };
81 |
82 | // populate cache
83 |
84 | Ok(z)
85 | }
86 | }
87 |
88 | impl ZipfDistribution {
89 | fn next(&self, rng: &mut R) -> usize {
90 | // The paper describes an algorithm for exponents larger than 1 (Algorithm ZRI).
91 | //
92 | // The original method uses
93 | // H(x) = (v + x)^(1 - q) / (1 - q)
94 | // as the integral of the hat function.
95 | //
96 | // This function is undefined for q = 1, which is the reason for the limitation of the
97 | // exponent.
98 | //
99 | // If instead the integral function
100 | // H(x) = ((v + x)^(1 - q) - 1) / (1 - q)
101 | // is used, for which a meaningful limit exists for q = 1, the method works for all
102 | // positive exponents.
103 | //
104 | // The following implementation uses v = 0 and generates integral number in the range [1,
105 | // num_elements]. This is different to the original method where v is defined to
106 | // be positive and numbers are taken from [0, i_max]. This explains why the implementation
107 | // looks slightly different.
108 |
109 | let hnum = self.h_integral_num_elements;
110 |
111 | loop {
112 | use std::cmp;
113 | let u: f64 = hnum + rng.gen::() * (self.h_integral_x1 - hnum);
114 | // u is uniformly distributed in (h_integral_x1, h_integral_num_elements]
115 |
116 | let x: f64 = ZipfDistribution::h_integral_inv(u, self.exponent);
117 |
118 | // Limit k to the range [1, num_elements] if it would be outside
119 | // due to numerical inaccuracies.
120 | let k64 = x.max(1.0).min(self.num_elements);
121 | // float -> integer rounds towards zero, so we add 0.5
122 | // to prevent bias towards k == 1
123 | let k = cmp::max(1, (k64 + 0.5) as usize);
124 |
125 | // Here, the distribution of k is given by:
126 | //
127 | // P(k = 1) = C * (hIntegral(1.5) - h_integral_x1) = C
128 | // P(k = m) = C * (hIntegral(m + 1/2) - hIntegral(m - 1/2)) for m >= 2
129 | //
130 | // where C = 1 / (h_integral_num_elements - h_integral_x1)
131 | if k64 - x <= self.s
132 | || u >= ZipfDistribution::h_integral(k64 + 0.5, self.exponent)
133 | - ZipfDistribution::h(k64, self.exponent)
134 | {
135 | // Case k = 1:
136 | //
137 | // The right inequality is always true, because replacing k by 1 gives
138 | // u >= hIntegral(1.5) - h(1) = h_integral_x1 and u is taken from
139 | // (h_integral_x1, h_integral_num_elements].
140 | //
141 | // Therefore, the acceptance rate for k = 1 is P(accepted | k = 1) = 1
142 | // and the probability that 1 is returned as random value is
143 | // P(k = 1 and accepted) = P(accepted | k = 1) * P(k = 1) = C = C / 1^exponent
144 | //
145 | // Case k >= 2:
146 | //
147 | // The left inequality (k - x <= s) is just a short cut
148 | // to avoid the more expensive evaluation of the right inequality
149 | // (u >= hIntegral(k + 0.5) - h(k)) in many cases.
150 | //
151 | // If the left inequality is true, the right inequality is also true:
152 | // Theorem 2 in the paper is valid for all positive exponents, because
153 | // the requirements h'(x) = -exponent/x^(exponent + 1) < 0 and
154 | // (-1/hInverse'(x))'' = (1+1/exponent) * x^(1/exponent-1) >= 0
155 | // are both fulfilled.
156 | // Therefore, f(x) = x - hIntegralInverse(hIntegral(x + 0.5) - h(x))
157 | // is a non-decreasing function. If k - x <= s holds,
158 | // k - x <= s + f(k) - f(2) is obviously also true which is equivalent to
159 | // -x <= -hIntegralInverse(hIntegral(k + 0.5) - h(k)),
160 | // -hIntegralInverse(u) <= -hIntegralInverse(hIntegral(k + 0.5) - h(k)),
161 | // and finally u >= hIntegral(k + 0.5) - h(k).
162 | //
163 | // Hence, the right inequality determines the acceptance rate:
164 | // P(accepted | k = m) = h(m) / (hIntegrated(m+1/2) - hIntegrated(m-1/2))
165 | // The probability that m is returned is given by
166 | // P(k = m and accepted) = P(accepted | k = m) * P(k = m)
167 | // = C * h(m) = C / m^exponent.
168 | //
169 | // In both cases the probabilities are proportional to the probability mass
170 | // function of the Zipf distribution.
171 |
172 | return k;
173 | }
174 | }
175 | }
176 | }
177 |
178 | impl rand::distributions::Distribution for ZipfDistribution {
179 | fn sample(&self, rng: &mut R) -> usize {
180 | self.next(rng)
181 | }
182 | }
183 |
184 | use std::fmt;
185 | impl fmt::Debug for ZipfDistribution {
186 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
187 | f.debug_struct("ZipfDistribution")
188 | .field("e", &self.exponent)
189 | .field("n", &self.num_elements)
190 | .finish()
191 | }
192 | }
193 |
194 | impl ZipfDistribution {
195 | /// Computes `H(x)`, defined as
196 | ///
197 | /// - `(x^(1 - exponent) - 1) / (1 - exponent)`, if `exponent != 1`
198 | /// - `log(x)`, if `exponent == 1`
199 | ///
200 | /// `H(x)` is an integral function of `h(x)`, the derivative of `H(x)` is `h(x)`.
201 | fn h_integral(x: f64, exponent: f64) -> f64 {
202 | let log_x = x.ln();
203 | helper2((1f64 - exponent) * log_x) * log_x
204 | }
205 |
206 | /// Computes `h(x) = 1 / x^exponent`
207 | fn h(x: f64, exponent: f64) -> f64 {
208 | (-exponent * x.ln()).exp()
209 | }
210 |
211 | /// The inverse function of `H(x)`.
212 | /// Returns the `y` for which `H(y) = x`.
213 | fn h_integral_inv(x: f64, exponent: f64) -> f64 {
214 | let mut t: f64 = x * (1f64 - exponent);
215 | if t < -1f64 {
216 | // Limit value to the range [-1, +inf).
217 | // t could be smaller than -1 in some rare cases due to numerical errors.
218 | t = -1f64;
219 | }
220 | (helper1(t) * x).exp()
221 | }
222 | }
223 |
224 | /// Helper function that calculates `log(1 + x) / x`.
225 | /// A Taylor series expansion is used, if x is close to 0.
226 | fn helper1(x: f64) -> f64 {
227 | if x.abs() > 1e-8 {
228 | x.ln_1p() / x
229 | } else {
230 | 1f64 - x * (0.5 - x * (1.0 / 3.0 - 0.25 * x))
231 | }
232 | }
233 |
234 | /// Helper function to calculate `(exp(x) - 1) / x`.
235 | /// A Taylor series expansion is used, if x is close to 0.
236 | fn helper2(x: f64) -> f64 {
237 | if x.abs() > 1e-8 {
238 | x.exp_m1() / x
239 | } else {
240 | 1f64 + x * 0.5 * (1f64 + x * 1.0 / 3.0 * (1f64 + 0.25 * x))
241 | }
242 | }
243 |
244 | #[cfg(test)]
245 | mod test {
246 | use super::ZipfDistribution;
247 | use rand::distributions::Distribution;
248 |
249 | #[inline]
250 | fn test(alpha: f64) {
251 | const N: usize = 100;
252 |
253 | // as the alpha increases, we need more samples, since the frequency in the tail grows so low
254 | let samples = (2.0f64.powf(alpha) * 5000000.0) as usize;
255 |
256 | let mut rng = rand::thread_rng();
257 | let zipf = ZipfDistribution::new(N, alpha).unwrap();
258 |
259 | let harmonic: f64 = (1..=N).map(|n| 1.0 / (n as f64).powf(alpha)).sum();
260 |
261 | // sample a bunch
262 | let mut buckets = vec![0; N];
263 | for _ in 0..samples {
264 | let sample = zipf.sample(&mut rng);
265 | buckets[sample - 1] += 1;
266 | }
267 |
268 | // for each bucket, see that frequency roughly matches what we expect
269 | // note that the ratios here are ratios _of fractions_, so 0.5 does not mean we're off by
270 | // 50%, it means we're off by 50% _of the frequency_. in other words, if the frequency was
271 | // supposed to be 0.10, and it was actually 0.05, that's a 50% difference.
272 | for i in 0..N {
273 | let freq = buckets[i] as f64 / samples as f64;
274 | let expected = (1.0 / ((i + 1) as f64).powf(alpha)) / harmonic;
275 | // println!("{} {} {}", i + 1, freq, expected);
276 |
277 | let off_by = (expected - freq).abs();
278 | assert!(off_by < 0.1); // never be off by more than 10% in absolute terms
279 |
280 | let good = off_by < expected;
281 | if !good {
282 | panic!("got {}, expected {} for k = {}", freq, expected, i + 1);
283 | }
284 | }
285 | }
286 |
287 | #[test]
288 | fn one() {
289 | test(1.00);
290 | }
291 |
292 | #[test]
293 | fn two() {
294 | test(2.00);
295 | }
296 |
297 | #[test]
298 | fn three() {
299 | test(3.00);
300 | }
301 |
302 | #[test]
303 | fn float() {
304 | test(1.08);
305 | }
306 |
307 | #[test]
308 | fn debug() {
309 | eprintln!("{:?}", ZipfDistribution::new(100, 1.0).unwrap());
310 | }
311 |
312 | #[test]
313 | fn errs() {
314 | ZipfDistribution::new(0, 1.0).unwrap_err();
315 | ZipfDistribution::new(100, 0.0).unwrap_err();
316 | ZipfDistribution::new(100, -1.0).unwrap_err();
317 | }
318 | }
319 |
--------------------------------------------------------------------------------