* Implemented de-inflected verb search option (experimental).
authorUrban Wallasch <urban.wallasch@freenet.de>
Thu, 17 Jun 2021 18:23:33 +0000 (20:23 +0200)
committerUrban Wallasch <urban.wallasch@freenet.de>
Thu, 17 Jun 2021 18:23:33 +0000 (20:23 +0200)
* Improved heuristic to locate configuration and support files.

ROADMAP.txt
jiten-pai.py
vconj.utf8 [new file with mode: 0644]

index a8a705dd87475d4fd9bd8113829e19e7cb6ce0e9..4cd69fb7858ed69571f320981efde1363ac021fa 100644 (file)
@@ -16,7 +16,7 @@ Phase II
 [x] Romaji input
 [ ] app icon, "About" dialog
 [ ] README
-[ ] verb de-inflection?
+[x] verb de-inflection
 
 
 Phase III
index 56de2b90781777794413e809ee000740d791f3e1..bbcf27d362e68342aad40132821247b59fb74033 100755 (executable)
@@ -13,9 +13,11 @@ See `LICENSE` file for more information.
 """
 
 
-_JITENPAI_VERSION = '0.0.6'
+_JITENPAI_VERSION = '0.0.7'
 _JITENPAI_NAME = 'Jiten-pai'
+_JITENPAI_DIR = 'jiten-pai'
 _JITENPAI_CFG = 'jiten-pai.conf'
+_JITENPAI_VCONJ = 'vconj.utf8'
 
 _JITENPAI_HELP = 'todo'
 
@@ -80,22 +82,14 @@ cfg = {
     'lfont': 'IPAPMincho',
     'lfont_sz': 24.0,
     'hl_col': 'blue',
+    'deinflect': False,
     'max_hist': 12,
     'history': [],
     # run-time only, not saved:
     'cfgfile': None,
 }
 
-def _save_cfg():
-    s_cfg = cfg.copy()
-    s_cfg.pop('cfgfile', None)
-    if cfg['cfgfile']:
-        try:
-            with open(cfg['cfgfile'], 'w') as cfgfile:
-                json.dump(s_cfg, cfgfile, indent=2)
-                return
-        except Exception as e:
-            eprint(cfg['cfgfile'], str(e))
+def _get_cfile_path(fname, mode=os.R_OK):
     cdirs = []
     if os.environ.get('APPDATA'):
         cdirs.append(os.environ.get('APPDATA'))
@@ -103,37 +97,98 @@ def _save_cfg():
         cdirs.append(os.environ.get('XDG_CONFIG_HOME'))
     if os.environ.get('HOME'):
         cdirs.append(os.path.join(os.environ.get('HOME'), '.config'))
-        cdirs.append(os.environ.get('HOME'))
     cdirs.append(os.path.dirname(os.path.realpath(__file__)))
     for d in cdirs:
-        cf = os.path.join(d, _JITENPAI_CFG)
+        path = os.path.join(d, fname)
+        if os.access(path, mode):
+            return path
+    return fname
+
+def _save_cfg():
+    s_cfg = cfg.copy()
+    s_cfg.pop('cfgfile', None)
+    if cfg['cfgfile']:
         try:
-            with open(cf, 'w') as cfgfile:
+            with open(cfg['cfgfile'], 'w') as cfgfile:
                 json.dump(s_cfg, cfgfile, indent=2)
                 return
         except Exception as e:
-            eprint(cf, str(e))
+            eprint(cfg['cfgfile'], str(e))
+    cfgdir = _get_cfile_path('', mode=os.R_OK | os.W_OK | os.X_OK)
+    cfname = os.path.join(cfgdir, _JITENPAI_CFG)
+    try:
+        with open(cfname, 'w') as cfgfile:
+            json.dump(s_cfg, cfgfile, indent=2)
+            cfg['cfgfile'] = cfname
+            return
+    except Exception as e:
+        eprint(cfname, str(e))
 
 def _load_cfg():
+    cfname = _get_cfile_path('', mode=os.R_OK)
+    cfname = os.path.join(cfname, _JITENPAI_CFG)
+    try:
+        with open(cfname, 'r') as cfgfile:
+            cfg.update(json.load(cfgfile))
+            cfg['cfgfile'] = cfname
+            return
+    except Exception as e:
+        eprint(cfname, str(e))
+        pass
+
+
+############################################################
+# verb de-inflection
+
+_vc_type = dict()
+_vc_deinf = []
+
+def _get_dfile_path(fname, mode=os.R_OK):
     cdirs = []
     if os.environ.get('APPDATA'):
         cdirs.append(os.environ.get('APPDATA'))
-    if os.environ.get('XDG_CONFIG_HOME'):
-        cdirs.append(os.environ.get('XDG_CONFIG_HOME'))
     if os.environ.get('HOME'):
-        cdirs.append(os.path.join(os.environ.get('HOME'), '.config'))
-        cdirs.append(os.environ.get('HOME'))
+        cdirs.append(os.path.join(os.environ.get('HOME'), '.local/share'))
+    cdirs.append('/usr/local/share')
+    cdirs.append('/usr/share')
     cdirs.append(os.path.dirname(os.path.realpath(__file__)))
     for d in cdirs:
-        cf = os.path.join(d, _JITENPAI_CFG)
-        try:
-            with open(cf, 'r') as cfgfile:
-                cfg.update(json.load(cfgfile))
-                cfg['cfgfile'] = cf
-                return
-        except Exception as e:
-            eprint(cf, str(e))
-            pass
+        path = os.path.join(d, fname)
+        if os.access(path, mode):
+            return path
+    return fname
+
+def _vc_load():
+    vcname = _JITENPAI_VCONJ
+    if not os.access(vcname, os.R_OK):
+        vcname = _get_dfile_path(os.path.join(_JITENPAI_DIR, _JITENPAI_VCONJ), mode=os.R_OK)
+    try:
+        with open(vcname) as vcfile:
+            re_type = re.compile(r'^(\d+)\s+(.+)$')
+            re_deinf = re.compile(r'^\s*([^#\s]+)\s+(\S+)\s+(\d+)\s*$')
+            for line in vcfile:
+                match = re_type.match(line)
+                if match:
+                    _vc_type[match.group(1)] = match.group(2)
+                    continue
+                match = re_deinf.match(line)
+                if match:
+                    r = re.compile('%s$' % match.group(1))
+                    _vc_deinf.append([r, match.group(1), match.group(2), match.group(3)])
+                    continue
+    except Exception as e:
+        eprint(vcname, str(e))
+        pass
+
+def _vc_deinflect(verb):
+    inf = verb
+    blurb = ''
+    for p in _vc_deinf:
+        v = p[0].sub(p[2], verb)
+        if v != verb:
+            inf = v
+            blurb = '%s %s → %s' % (_vc_type[p[3]], p[1], p[2])
+    return inf, blurb
 
 
 ############################################################
@@ -606,7 +661,14 @@ class prefDialog(QDialog):
         fonts_layout.addRow(self.lfont_button, self.lfont_edit)
         fonts_layout.addRow(self.color_button, self.color_edit)
         fonts_layout.addRow('Sample', self.font_sample)
-        fonts_group.setLayout(fonts_layout)
+        # search options
+        search_group = zQGroupBox('Search Options')
+        self.search_deinflect = QCheckBox('&Verb Deinflection (experimental)')
+        self.search_deinflect.setChecked(cfg['deinflect'])
+        self.search_deinflect.setEnabled(len(_vc_deinf) > 0)
+        search_layout = zQVBoxLayout(search_group)
+        search_layout.addWidget(self.search_deinflect)
+        search_layout.addSpacing(10)
         # dicts
         dicts_group = zQGroupBox('Dictionaries')
         self.dict_list = QTreeWidget()
@@ -667,6 +729,7 @@ class prefDialog(QDialog):
         # dialog layout
         main_layout = QVBoxLayout(self)
         main_layout.addWidget(fonts_group)
+        main_layout.addWidget(search_group)
         main_layout.addWidget(dicts_group)
         main_layout.addStretch()
         main_layout.addLayout(button_layout)
@@ -735,6 +798,7 @@ class prefDialog(QDialog):
         cfg['hl_col'] = color.name()
         self.color_edit.setText(color.name())
         self.update_font_sample()
+        cfg['deinflect'] = self.search_deinflect.isChecked()
         d = []
         it = QTreeWidgetItemIterator(self.dict_list)
         while it.value():
@@ -1066,6 +1130,37 @@ class jpMainWindow(QMainWindow):
                 return True
         return False
 
+    def _search_deinflected(self, term, mode, limit):
+        inf, blurb = _vc_deinflect(term)
+        if not blurb:
+            return [], inf, ''
+        result = []
+        while 0 == len(result):
+            # apply search options
+            s_term = self._search_apply_options(inf, mode)
+            if s_term[-5:] != '(;|$)':
+                s_term += '(;|$)'
+            # perform lookup
+            if self.genopt_dict.isChecked():
+                dic = self.genopt_dictsel.itemData(self.genopt_dictsel.currentIndex())
+                result = dict_lookup(dic, s_term, mode, limit)
+                self.result_group.setTitle(self.result_group.title() + '.')
+                QApplication.processEvents()
+            else:
+                for d in cfg['dicts']:
+                    r = dict_lookup(d[1], s_term, mode, limit)
+                    result.extend(r)
+                    limit -= len(r)
+                    if limit == 0:
+                        limit = -1
+                    self.result_group.setTitle(self.result_group.title() + '.')
+                    QApplication.processEvents()
+            # relax search options
+            if len(result) == 0 and self.genopt_auto.isChecked():
+                if not self._search_relax(mode):
+                    break;
+        return result, inf, blurb
+
     def search(self):
         self.search_box.setFocus()
         # validate input
@@ -1093,6 +1188,16 @@ class jpMainWindow(QMainWindow):
         self.result_group.setTitle('Search results: ...')
         QApplication.processEvents()
         mode = ScanMode.JAP if contains_cjk(term) else ScanMode.ENG
+        # search deinflected verb
+        v_result = []
+        v_inf = ''
+        v_blurb = ''
+        if cfg['deinflect'] and mode == ScanMode.JAP:
+            v_result, v_inf, v_blurb = self._search_deinflected(term, mode, limit)
+            for r in v_result:
+                r.append(1)
+            limit -= len(v_result)
+        # normal search
         result = []
         while 0 == len(result):
             # apply search options
@@ -1117,6 +1222,7 @@ class jpMainWindow(QMainWindow):
                 if not self._search_relax(mode):
                     break;
         # report results
+        result.extend(v_result)
         rlen = len(result)
         self.result_group.setTitle('Search results: %d%s' % (rlen, '+' if rlen>=limit else ''))
         QApplication.processEvents()
@@ -1124,6 +1230,9 @@ class jpMainWindow(QMainWindow):
         if rlen > cfg['hardlimit'] / 2:
             self.result_pane.setPlainText('Formatting...')
             QApplication.processEvents()
+        if v_blurb:
+            verb_message = '<span style="color:#bc3031;">Possible inflected verb or adjective:</span> %s:<br>' % v_blurb
+            re_inf = re.compile(v_inf, re.IGNORECASE)
         re_term = re.compile(term, re.IGNORECASE)
         re_entity = re.compile(r'EntL\d+X?; *$', re.IGNORECASE)
         re_mark = re.compile(r'(\(.+?\))')
@@ -1145,12 +1254,20 @@ class jpMainWindow(QMainWindow):
             res[2] = re_entity.sub('', res[2])
             # highlight matches
             if mode == ScanMode.JAP:
-                res[0] = re_term.sub(lambda m: hl_repl(m, res[0]), kata2hira(res[0]))
-                res[1] = re_term.sub(hl_repl, res[1])
+                if len(res) > 3:
+                    res[0] = re_inf.sub(lambda m: hl_repl(m, res[0]), kata2hira(res[0]))
+                    res[1] = re_inf.sub(hl_repl, res[1])
+                else:
+                    res[0] = re_term.sub(lambda m: hl_repl(m, res[0]), kata2hira(res[0]))
+                    res[1] = re_term.sub(hl_repl, res[1])
             else:
                 res[2] = re_term.sub(hl_repl, res[2])
             # construct display line
-            html[idx+1] = '<p>%s%s</span>%s %s</p>\n' % (lfmt, res[0], (' (%s)'%res[1] if len(res[1]) > 0 else ''), res[2])
+            html[idx+1] = '<p>%s%s%s</span>%s %s</p>\n' \
+                % ((verb_message if len(res)>3 else ''), \
+                   lfmt, res[0],\
+                   (' (%s)'%res[1] if len(res[1]) > 0 else ''),\
+                   res[2])
         html[rlen + 1] = '</div>'
         self.result_pane.setHtml(''.join(html))
         self.result_pane.setEnabled(True)
@@ -1199,6 +1316,7 @@ def dict_lookup(dict_fname, pattern, mode, limit=0):
 
 def main():
     _load_cfg()
+    _vc_load()
     # set up window
     os.environ['QT_LOGGING_RULES'] = 'qt5ct.debug=false'
     app = QApplication(sys.argv)
diff --git a/vconj.utf8 b/vconj.utf8
new file mode 100644 (file)
index 0000000..fc3bbc5
--- /dev/null
@@ -0,0 +1,358 @@
+#
+# V C O N J - control file for verb and adjective deinflection
+#
+# the following section sets up the labels which are used for the
+# various inflections. These are displayed by the program.
+# The initial labels can be edited by the user.
+#
+#  First there are the labels for the types of conjugations
+#
+0      plain, negative, nonpast
+1      polite, non-past
+2      conditional
+3      volitional
+4      te-form
+5      plain, past
+6      plain, negative, past
+7      passive
+8      causative
+9      potential or imperative
+10     imperative
+11     polite, past
+12     polite, negative, non-past
+13     polite, negative, past
+14     polite, volitional
+15     adj. -> adverb
+16     adj., past
+17     polite
+18     polite, volitional
+19     passive or potential
+20     passive (or potential if Grp 2)
+21     adj., negative
+22     adj., negative, past
+23     adj., past
+24     plain verb
+25     polite, te-form
+#
+#  and these are the conjugations/inflections, and their dictionary forms
+#      (please note that these are scanned from the top, so the order is
+#      critical if the correct guess is to be made.)
+#
+$   this line flags the start of them
+#
+た    る     5
+て    る     4
+かない      く     0
+かなか      く     6
+きます      く     1
+きました   く     11
+きまして   く     25
+# NB: the order of the two following must not change, as the scan is downwards
+きませんでした  く     13
+きません   く     12
+きましょう        く     18
+けば く     2
+こう く     3
+いて く     4
+って く     4
+いた く     5
+った く     5
+かれ く     7
+かせ く     8
+け    く     9
+さない      す     0
+さなか      す     6
+します      す     1
+しました   す     11
+しまして   す     25
+しませんでした  す     13
+しません   す     12
+しましょう        す     18
+せば す     2
+そう す     3
+して す     4
+した す     5
+され す     7
+させ す     8
+せ    す     9
+たない      つ     0
+たなか      つ     6
+ちます      つ     1
+ちました   つ     11
+ちまして   つ     25
+ちませんでした  つ     13
+ちません   つ     12
+ちましょう        つ     18
+てば つ     2
+とう つ     3
+って つ     4
+った つ     5
+たれ つ     7
+たせ つ     8
+て    つ     9
+なない      ぬ     0
+ななか      ぬ     6
+にます      ぬ     1
+にました   ぬ     11
+にまして   ぬ     25
+にませんでした  ぬ     13
+にません   ぬ     12
+にましょう        に     18
+ねば ぬ     2
+のう ぬ     3
+んで ぬ     4
+んだ ぬ     5
+なれ ぬ     7
+なせ ぬ     8
+ね    ぬ     9
+まない      む     0
+まなか      む     6
+みます      む     1
+みました   む     11
+みまして   む     25
+みませんでした  む     13
+みません   む     12
+みましょう        む     18
+めば む     2
+もう む     3
+んで む     4
+んだ む     5
+まれ む     7
+ませ む     8
+め    む     9
+らない      る     0
+らなか      る     6
+ります      る     1
+りました   る     11
+りまして   る     25
+りませんでした  る     13
+りません   る     12
+りましょう        る     18
+れば る     2
+ろう る     3
+って る     4
+った る     5
+られ る     20
+らせ る     8
+# れ  る     9 moved below
+わない      う     0
+わなか      う     6
+います      う     1
+いました   う     11
+いまして   う     25
+いませんでした  う     13
+いません   う     12
+いましょう        う     18
+えば う     2
+おう う     3
+って う     4
+った う     5
+われ う     7
+わせ う     8
+え    う     9
+がない      ぐ     0
+がなか      ぐ     6
+ぎます      ぐ     1
+ぎました   ぐ     11
+ぎまして   ぐ     25
+ぎませんでした  ぐ     13
+ぎません   ぐ     12
+ぎましょう        ぐ     18
+げば ぐ     2
+ごう ぐ     3
+いで ぐ     4
+いだ ぐ     5
+がれ ぐ     7
+がせ ぐ     8
+げ    ぐ     9
+ばない      ぶ     0
+ばなか      ぶ     6
+びます      ぶ     1
+びました   ぶ     11
+びまして   ぶ     25
+びませんでした  ぶ     13
+びません   ぶ     12
+びましょう        ぶ     18
+べば ぶ     2
+ぼう ぶ     3
+んで ぶ     4
+んだ ぶ     5
+ばれ ぶ     7
+ばせ ぶ     8
+べ    ぶ     9
+ない る     0
+なか る     6
+ます る     1
+ました      る     11
+ませんでした     る     13
+ません      る     12
+ましょう   る     18
+れば る     2
+よう る     3
+て    る     4
+た    る     5
+られ る     20
+させ る     8
+ろ    る     10
+らま る     17
+くなか      い     22
+くな い     21
+かった      い     23
+く    い     15
+しか しい  16
+けます      ける  1
+けました   ける  11
+けませんでした  ける  13
+けません   ける  12
+けましょう        ける  18
+けない      ける  0
+けなか      ける  6
+けれ ける  2
+けよ ける  3
+けて ける  4
+けた ける  5
+けら ける  19
+けさ ける  8
+けろ ける  10
+げます      げる  1
+げました   げる  11
+げませんでした  げる  13
+げません   げる  12
+げましょう        げる  18
+げない      げる  0
+げなか      げる  6
+げて げる  4
+げれ げる  2
+げよ げる  3
+げた げる  5
+げら げる  19
+げさ げる  8
+げろ げる  10
+べます      べる  1
+べました   べる  11
+べませんでした  べる  13
+べません   べる  12
+べましょう        べる  18
+べない      べる  0
+べなか      べる  6
+べれ べる  2
+べよ べる  3
+べて べる  4
+べた べる  5
+べら べる  19
+べさ べる  8
+べろ べる  10
+めます      める  1
+めました   める  11
+めませんでした  める  13
+めません   める  12
+めましょう        める  18
+めない      める  0
+めなか      める  6
+めれ める  2
+めよ める  3
+めて める  4
+めた める  5
+めら める  19
+めさ める  8
+めろ める  10
+えます      える  1
+えました   える  11
+えませんでした  える  13
+えません   える  12
+えましょう        える  18
+えない      える  0
+えなか      える  6
+えれ える  2
+えよ える  3
+えて える  4
+えた える  5
+えら える  19
+えさ える  8
+えろ える  10
+れます      れる  1
+れました   れる  11
+れませんでした  れる  13
+れません   れる  12
+れましょう        れる  18
+れない      れる  0
+れなか      れる  6
+れれ れる  2
+れよ れる  3
+れて れる  4
+れた れる  5
+れら れる  19
+れさ れる  8
+れろ れる  10
+れ    る     9
+ねます      ねる  1
+ねました   ねる  11
+ねませんでした  ねる  13
+ねません   ねる  12
+ねましょう        ねる  18
+ねない      ねる  0
+ねなか      ねる  6
+ねれ ねる  2
+ねよ ねる  3
+ねて ねる  4
+ねた ねる  5
+ねら ねる  19
+ねさ ねる  8
+ねろ ねる  10
+せます      せる  1
+せました   せる  11
+せませんでした  せる  13
+せません   せる  12
+せましょう        せる  18
+せない      せる  0
+せなか      せる  6
+せれ せる  2
+せよ せる  3
+せて せる  4
+せた せる  5
+せら せる  19
+せさ せる  8
+せろ せる  10
+ぜます      ぜる  1
+ぜました   ぜる  11
+ぜませんでした  ぜる  13
+ぜません   ぜる  12
+ぜましょう        ぜる  18
+ぜない      ぜる  0
+ぜなか      ぜる  6
+ぜれ ぜる  2
+ぜよ ぜる  3
+ぜて ぜる  4
+ぜた ぜる  5
+ぜら ぜる  19
+ぜさ ぜる  8
+ぜろ ぜる  10
+てます      てる  1
+てました   てる  11
+てませんでした  てる  13
+てません   てる  12
+てましょう        てる  18
+てない      てる  0
+てなか      てる  6
+てれ てる  2
+てよ てる  3
+てて てる  4
+てた てる  5
+てら てる  19
+てさ てる  8
+てろ てる  10
+でます      でる  1
+でました   でる  11
+でませんでした  でる  13
+でません   でる  12
+でましょう        でる  18
+でない      でる  0
+でなか      でる  6
+でれ でる  2
+でよ でる  3
+でて でる  4
+でた でる  5
+でら でる  19
+でさ でる  8
+でろ でる  10
+#く   く     24