daidai 0.2.5 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71d9328e95b0ca6e9597020c5c60128f5a699a5bb68c1bb9e24881bf2ebd1276
4
- data.tar.gz: 7e34ce14c5054479b186423bb1623dd519af4f05888a8fd63e5c1d3f12965e75
3
+ metadata.gz: 38b5d131989c0801d446ed50e9ab3399fef4813bb017a62304a0fed9629922a1
4
+ data.tar.gz: c5eafa61481b4291fc945ce43354a221c6093a9dd44a68d2535e6a2d6b2c8ba0
5
5
  SHA512:
6
- metadata.gz: 2a8d2ac93b3bfe2f44ba0c8e38c3a070703b76a1e197d001b7646cb0b439dd12e0fb440f208f33ab9326b438667c3c103ecd5a350a8b7820f0369f0341e88ba6
7
- data.tar.gz: bb4b1505f2646ea62d4fbcf7749f149ca0ac3d378c34fa7950194d20ac0759a13aec3840ec9aff5b37695b9c12b4951375cbc5d828de23906cd51093bb8402a2
6
+ metadata.gz: f307a1904436405e4a06298a583fe7638b8578d57d68a7e4535457437417f8713b7c1ea71b4d54e5a6c0806c801f86719e494a42ba84dc71871db4857b29bf04
7
+ data.tar.gz: 45cc7171d85e261559e9074945274bd6d9e16ac3a435f6a5a0ea4b77b5248c2272545547b4ebce19d05974528a6cd67f6c7b7f09ec94a9864aa5a5eef5c2786d
data/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-09-07
10
+
11
+ ### Added
12
+
13
+ - Deinflection candidates expose `word_classes` and `matches_pos?` for JMdict
14
+ part-of-speech validation. An ichidan candidate such as かかる from かかなかった
15
+ can now be rejected when the dictionary entry is godan.
16
+ - Suru-noun bases such as 勉強 from 勉強しました, nominal predicates such as
17
+ 静か from 静かだった, and standalone copula forms.
18
+ - Forward conjugation of だ with the JMdict `cop` tag.
19
+ - Automatic copula resolution for だ, です, だった, and でした.
20
+
21
+ ### Fixed
22
+
23
+ - Automatic POS resolution preserves the irregular classes of 問う, いい,
24
+ くださる, なさる, and いらっしゃる. Automatic past forms now include 問うた
25
+ and よかった rather than 問った and いかった.
26
+ - Polite representative forms such as しましたり now deinflect to する.
27
+ - Negative polite imperatives such as 死になさるな now deinflect to their base.
28
+ - Irregular subclasses validate their final transformation against the forward
29
+ paradigm, so 行いた cannot match 行く tagged `v5k-s`.
30
+
9
31
  ## [0.2.5] - 2026-08-03
10
32
 
11
33
  ### Removed
data/README.md CHANGED
@@ -190,7 +190,10 @@ d = Daidai.deinflect("食べてる").find { |x| x.term == "食べる" }
190
190
  d.term # => "食べる" (the candidate dictionary form)
191
191
  d.inflections # => ["-いる", "-て"] (rule names, surface to dictionary)
192
192
  d.labels # => ["progressive", "te-form"] (friendly English names)
193
- d.dictionary_form? # => true (chain lands on a known dictionary form)
193
+ d.dictionary_form? # => true (chain reaches a dictionary-form class)
194
+ d.word_classes # => ["v1"]
195
+ d.matches_pos?(["v1", "vt"]) # => true
196
+ d.base_surface # => "食べて" (surface before the final rule)
194
197
  d.to_s # => "食べる [-いる, -て]"
195
198
  ```
196
199
 
@@ -202,11 +205,20 @@ labels downstream if your app is multilingual.
202
205
  Deinflection is rule-based and **dictionary-free**, so it returns *every* base
203
206
  form the rules can reach, many of which are not real words (食べてる also yields
204
207
  食べつ as a hypothetical potential). It is meant to feed a dictionary lookup: keep
205
- the candidates whose `term` is a real entry. If you have no dictionary, filtering
206
- to `dictionary_form?` candidates keeps the plausible lemmas.
208
+ the candidates whose `term` is a real entry and whose `matches_pos?` accepts that
209
+ entry's JMdict part-of-speech codes. `dictionary_form?` checks the grammatical
210
+ shape, not whether the word exists. For example, かかなかった produces かかる as
211
+ an ichidan candidate; it must not match a godan かかる entry.
212
+
213
+ Suru nouns and nominal predicates are dictionary candidates too: 勉強しました
214
+ can reach 勉強 with class `vs-noun`, and 静かだった can reach 静か with classes
215
+ `adj-na` and `n`. `matches_pos?` maps these to JMdict tags. For irregular
216
+ subclasses it also checks supported final transformations against the forward
217
+ paradigm. The check retains the intermediate `base_surface`, so a valid
218
+ contraction around an irregular verb is not mistaken for a regularized form.
207
219
 
208
220
  This pairs naturally with a dictionary like JMdict: deinflect the query, look up
209
- each candidate `term`, and you have the lemma, its part of speech, and the named
221
+ each candidate `term`, validate its part of speech, and you have the lemma and the named
210
222
  inflection, without a morphological analyzer. (For a single authoritative lemma
211
223
  + reading from arbitrary text, including full sentences, the kabosu path above is
212
224
  still the tool; the two are complementary.)
@@ -81,7 +81,9 @@ module Daidai
81
81
  def inflect(strat, text, row)
82
82
  return nil if text.nil? || text.empty?
83
83
 
84
- if strat[:suffix]
84
+ if strat[:copula]
85
+ row.okuri.to_s
86
+ elsif strat[:suffix]
85
87
  # Copula forms are whole suffixes appended to the citation form
86
88
  # (静か → 静かだ / 静かではない); no stem stripping or euphony.
87
89
  text + row.okuri.to_s
@@ -113,6 +115,8 @@ module Daidai
113
115
  { pos_id: Tables.pos_ids.fetch(code), kind: kind }
114
116
  elsif code == "adj-na"
115
117
  { pos_id: COPULA_POS, kind: :na_adjective, suffix: true }
118
+ elsif code == "cop"
119
+ { pos_id: COPULA_POS, kind: :copula, copula: true }
116
120
  elsif code == "vs"
117
121
  { pos_id: SURU_POS, kind: :suru, append: "する" }
118
122
  end
@@ -6,14 +6,39 @@ module Daidai
6
6
  # A single deinflection candidate: a base-form `term` reached from the input by
7
7
  # applying `inflections` (transform names, ordered from the surface form inward
8
8
  # to the dictionary form). `dictionary_form?` is true when the rule chain lands
9
- # on a recognised dictionary form (a likely real lemma), useful for callers
10
- # without their own dictionary to look the term up in.
9
+ # on a dictionary-form grammatical class. This does not establish that the
10
+ # term exists; callers must check the spelling and part of speech in a dictionary.
11
11
  #
12
12
  # Daidai.deinflect("食べてる") # candidate base forms, each with named inflections;
13
13
  # # one is #<Daidai::Deinflection 食べる [-いる, -て]>
14
- Deinflection = Struct.new(:term, :inflections, :dictionary_form, keyword_init: true) do
14
+ Deinflection = Struct.new(:term, :inflections, :dictionary_form, :word_classes, :base_surface, keyword_init: true) do
15
15
  def dictionary_form? = dictionary_form
16
16
 
17
+ def matches_pos?(pos_codes)
18
+ Array(pos_codes).any? do |code|
19
+ next true if code.to_s == "vs" && Array(word_classes).include?("vs-noun")
20
+
21
+ word_class = case code.to_s
22
+ when /\Av1(?:-|\z)/ then "v1"
23
+ when /\Av5/ then "v5"
24
+ when /\Avs(?:-|\z)/ then "vs"
25
+ when "adj-ix" then "adj-i"
26
+ when "cop" then "copula"
27
+ else code.to_s
28
+ end
29
+ Array(word_classes).include?(word_class) && compatible_irregular_form?(code.to_s)
30
+ end
31
+ end
32
+
33
+ def compatible_irregular_form?(code)
34
+ return true unless base_surface && Deinflector::IRREGULAR_POS.include?(code) &&
35
+ Deinflector::GENERATED_RULES.include?(inflections.last)
36
+
37
+ word = Daidai.conjugate(term, code)
38
+ word&.any? { |form| form.text == base_surface }
39
+ end
40
+ private :compatible_irregular_form?
41
+
17
42
  # The inflections as friendly English labels for display (e.g. "-いる" =>
18
43
  # "progressive", "-て" => "te-form"), via Deinflector.label. Localise these
19
44
  # downstream (i18n) if your app is multilingual.
@@ -38,6 +63,8 @@ module Daidai
38
63
  # offline, string-rule deinflection.
39
64
  module Deinflector
40
65
  DATA_FILE = File.expand_path("resources/japanese-transforms.json", __dir__)
66
+ IRREGULAR_POS = %w[v5k-s v5u-s v5r-i v5aru v1-s adj-ix vs-s].freeze
67
+ GENERATED_RULES = %w[-た -て -ます negative -ば -たら -たり potential passive causative volitional imperative].freeze
41
68
 
42
69
  # Friendly English labels for the deinflection rule names #deinflect emits.
43
70
  # The underlying names (ported from Yomitan) are terse and sometimes symbolic
@@ -47,6 +74,7 @@ module Daidai
47
74
  # these rather than maintain their own map. Keyed by the rule name; see
48
75
  # Deinflector.label for the lookup (which falls back to the name itself).
49
76
  LABELS = {
77
+ "suru" => "suru verb", "copula" => "copula", "-なら" => "conditional (-nara)",
50
78
  "-いる" => "progressive", "-て" => "te-form", "-た" => "past",
51
79
  "-ます" => "polite", "negative" => "negative", "passive" => "passive",
52
80
  "potential" => "potential", "potential or passive" => "potential / passive",
@@ -108,9 +136,9 @@ module Daidai
108
136
  # `term`; callers without one can keep only `dictionary_form?` candidates.
109
137
  def deinflect(text)
110
138
  transform(text)
111
- .reject { |t| t.trace.empty? }
139
+ .reject { |t| t.trace.empty? || t.text.empty? }
112
140
  .map { |t| to_deinflection(t) }
113
- .uniq { |d| [ d.term, d.inflections ] }
141
+ .uniq { |d| [ d.term, d.inflections, d.word_classes, d.base_surface ] }
114
142
  end
115
143
 
116
144
  # Friendly English label for a deinflection rule name (the strings in a
@@ -159,7 +187,11 @@ module Daidai
159
187
  # trace is newest-first (innermost rule first); reverse so the names read
160
188
  # from the surface form inward to the dictionary form.
161
189
  inflections: transformed.trace.reverse.map { |frame| transforms_by_id[frame[:transform]].name },
162
- dictionary_form: transformed.conditions.anybits?(dictionary_mask)
190
+ dictionary_form: transformed.conditions.anybits?(dictionary_mask),
191
+ base_surface: transformed.trace.first&.fetch(:text),
192
+ word_classes: data["conditions"].filter_map do |name, condition|
193
+ name if condition["isDictionaryForm"] && transformed.conditions.anybits?(condition_flags.fetch(name))
194
+ end.freeze
163
195
  )
164
196
  end
165
197
 
data/lib/daidai/kabosu.rb CHANGED
@@ -38,7 +38,10 @@ module Daidai
38
38
  # can't distinguish (irregular okurigana inside an otherwise-regular row).
39
39
  LEMMA_POS = {
40
40
  "行く" => "v5k-s", "逝く" => "v5k-s", "往く" => "v5k-s",
41
- "有る" => "v5r-i", "在る" => "v5r-i", "ある" => "v5r-i"
41
+ "有る" => "v5r-i", "在る" => "v5r-i", "ある" => "v5r-i",
42
+ "問う" => "v5u-s",
43
+ "くださる" => "v5aru", "なさる" => "v5aru", "いらっしゃる" => "v5aru",
44
+ "いい" => "adj-ix"
42
45
  }.freeze
43
46
 
44
47
  class << self
@@ -85,11 +88,12 @@ module Daidai
85
88
  CONJUGATION_TYPE[pos[4]] || (pos[4].to_s.start_with?("上一段", "下一段") ? "v1" : nil)
86
89
  when "形容詞" then "adj-i"
87
90
  when "形状詞" then "adj-na"
91
+ when "助動詞" then "cop" if %w[助動詞-ダ 助動詞-デス].include?(pos[4])
88
92
  end
89
93
  end
90
94
 
91
95
  def inflecting?(pos)
92
- %w[動詞 形容詞 形状詞].include?(pos[0])
96
+ !from_conjugation_type(pos).nil?
93
97
  end
94
98
 
95
99
  def suru?(pos) = pos[4] == "サ行変格"
@@ -1,5 +1,25 @@
1
1
  {
2
2
  "conditions": {
3
+ "vs-noun": {
4
+ "name": "Suru verbal noun",
5
+ "isDictionaryForm": true,
6
+ "subConditions": null
7
+ },
8
+ "adj-na": {
9
+ "name": "Na adjective",
10
+ "isDictionaryForm": true,
11
+ "subConditions": null
12
+ },
13
+ "n": {
14
+ "name": "Noun",
15
+ "isDictionaryForm": true,
16
+ "subConditions": null
17
+ },
18
+ "copula": {
19
+ "name": "Copula attached to a nominal predicate",
20
+ "isDictionaryForm": true,
21
+ "subConditions": null
22
+ },
3
23
  "v": {
4
24
  "name": "Verb",
5
25
  "isDictionaryForm": false,
@@ -127,6 +147,71 @@
127
147
  }
128
148
  },
129
149
  "transforms": {
150
+ "-なら": {
151
+ "name": "-なら",
152
+ "rules": [
153
+ {
154
+ "type": "suffix",
155
+ "conditionsIn": [],
156
+ "conditionsOut": ["copula"],
157
+ "inflected": "なら",
158
+ "deinflected": "だ"
159
+ }
160
+ ]
161
+ },
162
+ "suru": {
163
+ "name": "suru",
164
+ "rules": [
165
+ {
166
+ "type": "suffix",
167
+ "conditionsIn": [
168
+ "copula"
169
+ ],
170
+ "conditionsOut": [
171
+ "copula"
172
+ ],
173
+ "inflected": "です",
174
+ "deinflected": "だ"
175
+ },
176
+ {
177
+ "type": "suffix",
178
+ "conditionsIn": [],
179
+ "conditionsOut": [
180
+ "copula"
181
+ ],
182
+ "inflected": "だった",
183
+ "deinflected": "だ"
184
+ },
185
+ {
186
+ "type": "suffix",
187
+ "conditionsIn": [],
188
+ "conditionsOut": [
189
+ "copula"
190
+ ],
191
+ "inflected": "でした",
192
+ "deinflected": "です"
193
+ },
194
+ {
195
+ "type": "suffix",
196
+ "conditionsIn": ["vs"],
197
+ "conditionsOut": ["vs-noun"],
198
+ "inflected": "する",
199
+ "deinflected": ""
200
+ }
201
+ ]
202
+ },
203
+ "copula": {
204
+ "name": "copula",
205
+ "rules": [
206
+ {
207
+ "type": "suffix",
208
+ "conditionsIn": ["copula"],
209
+ "conditionsOut": ["adj-na", "n"],
210
+ "inflected": "だ",
211
+ "deinflected": ""
212
+ }
213
+ ]
214
+ },
130
215
  "-ば": {
131
216
  "name": "-ば",
132
217
  "description": "1. Conditional form; shows that the previous stated condition's establishment is the condition for the latter stated condition to occur.\n2. Shows a trigger for a latter stated perception or judgment.\nUsage: Attach ば to the hypothetical form (仮定形) of verbs and i-adjectives.",
@@ -1905,6 +1990,20 @@
1905
1990
  "name": "-たら",
1906
1991
  "description": "1. Denotes the latter stated event is a continuation of the previous stated event.\n2. Assumes that a matter has been completed or concluded.\nUsage: Attach たら to the continuative form (連用形) of verbs after euphonic change form, かったら to the stem of i-adjectives.",
1907
1992
  "rules": [
1993
+ {
1994
+ "type": "suffix",
1995
+ "conditionsIn": [],
1996
+ "conditionsOut": ["copula"],
1997
+ "inflected": "だったら",
1998
+ "deinflected": "だ"
1999
+ },
2000
+ {
2001
+ "type": "suffix",
2002
+ "conditionsIn": [],
2003
+ "conditionsOut": ["copula"],
2004
+ "inflected": "でしたら",
2005
+ "deinflected": "です"
2006
+ },
1908
2007
  {
1909
2008
  "type": "suffix",
1910
2009
  "conditionsIn": [],
@@ -2244,6 +2343,13 @@
2244
2343
  "name": "-たり",
2245
2344
  "description": "1. Shows two actions occurring back and forth (when used with two verbs).\n2. Shows examples of actions and states (when used with multiple verbs and adjectives).\nUsage: Attach たり to the continuative form (連用形) of verbs after euphonic change form, かったり to the stem of i-adjectives",
2246
2345
  "rules": [
2346
+ {
2347
+ "type": "suffix",
2348
+ "conditionsIn": [],
2349
+ "conditionsOut": ["-ます"],
2350
+ "inflected": "ましたり",
2351
+ "deinflected": "ます"
2352
+ },
2247
2353
  {
2248
2354
  "type": "suffix",
2249
2355
  "conditionsIn": [],
@@ -5213,6 +5319,61 @@
5213
5319
  "name": "negative",
5214
5320
  "description": "1. Negative form of verbs.\n2. Expresses a feeling of solicitation to the other party.\nUsage: Attach ない to the irrealis form (未然形) of verbs, くない to the stem of i-adjectives. ない itself conjugates as i-adjective. ます becomes ません.",
5215
5321
  "rules": [
5322
+ {
5323
+ "type": "suffix",
5324
+ "conditionsIn": [
5325
+ "adj-i",
5326
+ "copula"
5327
+ ],
5328
+ "conditionsOut": [
5329
+ "copula"
5330
+ ],
5331
+ "inflected": "ではない",
5332
+ "deinflected": "だ"
5333
+ },
5334
+ {
5335
+ "type": "suffix",
5336
+ "conditionsIn": [
5337
+ "adj-i",
5338
+ "copula"
5339
+ ],
5340
+ "conditionsOut": [
5341
+ "copula"
5342
+ ],
5343
+ "inflected": "じゃない",
5344
+ "deinflected": "だ"
5345
+ },
5346
+ {
5347
+ "type": "suffix",
5348
+ "conditionsIn": [
5349
+ "copula",
5350
+ "-ません"
5351
+ ],
5352
+ "conditionsOut": [
5353
+ "copula"
5354
+ ],
5355
+ "inflected": "ではありません",
5356
+ "deinflected": "です"
5357
+ },
5358
+ {
5359
+ "type": "suffix",
5360
+ "conditionsIn": [
5361
+ "copula",
5362
+ "-ません"
5363
+ ],
5364
+ "conditionsOut": [
5365
+ "copula"
5366
+ ],
5367
+ "inflected": "じゃありません",
5368
+ "deinflected": "です"
5369
+ },
5370
+ {
5371
+ "type": "suffix",
5372
+ "conditionsIn": [],
5373
+ "conditionsOut": ["-なさい"],
5374
+ "inflected": "なさるな",
5375
+ "deinflected": "なさい"
5376
+ },
5216
5377
  {
5217
5378
  "type": "suffix",
5218
5379
  "conditionsIn": [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Daidai
4
- VERSION = "0.2.5"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daidai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - davafons
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-03 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: csv