配列を trim するとき両端から空文字列を取り除いてスカッとしてもよろしいでしょうか
hast のエレメントノードを trim 風に処理するユーティリティーを作っていたとき、空のテキストノード({"type": “text”, "value": “”} )を取り除くか悩みました。これは、その過程でウダウダと考え、自分を納得させるために作ったメモです。よって「数学とか言語の仕様とかの点で考えたとき、正しいのか?」は考慮していない内容となります。
空文字列と trim の関係
そもそも trim とは?今回は TypeScript で記述しているときにモヤっとした話なので、MDN で String.prototype.trim() を調べてみると以下のようなことが書かれています。
trim()はString値のメソッドで、この文字列の両端からホワイトスペースを取り除き、元の文字列を変更せずに新しい文字列を返します。
ドキュメントを読んだ感じでは空文字列についての言及はないので、正規表現と文字列置き換えを使って確認してみます。
まず、 trim 的なホワイスペースの置き換えを簡易的に再現。
> const s = ' \\n\\t\\u3000あいうえお\\n\\n '
undefined
> s.replace(/^\\s+/u, '').replace(/\\s+$/u, '')
'あいうえお'
> s.replace(/^\\s+/u, '').replace(/\\s+$/u, '') === s.trim()
true
次に、置き換えに使った正規表現で test してみます。
> const s = ' \\n\\t\\u3000あいうえお\\n\\n '
undefined
> /^\\s+/u.test(s) || /\\s+$/u.test(s)
true
これを空文字列で試すと false となります。
> const e = ''
undefined
> /^\\s+/u.test(e) || /\\s+$/u.test(e)
false
よって、「空文字列には取り除かれるべき文字(ホワイスペース)は含まれていない」となります。
「取り除かれるべき文字が含まれない要素」を取り除く?
hast のエレメントノードを trim 風に処理するのは少し複雑なので、文字列が格納された配列(string[])で考えます。
// これを
[" ", " ", " abc", "123 ", " ", " "]
// こうしたい
["abc", "123"]
これは、配列の両端の要素(文字列)からホワイスペースを取り除き、要素が空文字列になったら要素自体を取り除くというような処理です。
では、配列に空文字列が含まれていたらどのように考えるべきか。
// これは
[" ", "", " ", " abc", "123 ", " ", " ", ""]
// 取り除かれるべき文字は含まれてないので止める
["", " ", " abc", "123 ", " ", " ", ""]
// 文字そのものが存在しないのだから無視する
["", "abc", "123", ""]
// すべて取り除く(スカっとしますわ)
["abc", "123"]
個人的には ["abc", "123"] となるのが自然に思えますが、「trim 風な処理のとき、ホワイトスペースが含まれない要素(空文字列)を取り除いてよいのか?」という点で少しモヤっとします。よって「取り除くのは不自然でない」という理由を考えてみます。
全称量化子と存在量化子
先程の処理では、ホワイスペースを取り除いた後、さらに「要素を取り除くか?」を判定する順番で考えていました。しかし、(後で trim することは考えず)「両端にある要素は取り除くべきか?」を基起点として考えると、「その要素内の全ての文字はホワイトスペースか?」という考えを採用できます。
これは文字列を配列として扱うと以下のようになります。
> Array.from(' \\n\\t\\u3000あいうえお\\n\\n ').every(c=>c.match(/^\\s$/u))
false
> Array.from(' \\n\\t\\u3000\\n\\n ').every(c=>c.match(/^\\s$/u))
true
ここで注目したいのは every() が全称量化子の考えに基づいたメソッドであるということです。この場合、空文字列を渡すと true が返ってきます。
> Array.from("").every(c=>c.match(/^\\s$/u))
true
よって、取り除く要素の判定を全称量化子の考えに基づいた場合、「空文字列の要素を取り除くのは不自然でない」と言えるかと思います。今回はこの考え方を採用することでスカッとすることにしました。 (実際の処理では毎回 every 用に配列を作るのは効率良くないので、長さ判定を併用していく感じになるかとは思います)
なお、全称量化子と存在量化子は以下の記事が参考になりました。
まぁ、実際は日和っちゃうんですけどね
冒頭で触れた hast の tree での trim 風処理ユーティリティーに話を戻します。
文字列のことだけなら単純に取り除いてしまっても良かったのですが、hast は「空の要素(エレメントノード)は削除してもよいのか?」という点も(ホワイトスペース関連とは別の)悩み所となります。
たとえば、以下のような span があった場合は「取り除くとセレクターとかアンカー的にダメかな?」と考えてしまいます。
<p><span id="foo"></span>あいうえお<p>
ということで、実際に作ったユーティリティーでは、「空に関連するノード」への挙動を変更するオプションを付けることにしました。
おわりに
trim 風処理において、配列から空文字列の要素を取り除くとき「空文字列はホワイトスペースを含まないけど取り除いてもよい」と自分を納得させる理由を考えてみました。
頭の体操としてこういことを考えるのもたまには良いのかなと思います。
Discussion