Skip to content

Commit 40c1433

Browse files
committed
Update API to match mailing list discussion, add tests
Added pick(_:) to collection Added random(in:using:) to Randomizable Added tests Fix typo in Randomizable documentation Rename pick to sampling Move sampling below random
1 parent 22ea738 commit 40c1433

File tree

5 files changed

+214
-34
lines changed

5 files changed

+214
-34
lines changed

stdlib/public/core/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ set(SWIFTLIB_ESSENTIAL
9898
Policy.swift
9999
PrefixWhile.swift.gyb
100100
Print.swift
101-
Random.swift
101+
Random.swift.gyb
102102
RandomAccessCollection.swift
103103
Range.swift.gyb
104104
RangeReplaceableCollection.swift.gyb

stdlib/public/core/Collection.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ public protocol Collection : Sequence
783783

784784
/// Returns a random element from this collection.
785785
///
786+
/// This uses the default random number generator provided by the standard library.
786787
/// A good example of this is getting a random number from 1 to 10:
787788
///
788789
/// let randomToTen = (1 ... 10).random
@@ -1048,7 +1049,7 @@ extension Collection {
10481049
///
10491050
/// - Parameter generator: The random number generator to use when getting
10501051
/// a random element.
1051-
/// - Returns: A random element this collection.
1052+
/// - Returns: A random element from this collection.
10521053
///
10531054
/// A good example of this is getting a random number from 1 to 10:
10541055
///
@@ -1064,7 +1065,7 @@ extension Collection {
10641065
@_inlineable
10651066
public func random<T: RandomNumberGenerator>(using generator: T) -> Element? {
10661067
guard !isEmpty else { return nil }
1067-
var random = generator.next(upperBound: UInt(self.count))
1068+
let random = generator.next(upperBound: UInt(self.count))
10681069
let index = self.index(
10691070
self.startIndex,
10701071
offsetBy: IndexDistance(random),
@@ -1073,6 +1074,38 @@ extension Collection {
10731074
return self[index!]
10741075
}
10751076

1077+
/// Randomly samples n elements from the collection
1078+
///
1079+
/// - Parameter n: Number of elements to randomly sample without replacement
1080+
/// from this collection.
1081+
/// - Returns: An array of randomly sampled elements from this collection.
1082+
@_inlineable
1083+
public func sampling(_ n: Int) -> [Element] {
1084+
return self.sampling(n, using: Random.default)
1085+
}
1086+
1087+
/// Randomly samples n elements from the collection
1088+
///
1089+
/// - Parameter n: Number of elements to randomly sample without replacement
1090+
/// from this collection.
1091+
/// - Parameter generator: The random number generator to use when getting
1092+
/// random elements.
1093+
/// - Returns: An array of randomly sampled elements from this collection.
1094+
@_inlineable
1095+
public func sampling<T: RandomNumberGenerator>(
1096+
_ n: Int,
1097+
using generator: T
1098+
) -> [Element] {
1099+
guard n < count, n >= 0 else { return Array(self) }
1100+
var copySelf = Array(self)
1101+
copySelf.shuffle(using: generator)
1102+
var arr = [Element]()
1103+
for _ in 0 ..< n {
1104+
arr.append(copySelf.removeFirst())
1105+
}
1106+
return arr
1107+
}
1108+
10761109
/// Do not use this method directly; call advanced(by: n) instead.
10771110
@_inlineable
10781111
@_versioned

stdlib/public/core/CollectionAlgorithms.swift.gyb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ extension MutableCollection {
267267
guard count > 1 else { return }
268268
var amount = count
269269
while amount > 1 {
270-
var random = generator.next(upperBound: UInt(count))
270+
let random = generator.next(upperBound: UInt(count))
271271
guard amount != random else { continue }
272272
amount -= 1
273273
swapAt(

stdlib/public/core/Random.swift renamed to stdlib/public/core/Random.swift.gyb

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===--- Random.swift -----------------------------------------------------===//
1+
//===--- Random.swift.gyb -------------------------------------*- swift -*-===//
22
//
33
// This source file is part of the Swift.org open source project
44
//
@@ -19,8 +19,8 @@ import SwiftShims
1919
/// that implement their own pseudo-random or cryptographically secure
2020
/// pseudo-random number generation. Using the RandomNumberGenerator protocol
2121
/// allows these types to be used with types that conform to `Randomizable`,
22-
/// collections with .random, and collections/sequences with .shuffle() and
23-
/// .shuffled()
22+
/// collections with .random and .pick(_:), and collections/sequences with
23+
/// .shuffle() and .shuffled()
2424
///
2525
/// Conforming to the RandomNumberGenerator protocol
2626
/// ==========================================
@@ -100,20 +100,20 @@ extension RandomNumberGenerator {
100100
/// Using those functions should be preferred over using this directly. An
101101
/// example of calling this directly:
102102
///
103-
/// let random = Random.default.next(Int.self)
104-
/// let randomToTen = Random.default.next(Int.self, upperBound: 10)
103+
/// let random = Random.default.next(UInt8.self)
104+
/// let randomToTen = Random.default.next(UInt32.self, upperBound: 128)
105105
///
106106
/// However, you should strive to use the random functions on the numeric types.
107107
/// Using the preferred way:
108108
///
109-
/// let random = Int.random
110-
/// let randomToTen = (0 ... 10).random
109+
/// let random = UInt8.random
110+
/// let randomToTen = UInt32.random(in: 0 ... 128)
111111
///
112112
/// - Note: The default implementation of randomness is cryptographically secure.
113113
/// It utilizes arc4random on newer versions of macOS, iOS, etc. On older
114-
/// versions of these operating systems it uses /dev/urandom and SecRandomCopyBytes
115-
/// on iOS. For Linux, it tries to use the getrandom(2) system call on newer
116-
/// kernel versions. On older kernel versions, it uses /dev/urandom.
114+
/// versions of these operating systems it uses SecRandomCopyBytes. For Linux,
115+
/// it tries to use the getrandom(2) system call on newer kernel versions. On
116+
/// older kernel versions, it reads from /dev/urandom.
117117
public struct Random : RandomNumberGenerator {
118118
/// This random number generator generates the machine's bit width unsigned integer
119119
public typealias GeneratedNumber = UInt
@@ -150,37 +150,26 @@ public struct Random : RandomNumberGenerator {
150150
/// =======================================
151151
///
152152
/// In order to conform to the Randomizable protocol, all one must implement is
153-
/// the random(using:) function. As an example, below is a custom Date structure:
153+
/// the random(using:) function. As an example, below is a custom Color structure:
154154
///
155-
/// struct Date {
156-
/// let year: Int
157-
/// let month: Int
158-
/// let day: Int
155+
/// struct Color {
156+
/// let value: UInt32
159157
/// }
160158
///
161-
/// In order for `Date` to conform to `Randomizable`, it must declare conformance
159+
/// In order for `Color` to conform to `Randomizable`, it must declare conformance
162160
/// and implement the random(using:) function.
163161
///
164-
/// extension Date : Randomizable {
162+
/// extension Color : Randomizable {
165163
/// static func random<T: RandomNumberGenerator>(using generator: T) -> Self {
166-
/// let randomYear = (0 ... 3000).random(using: generator)
167-
/// let randomMonth = (1 ... 12).random(using: generator)
168-
/// let randomDay = (1 ... 31).random(using: generator)
169-
/// // This could produce a date with day 31 on months without 31 days
170-
/// // Ideally, you should check for this
171-
/// return Date(year: randomYear, month: randomMonth, day: randomDay)
164+
/// let value = UInt32.random(in: 0x0 ... 0xFFFFFF, using: generator)
165+
/// return Color(value: value)
172166
/// }
173167
/// }
174168
///
175-
/// Note how the ranges use the random(using:) function that is defined for them.
169+
/// Note how the function .random(in:using:) is using custom generator argument.
176170
/// Make sure to do this for custom types as it allows developers the ability
177171
/// to use their own random number generators on custom types.
178172
public protocol Randomizable {
179-
/// The random representation of this type.
180-
///
181-
/// Shorthand for using the random(using:) function. This uses the default
182-
/// random implementation defined in the standard library.
183-
static var random: Self { get }
184173

185174
/// Returns a random representation of this type.
186175
///
@@ -191,7 +180,7 @@ public protocol Randomizable {
191180
}
192181

193182
extension Randomizable {
194-
/// The random representation of this type.
183+
/// Returns a random representation of this type.
195184
///
196185
/// Shorthand for using the random(using:) function. This uses the default
197186
/// random implementation defined in the standard library.
@@ -200,3 +189,63 @@ extension Randomizable {
200189
return self.random(using: Random.default)
201190
}
202191
}
192+
193+
% for Range in ['Range', 'ClosedRange']:
194+
195+
extension Randomizable where Self: BinaryFloatingPoint {
196+
/// Returns a random representation of this type within the range
197+
///
198+
/// - Parameter range: The range to select a random value from.
199+
/// - Returns: A random representation of this type within the range.
200+
///
201+
/// Shorthand for using the random(in:using:) function. This uses the default
202+
/// random implementation defined in the standard library.
203+
public static func random(in range: ${Range}<Self>) -> Self {
204+
return range.random!
205+
}
206+
207+
/// Returns a random representation of this type within the range
208+
///
209+
/// - Parameter range: The range to select a random value from.
210+
/// - Parameter generator: The random number generator to use when getting
211+
/// a random value from the range.
212+
/// - Returns: A random representation of this type within the range.
213+
public static func random<T: RandomNumberGenerator>(
214+
in range: ${Range}<Self>,
215+
using generator: T
216+
) -> Self {
217+
return range.random(using: generator)!
218+
}
219+
}
220+
221+
% end
222+
223+
% for Range in ['CountableRange', 'CountableClosedRange']:
224+
225+
extension Randomizable where Self: Strideable, Self.Stride: SignedInteger {
226+
/// Returns a random representation of this type within the range
227+
///
228+
/// - Parameter range: The range to select a random value from.
229+
/// - Returns: A random representation of this type within the range.
230+
///
231+
/// Shorthand for using the random(in:using:) function. This uses the default
232+
/// random implementation defined in the standard library.
233+
public static func random(in range: ${Range}<Self>) -> Self {
234+
return range.random!
235+
}
236+
237+
/// Returns a random representation of this type within the range
238+
///
239+
/// - Parameter range: The range to select a random value from.
240+
/// - Parameter generator: The random number generator to use when getting
241+
/// a random value from the range.
242+
/// - Returns: A random representation of this type within the range.
243+
public static func random<T: RandomNumberGenerator>(
244+
in range: ${Range}<Self>,
245+
using generator: T
246+
) -> Self {
247+
return range.random(using: generator)!
248+
}
249+
}
250+
251+
% end

test/stdlib/Random.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// RUN: %target-run-simple-swift
2+
// REQUIRES: executable_test
3+
4+
import StdlibUnittest
5+
6+
let RandomTests = TestSuite("Random")
7+
8+
RandomTests.test("random numbers") {
9+
let randomNumber1 = Int.random
10+
let randomNumber2 = Int.random
11+
expectTrue(randomNumber1 != randomNumber2)
12+
13+
let randomDouble1 = Double.random
14+
expectTrue(randomDouble1 < 1 && randomDouble1 > 0)
15+
let randomDouble2 = Double.random
16+
expectTrue(randomDouble1 < 1 && randomDouble2 > 0)
17+
expectTrue(randomDouble1 != randomDouble2)
18+
}
19+
20+
RandomTests.test("random numbers from range") {
21+
let range1 = 0 ..< 20
22+
for _ in range1 {
23+
let randomNumber = Int.random(in: range1)
24+
expectTrue(range1.contains(randomNumber))
25+
}
26+
27+
let range2 = 0 ... 20
28+
for _ in range1 {
29+
let randomNumber = Int.random(in: range2)
30+
expectTrue(range2.contains(randomNumber))
31+
}
32+
33+
let range3 = 3.0 ..< 10.0
34+
for _ in range1 {
35+
let randomNumber = Double.random(in: range3)
36+
expectTrue(range3.contains(randomNumber))
37+
}
38+
39+
let range4 = 3.0 ... 10.0
40+
for _ in range1 {
41+
let randomNumber = Double.random(in: range4)
42+
expectTrue(range4.contains(randomNumber))
43+
}
44+
}
45+
46+
RandomTests.test("random elements") {
47+
let greetings = ["hello", "hi", "hey", "hola", "what's up"]
48+
for _ in 0 ..< 20 {
49+
let randomGreeting = greetings.random
50+
expectNotNil(randomGreeting)
51+
expectTrue(greetings.contains(randomGreeting!))
52+
}
53+
}
54+
55+
RandomTests.test("sampling from an array") {
56+
let range = 0 ..< 20
57+
let array = Array(range)
58+
for i in range {
59+
let chosen = array.sampling(i)
60+
expectTrue(chosen.count == i)
61+
for choice in chosen {
62+
expectTrue(array.contains(choice))
63+
}
64+
}
65+
66+
// If we pick more elements than the array contains, check if count == array.count
67+
expectTrue(array.sampling(100).count == array.count)
68+
}
69+
70+
func chi2Test(_ samples: [Double]) -> Bool {
71+
let upperBound = 50
72+
let numberOfTrials = 500_000
73+
let expected = Double(numberOfTrials / upperBound)
74+
let cvLow = 28.9 // 1% with a degree of freedom of (50 - 1)
75+
let cvHigh = 74.9 // 99% with a degree of freedom of (50 - 1)
76+
let chi2 = samples.map {
77+
(($0 - expected) * ($0 - expected)) / expected
78+
}.reduce(0, +)
79+
80+
if chi2 < cvLow || chi2 > cvHigh {
81+
return false
82+
}else {
83+
return true
84+
}
85+
}
86+
87+
RandomTests.test("uniform distribution") {
88+
let upperBound = 50
89+
let numberOfTrials = 500_000
90+
var array = [Double](repeating: 0.0, count: upperBound)
91+
for _ in 0 ..< numberOfTrials {
92+
let randomIndex = Int.random(in: 0 ..< upperBound)
93+
array[randomIndex] += 1.0
94+
}
95+
expectTrue(chi2Test(array))
96+
}
97+
98+
runAllTests()

0 commit comments

Comments
 (0)