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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strings
4
+ module Inflection
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Load gem inside irb console"
4
+ task :console do
5
+ require "irb"
6
+ require "irb/completion"
7
+ require File.join(__FILE__, "../../lib/strings-inflection")
8
+ ARGV.clear
9
+ IRB.start
10
+ end
11
+ task c: %w[ console ]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Measure code coverage"
4
+ task :coverage do
5
+ begin
6
+ original, ENV["COVERAGE"] = ENV["COVERAGE"], "true"
7
+ Rake::Task["spec"].invoke
8
+ ensure
9
+ ENV["COVERAGE"] = original
10
+ end
11
+ end
@@ -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