web-dev-qa-db-fra.com

Est-il possible d'incrémenter des nombres en utilisant la substitution de regex?

Est-il possible d'incrémenter des nombres en utilisant la substitution de regex? Ne pas utiliser substitution évaluée/basée sur la fonction , bien sûr.

Cette question a été inspirée par une autre, où le demandeur voulait incrémenter des nombres dans un éditeur de texte . Il existe probablement plus d'éditeurs de texte prenant en charge la substitution de regex que d'éditeurs prenant en charge les scripts complets; il est donc pratique qu'une expression rationnelle soit flottante, le cas échéant.

De plus, souvent, j'ai appris des solutions intelligentes en résolvant des problèmes pratiquement inutiles, alors je suis curieux.

Supposons que nous ne parlons que d’entiers décimaux non négatifs, c’est-à-dire \d+.

  • Est-ce possible en une seule substitution? Ou un nombre fini de substitutions?

  • Sinon, est-il au moins possible étant donné une limite supérieure, par ex. numéros jusqu'à 9999?

Bien sûr, c'est faisable étant donné une boucle while (remplacer par apparié), mais nous visons ici une solution sans boucle.

51
Andrew Cheong

Le sujet de cette question m'a amusé pour une implémentation particulière que j'ai faite plus tôt. Ma solution se trouve être deux substitutions alors je vais la poster.

Mon environnement de mise en œuvre est Solaris, exemple complet:

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

Le séparant pour l'explication:

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

Pour chaque numéro (#), remplacez-le par 0 # ~ 01234567890. Le premier 0 est nécessaire au cas où il faudrait arrondir de 9 à 10. Le bloc 01234567890 sert à l’incrémentation. Le texte d'exemple pour "9 10" est:

09~01234567890 010~01234567890

Les morceaux individuels de la prochaine expression rationnelle peuvent être décrits séparément, ils sont joints via des tuyaux pour réduire le nombre de substitutions:

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

Sélectionnez le chiffre "0" devant tous les chiffres qui n'ont pas besoin d'être arrondis et supprimez-le.

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

(? =) est positif,\1 est le groupe de correspondance n ° 1. Cela signifie donc de faire correspondre tous les chiffres suivis de 9 jusqu'à la marque '~', puis d'aller à la table de correspondance et de rechercher le chiffre suivant ce nombre. Remplacez par le chiffre suivant dans la table de recherche. Ainsi, "09 ~" devient "19 ~" puis "10 ~" au fur et à mesure que le moteur des expressions rationnelles analyse le nombre.

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

Cette expression rationnelle supprime la ~ table de consultation.

34
user1361991

Wow, il s'avère que c'est possible (bien que moche)!

Si vous n'avez pas le temps ou si vous ne voulez pas être dérangé pour lire toute l'explication, voici le code qui le fait:

$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;

Maintenant commençons.

Donc tout d’abord, comme d’autres l'ont mentionné, ce n'est pas possible avec un seul remplacement, même si vous le faites en boucle (car comment insérer l'insertion correspondante dans un seul chiffre). Mais si vous préparez la chaîne en premier, il y a un seul remplacement qui peut être mis en boucle. Voici ma mise en œuvre de démonstration utilisant PHP.

J'ai utilisé cette chaîne de test:

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

Tout d’abord, marquons tous les chiffres que nous voulons incrémenter en ajoutant un caractère de marqueur (j’utilise ~, mais vous devriez probablement utiliser un caractère Unicode ou une séquence de caractères ASCII fous qui ne se produira certainement pas dans votre chaîne cible. 

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

Comme nous allons remplacer un chiffre par numéro à la fois (de droite à gauche), nous ajouterons simplement ce caractère de marquage après chaque numéro complet.

Maintenant, voici le hack principal. Nous ajoutons un peu de 'recherche' à la fin de notre chaîne (également délimitée par un caractère unique qui ne figure pas dans votre chaîne; pour des raisons de simplicité, j'ai utilisé #).

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

Nous allons utiliser cela pour remplacer les chiffres par leurs successeurs correspondants.

Maintenant vient la boucle:

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);

Ok, qu'est-ce qui se passe? Le modèle correspondant a une alternative pour chaque chiffre possible. Ceci associe les chiffres aux successeurs. Prenons la première alternative par exemple:

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

Cela correspond à tout 0 suivi de notre marqueur d'incrémentation ~, puis à tout jusqu'à notre délimiteur de triche et son successeur (c'est pourquoi nous y mettons chaque chiffre). Si vous jetez un coup d'œil au remplacement, celui-ci sera remplacé par $2$1 (qui sera alors 1 et ensuite tout ce que nous avons mis en correspondance après le ~ pour le remettre en place). Notez que nous abandonnons le ~ dans le processus. Incrémenter un chiffre de 0 à 1 suffit. Le nombre a été incrémenté avec succès, il n'y a pas de report.

Les 8 alternatives suivantes sont exactement les mêmes pour les chiffres 1 à 8. Ensuite, nous nous occupons de deux cas particuliers.

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

Lorsque nous remplaçons le 9, nous n'abandonnons pas le marqueur d'incrément, mais le plaçons à la place du 0 résultant. Ceci (combiné avec la boucle environnante) est suffisant pour mettre en œuvre la propagation par report. Il reste maintenant un cas particulier. Pour tous les numéros composés uniquement de 9s, nous nous retrouverons avec le ~ devant le numéro. C'est ce que la dernière alternative est pour:

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

Si nous rencontrons un ~ qui n'est pas précédé d'un chiffre (donc du lookbehind négatif), il doit avoir été porté à travers un nombre et nous le remplaçons simplement par un 1. Je pense que nous n’avons même pas besoin de la surveillance négative (car c’est la dernière option vérifiée), mais nous nous sentons plus en sécurité de cette façon.

Une petite note sur le (?|...) tout autour du motif. Cela garantit que nous trouvons toujours les deux correspondances d'une alternative dans les mêmes références $1 et $2 (au lieu de nombres toujours plus grands dans la chaîne).

Enfin, nous ajoutons le modificateur DOTALL (s) pour que cela fonctionne avec des chaînes contenant des sauts de ligne (sinon, seuls les nombres de la dernière ligne seront incrémentés).

Cela fait une chaîne de remplacement assez simple. Nous écrivons simplement d'abord $2 (dans lequel nous avons capturé le successeur, et éventuellement le marqueur de report), puis nous remettons tout ce que nous avons mis en correspondance avec $1.

C'est tout! Nous avons juste besoin de retirer notre hack de la fin de la chaîne, et nous avons terminé:

$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

Nous pouvons donc le faire entièrement avec des expressions régulières. Et la seule boucle que nous avons utilise toujours la même expression régulière. Je pense que c’est aussi proche que possible sans utiliser preg_replace_callback().

Bien sûr, cela fera des choses horribles si nous avons des nombres avec des points décimaux dans notre chaîne. Mais cela pourrait probablement être réglé dès le premier remplacement-préparation.

Mise à jour: Je viens de me rendre compte que cette approche s'étend immédiatement aux incréments arbitraires (pas seulement +1). Il suffit de changer le premier remplacement. Le nombre de ~ que vous ajoutez est égal à l'incrément appliqué à tous les nombres. Alors

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

incrémenterait chaque entier de la chaîne de 3.

40
Martin Ender

J'ai réussi à le faire fonctionner en 3 substitutions (pas de boucles).

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

Explication

Soit ~ un caractère spécial non censé apparaître n'importe où dans le texte.

  1. Si un personnage ne figure nulle part dans le texte, il n'y a aucun moyen de le faire apparaître comme par magie. Nous commençons donc par insérer les caractères qui nous intéressent à la toute fin.

    s/$/ ~0123456789/
    

    Par exemple ( cliquez ici pour refiddle ),

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

    devient:

    0 1 2 3 7 8 9 10 19 99 109 199 909 999 1099 1909 ~0123456789
    
  2. Ensuite, pour chaque nombre, nous (1) incrémentons le dernier non -9 (ou ajoutons un 1 si tout sont 9s), et (2) " marquer "chaque groupe final de 9s.

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

    Par exemple, ( cliquez ici pour refiddle ), notre exemple devient:

    1 2 3 4 8 9 19~ 11 29~ 199~ 119~ 299~ 919~ 1999~ 1199~ 1919~ ~0123456789
    
  3. Enfin, nous (1) remplaçons chaque groupe "marqué" de 9s par 0s, (2) supprimons le ~s et (3) supprimons le jeu de caractères à la fin.

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

    Par exemple, ( cliquez ici pour refiddle ), notre exemple devient:

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

PHP Exemple

Vous pouvez copier et coller ceci dans http://www.writecodeonline.com/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/>';

Sortie:

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
10
Andrew Cheong

Est-ce possible en une seule substitution?

Non.

Si ce n'est pas le cas, est-il au moins possible avec une seule substitution étant donné une limite supérieure, par ex. numéros jusqu'à 9999?

Non.

Vous ne pouvez même pas remplacer les nombres compris entre 0 et 8 par leur successeur respectif. Une fois que vous avez identifié et groupé ce numéro:

/([0-8])/

vous devez le remplacer. Cependant, l'expression rationnelle ne fonctionne pas sur les nombres, mais sur les chaînes. Vous pouvez donc remplacer le "nombre" (ou mieux: chiffre) par deux fois ce chiffre, mais le moteur des expressions rationnelles ne sait pas qu'il duplique une chaîne contenant une valeur numérique.

Même si vous voulez faire quelque chose (idiot) comme ceci:

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

de sorte que le moteur des expressions rationnelles "sache" que si le groupe 1 est mis en correspondance, le chiffre '0' est mis en correspondance, il ne peut toujours pas effectuer de remplacement. Vous ne pouvez pas demander au moteur des expressions rationnelles de remplacer le groupe 1 par le chiffre '1', le groupe '2' par le chiffre '2', etc. Bien sûr, certains outils tels que PHP vous permettront de définir deux modèles différents avec les chaînes de remplacement correspondantes. mais j'ai l'impression que ce n'est pas ce à quoi vous pensiez.

5
Bart Kiers

Ce n'est pas possible uniquement par la recherche par expression régulière et la substitution.

Vous devez utiliser quelque chose d'autre pour atteindre cet objectif. Vous devez utiliser le langage de programmation disponible pour incrémenter le nombre.

Modifier:

La définition des expressions régulières, dans le cadre de Spécification Unix unique, ne mentionne pas les expressions régulières prenant en charge l'évaluation d'expressions arithmétiques ou les capacités permettant d'effectuer des opérations arithmétiques.


Néanmoins, je sais que certaines variantes (TextPad, éditeur de Windows) vous permettent d’utiliser \i comme terme de substitution, qui est un compteur incrémental du nombre de fois où la chaîne de recherche a été trouvée, mais elle n’évalue ni ne analyse les chaînes trouvées dans un numéro ne permet pas non plus d’ajouter un numéro.

0
Tulains Córdova

J'avais besoin d'incrémenter les index des fichiers de sortie d'un pipeline à partir d'un pipeline que je ne peux pas modifier. Après quelques recherches, j'ai eu un coup sur cette page. Bien que les lectures aient un sens, elles ne donnent vraiment pas une solution lisible au problème. Oui, il est possible de le faire avec seulement regex; non ce n'est pas aussi compréhensible.

Ici, je voudrais donner une solution lisible en utilisant Python, afin que d’autres ne soient pas obligés de réinventer les roues. J'imagine que beaucoup d'entre vous ont pu se retrouver avec une solution similaire.

L'idée est de partitionner le nom de fichier en trois groupes et de formater votre chaîne de correspondance de sorte que l'index incrémenté soit le groupe du milieu. Ensuite, il est possible d’incrémenter uniquement le groupe intermédiaire, après quoi nous réunissons à nouveau les trois groupes.

import re
import sys
import argparse
from os import listdir
from os.path import isfile, join



def main():
    parser = argparse.ArgumentParser(description='index shift of input')
    parser.add_argument('-r', '--regex', type=str,
            help='regex match string for the index to be shift')
    parser.add_argument('-i', '--indir', type=str,
            help='input directory')
    parser.add_argument('-o', '--outdir', type=str,
            help='output directory')

    args = parser.parse_args()
    # parse input regex string
    regex_str = args.regex
    regex = re.compile(regex_str)
    # target directories
    indir = args.indir
    outdir = args.outdir

    try:
        for input_fname in listdir(indir):
            input_fpath = join(indir, input_fname)
            if not isfile(input_fpath): # not a file
                continue

            matched = regex.match(input_fname)
            if matched is None: # not our target file
                continue
            # middle group is the index and we increment it
            index = int(matched.group(2)) + 1
            # reconstruct output
            output_fname = '{prev}{index}{after}'.format(**{
                'prev'  : matched.group(1),
                'index' : str(index),
                'after' : matched.group(3)
            })
            output_fpath = join(outdir, output_fname)

            # write the command required to stdout
            print('mv {i} {o}'.format(i=input_fpath, o=output_fpath))
    except BrokenPipeError:
        pass



if __== '__main__': main()

J'ai ce script nommé index_shift.py. Pour donner un exemple d'utilisation, mes fichiers sont nommés k0_run0.csv, pour les exécutions d'amorçage de modèles d'apprentissage machine à l'aide du paramètre k. Le paramètre k commence à zéro et la mappe d'index souhaitée commence à un. Nous préparons d’abord les répertoires d’entrée et de sortie pour éviter de surcharger les fichiers

$ ls -1 test_in/ | head -n 5
k0_run0.csv
k0_run10.csv
k0_run11.csv
k0_run12.csv
k0_run13.csv
$ ls -1 test_out/

Pour voir comment fonctionne le script, imprimez simplement sa sortie:

$ python3 -u index_shift.py -r '(^k)(\d+?)(_run.+)' -i test_in -o test_out | head -n5
mv test_in/k6_run26.csv test_out/k7_run26.csv
mv test_in/k25_run11.csv test_out/k26_run11.csv
mv test_in/k7_run14.csv test_out/k8_run14.csv
mv test_in/k4_run25.csv test_out/k5_run25.csv
mv test_in/k1_run28.csv test_out/k2_run28.csv

Il génère la commande bash mv pour renommer les fichiers. Maintenant, nous dirigeons les lignes directement dans bash.

$ python3 -u index_shift.py -r '(^k)(\d+?)(_run.+)' -i test_in -o test_out | bash

En vérifiant la sortie, nous avons réussi à déplacer l’index d’une unité.

$ ls test_out/k0_run0.csv
ls: cannot access 'test_out/k0_run0.csv': No such file or directory
$ ls test_out/k1_run0.csv
test_out/k1_run0.csv

Vous pouvez également utiliser cp au lieu de mv. Mes fichiers sont un peu volumineux, donc je voulais éviter de les dupliquer. Vous pouvez également modifier le nombre que vous modifiez en tant qu'argument d'entrée. Je ne me suis pas donné la peine, car le décalage par un est la plupart de mes cas d'utilisation.

0
PM Hui