katsuyou 1.0.0

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