0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

不可視文字の位置を出すとき、桁はコードポイントで数える

0
Posted at

背景

私は文字列の比較でよくハマります。
目で見て同じなのに === が false になるやつ…。
だいたい犯人はゼロ幅スペースで、毎回「またか」と思いながら手で探していました。

いいかげん道具にしようと思って、不可視文字チェッカーというツールを作りました。
https://hashitosystem.com/tools/invisible/

貼り付けた文字列から、目に見えない文字を見つけて位置を出すだけのものです。
作ってみたら、いちばん考えることになったのが「位置をどう数えるか」でした。

用語

不可視文字、と雑に呼んでいますが、実際には性質の違うものが混ざっています。

  • ゼロ幅スペース(U+200B)… 幅ゼロ。Webページからのコピーや翻訳サービスの出力で混ざる
  • ノーブレークスペース(U+00A0)… 空白に見えるが別の文字。HTMLの   がこれ
  • BOM(U+FEFF)… ファイル先頭に付く印。Excelは喜ぶがJSONパーサは死ぬ
  • 異体字セレクタ(U+FE00〜、U+E0100〜)… 直前の漢字の字形を切り替える文字

コードポイントというのはUnicodeが文字に振った通し番号のことで、U+200B の 200B の部分です。
一方 JavaScript の文字列は UTF-16 という形式で持たれていて、コードポイントが U+FFFF を超える文字は2つの単位に分けて格納されます。
この2つ組をサロゲートペアと呼びます。

何が問題だったか

最初、素直にこう書いていました。

for (var i = 0; i < s.length; i++) {
  var cp = s.charCodeAt(i);
  // cp が対象なら found に push
  col += 1;
}

s は入力欄に貼られた文字列、col は表示用の桁番号です。
これで動くには動きます。
ただ、絵文字を含む文字列を入れると桁がズレます。

具体的には "😀" + ZWSP(絵文字のあとにゼロ幅スペース)を入れたとき、s.length が 3 なので、ゼロ幅スペースの桁が 3 と出ます。
でもエディタで見れば、それは2文字目です。

"😀" は s.length が 2 です。
charCodeAt(0) と charCodeAt(1) はサロゲートペアの片割れずつを返すので、そもそも文字として意味を持ちません。

直した形

コードポイント単位で回すようにしました。

for (var i = 0; i < s.length; ) {
  var cp = s.codePointAt(i);
  var w = cp > 0xFFFF ? 2 : 1;   // サロゲートペアなら2単位ぶん進める
  var t = targetOf(cp);          // 対象の不可視文字かを判定して種別を返す
  if (t) found.push({ cp: cp, kind: t.kind, line: line, col: col });
  if (cp === 0x0A) { line += 1; col = 1; } else { col += 1; }
  i += w;
}

codePointAt はサロゲートペアを繋いだ本来のコードポイントを返します。
i の進み幅を自分で決めているのは、対象文字を「そのままの並びで」残したい処理(掃除する側)と同じ走査を使い回すためです。
for...of でも文字単位で回れますが、置換する側で元の文字列を切り出す都合があって、添字を持つ形にしました。

結果はこうなります。

入力: "😀" + U+200B
before … col = 3(UTF-16のコード単位で数えている)
after  … col = 2(コードポイントで数えている)

このズレは、掃除する側でも効きます。
s[i] で1単位ずつ拾って組み立てると、サロゲートペアの片割れだけを取り出す瞬間ができて、絵文字が壊れます。
s.substr(i, w) で幅ぶん取るようにしてから壊れなくなりました。

なぜ桁にこだわったか

このツールの価値は「見つけた」ではなく「どこにあるか」だと思っています。
ゼロ幅スペースが3個ある、と言われても直せません。
2行目の12桁目、と言われれば直せます。

その桁がエディタの表示とズレていたら、道具として成立しない…。
逆に言うと、位置を人に見せないツールならここまで気にしなくてよかった気もします。

結論

文字列を1文字ずつ回す処理を書くときは、その「1文字」がUTF-16のコード単位なのかコードポイントなのかを最初に決めてください。
人に位置を見せる用途ならコードポイント、バイト数やストレージの話ならコード単位です。
length と charCodeAt で書き始めた時点で前者は選べなくなっているので、決めるのは書く前です。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?