'Moving parts of {token} to the end of line based on condition

From a multiline input I want to move tokens if they're inside {} and match some number. Sample input

# (811) (1485) [2756] {29} [555] {15} 
# (811) (1476) {20} {15} (1485) [196] [2441] 
# (911) (619) {19} (1476) [2765] [2752] {21}

From the above line, I want to move tokens if it's not {19} or {20} to the end of line.
Sample output

# (811) (1485) [2756] [555] {15} {29} 
# (811) (1476) {20} (1485) [196] [2441] {15} 
# (911) (619) {19} (1476) [2765] [2752] {21}

I can do a preg match with preg_match_all("/\{\d+\}/", $input, $matches); but then what to do?



Solution 1:[1]

You can gather all the matches for {19} and {20} per line in an array while filtering the splitted string, and then joining them back together.

Code example

foreach (explode("\n", $str) as $str) {
    $result = array_reduce(explode(" ", $str), function($acc, $curr) {
        preg_match("/{(?!19|20)\d+}/", $curr) ? $acc['move'][] = $curr :  $acc['valid'][] = $curr;
        return $acc;
    }, ['valid' => [], 'move' => []]);
    echo implode(" ", array_merge($result['valid'], array_reverse($result['move']))) . PHP_EOL;
}

Output

# (811) (1485) [2756] [555] {15} {29}
# (811) (1476) {20} (1485) [196] [2441] {15}
# (911) (619) {19} (1476) [2765] [2752] {21}

About the code

Te code first splits the string on newlines, because the moving parts are per line.

Then you can use for example explode to split the line on a space and use array_reduce to inspect the separate parts.

You can initialise array reduce with an array that contains 2 arrays ['valid' => [], 'move' => []]

In the callback function, the accumulator $acc then already contains that array, which you can then populate with the difference in matches by using the array key like $acc['valid']

The pattern {(?!19|20)\d+} matches { and then asserts that it is not directly followed by either 19} or 20} If that is the case, it matches 1 or more digits between curly braces.

To get a result with just single spaces between the "words" you can merge both arrays, and then use implode on a space.

See a php demo.

Solution 2:[2]

This solution allows for multiple token punctuations. In this example all tokens starting with '{' or '(' will be moved to the end:

$input = <<< STRING
# (811) (1485) [2756] {29} [555] {15}
# (811) (1476) {20} {15} (1485) [196] [2441]
# (911) (619) {19} (1476) [2765] [2752] {21}
STRING;

$excluded = [ '{19}', '{20}', '(811)' ];

$startPunctuations = array_unique(array_map(fn($exclude) => $exclude[0], $excluded));

$result = implode(
  "\n",
  array_map(
    fn($line): string => implode(
      ' ',
      array_map(
        fn($element) => implode(' ', $element),
        array_reduce(
          explode(' ', $line),
          fn($carry, $item) => in_array($item[0], $startPunctuations) && !in_array($item, $excluded)
            ? [ $carry[0], [ ...$carry[1], $item ] ]
            : [ [ ...$carry[0], $item ], $carry[1] ],
          [ [], [] ]
        )
      )
    ),
    explode("\n", $input)
  )
);

echo $result;

// # (811) [2756] [555] (1485) {29} {15}
// # (811) {20} [196] [2441] (1476) {15} (1485)
// # {19} [2765] [2752] (911) (619) (1476) {21}

Solution 3:[3]

You can do it using preg_replace_callback:

echo preg_replace_callback('~ {(?!(?:19|20)})(\d+)}(?! *$)| *$~m', function($m) {
    static $rep = '';
    $rep .= $m[0];
    if ( !isset($m[1]) && [$ret, $rep] = [$rep, ''] ) return $ret;
}, $str);

demo

The pattern has two branches:

  • the first one looks for each {token} that are not {19} or {20} and that are not already at the end of the line. This branch contains also a useless capture group (I putted it around \d+ but you can put it everywhere in the branch except inside the lookaheads, it can also be empty).
  • the second one looks for the end of the line (with eventual trailing spaces).

If the first branch succeeds, the capture group is defined, if the second branch succeeds it isn't.

Each time a match is found, its content is concatenated to the $rep static variable. But when the second branch succeeds, this accumulated content is returned and $rep is reinitialized to the empty string.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 lukas.j
Solution 3 Casimir et Hippolyte