Regular Expression replacer (and its inverse) / 正規表現置換器 in Javascript

初期状態では置換しません。それぞれ「⬃ Code」ボタンを押して下さい。/ does not replace in the initial state. Please press each “⬃ Code” button.
隅の「正規表現チートシート」が全体的なヒント集になっています。/ The “Regular Expression Cheat Sheet” in the corner is the overall hint collection.

概要 / Abstract

このページは隅の「一覧」にある他のフィルタと異なり、Javascript の主に正規表現を使って「任意の変換」を行います。また、他のフィルタと同様に逆変換のための仕組みも備えておきました。

そして、このページが Javascript の正規表現の練習ツールとしても使えるように「何にもマッチしない正規表現/[^\s\S]/」で拙作のコード例を隠しておきますので適宜「⬃ Code」ボタンを押してコード例を「コード入力テキスト欄」へ代入してください。

左側の「コード入力テキスト欄」で左のテキストエリアから右のテキストエリアへの変換、及び、右側の「コード入力テキスト欄」で右のテキストエリアから左のテキストエリアへの変換が実行できます。実行は、左右どちらかのテキストエリアの編集または「コード入力テキスト欄」編集の確定 (⏎) です。テキストエリアのサンプルテキストは自由に編集できますが、初期状態に戻すには「Renew ⬀」ボタンを押して下さい。

実行に際して、ブラウザの開発ツールのコンソールを表示し、問題解決に役立てるとよいかもしれません。

入出力例と同様な結果になるように、まずは変換のためのコードを書いてみてはいかがでしょうか。そして、逆変換にも挑戦してみることもお勧めします。ものによっては可逆であることが非常に困難もしくは不可能であることが身に染みると思います。私のコード例も教育的観点からあまり複雑にならないように適度なところで工夫は止めてありますので、往々にして商業上の実用には向いていませんのでご了承ください。

正規表現による文字列処理 string.replace(/正規表現/, target) を主に扱います(よりよい方法があるなら、コード例ではそちらを採用しているかもしれません)。

また、すべての「コード入力テキスト欄」へ拙作のコード例をこのボタン で代入できます。 さらに、すべての「► 問題」をこのボタン で開閉できます。

ついでに、珍しいとは思いますが『「正規表現による置換」のフローチャート』を教育的配慮で添えました。すべてを で開閉できます。


㈱ ⟺ (株) 等の相互変換 / ㈱ ⟺ (株) etc. Mutual exchange

㈱ ㈲ ㈹ ㈲ ㈹ ㈱ ㈹ ㈱ ㈲
(株) (有) (代) (有) (代) (株) (代) (株) (有)
問題

Microsoft らによる CP932 由来の「㈱」等の囲み漢字と、それを分解した「(株)」を相互変換します。ここでは、後に規格化された文字集合 JIS X 0213 の括弧囲み漢字「㈱」「㈲」「㈹」のみを対象とします。

規格外の Shift_JIS (CP932) の「㈱」等は「機種依存文字」などと忌み嫌われていましたが、今や Unicode で規格化されており正式に使用することができます。とは言え、使用を避けるために変換したいことも多々あります。

ヒント

いろいろなやり方があり得ますが、コード例では単純に v.replace(/src/g, 'dst') としています。

それを必要な文字数分繰り返すには、次々と連結します。

v.replace(/src1/g, 'dst1').replace(/src2/g, 'dst2').replace(/src3/g, 'dst3')…

注意事項としては、右欄から左欄への逆変換において、括弧「()」は正規表現でのキャプチャグループを表すので、バックスラッシュ「\」でエスケープすることです。

解説と発展 〜 string.replace メソッドのさまざまな活用形態

コード例は実に単純な方法ですが、Unicode を扱う Javascript のような言語では、左欄に関して、このような変換のための仕組みは備わっています。以下のようにします。

v.normalize('NFKC')

これは Unicode 文字列の合字などを正規化する命令の一つです。対象となる合字などは数えきれないほどあり、意図しない変換が生じることが予想されます。よって、場面によっては以下の方が適切かもしれません。

v.replace(/㈱|㈲|㈹/g, v=>v.normalize('NFKC'))

しかし、この Unicode 正規化には当然のことながら逆変換は存在しません。それに、需要に応じた局所的な約束事を決めた上で「(株)」→「㈱」などの変換は行うことになるでしょう。


理解の一助として、既定のコード例のフローチャートを記しておきます。

本節の正規表現による置換のフローチャート
v.replace(/㈱/g, '(株)').replace(/㈲/g, '(有)').replace(/㈹/g, '(代)')

v.replace(/\(株\)/g, '㈱').replace(/\(有\)/g, '㈲').replace(/\(代\)/g, '㈹')

漢字(よみがな)⟺ ruby タグ相互変換 / Kanji (reading Kana) ⟺ ruby tag Mutual exchange

鹿児島県 志布志市(しぶしし) 志布志町(しぶししちょう) 志布志(しぶし) 岐阜県 安八郡(あんぱちぐん) 神戸町(ごうどちょう) 三重県 伊賀市 神戸(かんべ) 岡山県 津山市 神戸(じんご) 愛知県 知多郡 武豊町 ヱケ屋敷(えげやしき) 愛知県 知多郡 武豊町 ヲヲガケ(おおがけ) 石川県 輪島市 小田屋町 ヰ(い)
鹿児島県 <ruby><rb>志布志市</rb><rt>しぶしし</rt></ruby> <ruby><rb>志布志町</rb><rt>しぶししちょう</rt></ruby> <ruby><rb>志布志</rb><rt>しぶし</rt></ruby> 岐阜県 <ruby><rb>安八郡</rb><rt>あんぱちぐん</rt></ruby> <ruby><rb>神戸町</rb><rt>ごうどちょう</rt></ruby> 三重県 伊賀市 <ruby><rb>神戸</rb><rt>かんべ</rt></ruby> 岡山県 津山市 <ruby><rb>神戸</rb><rt>じんご</rt></ruby> 愛知県 知多郡 武豊町 <ruby><rb>ヱケ屋敷</rb><rt>えげやしき</rt></ruby> 愛知県 知多郡 武豊町 <ruby><rb>ヲヲガケ</rb><rt>おおがけ</rt></ruby> 石川県 輪島市 小田屋町 <ruby><rb>ヰ</rb><rt>い</rt></ruby>
問題

「漢字(よみがな)」という文字列と HTML の ruby タグ形式を相互変換します。しかし、「漢字(よみがな)」だけでは読み仮名の対象の漢字の始まりが定まりませんので、ここでは約束事として空白区切りを使った「␣漢字(よみがな)」の形式に定めることにします。

ここで「よみがな」を囲う括弧は全角と定めます。また「」はスペースを可視化したものです。

また、コード例としては rp タグを使用しない以下の単純な ruby タグの形式に留めてあります。

漢字よみがな
<ruby><rb>漢字</rb><rt>よみがな</rt></ruby>
ヒント

「よみがな」ということなので全角括弧の中は「ひらがな・カタカナ」に限定しておいた方が多少実用的かもしれません。その場合、文字集合は Unicode の並びから「[ぁ-んァ-ンㇰ-ㇿ]」となります。逆変換のときは、その文字集合は使わなくてよいかと思われます。

解説と発展 〜 最左最大マッチ・最左最小マッチと文字集合

まずは、右欄の逆変換において、コード例の正規表現「(.*?)」を初めは

(.*)

としてしまいがちです。この間違いを試してみると、意図せず2個以上の ruby タグまでマッチしてしまいます。正規表現は基本は「貪欲マッチ」なので「非貪欲マッチ」なるコード例 (.*?) が正解となります。しかし、この「貪欲」という用語は不十分ですので説明を続けます。

一方で、左欄の変換において、コード例の正規表現「␣([^␣]*)」を初めは

␣(.*)

としてしまいがちです。この間違いを試してみると、やはり、ルビの対象範囲が意図せず左端からマッチしてしまいます。そこで、「非貪欲マッチ」なる

␣(.*?)

に直してみましょう。これでもルビの対象範囲が意図せず左端からマッチしてしまう箇所が残っています。

正規表現の「貪欲マッチ」は正確には「最左最大マッチ」であると覚えておいてください。よって、まだこの「最左最小マッチ」の最左が意図せずルビの対象範囲を広げてしまっているので、この場合、文字集合を狭めてしまうのが適切で、ゆえに ␣([^␣]*) が正解となります。

ちなみに、「最右最大マッチ」「最右最小マッチ」というのは聞いたことはありませんが「絶対最大マッチ」は他の言語 Perl などにはあります。また、「貪欲」に並んで「強欲」という用語も一般に使われているようですが、誤りではないにせよ、いずれも正規表現の基本「最左最大マッチ」の一面のみを表す用語となります。

ruby > rp タグに対応した版がこちらにあります。


理解の一助として、既定のコード例のフローチャートを記しておきます。

本節の正規表現による置換のフローチャート
v.replace(/ ([^ ]*)(([ぁ-んァ-ンㇰ-ㇿ]*))/g, ' <ruby><rb>$1</rb><rt>$2</rt></ruby>')

v.replace(/<ruby><rb>(.*?)<\/rb><rt>(.*?)<\/rt><\/ruby>/g, '$1($2)')

rgb(𝑟(10), 𝑔(10), 𝑏(10)) ⟺ #FFFFFF(16) 相互変換 / Mutual exchange

rgb(0,0,0) rgb(0,43,54) rgb(7,54,66) rgb(88,110,117) rgb(101,123,131) rgb(131,148,150) rgb(147,161,161) rgb(238,232,213) rgb(253,246,227) rgb(181,137,0) rgb(203,75,22) rgb(220,50,47) rgb(211,54,130) rgb(108,113,196) rgb(38,139,210) rgb(42,161,152) rgb(133,153,0) rgb(255,255,255)
#000000 #002b36 #073642 #586e75 #657b83 #839496 #93a1a1 #eee8d5 #fdf6e3 #b58900 #cb4b16 #dc322f #d33682 #6c71c4 #268bd2 #2aa198 #859900 #ffffff
問題

ウェブ技術の CSS (Cascading Style Sheets) で RGB 色指定で用いられる2つの形式を相互変換します。

ヒント

Javascript において、文字列 ⟺ 数値の相互変換は以下の通りです。

文字列 (string) から数値 (value) への変換、及び、ゼロ詰め
10 進法
value = parseInt(string)
16 進法
value = parseInt(string, 16)
幅2桁(2文字)のゼロ(文字)詰め
string.padStart(2, '0')
数値 (value) から文字列 (string) への変換
10 進法
string = value.toString()(ですが、まず不要)
16 進法
string = value.toString(16)

また、拙作のコード例で使用している文字列や配列におけるアルゴリズムを紹介しておきます。

string.split(/,\s*/)
文字列を正規表現 /,\s*/ を区切りとして分割した配列を返す
array.join(',')
配列の要素を文字列 ',' を区切りとして連結した文字列を返す
string.slice(i, i + n)
文字列の i 文字目から n 文字取り出す
array.map(function (v) { return v; })
array.map(v=>v)
配列のすべての要素について関数で写像した配列を返す
解説と発展 〜 基本的な文字列処理

コード例では 16 進法は二桁ずつの RGB 値と決めうちしてしまっているので、実用的には不十分です。RGB 値には一桁ずつもあり得ます。つまり、原色分解能 256 の #FFFFFF だけでなく原色分解能 16 の #FFF でもよいわけです。

そこで、右欄では原色分解能 16 への対応、左欄でも(すべきか否かは別として)原色分解能 16 への対応を実現してみます。以下のようなコード例となります。

rgb(r,g,b)#FFF, #FFFFFF
v.replace(/rgb\(([\d,]*)\)/g, (m,p)=>'#' + p.split(/\s*,\s*/).map(v=>parseInt(v).toString(16).padStart(2, '0')).join('').replace(/^(.)\1(.)\2(.)\3$/, '$1$2$3'))
#FFF, #FFFFFFrgb(r,g,b)
v.replace(/#([0-9A-F]{6}|[0-9A-F]{3})/ig, (m,p)=>'rgb(' + (p.length == 6 ? [ p.slice(0,2), p.slice(2,4), p.slice(4,6) ] : [ p.slice(0,1).repeat(2), p.slice(1,2).repeat(2), p.slice(2,3).repeat(2) ]).map(v=>parseInt(v, 16)).join(',') + ')')

ここで、replace(/^(.)\1(.)\2(.)\3$/, '$1$2$3') は特に重要で、置換先の $1 等もキャプチャグループの参照ですが、置換元の正規表現のなかの 「\1」等もキャプチャグループの後方参照です。


理解の一助として、最終案のコード例のフローチャートを記しておきます。

本節の正規表現による置換のフローチャート
v.replace(/rgb\(([\d,]*)\)/g, (m,p)=>'#' + p.split(/\s*,\s*/).map(v=>parseInt(v).toString(16).padStart(2, '0')).join('').replace(/^(.)\1(.)\2(.)\3$/, '$1$2$3'))

v.replace(/#([0-9A-F]{6}|[0-9A-F]{3})/ig, (m,p)=>'rgb(' + (p.length == 6 ? [ p.slice(0,2), p.slice(2,4), p.slice(4,6) ] : [ p.slice(0,1).repeat(2), p.slice(1,2).repeat(2), p.slice(2,3).repeat(2) ]).map(v=>parseInt(v, 16)).join(',') + ')')

原色分解能 16 に対応したので、上記の右欄のコードは実用的に必要十分ですが、左欄のコードについては必ずしもそうは言えません。そもそも、構わず 6 桁の原色分解能 256 で統一しておくのが無難です。そうした場合、オプションで 3 桁で出力可能にしておく等が好ましいですが、以下のように言語 Javascript の「コメント」にしておくと後々役立つときが来るかもしれません。

v.replace(/rgb\(([\d,]*)\)/g, (m,p)=>'#' + p.split(/\s*,\s*/).map(v=>parseInt(v).toString(16).padStart(2, '0')).join('')/*.replace(/^(.)\1(.)\2(.)\3$/, '$1$2$3')*/)

TSV (tab separated values) ⟺ HTML table tbody contents 相互変換 / Mutual exchange

k(kilo/キロ/㌔) 10<sup>3</sup> Ki(kibi/キビ) 2<sup>10</sup> 2% M(mega/メガ/㍋) 10<sup>6</sup> Mi(mebi/メビ) 2<sup>20</sup> 4% G(giga/ギガ/㌐) 10<sup>9</sup> Gi(gibi/ギビ) 2<sup>30</sup> 7% T(tera/テラ) 10<sup>12</sup> Ti(tebi/テビ) 2<sup>40</sup> 9% P(peta/ペタ) 10<sup>15</sup> Pi(pebi/ペビ) 2<sup>50</sup> 12% E(exa/エクサ) 10<sup>18</sup> Ei(exbi/エクスビ) 2<sup>60</sup> 15% Z(zetta/ゼタ) 10<sup>21</sup> Zi(zebi/ゼビ) 2<sup>70</sup> 18% Y(yotta/ヨタ) 10<sup>24</sup> Yi(yobi/ヨビ) 2<sup>80</sup> 20%
<table><tbody> <tr><td>k(kilo/キロ/㌔)</td><td>10<sup>3</sup></td><td>Ki(kibi/キビ)</td><td>2<sup>10</sup></td><td>2%</td></tr> <tr><td>M(mega/メガ/㍋)</td><td>10<sup>6</sup></td><td>Mi(mebi/メビ)</td><td>2<sup>20</sup></td><td>4%</td></tr> <tr><td>G(giga/ギガ/㌐)</td><td>10<sup>9</sup></td><td>Gi(gibi/ギビ)</td><td>2<sup>30</sup></td><td>7%</td></tr> <tr><td>T(tera/テラ)</td><td>10<sup>12</sup></td><td>Ti(tebi/テビ)</td><td>2<sup>40</sup></td><td>9%</td></tr> <tr><td>P(peta/ペタ)</td><td>10<sup>15</sup></td><td>Pi(pebi/ペビ)</td><td>2<sup>50</sup></td><td>12%</td></tr> <tr><td>E(exa/エクサ)</td><td>10<sup>18</sup></td><td>Ei(exbi/エクスビ)</td><td>2<sup>60</sup></td><td>15%</td></tr> <tr><td>Z(zetta/ゼタ)</td><td>10<sup>21</sup></td><td>Zi(zebi/ゼビ)</td><td>2<sup>70</sup></td><td>18%</td></tr> <tr><td>Y(yotta/ヨタ)</td><td>10<sup>24</sup></td><td>Yi(yobi/ヨビ)</td><td>2<sup>80</sup></td><td>20%</td></tr> </tbody></table>
問題

プレーンテキストの TSV (tab separated values) とウェブ技術の HTML (Hyper Text Markup Language) の表 (table) 内の行列フォーマットを相互変換し、先頭 table, tbody タグ、末尾 /table, /tbody 閉じタグを付加・削除します。

また、TSV 形式のタブ出力において、この HTML で列が揃うような配慮は不要です。昨今はタブによるプレーンテキストの列を揃えることは、フォントに依存するので意味がありません。翻って TSV 形式のタブ入力において、人手でプレーンテキストのタブの列を揃えることは至極当然なので、タブ区切りは「連続する複数のタブ」を区切りとします。

ヒント

行区切りの改行文字、列区切りのタブコードの変換処理となります。

いろいろなやり方があり得ますが、コード例のように、HTML の捉え方から少し転換して「</tr>\n<tr>」「</td><td>」を行列区切りとみなすと簡潔に書けます。

解説と発展 〜 デリミタ(区切り)の処理と正規表現のプロパティ・フラグ

左欄のコード例では 'm' フラグを使っていないので、実は少し冗長です。この複数行フラグ multiline プロパティを使用すると以下のように .replace(/\n/g, '</td></tr>\n<tr><td>') が不要になります。

'<table><tbody>\n' + v.replace(/\n*$/, '').replace(/\t/g, '</td><td>').replace(/^/mg, '<tr><td>').replace(/$/mg, '</td></tr>') + '\n</tbody></table>'

ちなみに、いずれの例でも最初に replace(/\n*$/, '') していますが、最終行に空行はないものとして処理を行っていることに拠ります。よって、これがない場合は HTML の表で末尾が空行になります。

一方で、右欄においては、そもそも HTML は「行」という区切りはまず無意味なので、複数行フラグはむしろ不要です。さらに言えば、右欄のテキストエリアの改行がなくても同様に TSV 形式に変換できないといけません。そこで効いているのがコード例の「replace(/\s*<tr><td>(.*?)<\/td><\/tr>\s*/g, '$1\n')」の \n に関する処理となります。

また、右欄のコード例では、最初の replace(/^\s*<table><tbody>|<\/tbody><\/table>\s*$/g, '') の「g」フラグが冗長だと勘違いしがちですが、この global プロパティがないと、バッファ先頭にマッチしたときバッファ末尾にマッチしなくなるので(先頭と末尾の開きタグと閉じタグを消したいこの場合は)必須です。

table > tbody > tr > th タグに対応した版がこちらにあります。


理解の一助として、既定のコード例のフローチャートを記しておきます。

本節の正規表現による置換のフローチャート
'<table><tbody>\n' + v.replace(/\n*$/, '').replace(/\t/g, '</td><td>').replace(/^/, '<tr><td>').replace(/$/, '</td></tr>').replace(/\n/g, '</td></tr>\n<tr><td>') + '\n</tbody></table>'

v.replace(/^\s*<table><tbody>|<\/tbody><\/table>\s*$/g, '').replace(/\s*<tr><td>(.*?)<\/td><\/tr>\s*/g, '$1\n').replace(/<\/td><td>/g, '\t')

† これまでのフローチャートの書き方よりも簡略化した書き方になっています。


ハイフン-マイナス ⟺ ダッシュ 他、相互変換 / hypen-minuses ⟺ dash symbols etc. Mutual exchange

「万物の根源は数なり。」/ "The root of all things is number." --- Pythagoras (ピタゴラス) BC582-BC496 「ギャンブルの最大の利益は、それを行わないことで得られる。」/ "The greatest profit in gambling is obtained by not doing it." --- Gerolamo Cardano (ジェロラモ・カルダーノ) 1501-1576 「宇宙は数学という言語で書かれている。」/ "The universe is written in the language of mathematics." --- Galileo Galilei (ガリレオ・ガリレイ) 1564-1642 「私はこの命題の真に驚くべき証明をもっているが、余白が狭すぎるのでここに記すことはできない。」/ "I have a truly remarkable proof of this proposition, but the space is too small for me to give it here." --- Pierre de Fermat (ピエール・ド・フェルマー) 1601-1665 「人間は考える葦である。」/ "Man is a thinking reed." --- Blaise Pascal (ブレーズ・パスカル) 1623-1662 「神は数によって万物を創造した。」/ "God created all things by numbers." --- Isaac Newton (アイザック・ニュートン) 1642-1727 「整数は神の作ったものだが、他は人間の作ったものである。」/ "The integers are the work of God, but the rest is the work of man." --- Leopold Kronecker (レオポルト・クロネッカー) 1823-1891 「我々は知らねばならない、我々は知るであろう。」/ "We must know, and we shall know." --- David Hilbert (ダフィット・ヒルベルト) 1862-1943 「数学は生命の燃焼である。」/ "Mathematics is the combustion of life." --- 岡潔 (おかきよし) 1901-1978 「これで、世界で2番目に計算が上手な奴が生まれた。」/ "Now we have the second best calculator in the world." --- John von Neumann (ジョン・フォン・ノイマン) 1903-1957
「万物の根源は数なり.」/ “The root of all things is number.” — Pythagoras (ピタゴラス) BC582–BC496 「ギャンブルの最大の利益は,それを行わないことで得られる.」/ "The greatest profit in gambling is obtained by not doing it." — Gerolamo Cardano (ジェロラモ・カルダーノ) 1501–1576 「宇宙は数学という言語で書かれている.」/ "The universe is written in the language of mathematics." — Galileo Galilei (ガリレオ・ガリレイ) 1564–1642 「私はこの命題の真に驚くべき証明をもっているが,余白が狭すぎるのでここに記すことはできない.」/ "I have a truly remarkable proof of this proposition, but the space is too small for me to give it here." — Pierre de Fermat (ピエール・ド・フェルマー) 1601–1665 「人間は考える葦である.」/ "Man is a thinking reed." — Blaise Pascal (ブレーズ・パスカル) 1623–1662 「神は数によって万物を創造した.」/ "God created all things by numbers." — Isaac Newton (アイザック・ニュートン) 1642–1727 「整数は神の作ったものだが,他は人間の作ったものである.」/ "The integers are the work of God, but the rest is the work of man." — Leopold Kronecker (レオポルト・クロネッカー) 1823–1891 「我々は知らねばならない,我々は知るであろう.」/ "We must know, and we shall know." — David Hilbert (ダフィット・ヒルベルト) 1862–1943 「数学は生命の燃焼である.」/ "Mathematics is the combustion of life." — 岡潔 (おかきよし) 1901–1978 「これで,世界で2番目に計算が上手な奴が生まれた.」/ "Now we have the second best calculator in the world." — John von Neumann (ジョン・フォン・ノイマン) 1903–1957
問題

WordPress などの CMS (Content Management System) でよくやられることなのですが、3つのハイフンマイナス「---」と Unicode の EM ダッシュ「」を相互変換します。それだけでなく、西暦に囲まれた1つのハイフンマイナスと EN ダッシュ「」、ペアのダブルクオーテーションマーク「"」と左右ダブルクーテーションマーク「“〜”」のペア、「」と「」、「」と「」も相互変換します。一つのハイフンマイナス、及び、ペアのダブルクオーテーションマーク「"」は少し難しいですし、どのようなテキストにおいても万能な変換は困難です。ある程度、前提となる約束事があるものとして、ここではサンプルのテキストで通用すればよいでしょう。

ヒント

ペアのダブルクオーテーションマーク「"」に関しては、キャプチャグループ(後方参照とも)を知る必要があるでしょう。とは言え、アンバランスなダブルクオーテーションマークにまず対応は不可能だと思われます。

一つのハイフンマイナスに関しては正規表現の「先読み言明」「後読み言明」を知ると簡潔に書けますが、SeaMonkey では「後読み言明」が未対応なため、例では使用を避けています。

解説と発展 〜 キャプチャグループ(後方参照)・非キャプチャグループ・言明

見逃しがちなのが紀元前の「BC582-BC496」でしょうか。この「BC」のある無しは正規表現で 0 か 1 の繰り返しということで (BC)? と書きますが、参照のためのキャプチャは不要なので (?:BC)? となります。

「肯定先読み言明 (?=〜)」と「肯定後読み言明 (?<=〜)」を以下のように使用すると少しだけ簡潔になります。

変換:
v.replace(/---/g, '—').replace(/(?<=[0-9]{1,4})-(?=(?:BC)?[0-9]{1,4})/g, '–').replace(/。/g, '.').replace(/、/g, ',').replace(/"(.*?)"/g, '“$1”')
逆変換:
v.replace(/—/g, '---').replace(/(?<=[0-9]{1,4})–(?=(?:BC)?[0-9]{1,4})/g, '-').replace(/./g, '。').replace(/,/g, '、').replace(/[“”]/g, '"')

「肯定後読み言明」を使わなくてもキャプチャしてしまえばよいので何とかなりますが、「否定後読み言明 (?<!〜)」の方はそうはいかないでしょうから、未対応の SeaMonkey では戦略の練り直しが必要になるかもしれません。とは言え、簡潔に書けなくなるだけで、不可能ということにはならないはずです。特に Javascript では関数形式の置換先が可能なので、それを使えばなんとかなります。

ともあれ、「キャプチャグループ (後方参照)・非キャプチャグループ」はマッチする範囲に含まれますが、キャプチャグループの外の「言明」はマッチする範囲には含まれないことに留意しましょう。


理解の一助として、「肯定先読み言明」と「肯定後読み言明」を採ったコード例のフローチャートを記しておきます。

本節の正規表現による置換のフローチャート
v.replace(/---/g, '—').replace(/(?<=[0-9]{1,4})-(?=(?:BC)?[0-9]{1,4})/g, '–').replace(/。/g, '.').replace(/、/g, ',').replace(/"(.*?)"/g, '“$1”')

v.replace(/—/g, '---').replace(/(?<=[0-9]{1,4})–(?=(?:BC)?[0-9]{1,4})/g, '-').replace(/./g, '。').replace(/,/g, '、').replace(/[“”]/g, '"')

全角数字 ⟺ 半角数字 相互変換 / Full width ⟺ Half width Mutual exchange

北海道 060-8588 札幌市中央区北3条西6-1 011-231-4111 青森県 030-8570 青森市長島1-1-1 017-722-1111 岩手県 020-8570 盛岡市内丸10-1 019-651-3111 宮城県 980-8570 仙台市青葉区本町3-8-1 022-211-2111 秋田県 010-8570 秋田市山王4-1-1 018-860-1111 山形県 990-8570 山形市松波2-8-1 023-630-2211 福島県 960-8670 福島市杉妻町2-16 024-521-1111 茨城県 310-8555 水戸市笠原町978-6 029-301-1111 栃木県 320-8501 宇都宮市塙田1-1-20 028-623-2323 群馬県 371-8570 前橋市大手町1-1-1 027-223-1111 埼玉県 330-9301 さいたま市浦和区高砂3-15-1 048-824-2111 千葉県 260-8667 千葉市中央区市場町1-1 043-223-2110 東京都 163-8001 新宿区西新宿2-8-1 03-5321-1111 神奈川県 231-8588 横浜市中区日本大通1 045-210-1111 新潟県 950-8570 新潟市中央区新光町4-1 025-285-5511 富山県 930-8501 富山市新総曲輪1-7 076-431-4111 石川県 920-8580 金沢市鞍月1-1 076-225-1111 福井県 910-8580 福井市大手3-17-1 0776-21-1111 山梨県 400-8501 甲府市丸の内1-6-1 055-237-1111 長野県 380-8570 長野市大字南長野字幅下692-2 026-232-0111 岐阜県 500-8570 岐阜市藪田南2-1-1 058-272-1111 静岡県 420-8601 静岡市葵区追手町9-6 054-221-2455 愛知県 460-8501 名古屋市中区三の丸3-1-2 052-961-2111 三重県 514-8570 津市広明町13 059-224-3070 滋賀県 520-8577 大津市京町4-1-1 077-528-3993 京都府 602-8570 京都市上京区下立売通新町西入薮ノ内町 075-451-8111 大阪府 540-8570 大阪市中央区大手前2-1-22 06-6941-0351 兵庫県 650-8567 神戸市中央区下山手通5-10-1 078-341-7711 奈良県 630-8501 奈良市登大路町30 0742-22-1101 和歌山県 640-8585 和歌山市小松原通1-1 073-432-4111 鳥取県 680-8570 鳥取市東町1-220 0857-26-7111 島根県 690-8501 松江市殿町1 0852-22-5111 岡山県 700-8570 岡山市北区内山下2-4-6 086-224-2111 広島県 730-8511 広島市中区基町10-52 082-228-2111 山口県 753-8501 山口市滝町1-1 083-922-3111 徳島県 770-8570 徳島市万代町1-1 088-621-2500 香川県 760-8570 高松市番町4-1-10 087-831-1111 愛媛県 790-8570 松山市一番町4-4-2 089-941-2111 高知県 780-8570 高知市丸ノ内1-2-20 088-823-1111 福岡県 812-8577 福岡市博多区東公園7-7 092-651-1111 佐賀県 840-8570 佐賀市城内1-1-59 0952-24-2111 長崎県 850-8570 長崎市尾上町3-1 095-824-1111 熊本県 862-8570 熊本市中央区水前寺6-18-1 096-383-1111 大分県 870-8501 大分市大手町3-1-1 097-536-1111 宮崎県 880-8501 宮崎市橘通東2-10-1 0985-26-7111 鹿児島県 890-8577 鹿児島市鴨池新町10-1 099-286-2111 沖縄県 900-8570 那覇市泉崎1-2-2 098-866-2333
北海道 060−8588 札幌市中央区北3条西6−1 011−231−4111 青森県 030−8570 青森市長島1−1−1 017−722−1111 岩手県 020−8570 盛岡市内丸10−1 019−651−3111 宮城県 980−8570 仙台市青葉区本町3−8−1 022−211−2111 秋田県 010−8570 秋田市山王4−1−1 018−860−1111 山形県 990−8570 山形市松波2−8−1 023−630−2211 福島県 960−8670 福島市杉妻町2−16 024−521−1111 茨城県 310−8555 水戸市笠原町978−6 029−301−1111 栃木県 320−8501 宇都宮市塙田1−1−20 028−623−2323 群馬県 371−8570 前橋市大手町1−1−1 027−223−1111 埼玉県 330−9301 さいたま市浦和区高砂3−15−1 048−824−2111 千葉県 260−8667 千葉市中央区市場町1−1 043−223−2110 東京都 163−8001 新宿区西新宿2−8−1 03−5321−1111 神奈川県 231−8588 横浜市中区日本大通1 045−210−1111 新潟県 950−8570 新潟市中央区新光町4−1 025−285−5511 富山県 930−8501 富山市新総曲輪1−7 076−431−4111 石川県 920−8580 金沢市鞍月1−1 076−225−1111 福井県 910−8580 福井市大手3−17−1 0776−21−1111 山梨県 400−8501 甲府市丸の内1−6−1 055−237−1111 長野県 380−8570 長野市大字南長野字幅下692−2 026−232−0111 岐阜県 500−8570 岐阜市藪田南2−1−1 058−272−1111 静岡県 420−8601 静岡市葵区追手町9−6 054−221−2455 愛知県 460−8501 名古屋市中区三の丸3−1−2 052−961−2111 三重県 514−8570 津市広明町13 059−224−3070 滋賀県 520−8577 大津市京町4−1−1 077−528−3993 京都府 602−8570 京都市上京区下立売通新町西入薮ノ内町 075−451−8111 大阪府 540−8570 大阪市中央区大手前2−1−22 06−6941−0351 兵庫県 650−8567 神戸市中央区下山手通5−10−1 078−341−7711 奈良県 630−8501 奈良市登大路町30 0742−22−1101 和歌山県 640−8585 和歌山市小松原通1−1 073−432−4111 鳥取県 680−8570 鳥取市東町1−220 0857−26−7111 島根県 690−8501 松江市殿町1 0852−22−5111 岡山県 700−8570 岡山市北区内山下2−4−6 086−224−2111 広島県 730−8511 広島市中区基町10−52 082−228−2111 山口県 753−8501 山口市滝町1−1 083−922−3111 徳島県 770−8570 徳島市万代町1−1 088−621−2500 香川県 760−8570 高松市番町4−1−10 087−831−1111 愛媛県 790−8570 松山市一番町4−4−2 089−941−2111 高知県 780−8570 高知市丸ノ内1−2−20 088−823−1111 福岡県 812−8577 福岡市博多区東公園7−7 092−651−1111 佐賀県 840−8570 佐賀市城内1−1−59 0952−24−2111 長崎県 850−8570 長崎市尾上町3−1 095−824−1111 熊本県 862−8570 熊本市中央区水前寺6−18−1 096−383−1111 大分県 870−8501 大分市大手町3−1−1 097−536−1111 宮崎県 880−8501 宮崎市橘通東2−10−1 0985−26−7111 鹿児島県 890−8577 鹿児島市鴨池新町10−1 099−286−2111 沖縄県 900−8570 那覇市泉崎1−2−2 098−866−2333
問題

日本語の文字集合において特有の「全角文字」と「半角文字」を数字についてのみ相互変換します。本来、ラテン・アルファベットの全角文字もありますが、方法は同じようになるでしょう。

ヒント

「半角文字」から「全角文字」への変換は、変換テーブルを用意するしかなさそうですが、数字に関しては Unicode のコードポイントに順序通りにそれぞれ並んでいるので(例ではそのようにしてませんが)コード変換で達成できます。

「全角文字」から「半角文字」への変換も基本的に同じようで構わないのですが、コード例ではやはり Javascript の string.normalize('NFKC') を使用しています。

解説と発展 〜 キー値 { key: value, ... }(辞書型、連想配列、ハッシュ等)の利用

左欄のコード例を、右欄と同じ戦略で実装すると以下のようになります。

v.replace(/[-0-9]/g, m=>{ const imap = { '-': '-', '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, }; return imap[m]; })

しかし、数字は流石に Unicode で順序よく並びますので、コード変換で両者を実装し直すと以下のようになります。

変換:
v.replace(/[-0-9]/g, m=>{ const imap = { '-': '-', ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0xff10 + i), String.fromCodePoint(0x30 + i) ])), }; return imap[m]; })
逆変換:
v.replace(/[-0-9]/g, m=>{ const map = { '-': '-', ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0x30 + i), String.fromCodePoint(0xff10 + i) ])), }; return map[m]; })

元のコード例の戦略を踏襲したこともあって、コードとしては逆に長くなってしまいました。しかし、キー値である Javascript の Object (キー値、辞書型) をアルゴリズムで生成する好例となっています。キー値の手動での列挙と同等なものを自動生成しているだけですが、一見して不明な方はじっくり Javascript の仕様から読み解くことをお勧めします。

ちなみに、const map = { key: value, ... };v.replace 内のスコープ外で定義すべきです(そうしていないのは、この頁の設計上の都合です)。[2022/03/07 追記] そのように修正し、ついでに、正規表現も動的に生成するように改善したものを以下に記しておきます。その設計上の都合があって Javascript の深い知識がないと難しいかもしれませんが、大いに参考になると思います。

変換:
(v=>{
  const imap = {
    '-': '-',
    ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0xff10 + i), String.fromCodePoint(0x30 + i) ])),
  }; 
  return v.replace(new RegExp(`[${Object.keys(imap).join('')}]`, 'g'), m=>imap[m]);
})(v)
逆変換:
(v=>{
  const map = {
    '-': '-',
    ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0x30 + i), String.fromCodePoint(0xff10 + i) ])),
  };
  return v.replace(new RegExp(`[${Object.keys(map).join('')}]`, 'g'), m=>map[m]);
})(v)

用途は同じではないのですが string.normalize() のみに特化した版がこちらにあります。


理解の一助として、最終案のコード例のフローチャートを記しておきます。特に言語 Javascript を使い倒しているので、それを学ぶよい機会となるかと思います。

本節の正規表現による置換のフローチャート
(v=>{
  const imap = {
    '-': '-',
    ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0xff10 + i), String.fromCodePoint(0x30 + i) ])),
  }; 
  return v.replace(new RegExp(`[${Object.keys(imap).join('')}]`, 'g'), m=>imap[m]);
})(v)

(v=>{
  const map = {
    '-': '-',
    ...Object.fromEntries((new Array(10)).fill(0).map((v,i)=>[ String.fromCodePoint(0x30 + i), String.fromCodePoint(0xff10 + i) ])),
  };
  return v.replace(new RegExp(`[${Object.keys(map).join('')}]`, 'g'), m=>map[m]);
})(v)

言語 Javascript についてですが、

v=>{
  return v;
}

という記法は、無名関数:

function (v) {
  return v;
}

のシンタックスシュガーであり、以下の、括弧・括弧閉じで囲み、括弧・括弧閉じを付記した:

(function (v) {
  return v;
})(v)

は、引数 v におけるこの無名関数の呼び出しとなります。そして、function (v) { return v; } にように一文であるなら左右ブレース「{}」及び return が省略できて v=>v のようなシンタックスシュガーとなります。本稿では各所でそれが使われていることになります。複数の引数がある場合は (m, p)=>p のように左右括弧「()」は省略できません。


TSV (tab separated values) ⟺ CSV (comma separated values) 相互変換 / Mutual exchange

名称 発売年月 CPU コア数 GHz GPU Wh 価格(円) 15" 2015 2015/05/19 Core i7-4980HQ 4 2.8 Radeon R9 M370X 2GB 100 ¥282,800 15" 2016 2016/11/12 Core i7-6920HQ 4 3.8 Radeon Pro 460 4GB 76 ¥238,800 15" 2017 2017/06/08 Core i7-7920HQ 4 4.1 Radeon Pro 560 4GB 76 ¥258,800 15" 2018 2018/07/11 Core i9-8950HK 6 4.5 Radeon Pro 560X 4GB 84 ¥302,800 15" 2019 2019/05/21 Core i9-9980HK 8 5.0 Radeon Pro 560X 4GB 84 ¥407,800 16" 2019 2019/11/13 Core i9-9980HK 8 5.0 Radeon Pro 5500M 4GB 100 ¥288,800 16" 2021 2021/10/26 ARMv8-A 10 3.2 Radeon Pro 5600M 100 ¥299,800
名称,発売年月,CPU,コア数,GHz,GPU,Wh,価格(円) "15"" 2015",2015/05/19,Core i7-4980HQ,4,2.8,Radeon R9 M370X 2GB,100,"¥282,800" "15"" 2016",2016/11/12,Core i7-6920HQ,4,3.8,Radeon Pro 460 4GB,76,"¥238,800" "15"" 2017",2017/06/08,Core i7-7920HQ,4,4.1,Radeon Pro 560 4GB,76,"¥258,800" "15"" 2018",2018/07/11,Core i9-8950HK,6,4.5,Radeon Pro 560X 4GB,84,"¥302,800" "15"" 2019",2019/05/21,Core i9-9980HK,8,5.0,Radeon Pro 560X 4GB,84,"¥407,800" "16"" 2019",2019/11/13,Core i9-9980HK,8,5.0,Radeon Pro 5500M 4GB,100,"¥288,800" "16"" 2021",2021/10/26,ARMv8-A,10,3.2,Radeon Pro 5600M,100,"¥299,800"
問題

TSV (tab separated values) 形式とそれよりも一般的な CSV (comma separated values) 形式の相互変換をします。CSV には規格があり、列区切りのカンマ「,」そのものを行列セルの内容として使いたいときは、ダブルクオーテーションマーク「"」で括ります。そのため、ダブルクオーテーションマークそのものを使いたいときはダブルクーテーションマークを「""」2つ重ねなければなりません(プログラミング言語 BASIC 由来)。

また、前述のように TSV 形式のタブ入出力において、区切り一つとなる連続するタブの数は幾つでも構わないものとします。

ヒント

説明の通り、CSV 形式は多少厄介な代物です。一方、TSV 形式は広く使用されていますが、正式な規格などはないので、連続するタブ「\t+(正規表現)」を一つの区切りとみなす等、情報交換同士の約束事はあり得ます。

やり方はいろいろあると思いますが、正規表現の言明をうまく活用しないと左欄は難しいかもしれません。

解説と発展 〜 先読み言明、string.match メソッドの利用

コード例にて使用されている主な正規表現を解説します。ちなみに、以下「行列要素」とは表におけるセルの内容を指すものとします。

変換: TSV ⇒ CSV
/(^|\t+)(.*?)(?=\t+|$)/mg
行列要素を2番目のキャプチャグループへ抽出
/^(.*[,"].*)$/
行列要素の中にカンマやダブルクオートが存在
逆変換: CSV ⇒ TSV
/(".*?(?:""|,).*?"|.*?)(,|$)/mg
カンマで終わるか末尾の、ダブルクオートで囲われたカンマもしくは2つ並びのダブルクオートが中にある行列要素、または、任意の文字列の行列要素を、1番目のキャプチャグループへ抽出
/^"(.*)"$/
ダブルクオートで囲われた文字列である行列要素

コード例では、これらと関数形式の置換先も駆使して変換・逆変換を実現しています。

発展 〜 欠点をあげ、改良せよ

さて、このコード例の欠点をあげるとしたら何があるでしょうか。以下に挙げてみましょう。

  1. この TSV では「空文字」の要素を表すことができない。
  2. CSV 規格ではダブルクオートで囲まれていれば改行 \n そのものを行列の要素に含めてもよい。

1. に関しては、TSV における約束事を設けるしかないと思います。ここでは 「\u00a0」(U+00A0 NO-BREAK SPACE) のみの要素を「空文字」と見なすことにします。

2. に関しては、これも TSV における約束事を設けるしかありません。ここでは \n に対して \\n (バックスラッシュそのものと「n」) を TSV における改行と見なすことにします。

これらを踏まえた改良案を示します。

変換:
v.replace(/(^|\t+)(.*?)(?=\t+|$)/mg, (m,p1,p2)=>(p1 + p2.replace(/"/g, '""').replace(/\\n/g, '\n').replace(/^(.*[,"\n].*)$/s, '"$1"').replace(/^\u00a0$/g, '')).replace(/\t+/g, ','));
逆変換:
v.replace(/(".*?(?:""|,|\n).*?"|.*?)(,|$)/smg,  (m,p1,p2)=>(p1.match(/^".*"$/s) ? p1.replace(/^"(.*)"$/s,  (m, p)=>p.replace(/""/g, '"').replace(/\n/g, '\\n')) : p1.replace(/^$/, '\u00a0')) + p2.replace(/,/g, '\t'))

改良ポイントとしては、変換では replace(/\n/g, '\\n')replace(/^\u00a0$/g, '') を、逆変換では replace(/\\n/g, '\n')replace(/^\u00a0$/g, '') を、適所で行っていることに加えて、両者において、改行 \n を正規表現の任意の一文字「.」にマッチさせるために 's' フラグを適所に添えて、正規表現にてダブルクオート内で許される「,」と同様に「\n」も添えていることにあります。

この改良版がこちらにあります。


理解の一助として、(改良版ではなく)既定の改良版のコード例のフローチャートを記しておきます。正規表現だと何やらさらっと済んでいる処理が、プローチャートにすると具体的には緻密に処理されていることがわかると思います。

本節の正規表現による置換のフローチャート
v.replace(/(^|\t+)(.*?)(?=\t+|$)/mg, (m,p1,p2)=>(p1 + p2.replace(/"/g, '""').replace(/^(.*[,"].*)$/, '"$1"')).replace(/\t+/g, ','))

v.replace(/(".*?(?:""|,).*?"|.*?)(,|$)/mg, (m,p1,p2)=>(p1.match(/^".*"$/) ? p1.replace(/^"(.*)"$/, (m,p)=>p.replace(/""/g, '"')) : p1) + p2.replace(/,/g, '\t'))

TSV も CSV も行区切りは改行「\n」(規格では復帰・改行「\r\n」)に違いないので、置換処理において正規表現の「m」オプションによる行末の言明「$」に任せて、改行については、まったくノータッチであることがわかると思います。言い換えれば、行区切りである「\n」(規格では「\r\n」)は「マッチの範囲」の外側に常に存在して、適切に置換の対象外ということになります。改良版では行区切りと見なさない改行もあり得るので、さらに多少慎重な工夫が加わることになります。


♀ ⟺ ♂ 相互変換 / Mutual exchange

♀ 🏃‍♀️🏄‍♀️🏊‍♀️👮‍♀️👯‍♀️👰‍♀️👱‍♀️👳‍♀️👷‍♀️💁‍♀️💂‍♀️💆‍♀️💇‍♀️🙅‍♀️🙆‍♀️🙇‍♀️🙋‍♀️🙍‍♀️🙎‍♀️🚣‍♀️🚴‍♀️🚵‍♀️🚶‍♀️🤦‍♀️🤵‍♀️🤷‍♀️🤸‍♀️🤹‍♀️🤼‍♀️🤽‍♀️🤾‍♀️🦸‍♀️🦹‍♀️🧍‍♀️🧎‍♀️🧏‍♀️🧔‍♀️🧖‍♀️🧗‍♀️🧘‍♀️🧙‍♀️🧚‍♀️🧛‍♀️🧜‍♀️🧝‍♀️🧞‍♀️🧟‍♀️🙆‍♂️
♂ 🏃‍♂️🏄‍♂️🏊‍♂️👮‍♂️👯‍♂️👰‍♂️👱‍♂️👳‍♂️👷‍♂️💁‍♂️💂‍♂️💆‍♂️💇‍♂️🙅‍♂️🙆‍♂️🙇‍♂️🙋‍♂️🙍‍♂️🙎‍♂️🚣‍♂️🚴‍♂️🚵‍♂️🚶‍♂️🤦‍♂️🤵‍♂️🤷‍♂️🤸‍♂️🤹‍♂️🤼‍♂️🤽‍♂️🤾‍♂️🦸‍♂️🦹‍♂️🧍‍♂️🧎‍♂️🧏‍♂️🧔‍♂️🧖‍♂️🧗‍♂️🧘‍♂️🧙‍♂️🧚‍♂️🧛‍♂️🧜‍♂️🧝‍♂️🧞‍♂️🧟‍♂️🙆‍♀️
問題

絵文字の ♀ と ♂ を相互変換します。

ヒント

絵文字の多くはゼロ幅結合子 (\u200d) による合字なので、この例では ♀ 記号を ♂ 記号に置き換えるだけで女性から男性への変換が可能です。コード例はこれを実現する単純なものとなっています。

しかし、絵文字ではない ♀ 記号そのものも置き換わってしまいます。もしかすると、それは意図した挙動ではないかもしれません。

そして、♀ 記号と ♂ 記号を同時に入れ替えたい場合はどうでしょうか。テキストエリアの末尾に、実は、一人別姓の絵文字がいます(念の為「Renew ⬀」ボタン)。この性別も変えたいわけです。

解説と発展 〜 Unicode の基礎知識

上述のさまざまな条件を想定して、解決策を考えてみます。

絵文字の ♀ と ♂ を変換 … ♀ → ♂ のときに既存の ♂ はそのままという条件

記号と絵文字の変換を行うとき … 単純に置換(コード例のまま)
v.replace(/♀/g, '♂')
v.replace(/♂/g, '♀')
絵文字のみの変換を行うとき … ゼロ幅結合子 (\u200d) と一緒に変換
v.replace(/\u200d♀/g, '\u200d♂')
v.replace(/\u200d♂/g, '\u200d♀')
絵文字以外の変換を行うとき …
否定後読み言明を使用
v.replace(/(?<!\u200d)♀/g, '♂')
v.replace(/(?<!\u200d)♂/g, '♀')
否定後読み言明を不使用
v.replace(/(^|[^\u200d])♀/g, '$1♂')
v.replace(/(^|[^\u200d])♂/g, '$1♀')

絵文字の ♀ と ♂ を交換(変換と逆変換は等価となる)

記号と絵文字の交換を行うとき … 置換先関数を使う
v.replace(/[♀♂]/g, m=>m == '♀' ? '♂' : '♀')
絵文字のみの交換を行うとき … 置換先関数でゼロ幅結合子 (\u200d)と一緒に変換
v.replace(/\u200d([♀♂])/g, (m, p)=>p == '♀' ? '\u200d♂' : '\u200d♀')
絵文字以外の交換を行うとき … 置換先関数にて
否定後読み言明を使用
v.replace(/(?<!\u200d)[♀♂]/g, m=>m == '♀' ? '♂' : '♀')
否定後読み言明を不使用
v.replace(/(^|[^\u200d])([♀♂])/g, (m,p1,p2)=>p2 == '♀' ? `${p1}♂` : `${p1}♀`)

(SeaMonkey 対応などで) 否定の言明の使用を避けると、このように条件の否定のマッチをキャプチャして差し戻すかたちになり、少々婉曲的ですが実現不可能ではないことがわかるでしょう。


理解の一助として、コード例のうち「絵文字のみの交換」と「絵文字以外の交換」のフローチャートを記しておきます。特に後者は Unicode における処理の落とし穴として、「意図せず絵文字の性別が変わった」などという事故がありうるという筆者による問題提起でもあります。

本節の正規表現による置換のフローチャート
v.replace(/\u200d([♀♂])/g, (m, p)=>p == '♀' ? '\u200d♂' : '\u200d♀')

v.replace(/(?<!\u200d)[♀♂]/g, m=>m == '♀' ? '♂' : '♀')

一つ目のテキストエリアのテキストをその下のテキストフィールドの Javascript コード(入力文字列は v)で変換した文字列を、二つ目のテキストエリアに表示します。一方で、二つ目のテキストエリアのテキストをその下のテキストフィールドの Javascript コードで変換した文字列を、一つ目のテキストエリアに表示します。

The text in the first text area is converted by the Javascript code (input string is v) in the text field below it, and the string is displayed in the second text area. On the other hand, the string obtained by converting the text in the second text area with the Javascript code in the text field below it is displayed in the first text area.

正規表現チートシート

文字列の検索や置換を効率よく確実に行うためには「正規表現」が便利です。 以下、正規表現の任意のパターンx, y で表します。

正規表現リテラルの例
/\u[0-9A-F]{4}/i\u200b にマッチ、「i」はプロパティ・フラグ
正規表現オブジェクトの例
new RegExp("\\u[0-9A-Fa-f]{4}", "i") … 同上
主なプロパティ・フラグ flags
s
改行 ∈ すべての文字集合フラグ、dotAll プロパティ …「\n」が「.」にマッチ
i
大文字・小文字無視フラグ、ignoreCase プロパティ … Unicode の大文字か小文字を区別しない
u
Unicode 機能フラグ、unicode プロパティ … すべての Unicode の1文字が「.」にマッチ
g
グローバル・フラグ、global プロパティ … 正規表現のマッチから何回も検査(既定は一回)
y
スティッキー・フラグ、sticky プロパティ … 正規表現はバッファ先頭のみマッチ
m
複数行フラグ、multiline プロパティ … 言明「^」の前と「$」の次に「\n」がマッチ
文字集合
.(ピリオド)
任意の1文字(非 dotAll のとき、改行は含まない)
[-0-9A-Fa-f]
16 進法の数、加えて、マイナス記号
[^-0-9A-Fa-f]
上例以外の文字集合
[^-0-9A-Fa-f^]
上例、加えて「^」以外の文字集合
\d, \D
アラビア数字, それら以外
\s, \S
空白類, それら以外
\t, \n, \cA\cZ
タブ, 改行コード, 制御コード
\x00\xff
基本ラテン文字とラテン1補助の文字
\u0000\uffff
Unicode 基本多言語面 (BMP、第0面) の文字まで
\u{0000}\u{10ffff}
Unicode 基本多言語面〜私用面 (PUP、第15,16面)まですべて
基本的な言明
^(ハット)
文字列の先頭、または、行頭(multiline のとき)
$(ドル記号)
文字列の末尾、または、行末(multiline のとき)
最左最大マッチの量指定子
x*
0 以上の x の繰り返しの最左最大にマッチ
x+
1 以上の x の繰り返しの最左最大にマッチ
x?
1 以下の x の繰り返しの最左最大にマッチ
x{n}
nx の繰り返しの最左最大にマッチ
x{n,}
n 以上 の x の繰り返しの最左最大にマッチ
x{n,m}
n 以上 m 以下の x の繰り返しの最左最大にマッチ
最左最小マッチの量指定子
x*?
0 以上の x の繰り返しの最左最小にマッチ
x+?
1 以上の x の繰り返しの最左最小にマッチ
x{n,}?
n 以上 の x の繰り返しの最左最小にマッチ(他の量指定子についても同様)
キャプチャグループと後方参照の例
x|y
x または y にマッチ
(x|y)
同上をキャプチャ
(?:x|y)
同上を非キャプチャ … 後方参照が不要でキャプチャ番号を浪費しない
(x|y)\1
1番目のキャプチャに続いてマッチ
すすんだ言明
x(?=y)
肯定先読み言明
x(?!y)
否定先読み言明
(?<=x)y
肯定後読み言明
(?<!x)y
否定後読み言明
正規表現リテラルのエスケープ文字
^$.*+?/\()[]{} 」そのものはバックスラッシュ「\」でエスケープ
例: /\((株)\)/ … 「(株)」にマッチし「株」をキャプチャ
文字列リテラルのエスケープ文字
さらに、文字列で正規表現を表すとき、「\」は「\」でエスケープ
例: new RegExp("\\((株)\\)") … 同上
文字列の match 関数
string.match(/x/flags) はマッチの真偽を返す
文字列の replace 関数
string.replace(/x/flags, target) は正規表現と規則に基づいて置換文字列を返す
文字列の置換先 target の文字列形式
例:
"$1$2" … 1 番目と 2 番目のキャプチャグループへ置換
$n
n 番目のキャプチャ文字列を参照
$&
マッチ範囲の文字列を参照
$`
バッファ先頭からマッチ範囲先頭未満までの文字列を参照
$'
マッチ範囲末尾の次からバッファ末尾までの文字列を参照
$$
$」そのものを表す
文字列の置換先 target の関数形式
例:
(m, p1, p2)=>p1 + p2 … 1 番目と 2 番目のキャプチャグループへ置換
(m, p1, p2)=>`${p1}${p2}` … 同上
function (m, p1, p2) { return `${p1}${p2}`; } … 同上
参考文献
  1. the Mozilla Foundation, “正規表現,” mdn web docs, 2022.
  2. Taiji Yamada, “基本正規表現,” 2014.
  3. Taiji Yamada, “拡張正規表現,” 2014.
  4. Taiji Yamada, “テキストプロセッサとしての Python,” 2014.
  5. Taiji Yamada, “各種正規表現おける特徴,” 2014.

補遺:正規表現は、sed コマンド等の「基本正規表現」から Perl や POSIX 規格における「拡張正規表現」の歴史的な流れで追加された記法を眺めてみると、それらの需要が窺い知れて理解しやすいと思います。そして、モダンなプログラミング言語はおよそ「Perl の正規表現」をサポートしており、Javascript は機能過多や過負荷にならない程々のところの「Perl 互換正規表現のサブセット」といった趣きですので大変学習しやすいと思います。

Javascript の仕様で、他の実装 (Perl, PHP, etc.) では異なります。 SeaMonkey では後読み言明は未対応なので、少し苦労する場合があります。

Copyleft 🄯 2022 Taiji Yamada <taiji@aihara.co.jp>