LoginSignup
7
6

More than 5 years have passed since last update.

JavaScriptにmb_strwidthとmb_strimwidthを移植してみた

Last updated at Posted at 2015-11-19

Selenium-IDEで完全なテストを行うために、JavaScriptにmb_strwidthとmb_strimwidthを移植してみた。

その際にPHPのmb_strimwidthで空文字が返ってくる場合がある事を発見したため、併せて記載する。

検証バージョン

PHP 5.6.15

移植したコード

fromChatCodeExt(移植コードではないが、コードポイントの文字列化に必要)

fromChatCodeExt
/**
 * 整数値で表現されたコードポイントをUTF-8文字に変換する。
 *
 * @param   int     code_point  UTF-8文字に変換したいコードポイント
 * @return  string  コードポイントから作成したUTF-8文字
 */
function fromChatCodeExt (code_point) {
    //サロゲートペア無しの文字の場合
    if (code_point < 0x10000) {
        //素直に処理して返す
        return String.fromCharCode(code_point);
    }

    //サロゲートペア付の文字の場合
    code_point -= 0x10000;
    return String.fromCharCode(0xD800 + (code_point >> 10), 0xDC00 + (code_point & 0x3FF));
}

mb_strwidth

mb_strwidth
/**
 * 文字列の幅を返す。
 *
 * この関数はPHPのmb_strwidth互換です。
 *
 * @param   string  str 幅を測りたい文字列
 * @return  int     文字列幅
 */
function mb_strwidth (str) {
    var i = 0;
    var str_width = 0;
    var next_char_code = 0;
    var char_code = 0;

    //一文字ずつ処理
    while (!Number.isNaN((char_code = str.charCodeAt(i++)))) {
        //サロゲートペア対応
        if (0xD800 <= char_code && char_code <= 0xDBFF) {
            next_char_code = str.charCodeAt(i);
            char_code = ((char_code - 0xD800) * 0x400) + (next_char_code - 0xDC00) + 0x10000;
            i++;
        }

        if (0x0 <= char_code && char_code <= 0x10FF) {
            str_width += 1;
        } else if (0x1100 <= char_code && char_code <= 0x115F) {
            str_width += 2;
        } else if (0x1160 <= char_code && char_code <= 0x11A2) {
            str_width += 1;
        } else if (0x11A3 <= char_code && char_code <= 0x11A7) {
            str_width += 2;
        } else if (0x11A8 <= char_code && char_code <= 0x11F9) {
            str_width += 1;
        } else if (0x11FA <= char_code && char_code <= 0x11FF) {
            str_width += 2;
        } else if (0x1200 <= char_code && char_code <= 0x2328) {
            str_width += 1;
        } else if (0x2329 <= char_code && char_code <= 0x232A) {
            str_width += 2;
        } else if (0x232B <= char_code && char_code <= 0x2E7F) {
            str_width += 1;
        } else if (0x2E80 <= char_code && char_code <= 0x2E99) {
            str_width += 2;
        } else if (0x2E9A <= char_code && char_code <= 0x2E9A) {
            str_width += 1;
        } else if (0x2E9B <= char_code && char_code <= 0x2EF3) {
            str_width += 2;
        } else if (0x2EF4 <= char_code && char_code <= 0x2EFF) {
            str_width += 1;
        } else if (0x2F00 <= char_code && char_code <= 0x2FD5) {
            str_width += 2;
        } else if (0x2FD6 <= char_code && char_code <= 0x2FEF) {
            str_width += 1;
        } else if (0x2FF0 <= char_code && char_code <= 0x2FFB) {
            str_width += 2;
        } else if (0x2FFC <= char_code && char_code <= 0x2FFF) {
            str_width += 1;
        } else if (0x3000 <= char_code && char_code <= 0x303E) {
            str_width += 2;
        } else if (0x303F <= char_code && char_code <= 0x3040) {
            str_width += 1;
        } else if (0x3041 <= char_code && char_code <= 0x3096) {
            str_width += 2;
        } else if (0x3097 <= char_code && char_code <= 0x3098) {
            str_width += 1;
        } else if (0x3099 <= char_code && char_code <= 0x30FF) {
            str_width += 2;
        } else if (0x3100 <= char_code && char_code <= 0x3104) {
            str_width += 1;
        } else if (0x3105 <= char_code && char_code <= 0x312D) {
            str_width += 2;
        } else if (0x312E <= char_code && char_code <= 0x3130) {
            str_width += 1;
        } else if (0x3131 <= char_code && char_code <= 0x318E) {
            str_width += 2;
        } else if (0x318F <= char_code && char_code <= 0x318F) {
            str_width += 1;
        } else if (0x3190 <= char_code && char_code <= 0x31BA) {
            str_width += 2;
        } else if (0x31BB <= char_code && char_code <= 0x31BF) {
            str_width += 1;
        } else if (0x31C0 <= char_code && char_code <= 0x31E3) {
            str_width += 2;
        } else if (0x31E4 <= char_code && char_code <= 0x31EF) {
            str_width += 1;
        } else if (0x31F0 <= char_code && char_code <= 0x321E) {
            str_width += 2;
        } else if (0x321F <= char_code && char_code <= 0x321F) {
            str_width += 1;
        } else if (0x3220 <= char_code && char_code <= 0x3247) {
            str_width += 2;
        } else if (0x3248 <= char_code && char_code <= 0x324F) {
            str_width += 1;
        } else if (0x3250 <= char_code && char_code <= 0x32FE) {
            str_width += 2;
        } else if (0x32FF <= char_code && char_code <= 0x32FF) {
            str_width += 1;
        } else if (0x3300 <= char_code && char_code <= 0x4DBF) {
            str_width += 2;
        } else if (0x4DC0 <= char_code && char_code <= 0x4DFF) {
            str_width += 1;
        } else if (0x4E00 <= char_code && char_code <= 0xA48C) {
            str_width += 2;
        } else if (0xA48D <= char_code && char_code <= 0xA48F) {
            str_width += 1;
        } else if (0xA490 <= char_code && char_code <= 0xA4C6) {
            str_width += 2;
        } else if (0xA4C7 <= char_code && char_code <= 0xA95F) {
            str_width += 1;
        } else if (0xA960 <= char_code && char_code <= 0xA97C) {
            str_width += 2;
        } else if (0xA97D <= char_code && char_code <= 0xABFF) {
            str_width += 1;
        } else if (0xAC00 <= char_code && char_code <= 0xD7A3) {
            str_width += 2;
        } else if (0xD7A4 <= char_code && char_code <= 0xD7AF) {
            str_width += 1;
        } else if (0xD7B0 <= char_code && char_code <= 0xD7C6) {
            str_width += 2;
        } else if (0xD7C7 <= char_code && char_code <= 0xD7CA) {
            str_width += 1;
        } else if (0xD7CB <= char_code && char_code <= 0xD7FB) {
            str_width += 2;
        } else if (0xD7FC <= char_code && char_code <= 0xF8FF) {
            str_width += 1;
        } else if (0xF900 <= char_code && char_code <= 0xFAFF) {
            str_width += 2;
        } else if (0xFB00 <= char_code && char_code <= 0xFE0F) {
            str_width += 1;
        } else if (0xFE10 <= char_code && char_code <= 0xFE19) {
            str_width += 2;
        } else if (0xFE1A <= char_code && char_code <= 0xFE2F) {
            str_width += 1;
        } else if (0xFE30 <= char_code && char_code <= 0xFE52) {
            str_width += 2;
        } else if (0xFE53 <= char_code && char_code <= 0xFE53) {
            str_width += 1;
        } else if (0xFE54 <= char_code && char_code <= 0xFE66) {
            str_width += 2;
        } else if (0xFE67 <= char_code && char_code <= 0xFE67) {
            str_width += 1;
        } else if (0xFE68 <= char_code && char_code <= 0xFE6B) {
            str_width += 2;
        } else if (0xFE6C <= char_code && char_code <= 0xFF00) {
            str_width += 1;
        } else if (0xFF01 <= char_code && char_code <= 0xFF60) {
            str_width += 2;
        } else if (0xFF61 <= char_code && char_code <= 0xFFDF) {
            str_width += 1;
        } else if (0xFFE0 <= char_code && char_code <= 0xFFE6) {
            str_width += 2;
        } else if (0xFFE7 <= char_code && char_code <= 0x1AFFF) {
            str_width += 1;
        } else if (0x1B000 <= char_code && char_code <= 0x1B001) {
            str_width += 2;
        } else if (0x1B002 <= char_code && char_code <= 0x1F1FF) {
            str_width += 1;
        } else if (0x1F200 <= char_code && char_code <= 0x1F202) {
            str_width += 2;
        } else if (0x1F203 <= char_code && char_code <= 0x1F20F) {
            str_width += 1;
        } else if (0x1F210 <= char_code && char_code <= 0x1F23A) {
            str_width += 2;
        } else if (0x1F23B <= char_code && char_code <= 0x1F23F) {
            str_width += 1;
        } else if (0x1F240 <= char_code && char_code <= 0x1F248) {
            str_width += 2;
        } else if (0x1F249 <= char_code && char_code <= 0x1F24F) {
            str_width += 1;
        } else if (0x1F250 <= char_code && char_code <= 0x1F251) {
            str_width += 2;
        } else if (0x1F252 <= char_code && char_code <= 0x1FFFF) {
            str_width += 1;
        } else if (0x20000 <= char_code && char_code <= 0x2FFFD) {
            str_width += 2;
        } else if (0x2FFFE <= char_code && char_code <= 0x2FFFF) {
            str_width += 1;
        } else if (0x30000 <= char_code && char_code <= 0x3FFFD) {
            str_width += 2;
        } else if (0x3FFFE <= char_code && char_code <= 0x10FFFF) {
            str_width += 1;
        }
    }

    return str_width;
}

mb_strimwidth

mb_strimwidth
/**
 * 指定した幅で文字列を丸める。
 *
 * この関数はPHPのmb_strimwidth互換です。
 *
 * @param   string  str         丸めたい文字列
 * @param   int     start       開始位置のオフセット。文字列の始めからの文字数 (最初の文字は 0) です
 * @param   int     width       丸める幅
 * @param   string  trimmarker  丸めた後にその文字列の最後に追加される文字列
 * @return  string  丸められた文字列
 */
function mb_strimwidth (str, start, width, trimmarker) {
    //============================================
    // 初期化
    //============================================
    //トリムマーカーが未指定の場合は空文字として初期化
    if (trimmarker == null) {
        trimmarker = '';
    }

    var trimmarker_width = mb_strwidth(trimmarker);

    var stacker             = []
    var total_width         = 0;

    var current_char        = '';
    var current_char_code   = 0;
    var current_width       = 0;

    var next_char           = '';
    var next_char_code      = 0;
    var next_width          = 0;

    //============================================
    // mb_strimwidth互換 例外対応
    //============================================
    // 次の条件にマッチする場合、mb_strimwidthは常に空文字を返す。
    // ・対象文字列の1文字目のコードポイントが0x20以下または0x7E以上
    // ・トリムマーカーが空文字
    // ・対象文字列に特定のコードポイント(illegal_range_listにあるもの)内の文字が含まれる
    //
    // このブロックではそれの自動検知用の状態、値を定義する
    //============================================

    //コードポイントとトリムマーカーから例外対応有無を判定
    var irregular_caution       = (str.charCodeAt(0) < 0x21 || 0x7E < str.charCodeAt(0)) && trimmarker == '' && width == 1;

    //例外対応対象となる文字列のコードポイント定義
    //2次元目の配列の第一要素がコードポイント始点、第二要素がコードポイント終点
    //例)[0x1100, 0x115F]の場合
    // 0x1100 <= code_point && code_point <= 0x115F が例外対応対象のコードポイント範囲となる
    var illegal_range_list  = [
        [0x1100, 0x115F],
        [0x11A3, 0x11A7],
        [0x11FA, 0x11FF],
        [0x2329, 0x2329],
        [0x232A, 0x232A],
        [0x2E80, 0x2E99],
        [0x2E9B, 0x2EF3],
        [0x2F00, 0x2FD5],
        [0x2FF0, 0x2FFB],
        [0x3000, 0x303E],
        [0x3041, 0x3096],
        [0x3099, 0x30FF],
        [0x3105, 0x312D],
        [0x3131, 0x318E],
        [0x3190, 0x31BA],
        [0x31C0, 0x31E3],
        [0x31F0, 0x321E],
        [0x3220, 0x3247],
        [0x3250, 0x32FE],
        [0x3300, 0x4DBF],
        [0x4E00, 0xA48C],
        [0xA490, 0xA4C6],
        [0xA960, 0xA97C],
        [0xAC00, 0xD7A3],
        [0xD7B0, 0xD7C6],
        [0xD7CB, 0xD7FB],
        //サロゲートペア範囲も対象となるが、単体で渡す事自体がおかしいので仕様としては取り込まない
        [0xF900, 0xFAFF],
        [0xFE10, 0xFE19],
        [0xFE30, 0xFE52],
        [0xFE54, 0xFE66],
        [0xFE68, 0xFE6B],
        [0xFF01, 0xFF60],
        [0xFFE0, 0xFFE6],
        [0x1B000, 0x1B001],
        [0x1F200, 0x1F202],
        [0x1F210, 0x1F23A],
        [0x1F240, 0x1F248],
        [0x1F250, 0x1F251],
        [0x20000, 0x2FFFD],
        [0x30000, 0x3FFFD]
    ];
    var illegal_range_length = illegal_range_list.length;

    /**
     * 対象のコードポイントがmb_strimwidth互換 例外対応対象文字かどうか検証します。
     *
     * @param   int     char_code   コードポイント
     * @return  bool    コードポイントがmb_strimwidth互換 例外対応対象文字の場合はtrue、そうでない場合はfalse
     */
    var irregular_pattern           = function (char_code) {
        for (var i = 0; i < illegal_range_length;i++) {
            if (illegal_range_list[i][0] <= char_code && char_code <= illegal_range_list[i][1]) {
                return true;
            }
        }
        return false;
    };

    //============================================
    // 実処理
    //============================================
    for (var i = start, length = start + width;i < length;i++) {
        current_char_code   = str.charCodeAt(i);
        next_char_code      = str.charCodeAt(1 + i);

        //サロゲートペア対応
        if (0xD800 <= current_char_code && current_char_code <= 0xDBFF) {
            current_char_code = ((current_char_code - 0xD800) * 0x400) + (next_char_code - 0xDC00) + 0x10000;
            i++;
            next_char_code = str.charCodeAt(i + 1);
        }

        //次の文字のサロゲートペア対応
        if (0xD800 <= next_char_code && next_char_code <= 0xDBFF) {
            i++;
            next_char_code = ((next_char_code - 0xD800) * 0x400) + (str.charCodeAt(i + 1) - 0xDC00) + 0x10000;
        }

        //mb_strimwidth互換 例外対応
        if (irregular_caution) {
            if (irregular_pattern(current_char_code)) {
                return '';
            }
            if (irregular_pattern(next_char_code)) {
                return '';
            }
        }

        if (Number.isNaN(current_char_code)) {
            break;
        }

        current_char = fromChatCodeExt(current_char_code);
        next_char = Number.isNaN(next_char_code) ? '' : fromChatCodeExt(next_char_code);

        current_width       = mb_strwidth(current_char);
        next_width          = mb_strwidth(next_char);

        if (width < total_width + next_width + trimmarker_width) {
            stacker.push(trimmarker);
            break;
        }

        total_width         += current_width;
        stacker.push(current_char);

        if (width <= total_width) {
            break;
        }
    }

    return stacker.join('');
}

結論

mb_strimwidthではサロゲートペアを除き、40箇所のコードポイント範囲で空文字が返る実装となっている。

条件

次の条件にマッチする場合、mb_strimwidthは常に空文字を返す。
- 対象文字列の1文字目のコードポイントが0x20以下または0x7E以上
- トリムマーカーが空文字
- 対象文字列に特定のコードポイント([対象範囲]にあるもの)内の文字が含まれる

対象範囲

code point
U+1100 - U+115F
U+11A3 - U+11A7
U+11FA - U+11FF
U+2329 - U+2329
U+232A - U+232A
U+2E80 - U+2E99
U+2E9B - U+2EF3
U+2F00 - U+2FD5
U+2FF0 - U+2FFB
U+3000 - U+303E
U+3041 - U+3096
U+3099 - U+30FF
U+3105 - U+312D
U+3131 - U+318E
U+3190 - U+31BA
U+31C0 - U+31E3
U+31F0 - U+321E
U+3220 - U+3247
U+3250 - U+32FE
U+3300 - U+4DBF
U+4E00 - U+A48C
U+A490 - U+A4C6
U+A960 - U+A97C
U+AC00 - U+D7A3
U+D7B0 - U+D7C6
U+D7CB - U+D7FB
U+F900 - U+FAFF
U+FE10 - U+FE19
U+FE30 - U+FE52
U+FE54 - U+FE66
U+FE68 - U+FE6B
U+FF01 - U+FF60
U+FFE0 - U+FFE6
U+1B000 - U+1B001
U+1F200 - U+1F202
U+1F210 - U+1F23A
U+1F240 - U+1F248
U+1F250 - U+1F251
U+20000 - U+2FFFD
U+30000 - U+3FFFD

検証

次のコードを実行し、空テーブルが表示されればOK。
!!注意!!
- ブラウザで実行する必要があります
- 検証対象文字数が1,114,111文字あるため実行に時間がかかります
- 検証処理はJavaScriptで行っているため、状況によりブラウザがクラッシュする可能性があります
- 543回ページの自動遷移が行われます

<?php
//============================================
// 初期化
//============================================
//検証範囲最大コードポイント
$max = hexdec('0x10FFFF');

//1描画当たりの検証件数
$range = 2048;

//現在のページの算出
$page = isset($_GET['page']) ? $_GET['page'] - 1 : 0;

//今回描画範囲の確定
$start = $range * $page;
$end = $range + $start;

//スタッカの初期化
$stacker = [];

/**
 * 整数値で表現されたコードポイントをUTF-8文字に変換する。
 *
 * @param   int     $code_point UTF-8文字に変換したいコードポイント
 * @return  string  コードポイントから作成したUTF-8文字
 */
function int2utf8($code_point) {
    //UTF-16コードポイント内判定
    if ($code_point < 0) {
        throw new \Exception(sprintf('%1$s is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
    }
    if (0x10FFFF < $code_point) {
        throw new \Exception(sprintf('0x%1$X is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
    }

    //サロゲートペア判定
    if (0xD800 <= $code_point && $code_point <= 0xDFFF) {
        throw new \Exception(sprintf('0x%X is in of range surrogate pair code point (0xD800 - 0xDFFF)', $code_point));
    }

    //1番目のバイトのみでchr関数が使えるケース
    if ($code_point < 0x80) {
        return chr($code_point);
    }

    //2番目のバイトを考慮する必要があるケース
    if ($code_point < 0xA0) {
        return chr(0xC0 | $code_point >> 6) . chr(0x80 | $code_point & 0x3F);
    }

    //数値実体参照表記からの変換
    return html_entity_decode('&#'. $code_point .';');
}
?><html>
<head>
<script type="text/javascript">
/**
 * 整数値で表現されたコードポイントをUTF-8文字に変換する。
 *
 * @param   int     code_point  UTF-8文字に変換したいコードポイント
 * @return  string  コードポイントから作成したUTF-8文字
 */
function fromChatCodeExt (code_point) {
    //サロゲートペア無しの文字の場合
    if (code_point < 0x10000) {
        //素直に処理して返す
        return String.fromCharCode(code_point);
    }

    //サロゲートペア付の文字の場合
    code_point -= 0x10000;
    return String.fromCharCode(0xD800 + (code_point >> 10), 0xDC00 + (code_point & 0x3FF));
}

/**
 * 文字列の幅を返す。
 *
 * この関数はPHPのmb_strwidth互換です。
 *
 * @param   string  str 幅を測りたい文字列
 * @return  int     文字列幅
 */
function mb_strwidth (str) {
    var i = 0;
    var str_width = 0;
    var next_char_code = 0;
    var char_code = 0;

    //一文字ずつ処理
    while (!Number.isNaN((char_code = str.charCodeAt(i++)))) {
        //サロゲートペア対応
        if (0xD800 <= char_code && char_code <= 0xDBFF) {
            next_char_code = str.charCodeAt(i);
            char_code = ((char_code - 0xD800) * 0x400) + (next_char_code - 0xDC00) + 0x10000;
            i++;
        }

        if (0x0 <= char_code && char_code <= 0x10FF) {
            str_width += 1;
        } else if (0x1100 <= char_code && char_code <= 0x115F) {
            str_width += 2;
        } else if (0x1160 <= char_code && char_code <= 0x11A2) {
            str_width += 1;
        } else if (0x11A3 <= char_code && char_code <= 0x11A7) {
            str_width += 2;
        } else if (0x11A8 <= char_code && char_code <= 0x11F9) {
            str_width += 1;
        } else if (0x11FA <= char_code && char_code <= 0x11FF) {
            str_width += 2;
        } else if (0x1200 <= char_code && char_code <= 0x2328) {
            str_width += 1;
        } else if (0x2329 <= char_code && char_code <= 0x232A) {
            str_width += 2;
        } else if (0x232B <= char_code && char_code <= 0x2E7F) {
            str_width += 1;
        } else if (0x2E80 <= char_code && char_code <= 0x2E99) {
            str_width += 2;
        } else if (0x2E9A <= char_code && char_code <= 0x2E9A) {
            str_width += 1;
        } else if (0x2E9B <= char_code && char_code <= 0x2EF3) {
            str_width += 2;
        } else if (0x2EF4 <= char_code && char_code <= 0x2EFF) {
            str_width += 1;
        } else if (0x2F00 <= char_code && char_code <= 0x2FD5) {
            str_width += 2;
        } else if (0x2FD6 <= char_code && char_code <= 0x2FEF) {
            str_width += 1;
        } else if (0x2FF0 <= char_code && char_code <= 0x2FFB) {
            str_width += 2;
        } else if (0x2FFC <= char_code && char_code <= 0x2FFF) {
            str_width += 1;
        } else if (0x3000 <= char_code && char_code <= 0x303E) {
            str_width += 2;
        } else if (0x303F <= char_code && char_code <= 0x3040) {
            str_width += 1;
        } else if (0x3041 <= char_code && char_code <= 0x3096) {
            str_width += 2;
        } else if (0x3097 <= char_code && char_code <= 0x3098) {
            str_width += 1;
        } else if (0x3099 <= char_code && char_code <= 0x30FF) {
            str_width += 2;
        } else if (0x3100 <= char_code && char_code <= 0x3104) {
            str_width += 1;
        } else if (0x3105 <= char_code && char_code <= 0x312D) {
            str_width += 2;
        } else if (0x312E <= char_code && char_code <= 0x3130) {
            str_width += 1;
        } else if (0x3131 <= char_code && char_code <= 0x318E) {
            str_width += 2;
        } else if (0x318F <= char_code && char_code <= 0x318F) {
            str_width += 1;
        } else if (0x3190 <= char_code && char_code <= 0x31BA) {
            str_width += 2;
        } else if (0x31BB <= char_code && char_code <= 0x31BF) {
            str_width += 1;
        } else if (0x31C0 <= char_code && char_code <= 0x31E3) {
            str_width += 2;
        } else if (0x31E4 <= char_code && char_code <= 0x31EF) {
            str_width += 1;
        } else if (0x31F0 <= char_code && char_code <= 0x321E) {
            str_width += 2;
        } else if (0x321F <= char_code && char_code <= 0x321F) {
            str_width += 1;
        } else if (0x3220 <= char_code && char_code <= 0x3247) {
            str_width += 2;
        } else if (0x3248 <= char_code && char_code <= 0x324F) {
            str_width += 1;
        } else if (0x3250 <= char_code && char_code <= 0x32FE) {
            str_width += 2;
        } else if (0x32FF <= char_code && char_code <= 0x32FF) {
            str_width += 1;
        } else if (0x3300 <= char_code && char_code <= 0x4DBF) {
            str_width += 2;
        } else if (0x4DC0 <= char_code && char_code <= 0x4DFF) {
            str_width += 1;
        } else if (0x4E00 <= char_code && char_code <= 0xA48C) {
            str_width += 2;
        } else if (0xA48D <= char_code && char_code <= 0xA48F) {
            str_width += 1;
        } else if (0xA490 <= char_code && char_code <= 0xA4C6) {
            str_width += 2;
        } else if (0xA4C7 <= char_code && char_code <= 0xA95F) {
            str_width += 1;
        } else if (0xA960 <= char_code && char_code <= 0xA97C) {
            str_width += 2;
        } else if (0xA97D <= char_code && char_code <= 0xABFF) {
            str_width += 1;
        } else if (0xAC00 <= char_code && char_code <= 0xD7A3) {
            str_width += 2;
        } else if (0xD7A4 <= char_code && char_code <= 0xD7AF) {
            str_width += 1;
        } else if (0xD7B0 <= char_code && char_code <= 0xD7C6) {
            str_width += 2;
        } else if (0xD7C7 <= char_code && char_code <= 0xD7CA) {
            str_width += 1;
        } else if (0xD7CB <= char_code && char_code <= 0xD7FB) {
            str_width += 2;
        } else if (0xD7FC <= char_code && char_code <= 0xF8FF) {
            str_width += 1;
        } else if (0xF900 <= char_code && char_code <= 0xFAFF) {
            str_width += 2;
        } else if (0xFB00 <= char_code && char_code <= 0xFE0F) {
            str_width += 1;
        } else if (0xFE10 <= char_code && char_code <= 0xFE19) {
            str_width += 2;
        } else if (0xFE1A <= char_code && char_code <= 0xFE2F) {
            str_width += 1;
        } else if (0xFE30 <= char_code && char_code <= 0xFE52) {
            str_width += 2;
        } else if (0xFE53 <= char_code && char_code <= 0xFE53) {
            str_width += 1;
        } else if (0xFE54 <= char_code && char_code <= 0xFE66) {
            str_width += 2;
        } else if (0xFE67 <= char_code && char_code <= 0xFE67) {
            str_width += 1;
        } else if (0xFE68 <= char_code && char_code <= 0xFE6B) {
            str_width += 2;
        } else if (0xFE6C <= char_code && char_code <= 0xFF00) {
            str_width += 1;
        } else if (0xFF01 <= char_code && char_code <= 0xFF60) {
            str_width += 2;
        } else if (0xFF61 <= char_code && char_code <= 0xFFDF) {
            str_width += 1;
        } else if (0xFFE0 <= char_code && char_code <= 0xFFE6) {
            str_width += 2;
        } else if (0xFFE7 <= char_code && char_code <= 0x1AFFF) {
            str_width += 1;
        } else if (0x1B000 <= char_code && char_code <= 0x1B001) {
            str_width += 2;
        } else if (0x1B002 <= char_code && char_code <= 0x1F1FF) {
            str_width += 1;
        } else if (0x1F200 <= char_code && char_code <= 0x1F202) {
            str_width += 2;
        } else if (0x1F203 <= char_code && char_code <= 0x1F20F) {
            str_width += 1;
        } else if (0x1F210 <= char_code && char_code <= 0x1F23A) {
            str_width += 2;
        } else if (0x1F23B <= char_code && char_code <= 0x1F23F) {
            str_width += 1;
        } else if (0x1F240 <= char_code && char_code <= 0x1F248) {
            str_width += 2;
        } else if (0x1F249 <= char_code && char_code <= 0x1F24F) {
            str_width += 1;
        } else if (0x1F250 <= char_code && char_code <= 0x1F251) {
            str_width += 2;
        } else if (0x1F252 <= char_code && char_code <= 0x1FFFF) {
            str_width += 1;
        } else if (0x20000 <= char_code && char_code <= 0x2FFFD) {
            str_width += 2;
        } else if (0x2FFFE <= char_code && char_code <= 0x2FFFF) {
            str_width += 1;
        } else if (0x30000 <= char_code && char_code <= 0x3FFFD) {
            str_width += 2;
        } else if (0x3FFFE <= char_code && char_code <= 0x10FFFF) {
            str_width += 1;
        }
    }

    return str_width;
}

/**
 * 指定した幅で文字列を丸める。
 *
 * この関数はPHPのmb_strimwidth互換です。
 *
 * @param   string  str         丸めたい文字列
 * @param   int     start       開始位置のオフセット。文字列の始めからの文字数 (最初の文字は 0) です
 * @param   int     width       丸める幅
 * @param   string  trimmarker  丸めた後にその文字列の最後に追加される文字列
 * @return  string  丸められた文字列
 */
function mb_strimwidth (str, start, width, trimmarker) {
    //============================================
    // 初期化
    //============================================
    //トリムマーカーが未指定の場合は空文字として初期化
    if (trimmarker == null) {
        trimmarker = '';
    }

    var trimmarker_width = mb_strwidth(trimmarker);

    var stacker             = []
    var total_width         = 0;

    var current_char        = '';
    var current_char_code   = 0;
    var current_width       = 0;

    var next_char           = '';
    var next_char_code      = 0;
    var next_width          = 0;

    //============================================
    // mb_strimwidth互換 例外対応
    //============================================
    // 次の条件にマッチする場合、mb_strimwidthは常に空文字を返す。
    // ・対象文字列の1文字目のコードポイントが0x20以下または0x7E以上
    // ・トリムマーカーが空文字
    // ・対象文字列に特定のコードポイント(illegal_range_listにあるもの)内の文字が含まれる
    //
    // このブロックではそれの自動検知用の状態、値を定義する
    //============================================

    //コードポイントとトリムマーカーから例外対応有無を判定
    var irregular_caution       = (str.charCodeAt(0) < 0x21 || 0x7E < str.charCodeAt(0)) && trimmarker == '' && width == 1;

    //例外対応対象となる文字列のコードポイント定義
    //2次元目の配列の第一要素がコードポイント始点、第二要素がコードポイント終点
    //例)[0x1100, 0x115F]の場合
    // 0x1100 <= code_point && code_point <= 0x115F が例外対応対象のコードポイント範囲となる
    var illegal_range_list  = [
        [0x1100, 0x115F],
        [0x11A3, 0x11A7],
        [0x11FA, 0x11FF],
        [0x2329, 0x2329],
        [0x232A, 0x232A],
        [0x2E80, 0x2E99],
        [0x2E9B, 0x2EF3],
        [0x2F00, 0x2FD5],
        [0x2FF0, 0x2FFB],
        [0x3000, 0x303E],
        [0x3041, 0x3096],
        [0x3099, 0x30FF],
        [0x3105, 0x312D],
        [0x3131, 0x318E],
        [0x3190, 0x31BA],
        [0x31C0, 0x31E3],
        [0x31F0, 0x321E],
        [0x3220, 0x3247],
        [0x3250, 0x32FE],
        [0x3300, 0x4DBF],
        [0x4E00, 0xA48C],
        [0xA490, 0xA4C6],
        [0xA960, 0xA97C],
        [0xAC00, 0xD7A3],
        [0xD7B0, 0xD7C6],
        [0xD7CB, 0xD7FB],
        //サロゲートペア範囲も対象となるが、単体で渡す事自体がおかしいので仕様としては取り込まない
        [0xF900, 0xFAFF],
        [0xFE10, 0xFE19],
        [0xFE30, 0xFE52],
        [0xFE54, 0xFE66],
        [0xFE68, 0xFE6B],
        [0xFF01, 0xFF60],
        [0xFFE0, 0xFFE6],
        [0x1B000, 0x1B001],
        [0x1F200, 0x1F202],
        [0x1F210, 0x1F23A],
        [0x1F240, 0x1F248],
        [0x1F250, 0x1F251],
        [0x20000, 0x2FFFD],
        [0x30000, 0x3FFFD]
    ];
    var illegal_range_length = illegal_range_list.length;

    /**
     * 対象のコードポイントがmb_strimwidth互換 例外対応対象文字かどうか検証します。
     *
     * @param   int     char_code   コードポイント
     * @return  bool    コードポイントがmb_strimwidth互換 例外対応対象文字の場合はtrue、そうでない場合はfalse
     */
    var irregular_pattern           = function (char_code) {
        for (var i = 0; i < illegal_range_length;i++) {
            if (illegal_range_list[i][0] <= char_code && char_code <= illegal_range_list[i][1]) {
                return true;
            }
        }
        return false;
    };

    //============================================
    // 実処理
    //============================================
    for (var i = start, length = start + width;i < length;i++) {
        current_char_code   = str.charCodeAt(i);
        next_char_code      = str.charCodeAt(1 + i);

        //サロゲートペア対応
        if (0xD800 <= current_char_code && current_char_code <= 0xDBFF) {
            current_char_code = ((current_char_code - 0xD800) * 0x400) + (next_char_code - 0xDC00) + 0x10000;
            i++;
            next_char_code = str.charCodeAt(i + 1);
        }

        //次の文字のサロゲートペア対応
        if (0xD800 <= next_char_code && next_char_code <= 0xDBFF) {
            i++;
            next_char_code = ((next_char_code - 0xD800) * 0x400) + (str.charCodeAt(i + 1) - 0xDC00) + 0x10000;
        }

        //mb_strimwidth互換 例外対応
        if (irregular_caution) {
            if (irregular_pattern(current_char_code)) {
                return '';
            }
            if (irregular_pattern(next_char_code)) {
                return '';
            }
        }

        if (Number.isNaN(current_char_code)) {
            break;
        }

        current_char = fromChatCodeExt(current_char_code);
        next_char = Number.isNaN(next_char_code) ? '' : fromChatCodeExt(next_char_code);

        current_width       = mb_strwidth(current_char);
        next_width          = mb_strwidth(next_char);

        if (width < total_width + next_width + trimmarker_width) {
            stacker.push(trimmarker);
            break;
        }

        total_width         += current_width;
        stacker.push(current_char);

        if (width <= total_width) {
            break;
        }
    }

    return stacker.join('');
}

//============================================
// 検証コード自動構築
//============================================
var list = [];
<?php

for ($i = $start;$i < $end;$i++) {
    if (0xD800 <= $i && $i <= 0xDFFF) {
        continue;
    }

    $char = int2utf8($i);
?>
list.push({char : fromChatCodeExt(<?= $i ?>), code : <?= $i ?>, hex_code : '<?= sprintf('0x%X', $i) ?>', width : <?= mb_strwidth($char) ?>, imwidth : [<?php for ($n = 1;$n < 6;$n++) { ?><?php if ($n > 1) {echo ', ';} ?>[<?= mb_strwidth(mb_strimwidth($char, 0, $n, "")) ?>, <?= mb_strwidth(mb_strimwidth($char, 0, $n, ".")) ?>, <?= mb_strwidth(mb_strimwidth($char, 0, $n, "…")) ?>]<?php } ?>]});
<?php
}
?>

var error_list = [];
var element = null;
var n = 0;
var m = 0;

for (var i = 0, length = list.length;i < length;i++) {
    element = list[i];

    if (element.width != mb_strwidth(element.char)) {
        error_list.push({char : element.char, code : element.hex_code, message : 'width un match. php:'+ element.width +' js:'+ mb_strwidth(element.char)});
    }

    for (n = 0;n < 5;n++) {
        m = 1 + n;
        if (element.imwidth[n][0] != mb_strwidth(mb_strimwidth(element.char, 0, m, ''))) {
            error_list.push({char : element.char, code : element.hex_code, message : 'imwidth un match. trimmarker:"". width param:'+ m +' php:'+ element.imwidth[n][0] +' js:'+ mb_strwidth(mb_strimwidth(element.char, 0, m, '')) +' char:'+ mb_strimwidth(element.char, 0, m, '')});
        }
        if (element.imwidth[n][1] != mb_strwidth(mb_strimwidth(element.char, 0, m, '.'))) {
            error_list.push({char : element.char, code : element.hex_code, message : 'imwidth un match. trimmarker:".". width param:'+ m +' php:'+ element.imwidth[n][1] +' js:'+ mb_strwidth(mb_strimwidth(element.char, 0, m, '.')) +' char:'+ mb_strimwidth(element.char, 0, m, '.')});
        }
        if (element.imwidth[n][2] != mb_strwidth(mb_strimwidth(element.char, 0, m, ''))) {
            error_list.push({char : element.char, code : element.hex_code, message : 'imwidth un match. trimmarker:"…". width param:'+ m +' php:'+ element.imwidth[n][2] +' js:'+ mb_strwidth(mb_strimwidth(element.char, 0, m, '')) +' char:'+ mb_strimwidth(element.char, 0, m, '')});
        }
    }
}

//コードポイントが最大になるまで、リダイレクトで検証を続けていく
if (error_list.length == 0 && <?= $end + $range <= $max ? 'true' : 'false' ?>) {
    document.location = './?page='+ <?= (isset($_GET['page']) ? $_GET['page'] : 0) + 1 ?>;
}

</script>
</head>
<body>

<a href="./">restart</a>
<hr />
<table border="1">
    <tbody>
        <tr>
            <td>char</td>
            <td>char code</td>
            <td>message</td>
        </tr>
<script type="text/javascript">
for (var i = 0, length = error_list.length;i < length;i++) {
    element = error_list[i];
    document.write('<tr>');
    document.write('<td>'+ element.char +'</td>');
    document.write('<td>'+ element.code +'</td>');
    document.write('<td>'+ element.message +'</td>');
    document.write('</tr>');
}
</script>
    </tbody>
</table>

</body>
</html>
7
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6