fat_core 7.1.3 → 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 +4 -4
- data/.gitignore +1 -1
- data/.yardopts +3 -4
- data/CHANGELOG.md +97 -0
- data/CHANGELOG.org +5 -0
- data/Gemfile +1 -0
- data/README.md +1199 -0
- data/README.org +266 -225
- data/Rakefile +3 -7
- data/lib/fat_core/all.rb +1 -0
- data/lib/fat_core/string.rb +104 -58
- data/lib/fat_core/version.rb +2 -2
- data/lib/fat_core.rb +12 -2
- data/spec/lib/string_spec.rb +60 -3
- metadata +4 -2
data/Rakefile
CHANGED
|
@@ -24,16 +24,12 @@ RSpec::Core::RakeTask.new(:spec, :tag) do |t|
|
|
|
24
24
|
t.rspec_opts = '--tag ~online -f d'
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
require "gem_docs"
|
|
28
|
+
GemDocs.install
|
|
29
|
+
|
|
27
30
|
########################################################################
|
|
28
31
|
# Rubocop tasks
|
|
29
32
|
########################################################################
|
|
30
|
-
# Option A (recommended): Keep using Bundler and run rubocop via `bundle exec`.
|
|
31
|
-
# This wrapper task ensures the rubocop run uses the gems from your Gemfile,
|
|
32
|
-
# even when you invoke `rake rubocop` (no need to remember `bundle exec rake`).
|
|
33
|
-
#
|
|
34
|
-
# You can pass extra RuboCop CLI flags with the RUBOCOP_OPTS environment variable:
|
|
35
|
-
# RUBOCOP_OPTS="--format simple" rake rubocop
|
|
36
|
-
|
|
37
33
|
desc "Run rubocop under `bundle exec`"
|
|
38
34
|
task :rubocop do
|
|
39
35
|
opts = (ENV['RUBOCOP_OPTS'] || '').split
|
data/lib/fat_core/all.rb
CHANGED
data/lib/fat_core/string.rb
CHANGED
|
@@ -8,6 +8,9 @@ require_relative 'numeric'
|
|
|
8
8
|
|
|
9
9
|
module FatCore
|
|
10
10
|
module String
|
|
11
|
+
UPPERS = ('A'..'Z').to_a
|
|
12
|
+
REGEXP_META_CHARACTERS = "\\$()*+.<>?[]^{|}".chars.freeze
|
|
13
|
+
|
|
11
14
|
# @group Transforming
|
|
12
15
|
# :section: Transforming
|
|
13
16
|
|
|
@@ -93,22 +96,29 @@ module FatCore
|
|
|
93
96
|
r.gsub('XzXzXcbXzXzX', '\\}')
|
|
94
97
|
end
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
# Rather than truncate a String to make it fit a given length, this method
|
|
100
|
+
# removes characters from the middle of the string to make it fit the
|
|
101
|
+
# given size. This is often preferable to truncating at the end or
|
|
102
|
+
# beginning of a String because the most important information is often at
|
|
103
|
+
# the start or end of a String. By default the missing middle is
|
|
104
|
+
# indicated by a single '~' character, but you can set it to any string,
|
|
105
|
+
# even the empty string with the `ellipsis:` parameter.
|
|
106
|
+
def gut(max_size, ellipsis: '~', squeeze: nil)
|
|
107
|
+
return self if size <= max_size
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
s =
|
|
110
|
+
if squeeze
|
|
111
|
+
tr(squeeze, '')
|
|
112
|
+
else
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
chars_to_cut = (s.size - max_size) + ellipsis.size
|
|
116
|
+
chars_to_keep = s.size - chars_to_cut
|
|
117
|
+
start_chars = chars_to_keep / 2 + (chars_to_keep.odd? ? 1 : 0)
|
|
118
|
+
end_chars = chars_to_keep - start_chars
|
|
119
|
+
s[0..start_chars - 1] + ellipsis + s[-end_chars..-1]
|
|
108
120
|
end
|
|
109
121
|
|
|
110
|
-
public
|
|
111
|
-
|
|
112
122
|
# Return self capitalized according to the conventions for capitalizing
|
|
113
123
|
# titles of books or articles. Tries to follow the rules of the University
|
|
114
124
|
# of Chicago's *A Manual of Style*, Section 7.123, except to the extent that
|
|
@@ -215,33 +225,32 @@ module FatCore
|
|
|
215
225
|
DamerauLevenshtein.distance(self, other.to_s, 1, 10)
|
|
216
226
|
end
|
|
217
227
|
|
|
218
|
-
# Return the matched portion of self,
|
|
219
|
-
#
|
|
228
|
+
# Return the matched portion of self, with fuzzy-match punctuation normalized,
|
|
229
|
+
# if self matches `matcher` using the following rules:
|
|
220
230
|
#
|
|
221
|
-
# 1. Remove leading and trailing whitespace in the subject and
|
|
222
|
-
#
|
|
223
|
-
# 2. In
|
|
224
|
-
#
|
|
225
|
-
# asterisks so the
|
|
226
|
-
#
|
|
227
|
-
# 3.
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
# 5. Treat internal 'stuff: '
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
# 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.
|
|
245
254
|
#
|
|
246
255
|
# @example
|
|
247
256
|
# "St. Luke's Hospital".fuzzy_match('st lukes') #=> 'St Lukes'
|
|
@@ -251,28 +260,23 @@ module FatCore
|
|
|
251
260
|
# "St. Luke's Hospital".fuzzy_match('st:laks') #=> nil
|
|
252
261
|
# "St. Luke's Hospital".fuzzy_match(':lukes') #=> nil
|
|
253
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'
|
|
254
265
|
#
|
|
255
|
-
# @param matcher [String]
|
|
256
|
-
# @return [String] the
|
|
257
|
-
# @return [nil] if
|
|
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
|
|
258
269
|
def fuzzy_match(matcher)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
matcher.sub(/\A:/, "\\A").sub(/:\z/, "\\z")
|
|
268
|
-
.gsub(/:\s+/, "\\b.*").gsub(':', ".*\\b")
|
|
269
|
-
.gsub(/\s+/, ".*")
|
|
270
|
-
else
|
|
271
|
-
Regexp.escape(matcher)
|
|
272
|
-
end
|
|
273
|
-
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
|
|
274
278
|
matched_text =
|
|
275
|
-
if (match = regexp.match(
|
|
279
|
+
if (match = regexp.match(subject))
|
|
276
280
|
match[0]
|
|
277
281
|
end
|
|
278
282
|
matched_text
|
|
@@ -373,6 +377,48 @@ module FatCore
|
|
|
373
377
|
to_f.commas(places)
|
|
374
378
|
end
|
|
375
379
|
|
|
380
|
+
private
|
|
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
|
+
|
|
413
|
+
def upper?
|
|
414
|
+
UPPERS.include?(self[0])
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Return true if all the letters in self are upper case
|
|
418
|
+
def all_upper?
|
|
419
|
+
tr('^A-Za-z', '').split('').all? { |c| ('A'..'Z').to_a.include? c }
|
|
420
|
+
end
|
|
421
|
+
|
|
376
422
|
module ClassMethods
|
|
377
423
|
# @group Generating
|
|
378
424
|
# :section: Generating
|
data/lib/fat_core/version.rb
CHANGED
data/lib/fat_core.rb
CHANGED
|
@@ -3,5 +3,15 @@
|
|
|
3
3
|
require 'active_support/core_ext/object/blank'
|
|
4
4
|
require 'active_support/core_ext/object/deep_dup'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# Gem Overview (extracted from README.org by gem_docs)
|
|
7
|
+
#
|
|
8
|
+
# * Introduction
|
|
9
|
+
# ~fat-core~ is somewhat of a grab bag of core class extensions that I have
|
|
10
|
+
# found useful across several projects. It's higgeldy-piggeldy nature reflects
|
|
11
|
+
# the fact that none of them are important enough to deserve a gem of their own,
|
|
12
|
+
# but nonetheless need to be collected in one place to reduce redundancy across
|
|
13
|
+
# projects and provide a focused place to develop and test them.
|
|
14
|
+
module FatCore
|
|
15
|
+
require 'fat_core/version'
|
|
16
|
+
require 'fat_core/patches'
|
|
17
|
+
end
|
data/spec/lib/string_spec.rb
CHANGED
|
@@ -158,6 +158,15 @@ the people, for the people, shall not perish from the earth."
|
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
it 'guts a string in the middle' do
|
|
162
|
+
expect('hello'.gut(10)).to eq('hello')
|
|
163
|
+
expect('helloworld'.gut(6)).to eq('hel~ld')
|
|
164
|
+
expect('Class A Common Stock'.gut(15)).to eq("Class A~n Stock")
|
|
165
|
+
expect('Class A Common Stock'.gut(15, ellipsis: '...')).to eq("Class ... Stock")
|
|
166
|
+
expect('Class A Common Stock'.gut(15, ellipsis: '')).to eq("Class A n Stock")
|
|
167
|
+
expect('Class A Common Stock'.gut(16, ellipsis: '', squeeze: ' ')).to eq("ClassAComonStock")
|
|
168
|
+
end
|
|
169
|
+
|
|
161
170
|
it 'converts a string to a regular expression' do
|
|
162
171
|
# Ignores case by default
|
|
163
172
|
re = "/hello((\s+)(world))?/".as_regexp
|
|
@@ -261,6 +270,49 @@ the people, for the people, shall not perish from the earth."
|
|
|
261
270
|
expect('Hello, world'.fuzzy_match('ox')).to be_falsy
|
|
262
271
|
end
|
|
263
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
|
+
|
|
264
316
|
it 'replaces periods and commas with a space so they are still word separators' do
|
|
265
317
|
expect('WWW.WOLFRAM.COM'.fuzzy_match('wolf')).to be_truthy
|
|
266
318
|
expect('AMAZON,INC'.fuzzy_match('amazon inc')).to be_truthy
|
|
@@ -281,10 +333,14 @@ the people, for the people, shall not perish from the earth."
|
|
|
281
333
|
expect('Hello, what is with the world?'.fuzzy_match('at wi or')).to be_truthy
|
|
282
334
|
end
|
|
283
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
|
+
|
|
284
341
|
it 'fuzzy matches colon-separated parts' do
|
|
285
342
|
expect('Hello:world'.fuzzy_match('hel:wor')).to be_truthy
|
|
286
343
|
expect('Hello:world'.fuzzy_match('hel :wor')).to be_truthy
|
|
287
|
-
expect('Hello:world'.fuzzy_match('hel: wor')).to be_falsy
|
|
288
344
|
expect('Hello:world'.fuzzy_match('hel:orld')).to be_falsy
|
|
289
345
|
expect("Hello, 'world'".fuzzy_match('hel:wor')).to be_truthy
|
|
290
346
|
expect('Hello "world"'.fuzzy_match('hel:world')).to be_truthy
|
|
@@ -301,6 +357,8 @@ the people, for the people, shall not perish from the earth."
|
|
|
301
357
|
expect('Hello, what is with the world?'.fuzzy_match('llo: th: :wor')).to be_truthy
|
|
302
358
|
expect('Hello:world'.fuzzy_match('llox: ')).to be_falsy
|
|
303
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
|
|
304
362
|
end
|
|
305
363
|
|
|
306
364
|
it 'requires end-anchor for ending colon' do
|
|
@@ -333,9 +391,8 @@ the people, for the people, shall not perish from the earth."
|
|
|
333
391
|
expect('St Lukes, Inc.'.fuzzy_match('st luke inc')).to eq('St Lukes Inc')
|
|
334
392
|
expect('St Lukes, Inc.'.fuzzy_match('st lukes, inc')).to eq('St Lukes Inc')
|
|
335
393
|
expect('E*TRADE'.fuzzy_match('etrade')).to eq('ETRADE')
|
|
336
|
-
# Does not recognize non-alphanumerics as start of string.
|
|
337
394
|
expect('The 1 Dollar Store'.fuzzy_match('1 stor')).to be_truthy
|
|
338
|
-
expect('The $1 Dollar Store'.fuzzy_match('$1 stor')).to
|
|
395
|
+
expect('The $1 Dollar Store'.fuzzy_match('$1 stor')).to be_truthy
|
|
339
396
|
end
|
|
340
397
|
|
|
341
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.
|
|
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:
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -86,9 +86,11 @@ 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
|
|
93
|
+
- README.md
|
|
92
94
|
- README.org
|
|
93
95
|
- Rakefile
|
|
94
96
|
- TODO.org
|