strings-inflection 0.1.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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +424 -0
- data/Rakefile +8 -0
- data/lib/strings-inflection.rb +1 -0
- data/lib/strings/inflection.rb +202 -0
- data/lib/strings/inflection/combined_noun.rb +70 -0
- data/lib/strings/inflection/configuration.rb +84 -0
- data/lib/strings/inflection/extensions.rb +33 -0
- data/lib/strings/inflection/noun.rb +70 -0
- data/lib/strings/inflection/nouns.rb +715 -0
- data/lib/strings/inflection/parser.rb +99 -0
- data/lib/strings/inflection/term.rb +78 -0
- data/lib/strings/inflection/verb.rb +54 -0
- data/lib/strings/inflection/verbs.rb +130 -0
- data/lib/strings/inflection/version.rb +7 -0
- data/strings-inflection.gemspec +32 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +34 -0
- metadata +111 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "strscan"
|
4
|
+
|
5
|
+
require_relative "noun"
|
6
|
+
|
7
|
+
module Strings
|
8
|
+
module Inflection
|
9
|
+
class Parser
|
10
|
+
# Parse a string by evaluating content inside tags
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
def self.parse(str, count)
|
14
|
+
parser = new(str, count)
|
15
|
+
parser.parse
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(str, count)
|
19
|
+
@scanner = StringScanner.new(str)
|
20
|
+
@count = count
|
21
|
+
@value = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse
|
25
|
+
while !@scanner.eos?
|
26
|
+
parse_noun || parse_verb ||
|
27
|
+
parse_count || parse_char
|
28
|
+
end
|
29
|
+
|
30
|
+
@value.join
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def parse_noun
|
37
|
+
if @scanner.scan(/\{\{N([^\}]*?):([^\}]+?)\}\}/)
|
38
|
+
option = @scanner[1].to_s.tr(" ", "").downcase
|
39
|
+
if option =~ /[^sp]/i
|
40
|
+
raise "Unknown option '#{option}' in {{N:...}} tag"
|
41
|
+
end
|
42
|
+
inflection = if option.empty?
|
43
|
+
@count == 1 ? :singular : :plural
|
44
|
+
else
|
45
|
+
option == "s" ? :singular : :plural
|
46
|
+
end
|
47
|
+
noun = Noun[@scanner[2].to_s.tr(" ", "")]
|
48
|
+
@value << noun.public_send(inflection)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def parse_verb
|
54
|
+
if @scanner.scan(/\{\{V([^\}]*?):([^\}]+?)\}\}/)
|
55
|
+
option = @scanner[1].to_s.tr(" ", "").downcase
|
56
|
+
if !option.empty?
|
57
|
+
raise "Unknown option '#{option}' in {{V:...}} tag"
|
58
|
+
end
|
59
|
+
inflection = @count == 1 ? :singular : :plural
|
60
|
+
verb = Verb[@scanner[2].to_s.tr(" ", "")]
|
61
|
+
@value << verb.public_send(inflection)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def parse_count
|
67
|
+
if @scanner.scan(/\{\{#([^\}]*?):([^\}]+?)\}\}/)
|
68
|
+
option = @scanner[1].to_s.tr(" ", "").downcase
|
69
|
+
if option =~ /[^f]/
|
70
|
+
raise "Unknown option '#{option}' in {{#:...}} tag"
|
71
|
+
end
|
72
|
+
amount = case option
|
73
|
+
when "f"
|
74
|
+
fuzzy_count(@count)
|
75
|
+
else
|
76
|
+
@count
|
77
|
+
end
|
78
|
+
@value << amount
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def parse_char
|
84
|
+
@value << @scanner.getch
|
85
|
+
end
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
def fuzzy_count(count)
|
89
|
+
if count >= 10 then "many"
|
90
|
+
elsif count >= 6 then "several"
|
91
|
+
elsif count >= 3 then "a few"
|
92
|
+
elsif count == 2 then "a couple of"
|
93
|
+
elsif count == 1 then "one"
|
94
|
+
else "no"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end # Parser
|
98
|
+
end # Inflection
|
99
|
+
end # Strings
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strings
|
4
|
+
module Inflection
|
5
|
+
class Term
|
6
|
+
# Create a term
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def self.[](word)
|
10
|
+
self.new(word)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :word
|
14
|
+
|
15
|
+
# Create a new term
|
16
|
+
#
|
17
|
+
# @param [String] word
|
18
|
+
# the word to turn into a term object
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def initialize(word)
|
22
|
+
@word = word.dup
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
# Find a matching rule and replace
|
27
|
+
#
|
28
|
+
# @return [nil, String]
|
29
|
+
# nil or replaced word
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
def find_match(rules)
|
33
|
+
regex, replacement = rules.find { |rule| !!(word =~ rule[0]) }
|
34
|
+
|
35
|
+
return if regex.nil?
|
36
|
+
|
37
|
+
word.sub(regex, replacement)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if noun is in singular form
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# Strings::Inflection::Noun.new("error").singular?
|
44
|
+
# # => true
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def singular?
|
50
|
+
return false if word.to_s.empty?
|
51
|
+
|
52
|
+
word.downcase == singular
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if noun is in plural form
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# Strings::Inflection::Noun.new("errors").plural?
|
59
|
+
# # => true
|
60
|
+
#
|
61
|
+
# @return [Boolean]
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def plural?
|
65
|
+
return false if word.to_s.empty?
|
66
|
+
|
67
|
+
word.downcase == plural
|
68
|
+
end
|
69
|
+
|
70
|
+
# A string representation of this term
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
def to_s
|
74
|
+
word.to_s
|
75
|
+
end
|
76
|
+
end # Term
|
77
|
+
end # Inflection
|
78
|
+
end # Strings
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "term"
|
4
|
+
require_relative "verbs"
|
5
|
+
|
6
|
+
module Strings
|
7
|
+
module Inflection
|
8
|
+
class Verb < Term
|
9
|
+
# Check if word is uninflected
|
10
|
+
#
|
11
|
+
# @param [String] word
|
12
|
+
# the word to check
|
13
|
+
#
|
14
|
+
# @return [Boolean]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def uninflected?
|
18
|
+
Verbs.uninflected.include?(word)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Inflect a word to its singular form
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# Strings::Inflection::Verb.new("go").singular
|
25
|
+
# # => "goes"
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
# the verb inflected to singular form
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def singular
|
32
|
+
return word if word.to_s.empty? || uninflected?
|
33
|
+
|
34
|
+
find_match(Verbs.singulars) || word
|
35
|
+
end
|
36
|
+
|
37
|
+
# Inflect a word to its plural form
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# Strings::Inflection::Verb.new("goes").plural
|
41
|
+
# # => "go"
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
# the verb inflected to plural form
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def plural
|
48
|
+
return word if word.to_s.empty? || uninflected?
|
49
|
+
|
50
|
+
find_match(Verbs.plurals) || word
|
51
|
+
end
|
52
|
+
end # Verb
|
53
|
+
end # Inflection
|
54
|
+
end # Strings
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_litearl: true
|
2
|
+
|
3
|
+
module Strings
|
4
|
+
module Inflection
|
5
|
+
module Verbs
|
6
|
+
|
7
|
+
@uninflected = [
|
8
|
+
"can",
|
9
|
+
"may",
|
10
|
+
"must",
|
11
|
+
"ought",
|
12
|
+
"shall",
|
13
|
+
"will"
|
14
|
+
]
|
15
|
+
|
16
|
+
@singular_irregular = [
|
17
|
+
[/\A(is|are)$/, "is"],
|
18
|
+
[/\A(was|were)$/, "was"],
|
19
|
+
[/\A(ha)ve$/, "\\1s"],
|
20
|
+
[/(.*go|do)$/, "\\1es"],
|
21
|
+
[/(.*demo)$/, "\\1s"],
|
22
|
+
[/\A(gas)$/i, "\\1ses"],
|
23
|
+
[/(.*precis)$/i, "\\1es"],
|
24
|
+
[/(.*stomach)$/i, "\\1s"],
|
25
|
+
]
|
26
|
+
|
27
|
+
@singular_rules = [
|
28
|
+
[/(.*[y]ch)$/i, "\\1s"],
|
29
|
+
[/(.*[cs]h)$/i, "\\1es"],
|
30
|
+
[/(.*is)$/i, "\\1ses"],
|
31
|
+
[/(.*[ua]s)$/i, "\\1es"],
|
32
|
+
[/(.*[x])$/i, "\\1es"],
|
33
|
+
[/(.*oe)$/i, "\\1s"],
|
34
|
+
[/(.*iz)$/i, "\\1zes"],
|
35
|
+
[/(.*zz)$/i, "\\1es"],
|
36
|
+
[/(.*[il]tz)$/i, "\\1es"],
|
37
|
+
[/(.*ss)$/i, "\\1es"],
|
38
|
+
[/(.*[aeiou])y$/i, "\\1ys"],
|
39
|
+
[/(.*)y$/i, "\\1ies"],
|
40
|
+
[/(.*[io]o)$/i, "\\1s"],
|
41
|
+
[/(.*o)$/i, "\\1es"],
|
42
|
+
[/(.*[^s])$/i, "\\1s"],
|
43
|
+
[/(.*)s$/i, "\\1s"],
|
44
|
+
[/(.*)$/i, "\\1s"]
|
45
|
+
]
|
46
|
+
|
47
|
+
@singulars = @singular_irregular + @singular_rules
|
48
|
+
|
49
|
+
@plural_irregular = [
|
50
|
+
[/\A(am|is)$/, "are"],
|
51
|
+
[/\Awas$/, "were"],
|
52
|
+
[/\Ahas$/, "have"],
|
53
|
+
[/(.*go|do)es$/, "\\1"],
|
54
|
+
[/\A(bellyache|ache)s$/, "\\1"],
|
55
|
+
[/\A(axe)s$/, "\\1"],
|
56
|
+
[/\A(bus)es$/, "\\1"],
|
57
|
+
[/(.*caucus)es$/i, "\\1"],
|
58
|
+
[/(.*finesse)s$/i, "\\1"],
|
59
|
+
[/(.*bias)es$/i, "\\1"],
|
60
|
+
[/(.*cache)s$/i, "\\1"],
|
61
|
+
[/(.*caddie)s$/i, "\\1"],
|
62
|
+
[/(.*caucus)es$/i, "\\1"],
|
63
|
+
[/(.*chorus)es$/i, "\\1"],
|
64
|
+
[/\A(die|hie|vie)s$/i, "\\1"],
|
65
|
+
[/\A(lie|belie|overlie|underlie)s$/i, "\\1"],
|
66
|
+
[/\A(tie|untie)s$/i, "\\1"],
|
67
|
+
[/\A(dis|gas)ses$/i, "\\1"],
|
68
|
+
[/(.*echo|veto)es$/i, "\\1"],
|
69
|
+
[/(.*douche)s$/i, "\\1"],
|
70
|
+
[/(.*focus)es$/i, "\\1"],
|
71
|
+
[/(.*precis)es$/i, "\\1"],
|
72
|
+
[/(.*quiz)zes$/i, "\\1"],
|
73
|
+
[/(.*stymie)s$/i, "\\1"],
|
74
|
+
[/(.*sk)ies$/i, "\\1y"],
|
75
|
+
[/(.*whiz)zes$/i, "\\1"]
|
76
|
+
]
|
77
|
+
|
78
|
+
@plural_rules = [
|
79
|
+
[/(.*[aeiou]y)s$/i, "\\1"],
|
80
|
+
[/(.*[aiurywnopl]se)s$/i, "\\1"],
|
81
|
+
[/(.*[cs]h)es$/i, "\\1"],
|
82
|
+
[/(.*[sx])es$/i, "\\1"],
|
83
|
+
# [/(.*oe)s$/i, "\\1"],
|
84
|
+
[/(.*zz)es$/i, "\\1"],
|
85
|
+
[/(.*[il]tz)es$/i, "\\1"],
|
86
|
+
[/(.*ue)s$/i, "\\1"],
|
87
|
+
[/(.*[gk]ie)s$/i, "\\1"],
|
88
|
+
[/(.*all)ies$/i, "\\1y"],
|
89
|
+
[/(.*)ies$/i, "\\1y"],
|
90
|
+
[/(.*y)s$/i, "\\1"],
|
91
|
+
[/(.*[thn]oe)s$/i, "\\1"],
|
92
|
+
[/(.*o)es$/i, "\\1"],
|
93
|
+
[/(.+[^s])s$/i, "\\1"],
|
94
|
+
[/(.+)s$/i, "\\1"]
|
95
|
+
]
|
96
|
+
|
97
|
+
@plurals = @plural_irregular + @plural_rules
|
98
|
+
|
99
|
+
# A set of uninflected verbs
|
100
|
+
#
|
101
|
+
# @return [Array[String]]
|
102
|
+
#
|
103
|
+
# @api private
|
104
|
+
def uninflected
|
105
|
+
@uninflected
|
106
|
+
end
|
107
|
+
module_function :uninflected
|
108
|
+
|
109
|
+
# A list of singular rules
|
110
|
+
#
|
111
|
+
# @return [Array[String]]
|
112
|
+
#
|
113
|
+
# @api private
|
114
|
+
def singulars
|
115
|
+
@singulars
|
116
|
+
end
|
117
|
+
module_function :singulars
|
118
|
+
|
119
|
+
# A list of plural rules
|
120
|
+
#
|
121
|
+
# @return [Array[String]]
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
def plurals
|
125
|
+
@plurals
|
126
|
+
end
|
127
|
+
module_function :plurals
|
128
|
+
end # Verbs
|
129
|
+
end # Inflection
|
130
|
+
end # Strings
|
@@ -0,0 +1,32 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "strings/inflection/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "strings-inflection"
|
7
|
+
spec.version = Strings::Inflection::VERSION
|
8
|
+
spec.authors = ["Piotr Murach"]
|
9
|
+
spec.email = ["me@piotrmurach.com"]
|
10
|
+
spec.summary = %q{Inflect English nouns and verbs.}
|
11
|
+
spec.description = %q{Inflect English nouns and verbs. The algorithms are based on the analysis of 7,000 most frequently used nouns and 6,000 most used verbs in English language.}
|
12
|
+
spec.homepage = "https://github.com/piotrmurach/strings-inflection"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
if spec.respond_to?(:metadata)
|
16
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/piotrmurach/strings-inflection/blob/master/CHANGELOG.md"
|
18
|
+
spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/strings-inflection"
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/piotrmurach/strings-inflection"
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.files = Dir["lib/**/*.rb"]
|
24
|
+
spec.files += Dir["tasks/*", "strings-inflection.gemspec"]
|
25
|
+
spec.files += Dir["README.md", "CHANGELOG.md", "LICENSE.txt", "Rakefile"]
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", ">= 1.5"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
end
|
data/tasks/console.rake
ADDED
data/tasks/coverage.rake
ADDED
data/tasks/spec.rake
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
desc "Run all specs"
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
8
|
+
task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
namespace :spec do
|
12
|
+
desc "Run unit specs"
|
13
|
+
RSpec::Core::RakeTask.new(:unit) do |task|
|
14
|
+
task.pattern = "spec/unit{,/*/**}/*_spec.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run integration specs"
|
18
|
+
RSpec::Core::RakeTask.new(:integration) do |task|
|
19
|
+
task.pattern = "spec/integration{,/*/**}/*_spec.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Run performance specs"
|
23
|
+
RSpec::Core::RakeTask.new(:perf) do |task|
|
24
|
+
task.pattern = "spec/perf{,/*/**}/*_spec.rb"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
rescue LoadError
|
29
|
+
%w[spec spec:unit spec:integration].each do |name|
|
30
|
+
task name do
|
31
|
+
$stderr.puts "In order to run #{name}, do `gem install rspec`"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|