fat_core 7.2.0 → 7.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a1056fe04e707d5c3a9693876c26536997f14a2a22e8b431fc8c5012b30ec92
4
- data.tar.gz: e8e58c146a069ca694c07f42ffda4ebd255a8e8f7faa7fc65ad3f07de95206f7
3
+ metadata.gz: ab67f4ddf21be6773a44d0361acdc01257e4f390222c0a5da8434dadc80b108c
4
+ data.tar.gz: 33f3e511b3d2e1262d979f9909d8ad5a0a3c8b638756fb1a25dddaf302dcef65
5
5
  SHA512:
6
- metadata.gz: 474f00944f264d40761579329b2665d5119bb03b2758d7f4752f42f0fab6ded67162bd26c94db88e895bb19838500355f3abd7aba879b3d01bb8047ecc9990ec
7
- data.tar.gz: 303429d1884c5e344e90c3214f9566fc3ea753a6a7eda8d1df34e317b202cd02dcce8001eb3cc7e134eb348aecf99f2e059b8015d78af0c0c097b6bc39c2fe74
6
+ metadata.gz: 2e78ef4bf3001f91e8c43b7dd6216f5edc65aa8945db4dd79e77d75506a8233d30fafdc01e5e27099f53ee01daffe12fdeb06d7da04cbc9f28d5bce65005e502
7
+ data.tar.gz: a6dcf793b1d698e667570d40d89d2cced098b08d81d70d42e7a908eed1f787bfffcbd8cd0e1d6c1c732fe3472f2f84f9cf5d5fc2aa51c9e69b2a2417ce1a6497
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
@@ -24,3 +23,4 @@ tmp
24
23
  /.idea/
25
24
  /TAGS
26
25
  /.tangle-stamp
26
+ /AGENTS.*
data/CHANGELOG.md ADDED
@@ -0,0 +1,97 @@
1
+ - [Version 7.3.0](#org5f61e5a)
2
+ - [String](#org633deb6)
3
+ - [Version 7.1.1](#org003674c)
4
+ - [String](#org5df7dd5)
5
+ - [Version 7.0.0 from Version 6.0.0](#org3aecf27)
6
+ - [Enumerable](#orgff23338)
7
+ - [Hash](#org408609b)
8
+ - [Numeric](#org138cd73)
9
+ - [Range](#orgdebd7ac)
10
+ - [String](#org075bb7e)
11
+ - [Kernel](#org545d137)
12
+
13
+
14
+ <a id="org5f61e5a"></a>
15
+
16
+ # Version 7.3.0
17
+
18
+
19
+ <a id="org633deb6"></a>
20
+
21
+ ## String
22
+
23
+ Another visit to String#fuzzy\_match to ensure that a subject string is matched by its own matcher. This bug got tickled when 'Amazon.com' failed to match 'Amazon.com', which just isn't right.
24
+
25
+
26
+ <a id="org003674c"></a>
27
+
28
+ # Version 7.1.1
29
+
30
+
31
+ <a id="org5df7dd5"></a>
32
+
33
+ ## String
34
+
35
+ - `String#fuzzy_match` make period, comma, asterisk, and apostrophes optional in matcher instead of just deleting them and in the subject string replace periods and commas with spaces so they still act as word separators.
36
+
37
+ Before `'WWW.WOLFRAM.COM'.fuzzy_match('www:wolfram')` would not match because the periods were just eliminated so the matcher was looking for a word starting with 'wolfram' but the subject string was converted to 'WWWWOLFRAMCOM' so no match. Now it is converted to 'WWW WOLFRAM COM' so the match succeeds.
38
+
39
+
40
+ <a id="org3aecf27"></a>
41
+
42
+ # Version 7.0.0 from Version 6.0.0
43
+
44
+
45
+ <a id="orgff23338"></a>
46
+
47
+ ## Enumerable
48
+
49
+ - Remove `#groups_of` as duplicative of `each_slice`
50
+
51
+
52
+ <a id="org408609b"></a>
53
+
54
+ ## Hash
55
+
56
+ - Rename `delete_with_value` to `delete_with_values!` to make it clear that the Hash is modified in place,
57
+ - Add `delete_with_value` as a non-mutating variant
58
+ - Change arg to `keys_with_value(vals)` to allow variable arguments for multiple values instead of requiring an Array,
59
+ - Allow `<<` operator to merge any `Enumerable` as well as a `Hash`
60
+
61
+
62
+ <a id="org138cd73"></a>
63
+
64
+ ## Numeric
65
+
66
+ - Enhance `#tex_quote` to put special numbers such as `Math::PI`, `Math::E`, and `Float::INFINITY` in TeX notation, so, e.g., Complex(Math::E, Math:PI).tex\_quote will render to "$\\pi+e i$", Rationals will be rendered as fractions, etc.
67
+
68
+
69
+ <a id="orgdebd7ac"></a>
70
+
71
+ ## Range
72
+
73
+ - `Range#gaps` now better handles Ranges with countable endpoints that need `:succ` and `:pred` methods,
74
+ - `tex_quote` applies `#tex_quote` to endpoints if they respond to a `#tex_quote` method,
75
+ - Check argument Ranges to `#gaps`, `#spanned_by?`, and `#overlaps` for compatibility with the subject Range,
76
+
77
+
78
+ <a id="org075bb7e"></a>
79
+
80
+ ## String
81
+
82
+ - `String#fuzzy_match` changes the semantics of the string matcher argument:
83
+ 1. an internal ':' preceded and followed by non-whitespace now only applies to the word following it to indicate that the word must be a word boundary, so 'hel:wor' matches 'Shell worms' as well as 'Hello, world'.
84
+ 2. an internal ':' preceded by whitespace but followed by non-whitespace is also treated as requiring a word boundary for the following word but not the preceding word, as in 1, so 'hel :wor'
85
+ 3. but an internal ':' preceded by non-whitespace but followed by whitespace requires a word boundary after the preceding word, so 'hel: wor'
86
+ 4. a lone matcher now allows a match anywhere in the subject text regardless of boundaries; the prior version added a word boundary restriction to the bare matcher, so before 'zz' would not match "Pizza" since 'zz' did not occur at the end of word, but now 'zz' will match "Pizza". Rule 3 above allows putting a boundary restriction on the matcher with 'zz: ' as before.
87
+ 5. Leading and trailing ':' continue to require the text to occur at the beginning or end, respectively, of the subject string, so ':hel' and 'ld:' match "Hello, world" but ':wor' and 'wor:' do not.
88
+ - `String#number` no longer considers a hex number with a decimal point to be in the form of a number
89
+ - provide a :pred method in a refinement accesses with 'using StringPred
90
+ - `String#entitle` no longer tries to detect general acronyms for upper case handling since it was unreliable. Thus, `"ibm corporation".enetitle` becomers "Ibm Corporation" rather than "IBM corporation."
91
+
92
+
93
+ <a id="org545d137"></a>
94
+
95
+ ## Kernel
96
+
97
+ - removed the `Kernel.time_it` method since Benchmark already does this better.
data/CHANGELOG.org CHANGED
@@ -1,3 +1,8 @@
1
+ * Version 7.3.0
2
+ ** String
3
+ Another visit to String#fuzzy_match to ensure that a subject string is matched
4
+ by its own matcher. This bug got tickled when 'Amazon.com' failed to match
5
+ 'Amazon.com', which just isn't right.
1
6
  * Version 7.1.1
2
7
  ** String
3
8
  - =String#fuzzy_match= make period, comma, asterisk, and apostrophes optional
data/Gemfile.lock ADDED
@@ -0,0 +1,173 @@
1
+ GIT
2
+ remote: https://github.com/ddoherty03/rubocop-ddoherty.git
3
+ revision: 6b28e9614f18f94b684179c488acca8ac6cabdc6
4
+ branch: master
5
+ specs:
6
+ rubocop-ddoherty (0.1.2)
7
+ rubocop-performance (~> 1.0)
8
+ rubocop-rake (>= 0.7)
9
+ rubocop-rspec (~> 3.0)
10
+ rubocop-shopify (~> 2.0)
11
+
12
+ PATH
13
+ remote: .
14
+ specs:
15
+ fat_core (7.4.0)
16
+ activesupport
17
+ ostruct
18
+ stringio (>= 3.1.2)
19
+
20
+ GEM
21
+ remote: https://rubygems.org/
22
+ specs:
23
+ activesupport (8.1.1)
24
+ base64
25
+ bigdecimal
26
+ concurrent-ruby (~> 1.0, >= 1.3.1)
27
+ connection_pool (>= 2.2.5)
28
+ drb
29
+ i18n (>= 1.6, < 2)
30
+ json
31
+ logger (>= 1.4.2)
32
+ minitest (>= 5.1)
33
+ securerandom (>= 0.3)
34
+ tzinfo (~> 2.0, >= 2.0.5)
35
+ uri (>= 0.13.1)
36
+ ast (2.4.3)
37
+ base64 (0.3.0)
38
+ bigdecimal (4.0.1)
39
+ coderay (1.1.3)
40
+ concurrent-ruby (1.3.6)
41
+ connection_pool (3.0.2)
42
+ date (3.5.1)
43
+ debug (1.11.1)
44
+ irb (~> 1.10)
45
+ reline (>= 0.3.8)
46
+ diff-lcs (1.6.2)
47
+ docile (1.4.1)
48
+ drb (2.2.3)
49
+ erb (6.0.1)
50
+ gem_docs (0.2.0)
51
+ rake
52
+ yard
53
+ i18n (1.14.8)
54
+ concurrent-ruby (~> 1.0)
55
+ io-console (0.8.2)
56
+ irb (1.16.0)
57
+ pp (>= 0.6.0)
58
+ rdoc (>= 4.0.0)
59
+ reline (>= 0.4.2)
60
+ json (2.18.0)
61
+ language_server-protocol (3.17.0.5)
62
+ lint_roller (1.1.0)
63
+ logger (1.7.0)
64
+ method_source (1.1.0)
65
+ minitest (6.0.1)
66
+ prism (~> 1.5)
67
+ ostruct (0.6.3)
68
+ parallel (1.27.0)
69
+ parser (3.3.10.0)
70
+ ast (~> 2.4.1)
71
+ racc
72
+ pp (0.6.3)
73
+ prettyprint
74
+ prettyprint (0.2.0)
75
+ prism (1.7.0)
76
+ pry (0.16.0)
77
+ coderay (~> 1.1)
78
+ method_source (~> 1.0)
79
+ reline (>= 0.6.0)
80
+ pry-doc (1.6.0)
81
+ pry (~> 0.11)
82
+ yard (~> 0.9.11)
83
+ psych (5.3.1)
84
+ date
85
+ stringio
86
+ racc (1.8.1)
87
+ rainbow (3.1.1)
88
+ rake (13.3.1)
89
+ rdoc (7.0.3)
90
+ erb
91
+ psych (>= 4.0.0)
92
+ tsort
93
+ redcarpet (3.6.1)
94
+ regexp_parser (2.11.3)
95
+ reline (0.6.3)
96
+ io-console (~> 0.5)
97
+ rspec (3.13.2)
98
+ rspec-core (~> 3.13.0)
99
+ rspec-expectations (~> 3.13.0)
100
+ rspec-mocks (~> 3.13.0)
101
+ rspec-core (3.13.6)
102
+ rspec-support (~> 3.13.0)
103
+ rspec-expectations (3.13.5)
104
+ diff-lcs (>= 1.2.0, < 2.0)
105
+ rspec-support (~> 3.13.0)
106
+ rspec-mocks (3.13.7)
107
+ diff-lcs (>= 1.2.0, < 2.0)
108
+ rspec-support (~> 3.13.0)
109
+ rspec-support (3.13.6)
110
+ rubocop (1.82.1)
111
+ json (~> 2.3)
112
+ language_server-protocol (~> 3.17.0.2)
113
+ lint_roller (~> 1.1.0)
114
+ parallel (~> 1.10)
115
+ parser (>= 3.3.0.2)
116
+ rainbow (>= 2.2.2, < 4.0)
117
+ regexp_parser (>= 2.9.3, < 3.0)
118
+ rubocop-ast (>= 1.48.0, < 2.0)
119
+ ruby-progressbar (~> 1.7)
120
+ unicode-display_width (>= 2.4.0, < 4.0)
121
+ rubocop-ast (1.48.0)
122
+ parser (>= 3.3.7.2)
123
+ prism (~> 1.4)
124
+ rubocop-performance (1.26.1)
125
+ lint_roller (~> 1.1)
126
+ rubocop (>= 1.75.0, < 2.0)
127
+ rubocop-ast (>= 1.47.1, < 2.0)
128
+ rubocop-rake (0.7.1)
129
+ lint_roller (~> 1.1)
130
+ rubocop (>= 1.72.1)
131
+ rubocop-rspec (3.8.0)
132
+ lint_roller (~> 1.1)
133
+ rubocop (~> 1.81)
134
+ rubocop-shopify (2.18.0)
135
+ rubocop (~> 1.62)
136
+ ruby-progressbar (1.13.0)
137
+ securerandom (0.4.1)
138
+ simplecov (0.22.0)
139
+ docile (~> 1.1)
140
+ simplecov-html (~> 0.11)
141
+ simplecov_json_formatter (~> 0.1)
142
+ simplecov-html (0.13.2)
143
+ simplecov_json_formatter (0.1.4)
144
+ stringio (3.2.0)
145
+ tsort (0.2.0)
146
+ tzinfo (2.0.6)
147
+ concurrent-ruby (~> 1.0)
148
+ unicode-display_width (3.2.0)
149
+ unicode-emoji (~> 4.1)
150
+ unicode-emoji (4.2.0)
151
+ uri (1.1.1)
152
+ yard (0.9.38)
153
+
154
+ PLATFORMS
155
+ ruby
156
+ x86_64-linux
157
+
158
+ DEPENDENCIES
159
+ bundler
160
+ debug (>= 1.0.0)
161
+ fat_core!
162
+ gem_docs (>= 0.2.0)
163
+ pry
164
+ pry-doc
165
+ rake
166
+ redcarpet
167
+ rspec
168
+ rubocop
169
+ rubocop-ddoherty!
170
+ simplecov
171
+
172
+ BUNDLED WITH
173
+ 2.6.7
data/README.md CHANGED
@@ -768,7 +768,7 @@ result
768
768
 
769
769
  ## String
770
770
 
771
- FatCore::String has methods for performing matching of one string with another (`#matches_with`, `#fuzzy_match`), for converting a string to title-case as might by used in the title of a book (`#entitle`), for converting a String into a useable Symbol (`#as_sym`) and vice-versa (`#as_str` also `Symbol#as_str`), for wrapping with an optional hanging indent (`#wrap`), cleaning up errant spaces (`#clean`), and computing the Damerau-Levenshtein distance between strings (`#distance`). And several others.
771
+ FatCore::String has methods for performing matching of one string with another (`#matches_with`, `#fuzzy_match`), for converting a string to title-case as might by used in the title of a book (`#entitle`), for converting a String into a useable Symbol (`#as_sym`) and vice-versa (`#as_str` also `Symbol#as_str`), for wrapping with an optional hanging indent (`#wrap`), cleaning up errant spaces (`#clean`), and computing the Levenshtein distance between strings (`#distance`). And several others.
772
772
 
773
773
 
774
774
  <a id="org63111cb"></a>
@@ -883,7 +883,7 @@ result
883
883
 
884
884
  ### Method `#distance`
885
885
 
886
- `FatCore` provides `distance` as a simple wrapper around the Damerau-Levenshtein distance function in `damerau-levenshtein` gem, using a block size of 1 and a max distance of 10.
886
+ `FatCore` provides `distance` as a simple wrapper around the Levenshtein distance function in Ruby's bundled `DidYouMean` library. Insertions, deletions, and substitutions each count as one edit; swapping adjacent letters counts as two. Distances above 10 return 11, preserving the existing ceiling. No native extension is needed.
887
887
 
888
888
  ```ruby
889
889
  result = []
data/README.org CHANGED
@@ -175,7 +175,8 @@ Array are included in the result /unless/ they also appear in any of the
175
175
  | hammers | glue | bolts | hammers |
176
176
  #+end_example
177
177
 
178
- ** BigDecimal ~#inspect~
178
+ ** BigDecimal
179
+ *** Method ~#inspect~
179
180
  ~FatCore~ provides nothing but a better ~#inspect~ method for the ~BigDecimal~
180
181
  class since the default inspect method is not very readable.
181
182
 
@@ -718,7 +719,7 @@ FatCore::String has methods for performing matching of one string with another
718
719
  might by used in the title of a book (~#entitle~), for converting a String
719
720
  into a useable Symbol (~#as_sym~) and vice-versa (~#as_str~ also
720
721
  ~Symbol#as_str~), for wrapping with an optional hanging indent (~#wrap~),
721
- cleaning up errant spaces (~#clean~), and computing the Damerau-Levenshtein
722
+ cleaning up errant spaces (~#clean~), and computing the Levenshtein
722
723
  distance between strings (~#distance~). And several others.
723
724
 
724
725
  *** Method ~#fuzzy_match~
@@ -851,8 +852,10 @@ something like "In the Heat of the Night".
851
852
 
852
853
  *** Method ~#distance~
853
854
  ~FatCore~ provides ~distance~ as a simple wrapper around the
854
- Damerau-Levenshtein distance function in ~damerau-levenshtein~ gem, using a
855
- block size of 1 and a max distance of 10.
855
+ Levenshtein distance function in Ruby's bundled ~DidYouMean~ library.
856
+ Insertions, deletions, and substitutions each count as one edit; swapping
857
+ adjacent letters counts as two. Distances above 10 return 11, preserving
858
+ the existing ceiling. No native extension is needed.
856
859
 
857
860
  #+begin_src ruby
858
861
  result = []
data/fat_core.gemspec CHANGED
@@ -27,7 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
 
29
29
  spec.add_dependency 'activesupport'
30
- spec.add_dependency 'damerau-levenshtein'
31
30
  spec.add_dependency 'ostruct'
32
31
  spec.add_dependency 'stringio', '>=3.1.2'
33
32
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'bigdecimal'
4
4
  require 'fat_core/patches'
5
- require 'damerau-levenshtein'
5
+ require 'did_you_mean/levenshtein'
6
6
  require 'active_support/core_ext/regexp'
7
7
  require_relative 'numeric'
8
8
 
@@ -215,43 +215,42 @@ module FatCore
215
215
  # @group Matching
216
216
  # :section: Matching
217
217
 
218
- # Return the Damerau-Levenshtein distance between self an another string
219
- # using a transposition block size of 1 and quitting if a max distance of 10
220
- # is reached.
218
+ # Return the Levenshtein distance between self and another string.
219
+ # Distances above 10 return 11, preserving the existing ceiling.
220
+ # Swapping adjacent characters counts as two edits.
221
221
  #
222
222
  # @param other [#to_s] string to compute self's distance from
223
223
  # @return [Integer] the distance between self and other
224
224
  def distance(other)
225
- DamerauLevenshtein.distance(self, other.to_s, 1, 10)
225
+ [DidYouMean::Levenshtein.distance(self, other.to_s), 11].min
226
226
  end
227
227
 
228
- # Return the matched portion of self, minus punctuation characters, if self
229
- # matches the string `matcher` using the following notion of matching:
228
+ # Return the matched portion of self, with fuzzy-match punctuation normalized,
229
+ # if self matches `matcher` using the following rules:
230
230
  #
231
- # 1. Remove leading and trailing whitespace in the subject and the matcher
232
- # and collapse its internal whitespace to a single space,
233
- # 2. In the subject string replace periods and commas with a space (so
234
- # they still act as word separators) but remove apostrophes, and
235
- # asterisks so the user need not remember whether they were used when
236
- # forming the matcher.
237
- # 3. In the matcher, make any period, comma, asterisk, or apostrophe
238
- # optional for the same reason.
239
- # 4. Treat internal ':stuff' or ' :stuff' in the matcher as the equivalent
240
- # of /\bstuff.*/ in a regular expression, that is, match any word
241
- # starting with stuff in self,
242
- # 5. Treat internal 'stuff: ' in the matcher as the equivalent
243
- # of /.*stuff\b/ in a regular expression, that is, match any word
244
- # ending with stuff in self,
245
- # 6. A colon with no spaces around it is treated as belonging to the
246
- # following word, requiring it to start with it, so 'some:stuff'
247
- # requires 'some' anywhere followed by a word beginning with 'stuff',
248
- # i.e., /some.*\bstuff/i,
249
- # 7. Treat leading ':' in the matcher as anchoring the match to the
250
- # beginning of the target string,
251
- # 8. Treat ending ':' in the matcher as anchoring the match to the
252
- # end of the target string,
253
- # 9. Require each component to match some part of self, and
254
- # 10. Ignore case in the match
231
+ # 1. Remove leading and trailing whitespace in the subject and matcher and
232
+ # collapse internal whitespace to a single space.
233
+ # 2. In ordinary fuzzy matching, treat periods and commas between non-space
234
+ # characters as word separators. Otherwise ignore periods and commas.
235
+ # Ignore apostrophes and asterisks, so the matcher need not reproduce
236
+ # those punctuation characters exactly.
237
+ # 3. Escape regular-expression metacharacters in an ordinary matcher. A
238
+ # matcher enclosed in '/' characters is instead treated as a regular
239
+ # expression and matched against the original, unnormalized subject.
240
+ # 4. Treat internal ':stuff' or ' :stuff' in an ordinary matcher as the
241
+ # equivalent of /\bstuff.*/, that is, match a word beginning with stuff.
242
+ # 5. Treat internal 'stuff: ' as the equivalent of /stuff\b.*/, that is,
243
+ # require stuff to end at a word boundary and then match anything after it.
244
+ # 6. A colon with no spaces around it belongs to the following component, so
245
+ # 'some:stuff' requires 'some' followed somewhere by a word beginning with
246
+ # 'stuff', i.e. /some.*\bstuff/i.
247
+ # 7. Treat a leading ':' as anchoring the match to the beginning of the
248
+ # normalized subject.
249
+ # 8. Treat a trailing ':' as anchoring the match to the end of the normalized
250
+ # subject.
251
+ # 9. Require each component of an ordinary matcher to match some part of the
252
+ # subject, in order.
253
+ # 10. Ignore case in both fuzzy and regular-expression matching.
255
254
  #
256
255
  # @example
257
256
  # "St. Luke's Hospital".fuzzy_match('st lukes') #=> 'St Lukes'
@@ -261,28 +260,23 @@ module FatCore
261
260
  # "St. Luke's Hospital".fuzzy_match('st:laks') #=> nil
262
261
  # "St. Luke's Hospital".fuzzy_match(':lukes') #=> nil
263
262
  # "St. Luke's Hospital".fuzzy_match('lukes:hospital:') #=> 'Lukes Hospital'
263
+ # "Amazon.com".fuzzy_match('Amazon.com') #=> 'Amazon com'
264
+ # "Price is $12.50".fuzzy_match('/\$12\.50/') #=> '$12.50'
264
265
  #
265
- # @param matcher [String] pattern to test against where ':' is wildcard
266
- # @return [String] the unpunctuated part of self that matched
267
- # @return [nil] if self did not match matcher
266
+ # @param matcher [String] fuzzy matcher, or a regular expression enclosed in /
267
+ # @return [String] the portion of the subject that matched
268
+ # @return [nil] if the subject did not match
268
269
  def fuzzy_match(matcher)
269
- # Make asterisks, periods, commas, and apostrophes optional
270
- matcher = matcher.clean.gsub(/[\*.,']/, '\0?')
271
- # Replace periods and commas with a space (so they are still word
272
- # separators, e.g. 'WWW.WOLFRAM' -> 'WWW WOLFRAM' and 'AMZON,INC.' ->
273
- # 'AMAZON INC') and remove asterisks and apostrophes
274
- target = gsub(/[.,]/, ' ').gsub(/[\*']/, '').clean
275
- regexp_string =
276
- if matcher.match?(/[: ]/)
277
- matcher.sub(/\A:/, "\\A").sub(/:\z/, "\\z")
278
- .gsub(/:\s+/, "\\b.*").gsub(':', ".*\\b")
279
- .gsub(/\s+/, ".*")
280
- else
281
- Regexp.escape(matcher)
282
- end
283
- regexp = /#{regexp_string}/i
270
+ if matcher.match?(%r{\A/.*/\z})
271
+ regexp = Regexp.new(matcher[1...-1], Regexp::IGNORECASE)
272
+ subject = self
273
+ else
274
+ subject = fuzzy_match_clean(self)
275
+ matcher = fuzzy_match_clean(matcher)
276
+ regexp = Regexp.new(fuzzy_match_regexp(matcher), Regexp::IGNORECASE)
277
+ end
284
278
  matched_text =
285
- if (match = regexp.match(target))
279
+ if (match = regexp.match(subject))
286
280
  match[0]
287
281
  end
288
282
  matched_text
@@ -385,6 +379,37 @@ module FatCore
385
379
 
386
380
  private
387
381
 
382
+ def fuzzy_match_regexp(matcher)
383
+ start_anchor = matcher.start_with?(':')
384
+ end_anchor = matcher.end_with?(':')
385
+ matcher = matcher.delete_prefix(':') if start_anchor
386
+ matcher = matcher.delete_suffix(':') if end_anchor
387
+ parts = matcher.split(/(:\s+|:|\s+)/)
388
+ regexp_string = parts.map { |part|
389
+ case part
390
+ when /\A:\s+\z/
391
+ "\\b.*"
392
+ when ':'
393
+ ".*\\b"
394
+ when /\A\s+\z/
395
+ '.*'
396
+ else
397
+ Regexp.escape(part)
398
+ end
399
+ }.join
400
+ regexp_string = "\\A#{regexp_string}" if start_anchor
401
+ regexp_string = "#{regexp_string}\\z" if end_anchor
402
+ regexp_string
403
+ end
404
+
405
+ def fuzzy_match_clean(string)
406
+ string
407
+ .clean
408
+ .gsub(/(?<=\S)[.,](?=\S)/, ' ')
409
+ .gsub(/[.,*']/, '')
410
+ .clean
411
+ end
412
+
388
413
  def upper?
389
414
  UPPERS.include?(self[0])
390
415
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module FatCore
4
4
  MAJOR = 7
5
- MINOR = 2
5
+ MINOR = 4
6
6
  PATCH = 0
7
7
 
8
8
  # FatCore version number
@@ -246,7 +246,7 @@ the people, for the people, shall not perish from the earth."
246
246
  end
247
247
 
248
248
  it 'computes its Levenshtein distance from another string' do
249
- expect('Something'.distance('Smoething')).to eq 1
249
+ expect('Something'.distance('Smoething')).to eq 2
250
250
  expect('Something'.distance('meSothing')).to eq 4
251
251
  expect('SomethingElse'.distance('Encyclopedia')).to eq 11
252
252
  expect('SomethingElseUnrelated'.distance('EncyclopediaBritanica'))
@@ -254,6 +254,20 @@ the people, for the people, shall not perish from the earth."
254
254
  expect('SomethingElseUnrelated'.distance('EncyclopediaBritanica')).to eq 11
255
255
  end
256
256
 
257
+ it 'handles identical, empty, Unicode, and non-string inputs' do
258
+ expect('same'.distance('same')).to eq 0
259
+ expect(''.distance('abc')).to eq 3
260
+ expect('abc'.distance('')).to eq 3
261
+ expect('café'.distance('cafe')).to eq 1
262
+ expect('123'.distance(123)).to eq 0
263
+ expect(''.distance(nil)).to eq 0
264
+ end
265
+
266
+ it 'caps distances above ten at eleven' do
267
+ expect(('a' * 10).distance('')).to eq 10
268
+ expect(('a' * 12).distance('')).to eq 11
269
+ end
270
+
257
271
  describe 'Quoting' do
258
272
  it 'quotes special TeX characters' do
259
273
  expect('$10,000'.tex_quote).to eq('\\$10,000')
@@ -270,6 +284,49 @@ the people, for the people, shall not perish from the earth."
270
284
  expect('Hello, world'.fuzzy_match('ox')).to be_falsy
271
285
  end
272
286
 
287
+ it 'matches an exact match' do
288
+ expect('Amazon.com'.fuzzy_match('Amazon.com')).to be_truthy
289
+ expect('Hello:world'.fuzzy_match('Hello:world')).to be_truthy
290
+ expect('2+2=4'.fuzzy_match('2+2=4')).to be_truthy
291
+ expect('Whodunnit?'.fuzzy_match('Whodunnit?')).to be_truthy
292
+ expect('(I like (lisp))'.fuzzy_match('(I like (lisp))')).to be_truthy
293
+ # expect('Price is $12.50'.fuzzy_match('Price is $12.50')).to be_truthy
294
+ expect('foo[bar]'.fuzzy_match('foo[bar]')).to be_truthy
295
+ expect('a^b'.fuzzy_match('a^b')).to be_truthy
296
+ expect('foo|bar'.fuzzy_match('foo|bar')).to be_truthy
297
+ expect('path\\name'.fuzzy_match('path\\name')).to be_truthy
298
+ expect('Acme Co., Inc.'.fuzzy_match('Acme Co., Inc.')).to be_truthy
299
+ expect("St. Luke's Hospital".fuzzy_match("St. Luke's Hospital")).to be_truthy
300
+ expect('E*TRADE'.fuzzy_match('E*TRADE')).to be_truthy
301
+ end
302
+
303
+ it 'fuzzy matches ordinary strings against themselves' do
304
+ [
305
+ 'Amazon.com',
306
+ 'Amazon, Inc.',
307
+ 'St. Luke\'s Hospital',
308
+ 'E*TRADE',
309
+ 'WWW.WOLFRAM.COM',
310
+ 'Smith, Jones & Brown',
311
+ 'Acme Co., Inc.',
312
+ 'Hello, world.',
313
+ ].each do |string|
314
+ expect(string.fuzzy_match(string)).to be_truthy
315
+ end
316
+ end
317
+
318
+ it 'escapes regexp special characters in ordinary matchers' do
319
+ expect('Price is $12.50'.fuzzy_match('Price is $12.50')).to be_truthy
320
+ expect('Use a+b'.fuzzy_match('Use a+b')).to be_truthy
321
+ expect('Maybe yes?'.fuzzy_match('Maybe yes?')).to be_truthy
322
+ expect('Pick (one)'.fuzzy_match('Pick (one)')).to be_truthy
323
+ end
324
+
325
+ it 'honors regexp special characters in slash-delimited matchers' do
326
+ expect('Hello world'.fuzzy_match('/^hello.*world$/')).to be_truthy
327
+ expect('Hello world'.fuzzy_match('/^world/')).to be_falsy
328
+ end
329
+
273
330
  it 'replaces periods and commas with a space so they are still word separators' do
274
331
  expect('WWW.WOLFRAM.COM'.fuzzy_match('wolf')).to be_truthy
275
332
  expect('AMAZON,INC'.fuzzy_match('amazon inc')).to be_truthy
@@ -290,10 +347,14 @@ the people, for the people, shall not perish from the earth."
290
347
  expect('Hello, what is with the world?'.fuzzy_match('at wi or')).to be_truthy
291
348
  end
292
349
 
350
+ it 'does not normalize the subject for regexp matchers' do
351
+ expect('Price is $12.50'.fuzzy_match('/\\$12\\.50/')).to eq('$12.50')
352
+ expect('Price is $12.50'.fuzzy_match('/\\$12 50/')).to be_nil
353
+ end
354
+
293
355
  it 'fuzzy matches colon-separated parts' do
294
356
  expect('Hello:world'.fuzzy_match('hel:wor')).to be_truthy
295
357
  expect('Hello:world'.fuzzy_match('hel :wor')).to be_truthy
296
- expect('Hello:world'.fuzzy_match('hel: wor')).to be_falsy
297
358
  expect('Hello:world'.fuzzy_match('hel:orld')).to be_falsy
298
359
  expect("Hello, 'world'".fuzzy_match('hel:wor')).to be_truthy
299
360
  expect('Hello "world"'.fuzzy_match('hel:world')).to be_truthy
@@ -310,6 +371,8 @@ the people, for the people, shall not perish from the earth."
310
371
  expect('Hello, what is with the world?'.fuzzy_match('llo: th: :wor')).to be_truthy
311
372
  expect('Hello:world'.fuzzy_match('llox: ')).to be_falsy
312
373
  expect('Hello, what=+&is (with) the world?'.fuzzy_match('at: ith: the')).to be_truthy
374
+ # A colon at the end of a word in the matcher should require a word-boundary there.
375
+ expect('Hello:world'.fuzzy_match('hel: wor')).to be_falsy
313
376
  end
314
377
 
315
378
  it 'requires end-anchor for ending colon' do
@@ -342,9 +405,8 @@ the people, for the people, shall not perish from the earth."
342
405
  expect('St Lukes, Inc.'.fuzzy_match('st luke inc')).to eq('St Lukes Inc')
343
406
  expect('St Lukes, Inc.'.fuzzy_match('st lukes, inc')).to eq('St Lukes Inc')
344
407
  expect('E*TRADE'.fuzzy_match('etrade')).to eq('ETRADE')
345
- # Does not recognize non-alphanumerics as start of string.
346
408
  expect('The 1 Dollar Store'.fuzzy_match('1 stor')).to be_truthy
347
- expect('The $1 Dollar Store'.fuzzy_match('$1 stor')).to be_falsy
409
+ expect('The $1 Dollar Store'.fuzzy_match('$1 stor')).to be_truthy
348
410
  end
349
411
 
350
412
  it 'performs examples in documentation' do
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fat_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 7.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel E. Doherty
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-05-29 00:00:00.000000000 Z
10
+ date: 2026-10-07 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -24,20 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: damerau-levenshtein
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
26
  - !ruby/object:Gem::Dependency
42
27
  name: ostruct
43
28
  requirement: !ruby/object:Gem::Requirement
@@ -86,8 +71,10 @@ files:
86
71
  - ".simplecov"
87
72
  - ".travis.yml"
88
73
  - ".yardopts"
74
+ - CHANGELOG.md
89
75
  - CHANGELOG.org
90
76
  - Gemfile
77
+ - Gemfile.lock
91
78
  - LICENSE.txt
92
79
  - README.md
93
80
  - README.org
@@ -124,7 +111,6 @@ licenses:
124
111
  - MIT
125
112
  metadata:
126
113
  yard.run: yri
127
- post_install_message:
128
114
  rdoc_options: []
129
115
  require_paths:
130
116
  - lib
@@ -139,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
125
  - !ruby/object:Gem::Version
140
126
  version: '0'
141
127
  requirements: []
142
- rubygems_version: 3.5.23
143
- signing_key:
128
+ rubygems_version: 3.6.2
144
129
  specification_version: 4
145
130
  summary: some useful core extensions
146
131
  test_files: []