UTF8の文字境界を判定

日本語英数字が混在した文章を、指定バイト数以下に切り詰める必要が出た。


common lispにはsubseqという関数があるが、そのまま使うと文字ごとにきられてしまう。

 (subseq "あいうえお" 0 4) ;本当は4バイト以下にしたい。
 ->"あいうえ" ;4文字以下となってしまう。

そこで、バイト列にしてから、きってみた。

(defvar x (sb-ext:string-to-octets "あいうえお"))
#(227 129 130 227 129 132 227 129 134 227 129 136 227 129 138)                                                               
(subseq x 0 4)
#(227 129 130 227)

しかしこれだと、文字列に戻す際にデコードできない。
最後の227が邪魔である。

(sb-ext:octets-to-string #(227 129 130 227))
->error

なら三バイトずつで切ればいいかというと、そうでもない。
UTF-8では1バイト文字も2バイト文字も3バイト文字も混在しているらしい。

(sb-ext:string-to-octets "あaいbう")
#(227 129 130  97  227 129 132  98  227 129 134) 
;------------  --  -----------  --  -----------
;     あ       a       い       b        う

こうなってしまうと、どこが文字境界か判定できないとパースできない。


困ったなーと思いながらwikiのUTF-8の項目を見ていたら仕様が書いてあった。
http://ja.wikipedia.org/wiki/UTF-8

ビットパターンは以下のようになっている。

 0xxxxxxx                                               (00-7f) 7bit
 110yyyyx 10xxxxxx                                      (c0-df)(80-bf) 11bit
 1110yyyy 10yxxxxx 10xxxxxx                             (e0-ef)(80-bf)(80-bf) 16bit
 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx                    (f0-f7)(80-bf)(80-bf)(80-bf) 21bit
 111110yy 10yyyxxx 10xxxxxx 10xxxxxx 10xxxxxx           (f8-fb)(80-bf)(80-bf)(80-bf)(80-bf) 26bit
 1111110y 10yyyyxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx  (fc-fd)(80-bf)(80-bf)(80-bf)(80-bf)(80-bf) 31bit

Unicodeのコードポイントを2進表記したものを、上のビットパターンのx, yに右詰めに格納する。
最短のバイト数で符号化するため、yの部分には最低1回は1が出現する。
符号化されたバイト列は、[[バイトオーダー]]に関わらず左から順に出力する。
これにより4バイトで21bit、6バイトで31bitまで表現することができる。

これを見るに、トップのバイトは必ず、一番左の桁が0、もしくは1ならば一つ右の位も同じく1である。
よって、以下のようなコードで判定できた。
ビット演算は初めてだったからと惑った。

(defun top-byte-utf-8? (byte)
  (declare (optimize (speed 3) (safety 0)))
  (declare (fixnum byte))
  (or (= (logand (ash byte -7) 1) 0)
      (= (logand (ash byte -6) 1) 1)))

> (mapcar (lambda (byte) (top-byte-utf-8? byte))
          '(227 129 130 97 227 129 132 98 227 129 134))
->(T NIL NIL T T NIL NIL T T NIL NIL) 

これで、指定されたバイトがトップであるかどうかわかった。
よってこれをつかって解決できた。

(proclaim '(inline top-byte-utf-8?))

(defun subseq-string-utf-8 (sequence start &optional end) ;;内部string用
  (let* ((seq (sb-ext:string-to-octets sequence))
         (len (length seq)))
    (declare (simple-array seq) (fixnum len) (fixnum end)
             (optimize (speed 3) (safety 0)))
    (if (and end (or (<= len end) (< end 0)))
        (setq end len)
        (do () ((top-byte-utf-8? (elt seq end)))
          (setq end (1- end))))
    (the simple-array (sb-ext:octets-to-string (subseq seq start end)))))

(defun subseq-vector-utf-8 (sequence start &optional end) ;;webなど外部からとってきたstring用
  (let ((len (length sequence)))
    (declare (simple-array sequence) (fixnum len start end)
             (optimize (speed 3) (safety 0)))
    (if (and end (or (<= len end) (< end 0)))
        (setq end len)
        (do () ((top-byte-utf-8? (char-code (elt sequence end))))
          (setq end (1- end))))
    (the simple-array (subseq sequence start end))))

;;test
>(subseq-string-utf-8 "あaいbう" 0 2)
->""
>(subseq-string-utf-8 "あaいbう" 0 3)
->"あ"
>(subseq-string-utf-8 "あaいbう" 0 4)
->"あa"
>(subseq-string-utf-8 "あaいbう" 0 5)
->"あa"
>(subseq-string-utf-8 "あaいbう" 0 6)
->"あa"
>(subseq-string-utf-8 "あaいbう" 0 7)
->"あaい"
>(subseq-string-utf-8 "あaいbう" 0 8)
->"あaいb"
>(subseq-string-utf-8 "あaいbう" 0 9)
->"あaいb"
>(subseq-string-utf-8 "あaいbう" 0 10)
->"あaいb"
>(subseq-string-utf-8 "あaいbう" 0 11)
->"あaいbう"

うまくいっている。


でも、僕が知らないだけでおそらく組み込みのもっと確実な関数があるはずなんだけど・・・
知ってるひといたら教えてください ( _ _ )