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


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

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

## 検証バージョン
 PHP 5.6.15
 
## 移植したコード

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

```javascript: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

```javascript: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

```javascript: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
<?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>
```