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>