bblib 0.3.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +11 -10
- data/.rspec +2 -2
- data/.travis.yml +4 -4
- data/CODE_OF_CONDUCT.md +13 -13
- data/Gemfile +4 -4
- data/LICENSE.txt +21 -21
- data/README.md +247 -757
- data/Rakefile +6 -6
- data/bblib.gemspec +34 -34
- data/bin/console +14 -14
- data/bin/setup +7 -7
- data/lib/array/bbarray.rb +71 -29
- data/lib/bblib.rb +12 -12
- data/lib/bblib/version.rb +3 -3
- data/lib/class/effortless.rb +23 -0
- data/lib/error/abstract.rb +3 -0
- data/lib/file/bbfile.rb +93 -52
- data/lib/hash/bbhash.rb +130 -46
- data/lib/hash/hash_struct.rb +24 -0
- data/lib/hash/tree_hash.rb +364 -0
- data/lib/hash_path/hash_path.rb +210 -0
- data/lib/hash_path/part.rb +83 -0
- data/lib/hash_path/path_hash.rb +84 -0
- data/lib/hash_path/proc.rb +93 -0
- data/lib/hash_path/processors.rb +239 -0
- data/lib/html/bbhtml.rb +2 -0
- data/lib/html/builder.rb +34 -0
- data/lib/html/tag.rb +49 -0
- data/lib/logging/bblogging.rb +42 -0
- data/lib/mixins/attrs.rb +422 -0
- data/lib/mixins/bbmixins.rb +7 -0
- data/lib/mixins/bridge.rb +17 -0
- data/lib/mixins/family_tree.rb +41 -0
- data/lib/mixins/hooks.rb +139 -0
- data/lib/mixins/logger.rb +31 -0
- data/lib/mixins/serializer.rb +71 -0
- data/lib/mixins/simple_init.rb +160 -0
- data/lib/number/bbnumber.rb +15 -7
- data/lib/object/bbobject.rb +46 -19
- data/lib/opal/bbopal.rb +0 -4
- data/lib/os/bbos.rb +24 -16
- data/lib/os/bbsys.rb +60 -43
- data/lib/string/bbstring.rb +165 -66
- data/lib/string/cases.rb +37 -29
- data/lib/string/fuzzy_matcher.rb +48 -50
- data/lib/string/matching.rb +43 -30
- data/lib/string/pluralization.rb +156 -0
- data/lib/string/regexp.rb +45 -0
- data/lib/string/roman.rb +17 -30
- data/lib/system/bbsystem.rb +42 -0
- data/lib/time/bbtime.rb +79 -58
- data/lib/time/cron.rb +174 -132
- data/lib/time/task_timer.rb +86 -70
- metadata +27 -10
- data/lib/gem/bbgem.rb +0 -28
- data/lib/hash/hash_path.rb +0 -344
- data/lib/hash/hash_path_proc.rb +0 -256
- data/lib/hash/path_hash.rb +0 -81
- data/lib/object/attr.rb +0 -182
- data/lib/object/hooks.rb +0 -69
- data/lib/object/lazy_class.rb +0 -73
data/lib/string/cases.rb
CHANGED
@@ -1,27 +1,24 @@
|
|
1
1
|
module BBLib
|
2
|
-
|
3
|
-
|
4
|
-
ignoreables =
|
2
|
+
def self.title_case(str, first_only: true)
|
3
|
+
str = str.to_s unless str.is_a?(String)
|
4
|
+
ignoreables = %w(a an the on upon and but or in with to)
|
5
5
|
regx = /[[:space:]]+|\-|\_|\"|\'|\(|\)|\[|\]|\{|\}|\#/
|
6
6
|
spacing = str.scan(regx).to_a
|
7
7
|
words = str.split(regx).map do |word|
|
8
8
|
if ignoreables.include?(word.downcase)
|
9
9
|
word.downcase
|
10
|
+
elsif first_only
|
11
|
+
word.to_s.slice(0,1).to_s.upcase + word.to_s[1..-1].to_s
|
10
12
|
else
|
11
|
-
|
12
|
-
word[0] = word[0].upcase
|
13
|
-
word
|
14
|
-
else
|
15
|
-
word.capitalize
|
16
|
-
end
|
13
|
+
word.capitalize
|
17
14
|
end
|
18
15
|
end
|
19
16
|
# Always cap the first word
|
20
|
-
words.first.
|
17
|
+
words[0] = words.first.to_s.slice(0,1).to_s.upcase + words.first.to_s[1..-1].to_s
|
21
18
|
words.interleave(spacing).join
|
22
19
|
end
|
23
20
|
|
24
|
-
def self.start_case
|
21
|
+
def self.start_case(str, first_only: false)
|
25
22
|
regx = /[[:space:]]+|\-|\_|\"|\'|\(|\)|\[|\]|\{|\}|\#/
|
26
23
|
spacing = str.scan(regx).to_a
|
27
24
|
words = str.split(regx).map do |word|
|
@@ -35,49 +32,53 @@ module BBLib
|
|
35
32
|
words.interleave(spacing).join
|
36
33
|
end
|
37
34
|
|
38
|
-
def self.camel_case
|
35
|
+
def self.camel_case(str, style = :lower)
|
39
36
|
regx = /[[:space:]]+|[^[[:alnum:]]]+/
|
40
|
-
words = str.split(regx).map
|
41
|
-
word.capitalize
|
42
|
-
end
|
37
|
+
words = str.split(regx).map(&:capitalize)
|
43
38
|
words[0].downcase! if style == :lower
|
44
39
|
words.join
|
45
40
|
end
|
46
41
|
|
47
|
-
def self.delimited_case
|
42
|
+
def self.delimited_case(str, delimiter = '_')
|
48
43
|
regx = /[[:space:]]+|[^[[:alnum:]]]+|\#{delimiter}+/
|
49
|
-
|
44
|
+
str.split(regx).join(delimiter)
|
50
45
|
end
|
51
46
|
|
52
|
-
def self.snake_case
|
47
|
+
def self.snake_case(str)
|
53
48
|
BBLib.delimited_case str, '_'
|
54
49
|
end
|
55
50
|
|
56
|
-
def self.
|
51
|
+
def self.method_case(str)
|
52
|
+
str.gsub(/(?<=[^^])([A-Z])/, '_\1').gsub(/\s+/, ' ').snake_case.downcase
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.class_case(str)
|
56
|
+
str.gsub(/(?<=[^^])([A-Z])/, ' \1').gsub(/\s+/, ' ').title_case.gsub(/\s+|\_/, '')
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.spinal_case(str)
|
57
60
|
BBLib.delimited_case str, '-'
|
58
61
|
end
|
59
62
|
|
60
|
-
def self.train_case
|
63
|
+
def self.train_case(str)
|
61
64
|
BBLib.spinal_case(BBLib.start_case(str))
|
62
65
|
end
|
63
|
-
|
64
66
|
end
|
65
67
|
|
66
68
|
class String
|
67
|
-
|
68
|
-
|
69
|
-
BBLib.title_case self, first_only:first_only
|
69
|
+
def title_case(first_only: false)
|
70
|
+
BBLib.title_case self, first_only: first_only
|
70
71
|
end
|
71
72
|
|
72
|
-
def start_case
|
73
|
-
BBLib.start_case self, first_only:first_only
|
73
|
+
def start_case(first_only: false)
|
74
|
+
BBLib.start_case self, first_only: first_only
|
74
75
|
end
|
75
76
|
|
76
|
-
def camel_case
|
77
|
+
def camel_case(style = :lower)
|
77
78
|
BBLib.camel_case self, style
|
78
79
|
end
|
79
80
|
|
80
|
-
def delimited_case
|
81
|
+
def delimited_case(delimiter = '_')
|
81
82
|
BBLib.delimited_case self, delimiter
|
82
83
|
end
|
83
84
|
|
@@ -85,6 +86,14 @@ class String
|
|
85
86
|
BBLib.snake_case self
|
86
87
|
end
|
87
88
|
|
89
|
+
def method_case
|
90
|
+
BBLib.method_case(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
def class_case
|
94
|
+
BBLib.class_case(self)
|
95
|
+
end
|
96
|
+
|
88
97
|
def spinal_case
|
89
98
|
BBLib.spinal_case self
|
90
99
|
end
|
@@ -92,5 +101,4 @@ class String
|
|
92
101
|
def train_case
|
93
102
|
BBLib.train_case self
|
94
103
|
end
|
95
|
-
|
96
104
|
end
|
data/lib/string/fuzzy_matcher.rb
CHANGED
@@ -1,73 +1,71 @@
|
|
1
|
-
|
2
1
|
module BBLib
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
# Used to apply multiple string comparison algorithms to strings and
|
3
|
+
# normalize them to determine similarity for words or phrases.
|
4
|
+
class FuzzyMatcher
|
5
|
+
include Effortless
|
6
|
+
attr_float_between 0, 100, :threshold, default: 75, serialize: true
|
7
|
+
attr_bool :case_sensitive, default: true, serialize: true
|
8
|
+
attr_bool :remove_symbols, :move_articles, :convert_roman, default: false, serialize: true
|
9
|
+
attr_hash :algorithms, keys: [Symbol], values: [Float, Integer]
|
8
10
|
|
9
11
|
# Calculates a percentage match between string a and string b.
|
10
|
-
def similarity
|
11
|
-
prep_strings
|
12
|
-
return 100.0 if
|
13
|
-
score
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
def similarity(string_a, string_b)
|
13
|
+
string_a, string_b = prep_strings(string_a, string_b)
|
14
|
+
return 100.0 if string_a == string_b
|
15
|
+
score = 0
|
16
|
+
total_weight = algorithms.values.inject { |sum, weight| sum + weight }
|
17
|
+
algorithms.each do |algorithm, weight|
|
18
|
+
next unless weight.positive?
|
19
|
+
score+= string_a.send("#{algorithm}_similarity", string_b) * weight
|
17
20
|
end
|
18
21
|
score / total_weight
|
19
22
|
end
|
20
23
|
|
21
24
|
# Checks to see if the match percentage between Strings a and b are equal to or greater than the threshold.
|
22
|
-
def match?
|
23
|
-
similarity(
|
25
|
+
def match?(string_a, string_b)
|
26
|
+
similarity(string_a, string_b) >= threshold.to_f
|
24
27
|
end
|
25
28
|
|
26
29
|
# Returns the best match from array b to string a based on percent.
|
27
|
-
def best_match
|
28
|
-
similarities(
|
30
|
+
def best_match(string_a, *string_b)
|
31
|
+
similarities(string_a, *string_b).max_by { |_k, v| v }[0]
|
29
32
|
end
|
30
33
|
|
31
|
-
# Returns a hash of array 'b' with the percentage match to a. If sort is true,
|
32
|
-
|
33
|
-
|
34
|
-
[
|
35
|
-
sort ? matches.sort_by{ |k, v| v }.reverse.to_h : matches
|
34
|
+
# Returns a hash of array 'b' with the percentage match to a. If sort is true,
|
35
|
+
# the hash is sorted desc by match percent.
|
36
|
+
def similarities(string_a, *string_b)
|
37
|
+
[*string_b].map { |word| [word, matches[word] = similarity(string_a, word)] }
|
36
38
|
end
|
37
39
|
|
38
|
-
def set_weight
|
39
|
-
return nil unless
|
40
|
-
|
41
|
-
end
|
42
|
-
|
43
|
-
def algorithms
|
44
|
-
@algorithms.keys
|
40
|
+
def set_weight(algorithm, weight)
|
41
|
+
return nil unless algorithms.include? algorithm
|
42
|
+
algorithms[algorithm] = BBLib.keep_between(weight, 0, nil)
|
45
43
|
end
|
46
44
|
|
47
45
|
private
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
47
|
+
def simple_setup
|
48
|
+
self.algorithms = {
|
49
|
+
levenshtein: 10,
|
50
|
+
composition: 5,
|
51
|
+
numeric: 0,
|
52
|
+
phrase: 0
|
53
|
+
}
|
54
|
+
end
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
56
|
+
def prep_strings(string_a, string_b)
|
57
|
+
string_a = string_a.to_s.dup
|
58
|
+
string_b = string_b.to_s.dup
|
59
|
+
[
|
60
|
+
case_sensitive? ? nil : :downcase,
|
61
|
+
remove_symbols? ? :drop_symbols : nil,
|
62
|
+
convert_roman? ? :from_roman : nil,
|
63
|
+
move_articles? ? :move_articles : nil
|
64
|
+
].compact.each do |method|
|
65
|
+
string_a = string_a.send(method)
|
66
|
+
string_b = string_b.send(method)
|
69
67
|
end
|
70
|
-
|
68
|
+
[string_a, string_b]
|
69
|
+
end
|
71
70
|
end
|
72
|
-
|
73
71
|
end
|
data/lib/string/matching.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
##############################################
|
2
3
|
# String Comparison Algorithms
|
3
4
|
##############################################
|
4
5
|
|
5
6
|
module BBLib
|
6
|
-
|
7
7
|
# A simple rendition of the levenshtein distance algorithm
|
8
|
-
def self.levenshtein_distance
|
8
|
+
def self.levenshtein_distance(a, b)
|
9
9
|
costs = (0..b.length).to_a
|
10
10
|
(1..a.length).each do |i|
|
11
|
-
costs[0]
|
11
|
+
costs[0] = i
|
12
|
+
nw = i - 1
|
12
13
|
(1..b.length).each do |j|
|
13
14
|
costs[j], nw = [costs[j] + 1, costs[j-1] + 1, a[i-1] == b[j-1] ? nw : nw + 1].min, costs[j]
|
14
15
|
end
|
@@ -17,27 +18,32 @@ module BBLib
|
|
17
18
|
end
|
18
19
|
|
19
20
|
# Calculates a percentage based match using the levenshtein distance algorithm
|
20
|
-
def self.levenshtein_similarity
|
21
|
+
def self.levenshtein_similarity(a, b)
|
21
22
|
distance = BBLib.levenshtein_distance a, b
|
22
23
|
max = [a.length, b.length].max.to_f
|
23
|
-
|
24
|
+
((max - distance.to_f) / max) * 100.0
|
24
25
|
end
|
25
26
|
|
26
27
|
# Calculates a percentage based match of two strings based on their character composition.
|
27
|
-
def self.composition_similarity
|
28
|
-
if a.length <= b.length
|
29
|
-
|
28
|
+
def self.composition_similarity(a, b)
|
29
|
+
if a.length <= b.length
|
30
|
+
t = a
|
31
|
+
a = b
|
32
|
+
b = t
|
33
|
+
end
|
34
|
+
matches = 0
|
35
|
+
temp = b.dup
|
30
36
|
a.chars.each do |c|
|
31
37
|
if temp.chars.include? c
|
32
38
|
matches+=1
|
33
39
|
temp = temp.sub(c, '')
|
34
40
|
end
|
35
41
|
end
|
36
|
-
(matches / [a.length, b.length].max.to_f
|
42
|
+
(matches / [a.length, b.length].max.to_f)* 100.0
|
37
43
|
end
|
38
44
|
|
39
45
|
# Calculates a percentage based match between two strings based on the similarity of word matches.
|
40
|
-
def self.phrase_similarity
|
46
|
+
def self.phrase_similarity(a, b)
|
41
47
|
temp = b.drop_symbols.split ' '
|
42
48
|
matches = 0
|
43
49
|
a.drop_symbols.split(' ').each do |w|
|
@@ -51,34 +57,41 @@ module BBLib
|
|
51
57
|
|
52
58
|
# Extracts all numbers from two strings and compares them and generates a percentage of match.
|
53
59
|
# Percentage calculations here need to be weighted better...TODO
|
54
|
-
def self.numeric_similarity
|
55
|
-
a
|
60
|
+
def self.numeric_similarity(a, b)
|
61
|
+
a = a.extract_numbers
|
62
|
+
b = b.extract_numbers
|
56
63
|
return 100.0 if a.empty? && b.empty? || a == b
|
57
64
|
matches = []
|
58
|
-
|
65
|
+
(0..[a.size, b.size].max-1).each do |i|
|
59
66
|
matches << 1.0 / ([a[i].to_f, b[i].to_f].max - [a[i].to_f, b[i].to_f].min + 1.0)
|
60
67
|
end
|
61
|
-
(matches.inject{ |sum, m| sum + m } / matches.size.to_f) * 100.0
|
68
|
+
(matches.inject { |sum, m| sum + m } / matches.size.to_f) * 100.0
|
62
69
|
end
|
63
70
|
|
64
71
|
# A simple character distance calculator that uses qwerty key positions to determine how similar two strings are.
|
65
72
|
# May be useful for typo detection.
|
66
|
-
def self.qwerty_distance
|
67
|
-
a
|
68
|
-
|
73
|
+
def self.qwerty_distance(a, b)
|
74
|
+
a = a.downcase.strip
|
75
|
+
b = b.downcase.strip
|
76
|
+
if a.length <= b.length
|
77
|
+
t = a
|
78
|
+
a = b
|
79
|
+
b = t
|
80
|
+
end
|
69
81
|
qwerty = {
|
70
|
-
1 =>
|
71
|
-
2 =>
|
72
|
-
3 =>
|
73
|
-
4 =>
|
82
|
+
1 => %w(1 2 3 4 5 6 7 8 9 0),
|
83
|
+
2 => %w(q w e r t y u i o p),
|
84
|
+
3 => %w(a s d f g h j k l),
|
85
|
+
4 => %w(z x c v b n m)
|
74
86
|
}
|
75
|
-
count
|
87
|
+
count = 0
|
88
|
+
offset = 0
|
76
89
|
a.chars.each do |c|
|
77
90
|
if b.length <= count
|
78
91
|
offset+=10
|
79
92
|
else
|
80
|
-
ai = qwerty.keys.find{ |f| qwerty[f].include? c }.to_i
|
81
|
-
bi = qwerty.keys.find{ |f| qwerty[f].include? b.chars[count] }.to_i
|
93
|
+
ai = qwerty.keys.find { |f| qwerty[f].include? c }.to_i
|
94
|
+
bi = qwerty.keys.find { |f| qwerty[f].include? b.chars[count] }.to_i
|
82
95
|
offset+= (ai - bi).abs
|
83
96
|
offset+= (qwerty[ai].index(c) - qwerty[bi].index(b.chars[count])).abs
|
84
97
|
end
|
@@ -89,27 +102,27 @@ module BBLib
|
|
89
102
|
end
|
90
103
|
|
91
104
|
class String
|
92
|
-
def levenshtein_distance
|
105
|
+
def levenshtein_distance(str)
|
93
106
|
BBLib.levenshtein_distance self, str
|
94
107
|
end
|
95
108
|
|
96
|
-
def levenshtein_similarity
|
109
|
+
def levenshtein_similarity(str)
|
97
110
|
BBLib.levenshtein_similarity self, str
|
98
111
|
end
|
99
112
|
|
100
|
-
def composition_similarity
|
113
|
+
def composition_similarity(str)
|
101
114
|
BBLib.composition_similarity self, str
|
102
115
|
end
|
103
116
|
|
104
|
-
def phrase_similarity
|
117
|
+
def phrase_similarity(str)
|
105
118
|
BBLib.phrase_similarity self, str
|
106
119
|
end
|
107
120
|
|
108
|
-
def numeric_similarity
|
121
|
+
def numeric_similarity(str)
|
109
122
|
BBLib.numeric_similarity self, str
|
110
123
|
end
|
111
124
|
|
112
|
-
def qwerty_distance
|
125
|
+
def qwerty_distance(str)
|
113
126
|
BBLib.qwerty_distance self, str
|
114
127
|
end
|
115
128
|
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module BBLib
|
4
|
+
|
5
|
+
SPECIAL_PLURALS = {
|
6
|
+
addendum: :addenda,
|
7
|
+
alga: :algae,
|
8
|
+
alumnus: :alumni,
|
9
|
+
amoeba: :amoebae,
|
10
|
+
analysis: :analyses,
|
11
|
+
antenna: :antennae,
|
12
|
+
appendix: :appendices,
|
13
|
+
auto: :autos,
|
14
|
+
axis: :axes,
|
15
|
+
bacterium: :bacteria,
|
16
|
+
barracks: :barracks,
|
17
|
+
basis: :bases,
|
18
|
+
cactus: :cacti,
|
19
|
+
calf: :calves,
|
20
|
+
crisis: :crises,
|
21
|
+
curriculum: :curricula,
|
22
|
+
datum: :data,
|
23
|
+
deer: :deer,
|
24
|
+
diagnosis: :diagnoses,
|
25
|
+
echo: :echoes,
|
26
|
+
elf: :elves,
|
27
|
+
ellipsis: :ellipses,
|
28
|
+
embargo: :embargoes,
|
29
|
+
emphasis: :emphases,
|
30
|
+
fish: :fish,
|
31
|
+
foot: :feet,
|
32
|
+
fungus: :fungi,
|
33
|
+
gallows: :gallows,
|
34
|
+
genus: :genera,
|
35
|
+
goose: :geese,
|
36
|
+
half: :halves,
|
37
|
+
hero: :heroes,
|
38
|
+
hoof: :hooves,
|
39
|
+
hypothesis: :hypotheses,
|
40
|
+
index: :indices,
|
41
|
+
kangaroo: :kangaroos,
|
42
|
+
kilo: :kilos,
|
43
|
+
knife: :knives,
|
44
|
+
larva: :larvae,
|
45
|
+
leaf: :leaves,
|
46
|
+
life: :lives,
|
47
|
+
loaf: :loaves,
|
48
|
+
louse: :lice,
|
49
|
+
man: :men,
|
50
|
+
matrix: :matrices,
|
51
|
+
means: :means,
|
52
|
+
memo: :memos,
|
53
|
+
memorandum: :memoranda,
|
54
|
+
mouse: :mice,
|
55
|
+
neurosis: :neuroses,
|
56
|
+
oasis: :oases,
|
57
|
+
offspring: :offspring,
|
58
|
+
paralysis: :paralyses,
|
59
|
+
parenthesis: :parentheses,
|
60
|
+
person: :people,
|
61
|
+
photo: :photos,
|
62
|
+
piano: :pianos,
|
63
|
+
pimento: :pimentos,
|
64
|
+
potato: :potatoes,
|
65
|
+
pro: :pros,
|
66
|
+
self: :selves,
|
67
|
+
series: :series,
|
68
|
+
sheep: :sheep,
|
69
|
+
shelf: :shelves,
|
70
|
+
solo: :solos,
|
71
|
+
soprano: :sopranos,
|
72
|
+
species: :species,
|
73
|
+
stimulus: :stimuli,
|
74
|
+
studio: :studios,
|
75
|
+
syllabus: :syllabi,
|
76
|
+
tattoo: :tattoos,
|
77
|
+
thesis: :theses,
|
78
|
+
thief: :thieves,
|
79
|
+
tomato: :tomatoes,
|
80
|
+
tooth: :teeth,
|
81
|
+
torpedo: :torpedoes,
|
82
|
+
vertebra: :vertebrae,
|
83
|
+
veto: :vetoes,
|
84
|
+
video: :videos,
|
85
|
+
wife: :wives,
|
86
|
+
wolf: :wolves,
|
87
|
+
woman: :women,
|
88
|
+
zoo: :zoos
|
89
|
+
}
|
90
|
+
|
91
|
+
def self.pluralize(string, num = 2)
|
92
|
+
full_string = string.to_s
|
93
|
+
string = string.split(/\s+/).last
|
94
|
+
sym = string.to_s.downcase.to_sym
|
95
|
+
if plural = SPECIAL_PLURALS[sym]
|
96
|
+
result = num == 1 ? string : plural
|
97
|
+
else
|
98
|
+
if string.end_with?(*%w{ch z s x o})
|
99
|
+
result = num == 1 ? string : (string + 'es')
|
100
|
+
elsif string =~ /[^aeiou]y$/i
|
101
|
+
result = num == 1 ? string : string.sub(/y$/i, 'ies')
|
102
|
+
else
|
103
|
+
result = num == 1 ? string : (string + 's')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
full_string.sub(/#{Regexp.escape(string)}$/, copy_capitalization(string, result).to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.singularize(string)
|
110
|
+
full_string = string.to_s
|
111
|
+
string = string.split(/\s+/).last
|
112
|
+
sym = string.to_s.downcase.to_sym
|
113
|
+
sym = string.to_s.downcase.to_sym
|
114
|
+
if singular = SPECIAL_PLURALS.find { |k, v| v == sym }
|
115
|
+
result = singular.first
|
116
|
+
elsif string.downcase.end_with?(*%w{oes ches zes ses xes})
|
117
|
+
result = string.sub(/es$/i, '')
|
118
|
+
elsif string =~ /ies$/i
|
119
|
+
result = string.sub(/ies$/i, 'y')
|
120
|
+
elsif string =~ /s$/i && !(string =~ /s{2}$/i)
|
121
|
+
result = string.sub(/s$/i, '')
|
122
|
+
else
|
123
|
+
result = string
|
124
|
+
end
|
125
|
+
full_string.sub(/#{Regexp.escape(string)}$/, copy_capitalization(string, result).to_s)
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.custom_pluralize(num, base, plural = 's', singular = nil)
|
129
|
+
num == 1 ? "#{base}#{singular}" : "#{base}#{plural}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.plural_string(num, string)
|
133
|
+
"#{num} #{pluralize(string, num)}"
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
class String
|
139
|
+
def pluralize(num = 2)
|
140
|
+
BBLib.pluralize(self, num)
|
141
|
+
end
|
142
|
+
|
143
|
+
def singularize
|
144
|
+
BBLib.singularize(self)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class Symbol
|
149
|
+
def pluralize(num = 2)
|
150
|
+
BBLib.pluralize(self.to_s, num).to_sym
|
151
|
+
end
|
152
|
+
|
153
|
+
def singularize
|
154
|
+
BBLib.singularize(self.to_s).to_sym
|
155
|
+
end
|
156
|
+
end
|