fat_core 7.2.0 → 7.3.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: 3aced327c9c27abf23f852ce1db5e3347cb1b26558d9c4c337937db9773d1dde
4
+ data.tar.gz: 4cb7f01cae67351e9865476ab40f87ddf37375b1dec7a14e725633288c3b78eb
5
5
  SHA512:
6
- metadata.gz: 474f00944f264d40761579329b2665d5119bb03b2758d7f4752f42f0fab6ded67162bd26c94db88e895bb19838500355f3abd7aba879b3d01bb8047ecc9990ec
7
- data.tar.gz: 303429d1884c5e344e90c3214f9566fc3ea753a6a7eda8d1df34e317b202cd02dcce8001eb3cc7e134eb348aecf99f2e059b8015d78af0c0c097b6bc39c2fe74
6
+ metadata.gz: d90173d7c849ca52ab67e1ccb14af60e7171c16c39b5b482d3df08d6f080e8b2ebdf1f18b89122c3554186cc35d6ae572b84d06d584f3a9a01f674ef9a2f5b8a
7
+ data.tar.gz: 895eeef8c783f150bb1cb28283c1ff4e4ffdec5ab94cb6889ad1fcbe60e4475ac7326d2c45b3cd5b50b76bb518a52c1866ed70c42c525c1c8d76facab7780e32
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
@@ -225,33 +225,32 @@ module FatCore
225
225
  DamerauLevenshtein.distance(self, other.to_s, 1, 10)
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 = 3
6
6
  PATCH = 0
7
7
 
8
8
  # FatCore version number
@@ -270,6 +270,49 @@ the people, for the people, shall not perish from the earth."
270
270
  expect('Hello, world'.fuzzy_match('ox')).to be_falsy
271
271
  end
272
272
 
273
+ it 'matches an exact match' do
274
+ expect('Amazon.com'.fuzzy_match('Amazon.com')).to be_truthy
275
+ expect('Hello:world'.fuzzy_match('Hello:world')).to be_truthy
276
+ expect('2+2=4'.fuzzy_match('2+2=4')).to be_truthy
277
+ expect('Whodunnit?'.fuzzy_match('Whodunnit?')).to be_truthy
278
+ expect('(I like (lisp))'.fuzzy_match('(I like (lisp))')).to be_truthy
279
+ # expect('Price is $12.50'.fuzzy_match('Price is $12.50')).to be_truthy
280
+ expect('foo[bar]'.fuzzy_match('foo[bar]')).to be_truthy
281
+ expect('a^b'.fuzzy_match('a^b')).to be_truthy
282
+ expect('foo|bar'.fuzzy_match('foo|bar')).to be_truthy
283
+ expect('path\\name'.fuzzy_match('path\\name')).to be_truthy
284
+ expect('Acme Co., Inc.'.fuzzy_match('Acme Co., Inc.')).to be_truthy
285
+ expect("St. Luke's Hospital".fuzzy_match("St. Luke's Hospital")).to be_truthy
286
+ expect('E*TRADE'.fuzzy_match('E*TRADE')).to be_truthy
287
+ end
288
+
289
+ it 'fuzzy matches ordinary strings against themselves' do
290
+ [
291
+ 'Amazon.com',
292
+ 'Amazon, Inc.',
293
+ 'St. Luke\'s Hospital',
294
+ 'E*TRADE',
295
+ 'WWW.WOLFRAM.COM',
296
+ 'Smith, Jones & Brown',
297
+ 'Acme Co., Inc.',
298
+ 'Hello, world.',
299
+ ].each do |string|
300
+ expect(string.fuzzy_match(string)).to be_truthy
301
+ end
302
+ end
303
+
304
+ it 'escapes regexp special characters in ordinary matchers' do
305
+ expect('Price is $12.50'.fuzzy_match('Price is $12.50')).to be_truthy
306
+ expect('Use a+b'.fuzzy_match('Use a+b')).to be_truthy
307
+ expect('Maybe yes?'.fuzzy_match('Maybe yes?')).to be_truthy
308
+ expect('Pick (one)'.fuzzy_match('Pick (one)')).to be_truthy
309
+ end
310
+
311
+ it 'honors regexp special characters in slash-delimited matchers' do
312
+ expect('Hello world'.fuzzy_match('/^hello.*world$/')).to be_truthy
313
+ expect('Hello world'.fuzzy_match('/^world/')).to be_falsy
314
+ end
315
+
273
316
  it 'replaces periods and commas with a space so they are still word separators' do
274
317
  expect('WWW.WOLFRAM.COM'.fuzzy_match('wolf')).to be_truthy
275
318
  expect('AMAZON,INC'.fuzzy_match('amazon inc')).to be_truthy
@@ -290,10 +333,14 @@ the people, for the people, shall not perish from the earth."
290
333
  expect('Hello, what is with the world?'.fuzzy_match('at wi or')).to be_truthy
291
334
  end
292
335
 
336
+ it 'does not normalize the subject for regexp matchers' do
337
+ expect('Price is $12.50'.fuzzy_match('/\\$12\\.50/')).to eq('$12.50')
338
+ expect('Price is $12.50'.fuzzy_match('/\\$12 50/')).to be_nil
339
+ end
340
+
293
341
  it 'fuzzy matches colon-separated parts' do
294
342
  expect('Hello:world'.fuzzy_match('hel:wor')).to be_truthy
295
343
  expect('Hello:world'.fuzzy_match('hel :wor')).to be_truthy
296
- expect('Hello:world'.fuzzy_match('hel: wor')).to be_falsy
297
344
  expect('Hello:world'.fuzzy_match('hel:orld')).to be_falsy
298
345
  expect("Hello, 'world'".fuzzy_match('hel:wor')).to be_truthy
299
346
  expect('Hello "world"'.fuzzy_match('hel:world')).to be_truthy
@@ -310,6 +357,8 @@ the people, for the people, shall not perish from the earth."
310
357
  expect('Hello, what is with the world?'.fuzzy_match('llo: th: :wor')).to be_truthy
311
358
  expect('Hello:world'.fuzzy_match('llox: ')).to be_falsy
312
359
  expect('Hello, what=+&is (with) the world?'.fuzzy_match('at: ith: the')).to be_truthy
360
+ # A colon at the end of a word in the matcher should require a word-boundary there.
361
+ expect('Hello:world'.fuzzy_match('hel: wor')).to be_falsy
313
362
  end
314
363
 
315
364
  it 'requires end-anchor for ending colon' do
@@ -342,9 +391,8 @@ the people, for the people, shall not perish from the earth."
342
391
  expect('St Lukes, Inc.'.fuzzy_match('st luke inc')).to eq('St Lukes Inc')
343
392
  expect('St Lukes, Inc.'.fuzzy_match('st lukes, inc')).to eq('St Lukes Inc')
344
393
  expect('E*TRADE'.fuzzy_match('etrade')).to eq('ETRADE')
345
- # Does not recognize non-alphanumerics as start of string.
346
394
  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
395
+ expect('The $1 Dollar Store'.fuzzy_match('$1 stor')).to be_truthy
348
396
  end
349
397
 
350
398
  it 'performs examples in documentation' do
metadata CHANGED
@@ -1,14 +1,14 @@
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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel E. Doherty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-29 00:00:00.000000000 Z
11
+ date: 2026-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -86,6 +86,7 @@ files:
86
86
  - ".simplecov"
87
87
  - ".travis.yml"
88
88
  - ".yardopts"
89
+ - CHANGELOG.md
89
90
  - CHANGELOG.org
90
91
  - Gemfile
91
92
  - LICENSE.txt