2009年1月13日火曜日

[LSL] 文字列のバイト数を調べたい - llDialog

前回の投稿の後半で書いたお話ですが、HUD といった造形ができない私にとって llDialog が唯一のインターフェースになります。(笑

このダイアログのメッセージやボタンの内容が固定なものであれば、あらかじめ 512 bytes なり、 24 bytes といった最大長以内に収めることでスクリプト エラー の "Object: llDialog: button labels must be 24 or fewer characters long""Object: llDialog: message too long, must be less than 512 characters" を回避できますが、動的にメッセージやボタンを変更しようとすると、llStringLength だけでは対応できない場合が日本語環境では出てきます。

0

in-world では多言語対応として UTF-8 を使っているためで、たとえば 「ABCEDFGHIJKLMN」 は llStringLength では14文字で、実際のバイト数は14Bytesですが、「あいうえおかきくけ」 は9文字で27Bytesとなります。
めんどうなのは混在のパターンで 「ABあいうえおかきGH」 はllStringLengthでは11文字ですが、バイト数は25Bytesとなります。
アルファベットや数字などの ASCII 文字は 1バイト、ひらがななどは 3バイトを使うことになります。注意しなくてはいけないのは、半角カタカナなども 3バイト使います。昔の(笑) SBCS とか DBCS ってもう過去の話なんですねぇ、、、。

UTF-8 の日本語の文字数とバイト数についてはこちらが参考になります。

llDialog のスクリプトエラーに 24文字以下にしなさい、、、と出ますが、これ、24 bytes 以下にしなさい、という意味です。なので、上の例だと、「あいうえおかきくけ」は9文字ですが、バイト数は27bytesのためエラーになります。

実際、ボタンは表示するエリアが狭いため、アルファベットでも9文字程度しか表示できません。ボタンに長い文字列を入れることは現実的ではありませんが、ダイアログのメッセージのエリアに [1] ボタンの説明で 1:XXXXXXXX というのをボタンの数ぶん入れていくと、512バイトを超えることがたまに出るようになってきました。

1文字3バイトの日本語にあわせて llStringLengthllGetSubString で表示する文字数を制限するやり方が確実、安全ですが、利用できるアルファベット、数字などがその制限で 1/3 の文字数に減ってしまうのはいただけず、また、その中間の値をとると、当たり前ですが、今度は全部日本語になるとエラーになり、、、。

エラーで文字切捨ててダイアログを表示してくれるといいのですが、このエラーが発生するとダイアログが表示されないので、スクリプトとして確実に機能しないのが嫌でした。ただ、ボタンのラベルが返信されるので、仕方ない仕様と納得できます。。。

なんか、、、バイト数のチェックの話しは先輩の皆様が当の昔にやっていそうなんですが、検索の仕方が悪いのか、日本語対応がこの1年くらいで可能になったせいか、よいサンプルなどを探し出すことができませんでした。それで llGetStringBytes みたいのが欲しい、、、というのが前回の投稿の後半部分だったわけです。

で、、、先日、閃きました(笑


URL エンコードを使ってバイト数を計算する
文字列のバイト数を計算するには UTF-8 で 3 バイト使っている文字がどれかを判断してバイト数を計算すればいいわけですよね。 ASCII 文字か、そうじゃないかは URL エンコードに変換することでおおよそ確認できることに気がつきました。
URL エンコードllHTTPRequest などでも使いますし、昔、スクリプトのエディタで日本語の直接入力ができなかったときにお世話になっていました。

また、以前の投稿にある文字列変換で利用した llParseString2List を使って、極力 for 文や while 文を使わず、List 操作だけで処理することで、パフォーマンスの問題もなんとかなりそうな気がしました。というのも UTF-8 の ASCII 文字以外は URL エンコードすると % が必ず出てきます。たとえば「あ」は %E3%81%82 となるので % の数がバイト数と考えることができますね。また、\ や & といった特殊な文字も、%5C や %26 としてくれるので % の数をバイト数としても問題なさそうです。

問題は混在文字列です。「aあ」 は a%E3%81%82 となり、「あabc」 は %E3%81%82abc となります。このような混在文字列を llParseString2List で % を使って分解すると、
「aあ」 -> a%E3%81%82 -> ["a","%","E3","%","81","%","82"]
「あabc」 -> %E3%81%82abc -> ["%","E3","%","81","%","82abc"]
「abあcd」 -> ab%E3%81%82cd -> ["ab","%","E3","%","81","%","82cd"]
「あabい」 -> %E3%81%82ab%E3%81%84 -> ["%","E3","%","81","%","82ab","%","E3","%","81","%","84"]
となります。ASCII 文字が 3 バイトの文字に続くと直前の 3 バイト文字のエンコードの一部の List 要素の中に入ってしまうわけです。(上記赤の部分)
また、ASCII 文字が先頭にきた場合や ASCII 文字しかない場合も考慮すると % の数だけ、、というわけではありませんよね。

ですが、List 要素の数と % の数とバイト数の関係はこの例から

         バイト数 = List にあるすべての文字数の合計 - (%のList要素の数X2)

になりますよね。これなら混在環境でもバイト数を求めることができそうです。
ただ、%の要素数を求めるために for 文をまわすのは避けたいところ、、、。そこで MLDU のスロット管理でも使っている方法を応用してみます。% はこの利用方法の List の中では特殊な文字なので llListSort を使って対象となる要素数を求めることができそうです。

たとえば「あabい」の場合だと
URL エンコード: %E3%81%82ab%E3%81%84
LSLで分割後  : ["%","E3","%","81","%","82ab","%","E3","%","81","%","84"]
LSLでSort後 : ["E3","E3","84","82ab","81","81","%","%","%","%","%","%"]
Sort した後の List に対して、llListFindList で ["%"] を探すと、一番最初の % がある List 要素順の 6 という数値が戻されます。

よって、List の全要素数から 6 を引いた残りの数が % の要素数になります。
まったく 3バイトの文字がない場合も考慮して、ユーザー関数にしてみたのが以下になります。
getStringBytes のパラメータに文字列を渡すと、integer 型で byte 数を返すものです。

integer getStringBytes(string p1){
    integer len = 0;
    list temp = llParseString2List(llEscapeURL(p1),[],["%"]);
    if(llListFindList(temp,["%"])!=-1){
        temp = llListSort(temp,1,FALSE);
        integer index = llListFindList(temp,["%"]);
        integer u_bytes = llGetListLength(temp)-index;
        len = llStringLength(llDumpList2String(temp,""))-u_bytes*2;
    }else{
        len = llStringLength(p1);
    }
    return len;
}

みゅう、、、コードにするとシンプルですね、、、
[追記 2009.5.1] jinko さんが提示してくれたスクリプトのほうが素敵スクリプトですので参照してください。
http://jinko.slmame.com/e602732.html

//----------------------------------------------------
//  文字のバイト数
//  参考:http://mydiary.slmame.com/e492006.html
//----------------------------------------------------
integer strlenB(string str){
    string  esc = llEscapeURL(str);
    string  wk  = llDumpList2String(llParseString2List(esc,["%"],[]),""); //素敵!!
    integer cnt = llStringLength(esc) - llStringLength(wk);
    return llStringLength(esc) - cnt * 2;
}
--------- by jinko-san --------------------------------
[追記おわり]

文字列のバイト長をチェックした後の超過分の文字削除操作はパフォーマンスも考慮しなくてはいけないのでまだ先は長そうですが、、、でも、これでダイアログの説明表示の文字数の制御がうまくできるかも。
って、、、ぜんぜん本来作りたいスクリプトと違う枝葉の部分のお話しでした、、、(笑
ご参考になればうれしいかぎりです~。

6 件のコメント:

  1. はじめまして^^
    あ、なるほど、そんな感じで%の数調べたらいいんですね。
    私は1文字づつ調べて%をカウントしてたんですよ^^;;

    返信削除
  2. はじめまして~。
    やはり、悩んでいた方はいらっしゃったのですね。
    llListSort の方法は MLDU のダンススロットの空き数を管理するのに、やはり最初 for で回して使い物にならず、しばらく悩んで、、、(笑
    参考になったようでなによりです^^

    返信削除
  3. 前回の投稿よりちらちら考えていましたが、自分も求め方を見つけたのでトラックバック記事を書かせていただきました。
    参考になれば^^

    返信削除
  4. この方法、、、チェックする文字列を最小限にしないとすぐに stack/heap エラーになるので注意ですね。。。

    返信削除
  5. こんにちはー
    すばらしいアイディアですねー
    感激しましたー
    ワタシも、このアイディアをもとに関数書いてみましたのでトラバさせていただきました
    玉砕覚悟で^^;;

    返信削除
  6. Whitfield-In-World2009年5月1日 20:15

    jinkoさん、はじめまして~。
    って、完璧です!llListSort 使わないから、jinko さんのスクリプトのほうが日本語混在の場合は軽いですね。英語だけでもそんなに変わらないし、、、ありがとうございます!
    わたしも目からウロコでした、、、
    llParseString2List で % なくして、元の文字数からその文字数を引けば、、、% の数ですよね!
    ステップ数も少ないし、ほんとうにすばらしいです。

    返信削除