TM-SGNL-iOS/SignalServiceKit/Util/Refinery.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

128 lines
4.9 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
// A Refinery helps you iteratively assign values to keys. Each iteration provides the keys without values yet and
// through successive calls to refine(_:) values are provided by the caller. It's more useful than map when you can
// look up a collection of keys at once, such as in a SQL query because you don't need to worry about values that were
// resolved in a previous step.
//
// Example:
//
// Refinery(phoneNumbers).refine { phoneNumbers in
// return NamesForPhoneNumbersFromAddressBook(phoneNumbers) // returns nil if not in address book
// }.refine { phoneNumbers in
// return NamesForPhoneNumbersFromDatabase(phoneNumbers) // returns nil if not in database
// }.refine { phoneNumbers in
// return phoneNumbers.map { formatPhoneNumber($0) }
// }.values
//
// The `values` array is parallel to the `keys` the Refinery was initialized with.
public class Refinery<Key, Value> {
// Keys whose values the client wants to compute.
public let keys: [Key]
// Parallel to `keys`. Each call to `refine` may change some of these from nil to nonnil.
private(set) public var values: [Value?]
// Indexes in `values` that are nil.
private var indexes: IndexSet
public init(_ keys: [Key]) {
self.keys = keys
values = Array(repeating: nil, count: keys.count)
indexes = IndexSet(0..<keys.count)
}
public func refine<Result>(_ closure: (AnySequence<Key>) -> Result) -> Self where Result: Sequence, Result.Element == Value? {
internalRefine(indexes, closure: closure)
return self
}
// This helps you partition the keys so that they can be refined differently based on some precondition.
// Keys meeting the condition are sent to the `then` closure; all others go to `otherwise`.
public func refine<Result>(condition: (Key) -> Bool,
then: (AnySequence<Key>) -> Result,
otherwise: (AnySequence<Key>) -> Result) -> Self where Result: Sequence, Result.Element == Value? {
let (matching, nonMatching) = partitionIndexes {
condition(keys[$0])
}
internalRefine(matching, closure: then)
internalRefine(nonMatching, closure: otherwise)
return self
}
// When the key type is optional, use this to operate on only the nonnil keys.
public func refineNonnilKeys<Result, NonNilKey>(_ closure: (AnySequence<NonNilKey>) -> Result) -> Self where Key == NonNilKey?, Result: Sequence, Result.Element == Value? {
var nonNilKeys = [NonNilKey]()
var nonNilIndexes = IndexSet()
for index in indexes {
guard let key = keys[index] else {
continue
}
nonNilKeys.append(key)
nonNilIndexes.insert(index)
}
guard !nonNilIndexes.isEmpty else {
return self
}
let refinedValues = closure(AnySequence(nonNilKeys))
handleResult(indexes: nonNilIndexes, values: refinedValues)
return self
}
// `indexes` gives the indexes of `self.keys` to try to get values for from `closure`.
// As a side-effect, it assigns to self.values and removes from self.indexes when a value is assigned.
private func internalRefine<Result>(_ indexes: IndexSet,
closure: (AnySequence<Key>) -> Result) where Result: Sequence, Result.Element == Value? {
guard !indexes.isEmpty else {
return
}
let refinedValues = closure(keys(at: indexes))
handleResult(indexes: indexes, values: refinedValues)
}
private func handleResult<Result>(indexes: IndexSet,
values refinedValues: Result) where Result: Sequence, Result.Element == Value? {
for (index, maybeValue) in zip(indexes, refinedValues) {
guard let value = maybeValue else {
continue
}
self.indexes.remove(index)
values[index] = value
}
}
private func partitionIndexes(_ closure: (Int) -> Bool) -> (IndexSet, IndexSet) {
var lhs = IndexSet()
var rhs = IndexSet()
for index in indexes {
if closure(index) {
lhs.insert(index)
} else {
rhs.insert(index)
}
}
return (lhs, rhs)
}
private func keys(at indexes: IndexSet) -> AnySequence<Key> {
let keys = self.keys
return AnySequence(indexes.lazy.map { keys[$0] })
}
}
public extension Dictionary {
init<T: Refinery<Key, Value>>(_ refinery: T) {
let keysAndValues: [(Key, Value)] = zip(refinery.keys, refinery.values).compactMap { key, value in
guard let value = value else { return nil }
return (key, value)
}
self.init(uniqueKeysWithValues: keysAndValues)
}
}