bblib 0.3.0 → 0.4.1
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 +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
|