├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .spi.yml ├── .swiftformat ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── FindFaster │ └── BidirectionalCollection+fastSearch.swift └── Tests └── FindFasterTests └── FindFasterTests.swift /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 13 | 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: macOS-13 18 | strategy: 19 | matrix: 20 | destination: 21 | - "generic/platform=ios" 22 | - "platform=macOS" 23 | # - "generic/platform=xros" 24 | - "generic/platform=tvos" 25 | - "generic/platform=watchos" 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Install xcbeautify 30 | run: | 31 | brew update 32 | brew install xcbeautify 33 | - name: Build platform ${{ matrix.destination }} 34 | run: set -o pipefail && xcodebuild build -scheme FindFaster -destination "${{ matrix.destination }}" | xcbeautify --renderer github-actions 35 | test: 36 | name: Test 37 | runs-on: macOS-13 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Install xcbeautify 41 | run: | 42 | brew update 43 | brew install xcbeautify 44 | - name: Test 45 | run: set -o pipefail && xcodebuild test -scheme FindFaster -destination "platform=macOS" | xcbeautify --renderer github-actions 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [FindFaster] 5 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --enable blankLineAfterImports 2 | --enable blankLinesBetweenImports 3 | --enable blockComments 4 | --enable docComments 5 | --enable isEmpty 6 | --enable markTypes 7 | --enable organizeDeclarations 8 | 9 | --disable numberFormatting 10 | --disable redundantNilInit 11 | --disable trailingCommas 12 | --disable wrapMultilineStatementBraces 13 | 14 | --ifdef no-indent 15 | --funcattributes same-line 16 | --typeattributes same-line 17 | --varattributes same-line 18 | --ranges no-space 19 | --header strip 20 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "FindFaster", 7 | platforms: [.iOS(.v13), .macOS(.v10_15)], 8 | products: [.library(name: "FindFaster", targets: ["FindFaster"])], 9 | targets: [ 10 | .target(name: "FindFaster"), 11 | .testTarget(name: "FindFasterTests", dependencies: ["FindFaster"]), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FindFaster 2 | 3 | [![CI](https://github.com/Finnvoor/FindFaster/actions/workflows/CI.yml/badge.svg)](https://github.com/Finnvoor/FindFaster/actions/workflows/CI.yml) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FFinnvoor%2FFindFaster%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Finnvoor/FindFaster) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FFinnvoor%2FFindFaster%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Finnvoor/FindFaster) 4 | 5 | Fast asynchronous swift collection search using the [_Boyer–Moore string-search algorithm_](https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string-search_algorithm). `fastSearch` can be used with any `BidirectionalCollection` where `Element` is `Hashable`, and is especially useful for searching large amounts of data or long strings and displaying the results as they come in. 6 | 7 | FindFaster is used for find and replace in [HextEdit](https://apps.apple.com/app/apple-store/id1557247094?pt=120542042&ct=github&mt=8), a fast and native macOS hex editor. 8 | 9 | ## Usage 10 | ### Async 11 | ```swift 12 | import FindFaster 13 | 14 | let text = "Lorem ipsum dolor sit amet" 15 | let search = "or" 16 | 17 | for await index in text.fastSearchStream(for: search) { 18 | print("Found match at: \(index)") 19 | } 20 | 21 | // Prints: 22 | // Found match at: 1 23 | // Found match at: 15 24 | ``` 25 | 26 | ### Sync 27 | ```swift 28 | import FindFaster 29 | 30 | let text = "Lorem ipsum dolor sit amet" 31 | let search = "or" 32 | 33 | let results = text.fastSearch(for: search) 34 | print("Results: \(results)") 35 | 36 | // Prints: 37 | // Results: [1, 15] 38 | ``` 39 | 40 | ### Closure-based 41 | ```swift 42 | import FindFaster 43 | 44 | let text = "Lorem ipsum dolor sit amet" 45 | let search = "or" 46 | 47 | text.fastSearch(for: search) { index in 48 | print("Found match at: \(index)") 49 | } 50 | 51 | // Prints: 52 | // Found match at: 1 53 | // Found match at: 15 54 | ``` 55 | -------------------------------------------------------------------------------- /Sources/FindFaster/BidirectionalCollection+fastSearch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension BidirectionalCollection where Element: Equatable, Element: Hashable { 4 | /// Returns an `AsyncStream` delivering indices where the specified value appears in the collection. 5 | /// - Parameter element: An element to search for in the collection. 6 | /// - Returns: An `AsyncStream` delivering indices where `element` is found. 7 | func fastSearchStream(for element: Element) -> AsyncStream { 8 | fastSearchStream(for: [element]) 9 | } 10 | 11 | /// Returns an `AsyncStream` delivering indices where the specified sequence appears in the collection. 12 | /// - Parameter searchSequence: A sequence of elements to search for in the collection. 13 | /// - Returns: An `AsyncStream` delivering indices where `searchSequence` is found. 14 | func fastSearchStream(for searchSequence: some Collection) -> AsyncStream { 15 | AsyncStream { continuation in 16 | let task = Task { 17 | fastSearch(for: searchSequence) { index in 18 | continuation.yield(index) 19 | } 20 | continuation.finish() 21 | } 22 | continuation.onTermination = { _ in task.cancel() } 23 | } 24 | } 25 | 26 | /// Returns the indices where the specified value appears in the collection. 27 | /// - Parameters: 28 | /// - element: An element to search for in the collection. 29 | /// - onSearchResult: An optional closure that is called when a matching index is found. 30 | /// - Returns: The indices where `element` is found. If `element` is not found in the collection, returns an empty array. 31 | @discardableResult func fastSearch( 32 | for element: Element, 33 | onSearchResult: ((Index) -> Void)? = nil 34 | ) -> [Index] { 35 | fastSearch(for: [element], onSearchResult: onSearchResult) 36 | } 37 | 38 | /// Returns the indices where the specified sequence appears in the collection. 39 | /// - Parameters: 40 | /// - searchSequence: A sequence of elements to search for in the collection. 41 | /// - onSearchResult: An optional closure that is called when a matching index is found. 42 | /// - Returns: The indices where `searchSequence` is found. If `searchSequence` is not found in the collection, returns an empty array. 43 | @discardableResult func fastSearch( 44 | for searchSequence: some Collection, 45 | onSearchResult: ((Index) -> Void)? = nil 46 | ) -> [Index] { 47 | switch searchSequence.count { 48 | case 0: return [] 49 | case 1: return naiveSingleElementSearch(for: searchSequence.first!, onSearchResult: onSearchResult) 50 | default: return boyerMooreMultiElementSearch(for: searchSequence, onSearchResult: onSearchResult) 51 | } 52 | } 53 | } 54 | 55 | private extension BidirectionalCollection where Element: Equatable, Element: Hashable { 56 | @discardableResult func naiveSingleElementSearch( 57 | for element: Element, 58 | onSearchResult: ((Index) -> Void)? = nil 59 | ) -> [Index] { 60 | var indices: [Index] = [] 61 | var currentIndex = startIndex 62 | while currentIndex < endIndex, !Task.isCancelled { 63 | if self[currentIndex] == element { 64 | indices.append(currentIndex) 65 | onSearchResult?(currentIndex) 66 | } 67 | currentIndex = index(after: currentIndex) 68 | } 69 | return indices 70 | } 71 | 72 | /// Boyer–Moore algorithm 73 | @discardableResult func boyerMooreMultiElementSearch( 74 | for searchSequence: some Collection, 75 | onSearchResult: ((Index) -> Void)? = nil 76 | ) -> [Index] { 77 | guard searchSequence.count <= count else { return [] } 78 | 79 | var indices: [Index] = [] 80 | let skipTable: [Element: Int] = searchSequence 81 | .enumerated() 82 | .reduce(into: [:]) { $0[$1.element] = searchSequence.count - $1.offset - 1 } 83 | 84 | var currentIndex = index(startIndex, offsetBy: searchSequence.count - 1) 85 | while currentIndex < endIndex, !Task.isCancelled { 86 | let skip = skipTable[self[currentIndex]] ?? searchSequence.count 87 | if skip == 0 { 88 | let lowerBound = index(currentIndex, offsetBy: -searchSequence.count + 1) 89 | let upperBound = index(currentIndex, offsetBy: 1) 90 | if self[lowerBound..