Wow, il s'avère que c'est possible (même si c'est moche) !
Si vous n'avez pas le temps ou l'envie de lire toute l'explication, voici le code qui permet de le faire :
$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;
Commençons maintenant.
Donc, tout d'abord, comme les autres l'ont mentionné, ce n'est pas possible en un seul remplacement, même si vous le bouclez (parce que comment inséreriez-vous l'incrément correspondant à un seul chiffre). Mais si vous préparez d'abord la chaîne, il y a un remplacement unique qui peut être bouclé. Voici ma démonstration en 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 marquage (j'utilise ~
mais vous devriez probablement utiliser un caractère Unicode ou une séquence de caractères ASCII qui n'apparaîtra certainement pas dans votre chaîne cible.
$str = preg_replace("/\d+/", "$0~", $str);
Comme nous remplacerons un chiffre par numéro à la fois (de droite à gauche), nous ajouterons simplement le caractère de marquage après chaque numéro complet.
C'est là qu'intervient la principale difficulté. Nous ajoutons un petit "lookup" à la fin de notre chaîne (également délimitée par un caractère unique qui n'apparaît pas dans votre chaîne ; pour plus de simplicité, j'ai utilisé #
).
$str = preg_replace("/$/", "#123456789~0", $str);
Nous l'utiliserons pour remplacer les chiffres par leurs successeurs correspondants.
C'est maintenant qu'intervient 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);
D'accord, que se passe-t-il ? Le modèle de correspondance a une alternative pour chaque chiffre possible. Les chiffres sont ainsi associés à des successeurs. Prenons par exemple la première alternative :
0~(.*#.*(1))
Cela correspondra à n'importe quel 0
suivi de notre marqueur d'incrémentation ~
puis il fait correspondre le tout à notre délimiteur de triche et au successeur correspondant (c'est la raison pour laquelle nous avons placé chaque chiffre ici). 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 fait correspondre après le ~
pour le remettre en place). Notez que nous laissons tomber le ~
dans le processus. Incrémenter un chiffre de 0
a 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
. Nous nous occupons ensuite de deux cas particuliers.
9~(.*#.*(~0))
Lorsque nous remplaçons le 9
Nous ne laissons pas tomber le marqueur d'incrémentation, mais nous le plaçons à gauche du marqueur d'incrémentation résultant. 0
au lieu de cela. 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 nombres constitués uniquement de 9
nous nous retrouverons avec le ~
devant le numéro. C'est à cela que sert la dernière alternative :
(?<!\d)~(.*#.*(1))
Si nous rencontrons un ~
qui n'est pas précédé d'un chiffre (d'où le lookbehind négatif), il doit avoir été porté tout au long d'un nombre, et nous le remplaçons donc simplement par un 1
. Je pense que nous n'avons même pas besoin du lookbehind négatif (parce que c'est la dernière alternative qui est cochée), mais c'est plus sûr ainsi.
Une brève note sur la (?|...)
autour de l'ensemble du modèle. Cela permet de s'assurer que nous trouvons toujours les deux correspondances d'une alternative dans les mêmes références $1
y $2
(au lieu de chiffres de plus en plus grands dans la chaîne).
Enfin, nous ajoutons le DOTALL
( s
), pour que cela fonctionne avec les chaînes de caractères qui contiennent des sauts de ligne (sinon, seuls les nombres de la dernière ligne seront incrémentés).
Il s'agit donc d'une chaîne de remplacement assez simple. Il suffit d'écrire d'abord $2
(dans lequel nous capturons le successeur, et éventuellement le marqueur de report), puis nous remettons en place tout ce que nous avons apparié avec $1
.
C'est tout ! Il ne nous reste plus qu'à retirer notre hack de la fin de la chaîne, et nous aurons 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 à l'aide d'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 le plus proche que nous puissions obtenir sans utiliser de preg_replace_callback()
.
Bien sûr, cela aura des conséquences horribles si nous avons des nombres avec des points décimaux dans notre chaîne. Mais cela pourrait probablement être pris en charge par la toute première préparation-remplacement.
Mise à jour : Je viens de réaliser que cette approche s'étend immédiatement à des incréments arbitraires (et pas seulement aux +1
). Il suffit de modifier le premier remplacement. Le nombre de ~
que vous ajoutez est égal à l'incrément que vous appliquez à tous les nombres. Ainsi, le
$str = preg_replace("/\d+/", "$0~~~", $str);
incrémenterait chaque entier de la chaîne par 3
.