筆者のサイトには、2010 年に書いた C のコードが置いてあります。その中に、改行しない printf がありました。
if(0 == *pcount % 10000000) printf("%lld¥n", *pcount);
\n と書いたつもりの箇所が、円記号になっています。同じ形の最小のコードを書いてみると、gcc 12.2.0 は -Wall -Wextra -pedantic を付けても何も言わず、実行して od -c に通すと、改行のあるべき場所に 302 245 n の 3 バイトが並びました。このファイルには、同じ化け方をした円記号が 6 個ありました。
円記号に見える文字は、Unicode に 3 つあります。 U+005C、U+00A5、U+FFE5 です。このうち円記号として使ってよいのは U+00A5 だけで、残りの 2 つはそもそも円記号ではありません。以下では、その 2 つが定義上どういう文字なのかを確かめ、次に「Shift_JIS には U+00A5 が入らない」という最も強い反論を検証し、最後に筆者のリポジトリにある円記号を数えます。
U+005C はバックスラッシュであって、円記号ではない
Unicode が配布している NamesList.txt で U+005C を引くと、こう書いてあります。1
005C REVERSE SOLIDUS
= backslash
正式名称が「逆向きの斜線」で、別名が「バックスラッシュ」です。円について書かれた行は 1 行もありません。 U+00A5 のほうには注釈が 4 行あり、そこには「日本と中国の公式な記号は横棒 2 本」とまで書いてあります。
では、なぜ日本語環境では U+005C が円記号に見えるのでしょうか。 ISO/IEC 646 は 0x5C を各国が自由に決めてよい位置とし、日本の JIS X 0201 はそこに円記号を置きました。アメリカの ASCII がバックスラッシュを置いた場所です。 MS ゴシックや MS 明朝は今もこの割り当てに従って U+005C を円記号の形で描くので、これらのフォントで表示されたテキストでは、両者を見分けられません。2
厄介なのは、この曖昧さが表示だけの問題で終わらないことです。 Unicode コンソーシアムが配布する SHIFTJIS.TXT は、0x5C を U+00A5 に変換します。3
0x5C 0x00A5 # YEN SIGN
一方、Microsoft の CP932 と WHATWG Encoding Standard は、同じ 0x5C を U+005C にします。同じ 1 バイトに、公式な答えが 2 つあります。どちらの変換表を通ったかで、バックスラッシュは円記号になったりならなかったりします。筆者の 2010 年のコードがいつどの変換で化けたのかは、ブログを何度か引っ越しているので特定できません。特定できないこと自体が、この文字の性質をよく表しています。
U+FFE5 の定義は「U+00A5 の全角形」の一行だけ
全角の円記号のほうは、もう少し話が短く済みます。 NamesList.txt で U+FFE5 を引くと、注釈は次の一行しかありません。
FFE5 FULLWIDTH YEN SIGN
# <wide> 00A5
<wide> 00A5 は互換分解です。 U+FFE5 は「U+00A5 を全角の幅で表示したもの」と定義されていて、それ以上の意味を持ちません。実際、全角と半角の差を吸収する正規化形式である NFKC にかけると、U+00A5 に潰れます。
>>> import unicodedata as ud
>>> ud.decomposition('¥')
'<wide> 00A5'
>>> ud.normalize('NFKC', '¥') == '¥'
True
Unicode 標準は、U+FFE5 が属する Halfwidth and Fullwidth Forms ブロックの目的をこう書いています。4
This block consists of the additional forms needed to support conversion for existing texts that employ both forms.
このブロックは、全角と半角の両方を持つ既存のテキストを変換するために追加された形の集まりだ、という説明です。ここから出てくる指針は一つで、古い符号化方式から変換してきたテキストを保持するのでなければ、このブロックの文字は書かないほうがよいということになります。この指針が効かなくなるのは、変換の往復そのものが要件になっている場合です。 Shift_JIS で受け取ったデータを Shift_JIS で返す必要があるなら、U+FFE5 のまま持ち回るしかありません。
全角の円記号は検索を外す
原則の話をする前に、筆者が実際にやらかしたほうを先に置きます。この記事を書くにあたって、リポジトリにある円記号を数えようとして、まず素直にこう打ちました。
$ grep -rnP '\x{00A5}' notes/
notes/install-arch-linux-on-kvi-70b.md:17:約 ¥10,000 の Windows10 タブレットです.
notes/joi-2009-qual-q5-failed.md:25:// printf("%d %d¥n", x, y);
(以下略)
この結果には、Arduino の記事にある ¥4,200 も、自作 PC の記事にある ¥16,080 も出てきません。どちらも U+FFE5 なので、U+00A5 を探す検索には引っかからないからです。価格を含む記事を 2 本、取りこぼしていました。
この取りこぼしは、書き手が意識して NFKC を挟むまで直りません。そして grep のような素朴な検索は、正規化を挟まずに素通しします。自分のリポジトリを数えるときは気付けますが、利用者が入力した金額を検索する側に回れば話が変わります。 ¥1000 と打った利用者は、¥1000 と書かれた商品を見つけられません。
もっとも、U+FFE5 が選ばれる理由がないわけではありません。日本語の本文の中では、半角の U+00A5 は細く小さく、金額の頭に付いていることが目に入りにくくなります。全角なら漢字と同じ幅で並ぶので、視覚的な収まりはよくなります。ただしそれは表示の問題なので、フォントと組版で解くほうが筋がよく、文字そのものを別のものに置き換えて解くのは割に合いません。
Shift_JIS には U+00A5 が入らない
ここまでの話には、主張を丸ごと崩しうる反論があります。 U+00A5 は、Shift_JIS に入らないのです。
>>> '¥'.encode('cp932')
UnicodeEncodeError: 'cp932' codec can't encode character '\xa5' in position 0: illegal multibyte sequence
>>> '¥'.encode('cp932')
b'\x81\x8f'
CP932 では、U+00A5 はエンコードできずに例外になります。 U+FFE5 のほうは 0x818F として問題なく通ります。
Web の世界でも事情は変わりません。 WHATWG Encoding Standard の Shift_JIS encoder には、次の一行があります。5
If codePoint is U+00A5 (¥), then return byte 0x5C.
そして同じ仕様の decoder は、0x5C を U+005C に戻します。つまり、Web 標準に従って U+00A5 を Shift_JIS に通して戻すと、バックスラッシュになります。
>>> '¥'.encode('shift_jis').decode('shift_jis')
'\\'
>>> '¥'.encode('euc_jp').decode('euc_jp')
'\\'
古い符号化方式へ出す可能性がある限り、U+FFE5 のほうが安全に見えます。実際、往復して壊れないのは U+FFE5 だけです。
変換の都合は出口で処理する
それでも正本を U+00A5 で書くのは、この反論が変換の話であって、テキストそのものの話ではないからです。
Shift_JIS へ出す必要があるなら、書き出す処理に変換表を置いて U+00A5 を 0x818F に対応づければ、U+FFE5 を書いたときと同じバイト列が出ます。手当てが要ることは事実で、CP932 の既定の挙動が例外を投げる以上、そこは書かないと動きません。逆向きは、これほど簡単には片付きません。正本を U+FFE5 で書くと、UTF-8 のまま扱うすべての箇所、つまり検索、比較、集計、表示のそれぞれで NFKC を挟むことになります。書き出す処理は数えられるほどしかなく、テキストを読む箇所は数え切れないので、手当てを書き出す側に寄せたほうが少なくて済みます。
この判断が効かなくなる条件も、はっきりしています。変換層を持てず、Shift_JIS 固定の帳票やデータを直接書いているなら、U+FFE5 を書くしかありません。その場合に書いているのは Unicode のテキストではなく Shift_JIS のバイト列なので、この記事の主張の対象外です。
現実の側も、この方向で動いています。 2026 年 8 月 19 日にメルカリのトップページの HTML を取得して数えたところ、U+00A5 が 49 個あり、U+FFE5 は 0 個でした。少なくともこのページは、半角の円記号だけで成立しています。
筆者のリポジトリの円記号 9 個のうち、正しかったのは 1 個
円記号の書き方を人に説くつもりで自分のリポジトリを数えたところ、次のようになりました。
| ファイル | 文字 | 個数 |
|---|---|---|
joi-2009-qual-q5-failed.md | U+00A5 | 6 |
install-arch-linux-on-kvi-70b.md | U+00A5 | 1 |
arduino-one-minute-timer.md | U+FFE5 | 1 |
my-first-pc-build-parts.md | U+FFE5 | 1 |
全部で 9 個あり、正しかったのは ¥10,000 の 1 個だけでした。 JOI の記事の 6 個はバックスラッシュに戻し、価格の 2 個は U+00A5 にしました。これで 2010 年の printf は改行します。
同じ取りこぼしを繰り返さないために、以後は次の 2 つを見ています。
grep -rnP '\x{FFE5}' notes/: 本文に残った全角の円記号を洗うgrep -rnP '\x{00A5}' notes/: 出てきた箇所がコードブロックの中なら、それはバックスラッシュのはず
2 つ目のほうが大事です。円記号を円記号として書く話より、バックスラッシュが円記号に化ける話のほうが、壊れ方が静かだからです。金額の ¥ は見れば分かりますが、¥n のほうは今日まで見逃していました。
Footnotes
-
https://www.unicode.org/Public/UCD/latest/ucd/NamesList.txt ↩
-
「半角¥記号がバックスラッシュで表示される」 https://redmine.jp/faq/general/yen-sign-is-displayed-as-a-backslash/ ↩
-
https://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/SHIFTJIS.TXT ↩
-
The Unicode Standard 17.0.0, Chapter 18, 18.5.1 Halfwidth and Fullwidth Forms https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-18/ ↩
-
Encoding Standard, 12.3.2 Shift_JIS encoder https://encoding.spec.whatwg.org/#shift_jis-encoder ↩