katsuyou 1.0.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,175 @@
1
+ # Katsuyou / 活用 (lit. "Conjugation")
2
+
3
+ An API for conjugating Japanese words
4
+
5
+ ## Installation
6
+
7
+ Stick this in your Gemfile and 吸う it:
8
+
9
+ ```
10
+ gem "katsuyou"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### `Katsuyou.conjugate(word, type:)`
16
+
17
+ This gem generates conjugations for ichidan ("る") and godan ("う") verbs, as
18
+ well as the two notable exceptions in suru (する) and kuru (来る). Other verbs
19
+ exist (like [nidan
20
+ verbs](https://en.wiktionary.org/wiki/Category:Japanese_nidan_verbs)), but as
21
+ they're (apparently, I'm no expert) archaisms, I didn't bother implementing
22
+ them.
23
+
24
+ ``` ruby
25
+ Katsuyou.conjugate("食べる", type: :ichidan_verb)
26
+ Katsuyou.conjugate("聞く", type: :godan_verb)
27
+ Katsuyou.conjugate("学ぶ", type: "v5b")
28
+
29
+ # Our special cases
30
+ Katsuyou.conjugate("する", type: :suru_verb)
31
+ Katsuyou.conjugate("勉強", type: :suru_verb)
32
+ Katsuyou.conjugate("来る", type: :kuru_verb)
33
+ ```
34
+
35
+ (By the way, if you're not sure what type of verb a given word is, consult a
36
+ [primer on verb
37
+ groups](https://www.tofugu.com/japanese-grammar/verb-conjugation-groups/) and a
38
+ [dictionary](https://jisho.org) to be sure)
39
+
40
+ And you'll get back a [Struct](https://ruby-doc.org/core-2.7.0/Struct.html)
41
+ subclass containing a number of conjugated forms:
42
+
43
+ ```ruby
44
+ conjugations = Katsuyou.conjugate("見せる", type: :ichidan_verb)
45
+
46
+ # #<struct Katsuyou::VerbConjugation
47
+ # conjugation_type=
48
+ # #<struct Katsuyou::ConjugationType
49
+ # code="v1",
50
+ # description="Ichidan verb",
51
+ # category=:ichidan_verb,
52
+ # supported=true>,
53
+ # present="見せる",
54
+ # present_polite="見せます",
55
+ # present_negative="見せない",
56
+ # present_negative_polite="見せません",
57
+ # past="見せた",
58
+ # past_polite="見せました",
59
+ # past_negative="見せなかった",
60
+ # past_negative_polite="見せませんでした",
61
+ # conjunctive="見せて",
62
+ # conjunctive_polite="見せまして",
63
+ # conjunctive_negative="見せなくて",
64
+ # conjunctive_negative_polite="見せませんで",
65
+ # provisional="見せれば",
66
+ # provisional_negative="見せなければ",
67
+ # volitional="見せよう",
68
+ # volitional_polite="見せましょう",
69
+ # imperative="見せろ",
70
+ # imperative_negative="見せるな",
71
+ # potential="見せられる",
72
+ # potential_polite="見せられます",
73
+ # potential_negative="見せられない",
74
+ # potential_negative_polite="見せられません",
75
+ # passive="見せられる",
76
+ # passive_polite="見せられます",
77
+ # passive_negative="見せられない",
78
+ # passive_negative_polite="見せられません",
79
+ # causative="見せさせる",
80
+ # causative_polite="見せさせます",
81
+ # causative_negative="見せさせない",
82
+ # causative_negative_polite="見せさせません",
83
+ # causative_passive="見せさせられる",
84
+ # causative_passive_polite="見せさせられます",
85
+ # causative_passive_negative="見せさせられない",
86
+ # causative_passive_negative_polite="見せさせられません">
87
+ ```
88
+
89
+ ### `Katsuyou.conjugatable?(word, type:)`
90
+
91
+ If you're not sure whether a particular word & type is supported, you can ask
92
+ first with `conjugatable?` (note that `conjugate()` will raise if a conjugation
93
+ type is unsupported or unknown)
94
+
95
+ ```
96
+ Katsuyou.conjugatable?("食べる", type: :ichidan_verb) # => true
97
+ Katsuyou.conjugatable?("買う", type: "v5u") # => true
98
+ Katsuyou.conjugatable?("食", type: :ichidan_verb) # => false, all ichidan verbs end in る
99
+ Katsuyou.conjugatable?("ぷす", type: "v9s") # => false, unknown type "v9s"
100
+ ```
101
+
102
+ ### Conjugation types
103
+
104
+ The `conjugate(text, type:)` method **requires** you to supply a `type` value.
105
+ There are two categories of values that the gem accepts.
106
+
107
+ #### General conjugation types
108
+
109
+ First, as illustarted at the outset, these _general_ conjugation types are
110
+ supported:
111
+
112
+ * `ichidan_verb` (e.g. 食べる)
113
+ * `godan_verb` (e.g. 買う)
114
+ * `kuru_verb` (e.g. 来る)
115
+ * `suru_verb` (namely, する but also nouns that can take する like 勉強)
116
+
117
+ If you provide one of the above conjugation types, the gem will attempt to
118
+ translate it to a more specific conjugation form, which are identified by the
119
+ same codes as listed under conjugate-able entries' parts of speech in
120
+ [JMDict/EDICT](http://www.edrdg.org/jmdict/edict_doc.html).
121
+
122
+ #### Specific conjugation codes
123
+
124
+ As a result of the preceding, if you're passing in values from a JMDict-based
125
+ dictionary, it probably makes more sense to pass the specific code (e.g.
126
+ `Katsuyou.conjugate("請う", type: "v5u-s")`) to ensure that you get the most
127
+ accurate results.
128
+
129
+ You can find codes the gem knows about in `Katsuyou::CONJUGATION_TYPES`.
130
+
131
+ For example to see the supported conjugation types:
132
+
133
+ ```ruby
134
+ puts Katsuyou::CONJUGATION_TYPES.select(&:supported).map { |type| "# • #{type.code}" }.join("\n")
135
+ # • v1
136
+ # • v1-s
137
+ # • v5aru
138
+ # • v5b
139
+ # • v5g
140
+ # • v5k
141
+ # • v5k-s
142
+ # • v5m
143
+ # • v5n
144
+ # • v5r
145
+ # • v5r-i
146
+ # • v5s
147
+ # • v5t
148
+ # • v5u
149
+ # • v5u-s
150
+ # • vk
151
+ # • vs
152
+ # • vs-s
153
+ # • vs-i
154
+ ```
155
+
156
+ And likewise, for the known-but-unsupported ones:
157
+
158
+ ```ruby
159
+ puts Katsuyou::CONJUGATION_TYPES.reject(&:supported).map { |type| "# • #{type.code}" }.join("\n")
160
+ # • adj-i
161
+ # • adj-na
162
+ # • adj-t
163
+ # • adv-to
164
+ # • aux
165
+ # • aux-v
166
+ # • aux-adj
167
+ # • v2a-s
168
+ # • v4h
169
+ # • v4r
170
+ # • v5uru
171
+ # • vz
172
+ # • vn
173
+ # • vr
174
+ # • vs-c
175
+ ```
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "standard/rake"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.warning = false
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: [:test, "standard:fix"]
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "katsuyou"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ require_relative "lib/katsuyou/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "katsuyou"
5
+ spec.version = Katsuyou::VERSION
6
+ spec.authors = ["Justin Searls"]
7
+ spec.email = ["searls@gmail.com"]
8
+
9
+ spec.summary = "Conjugates Japanese words"
10
+ spec.homepage = "https://github.com/searls/katsuyou"
11
+ spec.license = "GPL-3.0-only"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+
16
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+ end
@@ -0,0 +1,17 @@
1
+ require "katsuyou/version"
2
+ require_relative "katsuyou/conjugates_verb"
3
+ require_relative "katsuyou/checks_conjugability"
4
+
5
+ module Katsuyou
6
+ class Error < StandardError; end
7
+ class UnsupportedConjugationTypeError < Error; end
8
+ class InvalidConjugationTypeError < Error; end
9
+
10
+ def self.conjugate(verb, type:)
11
+ ConjugatesVerb.new.call(verb, type: type)
12
+ end
13
+
14
+ def self.conjugatable?(verb, type:)
15
+ ChecksConjugability.new.call(verb, type: type)
16
+ end
17
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "determines_type"
2
+
3
+ module Katsuyou
4
+ class ChecksConjugability
5
+ def initialize
6
+ @determines_type = DeterminesType.new
7
+ end
8
+
9
+ def call(word, type:)
10
+ return false if any_not_present?(word, type)
11
+ return false unless (conjugation_type = @determines_type.call(text: word, type: type))
12
+ return false unless conjugation_type.supported?
13
+
14
+ case conjugation_type.category
15
+ when :ichidan_verb then valid_ichidan_verb?(word, conjugation_type)
16
+ when :godan_verb then valid_godan_verb?(word, conjugation_type)
17
+ when :kuru_verb then valid_kuru_verb?(word, conjugation_type)
18
+ when :suru_verb then valid_suru_verb?(word, conjugation_type)
19
+ else false
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def valid_ichidan_verb?(word, conjugation_type)
26
+ word.end_with?("る")
27
+ end
28
+
29
+ def valid_godan_verb?(word, conjugation_type)
30
+ return false unless word.end_with?("ぶ", "ぐ", "く", "む", "ぬ", "る", "す", "つ", "う")
31
+ last_char = word[-1]
32
+ case conjugation_type.code
33
+ when "v5b" then last_char == "ぶ"
34
+ when "v5g" then last_char == "ぐ"
35
+ when "v5k" then last_char == "く"
36
+ when "v5k-s" then last_char == "く"
37
+ when "v5m" then last_char == "む"
38
+ when "v5n" then last_char == "ぬ"
39
+ when "v5r" then last_char == "る"
40
+ when "v5r-i" then last_char == "る"
41
+ when "v5s" then last_char == "す"
42
+ when "v5t" then last_char == "つ"
43
+ when "v5u" then last_char == "う"
44
+ end
45
+ end
46
+
47
+ def valid_kuru_verb?(word, conjugation_type)
48
+ word.end_with?("来る", "くる")
49
+ end
50
+
51
+ def valid_suru_verb?(word, conjugation_type)
52
+ if conjugation_type.code == "vs"
53
+ !word.end_with?("為る", "する")
54
+ else
55
+ word.end_with?("為る", "する")
56
+ end
57
+ end
58
+
59
+ def any_not_present?(*args)
60
+ args.any? { |arg|
61
+ arg.to_s.size.zero?
62
+ }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "determines_type"
2
+ require_relative "verb_conjugation"
3
+ require_relative "zips_endings"
4
+
5
+ module Katsuyou
6
+ class ConjugatesVerb
7
+ def initialize
8
+ @determines_type = DeterminesType.new
9
+ @zips_endings = ZipsEndings.new
10
+ end
11
+
12
+ def call(verb, type:)
13
+ conjugation_type = @determines_type.call(text: verb, type: type)
14
+ ensure_valid_conjugation_type!(type, conjugation_type)
15
+
16
+ VerbConjugation.new({
17
+ conjugation_type: conjugation_type
18
+ }.merge(@zips_endings.call(verb, conjugation_type)))
19
+ end
20
+
21
+ private
22
+
23
+ def ensure_valid_conjugation_type!(user_type, conjugation_type)
24
+ if conjugation_type.nil?
25
+ raise InvalidConjugationTypeError.new(
26
+ "We don't know about conjugation type '#{user_type}'"
27
+ )
28
+ elsif !conjugation_type.supported
29
+ raise UnsupportedConjugationTypeError.new(
30
+ "Conjugation type '#{conjugation_type.code}' is not yet supported"
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module Katsuyou
2
+ class ConjugationType < Struct.new(:code, :description, :category, :supported, keyword_init: true)
3
+ def initialize(supported: true, **kwargs)
4
+ @supported = supported
5
+ super
6
+ end
7
+
8
+ def supported?
9
+ @supported
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,96 @@
1
+ require_relative "conjugation_type"
2
+ require_relative "verb_ending"
3
+
4
+ module Katsuyou
5
+ CONJUGATION_TYPES = [
6
+ ConjugationType.new(code: "adj-i", description: "adjective (keiyoushi)", category: :adjective, supported: false),
7
+ ConjugationType.new(code: "adj-na", description: "adjectival nouns or quasi-adjectives (keiyodoshi)", category: :adjective, supported: false),
8
+ ConjugationType.new(code: "adj-t", description: "`taru' adjective", category: :adjective, supported: false),
9
+ ConjugationType.new(code: "adv-to", description: "adverb taking the `to' particle", category: :adverb, supported: false),
10
+ ConjugationType.new(code: "aux", description: "auxiliary", category: :auxiliary, supported: false),
11
+ ConjugationType.new(code: "aux-v", description: "auxiliary verb", category: :auxiliary, supported: false),
12
+ ConjugationType.new(code: "aux-adj", description: "auxiliary adjective", category: :auxiliary, supported: false),
13
+ ConjugationType.new(code: "v1", description: "Ichidan verb", category: :ichidan_verb, supported: true),
14
+ ConjugationType.new(code: "v1-s", description: "Ichidan verb - kureru special class", category: :ichidan_verb, supported: true),
15
+ ConjugationType.new(code: "v2a-s", description: "Nidan verb with 'u' ending (archaic)", category: :other_verb, supported: false),
16
+ ConjugationType.new(code: "v4h", description: "Yodan verb with `hu/fu' ending (archaic)", category: :other_verb, supported: false),
17
+ ConjugationType.new(code: "v4r", description: "Yodan verb with `ru' ending (archaic)", category: :other_verb, supported: false),
18
+ ConjugationType.new(code: "v5aru", description: "Godan verb - -aru special class", category: :godan_verb, supported: true),
19
+ ConjugationType.new(code: "v5b", description: "Godan verb with `bu' ending", category: :godan_verb, supported: true),
20
+ ConjugationType.new(code: "v5g", description: "Godan verb with `gu' ending", category: :godan_verb, supported: true),
21
+ ConjugationType.new(code: "v5k", description: "Godan verb with `ku' ending", category: :godan_verb, supported: true),
22
+ ConjugationType.new(code: "v5k-s", description: "Godan verb - Iku/Yuku special class", category: :godan_verb, supported: true),
23
+ ConjugationType.new(code: "v5m", description: "Godan verb with `mu' ending", category: :godan_verb, supported: true),
24
+ ConjugationType.new(code: "v5n", description: "Godan verb with `nu' ending", category: :godan_verb, supported: true),
25
+ ConjugationType.new(code: "v5r", description: "Godan verb with `ru' ending", category: :godan_verb, supported: true),
26
+ ConjugationType.new(code: "v5r-i", description: "Godan verb with `ru' ending (irregular verb)", category: :godan_verb, supported: true),
27
+ ConjugationType.new(code: "v5s", description: "Godan verb with `su' ending", category: :godan_verb, supported: true),
28
+ ConjugationType.new(code: "v5t", description: "Godan verb with `tsu' ending", category: :godan_verb, supported: true),
29
+ ConjugationType.new(code: "v5u", description: "Godan verb with `u' ending", category: :godan_verb, supported: true),
30
+ ConjugationType.new(code: "v5u-s", description: "Godan verb with `u' ending (special class)", category: :godan_verb, supported: true),
31
+ ConjugationType.new(code: "v5uru", description: "Godan verb - Uru old class verb (old form of Eru)", category: :godan_verb, supported: false),
32
+ ConjugationType.new(code: "vz", description: "Ichidan verb - zuru verb (alternative form of -jiru verbs)", category: :ichidan_verb, supported: false),
33
+ ConjugationType.new(code: "vk", description: "Kuru verb - special class", category: :kuru_verb, supported: true),
34
+ ConjugationType.new(code: "vn", description: "irregular nu verb", category: :other_verb, supported: false),
35
+ ConjugationType.new(code: "vr", description: "irregular ru verb, plain form ends with -ri", category: :other_verb, supported: false),
36
+ ConjugationType.new(code: "vs", description: "noun or participle which takes the aux. verb suru", category: :suru_verb, supported: true),
37
+ ConjugationType.new(code: "vs-c", description: "su verb - precursor to the modern suru", category: :suru_verb, supported: false),
38
+ ConjugationType.new(code: "vs-s", description: "suru verb - special class", category: :suru_verb, supported: true),
39
+ ConjugationType.new(code: "vs-i", description: "suru verb - included", category: :suru_verb, supported: true)
40
+ ]
41
+
42
+ class DeterminesType
43
+ def self.type_for(code)
44
+ CONJUGATION_TYPES.find { |type| type.code == code }
45
+ end
46
+
47
+ def call(text:, type:)
48
+ type = type.to_s
49
+ if type == "ichidan_verb"
50
+ type_for("v1")
51
+ elsif type == "godan_verb"
52
+ guess_godan_type(text)
53
+ elsif type == "kuru_verb"
54
+ type_for("vk")
55
+ elsif type == "suru_verb"
56
+ guess_suru_type(text)
57
+ else
58
+ type_for(type)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def guess_godan_type(text)
65
+ if text.end_with?("行く", "いく")
66
+ type_for("v5k-s")
67
+ elsif text.end_with?("有る", "ある")
68
+ type_for("v5r-i")
69
+ else
70
+ case text[-1]
71
+ when "ぶ" then type_for("v5b")
72
+ when "ぐ" then type_for("v5g")
73
+ when "く" then type_for("v5k")
74
+ when "む" then type_for("v5m")
75
+ when "ぬ" then type_for("v5n")
76
+ when "る" then type_for("v5r")
77
+ when "す" then type_for("v5s")
78
+ when "つ" then type_for("v5t")
79
+ when "う" then type_for("v5u")
80
+ end
81
+ end
82
+ end
83
+
84
+ def guess_suru_type(text)
85
+ if text.end_with?("為る", "する")
86
+ type_for("vs-i")
87
+ else
88
+ type_for("vs")
89
+ end
90
+ end
91
+
92
+ def type_for(code)
93
+ self.class.type_for(code)
94
+ end
95
+ end
96
+ end