octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

Oji --- バイト列の文字境界を識別する

 Ojiは文字符号化の方法が分かっているバイト列における「一文字」の範囲を識別するライブラリです。

github.com

 どうしよう、これ。

いちおう動機

flexi-streamsにASCII範囲外の文字を渡してstream-read-charすると、正しく一文字を認識してくれない現象を発見しました。が、これは誤りでした

以下がまず、CL標準のreadでテキストストリームから一文字を取ってきた場合です。ひらがなの「こ」が取得されているのがわかります。

;; 文字列の一文字目「こ」が読まれる
CL-USER> (defparameter str "これはペンです")
STR
CL-USER> (with-input-from-string (in str)
           (format t "read from stream: ~s~%" (read-char in)))
read from stream: #\HIRAGANA_LETTER_KO
NIL

次に、flexi-streamsのほうでstream-read-charした場合です。こちらは、flexi-streamsのin-memory stream (バイナリストリーム) をbivalentなストリーム (バイナリ・テキスト両ストリームとして扱える) に変換した場合に発生します。

;; 文字列の一文字目「こ」ではない文字が読まれる
CL-USER> (defparameter octets (flex:string-to-octets str :external-format :utf-8))
OCTETS
CL-USER> (flex:with-input-from-sequence (in octets)
           (format t "read from flexi stream: ~s~%"
                   (trivial-gray-streams:stream-read-char
                    (flex:make-flexi-stream in))))
read from flexi stream: #\LATIN_SMALL_LETTER_A_WITH_TILDE

flexi streamに対してのstream-read-charでは~が読み取られていますが、これは「こんにちは」をUTF-8で符号化したときの最初のバイトの値です。

;; UTF-8の文字としてではなく、ASCIIバイト列として一番目を見てみる
CL-USER> (format t "first byte as a character: ~s~%"
                   (code-char (aref octets 0)))
first byte as a character: #\LATIN_SMALL_LETTER_A_WITH_TILDE
NIL

と、ここまで書いてて気づいたんですが、実際には、flexi streamを作るときにexternal-formatを指定すれば、ちゃんとデコードしてくれます。ほんといま気づいた……😢

;; なんとexternal-format指定漏れ!
CL-USER> (flex:with-input-from-sequence (in octets)
           (format t "read from flexi stream: ~s~%"
                   (trivial-gray-streams:stream-read-char
                    (flex:make-flexi-stream in :external-format :utf-8))))
read from flexi stream: #\HIRAGANA_LETTER_KO

 flexi-streamsは古くからあるライブラリだし、そういう部分は枯れてて当然ですね。

 はずかしい……。消えたい……。

使い方

CL-USER> (setf moji (oji:load-bytes (babel:string-to-octets "これはペンです" :encoding :utf-8) :utf-8))
CL-USER> (oji:encoding moji)
:utf-8
;; read-charは未実装
CL-USER> (oji:read-char moji)
#\HIRAGANA_LETTER_KO  ;; it's not #\LATIN_SMALL_LETTER_A_WITH_TILDE
CL-USER> (oji:boundary moji)
((0 . 2) (3 . 5) (6 . 8) (9 . 11) ...)

感想とか今後とか

 どうしよう、これ。

 当初の目的としてはふたつあって

  1. flexi-streamsに妙な点があるので直したい (そんな点なかった)
  2. バイト列から文字境界認識や区点番号へのデコードをすることで、文字符号化方式への理解を深める

だったんですが、そのうち1が潰えてしまい、えええーーー……😩

 そうすると、このライブラリ、何に使えるのだろう。とりあえずUTF-8の文字境界認識と区点番号デコードができたので、るんるん気分で「これからinquisitorサポートの文字コードについて実装していきます!!」とか書こうと思ってたのに……。

 動揺していますが、まあ遊びで使うことはできそうだし、調べるの楽しかったので、開発がんばろうと思います!