f1f991dc5e0d3cc9d74f037ad80900c73ee74e8f
[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 #      if er =~ /^U[-+]?([0-9A-Fa-f]+)/ #Unicode直打ち
162       if er =~ /^U[-+]?([0-9A-Fa-f]+)/ || er =~ /^#([0-9A-Fa-f]+)/ #Unicode直打ち
163         return $1.hex 
164       end
165
166       er.sub!(/^I-/, '') if er =~ /^I-/ #I-がついてるとどう違うのかはよくわからない
167       each_codesys {|codesys, er_prefix, keta, numtype| #p [codesys, er_prefix, keta, numtype]
168         numtyperegex = '\d' #if numtype == 'd'
169         numtyperegex = '[0-9A-Fa-f]' if numtype == 'X'
170         regexpstr = "^#{er_prefix}(#{numtyperegex}{#{keta},#{keta}})$"  #p regexpstr
171         if er =~ Regexp.new(regexpstr)
172           codestr = $1
173           code = codestr.to_i #if numtype == 'd'
174           code = codestr.hex if numtype == 'X'
175           char_id_u8 = EntityReference.get_database(codesys, code)
176           char_id_num = Character.parse_char_id(char_id_u8)
177           return char_id_num
178         end
179       }
180       return ""
181     end
182
183     def self.each_codesys()
184       CODESYS_ORDER.each {|lang|
185         CODESYS_TABLE.each {|codesys, er_prefix, keta, numtype| #普通こういう書き方はしない。ループ一個にする。
186           next unless codesys =~ lang
187           yield(codesys, er_prefix, keta, numtype)
188         }
189       }
190     end
191     def self.get_database(codesys, code)
192       c = CodesysDB.instance.get(codesys, code)
193       return c if c != nil
194       if codesys =~ /-jisx0208/
195         #return self.get_database("=jis-x0208", code) #再帰でどうだ?
196         c = CodesysDB.instance.get("=jis-x0208", code)
197         return c
198       end
199       return nil
200     end
201   end
202
203   class CharacterFactory #============================================文字オブジェクトの生成、cache
204     include Singleton
205     MAX = 10000
206     def initialize
207       @max = MAX
208       reset()
209     end
210     def get(char_id)
211       check_max()
212       n = Character.parse_char_id(char_id)
213       c = @chars[n]
214       @chars[n] = Character.new(n) if @chars[n] == nil
215       return @chars[n]
216     end
217     def reset()
218       @chars = nil
219       @chars = Hash.new
220       GC.start #ガーベージコレクション
221     end
222     def length() @chars.length; end
223     def check_max()
224       reset if @max < @chars.length #MAXを超えたらresetしてしまう。乱暴じゃがcacheなのでこれでいいのだ。
225     end
226   end
227
228   class Character #=============================================================== 文字オブジェクト
229     BASIC_KANJI = "人子女母父王口耳手足力目首毛心犬牛鳥貝角羽虫馬魚羊肉皮米竹木麦豆山川雨風水土石金田穴日月火音糸刀舟門戸衣矢弓車皿一二三四五六七八九十百千万寸尺上中下本玉立回食行止交向歩考入示走生出来書言大小白青多少高長"
230     def is_basic_kanji?
231       BASIC_KANJI.include?(self.to_s)
232     end
233
234     def initialize(char_id=nil)
235       @char_id = Character.parse_char_id(char_id)
236       @attributes = Hash.new
237       @check_all_database = false
238     end
239     attr_reader :char_id
240     def to_i() @char_id end
241     def mcs_utf8() Character.u4itou8(@char_id) end
242     def mcs_hex() sprintf("%x", @char_id) end
243
244     #----------------------------------------------------------------------
245     def self.get(char_id) CharacterFactory.instance.get(char_id) end #flyweightパターン
246
247     #----------------------------------------------------------------------
248     def get_char_attribute(a) # XEmacs UTF-2000互換API群
249       a.gsub!(/_/, '-') #underlineは-に置換
250       atr = @attributes[a]
251       return atr if atr != nil
252       atr = check_database(a)
253       @attributes[a] = atr if atr != nil
254       return get_char_attribute("=jis-x0208") if a =~ /jisx0208/
255       return @attributes[a]
256     end
257     def put_char_attribute(a,v)
258       a.gsub!(/_/, '-') #underlineは-に置換
259       @attributes[a] = v;
260       CharDB.instance.put(a, mcs_utf8(), v)
261     end
262     def char_attribute_alist() check_all_database(); @attributes; end
263     def char_attribute_list()  check_all_database(); @attributes.keys; end
264     alias [] get_char_attribute  #その略称
265     alias []= put_char_attribute
266     alias alist char_attribute_alist
267     alias list  char_attribute_list
268
269     def method_missing(mid, *args) #参考:ostruct.rb
270       mname = mid.id2name
271       return get_char_attribute(mname) if args.length == 0
272       put_char_attribute(mname.chop, args[0]) if mname =~ /=$/ #代入
273     end
274
275     def has_attribute?() #意味のあるattributeを持ってますか?
276       keys = list
277       keys.delete_if {|k|
278         k =~ /ids/
279       }
280       return (keys.length != 0)
281     end
282
283     #----------------------------------------------------------------------
284     def ==(ch)
285       return false if ch == nil
286       return false unless ch.is_a? Character
287       self.char_id == ch.char_id
288     end
289
290     #----------------------------------------------------------------------
291     def self.parse_char_id(char_id) #FIXNUMを返す
292       return nil if char_id == nil
293       if char_id.is_a?(Numeric) #p [char_id]
294         char_id = 0x80000000 + char_id if char_id < 0  #補数表現
295         return char_id.to_i
296       elsif char_id.is_a?(String)
297         return char_id.to_i if char_id =~ /^\d+$/ #文字列による数字だったら数値化してreturn
298         return EntityReference.parse(char_id) if char_id =~ Regexp.new(EntityReference::REGEXP_ALL) #実体参照?
299         char_id.sub!(/^\?/, '') if char_id =~ /^\?/ #もし先頭に?がついていたら削除
300         #このへん本当はもっとちゃんとチェックするべし
301         u4 = Uconv.u8tou4(char_id) #UCS-4文字列に変換
302         return Character.u4tou4i(u4) #UCS-4数値にしてreturn
303       else
304         raise ArgumentError, "unknown object for char_id", caller(1)
305       end
306     end
307     def self.u4tou4i(u4)
308       return 0 if u4 == nil || u4 == ""
309       return (u4[3] << 24 | u4[2] << 16 | u4[1] << 8 | u4[0]) #UCS-4数値にしてreturn
310     end
311     def self.u4itou4(num)
312       return "" if num == nil
313       return sprintf("%c%c%c%c", num&0xff, (num >> 8)&0xff, (num >> 16)&0xff, (num >> 24)&0xff) #UCS-4数値を文字列にしてreturn
314     end
315     def self.u4itou8(char_id) #ucsの数値を受けとり、UTF-8の文字一文字を返す
316       begin
317         u4 = Character.u4itou4(char_id)
318         u8 = Uconv.u4tou8(u4)
319         return u8
320       rescue
321         #raise ArgumentError, "invalid char_id (#{char_id})", caller(1)
322         #print "error\n"
323         return ""
324       end
325     end
326
327     #----------------------------------------------------------------------
328     def check_database(a)
329       db = CharDB.instance
330       u8 = mcs_utf8()
331       v = db.get(a, u8) #u8で表される文字のaアトリビュートを調べる。
332       return v
333     end
334     def check_all_database() #現在の@char_idから、文字データベースを参照する
335       return if @check_all_database
336       return if @char_id == nil
337       db = CharDB.instance
338       u8 = mcs_utf8()
339       atrs = db.get_all(u8) #u8で表される文字のアトリビュートを全部持ってこい
340       atrs.each {|a,v|
341         @attributes[a] = v #とかいう感じで代入するのでええかな?
342       }
343       @check_all_database = true #重い処理なので一応checkする
344     end
345
346     #----------------------------------------------------------------------
347     def ucs()      #p 'ucs'
348       #ar = %w{ucs ucs-big5 ucs-cdp ucs-cns ucs-jis ucs-ks =>ucs =>ucs* =>ucs-jis}
349       #ar = %w{ucs ucs-jis ucs-big5 ucs-cdp ucs-cns ucs-ks =>ucs =>ucs* =>ucs-jis}
350       ar = %w{ucs-jis ucs =>ucs-jis}
351       #並び順は恣意的で、ucs-jisを先に出している。本来はこれも指定できるようにするべき。
352       ar.each {|a|      #p [a]
353          u = get_char_attribute(a)
354          return u if u != nil
355        }
356        return nil
357      end
358
359      #----------------------------------------------------------------------CCS関係
360      def to_utf8() Uconv.u4tou8(Character.u4itou4(ucs())) end #UTF8文字列を返す
361      #alias to_s to_utf8
362      alias to_s mcs_utf8
363      def map_utf8()
364        u = ucs()
365        if u.nil? || 0xffff < u
366          return to_er()
367        else
368          return to_utf8()
369        end
370      end
371      def map_ucs_er()
372        u = ucs()
373        if u.nil? || 0xffff < u
374          return to_er()
375        else
376          return Character.get(u).to_er()
377        end
378      end
379      def to_euc()
380        u = ucs()
381        return "" if u.nil? || 0xffff < u
382        Uconv.u16toeuc(Uconv.u4tou16(Character.u4itou4(ucs())))
383      end
384      def map_euc()
385        e = to_euc()
386        return e if e != ""
387        return to_er()
388      end
389      def to_sjis()
390        u = ucs()
391        return "" if u.nil? || 0xffff < u
392        Uconv.u16tosjis(Uconv.u4tou16(Character.u4itou4(ucs())))
393      end
394      def map_sjis()
395        e = to_sjis()
396        return e if e != ""
397        return to_er()
398      end
399
400      #----------------------------------------------------------------------
401      def to_er(codesys=nil) #実体参照を返す、希望するcodesysが引数(未実装)
402        return "" if @char_id == nil
403 #       return sprintf("&U+%04X;", @char_id) if @char_id <= 0xffff
404        return sprintf("&#x%04x;", @char_id) if @char_id <= 0xffff
405 #       return sprintf("&#%05d;", @char_id) if @char_id <= 0xffff
406 #       return sprintf("&U-%05X;", @char_id) if @char_id <= 0xfffff
407        return sprintf("&#x%05x;", @char_id) if @char_id <= 0xfffff
408 #       return sprintf("&#%06d;", @char_id) if @char_id <= 0xfffff
409        EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
410          code = self[codesys]
411          next if code == nil
412          return sprintf("&#{er_prefix}%0#{keta}#{numtype};", code)
413        }
414        return sprintf("&MCS-%08X;", @char_id) #本当はこれは無しにしたい
415      end
416      def to_er_list()
417        ar = []
418        EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
419          er = to_er(codesys)
420          ar << er if er != nil
421        }
422        ar
423      end
424
425      def inspect_x()
426        return "<>" if @char_id == nil
427        ar = [to_utf8(), to_er().sub(/^&/,'').chop]
428        "<"+ar.join(',')+">"
429      end
430      alias inspect inspect_x
431      def inspect_all_codesys()
432        #to_erを全てのcodesysにおいて実行する。その結果をコンパクトにまとめる
433      end
434      def inspect_all()
435        ar = [inspect.chop]
436        alist.to_a.sort.each {|a, v| ar << "#{a}:#{v}" }
437        return ar.join(',')+">"
438      end
439      def get_attributes()
440        str = ""
441        alist.to_a.sort.each {|a, v|
442          str += "#{a}: #{v}\n"
443        }
444        str
445      end
446
447      def inspect_ids(hex_flag=false)
448        ids = decompose
449        ar = []
450        ar << (hex_flag ? "x"+mcs_hex : to_utf8)
451        if to_s != ids #idsが部品そのものだったら部品追加はしない
452          ids.each_char {|ch|
453            char = ch.char
454            next if char.is_ids?
455            if hex_flag then
456              ar << "x"+char.mcs_hex
457            else
458              u = char.to_utf8
459              if u != ""
460                ar << u
461              else
462 #              ar << char.to_s
463                ar << char.to_er
464              end
465            end
466          }
467        end
468        return "("+ar.join("\t")+")"
469      end
470
471      #----------------------------------------------------------------------IDS関係
472      def decompose
473        k = self.to_s
474 #       idss = self['ids']
475 #       return idss if idss
476        return k if self.is_basic_kanji? #基本漢字はstop kanjiとするぞと。
477
478        idss = self['ids-aggregated']
479        return idss if idss != nil && 0 < idss.length && k != idss
480        idss = self['ids']
481        return idss if idss != nil && 0 < idss.length && k != idss
482        return k
483 #       return k if idss.nil? || idss.length == 0 || k == idss
484 #       if idss.char_length == 2
485 #       p ['What???', k, idss, k.inspect_all]
486 #        #return idssx[1] #二個目だけ返すとか?
487 #        return k #IDSに展開する方法が無いと。
488 #       end
489 #       return k if k == idss
490 #       if idss.include?(k) #<C5-4C4D><C6-4A37>この二文字のBUG対策
491 #        #return idss.sub(k, '')
492 #        return k #IDSに展開する方法が無いと。
493 #       end
494 #       return idss
495      end
496      def is_ids?() 0x2ff0 <= @char_id && @char_id <= 0x2fff end
497      def ids_operator_argc()
498        return 0 unless is_ids?
499        return 3 if @char_id == 0x2ff2 || @char_id == 0x2ff3
500        return 2
501      end
502    end
503
504    class DBS #======================================================================複数のDBを集めたclass
505    end
506
507    class ADB < BDB::Hash #======================================================================一つのDB
508      def initialize(*args)
509        super
510        @modified = false
511        at_exit {
512          if @modified
513            self.close #これがないと、うまくデータベースがセーブされないのです。
514          end
515        }
516      end
517      def self.open_create(filename)
518        ADB.open(filename, nil, BDB::CREATE | BDB::EXCL) #上書きはしない
519      end
520      def mykey(key)
521        if key.is_a?(String)
522          if key.char_length == 1
523            return '?'+key  #Stringだったら引く前に?を足す
524          end
525        end
526        #key = key.to_s if key.is_a?(Numeric) #NumberだったらStringにする。
527        #ここで && key ! =~ /^\?/ をいれると、?自身を検索できなくなってしまう。
528        return key
529      end
530      def myvalue(v)
531        return v if v == nil
532        return v.to_i if v =~ /^\d+$/ #数字だったらここで変換しておく
533        return v.sub(/^\?/, '') if v =~ /^\?/ #冒頭の?は取り除く
534        return $1 if v =~ /^"(.+)"$/ #最初と最後に"がついていたら、取り除く
535        #p ['get', v, t, key, db]
536        #return parse_sexp(v) if v =~ /^\(.+\)$/ #最初と最後が()の時は、S式にparseする
537        return v #それ以外って何?
538      end
539      def myget(key) #keyキーを引いて返す
540        key = mykey(key)
541        v = get(key) #存在しなかったらnilを返すことになる
542        return myvalue(v)
543      end
544      def myput(key, v) #keyにvをいれる
545        key = mykey(key)
546        put(key, v) #putする
547        @modified = true
548      end
549    end
550
551    class DB #======================================================= データベース群のabstract class
552      def self.unix_to_win(unix) #Windowsファイル名制限のため、変換する
553        win = unix.gsub(/</, '(')
554        win.gsub!(/>/, ')')
555        win.gsub!(/\*/, '+')
556        win.gsub!(/\?/, '!')
557        return win
558      end
559      def self.win_to_unix(win)
560        unix = win.gsub(%r|\)|, '>')
561        unix.gsub!(%r|\(|, '<')
562        unix.gsub!(%r|!|, '?')
563        unix.gsub!(%r|\+|, '*')
564        return unix
565      end
566 #     def windows?() DB.windows?() end
567      def get_filename(t)
568        return @pre + DB.unix_to_win(t) + @post if windows?
569        return @pre + t + @post
570      end
571      def get_dirname(t) File.dirname(get_filename(t)) end
572      def open_dbs()
573        @dbs = Hash.new
574        keys = find_keys()
575        keys.each {|key| open_db(key) }
576      end
577      def find_keys()
578        files = []
579        Dir.glob(@glob){|f|
580          next if ! File.file?(f)
581          next if f =~ /.txt$/
582          files << f
583        }
584        keys = []
585        files.each {|f|
586          t = DB.win_to_unix(f)
587          t.sub!(%r|^#{@pre}|, '')
588          t.sub!(%r|#{@post}$|, '') if @post != ""
589          keys << t
590        }
591        return keys
592        #return keys.sort
593      end
594      def close_db(t)
595        db = get(t)
596        return nil if db.nil?
597        db.close
598        @dbs.delete(t)
599      end
600      def open_db(t)
601        return nil if get(t) #すでにopenしていたら再openはしない。
602        begin
603          bdb = ADB.open(get_filename(t), nil, 0)
604          @dbs[t] = bdb if bdb != nil
605        rescue
606          p ["open error", get_filename(t)]; return nil
607        end
608        return true
609      end
610      def make_db(t, h=nil) #tという名前でhという中身のデータベースを作る
611        return nil if get(t) #すでにある場合はreturn
612        Dir.mkdir(get_dirname(t)) unless FileTest.exist?(get_dirname(t))
613        db = nil
614        begin
615          db = ADB.open_create(get_filename(t)) #上書きはしない
616          if h != nil
617            h.each {|k, v|
618              k = '?'+k if k.is_a?(String)
619              db[k] = v
620            }
621          end
622          db.close
623        rescue
624          p ["make error", get_filename(t)]; return nil
625        end
626        return true
627      end
628      def make_db_no_question_mark(t, h=nil) #tという名前でhという中身のデータベースを作る
629        return nil if get(t) #すでにある場合はreturn
630        Dir.mkdir(get_dirname(t)) unless FileTest.exist?(get_dirname(t))
631        db = nil
632        begin
633          db = ADB.open_create(get_filename(t)) #上書きはしない
634          if h != nil
635            h.each {|k, v|
636 #            k = '?'+k if k.is_a?(String)
637              db[k] = v
638            }
639          end
640          db.close
641        rescue
642          p ["make error", get_filename(t)]; return nil
643        end
644        return true
645      end
646      def remove_db(t) #tという名前のデータベースを消去する
647        db = get(t)
648        if db
649          db.close
650          @dbs.delete(t)
651        end
652        begin
653          File.unlink(get_filename(t)) if FileTest.file?(get_filename(t))
654        rescue
655          p ["unlink error", get_filename(t)]; return nil
656        end
657        dn = get_dirname(t)
658        Dir.rmdir(dn) if FileTest.directory?(dn) && Dir.entries(dn).length <= 2 #空directoryだったら消す
659        return true
660      end
661      def to_num(s)
662        return s.to_i if s =~ /^\d+$/
663        return s
664      end
665      def dump_db(t)
666        db = get(t)
667        return nil unless db
668        file = get_filename(t)
669        open("#{file}.txt", "w"){|out|
670 #        out.binmode.sync = true
671          ar = db.to_a
672          ar.map! {|k, v| [to_num(k), to_num(v)] }
673          ar.sort.each {|k, v|
674            out.printf("%s\t%s\n", k, v)
675          }
676        }
677        return true
678      end
679      def each_db()  @dbs.to_a.sort.each {|t, db| yield(t, db) } end
680      def dump_all()  each_db {|t, db| dump_db(t) } end
681      def close_all() each_db {|t, db| db.close   } end
682      def keys() @dbs.keys end
683      def each(t)
684        return unless block_given?
685        db = @dbs[t]
686        return nil unless db
687        db.each {|k, v|
688          k = to_num(k)
689          v = to_num(v)
690          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
691          vv = get(t, k) #p ['each', t, k, v, vv]
692          yield(k, vv)
693        }
694      end
695      def each_sort(t)
696        return unless block_given?
697        db = @dbs[t]
698        return nil unless db
699        ar = db.to_a
700        ar.map! {|k, v| [to_num(k), to_num(v)] }
701        ar.sort.each {|k, v|
702          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
703          vv = get(t, k) #p ['each', t, k, v, vv]
704          yield(k, vv)
705        }
706      end
707      #----------------------------------------------------------------------
708      def get(t, key=nil) #tというデータベースのkeyキーを引いて返す
709        db = @dbs[t]
710        return db if key.nil?
711        return nil unless db
712        return db.myget(key)
713      end
714      def put(t, key, v) #tというデータベースのkeyにvをいれる
715        db = @dbs[t]
716        if db == nil
717          db = make_db(t) 
718          db = open_db(t) 
719          db = @dbs[t]
720        end
721        db.myput(key, v) #putする
722      end
723    end
724
725    class CharDB < DB #------------------------------------ MCS-UTF8をキーとした属性へのデータベース
726      include Singleton
727      def initialize()
728        super
729        @glob, @pre, @post = "#{DB_DIR}/system-char-id/*", "#{DB_DIR}/system-char-id/", ""
730        open_dbs()
731      end
732      def get_all(u8) #全データベースのu8キーを引いてHashにまとめて返す
733        atrs = Hash.new
734        @dbs.each {|t, db|
735          v = get(t, u8)
736          atrs[t] = v if v != nil
737        }
738        return atrs
739      end
740    end
741
742    class CodesysDB < DB #----------------------------------------------------------------------
743      include Singleton
744      def initialize()
745        super
746        @glob, @pre, @post = "#{DB_DIR}/*/system-char-id", "#{DB_DIR}/", "/system-char-id"
747        open_dbs()
748      end
749      #def keys() @dbs.keys.sort end #どんなCodesysの情報を持っているかの一覧
750      def keys() @dbs.keys end #どんなCodesysの情報を持っているかの一覧
751      def get_codesys(t)
752        db = get(t)
753        return nil unless db
754        return Codesys.new(t)
755      end
756    end
757
758    class Codesys < DB #======================================================================
759      def initialize(name)
760 #       super
761        @name = name
762        @dbs = CodesysDB.instance
763      end
764      def keys() #どんなコードポイントの情報を持っているかの一覧
765        ks = @dbs.get(@name).keys
766        if @name =~ /jisx0208/ #特別処理
767          n = @dbs.get('=jis-x0208').keys 
768          #       p ['keys', @name, ks, n]
769          ks += n
770        end
771        ks.map! {|k| to_num(k) }
772        ks
773      end
774      def get(key)
775        v = @dbs.get(@name, key)
776        return v if v
777        if @name =~ /jisx0208/ #jisx0208が含まれている場合だけ特別処理する
778          return @dbs.get('=jis-x0208', key)
779        end
780        return nil
781      end
782      def each()
783        return unless block_given?
784        db = @dbs.get(@name)
785        return nil unless db
786        db.each {|k, v|
787          k = to_num(k)
788          v = to_num(v)
789          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
790          vv = @dbs.get(@name, k)        #p ['each', t, k, v, vv]
791          yield(k, vv)
792        }
793      end
794      def each_sort()
795        return unless block_given?
796        db = @dbs.get(@name)
797        return nil unless db
798        ar = db.to_a
799        ar.map! {|k, v| [to_num(k), to_num(v)] }
800        ar.sort.each {|k, v|
801          k.sub!(/^\?/, '') if k =~ /^\?/ #冒頭の?は取り除く
802          vv = @dbs.get(@name, k)        #p ['each', t, k, v, vv]
803          yield(k, vv)
804        }
805      end
806    end
807
808   class IDS_TEXT_DB < DB #======================================================================
809     include Singleton
810     if CHISE.windows?()
811       IDS_DB_DIR = 'd:/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
812     else
813       IDS_DB_DIR = '/home/eto/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
814     end
815     IDS_LIST = "
816 IDS-UCS-Basic.txt
817 #IDS-UCS-Compat-Supplement.txt
818 #IDS-UCS-Compat.txt
819 IDS-UCS-Ext-A.txt
820 IDS-UCS-Ext-B-1.txt
821 IDS-UCS-Ext-B-2.txt
822 IDS-UCS-Ext-B-3.txt
823 IDS-UCS-Ext-B-4.txt
824 IDS-UCS-Ext-B-5.txt
825 IDS-UCS-Ext-B-6.txt
826 IDS-JIS-X0208-1990.txt
827 IDS-Daikanwa-01.txt
828 IDS-Daikanwa-02.txt
829 IDS-Daikanwa-03.txt
830 IDS-Daikanwa-04.txt
831 IDS-Daikanwa-05.txt
832 IDS-Daikanwa-06.txt
833 IDS-Daikanwa-07.txt
834 IDS-Daikanwa-08.txt
835 IDS-Daikanwa-09.txt
836 IDS-Daikanwa-10.txt
837 IDS-Daikanwa-11.txt
838 IDS-Daikanwa-12.txt
839 IDS-Daikanwa-dx.txt
840 IDS-Daikanwa-ho.txt
841 IDS-CBETA.txt
842 ".split
843     def initialize()
844       super
845       @ids_list = IDS_LIST
846       @chars = []
847       @glob, @pre, @post = "#{IDS_DB_DIR}/db/*", "#{IDS_DB_DIR}/db/", ""
848       dir = File.dirname(@pre)
849       Dir.mkdir(dir) unless FileTest.exist?(dir)
850       open_dbs()
851     end
852     def each_file()
853       return unless block_given?
854       @ids_list.each {|file|
855         next if file =~ /^#/
856         yield(IDS_DB_DIR+file)
857       }
858     end
859     def each_line(file)
860       open(file){|f|
861         while line = f.gets
862           next if line =~ /^;/ #コメントはとばす
863           line.chomp!
864           code, char, ids = line.split
865           yield(code, char, ids)
866         end
867       }
868     end
869     def dump_text_all
870       each_file {|file|
871         dir = File.dirname(file) + '/../ids-new/'
872         Dir.mkdir(dir) if ! FileTest.directory?(dir)
873         newfile = dir + File.basename(file)
874         p [file, newfile]
875         open(newfile, "w"){|out|
876           out.binmode.sync = true
877           each_line(file){|code, ch, ids|
878             char = Character.get(ch)
879             ids = char.decompose
880             out.print "#{code}  #{ch}   #{ids}\n"
881           }
882         }
883       }
884     end
885     def make_ids_error
886       each_file {|file|
887         dir = File.dirname(file) + '/../ids-error'
888         Dir.mkdir(dir) unless FileTest.exist?(dir)
889         errfile = dir + '/' + File.basename(file)
890 #       p [file, errfile]
891         open(errfile, "w"){|out|
892           out.binmode.sync = true
893           each_line(file){|code, ch, ids|
894             char = Character.get(ch)
895             ids_error = char['ids-error']
896             next if ids_error.nil?
897             out.print "#{code}  #{ch}   #{ids}  #{ids_error}\n"
898           }
899         }
900       }
901     end
902   end
903
904   class IDS_DB < DB #======================================================================BDB化したIDS DBを扱う
905     include Singleton
906     def initialize
907       @dbs = CharDB.instance
908     end
909     def make_ids_db
910       db = IDS_TEXT_DB.instance
911       db.each_file {|file|
912         @char_counter = 0
913         @same_ids_counter = 0
914         @good_ids_counter = 0
915         @conflict_ids_counter = 0
916         db.each_line(file){|code, ch, ids|
917           @char_counter += 1
918
919           ids = "" if ids == nil
920           next if ids == "" #IDSが定義されていない場合は、さっくりと無視するべしよ。
921
922           charimg = Character.get(ch) #実体参照である可能性がある
923
924           next if code =~ /'$/ || code =~ /"$/ #大漢和番号のダッシュ付きは無視する
925           char = Character.get("&"+code+";") #code表記を元に実体参照を作って解釈する
926           if char.nil? || char.to_s == "" #うまく文字にならなかった
927             print "char == null #{char.inspect} #{code} #{ch}   #{ids}\n" unless code =~ /^M-/ || code =~ /^CB/
928             #大漢和、CBETA以外の場合は、エラーメッセージ。
929             next
930           end
931           if char != charimg #code表記と文字が一致していない?
932             unless code =~ /^M-/ || code =~ /^MH-/ || code =~ /^CB/ #食い違っていて当然であるので何もしない
933               print "unknown char       #{char.inspect} #{code} #{ch}   #{ids}\n"
934               next #それ以外の場合はエラーメッセージをだして、次へ。
935             end
936           end
937           #next if !char.has_attribute? #isolated characterはまぎれこませない。
938
939           ids.de_er! #実体参照を解除する
940           next if ids == char.to_s #もし文字とまったく一緒なら、意味が無いので情報を持たない
941           next if ids.char_length == 1
942
943           idstree = IDS_Tree.new(ids)
944           c = idstree.check_integrity
945           c = "contains self" if ids.include?(char.to_s)
946           if c #ちょっとでもエラーがある場合は、
947             char['ids-error'] = c #エラーを記録して、データとしては保持しない
948             next
949           end
950
951           if char['ids'].nil? || char['ids'] == "" #元々IDSが無かった場合は、
952             char['ids'] = ids #普通に代入すればそれでいいです。
953             @good_ids_counter += 1
954           else #しかしいままでにすでにIDSが定義されていた場合は?
955             if char['ids'] == ids #新しいIDSと古いIDSが完全に一致するなら無視しましょう。
956               @same_ids_counter += 1
957             else #しかしいままでのIDSと新しいIDSが食い違った場合は?
958               @conflict_ids_counter += 1
959 #             print "conflict   #{char.inspect} #{code} #{ids}  #{char['ids']}\n"
960             end
961           end
962         }
963         print "#{file}  #{@char_counter}        #{@same_ids_counter}    #{@conflict_ids_counter}        #{@good_ids_counter}\n"
964         CharacterFactory.instance.reset()
965       }
966       @dbs.dump_db('ids-error') #テキスト化する
967       @dbs.dump_db('ids') #テキスト化する
968     end
969     def make_ids_reverse
970       h = Hash.new
971       @dbs.each('ids') {|k, v|
972         char = k.char
973         ids = char.decompose
974         h[ids] = "" if h[ids].nil?
975         h[ids] += k #追加する
976       }
977       h.each {|k, v|
978         h[k] = char_sort(v) #文字の順番を、よく使うっぽいものからの順番にする
979       }
980       h.delete_if {|k, v| #h[k]が""になる可能性もあるが、それはkeyとして入れないことにする。
981         v == ""
982       }
983       print "length     #{h.length}\n"
984       cdb = CodesysDB.instance
985       cdb.make_db_no_question_mark('ids', h)
986       cdb.open_db('ids') #これが無いと、dump_dbされません。
987       cdb.dump_db('ids')
988     end
989     def char_sort(composed)
990       return composed if composed.char_length == 1
991       ar = composed.to_a
992       arorg = ar.dup
993       ar2 = []
994       ar.dup.each {|ch|
995         char = ch.char
996         if char.char_id < 0xfffff #Unicodeっぽい?
997           ar2 << ch
998           ar.delete(ch)
999         end
1000       }
1001       if 0 < ar.length
1002         EntityReference.each_codesys{|codesys, er_prefix, keta, numtype|
1003           ar.each {|ch|
1004             char = ch.char
1005             v = char[codesys]
1006 #           p [codesys, v] if v
1007             if v #EntityReferenceの順番に準拠する。
1008               ar2 << ch
1009               ar.delete(ch)
1010             end
1011           }
1012         }
1013       end
1014       if 0 < ar.length
1015 #       p ['yokuwakaran character', ar, ar[0].inspect_all, arorg]
1016         EntityReference.each_codesys{|codesys, er_prefix, keta, numtype|
1017           ar.dup.each {|ch|
1018             char = ch.char
1019             v = char[codesys]
1020 #           p [codesys, v] if v
1021           }
1022         }
1023       end
1024       return ar2.join("")
1025     end
1026     def dump_ids_duplicated
1027       open('ids-duplicated.txt', 'w'){|out|
1028         #out.binmode
1029         CodesysDB.instance.each('ids') {|k, v|
1030           if v.nil?
1031             out.print "nil      #{k}    #{v}\n"
1032             next
1033           end
1034           n = v.char_length
1035           next if n == 1
1036           out.print "#{n}       #{k}    #{v}"
1037           v.each_char {|ch|
1038             char = ch.char
1039             out.print " #{char.inspect}"
1040           }
1041           out.print "\n"
1042         }
1043       }
1044     end
1045     def make_ids_aggregated
1046       @dbs.each('ids') {|k, v|
1047         char = k.char
1048         ids = char.decompose
1049         ag = ids.aggregate
1050         char['ids-aggregated'] = ag
1051       }
1052       @dbs.dump_db('ids-aggregated')
1053     end
1054     def dump_ids_aggregated
1055       open('ids-aggregated.txt', 'w'){|out|
1056         #out.binmode
1057         @dbs.each('ids') {|k, v|
1058           char = k.char
1059           ids = char['ids']
1060           ag  = char['ids-aggregated']
1061           out.print "#{char.to_s}       #{ag}   #{ids}\n" if ids != ag
1062         }
1063       }
1064     end
1065     def make_ids_parts
1066       @dbs.each('ids') {|k, v|
1067         char = k.char
1068         pids = char.to_s
1069         ar = []
1070         counter = 0
1071         loop {
1072           ids = pids.decompose
1073           break if ids == pids #これ以上分割できないようだったら終了〜。
1074           ar += ids.to_a
1075           counter += 1
1076           p [char.to_s, pids, ids, ar] if 10 < counter #これは何かおかしいぞと
1077           pids = ids
1078         }
1079         ar.sort!
1080         ar.uniq!
1081 #やっぱりIDS文字も加えることにする. by eto 2003-02-05
1082 #       ar.delete_if {|ch|
1083 #         ch.char.is_ids? #IDS文字はまぎれこませない。
1084 #       }
1085         str = ar.join('')
1086         char['ids-parts'] = str
1087       }
1088       @dbs.dump_db('ids-parts')
1089     end
1090     def make_ids_contained
1091       h = Hash.new
1092       @dbs.each('ids-parts') {|k, v|
1093         char = k.char
1094         parts = char.ids_parts
1095         parts.each_char {|ch|
1096 #         part = ch.char
1097           h[ch] = [] if h[ch].nil?
1098           h[ch] << k
1099 #         h[ch] += k
1100 #         part['ids-contained'] = "" if part['ids-contained'].nil?
1101 #         part['ids-contained'] += k
1102         }
1103       }
1104       h.each {|k, v|
1105         char = k.char
1106         v.sort!
1107         char['ids-contained'] = v.join('')
1108         
1109       }
1110       @dbs.dump_db('ids-contained')
1111     end
1112     def make_ids_decomposed
1113       @dbs.each('ids') {|k, v|
1114         char = k.char
1115         de= char.decompose_all
1116         char['ids-decomposed'] = de
1117       }
1118       @dbs.dump_db('ids-decomposed')
1119     end
1120   end
1121
1122   class Node < Array #=======================================================木構造の中の一つの枝
1123     def initialize(nodeleaf=nil, nodenum=nil)
1124       super()
1125       @nodeleaf = nodeleaf
1126       @nodenum = nodenum
1127       if @nodeleaf
1128         original_add(@nodeleaf)
1129       end
1130     end
1131     attr_reader :nodenum
1132     alias original_add <<
1133     private :original_add
1134     def <<(obj)
1135       original_add(obj)
1136       @nodenum -= 1 if @nodenum
1137     end
1138     def nodes
1139       ar = []
1140       ar << self.to_s
1141       self.each {|n|
1142         ar += n.nodes if n.is_a? Node
1143       }
1144       return ar
1145     end
1146   end
1147
1148   class Tree #======================================================================木構造を扱う
1149     def initialize()
1150       @root = Node.new()
1151       @stack = [@root]
1152       @leafnum = 0
1153       @depth = 1 #stackの深さが最大になったところの値、木構造が無いときは1となる
1154     end
1155     def depth() @depth - 1 end
1156     def add_node(nodeleaf=nil, nodenum=nil) #枝を追加
1157       new_node = Node.new(nodeleaf, nodenum)
1158       @stack.last << new_node
1159       @stack << new_node
1160       if @depth < @stack.length
1161         @depth = @stack.length
1162       end
1163       self
1164     end
1165     def end_node() #この枝は終り
1166       @stack.pop
1167       self
1168     end
1169     def add_leaf(a) #葉を追加
1170       @stack.last << a
1171       end_check()
1172       self
1173     end
1174     def end_check()
1175       n = @stack.last.nodenum
1176       if n && n == 0
1177         end_node()
1178         end_check() #再帰
1179       end
1180     end
1181     def check_integrity
1182       n = @stack.last.nodenum
1183       return nil if @root.length == 0 #no tree is good tree
1184       return "unmatch leaves" if n && n != 0
1185       return "extra nodes" if @root.first.is_a?(Node) && @root.length != 1
1186       return "extra leaves" if @root.length != 1
1187       return nil
1188     end
1189     def nodes
1190       r = @root.nodes
1191       r.shift
1192       r
1193     end
1194     def sub_nodes
1195       r = nodes
1196       r.shift
1197       r
1198     end
1199     def to_s()    @root.to_s    end
1200     def inspect() @root.inspect end
1201   end
1202
1203   class IDS_Tree < Tree #======================================================================
1204     def initialize(str)
1205       @str = str
1206       super()
1207       parse()
1208     end
1209     def parse()
1210       @str.each_char {|ch|
1211         char = Character.new(ch)
1212         if is_ids?(char)
1213           add_node(char, ids_operator_argc(char))
1214         else
1215           add_leaf(char)
1216         end
1217       }
1218     end
1219     def is_ids?(obj)
1220       return true if "+*".include?(obj.to_s) #テスト用ですかね
1221       return true if obj.is_ids?
1222       return false
1223     end
1224     def ids_operator_argc(obj)
1225       return obj.ids_operator_argc if 0 < obj.ids_operator_argc
1226       return 2 #テスト用ってことで
1227     end
1228     def check_integrity
1229       r = super
1230       return r if r #不完全がすでにわかっているならreturn
1231       return "contains ques" if @str =~ /\?/ #?が含まれている?
1232       return nil
1233     end
1234
1235   end
1236
1237   class IDS #======================================================================IDSそのものを扱うclass
1238     def initialize(str) #IDS文字列をうけとる。
1239       @str = str
1240     end
1241     def parse
1242     end
1243     def parse_x #柔軟型のParse. IDSキャラクターが前にきてなくてもよい。などなど。
1244     end
1245   end
1246
1247   class Counter #======================================================================
1248     #使い方
1249     #counter = Counter.new(50) { exit }
1250     #counter.count
1251     def initialize(max)
1252       @max = max
1253       @count = 0
1254       @proc = proc
1255     end
1256     def count
1257       @count += 1
1258       if @max <= @count
1259         @proc.call
1260       end
1261     end
1262   end
1263
1264   class DBS_Management #======================================================================ファイル管理
1265     OBSOLETE_ATTRIBUTES = "
1266 cns-radical
1267 cns-radical?
1268 kangxi-radical
1269 daikanwa-radical
1270 unicode-radical
1271
1272 cns-strokes
1273 kangxi-strokes
1274 daikanwa-strokes
1275 shinjigen-1-radical
1276 gb-original-radical
1277 japanese-strokes
1278 jis-strokes-a
1279 jis-strokes-b
1280 jisx0208-strokes
1281 jis-x0213-strokes
1282 jisx0213-strokes
1283 unicode-strokes
1284
1285 totalstrokes
1286 cns-total-strokes
1287 jis-total-strokes-b
1288
1289 non-morohashi
1290
1291 =>ucs*
1292 #=>mojikyo
1293 #=mojikyo
1294 ->identical
1295
1296 ancient-ideograph-of
1297 ancient-char-of-shinjigen-1
1298 original-ideograph-of
1299 original-char-of-shinjigen-1
1300 simplified-ideograph-of
1301 vulgar-ideograph-of
1302 vulgar-char-of-shinjigen-1
1303 ideograph=
1304 ideographic-variants
1305 variant-of-shinjigen-1
1306
1307 iso-10646-comment
1308 ".split
1309     def initialize
1310       @odir = DB_DIR+"/system-char-id/obsolete" #直打ちしている。
1311     end
1312     def move_obsolete_files # 廃止予定のbdbファイルをobsoleteディレクトリーにつっこむ
1313       db = CharDB.instance
1314       db.close_all
1315       Dir.mkdir(@odir) unless FileTest.directory? @odir
1316       OBSOLETE_ATTRIBUTES.each {|attr|
1317         next if attr =~ /^#/
1318         filename = db.get_filename(attr)
1319         move_to_obsolete(filename)
1320         move_to_obsolete(filename+".txt")
1321       }
1322     end
1323     def move_to_obsolete(file)
1324       cmd = "mv #{file} #{@odir}"
1325 #      p cmd
1326       system cmd
1327     end
1328   end
1329
1330   class JoyoList
1331     include Singleton
1332     #JP_JOYO_FILE = DB_DIR+"/../jp-joyo.txt" #EUC-jisx0213
1333     JP_JOYO_FILE = DB_DIR+"/../joyo-ucs.txt" #UCS
1334     COMPOSIT_KANJI = "鳴名加品古知問間聞取兄見切分粉貧林森校東明住位好岩砂里男畑習休短空坂島倉美孝赤看光初努協解新歌語話張強忘悲答晴現正字安守灰秋秒困国医包同合舌居右左受友反道返迷花菜集机主太氷州点店庫仕帳幼防引配早直班筆重番北化比死夏後進酒福私家世内谷半原前寺思電雲気布旅衆泣"
1335 #    COMPOSIT_KANJI = "鳴名加品古"
1336     def initialize
1337       @nchars = []
1338       read_file
1339     end
1340     attr_reader :nchars
1341     def read_file
1342       open(JP_JOYO_FILE) {|f|
1343         while line = f.gets
1344           next if line =~ /^;/ #コメントはとばす
1345           line.chomp!
1346           #stroke, nchar, ochar = line.split #new char, old char, old charはnilが多い
1347           stroke, nchar = line.split
1348           @nchars << nchar
1349         end
1350       }
1351     end
1352     def dump_ids(ar)
1353       ar.each {|ch|
1354         char = ch.char
1355         print char.inspect_ids(true), "\t;", char.inspect_ids(false), "\n"
1356       }
1357     end
1358   end
1359
1360 end
1361
1362 #----------------------------------------------------------------------終了