Last active
March 31, 2024 09:56
-
-
Save oskarirauta/b2be3039293aeb27dde20797252c8b55 to your computer and use it in GitHub Desktop.
OptionSetAssociated: Swift's OptionSet with associated value for members.
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 | |
public protocol OptionSetAssociated: OptionSet where RawValue: FixedWidthInteger { | |
var store: [RawValue: Any] { get set } | |
} | |
extension OptionSetAssociated { | |
public init<T>(_ member: Self, value: T) { | |
self.init(rawValue: member.rawValue) | |
self.store[member.rawValue] = value | |
} | |
public init<T>(rawValue: RawValue, value: T) { | |
self.init(rawValue: rawValue) | |
self.store[rawValue] = value | |
} | |
fileprivate init(rawValue: RawValue, store: [RawValue: Any]) { | |
self.init(rawValue: rawValue) | |
self.store = store | |
} | |
fileprivate static func combinedStore(_ old: [RawValue: Any], new: [RawValue: Any]) -> [RawValue: Any] { | |
new.map {$0.key}.reduce(into: old) { | |
$0[$1] = new[$1] ?? old[$1] | |
} | |
} | |
fileprivate static func storeOverride(_ store: [RawValue: Any], member: Self?, value: Any?) -> [RawValue: Any] { | |
guard let member: RawValue = member?.rawValue else { return store } | |
var store: [RawValue: Any] = store | |
store[member] = value | |
return store | |
} | |
public func getValue<T>(for member: Self) -> T? { | |
self.store[member.rawValue] as? T | |
} | |
mutating public func formUnion(_ other: __owned Self) { | |
self = Self(rawValue: self.rawValue | other.rawValue, store: Self.combinedStore(self.store, new: other.store)) | |
} | |
} | |
extension OptionSet where Self: OptionSetAssociated, Self == Element { | |
@discardableResult | |
public mutating func insert( | |
_ newMember: Element | |
) -> (inserted: Bool, memberAfterInsert: Element) { | |
let oldMember = self.intersection(newMember) | |
let shouldInsert = oldMember != newMember | |
var result = ( | |
inserted: shouldInsert, | |
memberAfterInsert: shouldInsert ? newMember : oldMember) | |
if shouldInsert { | |
self.formUnion(newMember) | |
} else { | |
self.store = Self.storeOverride( | |
Self.combinedStore(self.store, new: newMember.store), | |
member: newMember, value: newMember.store[newMember.rawValue]) | |
result.memberAfterInsert.store[newMember.rawValue] = newMember.store[newMember.rawValue] | |
} | |
return result | |
} | |
@discardableResult | |
public mutating func remove(_ member: Element) -> Element? { | |
var intersectionElements = intersection(member) | |
guard !intersectionElements.isEmpty else { | |
return nil | |
} | |
let store: [RawValue: Any] = self.store | |
self.subtract(member) | |
self.store = store | |
self.store[member.rawValue] = nil | |
intersectionElements.store = Self.storeOverride([:], member: member, value: store[member.rawValue]) | |
return intersectionElements | |
} | |
@discardableResult | |
public mutating func update(with newMember: Element) -> Element? { | |
let previousValue: Any? = self.store[newMember.rawValue] | |
var r = self.intersection(newMember) | |
self.formUnion(newMember) | |
self.store[newMember.rawValue] = newMember.store[newMember.rawValue] | |
if r.isEmpty { return nil } else { | |
r.store = Self.storeOverride([:], member: newMember, value: previousValue) | |
r.store[newMember.rawValue] = previousValue | |
return r | |
} | |
} | |
} | |
extension OptionSetAssociated where Self: Sequence { | |
public typealias Iterator = OptionSetAssociatedIterator | |
public func makeIterator() -> OptionSetAssociatedIterator<Self> { | |
OptionSetAssociatedIterator(element: self) | |
} | |
public static func - (lhs: Self, rhs: Self) -> Self { | |
rhs.reduce(into: lhs) { | |
$0.remove($1) | |
} | |
} | |
public static func + (lhs: Self, rhs: Self) -> Self { | |
rhs.reduce(into: lhs) { | |
$0.insert($1) | |
} | |
} | |
public static func += (lhs: inout Self, rhs: Self) { | |
lhs = lhs + rhs | |
} | |
public static func -= (lhs: inout Self, rhs: Self) { | |
lhs = lhs - rhs | |
} | |
} | |
extension OptionSetAssociated { // subscripts | |
public subscript<T>(for member: Self) -> T? { | |
self.store[member.rawValue] as? T | |
} | |
} |
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 | |
public struct OptionSetAssociatedIterator<Element: OptionSetAssociated>: IteratorProtocol { | |
private let value: Element | |
public init(element: Element) { | |
self.value = element | |
} | |
private lazy var remainingBits: Element.RawValue = value.rawValue | |
private var bitMask: Element.RawValue = 1 | |
public mutating func next() -> Element? { | |
while remainingBits != 0 { | |
defer { bitMask = bitMask &* 2 } | |
if remainingBits & bitMask != 0 { | |
remainingBits = remainingBits & ~bitMask | |
return Element(rawValue: bitMask, value: self.value.store[bitMask]) | |
} | |
} | |
return nil | |
} | |
} |
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 | |
func removeOptional(from string: String) -> String { | |
guard string.hasPrefix("Optional("), var _string: String = string as String? else { return string } | |
while _string.hasPrefix("Optional(") { _string = _string.replacingOccurrences(of: "Optional(", with: "") } | |
while _string.hasSuffix(")") { _string.removeLast() } | |
return _string | |
} | |
func testOptionSet() { | |
var testset: TestSet = [] | |
testset = [.int, .string("hello")] | |
testset.remove(.int) | |
testset.update(with: .string("world")) | |
testset.insert(.int(10)) | |
testset += TestSet.int(15) + .bool // Requires conformance to Sequence | |
print(testset.description + "\n") | |
// This test shows how each member only carries it's own value in store | |
testset.forEach { // Also requires conformance to Sequence | |
print($0.rawValue.description + ": " + $0.store.reduce(into: [String](), { | |
$0.append(removeOptional(from: String(describing: $1.value))) | |
}).joined(separator: ", ")) | |
} | |
print(testset.description) | |
} |
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 | |
extension TestSet: CustomStringConvertible, Sequence { | |
public var description: String { | |
var members: [String] = [] | |
var vars: [String] = [] | |
if self.contains(.bool) { members.append("bool" ) } | |
if self.contains(.int) { members.append("int") } | |
if self.contains(.string) { members.append("string") } | |
if self.contains(.optString) { members.append("Optional<String>") } | |
if let int: Int = self.int { vars.append("Int(" + int.description + ")") } | |
if let string: String = self.string { vars.append("String(" + string + ")") } | |
if let optString: String = self.optString { vars.append("Optional<String>(" + optString + ")")} | |
return "Members: " + (members.isEmpty ? ["none"] : members).joined(separator: ", ") + "\nVariables: " + vars.joined(separator: ", ") | |
} | |
} |
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 | |
public struct TestSet: OptionSetAssociated { | |
public typealias RawValue = Int | |
public let rawValue: RawValue | |
public var store: [RawValue : Any] = [:] | |
public init(rawValue: RawValue) { | |
self.rawValue = rawValue | |
} | |
} | |
extension TestSet { // Members | |
public static var bool: Self { Self(rawValue: 1 << 0) } | |
public static var int: Self { Self(rawValue: 1 << 1) } | |
public static var string: Self { Self(rawValue: 1 << 2) } | |
public static var optString: Self { Self(rawValue: 1 << 3) } | |
public static func int(_ value: Int) -> Self { | |
Self(.int, value: value) | |
} | |
public static func string(_ value: String) -> Self { | |
Self(.string, value: value) | |
} | |
public static func optString(_ value: String?) -> Self { | |
Self(.optString, value: value) | |
} | |
} | |
extension TestSet { // Member options | |
public var int: Int? { | |
self.getValue(for: .int) ?? ( self.contains(.int) ? Int() : nil ) | |
} | |
public var string: String? { | |
self.getValue(for: .string) ?? ( self.contains(.string) ? String() : nil ) | |
} | |
public var optString: String? { | |
self.getValue(for: .optString) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Creation is identical to OptionSet, except that struct also needs:
public var store: [RawValue : Any] = [:]
Example with TestSet shows how to associate values to members.
Results from testOptionSet():