swiftie has moved to separated lib

This commit is contained in:
Okan Yücel 2022-09-29 00:09:20 +03:00
parent f3508e0d1b
commit fe24e0f442
25 changed files with 18 additions and 1062 deletions

View File

@ -34,7 +34,7 @@
7898F68127D4023C00716057 /* CitySearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898F67A27D4023C00716057 /* CitySearchModels.swift */; };
7898F68227D4023C00716057 /* CitySearchWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898F67B27D4023C00716057 /* CitySearchWorker.swift */; };
7898F68327D4023C00716057 /* CitySearchRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898F67C27D4023C00716057 /* CitySearchRouter.swift */; };
78C15DC927D4B139002FA4E5 /* Swiftrie in Frameworks */ = {isa = PBXBuildFile; productRef = 78C15DC827D4B139002FA4E5 /* Swiftrie */; };
78C14BBD28E4EF0000A1FA7C /* Swiftrie in Frameworks */ = {isa = PBXBuildFile; productRef = 78C14BBC28E4EF0000A1FA7C /* Swiftrie */; };
78C15DCF27D508D9002FA4E5 /* CityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C15DCD27D508D9002FA4E5 /* CityCell.swift */; };
78C15DD027D508D9002FA4E5 /* CityCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 78C15DCE27D508D9002FA4E5 /* CityCell.xib */; };
78C15DD927D5348F002FA4E5 /* CityMap.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78C15DD227D5348F002FA4E5 /* CityMap.storyboard */; };
@ -113,7 +113,6 @@
78F19CA827D68F4E003BE8B2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
78F19CAA27D68F60003BE8B2 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
78F19CAC27D69006003BE8B2 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
78FC699D27D394A80088343D /* Swiftrie */ = {isa = PBXFileReference; lastKnownFileType = text; path = Swiftrie; sourceTree = SOURCE_ROOT; };
78FC69A627D3B15D0088343D /* API */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = API; sourceTree = SOURCE_ROOT; };
78FC69AA27D3B6D10088343D /* cities.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cities.json; sourceTree = SOURCE_ROOT; };
78FC69AC27D3BA590088343D /* Extensions */ = {isa = PBXFileReference; lastKnownFileType = text; path = Extensions; sourceTree = SOURCE_ROOT; };
@ -126,7 +125,7 @@
files = (
7898F67327D3BB3D00716057 /* Extensions in Frameworks */,
78FC69A827D3B51C0088343D /* API in Frameworks */,
78C15DC927D4B139002FA4E5 /* Swiftrie in Frameworks */,
78C14BBD28E4EF0000A1FA7C /* Swiftrie in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -183,7 +182,6 @@
780ABAF727D37B1800AC241A /* Info.plist */,
78FC69AC27D3BA590088343D /* Extensions */,
78FC69A627D3B15D0088343D /* API */,
78FC699D27D394A80088343D /* Swiftrie */,
78F19CA927D68F4E003BE8B2 /* Localizable.strings */,
);
path = CitieSearch;
@ -349,7 +347,7 @@
packageProductDependencies = (
78FC69A727D3B51C0088343D /* API */,
7898F67227D3BB3D00716057 /* Extensions */,
78C15DC827D4B139002FA4E5 /* Swiftrie */,
78C14BBC28E4EF0000A1FA7C /* Swiftrie */,
);
productName = CitieSearch;
productReference = 780ABAE627D37B1700AC241A /* CitieSearch.app */;
@ -425,6 +423,7 @@
);
mainGroup = 780ABADD27D37B1700AC241A;
packageReferences = (
78C14BBB28E4EF0000A1FA7C /* XCRemoteSwiftPackageReference "Swiftrie" */,
);
productRefGroup = 780ABAE727D37B1700AC241A /* Products */;
projectDirPath = "";
@ -1009,13 +1008,25 @@
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
78C14BBB28E4EF0000A1FA7C /* XCRemoteSwiftPackageReference "Swiftrie" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/yucelokan/Swiftrie.git";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
7898F67227D3BB3D00716057 /* Extensions */ = {
isa = XCSwiftPackageProductDependency;
productName = Extensions;
};
78C15DC827D4B139002FA4E5 /* Swiftrie */ = {
78C14BBC28E4EF0000A1FA7C /* Swiftrie */ = {
isa = XCSwiftPackageProductDependency;
package = 78C14BBB28E4EF0000A1FA7C /* XCRemoteSwiftPackageReference "Swiftrie" */;
productName = Swiftrie;
};
78FC69A727D3B51C0088343D /* API */ = {

View File

@ -5,17 +5,14 @@ First in first. You need to build all packages before building the project.
Packages:
- API
- Extensions
- Swiftrie
* Open API folder under project files and open Package.swift (You need to build it for iPhone)
* Open Extensions folder under project files and open Package.swift (You need to build it for iPhone)
* Open Swiftrie folder under project files and open Package.swift (You need to build it for iPhone)
* After you build them with successfully. Please open .xcodeproj of main project and build/run it.
## Swiftrie
Please, check [my solution](https://github.com/yucelokan/CitieSearch/blob/main/Swiftrie/README.md).
Please, check [my solution](https://github.com/yucelokan/Swiftrie).

7
Swiftrie/.gitignore vendored
View File

@ -1,7 +0,0 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Swiftrie"
BuildableName = "Swiftrie"
BlueprintName = "Swiftrie"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SwiftrieTests"
BuildableName = "SwiftrieTests"
BlueprintName = "SwiftrieTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Swiftrie"
BuildableName = "Swiftrie"
BlueprintName = "Swiftrie"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,27 +0,0 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Swiftrie",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Swiftrie",
targets: ["Swiftrie"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Swiftrie",
dependencies: []),
.testTarget(
name: "SwiftrieTests",
dependencies: ["Swiftrie"])
]
)

View File

@ -1,47 +0,0 @@
# Swiftrie
First thing first, what is a [Trie](https://en.wikipedia.org/wiki/Trie).?
> In computer science, a trie, also called digital tree or prefix tree, is a kind of search tree —an ordered tree data structure used to store a dynamic set or associative array where the keys are usually strings. [...] All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string. Keys tend to be associated with leaves, though some inner nodes may correspond to keys of interest. Hence, keys are not necessarily associated with every node.
This library provides all requirements about Trie implementation on Swift.
What does this library offer:
* First of all everything; `Swiftrie works with Codables`, not only with words. Every final node stores a JSON string for itself. It means you can use it with Codables.
* Suit for all data models. Just implement `Swiftriable Interface` and that's all you need to make your model suit for Swiftrie.
* A new item can be `insertable`/`removable`.
* `Cancelable searches`. Swiftrie does it automatically. If you call the searching method again while the algorithm is searching a prefix, Swiftrie cancels the last search.
* Also you can add `throttle`/`delay` while searching in Swiftrie to prevent unnecessary searches. `Default is 0`. Because `Swiftrie can show results in 0.001 seconds in 209k data for an entered character`. Because Swiftrie does not wait for all nodes that will be visited. Check the following topic.
* Swiftrie provides showing results part by part. No need waiting for until the algorithm visits all nodes. It returns the results while visiting the nodes. To manage this logic just use `.gradually`. Default is `case indexable(_ index: 3)` index 3 means, the find method will return the response that it found at every 3 nodes without waiting for all nodes that will be visited.
* Everything in a `custom queue`. QoS is `.userInitiated`. And it is `concurrent`.
* Inserting and removing an item executes with `.barrier` in the custom queue. It means searching/getting will wait for inserting/removing and the algorithm will show correct results always.
* Unit tests have provided.
## Codes
```swift
let trie = Swiftrie(swiftriables: response.cities)
trie.gradually = .indexable(3)
trie.removeItem(/* a swiftriable item */)
trie.insertItem(/* a swiftriable item */)
let cities: [City] = trie.getAllItems()
trie.findItems(prefix: "istanbu", throttle: 0, type: City.self) { result in
print(result)
}
trie.cancelSearch()
```

View File

@ -1,22 +0,0 @@
//
// Codable+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
extension Encodable {
var data: Data? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try? encoder.encode(self)
}
var jsonString: String {
guard let data = data else { return "" }
guard let jsonString = String(data: data, encoding: .utf8) else { return "" }
return jsonString
}
}

View File

@ -1,60 +0,0 @@
//
// DispatchQueue+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
extension DispatchQueue {
/**
- parameters:
- target: Object used as the sentinel for de-duplication.
- delay: The time window for de-duplication to occur
- work: The work item to be invoked on the queue.
Performs work only once for the given target, given the time window. The last added work closure
is the work that will finally execute.
Note: This is currently only safe to call from the main thread.
Example usage:
```
DispatchQueue.main.asyncDeduped(target: self, after: 1.0) { [weak self] in
self?.doTheWork()
}
```
*/
func asyncDeduped(
target: AnyObject,
after delay: TimeInterval,
execute work: @escaping @convention(block) () -> Void
) {
let dedupeIdentifier = DispatchQueue.dedupeIdentifierFor(target)
if let existingWorkItem = DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier) {
existingWorkItem.cancel()
}
let workItem = DispatchWorkItem {
DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier)
for ptr in DispatchQueue.weakTargets.allObjects {
if dedupeIdentifier == DispatchQueue.dedupeIdentifierFor(ptr as AnyObject) {
work()
break
}
}
}
DispatchQueue.workItems[dedupeIdentifier] = workItem
DispatchQueue.weakTargets.addPointer(Unmanaged.passUnretained(target).toOpaque())
asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
// MARK: - Static Properties for De-Duping
private extension DispatchQueue {
static var workItems = [AnyHashable: DispatchWorkItem]()
static var weakTargets = NSPointerArray.weakObjects()
static func dedupeIdentifierFor(_ object: AnyObject) -> String {
"\(Unmanaged.passUnretained(object).toOpaque())." + String(describing: object)
}
}

View File

@ -1,14 +0,0 @@
//
// String+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
extension String {
func toObject<T: Decodable>() -> T? {
return try? JSONDecoder().decode(T.self, from: Data(self.utf8))
}
}

View File

@ -1,63 +0,0 @@
//
// YOOrderedDictionary.swift
//
//
// Created by okan.yucel on 12.04.2022.
//
import Foundation
class YOOrderedDictionary<Key: Hashable, Value> {
typealias Element = (key: Key, value: Value)
init() { }
private (set) var values: [Element] = []
subscript(key: Key) -> Value? {
get {
guard let item = values.first(where: {$0.key == key}) else { return nil }
return item.value
}
set(newValue) {
guard let value = newValue else {
// if it is nil, remove it
values.removeAll(where: {$0.key == key})
return
}
let element = (key: key, value: value)
guard let index = values.firstIndex(where: {$0.key == key}) else {
// if there is no exist, append it
values.append(element)
return
}
// if it already exists, remove it and insert it
values.remove(at: index)
values.insert(element, at: index)
}
}
func sort(
by condition: (Element, Element) -> Bool
) {
values.sort(by: condition)
}
var first: Value? {
return values.first?.value
}
var last: Value? {
return values.last?.value
}
var count: Int {
return values.count
}
var isEmpty: Bool {
return values.isEmpty
}
}

View File

@ -1,12 +0,0 @@
//
// SwiftrieItem.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
public protocol Swiftriable: Codable {
var prefixableText: String { get }
}

View File

@ -1,59 +0,0 @@
//
// File.swift
//
//
// Created by okan.yucel on 7.03.2022.
//
import Foundation
class SwiftriableFindPrimes<T: Swiftriable>: Operation {
typealias PrimesTrie = SwiftriableLogic & SwiftrieFindableLogic & SwiftrieStorableLogic
init(swiftriable: PrimesTrie,
prefix: String,
type: T.Type,
completion: @escaping ([T]) -> Void) {
self.swiftriable = swiftriable
self.prefix = prefix
self.type = type
self.completion = completion
}
private unowned var swiftriable: PrimesTrie
private var prefix: String
private var type: T.Type
private var completion: ([T]) -> Void
override func main() {
findResults(with: prefix, type: type, completion: completion)
}
private func findResults<T: Swiftriable>(
with prefix: String, type: T.Type? = nil, completion: (([T]) -> Void)? = nil
) {
let graduallyIndex = (
swiftriable as? SwiftrieAccessibleLogic
)?.gradually.modIndex ?? -1
var items: [T] = []
let prefixLowerCased = prefix.lowercased()
if let lastNode = swiftriable.findLastNodeOf(word: prefixLowerCased) {
if lastNode.isFinal {
let subItems: [T] = lastNode.items.compactMap({$0.toObject()})
items.append(contentsOf: subItems)
}
for item in lastNode.childrens.values.enumerated() {
if isCancelled {
return
}
let childItems: [T] = swiftriable.itemsInSubtrie(rootNode: item.element.value, partialWord: prefixLowerCased)
items += childItems
if graduallyIndex > 1, item.offset%graduallyIndex == 0 {
swiftriable.queue.async {
completion?(items)
}
}
}
}
completion?(items)
}
}

View File

@ -1,23 +0,0 @@
//
// SwiftriableGraduallyLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
public enum SwiftriableGraduallyLogic {
case nonIndexable
case indexable(_ index: Int)
var modIndex: Int? {
switch self {
case .nonIndexable:
return nil
case .indexable(let index):
guard index > 1 else { return 2 }
return index
}
}
}

View File

@ -1,38 +0,0 @@
//
// Swiftrie+SwiftriableLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftriableLogic: AnyObject {
init(swiftriables: [Swiftriable])
func items<T: Swiftriable>() -> [T]
func find<T: Swiftriable>(
with prefix: String, type: T.Type, completion: @escaping (([T]) -> Void)
)
}
extension SwiftriableLogic where Self: SwiftrieFindableLogic & SwiftrieStorableLogic {
/// All Swiftriables currently in the trie
func items<T: Swiftriable>() -> [T] {
return itemsInSubtrie(rootNode: root, partialWord: "")
}
/// Fetchs part by part an array of Swiftriable in a subtrie of the trie that start with the given prefix
/// - Parameters:
/// - prefix: the letters for word prefix
/// - type: type of Swiftriable
/// - completion: Swiftriables in the subtrie that start with prefix (party by part)
func find<T: Swiftriable>(
with prefix: String, type: T.Type, completion: @escaping (([T]) -> Void)
) {
operations.cancelAllOperations()
let operation = SwiftriableFindPrimes<T>(
swiftriable: self, prefix: prefix, type: type, completion: completion
)
operations.addOperation(operation)
}
}

View File

@ -1,22 +0,0 @@
//
// SwiftrieNode+SwiftriableModifiableNodeLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwifriableModifiableNodeLogic: AnyObject {
init(value: Character?, parent: SwiftriableNode?, item: String?)
func add(value: Character, item: String?)
}
extension SwifriableModifiableNodeLogic where Self: SwiftriableStorableNodeLogic {
/// Adds a child node to self.
/// - Parameter value: The item to be added to this node.
func add(value: Character, item: String? = nil) {
childrens[value] = SwiftrieNode(value: value, parent: self, item: item)
childrens.sort(by: {$0.key.lowercased() < $1.key.lowercased()})
}
}

View File

@ -1,17 +0,0 @@
//
// SwiftrieNode+SwiftriableStorableNodeLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftriableStorableNodeLogic: AnyObject {
var value: Character? { get set }
var items: [String] { get set }
var parent: SwiftriableNode? { get set }
var childrens: YOOrderedDictionary<Character, SwiftriableNode> { get set }
var isFinal: Bool { get set }
var isLeaf: Bool { get }
}

View File

@ -1,26 +0,0 @@
//
// Swiftrie.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
typealias SwiftrieAllLogics = (
SwiftriableLogic & SwiftrieStorableLogic & SwiftrieFindableLogic & SwiftrieModifiableLogic
)
public class Swiftrie: SwiftrieAllLogics {
required public init(swiftriables: [Swiftriable]) {
swiftriables.forEach { swiftriable in
insert(swiftriable)
}
}
var root: SwiftriableNode = SwiftrieNode(value: nil, parent: nil, item: nil)
var operations: OperationQueue = OperationQueue()
var queue: DispatchQueue = DispatchQueue(label: "swiftrie-safe", qos: .userInitiated, attributes: .concurrent)
public var gradually: SwiftriableGraduallyLogic = .indexable(3)
}

View File

@ -1,76 +0,0 @@
//
// SwiftrieAccessibleLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
public protocol SwiftrieAccessibleLogic {
/// cancel all search thats are active
func cancelSearch()
/// get all items in Swiftrie.
/// - Returns: A Swiftriable array that is generic. Response type should be provided.
func getAllItems<T: Swiftriable>() -> [T]
/// Fetchs part by part an array of Swiftriable in a subtrie of the trie that start with the given prefix
/// - Parameters:
/// - prefix: the letters for word prefix
/// - throttle: delay for search / deduped
/// - type: type of Swiftriable
/// - completion: Swiftriables in the subtrie
/// that start with prefix (party by part) (to change logic check .gradually)
func findItems<T: Swiftriable>(
prefix: String, throttle: Double, type: T.Type, completion: @escaping (([T]) -> Void)
)
/// remove a Swiftriable item from Swiftrie .
/// - Parameter Swiftriable: the Swiftriable to be removed
func removeItem(_ swiftriable: Swiftriable)
/// Inserts a Swiftriable into the trie.
/// - Parameter Swiftriable: the Swiftriable to be inserted.
func insertItem(_ swiftriable: Swiftriable)
/// a logic to get data part by part. Default is `case indexable(_ index: 3)`
/// index 3 means, the find method will return the response
/// that it found at every 3 nodes without waiting for all nodes that will be visited.
var gradually: SwiftriableGraduallyLogic { get set }
}
extension Swiftrie: SwiftrieAccessibleLogic {
public func cancelSearch() {
operations.cancelAllOperations()
}
public func getAllItems<T: Swiftriable>() -> [T] {
queue.sync {
return items()
}
}
public func findItems<T: Swiftriable>(
prefix: String, throttle: Double, type: T.Type, completion: @escaping (([T]) -> Void)
) {
queue.asyncDeduped(target: self, after: throttle) { [weak self] in
self?.queue.sync {
self?.find(with: prefix, type: type, completion: completion)
}
}
}
public func removeItem(_ swiftriable: Swiftriable) {
queue.async(flags: .barrier) {
self.remove(swiftriable)
}
}
public func insertItem(_ swiftriable: Swiftriable) {
queue.async(flags: .barrier) {
self.insert(swiftriable)
}
}
}

View File

@ -1,90 +0,0 @@
//
// Swiftrie+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol SwiftrieFindableLogic: AnyObject {
func findLastNodeOf(word: String) -> SwiftriableNode?
func findTerminalNodeOf(word: String) -> SwiftriableNode?
func deleteNodesForWordEndingWith(terminalNode: SwiftriableNode)
func itemsInSubtrie<T: Swiftriable>(rootNode: SwiftriableNode, partialWord: String) -> [T]
}
extension SwiftrieFindableLogic where Self: SwiftrieStorableLogic {
/// Attempts to walk to the last node of a word. The
/// search will fail if the word is not present. Doesn't
/// check if the node is terminating
///
/// - Parameter word: the word in question
/// - Returns: the node where the search ended, nil if the
/// search failed.
func findLastNodeOf(word: String) -> SwiftriableNode? {
var currentNode = root
for character in word.lowercased() {
guard let childNode = currentNode.childrens[character] else {
return nil
}
currentNode = childNode
}
return currentNode
}
/// Attempts to walk to the terminating node of a word. The
/// search will fail if the word is not present.
///
/// - Parameter word: the word in question
/// - Returns: the node where the search ended, nil if the
/// search failed.
func findTerminalNodeOf(word: String) -> SwiftriableNode? {
if let lastNode = findLastNodeOf(word: word) {
return lastNode.isFinal ? lastNode : nil
}
return nil
}
/// Deletes a word from the trie by starting with the last letter
/// and moving back, deleting nodes until either a non-leaf or a
/// terminating node is found.
///
/// - Parameter terminalNode: the node representing the last node
/// of a word
func deleteNodesForWordEndingWith(terminalNode: SwiftriableNode) {
var lastNode = terminalNode
var character = lastNode.value
while lastNode.isLeaf, let parentNode = lastNode.parent {
lastNode = parentNode
lastNode.childrens[character ?? Character("empty")] = nil
character = lastNode.value
if lastNode.isFinal {
break
}
}
}
/// Returns an array of words in a subtrie of the trie
///
/// - Parameters:
/// - rootNode: the root node of the subtrie
/// - partialWord: the letters collected by traversing to this node
/// - Returns: the objects in the subtrie
func itemsInSubtrie<T: Swiftriable>(rootNode: SwiftriableNode, partialWord: String) -> [T] {
var subtrieItems: [T] = []
var previousLetters = partialWord
if let value = rootNode.value {
previousLetters.append(value)
}
if rootNode.isFinal {
let items: [T] = rootNode.items.compactMap({$0.toObject()})
subtrieItems.append(contentsOf: items)
}
for item in rootNode.childrens.values {
let childItems: [T] = itemsInSubtrie(rootNode: item.value, partialWord: previousLetters)
subtrieItems += childItems
}
return subtrieItems
}
}

View File

@ -1,74 +0,0 @@
//
// Swiftrie+SwiftrieModifiableLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftrieModifiableLogic {
func remove(_ swiftriable: Swiftriable)
func insert(_ swiftriable: Swiftriable)
}
extension SwiftrieModifiableLogic where Self: SwiftrieFindableLogic & SwiftrieStorableLogic {
/// Inserts a Swiftriable into the trie.
/// - Parameter Swiftriable: the Swiftriable to be inserted.
func insert(_ swiftriable: Swiftriable) {
guard !swiftriable.prefixableText.isEmpty else { return }
var currentNode = root
swiftriable.prefixableText.lowercased().forEach { character in
if let childNode = currentNode.childrens[character] {
currentNode = childNode
} else {
currentNode.add(value: character)
if let childNode = currentNode.childrens[character] {
currentNode = childNode
}
}
}
// Word already present?
guard !currentNode.isFinal else {
jsonAppender(node: currentNode, item: swiftriable)
return
}
currentNode.isFinal = true
jsonAppender(node: currentNode, item: swiftriable)
}
/// Removes a Swiftriable from the trie. If the Swiftriable is not present or
/// it is empty, just ignore it. If the last node is a leaf,
/// and this node has a only an item as JSON string
/// delete that node and higher nodes that are leaves until a
/// terminating node or non-leaf is found.
/// If it has more then one item only remove JSON string
/// - Parameter Swiftriable: the Swiftriable to be removed.
func remove(_ swiftriable: Swiftriable) {
guard !swiftriable.prefixableText.isEmpty,
let terminalNode = findTerminalNodeOf(word: swiftriable.prefixableText) else { return }
guard terminalNode.isLeaf else {
terminalNode.isFinal = false
return
}
if terminalNode.items.count > 1 {
if let index = terminalNode.items.firstIndex(where: {$0 == swiftriable.jsonString}) {
terminalNode.items.remove(at: index)
}
} else {
deleteNodesForWordEndingWith(terminalNode: terminalNode)
}
}
/// add the json if it doesn't exist
private func jsonAppender(node: SwiftriableNode, item: Swiftriable) {
let jsonString = item.jsonString
if !node.items.contains(jsonString) {
node.items.append(jsonString)
}
}
}

View File

@ -1,36 +0,0 @@
//
// SwiftrieNode.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
typealias SwiftriableNode = SwifriableModifiableNodeLogic & SwiftriableStorableNodeLogic
/// A node in the trie
class SwiftrieNode: SwiftriableNode {
/// Initializes a node.
///
/// - Parameters:
/// - value: The value that goes into the node
/// - parent: A reference to this node's parent
/// - item: a json string for the swiftriable item
required init(value: Character?, parent: SwiftriableNode?, item: String?) {
self.value = value
self.parent = parent
self.items.append(item ?? "")
}
var value: Character?
var items: [String] = []
weak var parent: SwiftriableNode?
var childrens: YOOrderedDictionary<Character, SwiftriableNode> = .init()
var isFinal: Bool = false
var isLeaf: Bool {
return childrens.isEmpty
}
}

View File

@ -1,14 +0,0 @@
//
// SwiftrieStorableLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
protocol SwiftrieStorableLogic: AnyObject {
var root: SwiftriableNode { get set }
var operations: OperationQueue { get set }
var queue: DispatchQueue { get set }
}

View File

@ -1,205 +0,0 @@
import XCTest
@testable import Swiftrie
final class SwiftrieTests: XCTestCase {
func testInitializing() {
let count = 100
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
let items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, models.count)
XCTAssertEqual(items.count, count)
}
func testEmptyInitializing() {
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: [])
let items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, 0)
XCTAssertEqual(items.count, 0)
}
func testOrdering() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
let items: [SwiftrieTestModel] = trie.getAllItems()
for index in 0..<count {
XCTAssertEqual(items[index].name, models[index].name)
}
}
func testInserting() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
var items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, models.count, "counts are not equal")
XCTAssertEqual(items.count, count, "counts are not equal")
let itemZero = SwiftrieTestModel(name: "item", id: 0)
trie.insertItem(itemZero)
items = trie.getAllItems()
XCTAssertEqual(items.count, models.count + 1, "counts are not equal")
XCTAssertEqual(items.count, count + 1, "counts are not equal")
XCTAssertEqual(
itemZero.name, items.first?.name, "position is wrong (item should be added to head of list)"
)
let itemSix = SwiftrieTestModel(name: "item 6", id: 6)
trie.insertItem(itemSix)
items = trie.getAllItems()
XCTAssertEqual(items.count, models.count + 2, "counts are not equal")
XCTAssertEqual(items.count, count + 2, "counts are not equal")
XCTAssertEqual(itemSix.name, items.last?.name, "position is wrong (item 6 added to end of list)")
}
func testRemoving() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
var items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, models.count, "counts are not equal")
XCTAssertEqual(items.count, count, "counts are not equal")
let itemThree = models[2]
trie.removeItem(itemThree)
items = trie.getAllItems()
XCTAssertEqual(items.count, models.count - 1, "counts are not equal")
XCTAssertEqual(items.count, count - 1, "counts are not equal")
XCTAssertFalse(items.contains(where: {$0.name == itemThree.name}), "removed item is still in the list")
trie.insertItem(itemThree) // insert it again
items = trie.getAllItems()
XCTAssertEqual(items.count, models.count, "counts are not equal")
XCTAssertEqual(items.count, count, "counts are not equal")
XCTAssertEqual(itemThree.name, items[2].name, "position is wrong (item 3 should be added at index 2)")
for index in 0..<count {
XCTAssertEqual(items[index].name, models[index].name)
}
}
func testFailFinding() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
var findedResult: [SwiftrieTestModel] = [.init(name: "addedAItem", id: 0)]
let promise = expectation(description: "response-handler-fail-finding")
trie.findItems(prefix: "wrong-prefix", throttle: 0, type: SwiftrieTestModel.self) { result in
findedResult = result
promise.fulfill()
}
wait(for: [promise], timeout: 2)
XCTAssertTrue(findedResult.count == 0)
}
func testSuccessAllFinding() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
var findedResult: [SwiftrieTestModel] = []
let promise = expectation(description: "response-handler-success-all-finding")
trie.findItems(prefix: "i", throttle: 0, type: SwiftrieTestModel.self) { result in
findedResult = result
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
promise.fulfill()
}
wait(for: [promise], timeout: 2)
XCTAssertTrue(findedResult.count == 5)
}
func testSuccessOneFinding() {
let count = 5
let models = TestModelCreator.get(count: count).models
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: models)
var findedResult: [SwiftrieTestModel] = []
let promise = expectation(description: "response-handler-success-one-finding")
trie.findItems(prefix: "item 1", throttle: 0, type: SwiftrieTestModel.self) { result in
findedResult = result
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
promise.fulfill()
}
wait(for: [promise], timeout: 2)
XCTAssertTrue(findedResult.count == 1)
let items = findedResult.filter({$0.name.hasPrefix("item")})
XCTAssertEqual(items.count, findedResult.count)
}
// testing same word and different object
func testDuplicationItems() {
let item0 = SwiftrieTestModel(name: "same name", id: 0)
let item1 = SwiftrieTestModel(name: "same name", id: 1)
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: [item0, item1])
var items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, 2)
// remove it
trie.removeItem(item0)
items = trie.getAllItems()
XCTAssertEqual(items.count, 1)
let hasContainItem2 = items[0].id == 1
XCTAssertTrue(hasContainItem2)
// insert it
trie.insertItem(item0)
items = trie.getAllItems()
XCTAssertEqual(items.count, 2)
}
// testing inserting same
func testInsertingSameItems() {
let item0 = SwiftrieTestModel(name: "same name", id: 0)
let trie: SwiftrieAccessibleLogic = Swiftrie(swiftriables: [item0])
trie.insertItem(item0)
let items: [SwiftrieTestModel] = trie.getAllItems()
XCTAssertEqual(items.count, 1)
}
}

View File

@ -1,20 +0,0 @@
//
// SwiftrieTestModel.swift
//
//
// Created by okan.yucel on 9.03.2022.
//
import Foundation
import Swiftrie
struct SwiftrieTestModel {
var name: String
var id: Int
}
extension SwiftrieTestModel: Swiftriable {
var prefixableText: String {
return name
}
}

View File

@ -1,23 +0,0 @@
//
// TestModelCreator.swift
//
//
// Created by okan.yucel on 9.03.2022.
//
import Foundation
enum TestModelCreator {
case get(count: Int)
var models: [SwiftrieTestModel] {
switch self {
case .get(let count):
var items: [SwiftrieTestModel] = []
for index in 0..<count {
items.append(.init(name: "item \(index)", id: index))
}
return items
}
}
}