lingo 1.8.0 → 1.8.1

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.
Files changed (100) hide show
  1. data/ChangeLog +13 -0
  2. data/README +49 -29
  3. data/Rakefile +28 -4
  4. data/TODO +2 -9
  5. data/bin/lingo +24 -0
  6. data/bin/lingoctl +24 -0
  7. data/de/lingo-dic.txt +559 -74
  8. data/info/gpl-hdr.txt +21 -24
  9. data/lib/lingo.rb +83 -112
  10. data/lib/lingo/agenda_item.rb +53 -0
  11. data/lib/lingo/attendee.rb +261 -0
  12. data/lib/lingo/attendee/abbreviator.rb +95 -97
  13. data/lib/lingo/attendee/debugger.rb +94 -93
  14. data/lib/lingo/attendee/decomposer.rb +76 -83
  15. data/lib/lingo/attendee/dehyphenizer.rb +141 -144
  16. data/lib/lingo/attendee/formatter.rb +65 -0
  17. data/lib/lingo/attendee/multi_worder.rb +302 -0
  18. data/lib/lingo/attendee/noneword_filter.rb +89 -84
  19. data/lib/lingo/attendee/object_filter.rb +91 -0
  20. data/lib/lingo/attendee/sequencer.rb +159 -158
  21. data/lib/lingo/attendee/synonymer.rb +81 -84
  22. data/lib/lingo/attendee/text_reader.rb +242 -0
  23. data/lib/lingo/attendee/text_writer.rb +169 -0
  24. data/lib/lingo/attendee/tokenizer.rb +192 -191
  25. data/lib/lingo/attendee/variator.rb +152 -156
  26. data/lib/lingo/attendee/vector_filter.rb +140 -135
  27. data/lib/lingo/attendee/word_searcher.rb +98 -0
  28. data/lib/lingo/buffered_attendee.rb +69 -0
  29. data/lib/lingo/cachable.rb +58 -0
  30. data/lib/lingo/call.rb +72 -0
  31. data/lib/lingo/cli.rb +26 -0
  32. data/lib/lingo/config.rb +23 -26
  33. data/lib/lingo/core_ext.rb +42 -0
  34. data/lib/lingo/ctl.rb +239 -173
  35. data/lib/lingo/database.rb +148 -496
  36. data/lib/lingo/database/crypter.rb +85 -0
  37. data/lib/lingo/database/gdbm_store.rb +49 -0
  38. data/lib/lingo/database/hash_store.rb +67 -0
  39. data/lib/lingo/database/libcdb_store.rb +58 -0
  40. data/lib/lingo/database/sdbm_store.rb +64 -0
  41. data/lib/lingo/database/show_progress.rb +81 -0
  42. data/lib/lingo/database/source.rb +134 -0
  43. data/lib/lingo/database/source/key_value.rb +62 -0
  44. data/lib/lingo/database/source/multi_key.rb +65 -0
  45. data/lib/lingo/database/source/multi_value.rb +65 -0
  46. data/lib/lingo/database/source/single_word.rb +60 -0
  47. data/lib/lingo/database/source/word_class.rb +64 -0
  48. data/lib/lingo/error.rb +122 -0
  49. data/lib/lingo/language.rb +78 -518
  50. data/lib/lingo/language/dictionary.rb +173 -0
  51. data/lib/lingo/language/grammar.rb +211 -0
  52. data/lib/lingo/language/lexical.rb +66 -0
  53. data/lib/lingo/language/lexical_hash.rb +88 -0
  54. data/lib/lingo/language/token.rb +48 -0
  55. data/lib/lingo/language/word.rb +130 -0
  56. data/lib/lingo/language/word_form.rb +83 -0
  57. data/lib/lingo/reportable.rb +59 -0
  58. data/lib/lingo/version.rb +1 -1
  59. data/lingo-all.cfg +14 -10
  60. data/lingo-call.cfg +5 -5
  61. data/lingo.cfg +14 -12
  62. data/lingo.rb +26 -0
  63. data/lir.cfg +13 -9
  64. data/spec/spec_helper.rb +1 -0
  65. data/test.cfg +11 -11
  66. data/test/attendee/ts_abbreviator.rb +0 -6
  67. data/test/attendee/ts_decomposer.rb +0 -6
  68. data/test/attendee/{ts_multiworder.rb → ts_multi_worder.rb} +1 -7
  69. data/test/attendee/ts_noneword_filter.rb +1 -7
  70. data/test/attendee/{ts_objectfilter.rb → ts_object_filter.rb} +1 -7
  71. data/test/attendee/ts_sequencer.rb +0 -6
  72. data/test/attendee/ts_synonymer.rb +0 -6
  73. data/test/attendee/{ts_textreader.rb → ts_text_reader.rb} +1 -7
  74. data/test/attendee/{ts_textwriter.rb → ts_text_writer.rb} +1 -7
  75. data/test/attendee/ts_tokenizer.rb +0 -6
  76. data/test/attendee/ts_variator.rb +0 -6
  77. data/test/attendee/ts_vector_filter.rb +1 -7
  78. data/test/attendee/{ts_wordsearcher.rb → ts_word_searcher.rb} +1 -7
  79. data/test/ref/artikel.non +2 -29
  80. data/test/ref/artikel.seq +13 -8
  81. data/test/ref/artikel.vec +30 -15
  82. data/test/ref/artikel.ven +29 -14
  83. data/test/ref/artikel.ver +58 -43
  84. data/test/ref/lir.csv +146 -145
  85. data/test/ref/lir.non +186 -210
  86. data/test/ref/lir.seq +54 -50
  87. data/test/test_helper.rb +41 -36
  88. data/test/ts_database.rb +12 -11
  89. data/test/ts_language.rb +118 -68
  90. metadata +67 -29
  91. data/lib/lingo/attendee/multiworder.rb +0 -301
  92. data/lib/lingo/attendee/objectfilter.rb +0 -86
  93. data/lib/lingo/attendee/textreader.rb +0 -237
  94. data/lib/lingo/attendee/textwriter.rb +0 -196
  95. data/lib/lingo/attendee/wordsearcher.rb +0 -96
  96. data/lib/lingo/attendees.rb +0 -289
  97. data/lib/lingo/const.rb +0 -131
  98. data/lib/lingo/modules.rb +0 -98
  99. data/lib/lingo/types.rb +0 -285
  100. data/lib/lingo/utilities.rb +0 -40
@@ -1,585 +1,237 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  #--
4
- # LINGO ist ein Indexierungssystem mit Grundformreduktion, Kompositumzerlegung,
5
- # Mehrworterkennung und Relationierung.
6
- #
7
- # Copyright (C) 2005-2007 John Vorhauer
8
- # Copyright (C) 2007-2011 John Vorhauer, Jens Wille
9
- #
10
- # This program is free software; you can redistribute it and/or modify it under
11
- # the terms of the GNU Affero General Public License as published by the Free
12
- # Software Foundation; either version 3 of the License, or (at your option)
13
- # any later version.
14
- #
15
- # This program is distributed in the hope that it will be useful, but WITHOUT
16
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
- # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
18
- # details.
19
- #
20
- # You should have received a copy of the GNU Affero General Public License along
21
- # with this program; if not, write to the Free Software Foundation, Inc.,
22
- # 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
23
- #
24
- # For more information visit http://www.lex-lingo.de or contact me at
25
- # welcomeATlex-lingoDOTde near 50°55'N+6°55'E.
26
- #
27
- # Lex Lingo rules from here on
4
+ ###############################################################################
5
+ # #
6
+ # Lingo -- A full-featured automatic indexing system #
7
+ # #
8
+ # Copyright (C) 2005-2007 John Vorhauer #
9
+ # Copyright (C) 2007-2012 John Vorhauer, Jens Wille #
10
+ # #
11
+ # Lingo is free software; you can redistribute it and/or modify it under the #
12
+ # terms of the GNU Affero General Public License as published by the Free #
13
+ # Software Foundation; either version 3 of the License, or (at your option) #
14
+ # any later version. #
15
+ # #
16
+ # Lingo is distributed in the hope that it will be useful, but WITHOUT ANY #
17
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
18
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
19
+ # more details. #
20
+ # #
21
+ # You should have received a copy of the GNU Affero General Public License #
22
+ # along with Lingo. If not, see <http://www.gnu.org/licenses/>. #
23
+ # #
24
+ ###############################################################################
28
25
  #++
29
26
 
30
- require 'sdbm'
31
27
  require 'pathname'
32
28
  require 'fileutils'
33
29
  require 'digest/sha1'
34
30
 
35
- require_relative 'const'
36
- require_relative 'types'
37
- require_relative 'utilities'
38
- require_relative 'modules'
31
+ require_relative 'database/show_progress'
32
+ require_relative 'database/crypter'
33
+ require_relative 'database/source'
34
+ require_relative 'database/hash_store'
35
+ require_relative 'database/sdbm_store'
36
+ require_relative 'database/gdbm_store'
37
+ require_relative 'database/libcdb_store'
39
38
 
40
39
  class Lingo
41
40
 
42
- class ShowProgress
43
-
44
- def initialize(msg, active = true, out = $stderr)
45
- @active, @out, format = active, out, ' [%3d%%]'
46
-
47
- # To get the length of the formatted string we have
48
- # to actually substitute the placeholder.
49
- length = (format % 0).length
50
-
51
- # Now we know how far to "go back" to
52
- # overwrite the formatted string...
53
- back = "\b" * length
54
-
55
- @format = format + back
56
- @clear = ' ' * length + back
57
-
58
- print msg, ': '
59
- end
60
-
61
- def start(msg, max)
62
- @ratio, @count, @next_step = max / 100.0, 0, 0
63
- print msg, ' '
64
- step
65
- end
66
-
67
- def stop(msg)
68
- print @clear
69
- print msg, "\n"
70
- end
71
-
72
- def tick(value)
73
- @count = value
74
- step if @count >= @next_step
75
- end
76
-
77
- private
78
-
79
- def step
80
- percent = @count / @ratio
81
- @next_step = (percent + 1) * @ratio
82
-
83
- print @format % percent
84
- end
85
-
86
- def print(*args)
87
- @out.print(*args) if @active
88
- end
89
-
90
- end
91
-
92
- # Crypter ermöglicht die Ver- und Entschlüsselung von Wörterbüchern
93
-
94
- class Crypter
95
-
96
- HEX_CHARS = '0123456789abcdef'.freeze
97
-
98
- def digest(key)
99
- Digest::SHA1.hexdigest(key)
100
- end
101
-
102
- def encode(key, val)
103
- hex = ''
104
-
105
- crypt(key, val).each_byte { |byte|
106
- # To get a hex representation for a char we just utilize
107
- # the quotient and the remainder of division by base 16.
108
- q, r = byte.divmod(16)
109
- hex << HEX_CHARS[q] << HEX_CHARS[r]
110
- }
41
+ # Die Klasse Database stellt eine einheitliche Schnittstelle auf Lingo-Datenbanken bereit.
42
+ # Die Identifizierung der Datenbank erfolgt über die ID der Datenbank, so wie sie in der
43
+ # Sprachkonfigurationsdatei <tt>de.lang</tt> unter <tt>language/dictionary/databases</tt>
44
+ # hinterlegt ist.
45
+ #
46
+ # Das Lesen und Schreiben der Datenbank erfolgt über die Funktionen []() und []=().
111
47
 
112
- [digest(key), hex]
113
- end
48
+ class Database
114
49
 
115
- def decode(key, val)
116
- str, q, first = '', 0, false
50
+ include Cachable
117
51
 
118
- val.each_byte { |byte|
119
- byte = byte.chr(ENC)
52
+ BACKENDS = %w[LibCDB SDBM GDBM].unshift(ENV['LINGO_BACKEND']).compact.uniq
120
53
 
121
- # Our hex chars are 2 bytes wide, so we have to keep track
122
- # of whether it's the first or the second of the two.
123
- if first = !first
124
- q = HEX_CHARS.index(byte)
125
- else
126
- # Now we got both parts, so let's revert the divmod(16)
127
- str << q * 16 + HEX_CHARS.index(byte)
128
- end
129
- }
54
+ FLD_SEP = '|'
55
+ IDX_REF = '^'
56
+ KEY_REF = '*'
57
+ SYS_KEY = '~'
130
58
 
131
- crypt(key, str)
132
- end
133
-
134
- private
59
+ INDEX_PATTERN = %r{\A#{Regexp.escape(IDX_REF)}\d+\z}
135
60
 
136
- def crypt(k, v)
137
- c, y = '', k.codepoints.reverse_each.cycle
138
- v.each_codepoint { |x| c << (x ^ y.next).chr(ENC) }
139
- c
61
+ def self.open(*args, &block)
62
+ new(*args).open(&block)
140
63
  end
141
64
 
142
- end
143
-
144
- # Die Klasse TxtFile stellt eine einheitliche Schnittstelle auf die unterschiedlichen Formate
145
- # von Wörterbuch-Quelldateien bereit. Die Identifizierung der Quelldatei erfolgt über die ID
146
- # der Datei, so wie sie in der Sprachkonfigurationsdatei <tt>de.lang</tt> unter
147
- # <tt>language/dictionary/databases</tt> hinterlegt ist.
148
- #
149
- # Die Verarbeitung der Wörterbücher erfolgt mittels des Iterators <b>each</b>, der für jede
150
- # Zeile der Quelldatei ein Array bereitstellt in der Form <tt>[ key, [val1, val2, ...] ]</tt>.
151
- #
152
- # Nicht korrekt erkannte Zeilen werden abgewiesen und in eine Revoke-Datei gespeichert, die
153
- # an der Dateiendung <tt>.rev</tt> zu erkennen ist.
154
-
155
- class TxtFile
156
-
157
- attr_reader :position
158
-
159
65
  def initialize(id, lingo)
160
- # Konfiguration der Datenbank auslesen
161
66
  @config = lingo.database_config(id)
162
67
 
163
- source_file = Lingo.find(:dict, name = @config['name'])
164
-
165
- @pn_source = Pathname.new(source_file)
166
- @pn_reject = Pathname.new(Lingo.find(:store, source_file) << '.rev')
68
+ @id, @lingo = id, lingo
69
+ @src_file = Lingo.find(:dict, @config['name'])
70
+ @crypter = Crypter.new if @config.has_key?('crypt')
167
71
 
168
- Lingo.error("No such source file `#{name}' for `#{id}'.") unless @pn_source.exist?
169
-
170
- @wordclass = @config.fetch('def-wc', '?').downcase
171
- @separator = @config['separator']
172
-
173
- @legal_word = '(?:' + PRINTABLE_CHAR + '|[' + Regexp.escape('- /&()[].,') + '])+' # TODO: v1.60 - ',' bei TxtFile zulassen; in const.rb einbauen
174
- @line_pattern = Regexp.new('^'+@legal_word+'$')
175
-
176
- @position = 0
177
- end
178
-
179
- def size
180
- @pn_source.size
181
- end
182
-
183
- def each
184
- # Reject-Datei öffnen
185
- fail_msg = "Fehler beim öffnen der Reject-Datei '#{@pn_reject.to_s}'"
186
- reject_file = @pn_reject.open('w', encoding: ENC)
187
-
188
- # Alle Zeilen der Quelldatei verarbeiten
189
- fail_msg = "Fehler beim öffnen der Wörterbuch-Quelldatei '#{@pn_source.to_s}'"
190
-
191
- @pn_source.each_line($/, encoding: ENC) do |raw_line|
192
- @position += raw_line.size # Position innerhalb der Datei aktualisieren
193
- line = raw_line.chomp.downcase # Zeile normieren
194
-
195
- next if line =~ /^\s*\043/ || line.strip == '' # Kommentarzeilen und leere Zeilen überspringen
196
-
197
- # Ungültige Zeilen protokollieren
198
- unless line.length < 4096 && line =~ @line_pattern
199
- fail_msg = "Fehler beim schreiben der Reject-Datei '#{@pn_reject.to_s}'"
200
- reject_file.puts line
201
- next
202
- end
203
-
204
- # Zeile in Werte konvertieren
205
- yield convert_line(line, $1, $2)
72
+ begin
73
+ @dbm_name = Lingo.find(:store, @src_file)
74
+ FileUtils.mkdir_p(File.dirname(@dbm_name))
75
+ rescue NoWritableStoreError
76
+ @backend = HashStore
206
77
  end
207
78
 
208
- fail_msg = "Fehler beim Schließen der Reject-Datei '#{@pn_reject.to_s}'"
209
- reject_file.close
210
- @pn_reject.delete if @pn_reject.size == 0
211
-
212
- self
213
- rescue RuntimeError
214
- Lingo.error(fail_msg)
215
- end
216
-
217
- end
218
-
219
- # Abgeleitet von TxtFile behandelt die Klasse Dateien mit dem Format <tt>SingleWord</tt>.
220
- # Eine Zeile <tt>"Fachbegriff\n"</tt> wird gewandelt in <tt>[ 'fachbegriff', ['#s'] ]</tt>.
221
- # Die Wortklasse kann über den Parameter <tt>def-wc</tt> beeinflusst werden.
222
-
223
- class TxtFile_Singleword < TxtFile
224
-
225
- def initialize(id, lingo)
226
- super
227
-
228
- @wc = @config.fetch('def-wc', 's').downcase
229
- @mul_wc = @config.fetch('def-mul-wc', @wc).downcase
79
+ extend(backend)
230
80
 
231
- @line_pattern = %r{^(#{@legal_word})$}
232
- end
233
-
234
- private
81
+ @dbm_name << store_ext if respond_to?(:store_ext, true)
235
82
 
236
- def convert_line(line, key, val)
237
- [key = key.strip, %W[##{key =~ /\s/ ? @mul_wc : @wc}]]
83
+ init_cachable
84
+ convert unless uptodate?
238
85
  end
239
86
 
240
- end
241
-
242
- # Abgeleitet von TxtFile behandelt die Klasse Dateien mit dem Format <tt>KeyValue</tt>.
243
- # Eine Zeile <tt>"Fachbegriff*Fachterminus\n"</tt> wird gewandelt in <tt>[ 'fachbegriff', ['fachterminus#s'] ]</tt>.
244
- # Die Wortklasse kann über den Parameter <tt>def-wc</tt> beeinflusst werden.
245
- # Der Trenner zwischen Schlüssel und Projektion kann über den Parameter <tt>separator</tt> geändert werden.
246
-
247
- class TxtFile_Keyvalue < TxtFile
248
-
249
- def initialize(id, lingo)
250
- super
251
-
252
- @separator = @config.fetch('separator', '*')
253
- @line_pattern = Regexp.new('^(' + @legal_word + ')' + Regexp.escape(@separator) + '(' + @legal_word + ')$')
87
+ def backend
88
+ @backend ||= BACKENDS.find { |mod|
89
+ break self.class.const_get("#{mod}Store") if Object.const_defined?(mod)
90
+ } || HashStore
254
91
  end
255
92
 
256
- private
257
-
258
- def convert_line(line, key, val)
259
- key, val = key.strip, val.strip
260
- val = '' if key == val
261
- val = [val + '#' + @wordclass]
262
- [key, val]
93
+ def closed?
94
+ @db.nil? || _closed?
263
95
  end
264
96
 
265
- end
266
-
267
- # Abgeleitet von TxtFile behandelt die Klasse Dateien mit dem Format <tt>WordClass</tt>.
268
- # Eine Zeile <tt>"essen,essen #v essen #o esse #s\n"</tt> wird gewandelt in <tt>[ 'essen', ['esse#s', 'essen#v', 'essen#o'] ]</tt>.
269
- # Der Trenner zwischen Schlüssel und Projektion kann über den Parameter <tt>separator</tt> geändert werden.
270
-
271
- class TxtFile_Wordclass < TxtFile
272
-
273
- def initialize(id, lingo)
274
- super
275
-
276
- @separator = @config.fetch('separator', ',')
277
- @line_pattern = Regexp.new('^(' + @legal_word + ')' + Regexp.escape(@separator) + '((?:' + @legal_word + '\043\w)+)$')
97
+ def open
98
+ @db = _open if closed?
99
+ block_given? ? yield(self) : self
100
+ ensure
101
+ close if @db && block_given?
278
102
  end
279
103
 
280
- private
104
+ def close
105
+ @db.close unless closed?
106
+ @db = nil
281
107
 
282
- def convert_line(line, key, val)
283
- key, valstr = key.strip, val.strip
284
- val = valstr.gsub(/\s+\043/, '#').scan(/\S.+?\s*\043\w/)
285
- val = val.map do |str|
286
- str =~ /^(.+)\043(.)/
287
- ($1 == key ? '' : $1) + '#' + $2
288
- end
289
- [key, val]
108
+ self
290
109
  end
291
110
 
292
- end
293
-
294
- # Abgeleitet von TxtFile behandelt die Klasse Dateien mit dem Format <tt>MultiValue</tt>.
295
- # Eine Zeile <tt>"Triumph;Sieg;Erfolg\n"</tt> wird gewandelt in <tt>[ nil, ['triumph', 'sieg', 'erfolg'] ]</tt>.
296
- # Der Trenner zwischen Schlüssel und Projektion kann über den Parameter <tt>separator</tt> geändert werden.
297
-
298
- class TxtFile_Multivalue < TxtFile
299
-
300
- def initialize(id, lingo)
301
- super
302
-
303
- @separator = @config.fetch('separator', ';')
304
- @line_pattern = Regexp.new('^' + @legal_word + '(?:' + Regexp.escape(@separator) + @legal_word + ')*$')
111
+ def to_h
112
+ {}.tap { |hash| @db.each { |key, val|
113
+ hash[key.force_encoding(ENC).freeze] = val.force_encoding(ENC)
114
+ } unless closed? }
305
115
  end
306
116
 
307
- private
117
+ def [](key)
118
+ val = _val(key) unless closed?
119
+ return unless val
308
120
 
309
- def convert_line(line, key, val)
310
- [nil, line.split(@separator).map { |value| value.strip }]
121
+ # Äquvalenzklassen behandeln
122
+ val.split(FLD_SEP).map { |v|
123
+ v =~ INDEX_PATTERN ? _val(v) : v
124
+ }.compact.join(FLD_SEP).split(FLD_SEP)
311
125
  end
312
126
 
313
- end
127
+ def []=(key, val)
128
+ return if closed?
314
129
 
315
- # Abgeleitet von TxtFile behandelt die Klasse Dateien mit dem Format <tt>MultiKey</tt>.
316
- # Eine Zeile <tt>"Triumph;Sieg;Erfolg\n"</tt> wird gewandelt in <tt>[ 'triumph', ['sieg', 'erfolg'] ]</tt>.
317
- # Die Sonderbehandlung erfolgt in der Klasse Txt2DbmConverter, wo daraus Schlüssel-Werte-Paare in der Form
318
- # <tt>[ 'sieg', ['triumph'] ]</tt> und <tt>[ 'erfolg', ['triumph'] ]</tt> erzeugt werden.
319
- # Der Trenner zwischen Schlüssel und Projektion kann über den Parameter <tt>separator</tt> geändert werden.
130
+ val = val.dup
131
+ val.concat(retrieve(key)) if hit?(key)
320
132
 
321
- class TxtFile_Multikey < TxtFile
133
+ val.sort!
134
+ val.uniq!
135
+ store(key, val)
322
136
 
323
- def initialize(id, lingo)
324
- super
137
+ val = val.join(FLD_SEP)
138
+ key, val = @crypter.encode(key, val) if @crypter
325
139
 
326
- @separator = @config.fetch('separator', ';')
327
- @line_pattern = Regexp.new('^' + @legal_word + '(?:' + Regexp.escape(@separator) + @legal_word + ')*$')
140
+ _set(key, val)
328
141
  end
329
142
 
330
143
  private
331
144
 
332
- def convert_line(line, key, val)
333
- values = line.split(@separator).map { |value| value.strip }
334
- [values[0], values[1..-1]]
335
- end
336
-
337
- end
145
+ def uptodate?(file = @dbm_name)
146
+ src = Pathname.new(@src_file)
147
+ @source_key = lambda { [src.size, src.mtime].join(FLD_SEP) }
338
148
 
339
- # Die Klasse DbmFile stellt eine einheitliche Schnittstelle auf Lingo-Datenbanken bereit.
340
- # Die Identifizierung der Datenbank erfolgt über die ID der Datenbank, so wie sie in der
341
- # Sprachkonfigurationsdatei <tt>de.lang</tt> unter <tt>language/dictionary/databases</tt>
342
- # hinterlegt ist.
343
- #
344
- # Das Lesen und Schreiben der Datenbank erfolgt über die Funktionen []() und []=().
345
-
346
- class DbmFile
347
-
348
- include Cachable
349
-
350
- INDEX_PATTERN = %r{\A#{Regexp.escape(IDX_REF)}\d+\z}
351
-
352
- def self.open(*args)
353
- dbm = new(*args)
354
- dbm.open { yield dbm }
149
+ sys_key = open { @db[SYS_KEY] } if File.exist?(file)
150
+ sys_key && (!src.exist? || sys_key == @source_key.call)
355
151
  end
356
152
 
357
- def initialize(id, lingo, read_mode = true)
358
- @lingo = lingo
359
-
360
- init_cachable
361
-
362
- config = lingo.database_config(id)
363
- raise "No such database `#{id}'." unless config && config.has_key?('name')
364
-
365
- @id, @dbm = id, nil
366
- @src_file = Lingo.find(:dict, config['name'])
367
- @dbm_name = Lingo.find(:store, @src_file)
368
-
369
- Txt2DbmConverter.new(id, lingo).convert if read_mode && !uptodate?
370
-
371
- @crypter = config.has_key?('crypt') ? Crypter.new : nil
372
-
373
- FileUtils.mkdir_p(File.dirname(@dbm_name))
153
+ def uptodate!
154
+ @db[SYS_KEY] = @source_key.call
374
155
  end
375
156
 
376
- # Überprüft die Aktualität des DbmFile
377
- def uptodate?
378
- begin
379
- key = open { @dbm[SYS_KEY] }
380
- rescue RuntimeError
381
- end if File.exist?("#{@dbm_name}.pag")
382
-
383
- key && (!(pn = Pathname.new(@src_file)).exist? || key == source_key(pn))
157
+ def create
158
+ _clear
159
+ open { yield }
384
160
  end
385
161
 
386
- def open
387
- if closed?
388
- @dbm = SDBM.open(@dbm_name)
389
- block_given? ? yield : self
390
- else
391
- Lingo.error("DbmFile #{@dbm_name} bereits geöffnet")
392
- end
393
- ensure
394
- close if @dbm && block_given?
162
+ def _clear
163
+ File.delete(@dbm_name) if File.exist?(@dbm_name)
395
164
  end
396
165
 
397
- def to_h
398
- hash = {}
399
-
400
- @dbm.each { |key, val|
401
- [key, val].each { |x| x.encode!(ENC) }
402
- hash[key.freeze] = val
403
- } unless closed?
404
-
405
- hash
166
+ def _open
167
+ raise NotImplementedError
406
168
  end
407
169
 
408
- def clear
409
- files = %w[pag dir].map { |ext| "#{@dbm_name}.#{ext}" }
410
-
411
- if closed?
412
- files.each { |file| File.delete(file) if File.exist?(file) }
413
- else
414
- close
415
- files.each { |file| File.delete(file) }
416
- open
417
- end
418
-
419
- self
420
- end
421
-
422
- def close
423
- unless closed?
424
- @dbm.close
425
- @dbm = nil
426
-
427
- self
428
- else
429
- #Lingo.error("DbmFile #{@dbm_name} nicht geöffnet")
430
- end
431
- end
432
-
433
- def closed?
434
- @dbm.nil? || @dbm.closed?
170
+ def _closed?
171
+ @db.closed?
435
172
  end
436
173
 
437
- def [](key)
438
- return if closed?
439
-
440
- if val = _get(key)
441
- # Äquvalenzklassen behandeln
442
- val.split(FLD_SEP).map { |v|
443
- v =~ INDEX_PATTERN ? _get(v) : v
444
- }.compact.join(FLD_SEP).split(FLD_SEP)
445
- end
446
- end
447
-
448
- def []=(key, val)
449
- return if closed?
450
-
451
- val += retrieve(key) if hit?(key)
452
-
453
- store(key, val = val.sort.uniq)
454
- _set(key, val.join(FLD_SEP))
174
+ def _set(key, val)
175
+ @db[key] = val
455
176
  end
456
177
 
457
- def set_source_file(filename)
458
- return if closed?
459
- @dbm[SYS_KEY] = source_key(Pathname.new(Lingo.find(:dict, filename)))
178
+ def _get(key)
179
+ @db[key]
460
180
  end
461
181
 
462
- private
463
-
464
- def _get(key)
465
- if val = @dbm[@crypter ? @crypter.digest(key) : key]
466
- val.encode!(ENC)
182
+ def _val(key)
183
+ if val = _get(@crypter ? @crypter.digest(key) : key)
184
+ val.force_encoding(ENC)
467
185
  @crypter ? @crypter.decode(key, val) : val
468
186
  end
469
187
  end
470
188
 
471
- def _set(key, val)
472
- key, val = @crypter.encode(key, val) if @crypter
473
- @dbm[key] = (val.length < 950) ? val : val[0, 950]
474
- end
475
-
476
- def source_key(src)
477
- [src.size, src.mtime].join(FLD_SEP)
478
- end
479
-
480
- end
481
-
482
- # Die Klasse Txt2DbConverter steuert die Konvertierung von Wörterbuch-Quelldateien in
483
- # Lingo-Datenbanken. Die Identifizierung der Quelldatei erfolgt über die ID
484
- # der Datei, so wie sie in der Sprachkonfigurationsdatei <tt>de.lang</tt> unter
485
- # <tt>language/dictionary/databases</tt> hinterlegt ist.
486
-
487
- class Txt2DbmConverter
189
+ def convert(verbose = @lingo.config.stderr.tty?)
190
+ src = Source.get(@config.fetch('txt-format', 'KeyValue'), @id, @lingo)
488
191
 
489
- def initialize(id, lingo, verbose = lingo.config.stderr.tty?)
490
- # Konfiguration der Datenbanken auslesen
491
- @config, @index = lingo.database_config(id), 0
192
+ if lex = @config['use-lex']
193
+ a, s = [{
194
+ 'source' => lex.split(STRING_SEPARATOR_RE),
195
+ 'mode' => @config['lex-mode']
196
+ }, @lingo], ' '
492
197
 
493
- # Objekt für Quelldatei erzeugen
494
- @format = @config.fetch( 'txt-format', 'KeyValue' ).downcase
495
- @source = case @format
496
- when 'singleword' then TxtFile_Singleword
497
- when 'keyvalue' then TxtFile_Keyvalue
498
- when 'wordclass' then TxtFile_Wordclass
499
- when 'multivalue' then TxtFile_Multivalue
500
- when 'multikey' then TxtFile_Multikey
501
- else
502
- Lingo.error("Unbekanntes Textformat '#{config['txt-format'].downcase}' bei '#{'language/dictionary/databases/' + id}'")
503
- end.new(id, lingo)
198
+ dic = Language::Dictionary.new(*a)
199
+ gra = Language::Grammar.new(*a)
504
200
 
505
- # Zielobjekt erzeugen
506
- @destination = DbmFile.new(id, lingo, false)
201
+ block = lambda { |form|
202
+ res = dic.find_word(form)
507
203
 
508
- # Ausgabesteuerung
509
- @progress = ShowProgress.new(@config['name'], verbose, lingo.config.stderr)
204
+ if res.unknown?
205
+ res = gra.find_compositum(form)
206
+ com = res.compo_form
207
+ end
510
208
 
511
- # Lexikalisierungen für Mehrwortgruppen vorbereiten
512
- lex_dic = @config['use-lex']
513
- lex_mod = @config['lex-mode']
514
-
515
- begin
516
- @lexicalize = true
517
- @dictionary = Dictionary.new({ 'source' => lex_dic.split(STRING_SEPERATOR_PATTERN), 'mode' => lex_mod }, lingo)
518
- @grammar = Grammar.new({ 'source' => lex_dic.split(STRING_SEPERATOR_PATTERN), 'mode' => lex_mod }, lingo)
519
- rescue RuntimeError
520
- Lingo.error("Auf das Wörterbuch (#{lex_dic}) für die Lexikalisierung der Mehrwortgruppen in (#{@config['name']}) konnte nicht zugegriffen werden")
521
- end if lex_dic
522
- end
523
-
524
- def convert
525
- @progress.start('convert', @source.size)
526
-
527
- @destination.open
528
- @destination.clear
209
+ com ? com.form : res.norm
210
+ }
211
+ end
529
212
 
530
- @source.each do |key, value|
531
- @progress.tick(@source.position)
213
+ ShowProgress.new(self, src.size, verbose) { |progress| create {
214
+ src.each { |key, val|
215
+ progress[src.position]
532
216
 
533
- # Behandle Mehrwortschlüssel
534
- if @lexicalize && key =~ / /
535
- # Schlüssel in Grundform wandeln
536
- gkey = key.split(' ').map do |form|
217
+ if key
218
+ key.chomp!('.')
537
219
 
538
- # => Wortform ohne Satzendepunkt benutzen
539
- wordform = form.gsub(/\.$/, '')
220
+ if lex && key.include?(s)
221
+ k = key.split(s).map!(&block).join(s)
540
222
 
541
- # => Wort suchen
542
- result = @dictionary.find_word(wordform)
223
+ c = k.count(s) + 1
224
+ self[k.split(s)[0, 3].join(s)] = ["#{KEY_REF}#{c}"] if c > 3
543
225
 
544
- # => Kompositum suchen, wenn Wort nicht erkannt
545
- if result.attr == WA_UNKNOWN
546
- result = @grammar.find_compositum(wordform)
547
- compo = result.compo_form
226
+ key, val = k, val.map { |v| v.start_with?('#') ? key + v : v }
548
227
  end
228
+ end
549
229
 
550
- compo ? compo.form : result.norm
551
- end.join(' ')
552
-
553
- skey = gkey.split
554
- # Zusatzschlüssel einfügen, wenn Anzahl Wörter > 3
555
- @destination[skey[0...3].join(' ')] = [KEY_REF + skey.size.to_s] if skey.size > 3
556
-
557
- value = value.map { |v| v =~ /^\043/ ? key + v : v }
558
- key = gkey
559
- end
560
-
561
- # Format Sonderbehandlungen
562
- key.gsub!(/\.$/, '') if key
563
- case @format
564
- when 'multivalue' # Äquvalenzklassen behandeln
565
- key = IDX_REF + @index.to_s
566
- @index += 1
567
- @destination[key] = value
568
- value.each { |v| @destination[v] = [key] }
569
- when 'multikey' # Äquvalenzklassen behandeln
570
- value.each { |v| @destination[v] = [key] }
571
- else
572
- @destination[key] = value
573
- end
230
+ src.set(self, key, val)
231
+ }
574
232
 
575
- end
576
-
577
- @destination.set_source_file(@config['name'])
578
- @destination.close
579
-
580
- @progress.stop('ok')
581
-
582
- self
233
+ uptodate!
234
+ } }
583
235
  end
584
236
 
585
237
  end