Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3 new sequence-based operators #29

Merged
merged 8 commits into from
Jan 25, 2022
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
19 changes: 19 additions & 0 deletions Sources/Genything/gen/build/Gen+iterate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

// MARK: Build

public extension Gen {
/// Returns: A generator which produces values from the provided sequence (in order) or `nil` if the sequence becomes exhausted
///
/// - Parameters:
/// - sequence: Sequence of values which will be produced in order
///
/// - Returns: The generator
static func iterate<S: Sequence>(_ sequence: S) -> Gen<T?> where S.Element == T {
var iterator = sequence.makeIterator()

return Gen<T?> { _ in
iterator.next()
}
}
}
59 changes: 59 additions & 0 deletions Sources/Genything/gen/build/Gen+looping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation

// MARK: Build

public extension Gen where T: Comparable {

private static func assertSequenceNotEmpty<S: Sequence>(_ sequence: S) {
var iterator = sequence.makeIterator()
assert(iterator.next() != nil, "`Gen.looping(sequence:)` was invoked with an empty sequence")
}

/// Returns: A generator which produces values from the sequence in sequential order.
/// If the sequence is exhausted it will be restarted.
///
/// e.g. `Gen.looping(["a", "b"])` will always output "abababababab"...
/// e.g. `Gen.looping(0...1)` will always output "0101010101"...
///
/// - Parameters:
/// - sequence: Sequence of values which will be produced in order. Looping if necessary.
///
/// - Returns: The generator
static func looping<S: Sequence>(_ sequence: S) -> Gen where S.Element == T {
assertSequenceNotEmpty(sequence)

var iterator = sequence.makeIterator()
return Gen<T> { _ in
if let next = iterator.next() {
return next
} else {
iterator = sequence.makeIterator()
return iterator.next()!
}
}
}

/// Returns: A generator which combines the output of a sequence of generators into a single generator.
///
/// Maintains sequential execution without interleaving emissions
///
/// e.g. `Gen.looping([Gen.constant("a"), Gen.constant("b")])` will always output "abababababab"...
///
/// - Parameters:
/// - sequence: Sequence of generators from which values will be selected from (in-order)
///
/// - Returns: The generator
static func looping<S: Sequence>(_ sequence: S) -> Gen where S.Element == Gen<T> {
assertSequenceNotEmpty(sequence)

var iterator = sequence.makeIterator()
return Gen<T> { ctx in
if let next = iterator.next() {
return next.generate(context: ctx)
} else {
iterator = sequence.makeIterator()
return iterator.next()!.generate(context: ctx)
}
}
}
}
10 changes: 2 additions & 8 deletions Sources/Genything/gen/combine/Gen+edgeCases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ public extension Gen {
/// Returns: A generator which randomly selects values from either the receiver or the edge cases
///
/// - Note: The probability is determined by `Context.edgeCaseProbability`
///
/// - Parameters:
/// - edgeCases: An array of edgecases
///
/// - Parameter edgeCases: An array of edgecases
/// - Returns: The generator
func withEdgeCases(_ edgeCases: [T]) -> Gen<T> {
Gen<T>.inContext { ctx in
Expand All @@ -20,10 +17,7 @@ public extension Gen {
/// Returns: A generator which randomly selects values from either the receiver or the edge cases
///
/// - Note: The probability is determined by `Context.edgeCaseProbability`
///
/// - Parameters:
/// - edgeCases: Another generator which may get selected to produce values
///
/// - Parameter edgeCases: Another generator which may get selected to produce values
/// - Returns: The generator
func withEdgeCases(_ edgeCases: Gen<T>) -> Gen<T> {
Gen<T>.inContext { ctx in
Expand Down
21 changes: 21 additions & 0 deletions Sources/Genything/gen/combine/Gen+startWith.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

// MARK: Combine

public extension Gen {
/// Returns: A generator which produces values by the provided sequence. When exhausted the generator will switch to producing values from the receiving generator.
///
/// - Parameters:
/// - values: Array of values which will be produced in order before the receiver takes over
///
/// - Returns: The generator
func startWith<S: Sequence>(_ sequence: S) -> Gen<T> where S.Element == T {
Gen.iterate(sequence).flatMap {
if let value = $0 {
return Gen.constant(value)
} else {
return self
}
}
}
}
21 changes: 20 additions & 1 deletion Sources/Genything/gen/mutate/Gen+filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public extension Gen {
///
/// - Parameters:
/// - isIncluded: A function which returns true if the value should be included
/// - maxDepth: The maximum amount of times `isIncluded` may return false in succession
///
/// - Returns: A `Gen` generator.
func filter(_ isIncluded: @escaping (T) -> Bool) -> Gen<T> {
Expand All @@ -29,4 +28,24 @@ public extension Gen {
return value
}
}

/// Returns: A generator that only produces values which pass the test `isIncluded`
///
/// - Warning: If the filtered condition is rare enough this function will become infinitely complex and will run forever
/// e.g. `Int.arbitrary.filter { $0 == 999 }` has a `$1/Int.max$` probability of occuring and will be nearly infinite
///
/// - Parameters:
/// - isIncluded: A function which returns true if the value should be included
///
/// - Returns: A `Gen` generator.
func filterForever(_ isIncluded: @escaping (T) -> Bool) -> Gen<T> {
Gen<T> { ctx in
while(true) {
let value = generate(context: ctx)
if isIncluded(value) {
return value
}
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/Genything/gen/produce/Gen+sample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public extension SafeGen {
///
/// - Returns: An array of sample values
func samples(count: Int = 20) throws -> [T] {
try take(count: count, context: .random)
try take(count, context: .random)
}

/// Returns: A single non-deterministic random sample of the generator's values
Expand Down
6 changes: 3 additions & 3 deletions Sources/Genything/gen/produce/Gen+take.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public extension SafeGen {
/// - context: The context to be used for generation
///
/// - Returns: An array of generated values
func take(count: Int? = nil,
func take(_ count: Int? = nil,
context: Context = .default) throws -> [T] {
try (0..<(count ?? context.maxIterations)).map { _ in
try generate(context: context)
Expand All @@ -26,8 +26,8 @@ public extension Gen {
/// - context: The context to be used for generation
///
/// - Returns: An array of generated values
func take(count: Int? = nil,
func take(_ count: Int? = nil,
context: Context = .default) -> [T] {
try! safe.take(count: count, context: context)
try! safe.take(count, context: context)
}
}
2 changes: 1 addition & 1 deletion Sources/GenythingTest/XCTest/Gen+xctest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public extension Gen {
case .failure(let info):
switch info.reason {
case let .predicate(value):
fail("assertForAll failed with value: `\(value)`", context: context, file: file, line: line)
fail("assertForAll failed for generated value: `\(value)` after `\(info.iteration) iterations.", context: context, file: file, line: line)
case let .error(error):
fail(error, context: context, file: file, line: line)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final internal class FoundationArbitraryTests: XCTestCase {

func test_dateAbitraryCanGenerateRandomDates() {
var datesSet: Set<Date> = Set()
Date.arbitrary.unique().take(count: arbitraryCount).forEach {
Date.arbitrary.unique().take(arbitraryCount).forEach {
datesSet.insert($0)
}
XCTAssertEqual(datesSet.count, arbitraryCount)
Expand Down
4 changes: 2 additions & 2 deletions Tests/GenythingTests/arbitrary/PizzaArbitraryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final internal class PizzaArbitraryTests: XCTestCase {

// Read as:
// For any pizza produced by `pizzaGen`, the pizza is a cheese pizza
pizzaGen.take(count: 100) /// Takes 100 random pizzas
pizzaGen.take(100) /// Takes 100 random pizzas
.forEach { (pizza: Pizza) in
XCTAssertTrue(pizza.isCheesePizza)
}
Expand Down Expand Up @@ -57,7 +57,7 @@ final internal class PizzaArbitraryTests: XCTestCase {
let pizzaDistribution = Gen<Pizza>.either(left: hawaiianPizzaGen, right: pepperoniPizzaGen, rightProbability: 0.75)

// Take a statistical sample of pizzas
let pizzas = pizzaDistribution.take(count: 1000)
let pizzas = pizzaDistribution.take(1000)

func pizzaQuotient(_ pizzaName: String) -> Double {
let countOfPizzaName = pizzas.filter { $0.name == pizzaName }.count
Expand Down
2 changes: 1 addition & 1 deletion Tests/GenythingTests/arbitrary/SwiftArbitraryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest

final internal class SwiftArbitraryTests: XCTestCase {
func test_stringGeneration_createsDifferentStrings_withDifferentSizes() throws {
let strings = String.arbitrary.take(count: 2)
let strings = String.arbitrary.take(2)

XCTAssertEqual(2, strings.count)
XCTAssertNotEqual(strings[0], strings[1])
Expand Down
6 changes: 3 additions & 3 deletions Tests/GenythingTests/examples/DiceRollerExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final internal class DiceRollerExampleTests: XCTestCase {

func test_d6_occurences() {
// All values occur in a small sample size
let occurences = d6.take(count: 50)
let occurences = d6.take(50)
XCTAssertTrue(occurences.contains(1))
XCTAssertTrue(occurences.contains(2))
XCTAssertTrue(occurences.contains(3))
Expand All @@ -27,7 +27,7 @@ final internal class DiceRollerExampleTests: XCTestCase {

func test_d6_mean() {
// With a large sample size the mean is very close to 3.5
let occurences = d6.take(count: 1_000_000)
let occurences = d6.take(1_000_000)
let mean = Double(occurences.reduce(0, +)) / Double(occurences.count)
XCTAssertEqual(mean, 3.5, accuracy: 0.005)
}
Expand Down Expand Up @@ -76,7 +76,7 @@ final internal class DiceRollerExampleTests: XCTestCase {
}

let occurences = twoDaggers
.take(count: 1000)
.take(1000)

XCTAssertTrue(occurences.contains(4))
XCTAssertTrue(occurences.contains(5))
Expand Down
4 changes: 2 additions & 2 deletions Tests/GenythingTests/gen/build/GenExhaustTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final internal class GenExhaustTests: XCTestCase {
gen.assertForAll { 0...10 ~= $0 }

let count = 100
let values = gen.take(count: count)
let values = gen.take(count)

XCTAssertEqual(0, values[0])
XCTAssertEqual(1, values[1])
Expand All @@ -26,7 +26,7 @@ final internal class GenExhaustTests: XCTestCase {
let gen2 = Gen.exhaust([0, 1, 2], then: other)

let count = 100
let values = gen1.zip(with: gen2).take(count: count)
let values = gen1.zip(with: gen2).take(count)

XCTAssertEqual(0, values[0].0)
XCTAssertEqual(0, values[0].1)
Expand Down
41 changes: 41 additions & 0 deletions Tests/GenythingTests/gen/build/GenIterateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import XCTest
@testable import Genything

final internal class GenIterateTests: XCTestCase {
func test_the_sequence_generates_expected_values() {
var expected = 0
Gen
.iterate(0...Int.max)
.assertForAll {
defer {
expected += 1
}
return expected == $0
}
}


func test_exhausted_sequence_generates_nil() {
let countNonNil = 10
var i = 0
Gen
.iterate(0..<10)
.assertForAll {
defer {
i += 1
}
if i < countNonNil {
return $0 != nil
}
return $0 == nil
}
}

func test_empty_sequence_generates_nil() {
Gen
.iterate([])
.assertForAll {
$0 == nil
}
}
}
33 changes: 33 additions & 0 deletions Tests/GenythingTests/gen/build/GenLoopingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import XCTest
@testable import Genything

final internal class GenLoopingTests: XCTestCase {
func test_a_long_sequence_never_loops_but_generates_expected_values() {
var expected = 0
Gen
.looping(0...Int.max)
.assertForAll(iterations: 1000) {
defer {
expected += 1
}
return expected == $0
}
}

func test_a_short_sequence_loops_with_expected_values() {
var expected = 0
Gen
.looping(0...10)
.assertForAll(iterations: 1000) {
defer {
if expected == 10 {
expected = 0
} else {
expected += 1
}
}
print($0)
return expected == $0
}
}
}
2 changes: 1 addition & 1 deletion Tests/GenythingTests/gen/combine/GenOneOfTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final internal class GenOneOfTests: XCTestCase {
// Count which bucket we are taking from
let countGen0 = Gen
.one(of: [gen0, gen1])
.take(count: iterations)
.take(iterations)
.filter { $0 == 0 }
.count

Expand Down
6 changes: 3 additions & 3 deletions Tests/GenythingTests/gen/combine/GenOrTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final internal class GenOrTests: XCTestCase {
let trueCount = Gen
.constant(false)
.or(.constant(true))
.take(count: totalCount)
.take(totalCount)
.filter { $0 }
.count

Expand All @@ -19,7 +19,7 @@ final internal class GenOrTests: XCTestCase {
let trueCount = Gen
.constant(false)
.or(.constant(true), otherProbability: 0.0)
.take(count: totalCount)
.take(totalCount)
.filter { $0 }
.count

Expand All @@ -31,7 +31,7 @@ final internal class GenOrTests: XCTestCase {
let trueCount = Gen
.constant(false)
.or(.constant(true), otherProbability: 1.0)
.take(count: totalCount)
.take(totalCount)
.filter { $0 }
.count

Expand Down
Loading