質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

3回答

789閲覧

javascript の正規表現

takahashi-one

総合スコア119

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

1グッド

2クリップ

投稿2020/04/29 06:35

編集2020/04/29 12:24

ネットに下記のような正規表現がありました。

javascript

1const price = "100000000"; 2console.log(price.replace(/(?!^)(?=(?:\d{3})+$)/g, ",")); 3//100,000,000 3桁ずつコンマを入れる

上記の正規表現を分解すると
1 (?!^) 否定先読み
2 (?=(?:\d{3})+$)

2をさらに分解すると
?= 肯定先読み
(?:\d{3}) 数字が3つ 記憶しない
+$ 1以上の繰り返しと末尾

あるサイトで
X(?=Y) はX の後にYが続けばXにマッチすると説明されていました。
これを(?!^)(?=(?:\d{3})+$)に当てはめると
(?!^)に(?=(?:\d{3})+$)が続けば(?!^)にマッチするとなります。

問題はここからです
X(?!Y) はXの後にYが続かなければXがマッチすると説明されていました。
これを(?!^)(?=(?:\d{3})+$)に当てはめると
Xの後に ^ が続かななければXがマッチするとなりますが、Xに当たる部分がありません。
(?!^)の対象は何になるのでしょうか?

回答ありがとうございます。
https://ja.javascript.info/regexp-lookahead-lookbehind
X(?=Y) はX の後にYが続けばXにマッチすると説明されていました。
この表現はサイトを読んで自分で勝手に解釈して書いたもので、みなさんの指摘だと誤解釈のようです。
https://ja.javascript.info/regexp-greedy-and-lazy
このサイトには左からマッチする文字を探していくと書いてありますが、右から探していくのでしょうか?
左からだとそこが右から数えて3桁ずつの場所かどうか解らないと思ったのですが。

kai0310👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/04/29 08:23

はてなブログのMarkdown記法で引用をする方法 - 前へ!前へ!! http://viriditas.hatenablog.com/entry/2015/08/16/031303 こちらも参考に。 「下記のような正規表現」と「X(?=Y) はX の後にYが続けばXにマッチすると説明」は引用対称だと思います。
Zuishin

2020/04/29 09:12

探していく順番はともかく最後に $ があるので右寄せになります。
Zuishin

2020/04/29 09:40 編集

少し具体的に言うと、左からの入力として探索を行い、全ての可能性の中から最長一致するものを最終的な結果として選択しますが、具体的なアルゴリズムは実装依存なので、左から評価するかどうかは決まっていません。バックトラックを行うものも多いと思いますし、構文によっては DFA を使うかもしれません。最後は行末で終わらなければならないので、人間が考える時には行末から三文字ずつ選んだと見る方が理解が容易です。
Zuishin

2020/04/29 12:39

使われた構文によってはバックトラックが必要ない(たとえば a は a 一文字にマッチするのでワンパスで確定する)ので、そのような場合に高速化のために部分的に他の方法を選択することもあるかもしれないと思いましたが、そのような実装が実際にあるかどうかは知りません。ただバックトラックの実装が義務付けられているということなら、ECMAScript の実装においてはバックトラックがあることを前提に考えた方が良さそうですね。 バックトラックするということは、入力は左からであっても右を評価してからそれを破棄して左に戻るということがあるので、右詰めの動きを想像するのもそこまで難しくないかと思います。
Zuishin

2020/04/29 16:20 編集

バックトラックというのは、要するにやり直しのことです。 たとえば 100000 の行頭にはカンマを置けないので、2 番目からカンマを置いていきます 1,000,00 すると、最後が二けたになって破綻します。 次に 3 番目からカンマを置いていきます。10,000,0 これも失敗しました。 もう一度 4 番目からカンマを置いていきます。100,000 今度はうまくいったので、これを出力します。 こういうのがバックトラックです。 実際にはもっと複雑ですが、あえて簡単に書きました。 だから左から探していくというのは間違いではありません。 しかし一番右に行った時に破綻したら前に戻るので、探索の順番としては必ずしも左を確定してからというわけではありません。一番右が決まることで初めて左のうちの一つが一つに決まることもあります。そして今回の場合、最終的には右から探したものと同じ出力になります。 そして、複雑な正規表現だと、正直に何度もバックトラックを行っては時間がかかるので、それを解消するために色々な工夫が凝らされているはずです。力づくで行えば分単位の時間がかかりそうな探索がわずかな時間で終わります。そのための工夫はそれぞれの正規表現エンジンなどに任されているため、実装依存と言いました。その場合、このような場面で本当は右から確定していることもあるかもしれません。 DFA というのは、バックトラックを行わず、時間のかわりにメモリを使って様々な可能性を短時間で計算する技術の一つです。DFA を使った場合、一つの入力に対して状態が一つに決まります。その状態一つ一つの中では、しばしば複数の可能性が同時に存在しています。または、一つの可能性が形を変えていくつもの状態を作ります。それによって一つの入力に対する道が一本に決まるので、どうしてもバックトラックを行わなければならない処理以外を DFA に置き換えることでかなりの時間の短縮が期待できます。 要するに、「左から」というのは必ずしも内部の動作ではなく、仮想的なロジックの話であると私は認識しています。 ただこれはあくまで可能性の話であり、実際にどのような方法を使って高速化を図っているかは存じません。 まだ説明が足りませんか? 実のところ、最初の質問から考えて、説明が多すぎるのではないかと思っているのですがどうでしょう? $ を基準に、三文字固定の表現が続いているので、「右から数えて三つずつ」と素直に考えたほうが理解しやすくありませんか?
Zuishin

2020/04/29 16:21 編集

> X(?=Y) はX の後にYが続けばXにマッチすると説明されていました。 この説明は間違いとは思いません。実際、X の後に Y が続かなければマッチしないからであり、続くなら X にマッチするからです。 しかしもし、この場合の X と Y を文字と思うなら間違いかもしれません。 なぜなら今回の場合、X も Y も「文字」ではなく「位置」だからです。 (?!^) は行頭以外の位置です。私の最初の回答では、質問者さんが「(?! )」の意味がわかっているという前提のもと、「^」を行頭と説明しました。しかし短すぎて誤解を招いたのか、伝わらなかったようなので、現在では「(?!^)」を合わせて「行頭ではない位置」と説明しています。
takahashi-one

2020/04/30 00:58

解りやす説明ありがとうございます。十分な説明をもらいました。ありがとうございました。
Zuishin

2020/04/30 01:09

過去の質問もそうですが、最も役に立った回答をベストアンサーに選んでください。今のところ 6 つの質問で自己解決が 2 回あるだけで、ベストアンサーは一度も選ばれていません。
guest

回答3

0

先読み

X(?=Y) はX の後にYが続けばXにマッチすると説明されていました。

間違いではありませんが、正確でもありません。
先読みは文字と文字の境界にマッチする」と覚えてください。

String.prototype.split

String.prototype.split と先読みを併用すると、挙動を理解しやすいと思います。

JavaScript

1console.log('a,b,c'.split(/(?=,)/)); // ["a", ",b", ",c"]

/(?=,)/カンマ(,)が後続する文字間の境界にマッチしています。

(?!^)

(?!^)の対象は何になるのでしょうか?

まず、(?!^) は文字列の先端にカンマが挿入されないようにする為のものです。

JavaScript

1console.log('100000000'.replace(/(?=(?:\d{3})+$)/g, ',')); // ",100,000,000" 2console.log('100000000'.replace(/(?!^)(?=(?:\d{3})+$)/g, ',')); // "100,000,000"

^ は「文字列の先端」を表し、1文字目の一つ手間の境界にマッチします。
これを先読みしますので、「1文字目の一つ手間の境界(^)」から更に一つ手前の境界にマッチしているのでしょう。

下記理由でお勧めし難い正規表現ではありますが…。

  • 境界の手前に境界があるのは非論理的に感じます
  • ECMAScript 仕様は未確認です
  • 複数ブラウザで期待通りに動くか分かりません

別解

後読みを使うと、部分一致で置換する正規表現を実装できるようになります。

JavaScript

1console.log('100000000'.replace(/(?<=\d)(?=(?:\d{3})+(?!\d))/g, ',')); // "100,000,000" 2console.log('abc100000000def'.replace(/(?<=\d)(?=(?:\d{3})+(?!\d))/g, ',')); // "abc100,000,000def"

こちらは、(?<=\d) によって数字が前述することを保証するので、^ にまつわる不自然さは解消されています。
変数にキャプチャすることをいとわなければ、先読みだけでも同じことが可能です。

JavaScript

1console.log('100000000'.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,')); // "100,000,000" 2console.log('abc100000000def'.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,')); // "abc100,000,000def"

先読みは文字を消費しない

「先読み」の正規表現を説明する時、

  • 位置にマッチする
  • 文字と文字の間にマッチする

という表現が良く使われます。
手元の「詳説 正規表現 第三版」でも次の一文がありました。

2.3.5 先後読みによって数値にカンマを付け加える

...
先後読み構文は、テキストではなく、テキストの中の位置にマッチするという点で、「\b」などの単語境界メタ文字、「^」や「$」などのアンカーに似ている。

これはこれで納得できる説明ですが、次節の

テキストを "消費" しない先後読み

「消費」の概念が本質なのかもしれません。
正規表現を実装する立場で考えた場合、「^」を「1文字も消費していない状態」と定義する事が出来ます。
その考えで正規表現を読み解くと、

JavaScript

1/(?!^)/

上記正規表現は、「1文字も消費していない状態にはマッチせず、この検索で文字列を消費しない」という事になります。

※「先端を否定先読みする」という正規表現は先端(^)の手前があるかのようで不思議に感じられましたが、「消費」の概念を人間に分かりやすく説明する為に「位置」や「文字と文字の間」という説明が生まれた、と考えると、この挙動の辻褄が合うように思います。

Re: takahashi-one さん

投稿2020/04/29 07:36

編集2020/04/29 11:26
think49

総合スコア18166

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/04/29 08:04

個人的には、(?!^) は不自然に感じるので、回答に載せた「別解」を使います。
think49

2020/04/29 11:43 編集

「先読みは文字を消費しない」を追記しました。 /(?=c)/.exec('abc')は「2文字目と3文字目の間(位置)にマッチする」と解釈する事が出来ますが、「2文字消費して、3文字目を消費しない状態にマッチする」の解釈も成り立ちます。 「^」は「0文字消費した状態」、「$」は「文字列の長さ(length)に等しい文字数を消費した状態」と捉える事も出来ます。
takahashi-one

2020/04/30 01:00

詳しい説明ありがとうございました。
guest

0

ベストアンサー

行頭にカンマを入れないようにしています。

正規表現がマッチするのは文字だけではありません。例えば ^ は行頭、$ は行末にマッチします。「行頭の文字」ではなく、「行頭の位置」にマッチするのです。

例えば文字列 abc の ^ を 0 に置換することは、行頭に 0 を挿入するのと同義です。結果はもちろん 0abc になります。

(?!^) は「行頭でない位置」を表す正規表現です。

(?=(?:\d{3})+$) は、行末から数えて数字 3 つ毎の先頭にマッチします。そこにカンマを挿入するわけですが、ただし行頭を除くというのが (?!^) の意味です。

例えば 100000000 が対象のとき、その記述が無いと次のようになることを確かめてください。

,100,000,000

なお、コメントに書いたように、正規表現を使わない標準的な方法が存在します。正規表現はパフォーマンスが悪く、車輪の再発明にもなるので、そちらをお勧めします。

追記

^ は行頭にマッチする正規表現で、複数行フラグを使った場合、改行の後の位置にもマッチします。$ も同様に改行の前にマッチします。

正規表現 - JavaScript | MDN

複数行モードを使用するには、次のように m フラグを入れます。そうしない場合、これらは文字列の先頭と末尾のみにマッチします。

JavaScript

1const price = "1000000\n10000\n100"; 2console.log(price.replace(/(?!^)(?=(?:\d{3})+$)/mg, ","));

この結果は次のようになります。

1,000,000 10,000 100

その他の言語では、複数行モードでも文字列の先頭と末尾にマッチする \A と \z が使えるものもありますが、JavaScript では使えないようです。

投稿2020/04/29 06:54

編集2020/04/29 11:47
Zuishin

総合スコア28662

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2020/04/29 08:20 編集

本文に転記
Zuishin

2020/04/29 07:21

また、自分が書いていないコードを載せる時は、必ず入手先を併記してください。
think49

2020/04/29 10:54

> (?!^) は「行頭でない位置」を表す正規表現です。 ^が行頭にマッチするには、mフラグを付与する必要があります。 本題の /(?!^)(?=(?:\d{3})+$)/g にはmフラグがない為、行頭にはマッチしないと思われます。 '1234\n5678'.match(/(?!^)(?=(?:\d{3})+$)/g); //  [""] '1234\n5678'.match(/(?!^)(?=(?:\d{3})+$)/gm); //  ["", ""]
Zuishin

2020/04/29 11:03

全部で一行と認識しています。改行は入っていないはずです。
think49

2020/04/29 11:29

質問に書かれた文字列リテラルは一行でしたが、この回答を読んで「^」を複数行の文字列にマッチする為に使用する人が現れないとも限らないので、補足をしました。
Zuishin

2020/04/29 11:35

ありがとうございます。追記します。
takahashi-one

2020/04/30 01:01

説明ありがとうございました。
guest

0

Xに相当するのは、空文字列です。

#追記
/(?!^)(?=(?:\d{3})+$)/は、否定先読み、肯定先読み以外の正規表現を含んでいないので、空文字列にマッチします。
どんな空文字列かというと、
・先頭でない
・その後ろに数字3桁の繰り返しが末尾まで続く
という空文字列です。

文字列に含まれる「空文字列」というのは、
・文字列の先頭
・文字列の末尾
・文字と文字の間
のどれかです。
例えば、"abc".replace(//,",")は、",a,b,c,"になります。

今回は、先頭でも末尾でもないので、文字と文字の間の事ですね。

投稿2020/04/29 06:48

編集2020/04/29 08:06
otn

総合スコア84713

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

takahashi-one

2020/04/30 01:01

説明ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問