先日のmb_strwidthの返す値が怪しかったので調べてみたで調べた情報を元にmb_strimwidthの移植を進めていたのだけど、想定しがたい箇所で結果の不一致が発生した。
JSとPHPで同じコードポイントを使った場合に同じ結果にするための方法について記載。
参考文献
PHP - コードポイントから UTF-8 の文字を生成する - Qiita
結論
JSはString.fromCharCode()メソッドを利用し、PHPは次の関数を利用すれば、同じコードポイントから同じ文字を得られる。
/**
* 整数値で表現されたコードポイントを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 .';');
}
発端
当初は次の実装で比較を行っていたが、0x200あたりからズレが発生するようになった。
<?php
$code_point = dechex(512); //0x200
$char_code = hexdec($code_point);
?>
実行結果:<?= mb_convert_encoding(pack('H*', $char_code), 'UTF-8', 'UCS-2'); ?>:<script type="text/javascript">document.write(String.fromCharCode(<?= $char_code ?>));</script>
実行結果:儠:Ȁ
全然違う文字やんけ!
UTF-8においてはASCIIの範囲内はコードポイントをそのまま使えるのですが、その先にあるLatin-1の範囲の処理に考慮が必要だったというオチ。
検証
簡易的に次の処理で目視で検証
:の前後の文字が同じならばOK
<html>
<head>
</head>
<body>
<?php
/**
* 整数値で表現されたコードポイントを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 .';');
}
?>
<?php foreach (range(1, 512) as $char_code) { ?>
<?= int2utf8($char_code) ?>:<script type="text/javascript">document.write(String.fromCharCode(<?= $char_code ?>));</script>
<br />
<?= $char_code ?>:document.write(String.fromCharCode(<?= $char_code ?>));
<hr />
<?php } ?>
</body>
</html>