Add SwiftTrie package
This commit is contained in:
50
.github/workflows/docs.yml
vendored
Normal file
50
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
runs-on: macos-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Pages
|
||||||
|
uses: actions/configure-pages@v3
|
||||||
|
- name: Generate Docs
|
||||||
|
uses: fwcd/swift-docc-action@v1
|
||||||
|
with:
|
||||||
|
target: SwiftTrie
|
||||||
|
output: ./docs
|
||||||
|
transform-for-static-hosting: 'true'
|
||||||
|
disable-indexing: 'true'
|
||||||
|
hosting-base-path: swift-trie
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./docs
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: docs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy Docs
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
16
.github/workflows/swiftlint.yml
vendored
Normal file
16
.github/workflows/swiftlint.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: SwiftLint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ '**' ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
SwiftLint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: GitHub Action for SwiftLint
|
||||||
|
uses: norio-nomura/action-swiftlint@3.2.1
|
||||||
|
with:
|
||||||
|
args: --strict
|
||||||
27
.github/workflows/unit.yml
vendored
Normal file
27
.github/workflows/unit.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ '**' ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
swift: ['5.8', '5.9', '5.10']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Swift
|
||||||
|
uses: SwiftyLab/setup-swift@latest
|
||||||
|
with:
|
||||||
|
swift-version: ${{ matrix.swift }}
|
||||||
|
|
||||||
|
- name: Build and Test
|
||||||
|
run: |
|
||||||
|
swift build
|
||||||
|
swift test
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
||||||
|
Package.resolved
|
||||||
|
docs
|
||||||
2
.swiftlint.yml
Normal file
2
.swiftlint.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
excluded:
|
||||||
|
- .build
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Terry Yiu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
27
Package.swift
Normal file
27
Package.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// swift-tools-version: 5.7
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "SwiftTrie",
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "SwiftTrie",
|
||||||
|
targets: ["SwiftTrie"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
// Dependencies declare other packages that this package depends on.
|
||||||
|
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
|
.target(
|
||||||
|
name: "SwiftTrie"),
|
||||||
|
.testTarget(
|
||||||
|
name: "SwiftTrieTests",
|
||||||
|
dependencies: ["SwiftTrie"])
|
||||||
|
]
|
||||||
|
)
|
||||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[](https://github.com/tyiu/swift-trie/actions/workflows/unit.yml) [](https://github.com/tyiu/swift-trie/actions/workflows/swiftlint.yml) [](https://github.com/tyiu/swift-trie/actions/workflows/docs.yml)
|
||||||
|
|
||||||
|
# SwiftTrie
|
||||||
|
|
||||||
|
A Swift package that provides a [Trie](https://en.wikipedia.org/wiki/Trie) data structure that allow efficient searches of values that map from prefixed keys or non-prefixed key substrings.
|
||||||
|
|
||||||
|
## Minimum Requirements
|
||||||
|
|
||||||
|
- Swift 5.8
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
SwiftTrie can be integrated as an Xcode project target or a Swift package target.
|
||||||
|
|
||||||
|
### Xcode Project Target
|
||||||
|
|
||||||
|
1. Go to `File` -> `Add Package Dependencies`.
|
||||||
|
2. Type https://github.com/tyiu/swift-trie.git into the search field.
|
||||||
|
3. Select `swift-trie` from the search results.
|
||||||
|
4. Select `Up to Next Major Version` starting from the latest release as the dependency rule.
|
||||||
|
5. Ensure your project is selected next to `Add to Project`.
|
||||||
|
6. Click `Add Package`.
|
||||||
|
7. On the package product dialog, add `SwiftTrie` to your target and click `Add Package`.
|
||||||
|
|
||||||
|
### Swift Package Target
|
||||||
|
|
||||||
|
In your `Package.swift` file:
|
||||||
|
1. Add the SwiftTrie package dependency to https://github.com/tyiu/swift-trie.git
|
||||||
|
2. Add `SwiftTrie` as a dependency on the targets that need to use the SDK.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let package = Package(
|
||||||
|
// ...
|
||||||
|
dependencies: [
|
||||||
|
// ...
|
||||||
|
.package(url: "https://github.com/tyiu/swift-trie.git", .upToNextMajor(from: "0.1.0"))
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
// ...
|
||||||
|
dependencies: ["SwiftTrie"]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
// ...
|
||||||
|
dependencies: ["SwiftTrie"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [TrieTests.swift](Tests/SwiftTrieTests/TrieTests.swift) for an example of how to use SwiftTrie.
|
||||||
157
Sources/SwiftTrie/Trie.swift
Normal file
157
Sources/SwiftTrie/Trie.swift
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
//
|
||||||
|
// Trie.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 6/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Trie is a tree data structure of all the substring permutations of a collection of strings
|
||||||
|
/// optimized for searching for values of type V.
|
||||||
|
///
|
||||||
|
/// Each node in the tree can have child nodes.
|
||||||
|
/// Each node represents a single character in substrings,
|
||||||
|
/// and each of its child nodes represent the subsequent character in those substrings.
|
||||||
|
///
|
||||||
|
/// A node that has no children mean that there are no substrings
|
||||||
|
/// with any additional characters beyond the branch of letters leading up to that node.
|
||||||
|
///
|
||||||
|
/// A node that has values mean that there are strings that end in the character represented by the node
|
||||||
|
/// and contain the substring represented by the branch of letters leading up to that node.
|
||||||
|
///
|
||||||
|
/// See the article on [Trie](https://en.wikipedia.org/wiki/Trie) on Wikipedia.
|
||||||
|
public class Trie<V: Hashable> {
|
||||||
|
private var children: [Character: Trie] = [:]
|
||||||
|
|
||||||
|
/// Separate exact matches from strict substrings so that exact matches appear first in returned results.
|
||||||
|
private var exactMatchValues = Set<V>()
|
||||||
|
private var substringMatchValues = Set<V>()
|
||||||
|
|
||||||
|
private var parent: Trie?
|
||||||
|
|
||||||
|
var hasChildren: Bool {
|
||||||
|
return !self.children.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasValues: Bool {
|
||||||
|
return !self.exactMatchValues.isEmpty || !self.substringMatchValues.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Trie {
|
||||||
|
/// Finds the branch that matches the specified key and returns the values from all of its descendant nodes.
|
||||||
|
/// Note: If `key` is an empty string, all values are returned.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key to find in the trie.
|
||||||
|
/// - Returns: The values that are mapped from matches of `key`.
|
||||||
|
func find(key: String) -> [V] {
|
||||||
|
var currentNode = self
|
||||||
|
|
||||||
|
// Find branch with matching prefix.
|
||||||
|
for char in key {
|
||||||
|
if let child = currentNode.children[char] {
|
||||||
|
currentNode = child
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform breadth-first search from matching branch and collect values from all descendants.
|
||||||
|
var substringMatches = Set<V>(currentNode.substringMatchValues)
|
||||||
|
var queue = Array(currentNode.children.values)
|
||||||
|
|
||||||
|
while !queue.isEmpty {
|
||||||
|
let node = queue.removeFirst()
|
||||||
|
substringMatches.formUnion(node.exactMatchValues)
|
||||||
|
substringMatches.formUnion(node.substringMatchValues)
|
||||||
|
queue.append(contentsOf: node.children.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize exact matches to be returned first,
|
||||||
|
// and then remove exact matches from the set of partial substring matches that are appended afterward.
|
||||||
|
return Array(currentNode.exactMatchValues) + (substringMatches.subtracting(currentNode.exactMatchValues))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a value into this trie for the specified key.
|
||||||
|
/// This function stores all substring endings of the key, not only the key itself.
|
||||||
|
/// Runtime performance is O(n^2) and storage cost is O(n), where n is the number of characters in the key.
|
||||||
|
/// Note: If `key` is an empty string, this operation is effectively a no-op.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key to insert that maps to `value`.
|
||||||
|
/// - value: The value that is mapped from `key`.
|
||||||
|
/// - includeNonPrefixedMatches: Whether the key and value should be inserted to allow for non-prefixed matches.
|
||||||
|
/// By default, it is `false`. If it is `true`, more memory will be used.
|
||||||
|
func insert(key: String, value: V, includeNonPrefixedMatches: Bool = false) {
|
||||||
|
// Create root branches for each character of the key to enable substring searches
|
||||||
|
// instead of only just prefix searches.
|
||||||
|
// Hence the nested loop.
|
||||||
|
for keyIndex in 0..<key.count {
|
||||||
|
var currentNode = self
|
||||||
|
|
||||||
|
// Find branch with matching prefix.
|
||||||
|
for char in key[key.index(key.startIndex, offsetBy: keyIndex)...] {
|
||||||
|
if let child = currentNode.children[char] {
|
||||||
|
currentNode = child
|
||||||
|
} else {
|
||||||
|
let child = Trie()
|
||||||
|
child.parent = currentNode
|
||||||
|
currentNode.children[char] = child
|
||||||
|
currentNode = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyIndex == 0 {
|
||||||
|
currentNode.exactMatchValues.insert(value)
|
||||||
|
|
||||||
|
// If includeNonPrefixedMatches is true, the first character of the key can be the only root branch
|
||||||
|
// and we terminate the loop early.
|
||||||
|
if !includeNonPrefixedMatches {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentNode.substringMatchValues.insert(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a value from this trie for the specified key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key to remove.
|
||||||
|
/// - value: The value to remove.
|
||||||
|
func remove(key: String, value: V) {
|
||||||
|
for keyIndex in 0..<key.count {
|
||||||
|
var currentNode = self
|
||||||
|
|
||||||
|
var foundLeafNode = true
|
||||||
|
|
||||||
|
// Find branch with matching prefix.
|
||||||
|
for keySubIndex in keyIndex..<key.count {
|
||||||
|
let char = key[key.index(key.startIndex, offsetBy: keySubIndex)]
|
||||||
|
|
||||||
|
if let child = currentNode.children[char] {
|
||||||
|
currentNode = child
|
||||||
|
} else {
|
||||||
|
foundLeafNode = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundLeafNode {
|
||||||
|
currentNode.exactMatchValues.remove(value)
|
||||||
|
currentNode.substringMatchValues.remove(value)
|
||||||
|
|
||||||
|
// Clean up the tree if this leaf node no longer holds values or children.
|
||||||
|
for keySubIndex in (keyIndex..<key.count).reversed() {
|
||||||
|
if let parent = currentNode.parent, !currentNode.hasValues && !currentNode.hasChildren {
|
||||||
|
currentNode = parent
|
||||||
|
let char = key[key.index(key.startIndex, offsetBy: keySubIndex)]
|
||||||
|
currentNode.children.removeValue(forKey: char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Tests/SwiftTrieTests/TrieTests.swift
Normal file
111
Tests/SwiftTrieTests/TrieTests.swift
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// TrieTests.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 6/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import SwiftTrie
|
||||||
|
|
||||||
|
final class TrieTests: XCTestCase {
|
||||||
|
|
||||||
|
func testFindPrefixedMatches() throws {
|
||||||
|
let trie = Trie<String>()
|
||||||
|
|
||||||
|
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
|
||||||
|
keys.forEach {
|
||||||
|
trie.insert(key: $0, value: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let allResults = trie.find(key: "")
|
||||||
|
XCTAssertEqual(Set(allResults), Set(["foobar", "food", "foo", "somethingelse", "duplicate"]))
|
||||||
|
|
||||||
|
let fooResults = trie.find(key: "foo")
|
||||||
|
XCTAssertEqual(fooResults.first, "foo")
|
||||||
|
XCTAssertEqual(Set(fooResults), Set(["foobar", "food", "foo"]))
|
||||||
|
|
||||||
|
let foodResults = trie.find(key: "food")
|
||||||
|
XCTAssertEqual(foodResults, ["food"])
|
||||||
|
|
||||||
|
let ooResults = trie.find(key: "oo")
|
||||||
|
XCTAssertEqual(Set(ooResults), Set([]))
|
||||||
|
|
||||||
|
let notFoundResults = trie.find(key: "notfound")
|
||||||
|
XCTAssertEqual(notFoundResults, [])
|
||||||
|
|
||||||
|
// Sanity check that the root node has children.
|
||||||
|
XCTAssertTrue(trie.hasChildren)
|
||||||
|
|
||||||
|
// Sanity check that the root node has no values.
|
||||||
|
XCTAssertFalse(trie.hasValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFindNonPrefixedMatches() throws {
|
||||||
|
let trie = Trie<String>()
|
||||||
|
|
||||||
|
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
|
||||||
|
keys.forEach {
|
||||||
|
trie.insert(key: $0, value: $0, includeNonPrefixedMatches: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let allResults = trie.find(key: "")
|
||||||
|
XCTAssertEqual(Set(allResults), Set(["foobar", "food", "foo", "somethingelse", "duplicate"]))
|
||||||
|
|
||||||
|
let fooResults = trie.find(key: "foo")
|
||||||
|
XCTAssertEqual(fooResults.first, "foo")
|
||||||
|
XCTAssertEqual(Set(fooResults), Set(["foobar", "food", "foo"]))
|
||||||
|
|
||||||
|
let foodResults = trie.find(key: "food")
|
||||||
|
XCTAssertEqual(foodResults, ["food"])
|
||||||
|
|
||||||
|
let ooResults = trie.find(key: "oo")
|
||||||
|
XCTAssertEqual(Set(ooResults), Set(["foobar", "food", "foo"]))
|
||||||
|
|
||||||
|
let aResults = trie.find(key: "a")
|
||||||
|
XCTAssertEqual(Set(aResults), Set(["foobar", "duplicate"]))
|
||||||
|
|
||||||
|
let notFoundResults = trie.find(key: "notfound")
|
||||||
|
XCTAssertEqual(notFoundResults, [])
|
||||||
|
|
||||||
|
// Sanity check that the root node has children.
|
||||||
|
XCTAssertTrue(trie.hasChildren)
|
||||||
|
|
||||||
|
// Sanity check that the root node has no values.
|
||||||
|
XCTAssertFalse(trie.hasValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemove() {
|
||||||
|
let trie = Trie<String>()
|
||||||
|
|
||||||
|
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
|
||||||
|
keys.forEach {
|
||||||
|
trie.insert(key: $0, value: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.forEach {
|
||||||
|
trie.remove(key: $0, value: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let allResults = trie.find(key: "")
|
||||||
|
XCTAssertTrue(allResults.isEmpty)
|
||||||
|
|
||||||
|
let fooResults = trie.find(key: "foo")
|
||||||
|
XCTAssertTrue(fooResults.isEmpty)
|
||||||
|
|
||||||
|
let foodResults = trie.find(key: "food")
|
||||||
|
XCTAssertTrue(foodResults.isEmpty)
|
||||||
|
|
||||||
|
let ooResults = trie.find(key: "oo")
|
||||||
|
XCTAssertTrue(ooResults.isEmpty)
|
||||||
|
|
||||||
|
let aResults = trie.find(key: "a")
|
||||||
|
XCTAssertTrue(aResults.isEmpty)
|
||||||
|
|
||||||
|
// Verify that removal of values from all the keys that were inserted in the trie previously
|
||||||
|
// also resulted in the cleanup of the trie.
|
||||||
|
XCTAssertFalse(trie.hasChildren)
|
||||||
|
XCTAssertFalse(trie.hasValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user