Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ You may also find the [Upgrade Guide](https://rust-random.github.io/book/update.

## [Unreleased]
- Fix feature `simd_support` for recent nightly rust (#1586)
- Add `Alphabetic` distribution. (#1587)

## [0.9.0] - 2025-01-27
### Security and unsafe
Expand Down
3 changes: 2 additions & 1 deletion benches/benches/standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use core::time::Duration;
use criterion::measurement::WallTime;
use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion};
use rand::distr::{Alphanumeric, Open01, OpenClosed01, StandardUniform};
use rand::distr::{Alphabetic, Alphanumeric, Open01, OpenClosed01, StandardUniform};
use rand::prelude::*;
use rand_pcg::Pcg64Mcg;

Expand Down Expand Up @@ -52,6 +52,7 @@ pub fn bench(c: &mut Criterion) {
do_ty!(f32, f64);
do_ty!(char);

bench_ty::<u8, Alphabetic>(&mut g, "Alphabetic");
bench_ty::<u8, Alphanumeric>(&mut g, "Alphanumeric");

bench_ty::<f32, Open01>(&mut g, "Open01/f32");
Expand Down
6 changes: 5 additions & 1 deletion src/distr/distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ mod tests {
#[test]
#[cfg(feature = "alloc")]
fn test_dist_string() {
use crate::distr::{Alphanumeric, SampleString, StandardUniform};
use crate::distr::{Alphabetic, Alphanumeric, SampleString, StandardUniform};
use core::str;
let mut rng = crate::test::rng(213);

Expand All @@ -261,5 +261,9 @@ mod tests {
let s2 = StandardUniform.sample_string(&mut rng, 20);
assert_eq!(s2.chars().count(), 20);
assert_eq!(str::from_utf8(s2.as_bytes()), Ok(s2.as_str()));

let s3 = Alphabetic.sample_string(&mut rng, 20);
assert_eq!(s3.len(), 20);
assert_eq!(str::from_utf8(s3.as_bytes()), Ok(s3.as_str()));
}
}
8 changes: 6 additions & 2 deletions src/distr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
//! numbers of the `char` type; in contrast [`StandardUniform`] may sample any valid
//! `char`.
//!
//! There's also an [`Alphabetic`] distribution which acts similarly to [`Alphanumeric`] but
//! doesn't include digits.
//!
//! For floats (`f32`, `f64`), [`StandardUniform`] samples from `[0, 1)`. Also
//! provided are [`Open01`] (samples from `(0, 1)`) and [`OpenClosed01`]
//! (samples from `(0, 1]`). No option is provided to sample from `[0, 1]`; it
Expand Down Expand Up @@ -104,7 +107,7 @@ pub use self::bernoulli::{Bernoulli, BernoulliError};
pub use self::distribution::SampleString;
pub use self::distribution::{Distribution, Iter, Map};
pub use self::float::{Open01, OpenClosed01};
pub use self::other::Alphanumeric;
pub use self::other::{Alphabetic, Alphanumeric};
#[doc(inline)]
pub use self::uniform::Uniform;

Expand All @@ -126,7 +129,8 @@ use crate::Rng;
/// code points in the range `0...0x10_FFFF`, except for the range
/// `0xD800...0xDFFF` (the surrogate code points). This includes
/// unassigned/reserved code points.
/// For some uses, the [`Alphanumeric`] distribution will be more appropriate.
/// For some uses, the [`Alphanumeric`] or [`Alphabetic`] distribution will be more
/// appropriate.
/// * `bool` samples `false` or `true`, each with probability 0.5.
/// * Floating point types (`f32` and `f64`) are uniformly distributed in the
/// half-open range `[0, 1)`. See also the [notes below](#floating-point-implementation).
Expand Down
69 changes: 69 additions & 0 deletions src/distr/other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Alphanumeric;

/// Sample a [`u8`], uniformly distributed over letters:
/// a-z and A-Z.
///
/// # Example
///
/// You're able to generate random Alphabetic characters via mapping or via the
/// [`SampleString::sample_string`] method like so:
///
/// ```
/// use rand::Rng;
/// use rand::distr::{Alphabetic, SampleString};
///
/// // Manual mapping
/// let mut rng = rand::rng();
/// let chars: String = (0..7).map(|_| rng.sample(Alphabetic) as char).collect();
/// println!("Random chars: {}", chars);
///
/// // Using [`SampleString::sample_string`]
/// let string = Alphabetic.sample_string(&mut rand::rng(), 16);
/// println!("Random string: {}", string);
/// ```
///
/// # Passwords
///
/// Refer to [`Alphanumeric#Passwords`].
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Alphabetic;

// ----- Implementations of distributions -----

impl Distribution<char> for StandardUniform {
Expand Down Expand Up @@ -123,6 +152,17 @@ impl Distribution<u8> for Alphanumeric {
}
}

impl Distribution<u8> for Alphabetic {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> u8 {
const RANGE: u8 = 26 + 26;

let offset = rng.random_range(0..RANGE) + b'A';

// Account for upper-cases
offset + (offset > b'Z') as u8 * (b'a' - b'Z' - 1)
}
}

#[cfg(feature = "alloc")]
impl SampleString for Alphanumeric {
fn append_string<R: Rng + ?Sized>(&self, rng: &mut R, string: &mut String, len: usize) {
Expand All @@ -133,6 +173,20 @@ impl SampleString for Alphanumeric {
}
}

#[cfg(feature = "alloc")]
impl SampleString for Alphabetic {
fn append_string<R: Rng + ?Sized>(&self, rng: &mut R, string: &mut String, len: usize) {
// SAFETY: With this distribution we guarantee that we're working with valid ASCII
// characters.
// See [#1590](https://github.com/rust-random/rand/issues/1590).
unsafe {
Comment thread
dhardy marked this conversation as resolved.
let v = string.as_mut_vec();
v.reserve_exact(len);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we don't need to use reserve when using extend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we don't need to use reserve when using extend

Thought the same but on my 1 million markdown headings project from earlier project it yields noticeably better performance.

With reserve_exact:

generate_markdown  0.54s user 0.30s system 94% cpu 0.883 total
generate_markdown  0.53s user 0.32s system 92% cpu 0.925 total
generate_markdown  0.53s user 0.21s system 99% cpu 0.747 total

Without reserve_exact:

generate_markdown  0.79s user 0.32s system 95% cpu 1.167 total
generate_markdown  0.79s user 0.31s system 93% cpu 1.181 total
generate_markdown  0.77s user 0.33s system 94% cpu 1.164 total

And its not like we'd need more than we use when the method itself takes len as an argument.

When opening std the definition is this:

    fn extend<I: IntoIterator<Item = char>>(&mut self, iter: I) {
        let iterator = iter.into_iter();
        let (lower_bound, _) = iterator.size_hint();
        self.reserve(lower_bound);
        iterator.for_each(move |c| self.push(c));
    }

Yet using a reserve_exact before it yields much better performance when just generating an exact string like so:

Alphabetic.sample_string(rng, 128)

Honestly, I'm unsure why it's that much more performant, but it's been consistently like that. Feel free to also test it out if you want: taboc/utils/generate_markdown/.

v.extend(self.sample_iter(rng).take(len));
}
}
}

impl Distribution<bool> for StandardUniform {
#[inline]
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> bool {
Expand Down Expand Up @@ -294,6 +348,20 @@ mod tests {
assert!(!incorrect);
}

#[test]
fn test_alphabetic() {
let mut rng = crate::test::rng(806);

// Test by generating a relatively large number of chars, so we also
// take the rejection sampling path.
let mut incorrect = false;
for _ in 0..100 {
let c: char = rng.sample(Alphabetic).into();
incorrect |= !c.is_ascii_alphabetic();
}
assert!(!incorrect);
}

#[test]
fn value_stability() {
fn test_samples<T: Copy + core::fmt::Debug + PartialEq, D: Distribution<T>>(
Expand Down Expand Up @@ -321,6 +389,7 @@ mod tests {
],
);
test_samples(&Alphanumeric, 0, &[104, 109, 101, 51, 77]);
test_samples(&Alphabetic, 0, &[97, 102, 89, 116, 75]);
test_samples(&StandardUniform, false, &[true, true, false, true, false]);
test_samples(
&StandardUniform,
Expand Down