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.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +11 -10
  3. data/.rspec +2 -2
  4. data/.travis.yml +4 -4
  5. data/CODE_OF_CONDUCT.md +13 -13
  6. data/Gemfile +4 -4
  7. data/LICENSE.txt +21 -21
  8. data/README.md +247 -757
  9. data/Rakefile +6 -6
  10. data/bblib.gemspec +34 -34
  11. data/bin/console +14 -14
  12. data/bin/setup +7 -7
  13. data/lib/array/bbarray.rb +71 -29
  14. data/lib/bblib.rb +12 -12
  15. data/lib/bblib/version.rb +3 -3
  16. data/lib/class/effortless.rb +23 -0
  17. data/lib/error/abstract.rb +3 -0
  18. data/lib/file/bbfile.rb +93 -52
  19. data/lib/hash/bbhash.rb +130 -46
  20. data/lib/hash/hash_struct.rb +24 -0
  21. data/lib/hash/tree_hash.rb +364 -0
  22. data/lib/hash_path/hash_path.rb +210 -0
  23. data/lib/hash_path/part.rb +83 -0
  24. data/lib/hash_path/path_hash.rb +84 -0
  25. data/lib/hash_path/proc.rb +93 -0
  26. data/lib/hash_path/processors.rb +239 -0
  27. data/lib/html/bbhtml.rb +2 -0
  28. data/lib/html/builder.rb +34 -0
  29. data/lib/html/tag.rb +49 -0
  30. data/lib/logging/bblogging.rb +42 -0
  31. data/lib/mixins/attrs.rb +422 -0
  32. data/lib/mixins/bbmixins.rb +7 -0
  33. data/lib/mixins/bridge.rb +17 -0
  34. data/lib/mixins/family_tree.rb +41 -0
  35. data/lib/mixins/hooks.rb +139 -0
  36. data/lib/mixins/logger.rb +31 -0
  37. data/lib/mixins/serializer.rb +71 -0
  38. data/lib/mixins/simple_init.rb +160 -0
  39. data/lib/number/bbnumber.rb +15 -7
  40. data/lib/object/bbobject.rb +46 -19
  41. data/lib/opal/bbopal.rb +0 -4
  42. data/lib/os/bbos.rb +24 -16
  43. data/lib/os/bbsys.rb +60 -43
  44. data/lib/string/bbstring.rb +165 -66
  45. data/lib/string/cases.rb +37 -29
  46. data/lib/string/fuzzy_matcher.rb +48 -50
  47. data/lib/string/matching.rb +43 -30
  48. data/lib/string/pluralization.rb +156 -0
  49. data/lib/string/regexp.rb +45 -0
  50. data/lib/string/roman.rb +17 -30
  51. data/lib/system/bbsystem.rb +42 -0
  52. data/lib/time/bbtime.rb +79 -58
  53. data/lib/time/cron.rb +174 -132
  54. data/lib/time/task_timer.rb +86 -70
  55. metadata +27 -10
  56. data/lib/gem/bbgem.rb +0 -28
  57. data/lib/hash/hash_path.rb +0 -344
  58. data/lib/hash/hash_path_proc.rb +0 -256
  59. data/lib/hash/path_hash.rb +0 -81
  60. data/lib/object/attr.rb +0 -182
  61. data/lib/object/hooks.rb +0 -69
  62. data/lib/object/lazy_class.rb +0 -73
@@ -1,27 +1,24 @@
1
1
  module BBLib
2
-
3
- def self.title_case str, first_only: true
4
- ignoreables = ['a', 'an', 'the', 'on', 'upon', 'and', 'but', 'or', 'in', 'with', 'to']
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
- if first_only
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.capitalize
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 str, first_only: false
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 str, style = :lower
35
+ def self.camel_case(str, style = :lower)
39
36
  regx = /[[:space:]]+|[^[[:alnum:]]]+/
40
- words = str.split(regx).map do |word|
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 str, delimiter = '_'
42
+ def self.delimited_case(str, delimiter = '_')
48
43
  regx = /[[:space:]]+|[^[[:alnum:]]]+|\#{delimiter}+/
49
- words = str.split(regx).join(delimiter)
44
+ str.split(regx).join(delimiter)
50
45
  end
51
46
 
52
- def self.snake_case str
47
+ def self.snake_case(str)
53
48
  BBLib.delimited_case str, '_'
54
49
  end
55
50
 
56
- def self.spinal_case str
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 str
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
- def title_case first_only: false
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 first_only: false
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 style = :lower
77
+ def camel_case(style = :lower)
77
78
  BBLib.camel_case self, style
78
79
  end
79
80
 
80
- def delimited_case delimiter = '_'
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
@@ -1,73 +1,71 @@
1
-
2
1
  module BBLib
3
-
4
- class FuzzyMatcher < LazyClass
5
- attr_float_between 0, 100, :threshold, default: 75
6
- attr_bool :case_sensitive, default: true
7
- attr_bool :remove_symbols, :move_articles, :convert_roman, default: false
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 a, b
11
- prep_strings a, b
12
- return 100.0 if @a == @b
13
- score, total_weight = 0, @algorithms.map{|alg, v| v[:weight] }.inject{ |sum, w| sum+=w }
14
- @algorithms.each do |algo, vals|
15
- next unless vals[:weight] > 0
16
- score+= @a.send(vals[:signature], @b) * vals[:weight]
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? a, b
23
- similarity(a, b) >= @threshold.to_f
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 a, b
28
- similarities(a, b).max_by{ |k, v| v}[0]
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, the hash is sorted desc by match percent.
32
- def similarities a, b, sort: false
33
- matches = Hash.new
34
- [b].flatten.each{ |m| matches[m] = self.similarity(a, m) }
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 algorithm, weight
39
- return nil unless @algorithms.include? algorithm
40
- @algorithms[algorithm][:weight] = BBLib.keep_between(weight, 0, nil)
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
- def lazy_setup
50
- @algorithms = {
51
- levenshtein: {weight: 10, signature: :levenshtein_similarity},
52
- composition: {weight: 5, signature: :composition_similarity},
53
- numeric: {weight: 0, signature: :numeric_similarity},
54
- phrase: {weight: 0, signature: :phrase_similarity}
55
- # FUTURE qwerty: {weight: 0, signature: :qwerty_similarity}
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
- def prep_strings a, b
60
- @a, @b = a.to_s.dup, b.to_s.dup
61
- methods = [
62
- @case_sensitive ? nil : :downcase,
63
- @remove_symbols ? :drop_symbols : nil,
64
- @convert_roman ? :from_roman : nil,
65
- @move_articles ? :move_articles : nil
66
- ].reject(&:nil?).each do |method|
67
- @a, @b = @a.send(method), @b.send(method)
68
- end
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
@@ -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 a, b
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], nw = i, i - 1
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 a, b
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
- return ((max - distance.to_f) / max) * 100.0
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 a, b
28
- if a.length <= b.length then t = a; a = b; b = t; end
29
- matches, temp = 0, b.dup
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 )* 100.0
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 a, b
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 a, b
55
- a, b = a.extract_numbers, b.extract_numbers
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
- for i in 0..[a.size, b.size].max-1
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 a, b
67
- a, b = a.downcase.strip, b.downcase.strip
68
- if a.length <= b.length then t = a; a = b; b = t; end
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 => ['1','2','3','4','5','6','7','8','9','0'],
71
- 2 => ['q','w','e','r','t','y','u','i','o','p'],
72
- 3 => ['a','s','d','f','g','h','j','k','l'],
73
- 4 => ['z','x','c','v','b','n','m']
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, offset = 0, 0
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 str
105
+ def levenshtein_distance(str)
93
106
  BBLib.levenshtein_distance self, str
94
107
  end
95
108
 
96
- def levenshtein_similarity str
109
+ def levenshtein_similarity(str)
97
110
  BBLib.levenshtein_similarity self, str
98
111
  end
99
112
 
100
- def composition_similarity str
113
+ def composition_similarity(str)
101
114
  BBLib.composition_similarity self, str
102
115
  end
103
116
 
104
- def phrase_similarity str
117
+ def phrase_similarity(str)
105
118
  BBLib.phrase_similarity self, str
106
119
  end
107
120
 
108
- def numeric_similarity str
121
+ def numeric_similarity(str)
109
122
  BBLib.numeric_similarity self, str
110
123
  end
111
124
 
112
- def qwerty_distance str
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