初期状態では置換しません。それぞれ「⬃ 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.
このページは隅の「一覧」にある他のフィルタと異なり、Javascript の主に正規表現を使って「任意の変換」を行います。また、他のフィルタと同様に逆変換のための仕組みも備えておきました。
そして、このページが Javascript の正規表現の練習ツールとしても使えるように「何にもマッチしない正規表現/[^\s\S]/
」で拙作のコード例を隠しておきますので適宜「⬃ Code」ボタンを押してコード例を「コード入力テキスト欄」へ代入してください。
左側の「コード入力テキスト欄」で左のテキストエリアから右のテキストエリアへの変換、及び、右側の「コード入力テキスト欄」で右のテキストエリアから左のテキストエリアへの変換が実行できます。実行は、左右どちらかのテキストエリアの編集または「コード入力テキスト欄」編集の確定 (⏎) です。テキストエリアのサンプルテキストは自由に編集できますが、初期状態に戻すには「Renew ⬀」ボタンを押して下さい。
実行に際して、ブラウザの開発ツールのコンソールを表示し、問題解決に役立てるとよいかもしれません。
入出力例と同様な結果になるように、まずは変換のためのコードを書いてみてはいかがでしょうか。そして、逆変換にも挑戦してみることもお勧めします。ものによっては可逆であることが非常に困難もしくは不可能であることが身に染みると思います。私のコード例も教育的観点からあまり複雑にならないように適度なところで工夫は止めてありますので、往々にして商業上の実用には向いていませんのでご了承ください。
正規表現による文字列処理 string.replace(/正規表現/, target)
を主に扱います(よりよい方法があるなら、コード例ではそちらを採用しているかもしれません)。
また、すべての「コード入力テキスト欄」へ拙作のコード例をこのボタン で代入できます。 さらに、すべての「► 問題」をこのボタン で開閉できます。
ついでに、珍しいとは思いますが『「正規表現による置換」のフローチャート』を教育的配慮で添えました。すべてを で開閉できます。
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, '㈹')
「漢字(よみがな)」という文字列と 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)')
#FFFFFF
(16) 相互変換 / Mutual exchangeウェブ技術の CSS (Cascading Style Sheets) で RGB 色指定で用いられる2つの形式を相互変換します。
Javascript において、文字列 ⟺ 数値の相互変換は以下の通りです。
string
) から数値 (value
) への変換、及び、ゼロ詰めvalue = parseInt(string)
value = parseInt(string, 16)
string.padStart(2, '0')
value
) から文字列 (string
) への変換string = value.toString()
(ですが、まず不要)string = value.toString(16)
また、拙作のコード例で使用している文字列や配列におけるアルゴリズムを紹介しておきます。
string.split(/,\s*/)
/,\s*/
を区切りとして分割した配列を返すarray.join(',')
','
を区切りとして連結した文字列を返すstring.slice(i, 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
, #FFFFFF
⇒ rgb(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 (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')
† これまでのフローチャートの書き方よりも簡略化した書き方になっています。
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, '"')
日本語の文字集合において特有の「全角文字」と「半角文字」を数字についてのみ相互変換します。本来、ラテン・アルファベットの全角文字もありますが、方法は同じようになるでしょう。
「半角文字」から「全角文字」への変換は、変換テーブルを用意するしかなさそうですが、数字に関しては 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) 形式の相互変換をします。CSV には規格があり、列区切りのカンマ「,
」そのものを行列セルの内容として使いたいときは、ダブルクオーテーションマーク「"
」で括ります。そのため、ダブルクオーテーションマークそのものを使いたいときはダブルクーテーションマークを「""
」2つ重ねなければなりません(プログラミング言語 BASIC 由来)。
また、前述のように TSV 形式のタブ入出力において、区切り一つとなる連続するタブの数は幾つでも構わないものとします。
説明の通り、CSV 形式は多少厄介な代物です。一方、TSV 形式は広く使用されていますが、正式な規格などはないので、連続するタブ「\t+
(正規表現)」を一つの区切りとみなす等、情報交換同士の約束事はあり得ます。
やり方はいろいろあると思いますが、正規表現の言明をうまく活用しないと左欄は難しいかもしれません。
string.match
メソッドの利用コード例にて使用されている主な正規表現を解説します。ちなみに、以下「行列要素」とは表におけるセルの内容を指すものとします。
/(^|\t+)(.*?)(?=\t+|$)/mg
/^(.*[,"].*)$/
/(".*?(?:""|,).*?"|.*?)(,|$)/mg
/^"(.*)"$/
コード例では、これらと関数形式の置換先も駆使して変換・逆変換を実現しています。
さて、このコード例の欠点をあげるとしたら何があるでしょうか。以下に挙げてみましょう。
\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
」)は「マッチの範囲」の外側に常に存在して、適切に置換の対象外ということになります。改良版では行区切りと見なさない改行もあり得るので、さらに多少慎重な工夫が加わることになります。
絵文字の ♀ と ♂ を相互変換します。
絵文字の多くはゼロ幅結合子 (\u200d
) による合字なので、この例では ♀ 記号を ♂ 記号に置き換えるだけで女性から男性への変換が可能です。コード例はこれを実現する単純なものとなっています。
しかし、絵文字ではない ♀ 記号そのものも置き換わってしまいます。もしかすると、それは意図した挙動ではないかもしれません。
そして、♀ 記号と ♂ 記号を同時に入れ替えたい場合はどうでしょうか。テキストエリアの末尾に、実は、一人別姓の絵文字がいます(念の為「Renew ⬀」ボタン)。この性別も変えたいわけです。
上述のさまざまな条件を想定して、解決策を考えてみます。
絵文字の ♀ と ♂ を変換 … ♀ → ♂ のときに既存の ♂ はそのままという条件
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")
† … 同上s
dotAll
プロパティ …「\n
」が「.
」にマッチi
ignoreCase
プロパティ … Unicode の大文字か小文字を区別しないu
unicode
プロパティ … すべての Unicode の1文字が「.
」にマッチg
global
プロパティ … 正規表現のマッチから何回も検査(既定は一回)y
sticky
プロパティ … 正規表現はバッファ先頭のみマッチm
multiline
プロパティ … 言明「^
」の前と「$
」の次に「\n
」がマッチ.
(ピリオド)dotAll
のとき、改行は含まない)[-0-9A-Fa-f]
[^-0-9A-Fa-f]
[^-0-9A-Fa-f^]
\d
, \D
\s
, \S
\t
, \n
, \cA
…\cZ
\x00
…\xff
\u0000
…\uffff
\u{0000}
…\u{10ffff}
^
(ハット)multiline
のとき)$
(ドル記号)multiline
のとき)x*
x+
x?
x{n}
x{n,}
x{n,m}
x*?
x+?
x{n,}?
x|y
(x|y)
(?:x|y)
(x|y)\1
x(?=y)
x(?!y)
(?<=x)y
(?<!x)y
^$.*+?/\()[]{}
」そのものはバックスラッシュ「\」でエスケープ/\((株)\)/
… 「(株)」にマッチし「株」をキャプチャnew RegExp("\\((株)\\)")
… 同上match
関数†string.match(/x/flags)
はマッチの真偽を返すreplace
関数†string.replace(/x/flags, target)
は正規表現と規則に基づいて置換文字列を返す"$1$2"
… 1 番目と 2 番目のキャプチャグループへ置換$n
$&
$`
$'
$$
$
」そのものを表す(m, p1, p2)=>p1 + p2
… 1 番目と 2 番目のキャプチャグループへ置換(m, p1, p2)=>`${p1}${p2}`
… 同上function (m, p1, p2) { return `${p1}${p2}`; }
… 同上補遺:正規表現は、sed コマンド等の「基本正規表現」から Perl や POSIX 規格における「拡張正規表現」の歴史的な流れで追加された記法を眺めてみると、それらの需要が窺い知れて理解しやすいと思います。そして、モダンなプログラミング言語はおよそ「Perl の正規表現」をサポートしており、Javascript は機能過多や過負荷にならない程々のところの「Perl 互換正規表現のサブセット」といった趣きですので大変学習しやすいと思います。
† Javascript の仕様で、他の実装 (Perl, PHP, etc.) では異なります。 ‡ SeaMonkey では後読み言明は未対応なので、少し苦労する場合があります。