bblib 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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