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
7 changes: 4 additions & 3 deletions src/array.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1604,9 +1604,10 @@ class Array(T)
shift { nil }
end

# Returns an array with all the elements in the collection randomized
# using the given *random* number generator.
def shuffle(random : Random = Random::DEFAULT) : Array(T)
# Returns a new instance with all elements in the collection randomized.
#
# See `Indexable::Mutable#shuffle!` for details.
def shuffle(random : Random? = nil) : Array(T)
dup.shuffle!(random)
end

Expand Down
2 changes: 1 addition & 1 deletion src/crystal/system/file.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module Crystal::System::File

LOWER_ALPHANUM = "0123456789abcdefghijklmnopqrstuvwxyz".to_slice

def self.mktemp(prefix : String?, suffix : String?, dir : String, random : ::Random = ::Random::DEFAULT) : {FileDescriptor::Handle, String, Bool}
def self.mktemp(prefix : String?, suffix : String?, dir : String, random : ::Random? = nil) : {FileDescriptor::Handle, String, Bool}
flags = LibC::O_RDWR | LibC::O_CREAT | LibC::O_EXCL
perm = ::File::Permissions.new(0o600)

Expand Down
56 changes: 39 additions & 17 deletions src/enumerable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1565,19 +1565,27 @@ module Enumerable(T)
reject { |e| pattern === e }
end

# Returns an `Array` of *n* random elements from `self`, using the given
# *random* number generator. All elements have equal probability of being
# drawn. Sampling is done without replacement; if *n* is larger than the size
# of this collection, the returned `Array` has the same size as `self`.
# Returns an `Array` of *n* random elements from `self`. All elements have
# equal probability of being drawn. Sampling is done without replacement; if
# *n* is larger than the size of this collection, the returned `Array` has the
# same size as `self`.
#
# Raises `ArgumentError` if *n* is negative.
#
# ```
# [1, 2, 3, 4, 5].sample(2) # => [3, 5]
# {1, 2, 3, 4, 5}.sample(2) # => [3, 4]
# {1, 2, 3, 4, 5}.sample(2, Random.new(1)) # => [1, 5]
# [1, 2, 3, 4, 5].sample(2) # => [3, 5]
# {1, 2, 3, 4, 5}.sample(2) # => [3, 4]
# ```
def sample(n : Int, random : Random = Random::DEFAULT) : Array(T)
#
# Uses the *random* instance when provided if the randomness needs to be
# controlled or to follow some traits. For example the following calls use a
# custom seed or a secure random source:
#
# ```
# {1, 2, 3, 4, 5}.sample(2, Random.new(1)) # => [1, 5]
# {1, 2, 3, 4, 5}.sample(2, Random::Secure) # => [2, 5]
# ```
def sample(n : Int, random : Random? = nil) : Array(T)
raise ArgumentError.new("Can't sample negative number of elements") if n < 0

# Unweighted reservoir sampling:
Expand All @@ -1588,40 +1596,54 @@ module Enumerable(T)
ary = Array(T).new(n)
return ary if n == 0

# FIXME: thread unsafe (#each may yield and the fiber switch threads)
rng = random || Random::DEFAULT

each_with_index do |elem, i|
if i < n
ary << elem
else
j = random.rand(i + 1)
j = rng.rand(i + 1)
if j < n
ary.to_unsafe[j] = elem
end
end
end

ary.shuffle!(random)
ary.shuffle!(rng)
end

# Returns a random element from `self`, using the given *random* number
# generator. All elements have equal probability of being drawn.
# Returns a random element from `self`. All elements have equal probability of
# being drawn.
#
# Raises `IndexError` if `self` is empty.
#
# ```
# a = [1, 2, 3]
# a.sample # => 2
# a.sample # => 1
# a.sample(Random.new(1)) # => 3
# a.sample # => 2
# a.sample # => 1
# ```
def sample(random : Random = Random::DEFAULT) : T
#
# Uses the *random* instance when provided if the randomness needs to be
# controlled or to follow some traits. For example the following calls use a
# custom seed or a secure random source:
#
# ```
# a.sample(Random.new(1)) # => 3
# a.sample(Random::Secure) # => 1
# ```
def sample(random : Random? = nil) : T
value = uninitialized T
found = false

# FIXME: thread unsafe (#each may yield and the fiber switch threads)
rng = random || Random::DEFAULT

each_with_index do |elem, i|
if !found
value = elem
found = true
elsif random.rand(i + 1) == 0
elsif rng.rand(i + 1) == 0
value = elem
end
end
Expand Down
20 changes: 15 additions & 5 deletions src/indexable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -950,21 +950,31 @@ module Indexable(T)
#
# ```
# a = [1, 2, 3]
# a.sample # => 3
# a.sample # => 1
# a.sample # => 3
# a.sample # => 1
# ```
#
# Uses the *random* instance when provided if the randomness needs to be
# controlled or to follow some traits. For example the following sample will
# always return the same value:
#
# ```
# a = [1, 2, 3]
# a.sample(Random.new(1)) # => 2
# a.sample(Random.new(1)) # => 2
# ```
def sample(random : Random = Random::DEFAULT)
def sample(random : Random? = nil)
raise IndexError.new("Can't sample empty collection") if size == 0
unsafe_fetch(random.rand(size))
rng = random || Random::DEFAULT
unsafe_fetch(rng.rand(size))
end

# :inherit:
#
# If `self` is not empty and `n` is equal to 1, calls `sample(random)` exactly
# once. Thus, *random* will be left in a different state compared to the
# implementation in `Enumerable`.
def sample(n : Int, random : Random = Random::DEFAULT) : Array(T)
def sample(n : Int, random : Random? = nil) : Array(T)
return super unless n == 1

if empty?
Expand Down
20 changes: 16 additions & 4 deletions src/indexable/mutable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,29 @@ module Indexable::Mutable(T)
self
end

# Modifies `self` by randomizing the order of elements in the collection
# using the given *random* number generator. Returns `self`.
# Modifies `self` by randomizing the order of elements in the collection.
# Returns `self`.
#
# ```
# a = [1, 2, 3, 4, 5]
# a.shuffle! # => [3, 5, 2, 4, 1]
# a # => [3, 5, 2, 4, 1]
# ```
#
# Uses the *random* instance when provided if the randomness needs to be
# controlled or to follow some traits. For example the following shuffle will
# always result in the same order:
#
# ```
# a = [1, 2, 3, 4, 5]
# a.shuffle!(Random.new(42)) # => [3, 2, 4, 5, 1]
# a.shuffle!(Random.new(42)) # => [3, 2, 4, 5, 1]
# a # => [3, 2, 4, 5, 1]
# ```
def shuffle!(random : Random = Random::DEFAULT) : self
def shuffle!(random : Random? = nil) : self
rng = random || Random::DEFAULT
(size - 1).downto(1) do |i|
j = random.rand(i + 1)
j = rng.rand(i + 1)
swap(i, j)
end
self
Expand Down
5 changes: 3 additions & 2 deletions src/pointer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,10 @@ struct Pointer(T)
# ptr.shuffle!(4)
# ptr # [3, 4, 1, 2]
# ```
def shuffle!(count : Int, random = Random::DEFAULT)
def shuffle!(count : Int, random : Random? = nil)
rng = random || Random::DEFAULT
(count - 1).downto(1) do |i|
j = random.rand(i + 1)
j = rng.rand(i + 1)
swap(i, j)
end
self
Expand Down
22 changes: 13 additions & 9 deletions src/range.cr
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,13 @@ struct Range(B, E)
# the method simply calls `random.rand(self)`.
#
# Raises `ArgumentError` if `self` is an open range.
def sample(random : Random = Random::DEFAULT)
def sample(random : Random? = nil)
rng = random || Random::DEFAULT

{% if B < Int && E < Int %}
random.rand(self)
rng.rand(self)
{% elsif B < Float && E < Float %}
random.rand(self)
rng.rand(self)
{% elsif B.nilable? || E.nilable? %}
b = self.begin
e = self.end
Expand All @@ -357,7 +359,7 @@ struct Range(B, E)
raise ArgumentError.new("Can't sample an open range")
end

Range.new(b, e, @exclusive).sample(random)
Range.new(b, e, @exclusive).sample(rng)
{% else %}
super
{% end %}
Expand All @@ -368,7 +370,9 @@ struct Range(B, E)
# If `self` is not empty and `n` is equal to 1, calls `sample(random)` exactly
# once. Thus, *random* will be left in a different state compared to the
# implementation in `Enumerable`.
def sample(n : Int, random = Random::DEFAULT)
def sample(n : Int, random : Random? = nil)
rng = random || Random::DEFAULT

if self.begin.nil? || self.end.nil?
raise ArgumentError.new("Can't sample an open range")
end
Expand Down Expand Up @@ -402,11 +406,11 @@ struct Range(B, E)
# If we must return all values in the range...
if possible == available
result = Array(B).new(possible) { |i| min + i }
result.shuffle!(random)
result.shuffle!(rng)
return result
end

range_sample(n, random)
range_sample(n, rng)
{% elsif B < Float && E < Float %}
min = self.begin
max = self.end
Expand All @@ -419,13 +423,13 @@ struct Range(B, E)
return [min]
end

range_sample(n, random)
range_sample(n, rng)
{% else %}
case n
when 0
[] of B
when 1
[sample(random)]
[sample(rng)]
else
super
end
Expand Down
2 changes: 1 addition & 1 deletion src/slice.cr
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ struct Slice(T)
# :inherit:
#
# Raises if this slice is read-only.
def shuffle!(random : Random = Random::DEFAULT) : self
def shuffle!(random : Random? = nil) : self
check_writable
super
end
Expand Down