f14b59fffb17c9a41968539b924f26d9ca9be978
[chise/ruby.git] / src / chise.rb
1 #!/usr/bin/env ruby
2 # Ruby/CHISE module by eto 2002-1114
3
4 require 'bdb'
5 require 'uconv'
6 require 'singleton'
7 require 'rbchise'
8 require 'db'
9 require 'ids'
10
11 $KCODE = 'u' #今のところこれ以外では動かない。String.splitが影響大。inspectも影響。
12 $debug = false #これはテスト用
13 $debug = true #これはテスト用
14 $stdout.binmode if $debug
15 $stdout.sync = true if $debug
16
17 class String #======================================================================
18   def to_a() self.split(//) end #$KCODEが設定されているので、UTF-8的な一文字づつがchにはいる
19   def each_char() to_a.each {|ch| yield ch } end
20   def each_character() to_a.each {|ch| yield ch.char } end
21   def char_length() to_a.length end
22   def char_at(n) to_a()[n] end
23   def char() Character.get(to_a[0]) end
24   #alias to_c char #悩み中
25   #def char_id() char.char_id() end #なんとなく廃止
26   #def get_char_attribute(a) char.get_char_attribute(a) end #なんとなく廃止
27   #def ucs() char.ucs() end
28   def to_utf8()
29     return to_a.map {|ch|
30       ch.char.to_utf8
31     }.join('')
32   end
33
34   def map_char(block = Proc.new)
35     return unless block_given?
36     return self.to_a.map {|ch| (block.call(ch)).to_s }.join("")
37   end
38   def map_char!(block = Proc.new)
39     return unless block_given?
40     self.replace(self.map_char {|ch| block.call(ch)})
41   end
42   def map_character(block = Proc.new)
43     return unless block_given?
44     return self.to_a.map {|ch| (block.call(ch.char)).to_s }.join("")
45   end
46   def map_character!(block = Proc.new)
47     return unless block_given?
48     self.replace(self.map_char {|ch| block.call(ch.char)})
49   end
50
51   def method_missing(mid, *args)
52     if char_length == 1 #省略形が有効なのは、一文字の時だけ
53       char.method_missing(mid, *args)
54     else
55       raise NameError, "undefined method `#{mid.id2name}'", caller(1)
56     end
57   end
58
59   def map_utf8() map_char {|ch| ch.char.map_utf8 } end
60   alias map_ucs map_utf8
61   def map_ucs_er() map_char {|ch| ch.char.map_ucs_er } end
62   def to_er() map_char {|ch| ch.char.to_er } end
63
64   #put関係、[]関係は用意しないことにした。
65   def de_er!() #EntityReferenceを取り除く
66     return self unless self =~ Regexp.new(EntityReference::REGEXP_PART) #それらしいのが無ければ何もしない
67     er = "&"+$1+";"      
68     self.sub!(Regexp.new(Regexp.escape(er)), Character.new(er).mcs_utf8) #変換自体はCharacterにまかせる
69     return self.de_er! if self =~ Regexp.new(EntityReference::REGEXP_PART) #まだあったら再帰
70     return self
71   end
72   def de_er() return self.dup.de_er!; end
73
74   def inspect_all() map_char {|ch| ch.char.inspect_all } end
75   def inspect_x()   map_char {|ch| ch.char.inspect_x   } end
76
77   def to_euc()   map_char {|ch| ch.char.to_euc   } end
78   def map_euc()  map_char {|ch| ch.char.map_euc  } end
79   def to_sjis()  map_char {|ch| ch.char.to_sjis  } end
80   def map_sjis() map_char {|ch| ch.char.map_sjis } end
81
82   def decompose() map_char {|ch| ch.char.decompose } end
83   def decompose!() self.replace(self.decompose); self; end
84   def decompose_all_nu(level=nil)
85     level = 0 if level.nil?
86     if 10 < level
87       p ['too many recursive', self] 
88       exit
89     end
90     de = self.decompose
91     return de.decompose_all(level+1) if de != self #なにか変化があったから再帰
92     de #もうこれ以上変化は無さそうだぞと。
93   end
94   def decompose_all() map_char {|ch| ch.char.decompose_all } end
95   def decompose_all!() self.replace(self.decompose_all); self; end
96
97   def find() #"日雲"→"曇"とかいう感じの操作
98     ar = []
99     length = char_length()
100     each_char {|ch|
101       char = ch.char
102       ar << char.ids_contained #その文字を含んでいる漢字のリスト
103     }
104     h = Hash.new(0)
105     ar.each {|list|
106       next if list.nil?
107       list.each_char {|ch|
108         h[ch] += 1
109       }
110     }
111     str = ""
112     h.each {|k, v|
113       #      p [k, v]
114       if length == v #全部に顔を出していたら
115         str += k
116       end
117     }
118     #    p str
119     str
120   end
121   def compose()
122     db = CodesysDB.instance
123     composed = db.get('ids', self)
124     return "" if composed.nil? #なかったよと。
125     return "" if composed.char_length == 0 #なにごと?
126     return composed if composed.char_length == 1
127     composed.each_char {|ch|
128       char = ch.char
129       return ch if char.has_attribute? #とりあえず最初にみつかったものを返すというヌルい仕様
130     }
131     return "" #attributeを持つものが一つも無かったら、""にする
132   end
133   def aggregate()
134     #selfである文字列をIDSだと仮定し、それを完全にcomposeしきらないで、
135     #その部分集合だけをとりだして、compose可能であればできるだけcomposeする。
136     tree = IDS_Tree.new(self)
137     return self if tree.depth <= 1 #sub_nodesが無い場合はここでさよなら
138     tree.sub_nodes.each {|node|
139       c = node.compose
140       next if c.nil? || c == ""
141       #      print "#{self}     #{node} #{c}\n"
142       #      p [self, node, c]
143       n = self.gsub(node, c)
144       return n.aggregate
145     }
146     return self #おきかえられるものがまったくなかったら、自分をかえす。
147   end
148 end
149
150 module CHISE #======================================================================
151   def windows?()
152     (RUBY_PLATFORM =~ /cygwin/ || RUBY_PLATFORM =~ /mswin32/ || RUBY_PLATFORM =~ /mingw32/)
153   end
154   module_function :windows?
155   if windows?()
156     DB_DIR = 'd:/work/chise/char-db' #この後に/sysmtem-char-id/ucsという感じに続く
157     IDS_DB_DIR = 'd:/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
158   else
159     DB_DIR = '/usr/local/lib/xemacs-21.4.12/i686-pc-linux/char-db' #この後に/sysmtem-char-id/ucsという感じに続く
160     IDS_DB_DIR = '/home/eto/work/chise/ids/' #この後にIDS-JIS-X0208-1990.txtという感じに続く
161   end
162
163   class EntityReference #======================================================================
164     #状況によってどのERに変換するかが異なる可能性があるので、普通のclassとして実装したほうがいい?
165 #    CODESYS_TABLE = [
166 #      %w( chinese-big5-cdp     CDP- 4 X),
167 #      %w( ideograph-daikanwa   M-   5 d),
168 #      %w( ideograph-cbeta      CB   5 d),
169 #      %w( ideograph-gt         GT-  5 d),
170 #      %w( ideograph-gt-k       GT-K 5 d),
171 #      %w( japanese-jisx0208-1990 J90- 4 X),
172 #      %w( japanese-jisx0208    J83- 4 X),
173 #      %w( japanese-jisx0213-1  JX1- 4 X),
174 #      %w( japanese-jisx0213-2  JX2- 4 X),
175 #      %w( japanese-jisx0212    JSP- 4 X),
176 #      %w( japanese-jisx0208-1978 J78- 4 X),
177 #      %w( chinese-cns11643-1   C1-  4 X),
178 #      %w( chinese-cns11643-2   C2-  4 X),
179 #      %w( chinese-cns11643-3   C3-  4 X),
180 #      %w( chinese-cns11643-4   C4-  4 X),
181 #      %w( chinese-cns11643-5   C5-  4 X),
182 #      %w( chinese-cns11643-6   C6-  4 X),
183 #      %w( chinese-cns11643-7   C7-  4 X),
184 #      %w( korean-ksc5601       K0- 4 X),
185 #    ]
186 #    CODESYS_ORDER = %w(japanese chinese korean ideograph)
187     CODESYS_TABLE = [
188       %w( =jis-x0208-1990       J90- 4 X),
189       %w( =jis-x0208-1983       J83- 4 X),
190       %w( =jis-x0208-1978       J78- 4 X),
191       %w( =jis-x0208            J90- 4 X), #継承のアドホックな実装
192       %w( =jis-x0208            J83- 4 X), #継承のアドホックな実装
193       %w( =jis-x0208            J78- 4 X), #継承のアドホックな実装
194       %w( =jis-x0213-1-2000     JX1- 4 X),
195       %w( =jis-x0213-2-2000     JX2- 4 X),
196       %w( =jis-x0212            JSP- 4 X),
197       %w( =big5-cdp             CDP- 4 X),
198       %w( =cns11643-1           C1-  4 X),
199       %w( =cns11643-2           C2-  4 X),
200       %w( =cns11643-3           C3-  4 X),
201       %w( =cns11643-4           C4-  4 X),
202       %w( =cns11643-5           C5-  4 X),
203       %w( =cns11643-6           C6-  4 X),
204       %w( =cns11643-7           C7-  4 X),
205       %w( =ks-x1001             K0-  4 X),
206       %w( =daikanwa             M-   5 d),
207       %w( =cbeta                CB   5 d),
208       %w( =gt                   GT-  5 d),
209       %w( =gt-k                 GT-K 5 d),
210     ]
211     REGEXP_PART = "&([-+0-9A-Za-z#]+);"
212     REGEXP_ALL = "^#{REGEXP_PART}$"
213
214     def self.match?(er) (er =~ Regexp.new(REGEXP_PART)) != nil end
215
216     def self.parse(er) #char_idをFIXNUMで返す
217       return "" unless er =~ Regexp.new(REGEXP_ALL) #なんか、間違ってる?
218       er = $1 #ついでに中身の部分を取り出す
219       return $1.hex if er =~ /^MCS-([0-9A-Fa-f]+)/ #MCS
220       #      if er =~ /^U[-+]?([0-9A-Fa-f]+)/ #Unicode直打ち
221       if er =~ /^U[-+]?([0-9A-Fa-f]+)/ || er =~ /^#x([0-9A-Fa-f]+)/ #Unicode直打ち
222         return $1.hex 
223       end
224
225       er.sub!(/^I-/, '') if er =~ /^I-/ #I-がついてるとどう違うのかはよくわからない
226       each_codesys {|codesys, er_prefix, keta, numtype| #p [codesys, er_prefix, keta, numtype]
227         numtyperegex = '\d' #if numtype == 'd'
228         numtyperegex = '[0-9A-Fa-f]' if numtype == 'X'
229         regexpstr = "^#{er_prefix}(#{numtyperegex}{#{keta},#{keta}})$"
230         if er =~ Regexp.new(regexpstr)
231           codestr = $1
232           code = codestr.to_i #if numtype == 'd'
233           code = codestr.hex if numtype == 'X'
234           char_id_u8 = EntityReference.get_database(codesys, code)
235           char_id_num = Character.parse_char_id(char_id_u8)
236           next if char_id_num == nil
237           return char_id_num
238         end
239       }
240       return ""
241     end
242
243     def self.each_codesys()
244 #      CODESYS_ORDER.each {|lang|
245 #       CODESYS_TABLE.each {|codesys, er_prefix, keta, numtype| #普通こういう書き方はしない。ループ一個にする。
246 #         next unless codesys =~ lang
247 #         yield(codesys, er_prefix, keta, numtype)
248 #       }
249 #      }
250       CODESYS_TABLE.each {|codesys, er_prefix, keta, numtype|
251         yield(codesys, er_prefix, keta, numtype)
252       }
253     end
254     def self.get_database(codesys, code)
255       c = CodesysDB.instance.get(codesys, code)
256       return c if c != nil
257 #      if codesys =~ /-jisx0208/
258 #       #return self.get_database("=jis-x0208", code) #再帰でどうだ?
259 #       c = CodesysDB.instance.get("=jis-x0208", code)
260 #       return c
261 #      end
262       return nil
263     end
264   end
265
266   class CharacterFactory #============================================文字オブジェクトの生成、cache
267     include Singleton
268     MAX = 10000
269     def initialize
270       @max = MAX
271       reset()
272     end
273     def get(char_id)
274       check_max()
275       n = Character.parse_char_id(char_id)
276       c = @chars[n]
277       @chars[n] = Character.new(n) if @chars[n] == nil
278       return @chars[n]
279     end
280     def reset()
281       @chars = nil
282       @chars = Hash.new
283       GC.start #ガーベージコレクション
284     end
285     def length() @chars.length; end
286     def check_max()
287       reset if @max < @chars.length #MAXを超えたらresetしてしまう。乱暴じゃがcacheなのでこれでいいのだ。
288     end
289   end
290
291   class Character #=============================================================== 文字オブジェクト
292     def initialize(char_id=nil)
293       @char_id = Character.parse_char_id(char_id)
294       @attributes = Hash.new
295       @check_all_database = false
296     end
297     attr_reader :char_id
298     def to_i() @char_id end
299     def mcs_utf8() Character.u4itou8(@char_id) end
300     def mcs_hex() sprintf("%x", @char_id) end
301
302     #----------------------------------------------------------------------
303     def self.get(char_id) CharacterFactory.instance.get(char_id) end #flyweightパターン
304
305     #----------------------------------------------------------------------
306     def normalize_attribute_name(b)
307       a = b.dup
308       a.gsub!(/_/, '-') #underlineは-に置換
309       a.sub!(/-at-/,  '@')
310       a.sub!(/^map-/,  '=>')
311       a.sub!(/^to-/,   '->')
312       a.sub!(/^from-/, '<-')
313       a
314     end
315     def get_char_attribute(b) # XEmacs UTF-2000互換API群
316       a = normalize_attribute_name(b)
317       #p [a, b]
318       atr = @attributes[a]
319       return atr if atr
320       atr = check_database(a)
321       if atr
322         @attributes[a] = atr
323         return atr
324       end
325       return get_char_attribute("="+a) unless a =~ /^=/ #頭に=がついてない場合はそれが省略されていることを仮定して、再帰する
326       nil
327     end
328     def put_char_attribute(b,v)
329       a = normalize_attribute_name(b)
330       @attributes[a] = v;
331       CharDB.instance.put(a, mcs_utf8(), v)
332     end
333     def char_attribute_alist() check_all_database(); @attributes; end
334     def char_attribute_list()  check_all_database(); @attributes.keys; end
335     alias [] get_char_attribute  #その略称
336     alias []= put_char_attribute
337     alias alist char_attribute_alist
338     alias list  char_attribute_list
339
340     def method_missing(mid, *args) #参考:ostruct.rb
341       mname = mid.id2name
342       return get_char_attribute(mname) if args.length == 0
343       put_char_attribute(mname.chop, args[0]) if mname =~ /=$/ #代入
344     end
345
346     def has_attribute?() #意味のあるattributeを持ってますか?
347       keys = list
348       keys.delete_if {|k|
349         k =~ /ids/
350       }
351       return (keys.length != 0)
352     end
353
354     #----------------------------------------------------------------------
355     def ==(ch)
356       return false if ch == nil
357       return false unless ch.is_a? Character
358       self.char_id == ch.char_id
359     end
360
361     #----------------------------------------------------------------------
362     def self.parse_char_id(char_id) #FIXNUMを返す
363       return nil if char_id == nil
364       if char_id.is_a?(Numeric) #p [char_id]
365         char_id = 0x80000000 + char_id if char_id < 0  #補数表現
366         return char_id.to_i
367       elsif char_id.is_a?(String)
368         return char_id.to_i if char_id =~ /^\d+$/ && 1 < char_id.length #文字列による数字だったら数値化してreturn
369         return EntityReference.parse(char_id) if char_id =~ Regexp.new(EntityReference::REGEXP_ALL) #実体参照?
370         char_id.sub!(/^\?/, '') if char_id =~ /^\?/ #もし先頭に?がついていたら削除
371         #このへん本当はもっとちゃんとチェックするべし
372         begin
373           u4 = Uconv.u8tou4(char_id) #UCS-4文字列に変換
374         rescue
375           p $!
376           p char_id
377           return 0
378         end
379         return Character.u4tou4i(u4) #UCS-4数値にしてreturn
380       else
381         raise ArgumentError, "unknown object for char_id", caller(1)
382       end
383     end
384     def self.u4tou4i(u4)
385       return 0 if u4 == nil || u4 == ""
386       return (u4[3] << 24 | u4[2] << 16 | u4[1] << 8 | u4[0]) #UCS-4数値にしてreturn
387     end
388     def self.u4itou4(num)
389       return "" unless num.is_a?(Integer)
390       return sprintf("%c%c%c%c", num&0xff, (num >> 8)&0xff, (num >> 16)&0xff, (num >> 24)&0xff) #UCS-4数値を文字列にしてreturn
391     end
392     def self.u4itou8(char_id) #ucsの数値を受けとり、UTF-8の文字一文字を返す
393       begin
394         u4 = Character.u4itou4(char_id)
395         u8 = Uconv.u4tou8(u4)
396         return u8
397       rescue
398         #raise ArgumentError, "invalid char_id (#{char_id})", caller(1)
399         #print "error\n"
400         return ""
401       end
402     end
403
404     #----------------------------------------------------------------------
405     def check_database(a)
406       db = CharDB.instance
407       u8 = mcs_utf8()
408       v = db.get(a, u8) #u8で表される文字のaアトリビュートを調べる。
409       return v
410     end
411     def check_all_database() #現在の@char_idから、文字データベースを参照する
412       return if @check_all_database
413       return if @char_id == nil
414       db = CharDB.instance
415       u8 = mcs_utf8()
416       atrs = db.get_all(u8) #u8で表される文字のアトリビュートを全部持ってこい
417       atrs.each {|a,v|
418         @attributes[a] = v #とかいう感じで代入するのでええかな?
419       }
420       @check_all_database = true #重い処理なので一応checkする
421     end
422
423     #----------------------------------------------------------------------
424     def ucs()      #p 'ucs'
425       #ar = %w{ucs ucs-big5 ucs-cdp ucs-cns ucs-jis ucs-ks =>ucs =>ucs* =>ucs-jis}
426       #ar = %w{ucs ucs-jis ucs-big5 ucs-cdp ucs-cns ucs-ks =>ucs =>ucs* =>ucs-jis}
427       ar = %w{ucs-jis ucs =>ucs-jis}
428       #並び順は恣意的で、ucs-jisを先に出している。本来はこれも指定できるようにするべき。
429       ar.each {|a|      #p [a]
430         u = get_char_attribute(a)
431         return u if u != nil
432       }
433       return nil
434     end
435
436     #----------------------------------------------------------------------CCS関係
437     def to_utf8() Uconv.u4tou8(Character.u4itou4(ucs())) end #UTF8文字列を返す
438     #alias to_s to_utf8
439     alias to_s mcs_utf8
440     def map_utf8()
441       u = ucs()
442       if u.nil? || 0xffff < u
443         return to_er()
444       else
445         return to_utf8()
446       end
447     end
448     alias map_ucs map_utf8
449     def map_ucs_er()
450       u = ucs()
451       if u.nil? || 0xffff < u
452         return to_er()
453       else
454         return Character.get(u).to_er()
455       end
456     end
457     def to_euc()
458       u = ucs()
459       return "" if u.nil? || 0xffff < u
460       Uconv.u16toeuc(Uconv.u4tou16(Character.u4itou4(ucs())))
461     end
462     def map_euc()
463       e = to_euc()
464       return e if e != ""
465       return to_er()
466     end
467     def to_sjis()
468       u = ucs()
469       return "" if u.nil? || 0xffff < u
470       Uconv.u16tosjis(Uconv.u4tou16(Character.u4itou4(ucs())))
471     end
472     def map_sjis()
473       e = to_sjis()
474       return e if e != ""
475       return to_er()
476     end
477
478     #----------------------------------------------------------------------
479     def to_er(codesys=nil) #実体参照を返す、希望するcodesysが引数(未実装)
480       return "" if @char_id == nil
481       return sprintf("&#x%04x;", @char_id) if @char_id <= 0xffff
482       return sprintf("&#x%05x;", @char_id) if @char_id <= 0xfffff
483       EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
484         code = self[codesys]
485         next if code == nil
486         return sprintf("&#{er_prefix}%0#{keta}#{numtype};", code)
487       }
488       return sprintf("&MCS-%08X;", @char_id) #本当はこれは無しにしたい
489     end
490     def to_er_list()
491       ar = []
492       EntityReference.each_codesys {|codesys, er_prefix, keta, numtype|
493         er = to_er(codesys)
494         ar << er if er != nil
495       }
496       ar
497     end
498
499     def inspect_x()
500       return "<>" if @char_id == nil
501       ar = [to_utf8(), to_er().sub(/^&/,'').chop]
502       "<"+ar.join(',')+">"
503     end
504     alias inspect inspect_x
505     def inspect_all_codesys() #未完成
506       #to_erを全てのcodesysにおいて実行する。その結果をコンパクトにまとめる
507     end
508     def inspect_all()
509       ar = [inspect.chop]
510       alist.to_a.sort.each {|a, v| ar << "#{a}:#{v}" }
511       return ar.join(',')+">"
512     end
513     def dump_all()
514       ar = [inspect]
515       alist.to_a.sort.each {|a, v| ar << "#{a}:#{v}" }
516       return ar.join('\n')+'\n'
517     end
518     def get_attributes()
519       str = ""
520       alist.to_a.sort.each {|a, v|
521         str += "#{a}: #{v}\n"
522       }
523       str
524     end
525
526     def inspect_ids(hex_flag=false)
527       ids = decompose
528       ar = []
529       ar << (hex_flag ? "x"+mcs_hex : to_utf8)
530       if to_s != ids #idsが部品そのものだったら部品追加はしない
531         ids.each_char {|ch|
532           char = ch.char
533           next if char.is_ids?
534           if hex_flag then
535             ar << "x"+char.mcs_hex
536           else
537             u = char.to_utf8
538             if u != ""
539               ar << u
540             else
541               ar << char.to_er
542             end
543           end
544         }
545       end
546       return "("+ar.join("\t")+")"
547     end
548
549     #----------------------------------------------------------------------IDS関係
550     def decompose
551       k = self.to_s
552       #       idss = self['ids']
553       #       return idss if idss
554       #       return k if self.is_basic_kanji? #基本漢字はstop kanjiとするぞと。
555       return self['ids-represent'] if self['ids-represent'] #ids_representを持っている場合はその値とする。
556       return self['ids-element'] if self['ids-element'] #ids_elementを持っている場合はその値とする。
557
558       idss = self['ids-meaning']
559       return idss if idss != nil && 0 < idss.length && k != idss
560       idss = self['ids-aggregated']
561       return idss if idss != nil && 0 < idss.length && k != idss
562       idss = self['ids']
563       return idss if idss != nil && 0 < idss.length && k != idss
564       return k
565       #       return k if idss.nil? || idss.length == 0 || k == idss
566       #       if idss.char_length == 2
567       # p ['What???', k, idss, k.inspect_all]
568       #  #return idssx[1] #二個目だけ返すとか?
569       #  return k #IDSに展開する方法が無いと。
570       #       end
571       #       return k if k == idss
572       #       if idss.include?(k) #<C5-4C4D><C6-4A37>この二文字のBUG対策
573       #  #return idss.sub(k, '')
574       #  return k #IDSに展開する方法が無いと。
575       #       end
576       #       return idss
577     end
578     def decompose_all
579       pde = ""
580       de = self.decompose #出発点
581       level = 0
582       while true
583         pde = de
584         de = pde.decompose #もう一度分解をしてみる。
585         break if pde == de #ループを抜けだす
586         exit if 10 < level #p ['too many recursive', self] 
587         level += 1
588       end
589       return de
590     end
591     def decompose_all_nu(level=nil)
592       level = 0 if level.nil?
593       if 10 < level
594         p ['too many recursive', self] 
595         exit
596       end
597       de = self.decompose
598       return de.decompose_all(level+1) if de != self #なにか変化があったから再帰
599       return de #もうこれ以上変化は無さそうだぞと。
600     end
601     def is_ids?() 0x2ff0 <= @char_id && @char_id <= 0x2fff end
602     def ids_operator_argc()
603       return 0 unless is_ids?
604       return 3 if @char_id == 0x2ff2 || @char_id == 0x2ff3
605       return 2
606     end
607   end
608
609 end
610
611 #----------------------------------------------------------------------終了