fts_fuzzy_match 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +26 -0
- data/.github/workflows/fuzzy_match.yml +37 -0
- data/.github/workflows/packaged_source.yml +37 -0
- data/.github/workflows/packaged_tarball.yml +41 -0
- data/.github/workflows/precompiled.yml +232 -0
- data/.github/workflows/system.yml +40 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +314 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +70 -0
- data/README.md +15 -0
- data/Rakefile +23 -0
- data/ext/fts_fuzzy_match/extconf.rb +5 -0
- data/ext/fts_fuzzy_match/fts_fuzzy_match.c +82 -0
- data/ext/fts_fuzzy_match/fts_fuzzy_match.h +6 -0
- data/ext/fts_fuzzy_match/fts_fuzzy_match_impl.h +203 -0
- data/fts_fuzzy_match.gemspec +32 -0
- data/lib/fts_fuzzy_match/version.rb +5 -0
- data/lib/fts_fuzzy_match.rb +22 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5ae4ec8437fae32168ace83c701014d8ad3172a9e31abbf68c4501f5b33b59eb
|
4
|
+
data.tar.gz: e4ca65980fda174f25361290429f8ebadf1a859b91b6c0723342464c7888f6b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e03afcccdf173c3106442247df9574aba34fe3b36fc9a3418a4b41c39714048f6e2f10314b53d66aaff10f7b101b657c4958efc97ba5f2b6178f2f0cd4d3ebc1
|
7
|
+
data.tar.gz: 952900929bfa9eb2282e81f47cb4ef0b65511f4d233f6ca99252f9050858b008d51f7486c61aebab2eeb28f3b0feb10c011683ca59e974604ceefda3ee105759
|
@@ -0,0 +1,26 @@
|
|
1
|
+
version: 2
|
2
|
+
updates:
|
3
|
+
- package-ecosystem: "github-actions"
|
4
|
+
directory: "/"
|
5
|
+
schedule:
|
6
|
+
interval: "weekly"
|
7
|
+
- package-ecosystem: "bundler"
|
8
|
+
directory: "/fuzzy_match"
|
9
|
+
schedule:
|
10
|
+
interval: "weekly"
|
11
|
+
- package-ecosystem: "bundler"
|
12
|
+
directory: "/system"
|
13
|
+
schedule:
|
14
|
+
interval: "weekly"
|
15
|
+
- package-ecosystem: "bundler"
|
16
|
+
directory: "/packaged_source"
|
17
|
+
schedule:
|
18
|
+
interval: "weekly"
|
19
|
+
- package-ecosystem: "bundler"
|
20
|
+
directory: "/packaged_tarball"
|
21
|
+
schedule:
|
22
|
+
interval: "weekly"
|
23
|
+
- package-ecosystem: "bundler"
|
24
|
+
directory: "/precompiled"
|
25
|
+
schedule:
|
26
|
+
interval: "weekly"
|
@@ -0,0 +1,37 @@
|
|
1
|
+
name: fuzzy_match
|
2
|
+
concurrency:
|
3
|
+
group: "${{github.workflow}}-${{github.ref}}"
|
4
|
+
cancel-in-progress: true
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
schedule:
|
8
|
+
- cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
|
9
|
+
push:
|
10
|
+
branches:
|
11
|
+
- main
|
12
|
+
- v*.*.x
|
13
|
+
tags:
|
14
|
+
- v*.*.*
|
15
|
+
pull_request:
|
16
|
+
types: [opened, synchronize]
|
17
|
+
branches:
|
18
|
+
- '*'
|
19
|
+
paths: ["fuzzy_match/**/*", ".github/workflows/fuzzy_match.yml"]
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
fuzzy_match:
|
23
|
+
strategy:
|
24
|
+
fail-fast: false
|
25
|
+
matrix:
|
26
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4", "head"]
|
27
|
+
runs-on: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
28
|
+
runs-on: ${{matrix.runs-on}}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v4
|
31
|
+
- uses: ruby/setup-ruby@v1
|
32
|
+
with:
|
33
|
+
working-directory: fuzzy_match
|
34
|
+
ruby-version: ${{matrix.ruby}}
|
35
|
+
bundler-cache: true
|
36
|
+
- run: bundle exec rake compile test
|
37
|
+
working-directory: fuzzy_match
|
@@ -0,0 +1,37 @@
|
|
1
|
+
name: packaged_source
|
2
|
+
concurrency:
|
3
|
+
group: "${{github.workflow}}-${{github.ref}}"
|
4
|
+
cancel-in-progress: true
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
schedule:
|
8
|
+
- cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
|
9
|
+
push:
|
10
|
+
branches:
|
11
|
+
- main
|
12
|
+
- v*.*.x
|
13
|
+
tags:
|
14
|
+
- v*.*.*
|
15
|
+
pull_request:
|
16
|
+
types: [opened, synchronize]
|
17
|
+
branches:
|
18
|
+
- '*'
|
19
|
+
paths: ["packaged_source/**/*", ".github/workflows/packaged_source.yml"]
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
packaged_source:
|
23
|
+
strategy:
|
24
|
+
fail-fast: false
|
25
|
+
matrix:
|
26
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4", "head"]
|
27
|
+
runs-on: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
28
|
+
runs-on: ${{matrix.runs-on}}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v4
|
31
|
+
- uses: ruby/setup-ruby@v1
|
32
|
+
with:
|
33
|
+
working-directory: packaged_source
|
34
|
+
ruby-version: ${{matrix.ruby}}
|
35
|
+
bundler-cache: true
|
36
|
+
- run: bundle exec rake compile test
|
37
|
+
working-directory: packaged_source
|
@@ -0,0 +1,41 @@
|
|
1
|
+
name: packaged_tarball
|
2
|
+
concurrency:
|
3
|
+
group: "${{github.workflow}}-${{github.ref}}"
|
4
|
+
cancel-in-progress: true
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
schedule:
|
8
|
+
- cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
|
9
|
+
push:
|
10
|
+
branches:
|
11
|
+
- main
|
12
|
+
- v*.*.x
|
13
|
+
tags:
|
14
|
+
- v*.*.*
|
15
|
+
pull_request:
|
16
|
+
types: [opened, synchronize]
|
17
|
+
branches:
|
18
|
+
- '*'
|
19
|
+
paths: ["packaged_tarball/**/*", ".github/workflows/packaged_tarball.yml"]
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
packaged_tarball:
|
23
|
+
strategy:
|
24
|
+
fail-fast: false
|
25
|
+
matrix:
|
26
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4", "head"]
|
27
|
+
runs-on: ["ubuntu-latest", "macos-13", "windows-latest"]
|
28
|
+
runs-on: ${{matrix.runs-on}}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v4
|
31
|
+
- uses: ruby/setup-ruby@v1
|
32
|
+
with:
|
33
|
+
working-directory: packaged_tarball
|
34
|
+
ruby-version: ${{matrix.ruby}}
|
35
|
+
bundler-cache: true
|
36
|
+
- uses: actions/cache@v4
|
37
|
+
with:
|
38
|
+
path: packaged_tarball/ports
|
39
|
+
key: packaged_tarball-ports-${{matrix.runs-on}}-${{hashFiles('packaged_tarball/ext/packaged_tarball/extconf.rb')}}
|
40
|
+
- run: bundle exec rake compile test
|
41
|
+
working-directory: packaged_tarball
|
@@ -0,0 +1,232 @@
|
|
1
|
+
name: precompiled
|
2
|
+
concurrency:
|
3
|
+
group: "${{github.workflow}}-${{github.ref}}"
|
4
|
+
cancel-in-progress: true
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
schedule:
|
8
|
+
- cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
|
9
|
+
push:
|
10
|
+
branches:
|
11
|
+
- main
|
12
|
+
- v*.*.x
|
13
|
+
tags:
|
14
|
+
- v*.*.*
|
15
|
+
pull_request:
|
16
|
+
types: [opened, synchronize]
|
17
|
+
branches:
|
18
|
+
- '*'
|
19
|
+
paths: ["precompiled/**/*", ".github/workflows/precompiled.yml"]
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
ruby_versions:
|
23
|
+
outputs:
|
24
|
+
setup_ruby: "['3.1', '3.2', '3.3', '3.4']"
|
25
|
+
image_tag: "['3.1', '3.2', '3.3', '3.4']"
|
26
|
+
runs-on: ubuntu-latest
|
27
|
+
steps:
|
28
|
+
- run: echo "generating rubies ..."
|
29
|
+
|
30
|
+
rcd_image_version:
|
31
|
+
runs-on: ubuntu-latest
|
32
|
+
outputs:
|
33
|
+
rcd_image_version: ${{steps.rcd_image_version.outputs.rcd_image_version}}
|
34
|
+
steps:
|
35
|
+
- uses: actions/checkout@v4
|
36
|
+
- uses: ruby/setup-ruby@v1
|
37
|
+
with:
|
38
|
+
working-directory: precompiled
|
39
|
+
ruby-version: "3.3"
|
40
|
+
bundler-cache: true
|
41
|
+
bundler: latest
|
42
|
+
- id: rcd_image_version
|
43
|
+
run: bundle exec ruby -e 'require "rake_compiler_dock"; puts "rcd_image_version=#{RakeCompilerDock::IMAGE_VERSION}"' >> $GITHUB_OUTPUT
|
44
|
+
working-directory: precompiled
|
45
|
+
|
46
|
+
test:
|
47
|
+
needs: ["ruby_versions"]
|
48
|
+
strategy:
|
49
|
+
fail-fast: false
|
50
|
+
matrix:
|
51
|
+
runs-on: ["ubuntu-latest", "macos-13", "windows-latest"]
|
52
|
+
ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }}
|
53
|
+
runs-on: ${{matrix.runs-on}}
|
54
|
+
steps:
|
55
|
+
- uses: actions/checkout@v4
|
56
|
+
- uses: ruby/setup-ruby@v1
|
57
|
+
with:
|
58
|
+
working-directory: precompiled
|
59
|
+
ruby-version: ${{matrix.ruby}}
|
60
|
+
bundler-cache: true
|
61
|
+
- uses: actions/cache@v4
|
62
|
+
with:
|
63
|
+
path: precompiled/ports
|
64
|
+
key: precompiled-ports-${{matrix.runs-on}}-${{hashFiles('precompiled/ext/precompiled/extconf.rb')}}
|
65
|
+
- run: bundle exec rake compile test
|
66
|
+
working-directory: precompiled
|
67
|
+
|
68
|
+
generic-package:
|
69
|
+
runs-on: "ubuntu-latest"
|
70
|
+
steps:
|
71
|
+
- uses: actions/checkout@v4
|
72
|
+
- uses: actions/cache@v4
|
73
|
+
with:
|
74
|
+
path: precompiled/ports/archives
|
75
|
+
key: archives-ubuntu-${{hashFiles('precompiled/ext/precompiled/extconf.rb')}}
|
76
|
+
- uses: ruby/setup-ruby@v1
|
77
|
+
with:
|
78
|
+
working-directory: precompiled
|
79
|
+
ruby-version: "3.3"
|
80
|
+
bundler-cache: true
|
81
|
+
- run: ./bin/test-gem-build gems ruby
|
82
|
+
working-directory: precompiled
|
83
|
+
- uses: actions/upload-artifact@v4
|
84
|
+
with:
|
85
|
+
name: cruby-gem
|
86
|
+
path: precompiled/gems
|
87
|
+
retention-days: 1
|
88
|
+
|
89
|
+
generic-install:
|
90
|
+
needs: ["generic-package", "ruby_versions"]
|
91
|
+
strategy:
|
92
|
+
fail-fast: false
|
93
|
+
matrix:
|
94
|
+
os: ["ubuntu-latest", "macos-13", "windows-latest"]
|
95
|
+
ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }}
|
96
|
+
runs-on: ${{ matrix.os }}
|
97
|
+
steps:
|
98
|
+
- uses: actions/checkout@v4
|
99
|
+
- uses: ruby/setup-ruby@v1
|
100
|
+
with:
|
101
|
+
working-directory: precompiled
|
102
|
+
ruby-version: "${{ matrix.ruby }}"
|
103
|
+
- uses: actions/download-artifact@v4
|
104
|
+
with:
|
105
|
+
name: cruby-gem
|
106
|
+
path: precompiled/gems
|
107
|
+
- run: ./bin/test-gem-install gems
|
108
|
+
working-directory: precompiled
|
109
|
+
shell: bash
|
110
|
+
|
111
|
+
native-package:
|
112
|
+
needs: ["rcd_image_version"]
|
113
|
+
strategy:
|
114
|
+
fail-fast: false
|
115
|
+
matrix:
|
116
|
+
platform:
|
117
|
+
- "aarch64-linux-gnu"
|
118
|
+
- "aarch64-linux-musl"
|
119
|
+
- "arm-linux-gnu"
|
120
|
+
- "arm-linux-musl"
|
121
|
+
- "x86-linux-gnu"
|
122
|
+
- "x86-linux-musl"
|
123
|
+
- "x86_64-linux-gnu"
|
124
|
+
- "x86_64-linux-musl"
|
125
|
+
- "arm64-darwin"
|
126
|
+
- "x86_64-darwin"
|
127
|
+
- "x64-mingw-ucrt"
|
128
|
+
runs-on: ubuntu-latest
|
129
|
+
steps:
|
130
|
+
- uses: actions/checkout@v4
|
131
|
+
- uses: actions/cache@v4
|
132
|
+
with:
|
133
|
+
path: precompiled/ports/archives
|
134
|
+
key: archives-ubuntu-${{hashFiles('precompiled/ext/precompiled/extconf.rb')}}
|
135
|
+
- run: |
|
136
|
+
docker run --rm -v $PWD/precompiled:/precompiled -w /precompiled \
|
137
|
+
ghcr.io/rake-compiler/rake-compiler-dock-image:${{ needs.rcd_image_version.outputs.rcd_image_version }}-mri-${{ matrix.platform }} \
|
138
|
+
./bin/test-gem-build gems ${{ matrix.platform }}
|
139
|
+
- uses: actions/upload-artifact@v4
|
140
|
+
with:
|
141
|
+
name: "cruby-${{ matrix.platform }}-gem"
|
142
|
+
path: precompiled/gems
|
143
|
+
retention-days: 1
|
144
|
+
|
145
|
+
linux-install:
|
146
|
+
needs: ["native-package", "ruby_versions"]
|
147
|
+
strategy:
|
148
|
+
fail-fast: false
|
149
|
+
matrix:
|
150
|
+
platform:
|
151
|
+
- "aarch64-linux-gnu"
|
152
|
+
- "aarch64-linux-musl"
|
153
|
+
- "arm-linux-gnu"
|
154
|
+
- "arm-linux-musl"
|
155
|
+
- "x86-linux-gnu"
|
156
|
+
- "x86-linux-musl"
|
157
|
+
- "x86_64-linux-gnu"
|
158
|
+
- "x86_64-linux-musl"
|
159
|
+
ruby: ${{ fromJSON(needs.ruby_versions.outputs.image_tag) }}
|
160
|
+
include:
|
161
|
+
# declare docker image for each platform
|
162
|
+
- { platform: aarch64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add bash &&" }
|
163
|
+
- { platform: arm-linux-musl, docker_tag: "-alpine", bootstrap: "apk add bash &&" }
|
164
|
+
- { platform: x86-linux-musl, docker_tag: "-alpine", bootstrap: "apk add bash &&" }
|
165
|
+
- { platform: x86_64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add bash &&" }
|
166
|
+
# declare docker platform for each platform
|
167
|
+
- { platform: aarch64-linux-gnu, docker_platform: "--platform=linux/arm64" }
|
168
|
+
- { platform: aarch64-linux-musl, docker_platform: "--platform=linux/arm64" }
|
169
|
+
- { platform: arm-linux-gnu, docker_platform: "--platform=linux/arm/v7" }
|
170
|
+
- { platform: arm-linux-musl, docker_platform: "--platform=linux/arm/v7" }
|
171
|
+
- { platform: x86-linux-gnu, docker_platform: "--platform=linux/386" }
|
172
|
+
- { platform: x86-linux-musl, docker_platform: "--platform=linux/386" }
|
173
|
+
runs-on: ubuntu-latest
|
174
|
+
steps:
|
175
|
+
- uses: actions/checkout@v4
|
176
|
+
- uses: actions/download-artifact@v4
|
177
|
+
with:
|
178
|
+
name: cruby-${{ matrix.platform }}-gem
|
179
|
+
path: precompiled/gems
|
180
|
+
- run: |
|
181
|
+
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
182
|
+
docker run --rm -v $PWD/precompiled:/precompiled -w /precompiled \
|
183
|
+
${{ matrix.docker_platform }} ruby:${{ matrix.ruby }}${{ matrix.docker_tag }} \
|
184
|
+
sh -c "
|
185
|
+
gem update --system &&
|
186
|
+
${{ matrix.bootstrap }}
|
187
|
+
./bin/test-gem-install gems
|
188
|
+
"
|
189
|
+
|
190
|
+
darwin-install:
|
191
|
+
needs: ["native-package", "ruby_versions"]
|
192
|
+
strategy:
|
193
|
+
fail-fast: false
|
194
|
+
matrix:
|
195
|
+
platform:
|
196
|
+
- arm64-darwin
|
197
|
+
- x86_64-darwin
|
198
|
+
ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }}
|
199
|
+
include:
|
200
|
+
- { platform: arm64-darwin, os: macos-14 }
|
201
|
+
- { platform: x86_64-darwin, os: macos-13 }
|
202
|
+
runs-on: ${{matrix.os}}
|
203
|
+
steps:
|
204
|
+
- uses: actions/checkout@v4
|
205
|
+
- uses: ruby/setup-ruby@v1
|
206
|
+
with:
|
207
|
+
ruby-version: "${{matrix.ruby}}"
|
208
|
+
- uses: actions/download-artifact@v4
|
209
|
+
with:
|
210
|
+
name: cruby-${{matrix.platform}}-gem
|
211
|
+
path: precompiled/gems
|
212
|
+
- run: ./bin/test-gem-install gems
|
213
|
+
working-directory: precompiled
|
214
|
+
|
215
|
+
windows-install:
|
216
|
+
needs: ["native-package", "ruby_versions"]
|
217
|
+
strategy:
|
218
|
+
fail-fast: false
|
219
|
+
matrix:
|
220
|
+
ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }}
|
221
|
+
runs-on: windows-2022
|
222
|
+
steps:
|
223
|
+
- uses: actions/checkout@v4
|
224
|
+
- uses: ruby/setup-ruby@v1
|
225
|
+
with:
|
226
|
+
ruby-version: "${{matrix.ruby}}"
|
227
|
+
- uses: actions/download-artifact@v4
|
228
|
+
with:
|
229
|
+
name: cruby-x64-mingw-ucrt-gem
|
230
|
+
path: precompiled/gems
|
231
|
+
- run: ./bin/test-gem-install gems
|
232
|
+
working-directory: precompiled
|
@@ -0,0 +1,40 @@
|
|
1
|
+
name: system
|
2
|
+
concurrency:
|
3
|
+
group: "${{github.workflow}}-${{github.ref}}"
|
4
|
+
cancel-in-progress: true
|
5
|
+
on:
|
6
|
+
workflow_dispatch:
|
7
|
+
schedule:
|
8
|
+
- cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
|
9
|
+
push:
|
10
|
+
branches:
|
11
|
+
- main
|
12
|
+
- v*.*.x
|
13
|
+
tags:
|
14
|
+
- v*.*.*
|
15
|
+
pull_request:
|
16
|
+
types: [opened, synchronize]
|
17
|
+
branches:
|
18
|
+
- '*'
|
19
|
+
paths: ["system/**/*", ".github/workflows/system.yml"]
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
system:
|
23
|
+
strategy:
|
24
|
+
fail-fast: false
|
25
|
+
matrix:
|
26
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4", "head"]
|
27
|
+
runs-on: ["ubuntu-latest", "macos-13", "windows-latest"]
|
28
|
+
runs-on: ${{matrix.runs-on}}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v4
|
31
|
+
- uses: MSP-Greg/setup-ruby-pkgs@v1
|
32
|
+
with:
|
33
|
+
working-directory: system
|
34
|
+
ruby-version: ${{matrix.ruby}}
|
35
|
+
bundler-cache: true
|
36
|
+
mingw: "libyaml" # windows
|
37
|
+
apt-get: "libyaml-dev" # linux
|
38
|
+
brew: "libyaml" # macos
|
39
|
+
- run: bundle exec rake compile test
|
40
|
+
working-directory: system
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
plugins:
|
2
|
+
- rubocop-minitest
|
3
|
+
- rubocop-performance
|
4
|
+
- rubocop-rake
|
5
|
+
|
6
|
+
inherit_mode:
|
7
|
+
merge:
|
8
|
+
- Exclude
|
9
|
+
|
10
|
+
AllCops:
|
11
|
+
SuggestExtensions: false
|
12
|
+
Exclude:
|
13
|
+
- "data/**/*"
|
14
|
+
|
15
|
+
# All cops except your using extensions are disabled by default.
|
16
|
+
Bundler:
|
17
|
+
Enabled: false
|
18
|
+
Gemspec:
|
19
|
+
Enabled: false
|
20
|
+
Layout:
|
21
|
+
Enabled: false
|
22
|
+
Lint:
|
23
|
+
Enabled: false
|
24
|
+
Metrics:
|
25
|
+
Enabled: false
|
26
|
+
Naming:
|
27
|
+
Enabled: false
|
28
|
+
Performance:
|
29
|
+
Enabled: false
|
30
|
+
Exclude:
|
31
|
+
- "test/**/*"
|
32
|
+
Security:
|
33
|
+
Enabled: false
|
34
|
+
Style:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
# Align `when` with `end`.
|
38
|
+
Layout/CaseIndentation:
|
39
|
+
Enabled: true
|
40
|
+
EnforcedStyle: end
|
41
|
+
|
42
|
+
# Align comments with method definitions.
|
43
|
+
Layout/CommentIndentation:
|
44
|
+
Enabled: true
|
45
|
+
|
46
|
+
Layout/ElseAlignment:
|
47
|
+
Enabled: true
|
48
|
+
|
49
|
+
Layout/EmptyLineAfterMagicComment:
|
50
|
+
Enabled: true
|
51
|
+
|
52
|
+
Layout/EmptyLinesAroundBlockBody:
|
53
|
+
Enabled: true
|
54
|
+
|
55
|
+
# In a regular class definition, no empty lines around the body.
|
56
|
+
Layout/EmptyLinesAroundClassBody:
|
57
|
+
Enabled: true
|
58
|
+
|
59
|
+
# In a regular method definition, no empty lines around the body.
|
60
|
+
Layout/EmptyLinesAroundMethodBody:
|
61
|
+
Enabled: true
|
62
|
+
|
63
|
+
# In a regular module definition, no empty lines around the body.
|
64
|
+
Layout/EmptyLinesAroundModuleBody:
|
65
|
+
Enabled: true
|
66
|
+
|
67
|
+
# Align `end` with the matching keyword or starting expression except for
|
68
|
+
# assignments, where it should be aligned with the LHS.
|
69
|
+
Layout/EndAlignment:
|
70
|
+
Enabled: true
|
71
|
+
EnforcedStyleAlignWith: variable
|
72
|
+
|
73
|
+
# Method definitions after `private` or `protected` isolated calls need one
|
74
|
+
# extra level of indentation.
|
75
|
+
#
|
76
|
+
# We break this rule in context, though, e.g. for private-only concerns,
|
77
|
+
# so we leave it disabled.
|
78
|
+
Layout/IndentationConsistency:
|
79
|
+
Enabled: false
|
80
|
+
EnforcedStyle: indented_internal_methods
|
81
|
+
|
82
|
+
# Detect hard tabs, no hard tabs.
|
83
|
+
Layout/IndentationStyle:
|
84
|
+
Enabled: true
|
85
|
+
|
86
|
+
# Two spaces, no tabs (for indentation).
|
87
|
+
#
|
88
|
+
# Doesn't behave properly with private-only concerns, so it's disabled.
|
89
|
+
Layout/IndentationWidth:
|
90
|
+
Enabled: false
|
91
|
+
|
92
|
+
Layout/LeadingCommentSpace:
|
93
|
+
Enabled: true
|
94
|
+
|
95
|
+
Layout/SpaceAfterColon:
|
96
|
+
Enabled: true
|
97
|
+
|
98
|
+
Layout/SpaceAfterComma:
|
99
|
+
Enabled: true
|
100
|
+
|
101
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
102
|
+
Enabled: true
|
103
|
+
|
104
|
+
Layout/SpaceAroundKeyword:
|
105
|
+
Enabled: true
|
106
|
+
|
107
|
+
# Use `foo {}` not `foo{}`.
|
108
|
+
Layout/SpaceBeforeBlockBraces:
|
109
|
+
Enabled: true
|
110
|
+
|
111
|
+
Layout/SpaceBeforeComma:
|
112
|
+
Enabled: true
|
113
|
+
|
114
|
+
Layout/SpaceBeforeFirstArg:
|
115
|
+
Enabled: true
|
116
|
+
|
117
|
+
# Use `->(x, y) { x + y }` not `-> (x, y) { x + y }`
|
118
|
+
Layout/SpaceInLambdaLiteral:
|
119
|
+
Enabled: true
|
120
|
+
|
121
|
+
# Use `[ a, [ b, c ] ]` not `[a, [b, c]]`
|
122
|
+
# Use `[]` not `[ ]`
|
123
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
124
|
+
Enabled: true
|
125
|
+
EnforcedStyle: space
|
126
|
+
EnforcedStyleForEmptyBrackets: no_space
|
127
|
+
|
128
|
+
# Use `%w[ a b ]` not `%w[ a b ]`.
|
129
|
+
Layout/SpaceInsideArrayPercentLiteral:
|
130
|
+
Enabled: true
|
131
|
+
|
132
|
+
# Use `foo { bar }` not `foo {bar}`.
|
133
|
+
# Use `foo { }` not `foo {}`.
|
134
|
+
Layout/SpaceInsideBlockBraces:
|
135
|
+
Enabled: true
|
136
|
+
EnforcedStyleForEmptyBraces: space
|
137
|
+
|
138
|
+
# Use `{ a: 1 }` not `{a:1}`.
|
139
|
+
# Use `{}` not `{ }`.
|
140
|
+
Layout/SpaceInsideHashLiteralBraces:
|
141
|
+
Enabled: true
|
142
|
+
EnforcedStyle: space
|
143
|
+
EnforcedStyleForEmptyBraces: no_space
|
144
|
+
|
145
|
+
# Use `foo(bar)` not `foo( bar )`
|
146
|
+
Layout/SpaceInsideParens:
|
147
|
+
Enabled: true
|
148
|
+
|
149
|
+
# Requiring a space is not yet supported as of 0.59.2
|
150
|
+
# Use `%w[ foo ]` not `%w[foo]`
|
151
|
+
Layout/SpaceInsidePercentLiteralDelimiters:
|
152
|
+
Enabled: false
|
153
|
+
#EnforcedStyle: space
|
154
|
+
|
155
|
+
# Use `hash[:key]` not `hash[ :key ]`
|
156
|
+
Layout/SpaceInsideReferenceBrackets:
|
157
|
+
Enabled: true
|
158
|
+
|
159
|
+
# Blank lines should not have any spaces.
|
160
|
+
Layout/TrailingEmptyLines:
|
161
|
+
Enabled: true
|
162
|
+
|
163
|
+
# No trailing whitespace.
|
164
|
+
Layout/TrailingWhitespace:
|
165
|
+
Enabled: true
|
166
|
+
|
167
|
+
Lint/RedundantStringCoercion:
|
168
|
+
Enabled: true
|
169
|
+
|
170
|
+
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
|
171
|
+
Lint/RequireParentheses:
|
172
|
+
Enabled: true
|
173
|
+
|
174
|
+
Lint/UriEscapeUnescape:
|
175
|
+
Enabled: true
|
176
|
+
|
177
|
+
Performance/FlatMap:
|
178
|
+
Enabled: true
|
179
|
+
|
180
|
+
# We generally prefer &&/|| but like low-precedence and/or in context
|
181
|
+
Style/AndOr:
|
182
|
+
Enabled: false
|
183
|
+
|
184
|
+
# Prefer Foo.method over Foo::method
|
185
|
+
Style/ColonMethodCall:
|
186
|
+
Enabled: true
|
187
|
+
|
188
|
+
Style/DefWithParentheses:
|
189
|
+
Enabled: true
|
190
|
+
|
191
|
+
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
|
192
|
+
Style/HashSyntax:
|
193
|
+
Enabled: true
|
194
|
+
EnforcedShorthandSyntax: either
|
195
|
+
|
196
|
+
# Defining a method with parameters needs parentheses.
|
197
|
+
Style/MethodDefParentheses:
|
198
|
+
Enabled: true
|
199
|
+
|
200
|
+
Style/ParenthesesAroundCondition:
|
201
|
+
Enabled: true
|
202
|
+
|
203
|
+
Style/PercentLiteralDelimiters:
|
204
|
+
Enabled: true
|
205
|
+
PreferredDelimiters:
|
206
|
+
default: "()"
|
207
|
+
"%i": "[]"
|
208
|
+
"%I": "[]"
|
209
|
+
"%r": "{}"
|
210
|
+
"%w": "[]"
|
211
|
+
"%W": "[]"
|
212
|
+
|
213
|
+
# Use quotes for string literals when they are enough.
|
214
|
+
Style/RedundantPercentQ:
|
215
|
+
Enabled: false
|
216
|
+
|
217
|
+
Style/RedundantReturn:
|
218
|
+
Enabled: true
|
219
|
+
AllowMultipleReturnValues: true
|
220
|
+
|
221
|
+
Style/Semicolon:
|
222
|
+
Enabled: true
|
223
|
+
AllowAsExpressionSeparator: true
|
224
|
+
|
225
|
+
Style/StabbyLambdaParentheses:
|
226
|
+
Enabled: true
|
227
|
+
|
228
|
+
# Use `"foo"` not `'foo'` unless escaping is required
|
229
|
+
Style/StringLiterals:
|
230
|
+
Enabled: true
|
231
|
+
EnforcedStyle: double_quotes
|
232
|
+
Include:
|
233
|
+
- "app/**/*"
|
234
|
+
- "config/**/*"
|
235
|
+
- "lib/**/*"
|
236
|
+
- "test/**/*"
|
237
|
+
- "Gemfile"
|
238
|
+
|
239
|
+
Style/TrailingCommaInArrayLiteral:
|
240
|
+
Enabled: true
|
241
|
+
|
242
|
+
Style/TrailingCommaInHashLiteral:
|
243
|
+
Enabled: true
|
244
|
+
|
245
|
+
Minitest/AssertInDelta: # new in 0.10
|
246
|
+
Enabled: true
|
247
|
+
Minitest/AssertKindOf: # new in 0.10
|
248
|
+
Enabled: true
|
249
|
+
Minitest/AssertOperator: # new in 0.32
|
250
|
+
Enabled: true
|
251
|
+
Minitest/AssertOutput: # new in 0.10
|
252
|
+
Enabled: true
|
253
|
+
Minitest/AssertPathExists: # new in 0.10
|
254
|
+
Enabled: true
|
255
|
+
Minitest/AssertPredicate: # new in 0.18
|
256
|
+
Enabled: true
|
257
|
+
Minitest/AssertRaisesCompoundBody: # new in 0.21
|
258
|
+
Enabled: true
|
259
|
+
Minitest/AssertRaisesWithRegexpArgument: # new in 0.22
|
260
|
+
Enabled: true
|
261
|
+
Minitest/AssertSame: # new in 0.26
|
262
|
+
Enabled: true
|
263
|
+
Minitest/AssertSilent: # new in 0.10
|
264
|
+
Enabled: true
|
265
|
+
Minitest/AssertWithExpectedArgument: # new in 0.11
|
266
|
+
Enabled: true
|
267
|
+
Minitest/AssertionInLifecycleHook: # new in 0.10
|
268
|
+
Enabled: true
|
269
|
+
Minitest/DuplicateTestRun: # new in 0.19
|
270
|
+
Enabled: true
|
271
|
+
Minitest/EmptyLineBeforeAssertionMethods: # new in 0.23
|
272
|
+
Enabled: true
|
273
|
+
Minitest/Focus: # new in 0.35
|
274
|
+
Enabled: true
|
275
|
+
Minitest/LifecycleHooksOrder: # new in 0.28
|
276
|
+
Enabled: true
|
277
|
+
Minitest/LiteralAsActualArgument: # new in 0.10
|
278
|
+
Enabled: true
|
279
|
+
Minitest/MultipleAssertions: # new in 0.10
|
280
|
+
Enabled: true
|
281
|
+
Minitest/NonExecutableTestMethod: # new in 0.34
|
282
|
+
Enabled: true
|
283
|
+
Minitest/NonPublicTestMethod: # new in 0.27
|
284
|
+
Enabled: true
|
285
|
+
Minitest/RedundantMessageArgument: # new in 0.34
|
286
|
+
Enabled: true
|
287
|
+
Minitest/RefuteInDelta: # new in 0.10
|
288
|
+
Enabled: true
|
289
|
+
Minitest/RefuteKindOf: # new in 0.10
|
290
|
+
Enabled: true
|
291
|
+
Minitest/RefuteOperator: # new in 0.32
|
292
|
+
Enabled: true
|
293
|
+
Minitest/RefutePathExists: # new in 0.10
|
294
|
+
Enabled: true
|
295
|
+
Minitest/RefutePredicate: # new in 0.18
|
296
|
+
Enabled: true
|
297
|
+
Minitest/RefuteSame: # new in 0.26
|
298
|
+
Enabled: true
|
299
|
+
Minitest/ReturnInTestMethod: # new in 0.31
|
300
|
+
Enabled: true
|
301
|
+
Minitest/SkipEnsure: # new in 0.20
|
302
|
+
Enabled: true
|
303
|
+
Minitest/SkipWithoutReason: # new in 0.24
|
304
|
+
Enabled: true
|
305
|
+
Minitest/TestFileName: # new in 0.26
|
306
|
+
Enabled: true
|
307
|
+
Minitest/TestMethodName: # new in 0.10
|
308
|
+
Enabled: true
|
309
|
+
Minitest/UnreachableAssertion: # new in 0.14
|
310
|
+
Enabled: true
|
311
|
+
Minitest/UnspecifiedException: # new in 0.10
|
312
|
+
Enabled: true
|
313
|
+
Minitest/UselessAssertion: # new in 0.26
|
314
|
+
Enabled: true
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in fts_fuzzy_match.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'rake', '~> 13.0'
|
9
|
+
gem 'rake-compiler'
|
10
|
+
gem 'minitest', '~> 5.0'
|
11
|
+
gem 'json'
|
12
|
+
|
13
|
+
gem 'rubocop-minitest'
|
14
|
+
gem 'rubocop-performance'
|
15
|
+
gem 'rubocop-rake'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fts_fuzzy_match (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.3)
|
10
|
+
json (2.12.2)
|
11
|
+
language_server-protocol (3.17.0.5)
|
12
|
+
lint_roller (1.1.0)
|
13
|
+
minitest (5.25.5)
|
14
|
+
parallel (1.27.0)
|
15
|
+
parser (3.3.8.0)
|
16
|
+
ast (~> 2.4.1)
|
17
|
+
racc
|
18
|
+
prism (1.4.0)
|
19
|
+
racc (1.8.1)
|
20
|
+
rainbow (3.1.1)
|
21
|
+
rake (13.3.0)
|
22
|
+
rake-compiler (1.3.0)
|
23
|
+
rake
|
24
|
+
regexp_parser (2.10.0)
|
25
|
+
rubocop (1.76.0)
|
26
|
+
json (~> 2.3)
|
27
|
+
language_server-protocol (~> 3.17.0.2)
|
28
|
+
lint_roller (~> 1.1.0)
|
29
|
+
parallel (~> 1.10)
|
30
|
+
parser (>= 3.3.0.2)
|
31
|
+
rainbow (>= 2.2.2, < 4.0)
|
32
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
33
|
+
rubocop-ast (>= 1.45.0, < 2.0)
|
34
|
+
ruby-progressbar (~> 1.7)
|
35
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
36
|
+
rubocop-ast (1.45.1)
|
37
|
+
parser (>= 3.3.7.2)
|
38
|
+
prism (~> 1.4)
|
39
|
+
rubocop-minitest (0.38.1)
|
40
|
+
lint_roller (~> 1.1)
|
41
|
+
rubocop (>= 1.75.0, < 2.0)
|
42
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
43
|
+
rubocop-performance (1.25.0)
|
44
|
+
lint_roller (~> 1.1)
|
45
|
+
rubocop (>= 1.75.0, < 2.0)
|
46
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
47
|
+
rubocop-rake (0.7.1)
|
48
|
+
lint_roller (~> 1.1)
|
49
|
+
rubocop (>= 1.72.1)
|
50
|
+
ruby-progressbar (1.13.0)
|
51
|
+
unicode-display_width (3.1.4)
|
52
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
53
|
+
unicode-emoji (4.0.4)
|
54
|
+
|
55
|
+
PLATFORMS
|
56
|
+
arm64-darwin-24
|
57
|
+
ruby
|
58
|
+
|
59
|
+
DEPENDENCIES
|
60
|
+
fts_fuzzy_match!
|
61
|
+
json
|
62
|
+
minitest (~> 5.0)
|
63
|
+
rake (~> 13.0)
|
64
|
+
rake-compiler
|
65
|
+
rubocop-minitest
|
66
|
+
rubocop-performance
|
67
|
+
rubocop-rake
|
68
|
+
|
69
|
+
BUNDLED WITH
|
70
|
+
2.6.9
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# FtsFuzzyMatch
|
2
|
+
|
3
|
+
This will return a match score from a pattern and string. These scores are only
|
4
|
+
useful for sorting against each other.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
## Credits
|
11
|
+
|
12
|
+
- This gem was started by using the Ruby C Extensions Explained project at
|
13
|
+
https://github.com/flavorjones/ruby-c-extensions-explained
|
14
|
+
- This gem used the fts fuzzy_match library from
|
15
|
+
https://github.com/forrestthewoods/lib_fts/tree/master
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rubygems/package_task"
|
5
|
+
require "rake/testtask"
|
6
|
+
require "rake/extensiontask"
|
7
|
+
|
8
|
+
fts_fuzzy_match_spec = Bundler.load_gemspec("fts_fuzzy_match.gemspec")
|
9
|
+
Gem::PackageTask.new(fts_fuzzy_match_spec).define
|
10
|
+
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
12
|
+
t.libs << "test"
|
13
|
+
t.libs << "lib"
|
14
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::ExtensionTask.new("fts_fuzzy_match") do |ext|
|
18
|
+
ext.lib_dir = "lib/fts_fuzzy_match"
|
19
|
+
end
|
20
|
+
|
21
|
+
task default: [:clobber, :compile, :test]
|
22
|
+
|
23
|
+
CLEAN.add("{ext,lib}/**/*.{o,so}", "pkg")
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#include "fts_fuzzy_match.h"
|
2
|
+
#define FTS_FUZZY_MATCH_IMPLEMENTATION
|
3
|
+
#include "fts_fuzzy_match_impl.h"
|
4
|
+
|
5
|
+
VALUE rb_mFtsFuzzyMatch;
|
6
|
+
VALUE rb_cFtsFuzzyMatchExtension;
|
7
|
+
|
8
|
+
static VALUE
|
9
|
+
rb_fts_fuzzy_match_extension_class_fuzzy_match(VALUE self, VALUE pattern, VALUE str)
|
10
|
+
{
|
11
|
+
char* patternPtr;
|
12
|
+
patternPtr = StringValueCStr(pattern);
|
13
|
+
char* strPtr;
|
14
|
+
strPtr = StringValueCStr(str);
|
15
|
+
|
16
|
+
int outScore;
|
17
|
+
int matched = fts_fuzzy_match_simple(patternPtr, strPtr, &outScore);
|
18
|
+
// return rb_sprintf("Matched: %d\nScore: %d\n", matched, outScore);
|
19
|
+
if (matched) {
|
20
|
+
return INT2FIX(outScore);
|
21
|
+
} else {
|
22
|
+
return Qfalse;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
struct StringScore {
|
27
|
+
VALUE str;
|
28
|
+
bool matched;
|
29
|
+
int score;
|
30
|
+
};
|
31
|
+
|
32
|
+
int comp(const void *a, const void *b) {
|
33
|
+
const struct StringScore *aa = (const struct StringScore *)a;
|
34
|
+
const struct StringScore *bb = (const struct StringScore *)b;
|
35
|
+
if (!aa->matched && !bb->matched) {
|
36
|
+
return 0;
|
37
|
+
} else if (aa->matched && !bb->matched) {
|
38
|
+
return -1;
|
39
|
+
} else if (!aa->matched && bb->matched) {
|
40
|
+
return 1;
|
41
|
+
}
|
42
|
+
return bb->score - aa->score;
|
43
|
+
}
|
44
|
+
|
45
|
+
static VALUE
|
46
|
+
rb_fts_fuzzy_match_extension_class_sort_n(VALUE self, VALUE pattern, VALUE strings, VALUE n)
|
47
|
+
{
|
48
|
+
char* patternPtr;
|
49
|
+
patternPtr = StringValueCStr(pattern);
|
50
|
+
long stringsLen = RARRAY_LEN(strings);
|
51
|
+
|
52
|
+
struct StringScore *scores = (struct StringScore *)malloc(stringsLen * sizeof(struct StringScore));
|
53
|
+
for (long i=0; i<stringsLen; i++) {
|
54
|
+
const VALUE str = RARRAY_AREF(strings, i);
|
55
|
+
const char* strPtr = StringValueCStr(str);
|
56
|
+
scores[i].str = str;
|
57
|
+
scores[i].matched = fts_fuzzy_match_simple(patternPtr, strPtr, &scores[i].score);
|
58
|
+
}
|
59
|
+
|
60
|
+
qsort(scores, stringsLen, sizeof(struct StringScore), comp);
|
61
|
+
|
62
|
+
long n2 = NUM2INT(n);
|
63
|
+
if (n2 > stringsLen) n2 = stringsLen;
|
64
|
+
|
65
|
+
VALUE result = rb_ary_new_capa(n2);
|
66
|
+
for (long i=0; i<n2; i++) {
|
67
|
+
rb_ary_push(result, scores[i].str);
|
68
|
+
}
|
69
|
+
|
70
|
+
return result;
|
71
|
+
}
|
72
|
+
|
73
|
+
void
|
74
|
+
Init_fts_fuzzy_match(void)
|
75
|
+
{
|
76
|
+
rb_mFtsFuzzyMatch = rb_define_module("FtsFuzzyMatch");
|
77
|
+
rb_cFtsFuzzyMatchExtension = rb_define_class_under(rb_mFtsFuzzyMatch, "Extension", rb_cObject);
|
78
|
+
rb_define_singleton_method(rb_cFtsFuzzyMatchExtension, "fuzzy_match",
|
79
|
+
rb_fts_fuzzy_match_extension_class_fuzzy_match, 2);
|
80
|
+
rb_define_singleton_method(rb_cFtsFuzzyMatchExtension, "sort_n",
|
81
|
+
rb_fts_fuzzy_match_extension_class_sort_n, 3);
|
82
|
+
}
|
@@ -0,0 +1,203 @@
|
|
1
|
+
// LICENSE
|
2
|
+
//
|
3
|
+
// This software is dual-licensed to the public domain and under the following
|
4
|
+
// license: you are granted a perpetual, irrevocable license to copy, modify,
|
5
|
+
// publish, and distribute this file as you see fit.
|
6
|
+
//
|
7
|
+
// VERSION
|
8
|
+
// 0.2.0 (2017-02-18) Scored matches perform exhaustive search for best score
|
9
|
+
// 0.1.0 (2016-03-28) Initial release
|
10
|
+
//
|
11
|
+
// AUTHOR
|
12
|
+
// Forrest Smith
|
13
|
+
//
|
14
|
+
// NOTES
|
15
|
+
// Compiling
|
16
|
+
// You MUST add '#define FTS_FUZZY_MATCH_IMPLEMENTATION' before including this header in ONE source file to create implementation.
|
17
|
+
//
|
18
|
+
// fts_fuzzy_match_simple(...)
|
19
|
+
// Simplified version of fts_fuzzy_match
|
20
|
+
//
|
21
|
+
// fts_fuzzy_match(...)
|
22
|
+
// Returns true if pattern is found AND calculates a score.
|
23
|
+
// Performs exhaustive search via recursion to find all possible matches and match with highest score.
|
24
|
+
// Scores values have no intrinsic meaning. Possible score range is not normalized and varies with pattern.
|
25
|
+
// Recursion is limited internally (default=10) to prevent degenerate cases (pattern="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
26
|
+
// Uses uint8_t for match indices. Therefore patterns are limited to 256 characters.
|
27
|
+
// Score system should be tuned for YOUR use case. Words, sentences, file names, or method names all prefer different tuning.
|
28
|
+
|
29
|
+
|
30
|
+
#ifndef FTS_FUZZY_MATCH_H
|
31
|
+
#define FTS_FUZZY_MATCH_H
|
32
|
+
|
33
|
+
|
34
|
+
#include <ctype.h> // tolower, toupper
|
35
|
+
|
36
|
+
// Public interface
|
37
|
+
static bool fts_fuzzy_match_simple(char const * pattern, char const * str, int * outScore);
|
38
|
+
static bool fts_fuzzy_match(char const * pattern, char const * str, int * outScore, uint8_t * matches, int maxMatches);
|
39
|
+
|
40
|
+
#ifdef FTS_FUZZY_MATCH_IMPLEMENTATION
|
41
|
+
|
42
|
+
// Private interface
|
43
|
+
static bool fts_fuzzy_match_recursive(const char * pattern, const char * str, int * outScore,
|
44
|
+
const char * strBegin, uint8_t const * srcMatches, uint8_t * matches, int maxMatches,
|
45
|
+
int nextMatch, int * recursionCount, int recursionLimit);
|
46
|
+
|
47
|
+
static bool fts_fuzzy_match_simple(char const * pattern, char const * str, int * outScore) {
|
48
|
+
uint8_t matches[256];
|
49
|
+
return fts_fuzzy_match(pattern, str, outScore, matches, sizeof(matches));
|
50
|
+
}
|
51
|
+
|
52
|
+
static bool fts_fuzzy_match(char const * pattern, char const * str, int * outScore, uint8_t * matches, int maxMatches) {
|
53
|
+
int recursionCount = 0;
|
54
|
+
int recursionLimit = 10;
|
55
|
+
|
56
|
+
return fts_fuzzy_match_recursive(pattern, str, outScore, str, NULL, matches, maxMatches, 0, &recursionCount, recursionLimit);
|
57
|
+
}
|
58
|
+
|
59
|
+
// Private implementation
|
60
|
+
static bool fts_fuzzy_match_recursive(const char * pattern, const char * str, int * outScore,
|
61
|
+
const char * strBegin, uint8_t const * srcMatches, uint8_t * matches, int maxMatches,
|
62
|
+
int nextMatch, int * recursionCount, int recursionLimit)
|
63
|
+
{
|
64
|
+
// Count recursions
|
65
|
+
++*recursionCount;
|
66
|
+
if (*recursionCount >= recursionLimit)
|
67
|
+
return false;
|
68
|
+
|
69
|
+
// Detect end of strings
|
70
|
+
if (*pattern == '\0' || *str == '\0')
|
71
|
+
return false;
|
72
|
+
|
73
|
+
unsigned long stringLength = strlen(str);
|
74
|
+
|
75
|
+
// Recursion params
|
76
|
+
bool recursiveMatch = false;
|
77
|
+
uint8_t bestRecursiveMatches[256];
|
78
|
+
int bestRecursiveScore = 0;
|
79
|
+
|
80
|
+
// Loop through pattern and str looking for a match
|
81
|
+
bool first_match = true;
|
82
|
+
while (*pattern != '\0' && *str != '\0') {
|
83
|
+
|
84
|
+
// Found match
|
85
|
+
if (tolower(*pattern) == tolower(*str)) {
|
86
|
+
|
87
|
+
// Supplied matches buffer was too short
|
88
|
+
if (nextMatch >= maxMatches)
|
89
|
+
return false;
|
90
|
+
|
91
|
+
// "Copy-on-Write" srcMatches into matches
|
92
|
+
if (first_match && srcMatches) {
|
93
|
+
memcpy(matches, srcMatches, nextMatch);
|
94
|
+
first_match = false;
|
95
|
+
}
|
96
|
+
|
97
|
+
// Recursive call that "skips" this match
|
98
|
+
uint8_t recursiveMatches[256];
|
99
|
+
int recursiveScore;
|
100
|
+
if (fts_fuzzy_match_recursive(pattern, str + 1, &recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) {
|
101
|
+
|
102
|
+
// Pick best recursive score
|
103
|
+
if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
|
104
|
+
memcpy(bestRecursiveMatches, recursiveMatches, 256);
|
105
|
+
bestRecursiveScore = recursiveScore;
|
106
|
+
}
|
107
|
+
recursiveMatch = true;
|
108
|
+
}
|
109
|
+
|
110
|
+
// Advance
|
111
|
+
matches[nextMatch++] = (uint8_t)(str - strBegin);
|
112
|
+
++pattern;
|
113
|
+
}
|
114
|
+
++str;
|
115
|
+
}
|
116
|
+
|
117
|
+
// Determine if full pattern was matched
|
118
|
+
bool matched = *pattern == '\0' ? true : false;
|
119
|
+
|
120
|
+
// Calculate score
|
121
|
+
if (matched) {
|
122
|
+
const int sequential_bonus = 20; // bonus for adjacent matches (DEFAULT: 15)
|
123
|
+
const int separator_bonus = 30; // bonus if match occurs after a separator
|
124
|
+
const int camel_bonus = 0; // bonus if match is uppercase and prev is lower (DEFAULT: 30)
|
125
|
+
const int first_letter_bonus = 15; // bonus if the first letter is matched
|
126
|
+
|
127
|
+
const int leading_letter_penalty = -5; // penalty applied for every letter in str before the first match
|
128
|
+
const int max_leading_letter_penalty = -15; // maximum penalty for leading letters
|
129
|
+
const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter
|
130
|
+
const int string_length_penalty = -1; // (DEFAULT: 0)
|
131
|
+
|
132
|
+
// Iterate str to end
|
133
|
+
while (*str != '\0')
|
134
|
+
++str;
|
135
|
+
|
136
|
+
// Initialize score
|
137
|
+
*outScore = 100;
|
138
|
+
|
139
|
+
// Apply length penalty
|
140
|
+
*outScore += stringLength * string_length_penalty;
|
141
|
+
|
142
|
+
// Apply leading letter penalty
|
143
|
+
int penalty = leading_letter_penalty * matches[0];
|
144
|
+
if (penalty < max_leading_letter_penalty)
|
145
|
+
penalty = max_leading_letter_penalty;
|
146
|
+
*outScore += penalty;
|
147
|
+
|
148
|
+
// Apply unmatched penalty
|
149
|
+
int unmatched = (int)(str - strBegin) - nextMatch;
|
150
|
+
*outScore += unmatched_letter_penalty * unmatched;
|
151
|
+
|
152
|
+
// Apply ordering bonuses
|
153
|
+
for (int i = 0; i < nextMatch; ++i) {
|
154
|
+
uint8_t currIdx = matches[i];
|
155
|
+
|
156
|
+
if (i > 0) {
|
157
|
+
uint8_t prevIdx = matches[i - 1];
|
158
|
+
|
159
|
+
// Sequential
|
160
|
+
if (currIdx == (prevIdx + 1))
|
161
|
+
*outScore += sequential_bonus;
|
162
|
+
}
|
163
|
+
|
164
|
+
// Check for bonuses based on neighbor character value
|
165
|
+
if (currIdx > 0) {
|
166
|
+
// Camel case
|
167
|
+
char neighbor = strBegin[currIdx - 1];
|
168
|
+
char curr = strBegin[currIdx];
|
169
|
+
if (islower(neighbor) && isupper(curr))
|
170
|
+
*outScore += camel_bonus;
|
171
|
+
|
172
|
+
// Separator
|
173
|
+
bool neighborSeparator = neighbor == '_' || neighbor == ' ';
|
174
|
+
if (neighborSeparator)
|
175
|
+
*outScore += separator_bonus;
|
176
|
+
}
|
177
|
+
else {
|
178
|
+
// First letter
|
179
|
+
*outScore += first_letter_bonus;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
// Return best result
|
185
|
+
if (recursiveMatch && (!matched || bestRecursiveScore > *outScore)) {
|
186
|
+
// Recursive score is better than "this"
|
187
|
+
memcpy(matches, bestRecursiveMatches, maxMatches);
|
188
|
+
*outScore = bestRecursiveScore;
|
189
|
+
return true;
|
190
|
+
}
|
191
|
+
else if (matched) {
|
192
|
+
// "this" score is better than recursive
|
193
|
+
return true;
|
194
|
+
}
|
195
|
+
else {
|
196
|
+
// no match
|
197
|
+
return false;
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
#endif // FTS_FUZZY_MATCH_IMPLEMENTATION
|
202
|
+
|
203
|
+
#endif // FTS_FUZZY_MATCH_H
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/fts_fuzzy_match/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "fts_fuzzy_match"
|
7
|
+
spec.version = FtsFuzzyMatch::VERSION
|
8
|
+
spec.authors = ["Dave Goddard"]
|
9
|
+
spec.email = ["dave@goddard.au"]
|
10
|
+
|
11
|
+
spec.summary = "Gem to use FTS for fuzzy matching."
|
12
|
+
spec.description = "Gem to use FTS for fuzzy matching."
|
13
|
+
spec.homepage = "https://github.com/dgodd/fuzzy_match"
|
14
|
+
spec.required_ruby_version = ">= 3.3.0"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
spec.extensions = ["ext/fts_fuzzy_match/extconf.rb"]
|
26
|
+
|
27
|
+
# Uncomment to register a new dependency of your gem
|
28
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
29
|
+
|
30
|
+
# For more information and examples about making a new gem, checkout our
|
31
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "fts_fuzzy_match/version"
|
4
|
+
require_relative "fts_fuzzy_match/fts_fuzzy_match"
|
5
|
+
|
6
|
+
# FTS Fuzzy Match module. Can score or sort
|
7
|
+
module FtsFuzzyMatch
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
def self.sort_in_ruby(pattern, strings)
|
11
|
+
# fuzzy_match is -50..50 so -200 is the lowest possible score
|
12
|
+
strings.sort_by { |string| -1 * (::FtsFuzzyMatch::Extension.fuzzy_match(pattern, string) || -200) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.sort_n(pattern, strings, n)
|
16
|
+
::FtsFuzzyMatch::Extension.sort_n(pattern, strings, n)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.sort(pattern, strings)
|
20
|
+
::FtsFuzzyMatch::Extension.sort_n(pattern, strings, strings.length)
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fts_fuzzy_match
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dave Goddard
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: Gem to use FTS for fuzzy matching.
|
13
|
+
email:
|
14
|
+
- dave@goddard.au
|
15
|
+
executables: []
|
16
|
+
extensions:
|
17
|
+
- ext/fts_fuzzy_match/extconf.rb
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".github/dependabot.yml"
|
21
|
+
- ".github/workflows/fuzzy_match.yml"
|
22
|
+
- ".github/workflows/packaged_source.yml"
|
23
|
+
- ".github/workflows/packaged_tarball.yml"
|
24
|
+
- ".github/workflows/precompiled.yml"
|
25
|
+
- ".github/workflows/system.yml"
|
26
|
+
- ".gitignore"
|
27
|
+
- ".rubocop.yml"
|
28
|
+
- Gemfile
|
29
|
+
- Gemfile.lock
|
30
|
+
- README.md
|
31
|
+
- Rakefile
|
32
|
+
- ext/fts_fuzzy_match/extconf.rb
|
33
|
+
- ext/fts_fuzzy_match/fts_fuzzy_match.c
|
34
|
+
- ext/fts_fuzzy_match/fts_fuzzy_match.h
|
35
|
+
- ext/fts_fuzzy_match/fts_fuzzy_match_impl.h
|
36
|
+
- fts_fuzzy_match.gemspec
|
37
|
+
- lib/fts_fuzzy_match.rb
|
38
|
+
- lib/fts_fuzzy_match/version.rb
|
39
|
+
homepage: https://github.com/dgodd/fuzzy_match
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata: {}
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.3.0
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.6.9
|
58
|
+
specification_version: 4
|
59
|
+
summary: Gem to use FTS for fuzzy matching.
|
60
|
+
test_files: []
|