Windows 1258 から UTF-8への変換

概要

この話題はあまりにマニアックすぎて、どれほどの人によって有用かわからないが、自分のためのメモとして書いてみる。

ベトナム語は、日本語同様、さまざまな文字コードが使われていることで悪名高い。最近は UTF-8 へ収束しつつあるようだが、数年前まではひどい状態だったらしい。Lac Viet 2002 という有名な英語<=>ベトナム語辞典で使われている文字コードが Windows 1258 である。(ベトナム語文字コードについては、East Asian Character Sets Overview: Vietnameseがバイブル的存在) この Windows 1258 を UTF-8 に変換するコードを Ruby で書いてみた。

使い方

ソースコードを windows1258_converter.rb という名前で保存。

require 'windows1258_converter'

Windows1258Converter.toutf8([Windows 1258 文字列]) # => UTF-8 文字列

これだけ。

ソースコード

=begin
ベトナムの文字コードに関して、Windows 1258 から UTF-8 に変換する。

■使い方

Windows1258Converter.toutf8([Windows 1258 文字列]) # => UTF-8 文字列

■変換方法

まずは Windows 1258 から裸の Unicode である UCS-2 に変換し、ついで UTF-8 に変換する

1258 の特徴は、声調記号に独立した文字コードが当てられている点である。
厄介なのは、1258 では1バイトまたは2バイトのシーケンスが、2バイトの UCS-2 に置き換えられるということだ。
つまり可変長の文字列を変換する必要があるわけだ。

例をあげると

     1258     UCS-2  
a     61          00 61
a'    E1          00 E1
a`   E0          00 E0
a?   E0 D2     1E A3
a~   E0 DE     00 E3
a.    E0 F2     1E A1

声調記号のコードは次のとおり。
'(EC) `(CC) ?(D2) ~(DE) .(F2) 

変換の戦略としては次のような感じ。

1. 1258 => UCS-2 の変換表を作る(ハッシュ)
2. 変換対象文字列の現在位置から向こう2バイトをキーとして、変換表に対応する UCS-2 表現が見つかるかどうか調べる。
3. 2 で見つかったら、それを UCS-2 表現とする。見つからなかったら次へ。
4. 変換対象文字列の現在位置から向こう1バイトをキーとして、変換表に対応する UCS-2 表現が見つかるかどうか調べる。
5. 4 で見つかったら、それを UCS-2 表現とする。見つからなかったら次へ。
6. 変換対象文字列の現在位置から向こう1バイトを取り出す。これを下位1バイト、上位1バイトを0とする2バイトの文字列をつくり、これを UCS-2 表現とする。

=end

class Windows1258Converter

#'(EC) `(CC) ?(D2) ~(DE) .(F2) 

	@@map_source =<<EOS
A		41	0041
A	'	C1	00C1
A	`	C0	00C0
A	?	41D2	1EA2
A	~	41DE	00C3
A	.	41F2	1EA0
a		61	0061
a	'	E1	00E1
a	`	E0	00E0
a	?	61D2	1EA3
a	~	61DE	00E3
a	.	61F2	1EA1
A(		C3	0102
A(	'	C3EC	1EAE
A(	`	C3CC	1EB0
A(	?	C3D2	1EB2
A(	~	C3DE	1EB4
A(	.	C3F2	1EB6
a(		E3	0103
a(	'	E3EC	1EAF
a(	`	E3CC	1EB1
a(	?	E3D2	1EB3
a(	~	E3DE	1EB5
a(	.	E3F2	1EB7
A^		C2	00C2
A^	'	C2EC	1EA4
A^	`	C2CC	1EA6
A^	?	C2D2	1EA8
A^	~	C2DE	1EAA
A^	.	C2F2	1EAC
a^		E2	00E2
a^	'	E2EC	1EA5
a^	`	E2CC	1EA7
a^	?	E2D2	1EA9
a^	~	E2DE	1EAB
a^	.	E2F2	1EAD
E		45	0045
E	'	C9	00C9
E	`	C8	00C8
E	?	45D2	1EBA
E	~	45DE	1EBC
E	.	45F2	1EB8
e		65	0065
e	'	E9	00E9
e	`	E8	00E8
e	?	65D2	1EBB
e	~	65DE	1EBD
e	.	65F2	1EB9
E^		CA	00CA
E^	'	CAEC	1EBE
E^	`	CACC	1EC0
E^	?	CAD2	1EC2
E^	~	CADE	1EC4
E^	.	CAF2	1EC6
e^		EA	00EA
e^	'	EAEC	1EBF
e^	`	EACC	1EC1
e^	?	EAD2	1EC3
e^	~	EADE	1EC5
e^	.	EAF2	1EC7
I		49	0049
I	'	CD	00CD
I	`	49CC	00CC
I	?	49D2	1EC8
I	~	49DE	0128
I	.	49F2	1ECA
i		69	0069
i	'	ED	00ED
i	`	69CC	00EC
i	?	69D2	1EC9
i	~	69DE	0129
i	.	69F2	1ECB
O		4F	004F
O	'	D3	00D3
O	`	4FCC	00D2
O	?	4FD2	1ECE
O	~	4FDE	00D5
O	.	4FF2	1ECC
o		6F	006F
o	'	F3	00F3
o	`	6FCC	00F2
o	?	6FD2	1ECF
o	~	6FDE	00F5
o	.	6FF2	1ECD
O^		D4	00D4
O^	'	D4EC	1ED0
O^	`	D4CC	1ED2
O^	?	D4D2	1ED4
O^	~	D4DE	1ED6
O^	.	D4F2	1ED8
o^		F4	00F4
o^	'	F4EC	1ED1
o^	`	F4CC	1ED3
o^	?	F4D2	1ED5
o^	~	F4DE	1ED7
o^	.	F4F2	1ED9
O+		D5	01A0
O+	'	D5EC	1EDA
O+	`	D5CC	1EDC
O+	?	D5D2	1EDE
O+	~	D5DE	1EE0
O+	.	D5F2	1EE2
o+		F5	01A1
o+	'	F5EC	1EDB
o+	`	F5CC	1EDD
o+	?	F5D2	1EDF
o+	~	F5DE	1EE1
o+	.	F5F2	1EE3
U		55	0055
U	'	DA	00DA
U	`	D9	00D9
U	?	55D2	1EE6
U	~	55DE	0168
U	.	55F2	1EE4
u		75	0075
u	'	FA	00FA
u	`	F9	00F9
u	?	75D2	1EE7
u	~	75DE	0169
u	.	75F2	1EE5
U+		DD	01AF
U+	'	DDEC	1EE8
U+	`	DDCC	1EEA
U+	?	DDD2	1EEC
U+	~	DDDE	1EEE
U+	.	DDF2	1EF0
u+		FD	01B0
u+	'	FDEC	1EE9
u+	`	FDCC	1EEB
u+	?	FDD2	1EED
u+	~	FDDE	1EEF
u+	.	FDF2	1EF1
Y		59	0059
Y	'	59EC	00DD
Y	`	59CC	1EF2
Y	?	59D2	1EF6
Y	~	59DE	1EF8
Y	.	59F2	1EF4
y		79	0079
y	'	79EC	00FD
y	`	79CC	1EF3
y	?	79D2	1EF7
y	~	79DE	1EF9
y	.	79F2	1EF5
DD		D0	0110
dd		F0	0111
dong		FE	20AB
EOS

	@@instance = nil
	@map = nil	

	def hex_to_str(s)
		s.gsub!(/ /, '')
		i = 0
		r = ''
		while i < s.size
			n = s[i, 2].hex	
			c = n.chr
			r += c
			i += 2
		end
		r
	end

	def load_map
		@map = {}
		@@map_source.each do |line|
			ch, tone, w, u = line.chomp.split(/\s/)
			ws = hex_to_str(w) 
			uc = u.hex
			@map[ws] = uc
		end
	end

	# 一文字のUCS-2 を UTF-8 に変換
	def ucs2_to_utf8(c)
	  raise if c < 0
	  return sprintf("%c", c) if c <= 0x7F
	  if c <= 0x07FF
	      c1 = 0b1100_0000 | (c >> 6)
	      c2 = 0b1000_0000 | (c & 0b111111)
	      return sprintf("%c%c", c1, c2)
	  end
	  if c <= 0xFFFF
	      c1 = 0b1110_0000 | (c >> 12)
	      c2 = 0b1000_0000 | ((c >> 6) & 0b111111)
	      c3 = 0b1000_0000 | (c & 0b111111)
	      return sprintf("%c%c%c", c1, c2, c3)
	  end
	  raise
	end

	# windows 1258 => utf-8 変換
	def windows1258_to_utf8(w)
		i = 0
		u = ''
		while i < w.size
			# v に ucs-2 表現が入る
			v = nil
			# 2バイトのキーを試す
			if w.size - i >= 2
				key = w[i, 2]
				v = @map[key]
				i += 2 if v
			end
			# 1バイトのキーを試す
			if v.nil?
				key = w[i, 1]
				v = @map[key]
				i += 1 if v
			end
			# それでも見つからなかったら、0x00 を上位バイトに付け加えて2バイトの UCS-2 を作る
			if v.nil?
				v =  w[i]
				i += 1
			end
			uch = ucs2_to_utf8(v)
			u += uch
		end
		u
	end


	def initialize
		load_map
	end
	
	def self.instance
		unless @@instance 
			@@instance = Windows1258Converter.new
		end
		@@instance
	end
	
	def self.toutf8(w)
		instance.windows1258_to_utf8(w)
	end

end