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