nkf を使用した文字コード変換ライブラリCinnamon についてUCS4⇔UTF8の変換にてバグらしきものを発見したと指摘があります。
指摘されているのは UCS4 の1文字を Utf8 の複数バイトに変換する ucs4CharToUtf8Chars 関数です。
文字列を変換して比較し、ucs4CharToUtf8Chars について検証してみました。
- まず、rfc2279。
module Main where import Data.Bits ((.&.), (.|.), shiftR) import Data.Char (chr, ord) import Codec.Binary.UTF8.String (encodeString) {- 参照: <http://www.ietf.org/rfc/rfc2279.txt> UCS-4 range (hex.) UTF-8 octet sequence (binary) 0000 0000-0000 007F 0xxxxxxx 0000 0080-0000 07FF 110xxxxx 10xxxxxx 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx 0001 0000-001F FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 0020 0000-03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 0400 0000-7FFF FFFF 1111110x 10xxxxxx ... 10xxxxxx -- http://d.hatena.ne.jp/sirocco/20100331/1270027037 Prelude> Numeric.showIntAtBase 2 Char.intToDigit 123 "" -- 2進数文字列に "1111011" Prelude> Numeric.showIntAtBase 4 Char.intToDigit 123 "" -- 4進数文字列に "1323" Prelude> Numeric.showIntAtBase 16 Char.intToDigit 123 "" -- 16進数文字列に "7b" -} -- 【Cinnamonオリジナル】 -- | UCS4の文字をUTF8の文字に変換します。 -- UTF8の文字は最大Char6文字分で表現されます。 ucs4CharToUtf8Chars :: Char -> [Char] ucs4CharToUtf8Chars c | c' <= 0x0000007f = [c] | c' <= 0x000007ff = map chr $ marks 0xc0 $ masks 2 | c' <= 0x0000ffff = map chr $ marks 0xe0 $ masks 3 | c' <= 0x1fffffff = map chr $ marks 0xf0 $ masks 4 | c' <= 0x3fffffff = map chr $ marks 0xf8 $ masks 5 | c' <= 0x7fffffff = map chr $ marks 0xfc $ masks 6 | otherwise = error "ucs4CharToUtf8Char: out of range." where c' :: Int c' = ord c mask :: Int -> Int mask n = (c' `shiftR` (n * 6)) .&. 0x3f masks :: Int -> [Int] masks n = map mask $ reverse $ [0 .. n - 1] marks :: Int -> [Int] -> [Int] marks m0 = zipWith (\ m c -> m .|. c) (m0 : repeat 0x80) -- | UCS4の文字列をUTF8の文字列に変換します。 ucs4ToUtf8 :: String -> String ucs4ToUtf8 = concatMap ucs4CharToUtf8Chars -- 【指摘のあったもの】 -- | UCS4の文字をUTF8の文字に変換します。 -- UTF8の文字は最大Char6文字分で表現されます。 ucs4CharToUtf8Chars2 :: Char -> [Char] ucs4CharToUtf8Chars2 c | c' <= 0x0000007f = map chr $ marks 0x00 $ masks 1 0x7f | c' <= 0x000007ff = map chr $ marks 0xc0 $ masks 2 0x3f | c' <= 0x0000ffff = map chr $ marks 0xe0 $ masks 3 0x3f | c' <= 0x1fffffff = map chr $ marks 0xf0 $ masks 4 0x3f | c' <= 0x3fffffff = map chr $ marks 0xf8 $ masks 5 0x3f | c' <= 0x7fffffff = map chr $ marks 0xfc $ masks 6 0x3f | otherwise = error "ucs4CharToUtf8Char: out of range." where c' :: Int c' = ord c mask :: Int -> Int -> Int mask m n = (c' `shiftR` (n * 6)) .&. m masks :: Int -> Int -> [Int] masks n m= map (mask m) $ reverse $ [0 .. n - 1] marks :: Int -> [Int] -> [Int] marks m0 = zipWith (\ m c -> m .|. c) (m0 : repeat 0x80) ucs4ToUtf8' :: String -> String ucs4ToUtf8' = concatMap ucs4CharToUtf8Chars2 -- 【utf8-Stringのパクリ】 ucs4CharToUtf8Chars3 :: Char -> [Char] ucs4CharToUtf8Chars3 c | c' <= 0x7f = [c] -- 0x7f 以下(ASCII)は同じ -- ラテン補助、拡張、・・・ギリシャ文字、アラビア文字 | c' <= 0x7ff = map chr [ 0xc0 + (c' `shiftR` 6) -- 右へ6ビットシフトした残りと 110B の OR , 0x80 + c' .&. 0x3f] -- 甲斐6ビットをマスクし 10B と OR -- ひらがな、漢字、ハングルなど。Windows IME 文字パッドはここまで | c' <= 0xffff = map chr [ 0xe0 + (c' `shiftR` 12) -- 右へ6ビットシフトして1110B と OR , 0x80 + ((c' `shiftR` 6) .&. 0x3f) -- 右へ6ビットシフトして下位6ビットをマスク -- 10B と OR , 0x80 + c' .&. 0x3f] -- 下位6ビットをマスクし 10B と OR -- それ以外は4バイト表現。5バイト、6バイト表現の文字には対応していない。 | otherwise = map chr [ 0xf0 + (c' `shiftR` 18) , 0x80 + ((c' `shiftR` 12) .&. 0x3f) , 0x80 + ((c' `shiftR` 6) .&. 0x3f) , 0x80 + c' .&. 0x3f] where c' :: Int c' = ord c ucs4ToUtf8'' :: String -> String ucs4ToUtf8'' = concatMap ucs4CharToUtf8Chars3 -- UCS4 の文字コードは0x7fffffffまでありますがCharのmaxBound は0x10ffffです。 uni :: [Char] uni = map chr [0x0..0x10ffff] -- maxBound :: Char '\1114111' -- ucs.exe: Prelude.chr: bad argument: 1114112 -- Numeric.showIntAtBase 16 Char.intToDigit 1114111 "" --> "10ffff" main :: IO () main = do print (ucs4ToUtf8 uni == ucs4ToUtf8' uni) -- > True print (ucs4ToUtf8 uni == ucs4ToUtf8'' uni) -- > True print (ucs4ToUtf8' uni == encodeString uni) -- > True
0x0から0x10ffffのUCS4文字列を作りUTF8に変換後の文字列を比較しましたが違いはないようです。
- 速度の比較
-- print (length (ucs4ToUtf8 uni)) -- > 0m0.985s -- print (length (ucs4ToUtf8' uni)) -- > 0m0.901s -- print (length (ucs4ToUtf8'' uni)) -- > 0m0.381s -- print (length (encodeString uni)) -- > 0m0.286s
速度については、utf8-stringのencodeStringが最速、utf8-string とほぼ同じものが次いで、さらに、修正版、オリジナルとなります。
nkfを用いた文字コード変換ライブラリCinnamon はUCS4からUTF8に変換し、それをnkfで変換しています。UCS4⇔UTF8の部分をencodeString、decodeStringを使用すればコードもすっきりし、高速になります。その変更手順を後日書きます。