419 lines
16 KiB
Swift
419 lines
16 KiB
Swift
import UIKit
|
|
|
|
protocol FilterPipelineDelegate: AnyObject {
|
|
/// Call `reloadData()`
|
|
func filterPipelineDidReset()
|
|
/// Call `safeDeleteRows()`
|
|
func filterPipeline(delete rows: [Int])
|
|
/// Call `safeInsertRow()`
|
|
func filterPipeline(insert row: Int)
|
|
/// Call `safeReloadRow()`
|
|
func filterPipeline(update row: Int)
|
|
/// Call `safeMoveRow()`
|
|
func filterPipeline(move oldRow: Int, to newRow: Int)
|
|
}
|
|
|
|
// MARK: - FilterPipeline
|
|
|
|
class FilterPipeline<T> {
|
|
|
|
private(set) fileprivate var dataSource: [T] = []
|
|
|
|
private var pipeline: [PipelineFilter<T>] = []
|
|
private var display: PipelineSorting<T>!
|
|
weak var delegate: FilterPipelineDelegate?
|
|
|
|
/// - Returns: Number of elements in `projection`
|
|
@inline(__always) func displayObjectCount() -> Int { display.projection.count }
|
|
|
|
/// Dereference `projection` index to `dataSource` index
|
|
/// - Complexity: O(1)
|
|
@inline(__always) func displayObject(at index: Int) -> T { dataSource[display.projection[index]] }
|
|
|
|
/// Search and return first element in `dataSource` that matches `predicate`.
|
|
/// - Returns: Index in `dataSource` and found object or `nil` if no matching item found.
|
|
/// - Complexity: O(*n*), where *n* is the length of the `dataSource`.
|
|
func dataSourceGet(where predicate: ((T) -> Bool)) -> (index: Int, object: T)? {
|
|
// TODO: use sorted dataSource for binary lookup?
|
|
// would require to shift filter and sorting indices for every new element
|
|
guard let i = dataSource.firstIndex(where: predicate) else {
|
|
return nil
|
|
}
|
|
return (i, dataSource[i])
|
|
}
|
|
|
|
/// Set new data source and re-built filter and display sorting order.
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
func reset(dataSource: [T]) {
|
|
self.dataSource = dataSource
|
|
self.resetFilters()
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Returns the index set of either the last filter layer, or `dataSource` if no filter is set.
|
|
fileprivate func lastLayerIndices() -> [Int] {
|
|
pipeline.last?.selection ?? dataSource.indices.arr()
|
|
}
|
|
|
|
/// Get pipeline index of filter with given identifier
|
|
private func indexOfFilter(_ identifier: String) -> Int? {
|
|
pipeline.firstIndex(where: {$0.id == identifier})
|
|
}
|
|
|
|
|
|
// MARK: manage pipeline
|
|
|
|
/// Add new filter layer. Each layer is applied upon the previous layer. Therefore, each filter
|
|
/// can only restrict the display further. A filter cannot introduce previously removed elements.
|
|
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
/// - Parameters:
|
|
/// - identifier: Use this id to find the filter again. For reload and remove operations.
|
|
/// - otherId: If `nil` or non-existent the new filter will be appended at the end.
|
|
/// - predicate: Return `true` if you want to keep the element.
|
|
func addFilter(_ identifier: String, before otherId: String? = nil, _ predicate: @escaping PipelineFilter<T>.Predicate) {
|
|
guard indexOfFilter(identifier) == nil else { return }
|
|
let newFilter = PipelineFilter(identifier, predicate)
|
|
if let other = otherId, let i = indexOfFilter(other) {
|
|
pipeline.insert(newFilter, at: i)
|
|
resetFilters(startingAt: i)
|
|
} else {
|
|
newFilter.reset(to: dataSource, previous: lastLayerIndices())
|
|
pipeline.append(newFilter)
|
|
display?.apply(moreRestrictive: newFilter.selection)
|
|
}
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Find and remove filter with given identifier. Will automatically update remaining filters and display sorting.
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
func removeFilter(withId ident: String) {
|
|
guard let i = indexOfFilter(ident) else { return }
|
|
pipeline.remove(at: i)
|
|
if i == pipeline.count {
|
|
// only if we don't reset other layers we can assure `toLessRestrictive`
|
|
display?.apply(lessRestrictive: lastLayerIndices())
|
|
} else {
|
|
resetFilters(startingAt: i)
|
|
}
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Start filter evaluation on all entries from previous filter.
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
func reloadFilter(withId ident: String) {
|
|
guard let i = indexOfFilter(ident) else { return }
|
|
resetFilters(startingAt: i)
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Sets the sort and display order. You should set the `delegate` to automatically update your `tableView`.
|
|
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
|
func setSorting(_ predicate: @escaping PipelineSorting<T>.Predicate) {
|
|
display = .init(predicate, pipe: self)
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Will reverse the current display order without resorting. This is faster than setting a new sorting `predicate`.
|
|
/// However, the `predicate` must be dynamic and support a sort order flag.
|
|
/// - Note: Will call `filterPipelineDidReset()`
|
|
/// - Warning: Make sure `predicate` does reflect the change or it will lead to data inconsistency!
|
|
func reverseSorting() {
|
|
// TODO: use semaphore to prevent concurrent edits
|
|
display?.reverseOrder()
|
|
delegate?.filterPipelineDidReset()
|
|
}
|
|
|
|
/// Re-built filter and display sorting order.
|
|
/// - Parameter index: Must be: `index <= pipeline.count`
|
|
private func resetFilters(startingAt index: Int = 0) {
|
|
for i in index..<pipeline.count {
|
|
pipeline[i].reset(to: dataSource, previous: (i>0)
|
|
? pipeline[i-1].selection : dataSource.indices.arr())
|
|
}
|
|
// Reset is NOT less-restrictive because filters are dynamic
|
|
// Calling reset on a filter twice may yield different results
|
|
// E.g. if filter uses variables outside of scope (current time, search term)
|
|
display?.reset(to: lastLayerIndices())
|
|
}
|
|
|
|
/// Push object through filter pipeline to check whether it survives all filters.
|
|
/// - Parameter index: The index of the object in the original `dataSource`
|
|
/// - Returns: `changed` is `true` if element persists or should be removed with this update.
|
|
/// `display` indicates whther element should be shown (`true`) or hidden (`false`).
|
|
/// - Complexity: O(*m* log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
|
private func processPipeline(with obj: T, at index: Int) -> (changed: Bool, display: Bool) {
|
|
var keepGoing = true
|
|
for filter in pipeline {
|
|
let lastIndex: Int?
|
|
if keepGoing {
|
|
(keepGoing, lastIndex) = filter.update(obj, at: index)
|
|
} else {
|
|
lastIndex = filter.remove(dataSource: index)
|
|
}
|
|
// if it isnt in this layer, it wont appear in the following either
|
|
if lastIndex == nil { return (false, false) }
|
|
}
|
|
return (true, keepGoing)
|
|
}
|
|
|
|
|
|
// MARK: data updates
|
|
|
|
/// Add new element to the original `dataSource` and immediately apply filter and sorting.
|
|
/// - Note: Will call `filterPipeline(insert:)` if not filtered.
|
|
/// - Complexity: O((*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
|
func addNew(_ obj: T) {
|
|
let index = dataSource.count
|
|
dataSource.append(obj)
|
|
for filter in pipeline {
|
|
if filter.add(obj, at: index) == nil { return }
|
|
}
|
|
// survived all filters
|
|
let displayIndex = display.insertNew(index)
|
|
delegate?.filterPipeline(insert: displayIndex)
|
|
}
|
|
|
|
/// Update element at `index` in the original `dataSource` and immediately re-apply filter and sorting.
|
|
/// - Note: Will call `filterPipeline(delete:)`, `(insert:)`, `(update:)`, or `(move:)`
|
|
/// - Parameters:
|
|
/// - obj: Element to be added. Will overwrite previous `dataSource` object.
|
|
/// - index: Index in the original `dataSource`
|
|
/// - Complexity: O(*n* + (*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter / projection.
|
|
func update(_ obj: T, at index: Int) {
|
|
let status = processPipeline(with: obj, at: index)
|
|
guard status.changed else {
|
|
dataSource[index] = obj // we need to update anyway
|
|
return
|
|
}
|
|
let oldPos = display.deleteOld(index)
|
|
dataSource[index] = obj
|
|
guard status.display else {
|
|
if oldPos != -1 { delegate?.filterPipeline(delete: [oldPos]) }
|
|
return
|
|
}
|
|
let newPos = display.insertNew(index, previousIndex: oldPos)
|
|
if oldPos == -1 {
|
|
delegate?.filterPipeline(insert: newPos)
|
|
} else {
|
|
if oldPos == newPos {
|
|
delegate?.filterPipeline(update: oldPos)
|
|
} else {
|
|
delegate?.filterPipeline(move: oldPos, to: newPos)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Remove elements from the original `dataSource`, from all filters, and from display sorting.
|
|
/// - Note: Will call `filterPipeline(delete:)` if `sorted` array is not empty.
|
|
/// - Parameter sorted: Indices in the original `dataSource`
|
|
/// - Complexity: O(*t*(*m*+*n*) + *m* log *n*), where *t* is the number of filters,
|
|
/// *m* the number of elements in each filter / projection, and *n* the length of `sorted` indices.
|
|
func remove(indices sorted: [Int]) {
|
|
guard sorted.count > 0 else { return }
|
|
for i in sorted.reversed() {
|
|
dataSource.remove(at: i)
|
|
}
|
|
for filter in pipeline {
|
|
filter.shiftRemove(indices: sorted)
|
|
}
|
|
let indices = display.shiftRemove(indices: sorted)
|
|
delegate?.filterPipeline(delete: indices)
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Filter
|
|
|
|
class PipelineFilter<T>: CustomStringConvertible {
|
|
var description: String { "\(Self.self)(id: \(id))" }
|
|
|
|
typealias Predicate = (T) -> Bool
|
|
|
|
let id: String
|
|
private(set) var selection: [Int] = []
|
|
private let shouldPersist: Predicate
|
|
|
|
/// - Parameter predicate: Return `true` if you want to keep the element
|
|
required init(_ identifier: String, _ predicate: @escaping Predicate) {
|
|
self.id = identifier
|
|
shouldPersist = predicate
|
|
}
|
|
|
|
/// Reset `selection` by copying the indices and applying the filter function
|
|
fileprivate func reset(to dataSource: [T], previous filterIndices: [Int]) {
|
|
selection = filterIndices
|
|
selection.removeAll { !shouldPersist(dataSource[$0]) }
|
|
}
|
|
|
|
/// Apply filter to `obj` and either insert or do nothing.
|
|
/// - Parameters:
|
|
/// - obj: Object that should be inserted if filter allows.
|
|
/// - index: Index of object in original `dataSource`
|
|
/// - Returns: Index in `selection` or `nil` if `obj` is removed by the filter.
|
|
/// - Complexity:
|
|
/// * O(1), if `index` is appended at end.
|
|
/// * O(log *n*), where *n* is the length of the `selection`.
|
|
fileprivate func add(_ obj: T, at index: Int) -> Int? {
|
|
guard shouldPersist(obj) else {
|
|
return nil
|
|
}
|
|
if selection.last ?? 0 < index { // in case we only append at end
|
|
selection.append(index)
|
|
return selection.count - 1
|
|
}
|
|
return selection.binTreeInsert(index, compare: (<))
|
|
}
|
|
|
|
/// Search and remove original `dataSource` index
|
|
/// - Parameter index: Index of object in original `dataSource`
|
|
/// - Returns: Index of removed element in `selection` or `nil` if element does not exist
|
|
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
|
fileprivate func remove(dataSource index: Int) -> Int? {
|
|
selection.binTreeRemove(index, compare: (<))
|
|
}
|
|
|
|
/// Perform filter check and update internal `selection` indices.
|
|
/// - Parameters:
|
|
/// - obj: Object that was inserted or updated.
|
|
/// - index: Index where the object is located after the update.
|
|
/// - Returns: `keep` indicates whether the value should be displayed (`true`) or hidden (`false`).
|
|
/// `idx` contains the selection filter index or `nil` if the value should be removed.
|
|
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
|
fileprivate func update(_ obj: T, at index: Int) -> (keep: Bool, idx: Int?) {
|
|
let currentIndex = selection.binTreeIndex(of: index, compare: (<), mustExist: true)
|
|
if shouldPersist(obj) {
|
|
return (true, currentIndex ?? selection.binTreeInsert(index, compare: (<)))
|
|
}
|
|
if let i = currentIndex { selection.remove(at: i) }
|
|
return (false, currentIndex)
|
|
}
|
|
|
|
/// Instead of re-sorting we can decrement all remaining elements after X.
|
|
/// - Parameter sorted: Elements to remove from collection
|
|
/// - Complexity: O(*m*+*n*), where *m* is the length of the `selection`.
|
|
/// *n* is equal to: *length of selection* `-` *index of first element* of `sorted` indices
|
|
fileprivate func shiftRemove(indices sorted: [Int]) {
|
|
guard sorted.count > 0 else {
|
|
return
|
|
}
|
|
var list = sorted
|
|
var del = list.popLast()
|
|
for (i, val) in selection.enumerated().reversed() {
|
|
while let d = del, d > val {
|
|
del = list.popLast()
|
|
}
|
|
guard let d = del else { break }
|
|
if d < val { selection[i] -= (list.count + 1) }
|
|
else if d == val { selection.remove(at: i) }
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Sorting
|
|
|
|
class PipelineSorting<T> {
|
|
typealias Predicate = (T, T) -> Bool
|
|
|
|
private(set) var projection: [Int] = []
|
|
private let comperator: (Int, Int) -> Bool // links to pipeline.dataSource
|
|
|
|
/// Create a fresh, already sorted, display order projection.
|
|
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
|
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
|
required init(_ predicate: @escaping Predicate, pipe: FilterPipeline<T>) {
|
|
comperator = { [unowned pipe] in
|
|
predicate(pipe.dataSource[$0], pipe.dataSource[$1])
|
|
}
|
|
reset(to: pipe.lastLayerIndices())
|
|
}
|
|
|
|
/// - Warning: Make sure `predicate` does reflect the change. Or it will lead to data inconsistency.
|
|
/// - Complexity: O(*n*), where *n* is the length of the `filter`.
|
|
fileprivate func reverseOrder() {
|
|
projection.reverse()
|
|
}
|
|
|
|
/// Replace current `projection` with new filter indices and apply sorting.
|
|
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
|
fileprivate func reset(to filterIndices: [Int]) {
|
|
projection = filterIndices.sorted(by: comperator)
|
|
}
|
|
|
|
/// After adding a new layer of filtering the new layer can only restrict the display even further.
|
|
/// Therefore, indices that were removed in the last layer will be removed from the projection too.
|
|
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* the length of the `filter`.
|
|
fileprivate func apply(moreRestrictive filterIndices: [Int]) {
|
|
projection.removeAll { !filterIndices.binTreeExists($0, compare: (<)) }
|
|
}
|
|
|
|
/// After removing a layer of filtering the previous layers are less restrictive and thus contain more indices.
|
|
/// Therefore, the difference between both index sets will be inserted into the projection.
|
|
/// - Complexity: O(*m* log *n*), where *m* is the difference to the previous layer and *n* is the length of the `projection`.
|
|
fileprivate func apply(lessRestrictive filterIndices: [Int]) {
|
|
for x in filterIndices.difference(toSubset: projection.sorted(), compare: (<)) {
|
|
insertNew(x)
|
|
}
|
|
}
|
|
|
|
/// Add new element and automatically sort according to predicate
|
|
/// - Parameters:
|
|
/// - index: Index of the element position in the original `dataSource`
|
|
/// - prev: If greater than `0`, try re-insert at the same position.
|
|
/// - Returns: Index in the projection
|
|
/// - Complexity: O(log *n*), where *n* is the length of the `projection`.
|
|
@discardableResult fileprivate func insertNew(_ index: Int, previousIndex prev: Int = -1) -> Int {
|
|
if prev >= 0, prev <= projection.count { // '<=' because previous delete removed one element
|
|
if (prev == 0 || !comperator(index, projection[prev - 1])),
|
|
(prev == projection.count || !comperator(projection[prev], index)) {
|
|
// If element can be inserted at the same position without resorting, do that
|
|
projection.insert(index, at: prev)
|
|
return prev
|
|
}
|
|
}
|
|
return projection.binTreeInsert(index, compare: comperator)
|
|
}
|
|
|
|
/// Remove element from projection
|
|
/// - Parameter index: Index of the element position in the original `dataSource`
|
|
/// - Returns: Index in the projection or `-1` if element did not exist
|
|
/// - Complexity: O(*n*), where *n* is the length of the `projection`.
|
|
fileprivate func deleteOld(_ index: Int) -> Int {
|
|
guard let i = projection.firstIndex(of: index) else {
|
|
return -1
|
|
}
|
|
projection.remove(at: i)
|
|
return i
|
|
}
|
|
|
|
/// Instead of re-sorting we can decrement all remaining elements after X.
|
|
/// - Parameter sorted: Elements to remove from collection
|
|
/// - Returns: List of `projection` indices that were removed (reverse sort order)
|
|
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* is the length of `sorted`.
|
|
@discardableResult fileprivate func shiftRemove(indices sorted: [Int]) -> [Int] {
|
|
guard sorted.count > 0 else {
|
|
return []
|
|
}
|
|
var listOfDeletes: [Int] = []
|
|
let min = sorted.first!, max = sorted.last!
|
|
for (i, val) in projection.enumerated().reversed() {
|
|
guard val >= min else { continue }
|
|
if val > max {
|
|
projection[i] -= sorted.count
|
|
} else {
|
|
let c = sorted.binTreeIndex(of: val, compare: (<))!
|
|
if val == sorted[c] {
|
|
projection.remove(at: i)
|
|
listOfDeletes.append(i)
|
|
} else {
|
|
projection[i] -= c
|
|
}
|
|
}
|
|
}
|
|
return listOfDeletes
|
|
}
|
|
}
|