Last active
January 29, 2019 17:07
-
-
Save a-voronov/12fcc2139fa2d14e31b256b57ef83f27 to your computer and use it in GitHub Desktop.
Lightweight Random utils inspired by RandomKit, using seedable Xoroshiro generator from CwlUtils. Sourcery templates included.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
{# So far getting imports hardcoded from config, correct approach might be found here: https://github.com/krzysztofzablocki/Sourcery/issues/670 #} | |
{% for import in argument.imports %} | |
import {{ import }} | |
{% endfor %} | |
{% if argument.testable %}{% for testable in argument.testable %} | |
@testable import {{ testable }} | |
{% endfor %}{% endif %} | |
{% macro randomValue type %}{% if type.kind == "protocol" %}{{ type.inheritedTypes.0.name }}{% endif %}{% endmacro %} | |
// MARK: - Structs | |
{# Random Struct #} | |
{% macro rng %}&{{ argument.rng }}{% endmacro %} | |
{% macro customRandomValueType variable %}{% if variable.annotations.random %}{{ variable.annotations.random }}{% endif %}{% endmacro %} | |
{% macro randomValueUsingGenerator variable %}{% call customRandomValueType variable %}{% if variable.isTuple %}randomTuple(using: &generator){% else %}.random(using: &generator){% endif %}{% endmacro %} | |
{% macro randomValueUsingRNG variable %}{% call customRandomValueType variable %}{% if variable.isTuple %}randomTuple(using: {% call rng %}){% else %}.random(using: {% call rng %}){% endif %}{% endmacro %} | |
{% for type in types.structs where type|annotated:"Random" %} | |
extension {{ type.name }}: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> {{ type.name }} { | |
return {{ type.name }}( | |
{% for variable in type.variables where not variable.isComputed %} | |
{{ variable.name }}: {% call randomValueUsingGenerator variable %}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) | |
} | |
{% if argument.rng %} | |
{# user-friendly version, so that you don't have to bother with closures and incoming generators. But it's tightly coupled to `rng` argument value from sourcery config #} | |
public static func random( | |
{% for variable in type.variables where not variable.isComputed %} | |
{{ variable.name }}: {{ variable.typeName }} = {% call randomValueUsingRNG variable %}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) -> {{ type.name }} { | |
return {{ type.name }}( | |
{% for variable in type.variables where not variable.isComputed %} | |
{{ variable.name }}: {{ variable.name }}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) | |
} | |
{% else %} | |
{# this one is generated additionally, so that you can have everything random except for some selected fields #} | |
public static func random<G: RandomNumberGenerator>( | |
_ generator: inout G, | |
{% for variable in type.variables where not variable.isComputed %} | |
{{ variable.name }}: (inout G) -> {{ variable.typeName }} = { generator in {% call randomValueUsingGenerator variable %} }{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) -> {{ type.name }} { | |
return {{ type.name }}( | |
{% for variable in type.variables where not variable.isComputed %} | |
{{ variable.name }}: {{ variable.name }}(&generator){% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) | |
} | |
{% endif %} | |
} | |
{% endfor %} | |
// MARK: - Enums | |
{# Random Enum #} | |
{% macro randomEnumCaseUsingGenerator case %}.{{ case.name }}{% if case.hasAssociatedValue %}({% for value in case.associatedValues %}{% if value.localName %}{{ value.localName }}: {% endif %}{% call randomValueUsingGenerator value %}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endmacro %} | |
{% macro randomIfCaseUsingGenerator case %}{% if case.hasAssociatedValue %}{{ case.name }}(&generator){% else %}.{{ case.name }}{% endif %}{% endmacro %} | |
{% macro randomEnumCaseUsingRNG case %}.{{ case.name }}{% if case.hasAssociatedValue %}({% for value in case.associatedValues %}{% if value.localName %}{{ value.localName }}: {% endif %}{% call randomValueUsingRNG value %}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endmacro %} | |
{% macro randomIfCaseUsingRNG case %}{% if case.hasAssociatedValue %}{{ case.name }}{% else %}.{{ case.name }}{% endif %}{% endmacro %} | |
{% for enum in types.enums where enum|annotated:"Random" %} | |
{% if enum.cases.count == 0 %} | |
#warning("`{{ enum.name }}` is an uninhabitant type and no value of it can be created, thus it can't have random value as well.") | |
// extension {{ enum.name }}: Random { } | |
{% elif not enum.hasAssociatedValues %} | |
extension {{ enum.name }}: Random { | |
{# I'd expect to check `enum.based` property, but it's always empty ¯\_(ツ)_/¯ #} | |
{% if enum.rawTypeName.name != "CaseIterable" and enum.inheritedTypes|join:" "|!contains:"CaseIterable" %} | |
#warning("Please, conform to `CaseIterable` protocol, so that compiler takes care of this") | |
public static let allCases: [{{ enum.name }}] = [{% for case in enum.cases %}.{{ case.name }}{% if not forloop.last %}, {% endif %}{% endfor %}] | |
{% endif %} | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> {{ enum.name }} { | |
return allCases.randomElement(using: &generator)! | |
} | |
} | |
{% else %} | |
extension {{ enum.name }}: RandomAll { | |
public static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [{{ enum.name }}] { | |
{% if enum.cases.count == 1 %} | |
return [{% call randomEnumCaseUsingGenerator enum.cases.0 %}] | |
{% else %} | |
return [{% for case in enum.cases %} | |
{% call randomEnumCaseUsingGenerator case %}{% if not forloop.last %},{% endif %}{% endfor %} | |
] | |
{% endif %} | |
} | |
{# | |
alternative version, so that you don't have to bother with closures and incoming generators | |
but it's tightly coupled to `rng` argument value from sourcery config | |
and it executes and calculates randoms for all cases even though only one will be needed (thus a bit more expensive) | |
#} | |
{% if argument.rng %} | |
public static func random( | |
{% for case in enum.cases where case.associatedValues %} | |
{{ case.name }}: {{ enum.name }} = {% call randomEnumCaseUsingRNG case %}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) -> {{ enum.name }} { | |
{% if enum.cases.count == 1 %} | |
return {% call randomIfCaseUsingRNG enum.cases.0 %} | |
{% else %} | |
return [ | |
{% for case in enum.cases %} | |
{% call randomIfCaseUsingRNG case %}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
].randomElement(using: {% call rng %})! | |
{% endif %} | |
} | |
{% else %} | |
{# same for enum - you can override `random` for some cases and leave others generated by default #} | |
public static func random<G: RandomNumberGenerator>( | |
_ generator: inout G, | |
{% for case in enum.cases where case.associatedValues %} | |
{{ case.name }}: (inout G) -> {{ enum.name }} = { generator in {% call randomEnumCaseUsingGenerator case %} }{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
) -> {{ enum.name }} { | |
{% if enum.cases.count == 1 %} | |
return {% call randomIfCaseUsingGenerator enum.cases.0 %} | |
{% else %} | |
return [ | |
{% for case in enum.cases %} | |
{% call randomIfCaseUsingGenerator case %}{% if not forloop.last %},{% endif %} | |
{% endfor %} | |
].randomElement(using: &generator)! | |
{% endif %} | |
} | |
{% endif %} | |
} | |
{% endif %} | |
{% endfor %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - Random | |
public protocol Random { | |
static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self | |
} | |
// MARK: - Dictionary | |
extension Dictionary: Random where Key: Random, Value: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> [Key: Value] { | |
return random(ofLength: 5, using: &generator) | |
} | |
} | |
extension Dictionary where Key: Random, Value: Random { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> [Key: Value] { | |
return (0 ..< length).reduce([:]) { (acc, _) in | |
var res = acc | |
res[Key.random(using: &generator)] = Value.random(using: &generator) | |
return res | |
} | |
} | |
} | |
extension Dictionary where Key: Hashable & Strideable, Value: Hashable & Strideable { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, inKeys keys: [Key], inValues values: [Value], using generator: inout G) -> [Key: Value] { | |
precondition(!keys.isEmpty, "keys range shouldn't be empty") | |
precondition(!values.isEmpty, "values range shouldn't be empty") | |
return (0 ..< length).reduce([:]) { (acc, _) in | |
var res = acc | |
res[keys.randomElement(using: &generator)!] = values.randomElement(using: &generator)! | |
return res | |
} | |
} | |
} | |
// MARK: - Set | |
extension Set: Random where Element: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Set<Element> { | |
return random(ofLength: 5, using: &generator) | |
} | |
} | |
extension Set where Element: Random { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> Set<Element> { | |
var buffer = Set() | |
while buffer.count != length { | |
buffer.insert(Element.random(using: &generator)) | |
} | |
return buffer | |
} | |
} | |
// MARK: - Array | |
extension Array: Random where Element: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> [Element] { | |
return random(ofLength: 5, using: &generator) | |
} | |
} | |
extension Array where Element: Random { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, using generator: inout G) -> [Element] { | |
return (0 ..< length).map { _ in Element.random(using: &generator) } | |
} | |
} | |
extension Array { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, from array: [Element], using generator: inout G) -> [Element] { | |
guard !array.isEmpty else { return [] } | |
return (0 ..< length).map { _ in array.randomElement(using: &generator)! } | |
} | |
} | |
// MARK: - String | |
extension String: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> String { | |
var result = UnicodeScalarView() | |
for _ in 0 ..< 10 { | |
result.append(UnicodeScalar.random(using: &generator)) | |
} | |
return String(result) | |
} | |
} | |
extension String { | |
public static func random<G: RandomNumberGenerator>(ofLength length: UInt, from string: String, using generator: inout G) -> String { | |
guard !string.isEmpty else { return "" } | |
return (0 ..< length).reduce("") { (acc, _) in | |
var res = acc | |
res.append(string.randomElement(using: &generator)!) | |
return res | |
} | |
} | |
} | |
// MARK: - Character | |
extension Character: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Character { | |
return Character(UnicodeScalar.random(using: &generator)) | |
} | |
} | |
extension Character { | |
// Can't convert Character into UnicodeScalar or into UInt, thus constraining it with UnicodeScalar range | |
public static func random<G: RandomNumberGenerator>(in closedRange: ClosedRange<UnicodeScalar>, using generator: inout G) -> Character { | |
return Character(UnicodeScalar.random(in: closedRange, using: &generator)) | |
} | |
} | |
extension Character { | |
// Can't convert Character into UnicodeScalar or into UInt, thus constraining it with UnicodeScalar range | |
public static func random<G: RandomNumberGenerator>(in range: Range<UnicodeScalar>, using generator: inout G) -> Character { | |
return Character(UnicodeScalar.random(in: range, using: &generator)) | |
} | |
} | |
// MARK: - UnicodeScalar | |
extension UnicodeScalar: Random { | |
static let randomRange: ClosedRange<UnicodeScalar> = " " ... "~" | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> UnicodeScalar { | |
return UnicodeScalar.random(in: randomRange, using: &generator) | |
} | |
} | |
extension UnicodeScalar { | |
public static func random<G: RandomNumberGenerator>(in closedRange: ClosedRange<UnicodeScalar>, using generator: inout G) -> UnicodeScalar { | |
let newRange = ClosedRange(uncheckedBounds: (lower: closedRange.lowerBound.value, upper: closedRange.upperBound.value)) | |
let random = UInt32.random(in: newRange, using: &generator) | |
return UnicodeScalar(random)! | |
} | |
} | |
extension UnicodeScalar { | |
public static func random<G: RandomNumberGenerator>(in range: Range<UnicodeScalar>, using generator: inout G) -> UnicodeScalar { | |
let newRange = Range(uncheckedBounds: (lower: range.lowerBound.value, upper: range.upperBound.value)) | |
let random = UInt32.random(in: newRange, using: &generator) | |
return UnicodeScalar(random)! | |
} | |
} | |
// MARK: - FloatingPoint | |
extension FloatingPoint where Self: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self { | |
return Self(UInt.random(using: &generator)) / Self(UInt.max) | |
} | |
} | |
extension Float: Random { } | |
extension Double: Random { } | |
// MARK: - Integer | |
extension FixedWidthInteger where Self: Random { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self { | |
return Self.random(in: Self.min...Self.max, using: &generator) | |
} | |
} | |
// MARK: Int | |
extension Int: Random { } | |
extension Int64: Random { } | |
extension Int32: Random { } | |
extension Int16: Random { } | |
extension Int8: Random { } | |
// MARK: UInt | |
extension UInt: Random { } | |
extension UInt64: Random { } | |
extension UInt32: Random { } | |
extension UInt16: Random { } | |
extension UInt8: Random { } | |
// MARK: - Bool | |
extension Bool: Random { } | |
// MARK: - Tuples | |
public func randomTuple< | |
A: Random, | |
B: Random, | |
G: RandomNumberGenerator | |
>(using generator: inout G) -> (A, B) { | |
return ( | |
.random(using: &generator), | |
.random(using: &generator) | |
) | |
} | |
public func randomTuple< | |
A: Random, | |
B: Random, | |
C: Random, | |
G: RandomNumberGenerator | |
>(using generator: inout G) -> (A, B, C) { | |
return ( | |
.random(using: &generator), | |
.random(using: &generator), | |
.random(using: &generator) | |
) | |
} | |
public func randomTuple< | |
A: Random, | |
B: Random, | |
C: Random, | |
D: Random, | |
G: RandomNumberGenerator | |
>(using generator: inout G) -> (A, B, C, D) { | |
return ( | |
.random(using: &generator), | |
.random(using: &generator), | |
.random(using: &generator), | |
.random(using: &generator) | |
) | |
} | |
// MARK: - RandomAll | |
public protocol RandomAll: Random { | |
static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [Self] | |
} | |
extension RandomAll { | |
public static func random<G: RandomNumberGenerator>(using generator: inout G) -> Self { | |
return allRandom(using: &generator).randomElement(using: &generator)! | |
} | |
} | |
extension RandomAll { | |
public static func allRandom<G: RandomNumberGenerator>(where predicate: (Self) -> Bool, using generator: inout G) -> [Self] { | |
return allRandom(using: &generator).filter(predicate) | |
} | |
public static func allRandom<G: RandomNumberGenerator>(except predicate: (Self) -> Bool, using generator: inout G) -> [Self] { | |
return allRandom(using: &generator).filter { !predicate($0) } | |
} | |
} | |
extension RandomAll where Self: Equatable { | |
public static func allRandom<G: RandomNumberGenerator>(where item: Self, using generator: inout G) -> [Self] { | |
return allRandom(where: { $0 == item }, using: &generator) | |
} | |
public static func allRandom<G: RandomNumberGenerator>(except item: Self, using generator: inout G) -> [Self] { | |
return allRandom(except: { $0 == item }, using: &generator) | |
} | |
} | |
extension RandomAll { | |
public static func allRandom<G: RandomNumberGenerator>(where keyPath: KeyPath<Self, Bool>, using generator: inout G) -> [Self] { | |
return allRandom(where: { $0[keyPath: keyPath] }, using: &generator) | |
} | |
public static func allRandom<G: RandomNumberGenerator>(except keyPath: KeyPath<Self, Bool>, using generator: inout G) -> [Self] { | |
return allRandom(except: { $0[keyPath: keyPath] }, using: &generator) | |
} | |
} | |
// MARK: - Optional | |
extension Optional: RandomAll where Wrapped: Random { | |
public static func allRandom<G: RandomNumberGenerator>(using generator: inout G) -> [Wrapped?] { | |
return [.none, .some(.random(using: &generator))] | |
} | |
} | |
extension Optional: Random where Wrapped: Random {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2008-2018 Matt Gallagher (http://cocoawithlove.com). All rights reserved. | |
// | |
// Permission to use, copy, modify, and/or distribute this software for any | |
// purpose with or without fee is hereby granted, provided that the above | |
// copyright notice and this permission notice appear in all copies. | |
// | |
// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING | |
// FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, | |
// NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH | |
// THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
public struct Xoroshiro: RandomNumberGenerator { | |
public typealias State = (UInt64, UInt64, UInt64, UInt64) | |
public private(set) var state: State = (0, 0, 0, 0) | |
public init() { | |
var generator = SystemRandomNumberGenerator() | |
state = randomTuple(using: &generator) | |
} | |
public init(seed: State) { | |
state = seed | |
} | |
public mutating func next() -> UInt64 { | |
// Derived from public domain implementation of xoshiro256** here: | |
// http://xoshiro.di.unimi.it | |
// by David Blackman and Sebastiano Vigna | |
let x = state.1 &* 5 | |
let result = ((x &<< 7) | (x &>> 57)) &* 9 | |
let t = state.1 &<< 17 | |
state.2 ^= state.0 | |
state.3 ^= state.1 | |
state.1 ^= state.2 | |
state.0 ^= state.3 | |
state.2 ^= t | |
state.3 = (state.3 &<< 45) | (state.3 &>> 19) | |
return result | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Random
Swift: 4.2
Random
protocol is mostly needed to know which entities can be generated and thus to extend existing API using this knowledge.RandomAll
protocol is mostly needed for enums with associated values to implement (there'sCaseIterable
for ones without associated values).Xoroshiro
is needed as a seedable generator (very useful when repeating tests with data generated randomly) and is taken from CwlUtils.random.stencil
Sourcery template is used to automatically extend Structs and Enums to supportRandom
orRandomAll
protocols.args
key:testable
- which adds@testable
imports (list).imports
- which just adds needed imports as we're not able to tell 100% what type is used from what Module (list).rng
- in case you already have globally accessible RNG variable, it will generate additionalrandom
helper method for your Struct with default (random) values for each propertyExample
As a result, you end up with 2 versions of random methods generated:
rng
defined in Sourcery config), with generated randoms by default, but giving ability to customize (via closures)rng
defined in Sourcery config), but tight-coupling to sourcery configrng
argument value (and might seem inefficiency in case of enums)