最初に結論
- Mac OS Xではシェルによって、入力文字列にUnicodeの結合文字列が混ざることがある
- JavaScriptでは、String.prototype.normalize()を使って
NFC
に変換すると良い
事件
事象
デ
を含む文字列(デイリー)をrepalaceできない現象が起きました。
再現コード
const {
Transform
} = require('stream')
process.stdin
.pipe(new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString()
.replace('デイリー', ''))
}
}))
.pipe(process.stdout)
を
echo 'デイリー' | node .
な風に実行します。
デイリー
が空文字に置換されます。普通は何も表示されません。
ところが、Mac OS上のfish shellで実行するとデイリー
がそのまま表示されます。
不可解な事件です。
発生条件
置換条件のデイリー
を少しずつ変えて、発生条件を絞り込みます。
デ
が入ると、置換できません。
置換される
- イリー
- リー
- ー
置換されない
- デイリー
- デイ
- デ
調査内容
実際に受け取っているバイト文字列
次のプログラムを使ってバイト文字列の差を確認してみます。
process.stdin.on('readable', () => {
var chunk = process.stdin.read();
if (chunk !== null) {
console.log(chunk)
}
})
結果
実際、違うのです。
Bash
<Buffer e3 83 87 e3 82 a4 e3 83 aa e3 83 bc 0a>
fish shell
<Buffer e3 83 86 e3 82 99 e3 82 a4 e3 83 aa e3 83 bc 0a>
何者かが一文字増えています。
何がちがうのか?
複数の文字コード方式に一括変換 | エンコード / デコード ツールを使って何が違うか確認してみましょう。
Bash
<Buffer e3 83 87 e3 82 a4 e3 83 aa e3 83 bc 0a>
- デ
e3 83 87
- イ
e3 82 a4
- リ
e3 83 aa
- ー
e3 83 bc
fish shell
<Buffer e3 83 86 e3 82 99 e3 82 a4 e3 83 aa e3 83 bc 0a>
- テ
e3 83 86
- ?
e3 82 99
- イ
e3 82 a4
- リ
e3 83 aa
- ー
e3 83 bc
デ
がテ
に変わています。何者かが、一文字増えています。
何か?
u+e38299
でググるとUnicode Character 'COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK' (U+3099)がでてきます。
つまりデ
がテ
と濁点に分かれていました。
原因
結論を言うと、一見デ
見えているものは、テ
と゛
がUnicodeの結合文字列として、一つに見えているものです。
テ
と゛
は、バイト文字列としてはデ
ではありません。
デ
はマッチしません。
対策
String.prototype.normalize()を使うと、テ
と゛
をデ
に変換できます。デ
として、見た目通りに、扱えます。
謝辞
原因究明にあたり、 @mysticatea 氏と @matarillo 氏にとても助けられました。
- https://twitter.com/mysticatea/status/780696729817493504
- https://twitter.com/matarillo/status/780696380666814464
ありがとうございました。