OCaml

UTF-8文字列を桁数を指定して画面に出力したい(OCaml)

Ubuntuの端末上で動作するアプリケーションを作ったのだが、データを画面に出力するさいに、

String.sub s start len

を使うとすると、うまくいかない。

たとえば、「大阪市立図書館」の「大阪市立」だけを画面に出力しようと、

String.sub "大阪市立図書館" 0 8

とすると、

大阪å¸

となる。

(8 としたのは、画面上では全角文字1文字は、半角2文字分だから)

狙い通りにするには、1文字3バイトなので、

String.sub "大阪市立図書館" 0 12

としなくてはならない。

これは非常に面倒である。

半角英数字と全角文字が混在しているようなデータでは、困ることになる。

半角英数字を対象とした指定のしかたで、全角文字も指定できればいいのだが。

ということで、以下のユーティリティを作成した。

使い方は、

show_length s len

上記の例では、

show_length "大阪市立図書館" 8

でOK。

$ ocamlc -c len.ml

としておいて、

$ ocaml

# #load "len.cmo";;
# open Len;;

とすれば使える。

# show_length "大阪市立図書館" 8;;
- : string = "大阪市立"

なお、データが表示したい画面桁数よりも小さい場合、たとえば、

# show_length "図書館" 10;;

とした場合は、足らない部分を半角空白で埋めることとした。

コード

(*
 * len.ml -- 文字列の長さを取得する
 *
 * han_length s -- string -> int 
 *                 文字列s 中の半角文字数を求める。
 * zen_length s -- string -> int
 *                 文字列s 中の全角文字数を求める。
 * mblength s   -- string -> int
 *                 文字列s 中の半角文字数 + 全角文字数 * 2 を求める。
 *                 つまり、画面に表示される桁数を求める。
 * show_length s n -- string -> int -> string
 *                 文字列s を n桁 で表示する。
 *                 もし、文字列s が、n桁よりも小さければ、空白で埋める。
 *                 もし、文字列s が、n桁よりも大きければ、その分を削除する。
 *)
let s1 = "google";;
let s2 = "itソリューション";;
let s3 = "大阪市立図書館";;

(* あ -- \227 \129 \130
 * い -- \227 \129 \132
 *   -- \227 \128 \128 (全角空白)
 *)

(*
 * 半角文字数を求める関数
 *)
let han_length s =
    let rec loop n x sw =
        if n = (String.length s) then x
        else
            let c = s.[n] in
            if (Char.code c) >= 227
            then loop (n+1) x (sw+1)
            else
                match sw with
                    0 -> loop (n+1) (x+1) sw
                    | 3 -> loop (n+1) x 0
                    | _ -> loop (n+1) x (sw+1)
    in
    loop 0 0 0

(*
 * 全角文字数を求める関数
 *)
let zen_length s =
    let rec loop n x sw =
        if n = String.length s then x
        else
            let c = s.[n] in
            if (Char.code c) >= 227
            then loop (n+1) (x+1) (sw+1)
            else
                match sw with
                    0 -> loop (n+1) x sw
                    | 3 -> loop (n+1) (x+1) 0
                    | _ -> loop (n+1) x (sw+1)
    in
    loop 0 0 0


(*
 * 半角文字数 + 全角文字数 * 2 を求める関数
 *)
let mblength s =
    (han_length s) + (zen_length s) * 2


(*
 * 文字列の各文字が1バイト文字か utf-8かをチェックし、
 * その結果をリストで返す
 * @return: int list
 *    (例)
 *    "itコム" -- [0; 0; 1; 2; 3; 1; 2; 3]
 *    0 -- 1バイト文字
 *    1 -- utf-8 の 1バイトめ
 *    2 -- utf-8 の 2バイトめ
 *    3 -- utf-8 の 3バイトめ
 *)
exception Coming_3
let mbcheck s =
    try
        let rec loop n sw x =
            if n = String.length s then List.rev x
            else
                let c = (Char.code s.[n]) in
                if c >= 227 && sw = 0
                then loop (n+1) (sw+1) (1::x) 
                else
                    match sw with
                        0 -> loop (n+1) (sw) (0::x)
                        | 1 -> loop (n+1) (sw+1) (2::x)
                        | 2 -> loop (n+1) 0 (3::x)
                        | _ -> raise Coming_3
        in
        loop 0 0 []
    with Coming_3 -> print_endline "Coming_3"; []


(*
 * 文字列s を先頭v から n文字分切り取った文字列を求める関数
 * 文字列s は、n文字分以上の長さがあるものとする
 * @param:
 *    s : string -- 対象となる文字列
 *    v : int    -- スタート位置
 *                (0 を想定している。他はまだ考えていない)
 *    n : int    -- 表示したい桁数
 * @return: 文字列
 *    画面上では全角文字は、1文字につき、1バイト文字2桁分を必要とする。
 *    String.sub s v n の場合、画面上で全角文字を4桁で表示したいとする
 *    なら、n には、6文字分を与えなければならない。
 *    すなわち、String.sub s 0 5 となる。
 *)
let mbsubstr s v n =
    (*
     * 与えられた n という桁数を
     * 1バイト文字なら 1 
     * 3バイト文字なら 3 というふうに換算して
     * 新しい n2 というバイト文字数に変換する
     * String.sub s n2
     *)
    let get_new_n x =
        let ml = mbcheck s in
        let rec loop n2 x l = 
            if x = 0 then n2
            else
                match l with
                    [] -> n2
                    | a :: rest ->
                        match a with
                            0 -> loop (n2 + 1) (x-1) rest
                            | 3 -> loop (n2 + 1) (x-2) rest
                            | _ -> loop (n2 + 1) x rest
        in
        loop 0 x ml
    in
    String.sub s v (get_new_n n)


(*
 * n 個の連続した s を求める関数
 *)
let rec rep s n =
    if n = 1 then s
    else
        s ^ (rep s (n-1))

(*
 * 決められた文字数で文字列を表示する
 * @param: s : string -- 表示する文字列
 *         l : int    -- 表示の幅(文字数)
 * @return: string -- 決められたサイズの文字列
 *                    サイズの小さな文字列は空白で埋めることとする
 *)
let show_length s l =
    let s_length = mblength s in
    if s_length < l
    then s ^ (rep " " (l - s_length))
    else
        if s_length = l
        then s
        else mbsubstr s 0 l