change IDS text read
[chise/ruby.git] / src / chise.rb
1 #!c:/ruby/bin/ruby.exe
2 # Ruby/UTF-2000 module by eto 2002-1114
3
4 require 'bdb'
5 require 'uconv'
6 require 'singleton'
7
8 $KCODE = 'u' #今のところこれ以外では動かない。String.splitが影響大。inspectも影響。
9 $debug = true #これはテスト用
10 #$stdout.binmode if $debug
11 $stdout.sync = true if $debug
12
13 class String
14   def to_a() self.split(//) end #$KCODEが設定されているので、UTF-8的な一文字づつがchにはいる
15   def each_char() to_a.each {|ch| yield ch } end
16   def char_length() to_a.length end
17   def char_at(n) to_a()[n] end
18   def char() Character.get(to_a[0]) end
19   def method_missing(mid, *args) char.method_missing(mid, *args) end
20   def char_id() char.char_id() end
21   def get_char_attribute(a) char.get_char_attribute(a) end
22   def ucs() char.ucs() end
23   def to_utf8()
24     return to_a.map {|ch|
25       ch.char.to_utf8
26     }.join('')
27   end
28
29   #put関係、[]関係は用意しないことにした。
30   def de_er!() #EntityReferenceを取り除く
31     return self unless self =~ Regexp.new(EntityReference::REGEXP_PART) #それらしいのが無ければ何もしない
32     er = "&"+$1+";"      
33     self.sub!(Regexp.new(Regexp.escape(er)), Character.new(er).mcs_utf8) #変換自体はCharacterにまかせる
34     return self.de_er! if self =~ Regexp.new(EntityReference::REGEXP_PART) #まだあったら再帰
35     return self
36   end
37   def de_er() return self.dup.de_er!; end
38
39   def map_char(block = Proc.new)
40     return unless block_given?
41     return self.to_a.map {|ch| (block.call(ch)).to_s }.join("")
42   end
43   def map_char!(block = Proc.new)
44     return unless block_given?
45     self.replace(self.map_char {|ch| block.call(ch)})
46   end
47   def inspect_all() map_char {|ch| ch.char.inspect_all } end
48   def inspect_x()   map_char {|ch| ch.char.inspect_x   } end
49
50   def decompose() map_char {|ch| ch.char.decompose } end
51   def decompose!() self.replace(self.decompose); self; end
52   def decompose_all(level=nil)
53     level = 0 if level.nil?
54     if 10 < level
55       p ['too many recursive', self] 
56       exit
57     end
58     de = self.decompose
59     return de.decompose_all(level+1) if de != self #なにか変化があったから再帰
60     de #もうこれ以上変化は無さそうだぞと。
61   end
62   def decompose_all!() self.replace(self.decompose_all); self; end
63
64   def find() #"日雲"→"曇"とかいう感じの操作
65     ar = []
66     length = char_length()
67     each_char {|ch|
68       char = ch.char
69       ar << char.ids_contained #その文字を含んでいる漢字のリスト
70     }
71     h = Hash.new(0)
72     ar.each {|list|
73       next if list.nil?
74       list.each_char {|ch|
75         h[ch] += 1
76       }
77     }
78     str = ""
79     h.each {|k, v|
80 #      p [k, v]
81       if length == v #全部に顔を出していたら
82         str += k
83       end
84     }
85 #    p str
86     str
87   end
88   def compose()
89     db = CodesysDB.instance
90     composed = db.get('ids', self)
91     return "" if composed.nil? #なかったよと。
92     return "" if composed.char_length == 0 #なにごと?
93     return composed if composed.char_length == 1
94     composed.each_char {|ch|
95       char = ch.char
96       return ch if char.has_attribute? #とりあえず最初にみつかったものを返すというヌルい仕様
97     }
98     return "" #attributeを持つものが一つも無かったら、""にする
99   end
100   def aggregate()
101 #selfである文字列をIDSだと仮定し、それを完全にcomposeしきらないで、
102 #その部分集合だけをとりだして、compose可能であればできるだけcomposeする。
103     tree = IDS_Tree.new(self)
104     return self if tree.depth <= 1 #sub_nodesが無い場合はここでさよなら
105     tree.sub_nodes.each {|node|
106       c = node.compose
107       next if c.nil? || c == ""
108 #      print "#{self}   #{node} #{c}\n"
109 #      p [self, node, c]
110       n = self.gsub(node, c)
111       return n.aggregate
112     }
113     return self #おきかえられるものがまったくなかったら、自分をかえす。
114   end
115 end
116
117 module CHISE
118   def windows?()
119     (RUBY_PLATFORM =~ /cygwin/ || RUBY_PLATFORM =~ /mswin32/ || RUBY_PLATFORM =~ /mingw32/)
120   end
121   module_function :windows?
122   if windows?()
123     DB_DIR = 'd:/work/chise/char-db' #この後に/sysmtem-char-id/ucsという感じに続く
124   else
125     DB_DIR = '/usr/local/lib/xemacs-21.4.10/i686-pc-linux/char-db' #この後に/sysmtem-char-id/ucsという感じに続く
126   end
127
128   class EntityReference #======================================================================
129     #状況によってどのERに変換するかが異なる可能性があるので、普通のclassとして実装したほうがいい?
130     CODESYS_TABLE = [
131       %w( chinese-big5-cdp      CDP- 4 X),
132       %w( ideograph-daikanwa    M-   5 d),
133       %w( ideograph-cbeta       CB   5 d),
134       %w( ideograph-gt          GT-  5 d),
135       %w( ideograph-gt-k        GT-K 5 d),
136       %w( japanese-jisx0208-1990 J90- 4 X),
137       %w( japanese-jisx0208     J83- 4 X),
138       %w( japanese-jisx0213-1   JX1- 4 X),
139       %w( japanese-jisx0213-2   JX2- 4 X),
140       %w( japanese-jisx0212     JSP- 4 X),
141       %w( japanese-jisx0208-1978 J78- 4 X),
142       %w( chinese-cns11643-1    C1-  4 X),
143       %w( chinese-cns11643-2    C2-  4 X),
144       %w( chinese-cns11643-3    C3-  4 X),
145       %w( chinese-cns11643-4    C4-  4 X),
146       %w( chinese-cns11643-5    C5-  4 X),
147       %w( chinese-cns11643-6    C6-  4 X),
148       %w( chinese-cns11643-7    C7-  4 X),
149       %w( korean-ksc5601        K0- 4 X),
150     ]
151     CODESYS_ORDER = %w(japanese chinese korean ideograph)
152     REGEXP_PART = "&([-+0-9A-Za-z]+);"
153     REGEXP_ALL = "^#{REGEXP_PART}$"
154
155     def self.match?(er) (er =~ Regexp.new(REGEXP_PART)) != nil end
156
157     def self.parse(er) #char_idをFIXNUMで返す
158       return "" unless er =~ Regexp.new(REGEXP_ALL) #なんか、間違ってる?
159       er = $1 #ついでに中身の部分を取り出す
160       return $1.hex if er =~ /^MCS-([0-9A-Fa-f]+)/ #MCS
161       return $1.hex if er =~ /^U[-+]?([0-9A-Fa-f]+)/ #Unicode直打ち
162
163       er.sub!(/^I-/, '') if er =~ /^I-/ #I-がついてるとどう違うのかはよくわからない
164       each_codesys {|codesys, er_prefix, keta, numtype| #p [codesys, er_prefix, keta, numtype]
165         numtyperegex = '\d' #if numtype == 'd'
166         numtyperegex = '[0-9A-Fa-f]' if numtype == 'X'
167         regexpstr = "^#{er_prefix}(#{numtyperegex}{#{keta},#{keta}})$"  #p regexpstr
168         if er =~ Regexp.new(regexpstr)
169           codestr = $1
170           code = codestr.to_i #if numtype == 'd'
171           code = codestr.hex if numtype == 'X'
172           char_id_u8 = EntityReference.get_database(codesys, code)
173           char_id_num = Character.parse_char_id(char_id_u8)
174           return char_id_num
175         end
176       }
177       return ""
178     end
179
180     def self.each_codesys()
181       CODESYS_ORDER.each {|lang|
182         CODESYS_TABLE.each {|codesys, er_prefix, keta, numtype| #普通こういう書き方はしない。ループ一個にする。
183           next unless codesys =~ lang
184           yield(codesys, er_prefix, keta, numtype)
185         }
186       }
187     end
188     def self.get_database(codesys, code)
189       c = CodesysDB.instance.get(codesys, code)
190       return c if c != nil
191       if codesys =~ /-jisx0208/
192         #return self.get_database("=jis-x0208", code) #再帰でどうだ?
193         c = CodesysDB.instance.get("=jis-x0208", code)
194         return c
195       end
196       return nil
197     end
198   end
199
200   class CharacterFactory #============================================文字オブジェクトの生成、cache
201     include Singleton
202     MAX = 10000
203     def initialize
204       @max = MAX
205       reset()
206     end
207     def get(char_id)
208       check_max()
209       n = Character.parse_char_id(char_id)
210       c = @chars[n]
211       @chars[n] = Character.new(n) if @chars[n] == nil
212       return @chars[n]
213     end
214     def reset()
215       @chars = nil
216       @chars = Hash.new
217       GC.start #ガーベージコレクション
218     end
219     def length() @chars.length; end
220     def check_max()
221       reset if @max < @chars.length #MAXを超えたらresetしてしまう。乱暴じゃがcacheなのでこれでいいのだ。
222     end
223   end
224
225   class Character #=============================================================== 文字オブジェクト
226     def initialize(char_id=nil)
227       @char_id = Character.parse_char_id(char_id)
228       @attributes = Hash.new
229       @check_all_database = false
230     end
231     attr_reader :char_id
232     def mcs_utf8() Character.u4itou8(@char_id) end
233
234     #----------------------------------------------------------------------
235     def self.get(char_id) CharacterFactory.instance.get(char_id) end #flyweightパターン
236
237     #----------------------------------------------------------------------
238     def get_char_attribute(a) # XEmacs UTF-2000互換API群
239       a.gsub!(/_/, '-') #underlineは-に置換
240       atr = @attributes[a]
241       return atr if atr != nil
242       atr = check_database(a)
243       @attributes[a] = atr if atr != nil
244       return get_char_attribute("=jis-x0208") if a =~ /jisx0208/
245       return @attributes[a]
246     end
247     def put_char_attribute(a,v)
248       a.gsub!(/_/, '-') #underlineは-に置換
249       @attributes[a] = v;
250       CharDB.instance.put(a, mcs_utf8(), v)
251     end
252     def char_attribute_alist() check_all_database(); @attributes; end
253     def char_attribute_list()  check_all_database(); @attributes.keys; end
254     alias [] get_char_attribute  #その略称
255     alias []= put_char_attribute
256     alias alist char_attribute_alist
257     alias list  char_attribute_list
258
259     def method_missing(mid, *args) #参考:ostruct.rb
260       mname = mid.id2name
261       return get_char_attribute(mname) if args.length == 0
262       put_char_attribute(mname.chop, args[0]) if mname =~ /=$/ #代入
263     end
264
265     def has_attribute?() #意味のあるattributeを持ってますか?
266       keys = list
267       keys.delete_if {|k|
268         k =~ /ids/
269       }
270       return (keys.length != 0)
271     end
272
273     #----------------------------------------------------------------------
274     def ==(ch)
275       return false if ch == nil
276       return false unless ch.is_a? Character
277       self.char_id == ch.char_id
278     end
279
280     #----------------------------------------------------------------------
281     def self.parse_char_id(char_id) #FIXNUMを返す
282       return nil if char_id == nil
283       if char_id.is_a?(Numeric) #p [char_id]
284         char_id = 0x80000000 + char_id if char_id < 0  #補数表現
285         return char_id.to_i
286       elsif char_id.is_a?(String)
287         return char_id.to_i if char_id =~ /^\d+$/ #文字列による数字だったら数値化してreturn
288         return EntityReference.parse(char_id) if char_id =~ Regexp.new(EntityReference::REGEXP_ALL) #実体参照?
289         char_id.sub!(/^\?/, '') if char_id =~ /^\?/ #もし先頭に?がついていたら削除
290         #このへん本当はもっとちゃんとチェックするべし
291         u4 = Uconv.u8tou4(char_id) #UCS-4文字列に変換
292         return Character.u4tou4i(u4) #UCS-4数値にしてreturn
293       else
294         raise ArgumentError, "unknown object for char_id", caller(1)
295       end
296     end
297     def self.u4tou4i(u4)
298       return 0 if u4 == nil || u4 == ""
299       return (u4[3] << 24 | u4[2] << 16 | u4[1] << 8 | u4[0]) #UCS-4数値にしてreturn
300     end
301     def self.u4itou4(num)
302       return "" if num == nil
303       return sprintf("%c%c%c%c", num&0xff, (num >> 8)&0xff, (num >> 16)&0xff, (num >> 24)&0xff) #UCS-4数値を文字列にしてreturn
304     end
305     def self.u4itou8(char_id) #ucsの数値を受けとり、UTF-8の文字一文字を返す
306       begin
307         u4 = Character.u4itou4(char_id)
308         u8 = Uconv.u4tou8(u4)
309         return u8
310       rescue
311         #raise ArgumentError, "invalid char_id (#{char_id})", caller(1)
312         #print "error\n"
313         return ""
314       end
315     end
316
317     #----------------------------------------------------------------------
318     def check_database(a)
319       db = CharDB.instance
320       u8 = mcs_utf8()
321       v = db.get(a, u8) #u8で表される文字のaアトリビュートを調べる。
322       return v
323     end
324     def check_all_database() #現在の@char_idから、文字データベースを参照する
325       return if @check_all_database
326       return if @char_id == nil
327       db = CharDB.instance
328       u8 = mcs_utf8()
329       atrs = db.get_all(u8) #u8で表される文字のアトリビュートを全部持ってこい
330       atrs.each {|a,v|
331         @attributes[a] = v #とかいう感じで代入するのでええかな?
332       }
333       @check_all_database = true #重い処理なので一応checkする
334     end
335
336     #----------------------------------------------------------------------
337     def ucs()      #p 'ucs'
338       #ar = %w{ucs ucs-big5 ucs-cdp ucs-cns ucs-jis ucs-ks =>ucs =>ucs* =>ucs-jis}
339       ar = %w{ucs ucs-jis ucs-big5 ucs-cdp ucs-cns ucs-ks =>ucs =>ucs* =>ucs-jis}
340       #並び順は恣意的で、ucs-jisを先に出している。本来はこれも指定できるようにするべき。
341       ar.each {|a|      #p [a]
342          u = get_char_attribute(a)
343          return u if u != nil
344        }
345        return nil
346      end
347
348      #----------------------------------------------------------------------CCS関係
349      def to_utf8() Uconv.u4tou8(Character.u4itou4(ucs())) end #UTF8文字列を返す
350      #alias to_s to_utf8
351      alias to_s mcs_utf8
352
353      #----------------------------------------------------------------------
354      def to_er(codesys=nil) #実体参照を返す、希望するcodesysが引数(未実装)
355        return "" if @char_id == nil
356        return sprintf("&U+%04X;", @char_id) if @char_id <= 0xffff
357        return sprintf("&U-%05X;", @char_id) if @char_id <= 0xfffff
358        EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
359          code = self[codesys]
360          next if code == nil
361          return sprintf("&#{er_prefix}%0#{keta}#{numtype};", code)
362        }
363        return sprintf("&MCS-%08X;", @char_id) #本当はこれは無しにしたい
364      end
365      def to_er_list()
366        ar = []
367        EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
368          er = to_er(codesys)
369          ar << er if er != nil
370        }
371        ar
372      end
373
374      def inspect_x()
375        return "<>" if @char_id == nil
376        ar = [to_utf8(), to_er().sub(/^&/,'').chop]
377        "<"+ar.join(',')+">"
378      end
379      alias inspect inspect_x
380      def inspect_all_codesys()
381        #to_erを全てのcodesysにおいて実行する。その結果をコンパクトにまとめる
382      end
383      def inspect_all()
384        ar = [inspect.chop]
385        alist.to_a.sort.each {|a, v| ar << "#{a}:#{v}" }
386        return ar.join(',')+">"
387      end
388      def get_attributes()
389        str = ""
390        alist.to_a.sort.each {|a, v|
391          str += "#{a}: #{v}\n"
392        }
393        str
394      end
395
396      #----------------------------------------------------------------------IDS関係
397      def decompose
398        k = self.to_s
399 #       idss = self['ids']
400 #       return idss if idss
401        idss = self['ids-aggregated']
402        return idss if idss != nil && 0 < idss.length && k != idss
403        idss = self['ids']
404        return idss if idss != nil && 0 < idss.length && k != idss
405        return k
406 #       return k if idss.nil? || idss.length == 0 || k == idss
407 #       if idss.char_length == 2
408 #       p ['What???', k, idss, k.inspect_all]
409 #        #return idssx[1] #二個目だけ返すとか?
410 #        return k #IDSに展開する方法が無いと。
411 #       end
412 #       return k if k == idss
413 #       if idss.include?(k) #<C5-4C4D><C6-4A37>この二文字のBUG対策
414 #        #return idss.sub(k, '')
415 #        return k #IDSに展開する方法が無いと。
416 #       end
417 #       return idss
418      end
419      def is_ids?() 0x2ff0 <= @char_id && @char_id <= 0x2fff end
420      def ids_operator_argc()
421        return 0 unless is_ids?
422        return 3 if @char_id == 0x2ff2 || @char_id == 0x2ff3
423        return 2
424      end
425    end
426
427    class DBS #======================================================================複数のDBを集めたclass
428    end
429
430    class ADB < BDB::Hash #======================================================================一つのDB
431      def initialize(*args)
432        super
433        @modified = false
434        at_exit {
435          if @modified
436            self.close #これがないと、うまくデータベースがセーブされないのです。
437          end
438        }
439      end
440      def self.open_create(filename)
441        ADB.open(filename, nil, BDB::CREATE | BDB::EXCL) #上書きはしない
442      end
443      def mykey(key)
444        if key.is_a?(String)
445          if key.char_length == 1
446            return '?'+key  #Stringだったら引く前に?を足す
447          end
448        end
449        #key = key.to_s if key.is_a?(Numeric) #NumberだったらStringにする。
450        #ここで && key ! =~ /^\?/ をいれると、?自身を検索できなくなってしまう。
451        return key
452      end
453      def myvalue(v)
454        return v if v == nil
455        return v.to_i if v =~ /^\d+$/ #数字だったらここで変換しておく
456        return v.sub(/^\?/, '') if v =~ /^\?/ #冒頭の?は取り除く
457        return $1 if v =~ /^"(.+)"$/ #最初と最後に"がついていたら、取り除く
458        #p ['get', v, t, key, db]
459        #return parse_sexp(v) if v =~ /^\(.+\)$/ #最初と最後が()の時は、S式にparseする
460        return v #それ以外って何?
461      end
462      def myget(key) #keyキーを引いて返す
463        key = mykey(key)
464        v = get(key) #存在しなかったらnilを返すことになる
465        return myvalue(v)
466      end
467      def myput(key, v) #keyにvをいれる
468        key = mykey(key)
469        put(key, v) #putする
470        @modified = true
471      end
472    end
473
474    class DB #======================================================= データベース群のabstract class
475      def self.unix_to_win(unix) #Windowsファイル名制限のため、変換する
476        win = unix.gsub(/</, '(')
477        win.gsub!(/>/, ')')
478        win.gsub!(/\*/, '+')
479        win.gsub!(/\?/, '!')
480        return win
481      end
482      def self.win_to_unix(win)
483        unix = win.gsub(%r|\)|, '>')
484        unix.gsub!(%r|\(|, '<')
485        unix.gsub!(%r|!|, '?')
486        unix.gsub!(%r|\+|, '*')
487        return unix
488      end
489 #     def windows?() DB.windows?() end
490      def get_filename(t)
491        return @pre + DB.unix_to_win(t) + @post if windows?
492        return @pre + t + @post
493      end
494      def get_dirname(t) File.dirname(get_filename(t)) end
495      def open_dbs()
496        @dbs = Hash.new
497        keys = find_keys()
498        keys.each {|key| open_db(key) }
499      end
500      def find_keys()
501        files = []
502        Dir.glob(@glob){|f|
503          next if ! File.file?(f)
504          next if f =~ /.txt$/
505          files << f
506        }
507        keys = []
508        files.each {|f|
509          t = DB.win_to_unix(f)
510          t.sub!(%r|^#{@pre}|, '')
511          t.sub!(%r|#{@post}$|, '') if @post != ""
512          keys << t
513        }
514        return keys
515        #return keys.sort
516      end
517      def close_db(t)
518        db = get(t)
519        return nil if db.nil?
520        db.close
521        @dbs.delete(t)
522      end
523      def open_db(t)
524        return nil if get(t) #すでにopenしていたら再openはしない。
525        begin
526          bdb = ADB.open(get_filename(t), nil, 0)
527          @dbs[t] = bdb if bdb != nil
528        rescue
529          p ["open error", get_filename(t)]; return nil
530        end
531        return true
532      end
533      def make_db(t, h=nil) #tという名前でhという中身のデータベースを作る
534        return nil if get(t) #すでにある場合はreturn
535        Dir.mkdir(get_dirname(t)) unless FileTest.exist?(get_dirname(t))
536        db = nil
537        begin
538          db = ADB.open_create(get_filename(t)) #上書きはしない
539          if h != nil
540            h.each {|k, v|
541              k = '?'+k if k.is_a?(String)
542              db[k] = v
543            }
544          end
545          db.close
546        rescue
547          p ["make error", get_filename(t)]; return nil
548        end
549        return true
550      end
551      def make_db_no_question_mark(t, h=nil) #tという名前でhという中身のデータベースを作る
552        return nil if get(t) #すでにある場合はreturn
553        Dir.mkdir(get_dirname(t)) unless FileTest.exist?(get_dirname(t))
554        db = nil
555        begin
556          db = ADB.open_create(get_filename(t)) #上書きはしない
557          if h != nil
558            h.each {|k, v|
559 #            k = '?'+k if k.is_a?(String)
560              db[k] = v
561            }
562          end
563          db.close
564        rescue
565          p ["make error", get_filename(t)]; return nil
566        end
567        return true
568      end
569      def remove_db(t) #tという名前のデータベースを消去する
570        db = get(t)
571        if db
572          db.close
573          @dbs.delete(t)
574        end
575        begin
576          File.unlink(get_filename(t)) if FileTest.file?(get_filename(t))
577        rescue
578          p ["unlink error", get_filename(t)]; return nil
579        end
580        dn = get_dirname(t)
581        Dir.rmdir(dn) if FileTest.directory?(dn) && Dir.entries(dn).length <= 2 #空directoryだったら消す
582        return true
583      end
584      def to_num(s)
585        return s.to_i if s =~ /^\d+$/
586        return s
587      end
588      def dump_db(t)
589        db = get(t)
590        return nil unless db
591        file = get_filename(t)
592        open("#{file}.txt", "w"){|out|
593 #        out.binmode.sync = true
594          ar = db.to_a
595          ar.map! {|k, v| [to_num(k), to_num(v)] }
596          ar.sort.each {|k, v|
597            out.printf("%s\t%s\n", k, v)
598          }
599        }
600        return true
601      end
602      def each_db()  @dbs.to_a.sort.each {|t, db| yield(t, db) } end
603      def dump_all()  each_db {|t, db| dump_db(t) } end
604      def close_all() each_db {|t, db| db.close   } end
605      def keys() @dbs.keys end
606      def each(t)
607        return unless block_given?
608        db = @dbs[t]
609        return nil unless db
610        db.each {|k, v|
611          k = to_num(k)
612          v = to_num(v)
613          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
614          vv = get(t, k) #p ['each', t, k, v, vv]
615          yield(k, vv)
616        }
617      end
618      def each_sort(t)
619        return unless block_given?
620        db = @dbs[t]
621        return nil unless db
622        ar = db.to_a
623        ar.map! {|k, v| [to_num(k), to_num(v)] }
624        ar.sort.each {|k, v|
625          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
626          vv = get(t, k) #p ['each', t, k, v, vv]
627          yield(k, vv)
628        }
629      end
630      #----------------------------------------------------------------------
631      def get(t, key=nil) #tというデータベースのkeyキーを引いて返す
632        db = @dbs[t]
633        return db if key.nil?
634        return nil unless db
635        return db.myget(key)
636      end
637      def put(t, key, v) #tというデータベースのkeyにvをいれる
638        db = @dbs[t]
639        if db == nil
640          db = make_db(t) 
641          db = open_db(t) 
642          db = @dbs[t]
643        end
644        db.myput(key, v) #putする
645      end
646    end
647
648    class CharDB < DB #------------------------------------ MCS-UTF8をキーとした属性へのデータベース
649      include Singleton
650      def initialize()
651        super
652        @glob, @pre, @post = "#{DB_DIR}/system-char-id/*", "#{DB_DIR}/system-char-id/", ""
653        open_dbs()
654      end
655      def get_all(u8) #全データベースのu8キーを引いてHashにまとめて返す
656        atrs = Hash.new
657        @dbs.each {|t, db|
658          v = get(t, u8)
659          atrs[t] = v if v != nil
660        }
661        return atrs
662      end
663    end
664
665    class CodesysDB < DB #----------------------------------------------------------------------
666      include Singleton
667      def initialize()
668        super
669        @glob, @pre, @post = "#{DB_DIR}/*/system-char-id", "#{DB_DIR}/", "/system-char-id"
670        open_dbs()
671      end
672      #def keys() @dbs.keys.sort end #どんなCodesysの情報を持っているかの一覧
673      def keys() @dbs.keys end #どんなCodesysの情報を持っているかの一覧
674      def get_codesys(t)
675        db = get(t)
676        return nil unless db
677        return Codesys.new(t)
678      end
679    end
680
681    class Codesys < DB #======================================================================
682      def initialize(name)
683 #       super
684        @name = name
685        @dbs = CodesysDB.instance
686      end
687      def keys() #どんなコードポイントの情報を持っているかの一覧
688        ks = @dbs.get(@name).keys
689        if @name =~ /jisx0208/ #特別処理
690          n = @dbs.get('=jis-x0208').keys 
691          #       p ['keys', @name, ks, n]
692          ks += n
693        end
694        ks.map! {|k| to_num(k) }
695        ks
696      end
697      def get(key)
698        v = @dbs.get(@name, key)
699        return v if v
700        if @name =~ /jisx0208/ #jisx0208が含まれている場合だけ特別処理する
701          return @dbs.get('=jis-x0208', key)
702        end
703        return nil
704      end
705      def each()
706        return unless block_given?
707        db = @dbs.get(@name)
708        return nil unless db
709        db.each {|k, v|
710          k = to_num(k)
711          v = to_num(v)
712          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
713          vv = @dbs.get(@name, k)        #p ['each', t, k, v, vv]
714          yield(k, vv)
715        }
716      end
717      def each_sort()
718        return unless block_given?
719        db = @dbs.get(@name)
720        return nil unless db
721        ar = db.to_a
722        ar.map! {|k, v| [to_num(k), to_num(v)] }
723        ar.sort.each {|k, v|
724          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
725          vv = @dbs.get(@name, k)        #p ['each', t, k, v, vv]
726          yield(k, vv)
727        }
728      end
729    end
730
731   class IDS_TEXT_DB < DB #======================================================================
732     include Singleton
733     if CHISE.windows?()
734       IDS_DB_DIR = 'd:/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
735     else
736       IDS_DB_DIR = '/home/eto/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
737     end
738     IDS_LIST = "
739 IDS-UCS-Basic.txt
740 #IDS-UCS-Compat-Supplement.txt
741 #IDS-UCS-Compat.txt
742 IDS-UCS-Ext-A.txt
743 IDS-UCS-Ext-B-1.txt
744 IDS-UCS-Ext-B-2.txt
745 IDS-UCS-Ext-B-3.txt
746 IDS-UCS-Ext-B-4.txt
747 IDS-UCS-Ext-B-5.txt
748 IDS-UCS-Ext-B-6.txt
749 IDS-JIS-X0208-1990.txt
750 IDS-Daikanwa-01.txt
751 IDS-Daikanwa-02.txt
752 IDS-Daikanwa-03.txt
753 IDS-Daikanwa-04.txt
754 IDS-Daikanwa-05.txt
755 IDS-Daikanwa-06.txt
756 IDS-Daikanwa-07.txt
757 IDS-Daikanwa-08.txt
758 IDS-Daikanwa-09.txt
759 IDS-Daikanwa-10.txt
760 IDS-Daikanwa-11.txt
761 IDS-Daikanwa-12.txt
762 IDS-Daikanwa-dx.txt
763 IDS-Daikanwa-ho.txt
764 IDS-CBETA.txt
765 ".split
766     def initialize()
767       super
768       @ids_list = IDS_LIST
769       @chars = []
770       @glob, @pre, @post = "#{IDS_DB_DIR}/db/*", "#{IDS_DB_DIR}/db/", ""
771       dir = File.dirname(@pre)
772       Dir.mkdir(dir) unless FileTest.exist?(dir)
773       open_dbs()
774     end
775     def each_file()
776       return unless block_given?
777       @ids_list.each {|file|
778         next if file =~ /^#/
779         yield(IDS_DB_DIR+file)
780       }
781     end
782     def each_line(file)
783       open(file){|f|
784         while line = f.gets
785           next if line =~ /^;/ #コメントはとばす
786           line.chomp!
787           code, char, ids = line.split
788           yield(code, char, ids)
789         end
790       }
791     end
792     def dump_text_all
793       each_file {|file|
794         dir = File.dirname(file) + '/../ids-new/'
795         Dir.mkdir(dir) if ! FileTest.directory?(dir)
796         newfile = dir + File.basename(file)
797         p [file, newfile]
798         open(newfile, "w"){|out|
799           out.binmode.sync = true
800           each_line(file){|code, ch, ids|
801             char = Character.get(ch)
802             ids = char.decompose
803             out.print "#{code}  #{ch}   #{ids}\n"
804           }
805         }
806       }
807     end
808     def make_ids_error
809       each_file {|file|
810         dir = File.dirname(file) + '/../ids-error'
811         Dir.mkdir(dir) unless FileTest.exist?(dir)
812         errfile = dir + '/' + File.basename(file)
813 #       p [file, errfile]
814         open(errfile, "w"){|out|
815           out.binmode.sync = true
816           each_line(file){|code, ch, ids|
817             char = Character.get(ch)
818             ids_error = char['ids-error']
819             next if ids_error.nil?
820             out.print "#{code}  #{ch}   #{ids}  #{ids_error}\n"
821           }
822         }
823       }
824     end
825   end
826
827   class IDS_DB < DB #======================================================================BDB化したIDS DBを扱う
828     include Singleton
829     def initialize
830       @dbs = CharDB.instance
831     end
832     def make_ids_db
833       db = IDS_TEXT_DB.instance
834       db.each_file {|file|
835         @char_counter = 0
836         @same_ids_counter = 0
837         @good_ids_counter = 0
838         @conflict_ids_counter = 0
839         db.each_line(file){|code, ch, ids|
840           @char_counter += 1
841
842           ids = "" if ids == nil
843           next if ids == "" #IDSが定義されていない場合は、さっくりと無視するべしよ。
844
845           charimg = Character.get(ch) #実体参照である可能性がある
846
847           next if code =~ /'$/ || code =~ /"$/ #大漢和番号のダッシュ付きは無視する
848           char = Character.get("&"+code+";") #code表記を元に実体参照を作って解釈する
849           if char.nil? || char.to_s == "" #うまく文字にならなかった
850             print "char == null #{char.inspect} #{code} #{ch}   #{ids}\n" unless code =~ /^M-/ || code =~ /^CB/
851             #大漢和、CBETA以外の場合は、エラーメッセージ。
852             next
853           end
854           if char != charimg #code表記と文字が一致していない?
855             unless code =~ /^M-/ || code =~ /^MH-/ || code =~ /^CB/ #食い違っていて当然であるので何もしない
856               print "unknown char       #{char.inspect} #{code} #{ch}   #{ids}\n"
857               next #それ以外の場合はエラーメッセージをだして、次へ。
858             end
859           end
860           next if !char.has_attribute? #isolated characterはまぎれこませない。
861
862           ids.de_er! #実体参照を解除する
863           next if ids == char.to_s #もし文字とまったく一緒なら、意味が無いので情報を持たない
864           next if ids.char_length == 1
865
866           idstree = IDS_Tree.new(ids)
867           c = idstree.check_integrity
868           c = "contains self" if ids.include?(char.to_s)
869           if c #ちょっとでもエラーがある場合は、
870             char['ids-error'] = c #エラーを記録して、データとしては保持しない
871             next
872           end
873
874           if char['ids'].nil? || char['ids'] == "" #元々IDSが無かった場合は、
875             char['ids'] = ids #普通に代入すればそれでいいです。
876             @good_ids_counter += 1
877           else #しかしいままでにすでにIDSが定義されていた場合は?
878             if char['ids'] == ids #新しいIDSと古いIDSが完全に一致するなら無視しましょう。
879               @same_ids_counter += 1
880             else #しかしいままでのIDSと新しいIDSが食い違った場合は?
881               @conflict_ids_counter += 1
882 #             print "conflict   #{char.inspect} #{code} #{ids}  #{char['ids']}\n"
883             end
884           end
885         }
886         print "#{file}  #{@char_counter}        #{@same_ids_counter}    #{@conflict_ids_counter}        #{@good_ids_counter}\n"
887         CharacterFactory.instance.reset()
888       }
889       @dbs.dump_db('ids-error') #テキスト化する
890       @dbs.dump_db('ids') #テキスト化する
891     end
892     def make_ids_reverse
893       h = Hash.new
894       @dbs.each('ids') {|k, v|
895         char = k.char
896         ids = char.decompose
897         h[ids] = "" if h[ids].nil?
898         h[ids] += k #追加する
899       }
900       h.each {|k, v|
901         h[k] = char_sort(v) #文字の順番を、よく使うっぽいものからの順番にする
902       }
903       h.delete_if {|k, v| #h[k]が""になる可能性もあるが、それはkeyとして入れないことにする。
904         v == ""
905       }
906       print "length     #{h.length}\n"
907       cdb = CodesysDB.instance
908       cdb.make_db_no_question_mark('ids', h)
909       p ['make db no q mark done.']
910       cdb.open_db('ids') #これが無いと、dump_dbされません。
911       cdb.dump_db('ids')
912       p ['dump_db done.']
913     end
914     def char_sort(composed)
915       return composed if composed.char_length == 1
916       ar = composed.to_a
917       arorg = ar.dup
918       ar2 = []
919       ar.dup.each {|ch|
920         char = ch.char
921         if char.char_id < 0xfffff #Unicodeっぽい?
922           ar2 << ch
923           ar.delete(ch)
924         end
925       }
926       if 0 < ar.length
927         EntityReference.each_codesys{|codesys, er_prefix, keta, numtype|
928           ar.each {|ch|
929             char = ch.char
930             v = char[codesys]
931 #           p [codesys, v] if v
932             if v #EntityReferenceの順番に準拠する。
933               ar2 << ch
934               ar.delete(ch)
935             end
936           }
937         }
938       end
939       if 0 < ar.length
940 #       p ['yokuwakaran character', ar, ar[0].inspect_all, arorg]
941         EntityReference.each_codesys{|codesys, er_prefix, keta, numtype|
942           ar.dup.each {|ch|
943             char = ch.char
944             v = char[codesys]
945 #           p [codesys, v] if v
946           }
947         }
948       end
949       return ar2.join("")
950     end
951     def dump_ids_duplicated
952       open('ids-duplicated.txt', 'w'){|out|
953         #out.binmode
954         CodesysDB.instance.each('ids') {|k, v|
955           if v.nil?
956             out.print "nil      #{k}    #{v}\n"
957             next
958           end
959           n = v.char_length
960           next if n == 1
961           out.print "#{n}       #{k}    #{v}"
962           v.each_char {|ch|
963             char = ch.char
964             out.print " #{char.inspect}"
965           }
966           out.print "\n"
967         }
968       }
969     end
970     def make_ids_aggregated
971       @dbs.each('ids') {|k, v|
972         char = k.char
973         ids = char.decompose
974         ag = ids.aggregate
975         char['ids-aggregated'] = ag
976       }
977       @dbs.dump_db('ids-aggregated')
978     end
979     def dump_ids_aggregated
980       open('ids-aggregated.txt', 'w'){|out|
981         #out.binmode
982         @dbs.each('ids') {|k, v|
983           char = k.char
984           ids = char['ids']
985           ag  = char['ids-aggregated']
986           out.print "#{char.to_s}       #{ag}   #{ids}\n" if ids != ag
987         }
988       }
989     end
990     def make_ids_parts
991       @dbs.each('ids') {|k, v|
992         char = k.char
993         pids = char.to_s
994         ar = []
995         counter = 0
996         loop {
997           ids = pids.decompose
998           break if ids == pids #これ以上分割できないようだったら終了〜。
999           ar += ids.to_a
1000           counter += 1
1001           p [char.to_s, pids, ids, ar] if 10 < counter #これは何かおかしいぞと
1002           pids = ids
1003         }
1004         ar.sort!
1005         ar.uniq!
1006 #やっぱりIDS文字も加えることにする. by eto 2003-02-05
1007 #       ar.delete_if {|ch|
1008 #         ch.char.is_ids? #IDS文字はまぎれこませない。
1009 #       }
1010         str = ar.join('')
1011         char['ids-parts'] = str
1012       }
1013       @dbs.dump_db('ids-parts')
1014     end
1015     def make_ids_contained
1016       h = Hash.new
1017       @dbs.each('ids-parts') {|k, v|
1018         char = k.char
1019         parts = char.ids_parts
1020         parts.each_char {|ch|
1021 #         part = ch.char
1022           h[ch] = [] if h[ch].nil?
1023           h[ch] << k
1024 #         h[ch] += k
1025 #         part['ids-contained'] = "" if part['ids-contained'].nil?
1026 #         part['ids-contained'] += k
1027         }
1028       }
1029       h.each {|k, v|
1030         char = k.char
1031         v.sort!
1032         char['ids-contained'] = v.join('')
1033         
1034       }
1035       @dbs.dump_db('ids-contained')
1036     end
1037     def make_ids_decomposed
1038       @dbs.each('ids') {|k, v|
1039         char = k.char
1040         de= char.decompose_all
1041         char['ids-decomposed'] = de
1042       }
1043       @dbs.dump_db('ids-decomposed')
1044     end
1045   end
1046
1047   class Node < Array #=======================================================木構造の中の一つの枝
1048     def initialize(nodeleaf=nil, nodenum=nil)
1049       super()
1050       @nodeleaf = nodeleaf
1051       @nodenum = nodenum
1052       if @nodeleaf
1053         original_add(@nodeleaf)
1054       end
1055     end
1056     attr_reader :nodenum
1057     alias original_add <<
1058     private :original_add
1059     def <<(obj)
1060       original_add(obj)
1061       @nodenum -= 1 if @nodenum
1062     end
1063     def nodes
1064       ar = []
1065       ar << self.to_s
1066       self.each {|n|
1067         ar += n.nodes if n.is_a? Node
1068       }
1069       return ar
1070     end
1071   end
1072
1073   class Tree #======================================================================木構造を扱う
1074     def initialize()
1075       @root = Node.new()
1076       @stack = [@root]
1077       @leafnum = 0
1078       @depth = 1 #stackの深さが最大になったところの値、木構造が無いときは1となる
1079     end
1080     def depth() @depth - 1 end
1081     def add_node(nodeleaf=nil, nodenum=nil) #枝を追加
1082       new_node = Node.new(nodeleaf, nodenum)
1083       @stack.last << new_node
1084       @stack << new_node
1085       if @depth < @stack.length
1086         @depth = @stack.length
1087       end
1088       self
1089     end
1090     def end_node() #この枝は終り
1091       @stack.pop
1092       self
1093     end
1094     def add_leaf(a) #葉を追加
1095       @stack.last << a
1096       end_check()
1097       self
1098     end
1099     def end_check()
1100       n = @stack.last.nodenum
1101       if n && n == 0
1102         end_node()
1103         end_check() #再帰
1104       end
1105     end
1106     def check_integrity
1107       n = @stack.last.nodenum
1108       return nil if @root.length == 0 #no tree is good tree
1109       return "unmatch leaves" if n && n != 0
1110       return "extra nodes" if @root.first.is_a?(Node) && @root.length != 1
1111       return "extra leaves" if @root.length != 1
1112       return nil
1113     end
1114     def nodes
1115       r = @root.nodes
1116       r.shift
1117       r
1118     end
1119     def sub_nodes
1120       r = nodes
1121       r.shift
1122       r
1123     end
1124     def to_s()    @root.to_s    end
1125     def inspect() @root.inspect end
1126   end
1127
1128   class IDS_Tree < Tree #======================================================================
1129     def initialize(str)
1130       @str = str
1131       super()
1132       parse()
1133     end
1134     def parse()
1135       @str.each_char {|ch|
1136         char = Character.new(ch)
1137         if is_ids?(char)
1138           add_node(char, ids_operator_argc(char))
1139         else
1140           add_leaf(char)
1141         end
1142       }
1143     end
1144     def is_ids?(obj)
1145       return true if "+*".include?(obj.to_s) #テスト用ですかね
1146       return true if obj.is_ids?
1147       return false
1148     end
1149     def ids_operator_argc(obj)
1150       return obj.ids_operator_argc if 0 < obj.ids_operator_argc
1151       return 2 #テスト用ってことで
1152     end
1153     def check_integrity
1154       r = super
1155       return r if r #不完全がすでにわかっているならreturn
1156       return "contains ques" if @str =~ /\?/ #?が含まれている?
1157       return nil
1158     end
1159
1160   end
1161
1162   class IDS #======================================================================IDSそのものを扱うclass
1163     def initialize(str) #IDS文字列をうけとる。
1164       @str = str
1165     end
1166     def parse
1167     end
1168     def parse_x #柔軟型のParse. IDSキャラクターが前にきてなくてもよい。などなど。
1169     end
1170   end
1171
1172   class Counter #======================================================================
1173     #使い方
1174     #counter = Counter.new(50) { exit }
1175     #counter.count
1176     def initialize(max)
1177       @max = max
1178       @count = 0
1179       @proc = proc
1180     end
1181     def count
1182       @count += 1
1183       if @max <= @count
1184         @proc.call
1185       end
1186     end
1187   end
1188
1189 end
1190
1191 #----------------------------------------------------------------------終了