├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE.md
├── Package.swift
├── README.md
├── Sources
└── FuzzyFind
│ ├── Alignment.swift
│ ├── FuzzyFind.swift
│ ├── FuzzyResult.swift
│ ├── FuzzyResultSegment.swift
│ └── Score.swift
└── Tests
└── FuzzyFindTests
└── FuzzyFindTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (C) 2021 Tomás Ruiz López
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 | -----------------------------------------------------------------------------
16 |
17 | Code in this repository is derived from a Haskell version implemented by
18 | Rúnar Bjarnason & Unison Computing. Its license follows:
19 |
20 | Copyright 2021 Unison Computing
21 |
22 | 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:
23 |
24 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
25 |
26 | 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:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "FuzzyFind",
7 | products: [
8 | .library(
9 | name: "FuzzyFind",
10 | targets: ["FuzzyFind"]),
11 | ],
12 | dependencies: [],
13 | targets: [
14 | .target(
15 | name: "FuzzyFind",
16 | dependencies: []),
17 | .testTarget(
18 | name: "FuzzyFindTests",
19 | dependencies: ["FuzzyFind"]),
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FuzzyFind
2 |
3 | A Swift package with utilities to perform fuzzy search, using a modified version of the Smith-Waterman algorithm. This implementation is a port from the [Haskell version](https://github.com/runarorama/fuzzyfind) implemented by Rúnar Bjarnason.
4 |
5 | ## Usage
6 |
7 | This package includes two core functions: `bestMatch` and `fuzzyFind`.
8 |
9 | With `bestMatch`, you can find the best alignment of your query into a single string, if any alignment is possible:
10 |
11 | ```swift
12 | import FuzzyFind
13 |
14 | let alignment = bestMatch(query: "ff", input: "FuzzyFind") // Matches
15 | let noAlignment = bestMatch(query: "ww", input: "FuzzyFind") // Not possible to find a match, returns nil
16 | ```
17 |
18 | With `fuzzyFind`, you can run multiple queries over multiple inputs, and get all alignments for inputs that match all provided queries. Alignments will be provided in an array and sorted by their score; a higher score means a better alignment.
19 |
20 | ```swift
21 | import FuzzyFind
22 |
23 | let allAlignments = fuzzyFind(queries: ["dad", "mac", "dam"], inputs: ["red macadamia", "Madam Card"])
24 | ```
25 |
26 | You can visualize the matched alignment by calling `highlight()` on an alignment:
27 |
28 | ```swift
29 |
30 | let alignment = bestMatch(query: "ff", input: "FuzzyFind")!
31 | print(alignment.highlight())
32 | ```
33 |
34 | This will print:
35 |
36 | ```
37 | FuzzyFind
38 | * *
39 | ```
40 |
--------------------------------------------------------------------------------
/Sources/FuzzyFind/Alignment.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Alignment {
4 | public let score: Score
5 | public let result: FuzzyResult
6 |
7 | static var empty: Alignment {
8 | return Alignment(score: 0, result: .empty)
9 | }
10 |
11 | func combine(_ other: Alignment) -> Alignment {
12 | return Alignment(
13 | score: self.score + other.score,
14 | result: self.result.merge(other.result)
15 | )
16 | }
17 |
18 | public func highlight() -> String {
19 | return """
20 | \(result.segments.map(\.asString).joined())
21 | \(result.segments.map(\.asGaps).joined())
22 | """
23 | }
24 |
25 | public var asString: String {
26 | return result.asString
27 | }
28 | }
29 |
30 | extension Alignment: Equatable {}
31 |
--------------------------------------------------------------------------------
/Sources/FuzzyFind/FuzzyFind.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Finds input strings that match all the given input patterns. For each input that matches, it returns one `Alignment`. The output is sorted by ascending `score`.
4 | ///
5 | /// - Parameters:
6 | /// - queries: An array of patterns to match.
7 | /// - inputs: An array of strings to search into.
8 | /// - match: Score for a match.
9 | /// - mismatch: Score for a mismatch.
10 | /// - gapPenalty: Function to provide a penalty for a gap.
11 | /// - boundaryBonus: Bonus for a match in a boundary.
12 | /// - camelCaseBonus: Bonus for a match in camel case.
13 | /// - firstCharBonusMultiplier: Multiplier for a match in the first character of the input.
14 | /// - consecutiveBonus: Bonus for consecutive matches.
15 | /// - Returns: An array of all alignments found for the input words, sorted by their score.
16 | public func fuzzyFind(
17 | queries: [String],
18 | inputs: [String],
19 | match: Score = .defaultMatch,
20 | mismatch: Score = .defaultMismatch,
21 | gapPenalty: (Int) -> Score = Score.defaultGapPenalty,
22 | boundaryBonus: Score = .defaultBoundary,
23 | camelCaseBonus: Score = .defaultCamelCase,
24 | firstCharBonusMultiplier: Int = Score.defaultFirstCharBonusMultiplier,
25 | consecutiveBonus: Score = Score.defaultConsecutiveBonus
26 | ) -> [Alignment] {
27 | inputs.compactMap { input in
28 | queries.reduce(.some(Alignment.empty)) { partial, next in
29 | partial.flatMap { alignment in
30 | bestMatch(
31 | query: next,
32 | input: input,
33 | match: match,
34 | mismatch: mismatch,
35 | gapPenalty: gapPenalty,
36 | boundaryBonus: boundaryBonus,
37 | camelCaseBonus: camelCaseBonus,
38 | firstCharBonusMultiplier: firstCharBonusMultiplier,
39 | consecutiveBonus: consecutiveBonus).map { match in
40 | alignment.combine(match)
41 | }
42 | }
43 | }
44 | }.sorted { a1, a2 in a1.score > a2.score }
45 | }
46 |
47 | /// Finds the best Alignment, if any, for the query in the input word.
48 | ///
49 | /// If an alignment can be found, it returns the best way, according to the provided scores, to line up the characters in the query with the ones in the input.
50 | ///
51 | /// The score indicates how good the match is. Better matches have higher scores.
52 | ///
53 | /// A substring from the query will generate a match, and any characters from the input the don't result in a match will generate a gap. Concatenating all match and gap results should yield the original input string.
54 | ///
55 | /// All matched characters in the input always occur in the same order as the do in the query pattern.
56 | ///
57 | /// The algorithm prefers (and will generate higher scorers for) the following kind of matches:
58 | ///
59 | /// 1. Contiguous characters from the query string.
60 | /// 2. Characters at the beginnings of words.
61 | /// 3. Characters at CamelCase humps.
62 | /// 4. First character of the query pattern at the beginning of a word or CamelHump.
63 | /// 5. All else being equal, matchs that occur later in the input string are preferred.
64 | ///
65 | /// - Parameters:
66 | /// - query: Query pattern.
67 | /// - input: Input string to match the query.
68 | /// - match: Score for a match.
69 | /// - mismatch: Score for a mismatch.
70 | /// - gapPenalty: Function to provide a penalty for a gap.
71 | /// - boundaryBonus: Bonus for a match in a boundary.
72 | /// - camelCaseBonus: Bonus for a match in camel case.
73 | /// - firstCharBonusMultiplier: Multiplier for a match in the first character of the input.
74 | /// - consecutiveBonus: Bonus for consecutive matches.
75 | /// - Returns: An `Alignment` of the query and the input, if it is possible.
76 | public func bestMatch(
77 | query: String,
78 | input: String,
79 | match: Score = .defaultMatch,
80 | mismatch: Score = .defaultMismatch,
81 | gapPenalty: (Int) -> Score = Score.defaultGapPenalty,
82 | boundaryBonus: Score = .defaultBoundary,
83 | camelCaseBonus: Score = .defaultCamelCase,
84 | firstCharBonusMultiplier: Int = Score.defaultFirstCharBonusMultiplier,
85 | consecutiveBonus: Score = Score.defaultConsecutiveBonus
86 | ) -> Alignment? {
87 | let a = query.map { $0 }
88 | let b = input.map { $0 }
89 | let m = query.count
90 | let n = input.count
91 | let bonuses = (0 ... m).map { i in
92 | (0 ... n).map { j in bonus(i,j) }
93 | }
94 | var hs: [Pair: Score] = [:]
95 |
96 | func find(_ array: [Character], at position: Int) -> Character {
97 | return array[position - 1]
98 | }
99 |
100 | func similarity(_ a: Character, _ b: Character) -> Score {
101 | return (a.lowercased() == b.lowercased()) ? match : mismatch
102 | }
103 |
104 | func bonus(_ i: Int, _ j: Int) -> Score {
105 | if i == 0 || j == 0 {
106 | return 0
107 | } else {
108 | let similarityScore = similarity(find(a, at: i), find(b, at: j))
109 | if similarityScore > 0 {
110 | let boundary = (j < 2 || (find(b, at: j).isAlphaNum) && !(find(b, at: j - 1).isAlphaNum)) ? boundaryBonus : 0
111 | let camel = (j > 1 && find(b, at: j - 1).isLowercase && find(b, at: j).isUppercase) ? camelCaseBonus : 0
112 | let multiplier = (i == 1) ? firstCharBonusMultiplier : 1
113 | let similar = i > 0 && j > 0 && similarityScore > 0
114 | let afterMatch = i > 1 && j > 1 && similarity(find(a, at: i - 1), find(b, at: j - 1)) > 0
115 | let beforeMatch = i < m && j < n && similarity(find(a, at: i + 1), find(b, at: j + 1)) > 0
116 | let consecutive = (similar && (afterMatch || beforeMatch)) ? consecutiveBonus : 0
117 | return multiplier * (boundary + camel + consecutive)
118 | } else {
119 | return 0
120 | }
121 | }
122 | }
123 |
124 | func h(_ i: Int, _ j: Int) -> Score {
125 | if let score = hs[Pair(i, j)] { return score }
126 | if i == 0 || j == 0 {
127 | hs[Pair(i, j)] = 0
128 | return 0
129 | }
130 | let scoreMatch = h(i - 1, j - 1) + similarity(find(a, at: i), find(b, at: j)) + bonuses[i][j]
131 | let scoreGap = (1 ... j).map { l in
132 | h(i, j - l) - gapPenalty(l)
133 | }.max()!
134 | let score = [scoreMatch, scoreGap, Score(integerLiteral: 0)].max()!
135 | hs[Pair(i, j)] = score
136 | return score
137 | }
138 |
139 | func localMax(_ m: Int, _ n: Int) -> Int {
140 | return (1 ... n).max { b, d in
141 | totalScore(m, b) < totalScore(m, d)
142 | }!
143 | }
144 |
145 | func totalScore(_ i: Int, _ j: Int) -> Score {
146 | return (i > m) ? 0 : (h(i, j) + bonuses[i][j])
147 | }
148 |
149 | func go(_ x: Int, _ y: Int) -> FuzzyResult? {
150 | var i = x
151 | var j = y
152 | var result = FuzzyResult.empty
153 | while true {
154 | if i == 0 {
155 | return result.combine(.gaps(String(input.prefix(j))))
156 | } else if j == 0 {
157 | return nil
158 | } else {
159 | if similarity(find(a, at: i), find(b, at: j)) > 0 {
160 | result = result.combine(.match(find(b, at: j)))
161 | i -= 1
162 | j -= 1
163 | } else {
164 | result = result.combine(.gap(find(b, at: j)))
165 | j -= 1
166 | }
167 | }
168 | }
169 | }
170 |
171 | let nx = localMax(m, n)
172 | let traceback = go(m, nx).flatMap { result in
173 | FuzzyResult.gaps(String(input.dropFirst(nx))).combine(result)
174 | }
175 |
176 | return traceback.flatMap { result in
177 | Alignment(score: totalScore(m, nx), result: result.reversed())
178 | }
179 | }
180 |
181 | private extension Character {
182 | var isAlphaNum: Bool {
183 | isLetter || isNumber
184 | }
185 | }
186 |
187 | private struct Pair: Hashable {
188 | let a: A
189 | let b: B
190 |
191 | init(_ a: A, _ b: B) {
192 | self.a = a
193 | self.b = b
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/Sources/FuzzyFind/FuzzyResult.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct FuzzyResult {
4 | public let segments: [FuzzyResultSegment]
5 |
6 | public var asString: String {
7 | return segments.map(\.asString).joined()
8 | }
9 |
10 | static func match(_ a: Character) -> FuzzyResult {
11 | return FuzzyResult(segments: [.match([a])])
12 | }
13 |
14 | static func gap(_ a: Character) -> FuzzyResult {
15 | return FuzzyResult(segments: [.gap([a])])
16 | }
17 |
18 | static func gaps(_ str: String) -> FuzzyResult {
19 | return FuzzyResult(segments: str.reversed().map { char in
20 | FuzzyResultSegment.gap([char])
21 | })
22 | }
23 |
24 | static let empty: FuzzyResult = FuzzyResult(segments: [])
25 |
26 | func reversed() -> FuzzyResult {
27 | return FuzzyResult(segments: self.segments.map { segment in
28 | segment.reversed()
29 | }.reversed())
30 | }
31 |
32 | func combine(_ other: FuzzyResult) -> FuzzyResult {
33 | if let last = self.segments.last, let first = other.segments.first {
34 | if last.isEmpty {
35 | return FuzzyResult(segments: self.segments.lead).combine(other)
36 | } else if first.isEmpty {
37 | return self.combine(FuzzyResult(segments: other.segments.tail))
38 | } else if case let .gap(l) = last, case let .gap(h) = first {
39 | return FuzzyResult(segments: self.segments.lead + [.gap(l + h)] + other.segments.tail)
40 | } else if case let .match(l) = last, case let .match(h) = first {
41 | return FuzzyResult(segments: self.segments.lead + [.match(l + h)] + other.segments.tail)
42 | } else {
43 | return FuzzyResult(segments: self.segments + other.segments)
44 | }
45 | } else {
46 | return self.isEmpty ? other : self
47 | }
48 | }
49 |
50 | func merge(_ other: FuzzyResult) -> FuzzyResult {
51 | if self.isEmpty { return other }
52 | if other.isEmpty { return self }
53 | let xs = self.segments[0]
54 | let ys = other.segments[0]
55 | switch (xs, ys) {
56 | case let (.gap(g1), .gap(g2)):
57 | if g1.count <= g2.count {
58 | return FuzzyResult(segments: [.gap(g1)]).combine(
59 | self.tail.merge(other.drop(g1.count))
60 | )
61 | } else {
62 | return FuzzyResult(segments: [.gap(g2)]).combine(
63 | self.drop(g2.count).merge(other.tail)
64 | )
65 | }
66 | case let (.match(m1), .match(m2)):
67 | if m1.count >= m2.count {
68 | return FuzzyResult(segments: [.match(m1)]).combine(
69 | self.tail.merge(other.drop(m1.count))
70 | )
71 | } else {
72 | return FuzzyResult(segments: [.match(m2)]).combine(
73 | self.drop(m2.count).merge(other.tail)
74 | )
75 | }
76 | case let (.gap(_), .match(m)):
77 | return FuzzyResult(segments: [.match(m)]).combine(
78 | self.drop(m.count).merge(other.tail)
79 | )
80 | case let (.match(m), .gap(_)):
81 | return FuzzyResult(segments: [.match(m)]).combine(
82 | self.tail.merge(other.drop(m.count))
83 | )
84 | }
85 | }
86 |
87 | private func drop(_ n: Int) -> FuzzyResult {
88 | guard n >= 1 else { return self }
89 | if let first = self.segments.first {
90 | switch first {
91 | case .gap(let array):
92 | if n >= array.count {
93 | return self.tail.drop(n - array.count)
94 | } else {
95 | return FuzzyResult(segments: [.gap(array.drop(n))]).combine(self.tail)
96 | }
97 | case .match(let array):
98 | if n >= array.count {
99 | return self.tail.drop(n - array.count)
100 | } else {
101 | return FuzzyResult(segments: [.match(array.drop(n))]).combine(self.tail)
102 | }
103 | }
104 | } else {
105 | return .empty
106 | }
107 | }
108 |
109 | private var isEmpty: Bool {
110 | return segments.isEmpty
111 | }
112 |
113 | private var tail: FuzzyResult {
114 | return FuzzyResult(segments: self.segments.tail)
115 | }
116 |
117 | private var lead: FuzzyResult {
118 | return FuzzyResult(segments: self.segments.lead)
119 | }
120 | }
121 |
122 | extension FuzzyResult: Equatable {}
123 |
124 | private extension Array {
125 | func drop(_ n: Int) -> Array {
126 | return Array(self.dropFirst(n))
127 | }
128 |
129 | var tail: Array {
130 | if isEmpty { return [] }
131 | return Array(self.dropFirst())
132 | }
133 |
134 | var lead: Array {
135 | if isEmpty { return [] }
136 | return Array(self.dropLast())
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/FuzzyFind/FuzzyResultSegment.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum FuzzyResultSegment {
4 | case gap([Character])
5 | case match([Character])
6 |
7 | func reversed() -> FuzzyResultSegment {
8 | switch self {
9 | case let .gap(array): return .gap(array.reversed())
10 | case let .match(array): return .match(array.reversed())
11 | }
12 | }
13 |
14 | var isEmpty: Bool {
15 | switch self {
16 | case .gap(let array), .match(let array): return array.isEmpty
17 | }
18 | }
19 |
20 | var asString: String {
21 | switch self {
22 | case .gap(let array), .match(let array): return String(array)
23 | }
24 | }
25 |
26 | var asGaps: String {
27 | switch self {
28 | case .gap(let array): return String(repeating: " ", count: array.count)
29 | case .match(let array): return String(repeating: "*", count: array.count)
30 | }
31 | }
32 | }
33 |
34 | extension FuzzyResultSegment: Equatable {}
35 |
--------------------------------------------------------------------------------
/Sources/FuzzyFind/Score.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Score: ExpressibleByIntegerLiteral {
4 | public let value: Int
5 |
6 | public init(integerLiteral value: Int) {
7 | self.value = value
8 | }
9 |
10 | public static let defaultMatch: Score = 16
11 |
12 | public static let defaultMismatch: Score = 0
13 |
14 | public static var defaultBoundary: Score {
15 | return Score(integerLiteral: Score.defaultMatch.value / 2)
16 | }
17 |
18 | public static var defaultCamelCase: Score {
19 | return Score(integerLiteral: Score.defaultBoundary.value - 1)
20 | }
21 |
22 | public static var defaultFirstCharBonusMultiplier: Int = 2
23 |
24 | public static func defaultGapPenalty(_ n: Int) -> Score {
25 | return Score(integerLiteral: (n == 1) ? 3 : max(0, n + 3))
26 | }
27 |
28 | public static var defaultConsecutiveBonus: Score {
29 | defaultGapPenalty(8)
30 | }
31 | }
32 |
33 | extension Score: Equatable {}
34 |
35 | func +(lhs: Score, rhs: Score) -> Score {
36 | return Score(integerLiteral: lhs.value + rhs.value)
37 | }
38 |
39 | func -(lhs: Score, rhs: Score) -> Score {
40 | return Score(integerLiteral: lhs.value - rhs.value)
41 | }
42 |
43 | func *(lhs: Int, rhs: Score) -> Score {
44 | return Score(integerLiteral: lhs * rhs.value)
45 | }
46 |
47 | extension Score: Comparable {
48 | public static func < (lhs: Score, rhs: Score) -> Bool {
49 | return lhs.value < rhs.value
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/FuzzyFindTests/FuzzyFindTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import FuzzyFind
3 |
4 | final class FuzzyFindTests: XCTestCase {
5 | func testMatch() {
6 | let actual = bestMatch(query: "ff", input: "FuzzyFind")
7 | let expected = Alignment(
8 | score: 55,
9 | result: FuzzyResult(segments: [
10 | .match(["F"]),
11 | .gap(["u", "z", "z", "y"]),
12 | .match(["F"]),
13 | .gap(["i"]),
14 | .gap(["n"]),
15 | .gap(["d"])
16 | ])
17 | )
18 | XCTAssertEqual(actual, expected)
19 | }
20 |
21 | func testContiguousCharactersHaveHigherScore() {
22 | let a1 = bestMatch(query: "pp", input: "pickled pepper")!
23 | let a2 = bestMatch(query: "pp", input: "Pied Piper")!
24 | XCTAssertTrue(a1.score > a2.score)
25 | }
26 |
27 | func testCharactersAtBeginningOfWordsHaveHigherScore() {
28 | let a1 = bestMatch(query: "pp", input: "Pied Piper")!
29 | let a2 = bestMatch(query: "pp", input: "porcupine")!
30 | XCTAssertTrue(a1.score > a2.score)
31 | }
32 |
33 | func testCamelCaseHumpsHaveHigherScore() {
34 | let a1 = bestMatch(query: "bm", input: "BatMan")!
35 | let a2 = bestMatch(query: "bm", input: "Batman")!
36 | XCTAssertTrue(a1.score > a2.score)
37 | }
38 |
39 | func testFirstLettersOfWordsHaveHigherScore() {
40 | let a1 = bestMatch(query: "bm", input: "Bat man")!
41 | let a2 = bestMatch(query: "bm", input: "Batman")!
42 | XCTAssertTrue(a1.score > a2.score)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------