81

正規表現置換を使用して数値をインクリメントすることは可能ですか? もちろん、評価/関数ベースの置換は使用しません。

この質問は、質問者がテキスト エディターで数値を増やしたいという別の質問に触発されました。完全なスクリプトをサポートするテキスト エディターよりも、正規表現の置換をサポートするテキスト エディターの方がおそらく多いため、正規表現が存在する場合は、あちこちに移動すると便利な場合があります。

また、巧妙な解決策から実際には役に立たない問題まで、きちんとしたことを学ぶことがよくあるので、興味があります。

非負の 10 進整数、つまり のみについて話していると仮定します\d+

  • 1回の交換で可能ですか?または、置換の有限数ですか?

  • そうでない場合、少なくとも9999 までの数値など、上限が与えられた可能性はありますか?

もちろん、while ループ (while マッチを代入) があれば実行可能ですが、ここではループのないソリューションを使用します。

4

6 に答える 6

48

この質問のトピックは、私が以前に行った特定の実装で私を楽しませました。私の解決策はたまたま2つの置換であるため、投稿します。

私の実装環境はsolarisです。完全な例:

echo "0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909" |
perl -pe 's/\b([0-9]+)\b/0$1~01234567890/g' |
perl -pe 's/\b0(?!9*~)|([0-9])(?=9*~[0-9]*?\1([0-9]))|~[0-9]*/$2/g'

1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910

説明のためにそれを引き離します:

s/\b([0-9]+)\b/0$1~01234567890/g

各番号 (#) を 0#~01234567890 に置き換えます。最初の 0 は、9 から 10 への丸めが必要な場合です。01234567890 ブロックはインクリメント用です。「9 10」のテキスト例は次のとおりです。

09~01234567890 010~01234567890

次の正規表現の個々の部分は個別に記述できます。それらはパイプを介して結合され、置換数を減らします。

s/\b0(?!9*~)/$2/g

丸める必要のないすべての数字の前にある「0」の数字を選択して破棄します。

s/([0-9])(?=9*~[0-9]*?\1([0-9]))/$2/g

(?=) は正の先読み、\1 は一致グループ #1 です。したがって、これは、'~' マークまで 9 が続くすべての数字を照合し、ルックアップ テーブルに移動して、この数字に続く数字を見つけることを意味します。ルックアップ テーブルの次の桁に置き換えます。したがって、正規表現エンジンが数値を解析すると、「09~」は「19~」、次に「10~」になります。

s/~[0-9]*/$2/g

この正規表現は ~ ルックアップ テーブルを削除します。

于 2015-07-23T23:27:58.970 に答える
47

うわー、それは可能であることがわかりました(醜いですが)!

時間がない場合や、説明全体をわざわざ読むことができない場合は、次のコードを使用してください。

$str = '0 1 2 3 4 5 6 7 8 9 10 11 12 13 19 20 29 99 100 139';
$str = preg_replace("/\d+/", "$0~", $str);
$str = preg_replace("/$/", "#123456789~0", $str);
do
{
$str = preg_replace(
    "/(?|0~(.*#.*(1))|1~(.*#.*(2))|2~(.*#.*(3))|3~(.*#.*(4))|4~(.*#.*(5))|5~(.*#.*(6))|6~(.*#.*(7))|7~(.*#.*(8))|8~(.*#.*(9))|9~(.*#.*(~0))|~(.*#.*(1)))/s",
    "$2$1",
    $str, -1, $count);
} while($count);
$str = preg_replace("/#123456789~0$/", "", $str);
echo $str;

それでは始めましょう。

したがって、まず、他の人が述べたように、ループしても(対応する増分を1桁に挿入する方法のため)、1回の置換では不可能です。ただし、最初に文字列を準備すると、ループできる単一の置換があります。これがPHPを使用した私のデモ実装です。

私はこのテスト文字列を使用しました:

$str = '0 1 2 3 4 5 6 7 8 9 10 11 12 13 19 20 29 99 100 139';

まず、マーカー文字を追加して、インクリメントするすべての数字にマークを付けましょう(私はを使用~しますが、ターゲット文字列では絶対に発生しないクレイジーなUnicode文字またはASCII文字シーケンスを使用する必要があります。

$str = preg_replace("/\d+/", "$0~", $str);

一度に(右から左に)数字ごとに1桁を置き換えるので、完全な数字ごとにそのマーキング文字を追加するだけです。

ここに主なハックがあります。文字列の最後に小さな「ルックアップ」を追加します(文字列には含まれない一意の文字で区切られます。簡単にするために使用しまし#た)。

$str = preg_replace("/$/", "#123456789~0", $str);

これを使用して、数字を対応する後継者に置き換えます。

ここでループが発生します。

do
{
$str = preg_replace(
    "/(?|0~(.*#.*(1))|1~(.*#.*(2))|2~(.*#.*(3))|3~(.*#.*(4))|4~(.*#.*(5))|5~(.*#.*(6))|6~(.*#.*(7))|7~(.*#.*(8))|8~(.*#.*(9))|9~(.*#.*(~0))|(?<!\d)~(.*#.*(1)))/s",
    "$2$1",
    $str, -1, $count);
} while($count);

さて、何が起こっているのですか?一致するパターンには、考えられるすべての桁に対して1つの選択肢があります。これにより、数字が後継者にマップされます。たとえば、最初の選択肢を考えてみましょう。

0~(.*#.*(1))

これは、0後に続く増分マーカー~と一致し、次にチート区切り文字と対応する後続文字までのすべてに一致します(これが、すべての数字をそこに配置する理由です)。置換を一瞥すると、これはに置き換えられます$2$1(これは、その後、元に戻すため1に一致したすべてのものになります~)。プロセスでを削除することに注意してください~0からへの数字をインクリメントする1だけで十分です。番号は正常にインクリメントされ、持ち越しはありません。

次の8つの選択肢は、の数字がまったく同じ1です8。次に、2つの特殊なケースを処理します。

9~(.*#.*(~0))

を置き換える場合9、増分マーカーを削除せず、0代わりに結果の左側に配置します。これ(周囲のループと組み合わせる)は、キャリーオーバー伝搬を実装するのに十分です。現在、1つの特別なケースが残っています。9sのみで構成されるすべての番号について~は、番号の前にが表示されます。それが最後の選択肢です。

(?<!\d)~(.*#.*(1))

~数字が前に付いていない(したがって、ネガティブルックビハインド)に遭遇した場合、それは数字全体に渡されている必要があるため、単に。に置き換え1ます。ネガティブな後ろ姿も必要ないと思いますが(これがチェックされる最後の選択肢であるため)、この方法の方が安全だと感じます。

(?|...)パターン全体の周りの短いメモ。$1これにより、$2(文字列の下のこれまでにない大きな数字の代わりに)同じ参照内で代替の2つの一致が常に見つかるようになります。

DOTALL最後に、修飾子( )を追加して、s改行を含む文字列でこれが機能するようにします(そうでない場合は、最後の行の数値のみがインクリメントされます)。

これにより、かなり単純な置換文字列になります。最初に(後継者と、場合によってはキャリーオーバーマーカーをキャプチャした)書き込み$2を行ってから、一致した他のすべてをで元の場所に戻し$1ます。

それでおしまい!文字列の最後からハックを削除するだけで、完了です。

$str = preg_replace("/#123456789~0$/", "", $str);
echo $str;
> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 20 21 30 100 101 140

したがって、これは完全に正規表現で行うことができます。そして、私たちが持っている唯一のループは常に同じ正規表現を使用しています。これは、を使用せずに取得できる限り近いと思いますpreg_replace_callback()

もちろん、文字列に小数点付きの数値がある場合、これは恐ろしいことをします。しかし、それはおそらく最初の準備-交換によって対処することができます。

更新:私は、このアプローチが(だけでなく)任意の増分にすぐに拡張されることに気づきました+1。最初の交換を変更するだけです。追加する数は~、すべての数に適用する増分と同じです。それで

$str = preg_replace("/\d+/", "$0~~~", $str);

文字列内のすべての整数を。ずつインクリメントします3

于 2012-10-17T20:15:25.027 に答える
12

私はそれを3回の置換で動作させることができました(ループなし)。

tl;dr

s/$/ ~0123456789/

s/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/$2$3$4$5/g

s/9(?=9*~)(?=.*(0))|~| ~0123456789$/$1/g

説明

テキストのどこにも表示されない~特殊文字にしましょう。

  1. 文字がテキストのどこにも見つからない場合、魔法のように表示する方法はありません。まず、気になる文字を最後に挿入します。

    s/$/ ~0123456789/
    

    例えば、

    0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909
    

    になります:

    0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909 ~0123456789
    
  2. 次に、各数値について、(1) 最後の non- をインクリメントし9(またはすべてがs の1場合は先頭に aを追加)、(2) 末尾の s の各グループを「マーク」します。99

    s/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/$2$3$4$5/g
    

    たとえば、この例は次のようになります。

    1 2 3 4 8 9 19~ 11 29~ 199~ 119~ 299~ 919~ 1999~ 1199~ 1919~ ~0123456789
    
  3. 最後に、(1) s の「マークされた」各グループを9s に置き換え0、(2) ~s を削除し、(3) 末尾の文字セットを削除します。

    s/9(?=9*~)(?=.*(0))|~| ~0123456789$/$1/g
    

    たとえば、この例は次のようになります。

    1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910
    

PHP の例

$str = '0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909';
echo $str . '<br/>';
$str = preg_replace('/$/', ' ~0123456789', $str);
echo $str . '<br/>';
$str = preg_replace('/(?=\d)(?:([0-8])(?=.*\1(\d)\d*$)|(?=.*(1)))(?:(9+)(?=.*(~))|)(?!\d)/', '$2$3$4$5', $str);
echo $str . '<br/>';
$str = preg_replace('/9(?=9*~)(?=.*(0))|~| ~0123456789$/', '$1', $str);
echo $str . '<br/>';

出力:

0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909
0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909 ~0123456789
1 2 3 4 8 9 19~ 11 29~ 199~ 119~ 299~ 919~ 1999~ 1199~ 1919~ ~0123456789
1 2 3 4 8 9 10 11 20 100 110 200 910 1000 1100 1910
于 2012-10-18T02:17:25.683 に答える
6

1回の交換で可能ですか?

いいえ。

そうでない場合、上限、たとえば 9999 までの数値を指定して、少なくとも単一の置換で可能ですか?

いいえ。

0 から 8 までの数字をそれぞれの後継者に置き換えることさえできません。この番号を照合してグループ化すると、次のようになります。

/([0-8])/

交換する必要があります。ただし、正規表現は数値ではなく文字列に対して機能します。したがって、「数字」(またはより良い:数字)をこの数字の2倍に置き換えることができますが、正規表現エンジンは、数値を保持する文字列を複製していることを知りません。

このように(ばかげた)何かをしたとしても:

/(0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)/

これにより、正規表現エンジンは、グループ 1 が一致した場合、数字'0'が一致しても置換できないことを「認識」します。'1'group 1 を digit 、 group'2'を digitなどに置き換えるように正規表現エンジンに指示することはできません'2'。確かに、PHP などの一部のツールでは、対応する置換文字列を使用していくつかの異なるパターンを定義できますが、そのような印象を受けます。あなたが考えていたものではありません。

于 2012-10-17T19:30:27.210 に答える
2

正規表現検索と置換だけではできません。

それを達成するには、何か他のものを使用する必要があります。数値をインクリメントするには、手元にあるプログラミング言語を使用する必要があります。

編集:

Single Unix Specificationの一部としての正規表現の定義では、算術式の評価をサポートする正規表現や、算術演算を実行する機能については言及されていません。


それにもかかわらず、いくつかのフレーバー(TextPad、Windows用エディター)では\i、検索文字列が見つかった回数の増分カウンターである置換用語として使用できますが、見つかった文字列を数値に評価または解析しませんまた、数値を追加することもできません。

于 2012-10-17T19:25:05.507 に答える