├── .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 | [](https://github.com/Finnvoor/FindFaster/actions/workflows/CI.yml) [](https://swiftpackageindex.com/Finnvoor/FindFaster) [](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..