Pregunta

Supongamos que tengo las siguientes dos cadenas que contienen expresiones regulares. ¿Cómo los fusiono? Más específicamente, quiero tener las dos expresiones como alternativas.

$a = '# /[a-z] #i';
$b = '/ Moo /x';
$c = preg_magic_coalesce('|', $a, $b);
// Desired result should be equivalent to:
// '/ \/[a-zA-Z] |Moo/'

Por supuesto, hacer esto como operaciones de cadena no es práctico porque involucraría analizar las expresiones, construir árboles de sintaxis, fusionar los árboles y luego generar otra expresión regular equivalente al árbol. Estoy completamente feliz sin este último paso. Desafortunadamente, PHP no tiene una clase RegExp (¿o sí?).

¿Hay alguna forma de lograr esto? Por cierto, ¿alguna otra lengua ofrece alguna forma? ¿No es este un escenario bastante normal? Supongo que no. :-(

Alternativamente , hay una manera de verificar eficientemente si alguna de las dos expresiones coincide, y cuál coincide antes (y si coinciden en la misma posición, que partido es más largo)? Esto es lo que estoy haciendo en este momento. Desafortunadamente, hago esto en cadenas largas, muy a menudo, por más de dos patrones. El resultado es lento (y sí, este es definitivamente el cuello de botella).

EDITAR:

Debería haber sido más específico, lo siento. $ a y $ b son variables , ¡su contenido está fuera de mi control! De lo contrario, simplemente los uniría manualmente. Por lo tanto, no puedo hacer suposiciones sobre los delimitadores o modificadores de expresiones regulares utilizados. Tenga en cuenta, por ejemplo, que mi primera expresión usa el modificador i (ignorar la caja) mientras que la segunda usa x (sintaxis extendida). Por lo tanto, no puedo simplemente concatenar los dos porque la segunda expresión no ignora no y la primera no usa la sintaxis extendida (¡y cualquier espacio en blanco es significativo!

¿Fue útil?

Solución

Veo que porneL en realidad describió un montón de esto, pero esto Maneja la mayor parte del problema. Cancela los modificadores establecidos en las subexpresiones anteriores (que la otra respuesta omitió) y establece los modificadores como se especifica en cada subexpresión. También maneja delimitadores sin barra (no pude encontrar una especificación de qué caracteres están permitidos aquí, así que usé . , es posible que desee restringir aún más).

Una debilidad es que no maneja referencias inversas dentro de las expresiones. Mi mayor preocupación con eso son las limitaciones de las referencias anteriores. Dejaré eso como un ejercicio para el lector / interrogador.

// Pass as many expressions as you'd like
function preg_magic_coalesce() {
    $active_modifiers = array();

    $expression = '/(?:';
    $sub_expressions = array();
    foreach(func_get_args() as $arg) {
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1([eimsuxADJSUX]+)$/', $arg, $matches)) {
            $modifiers = preg_split('//', $matches[3]);
            if($modifiers[0] == '') {
                array_shift($modifiers);
            }
            if($modifiers[(count($modifiers) - 1)] == '') {
                array_pop($modifiers);
            }

            $cancel_modifiers = $active_modifiers;
            foreach($cancel_modifiers as $key => $modifier) {
                if(in_array($modifier, $modifiers)) {
                    unset($cancel_modifiers[$key]);
                }
            }
            $active_modifiers = $modifiers;
        } elseif(preg_match('/(.)(.*)\1$/', $arg)) {
            $cancel_modifiers = $active_modifiers;
            $active_modifiers = array();
        }

        // If expression has modifiers, include them in sub-expression
        $sub_modifier = '(?';
        $sub_modifier .= implode('', $active_modifiers);

        // Cancel modifiers from preceding sub-expression
        if(count($cancel_modifiers) > 0) {
            $sub_modifier .= '-' . implode('-', $cancel_modifiers);
        }

        $sub_modifier .= ')';

        $sub_expression = preg_replace('/^(.)(.*)\1[eimsuxADJSUX]*$/', $sub_modifier . '$2', $arg);

        // Properly escape slashes
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/';
    return $expression;
}

Editar: He reescrito esto (porque soy TOC) y terminé con:

function preg_magic_coalesce($expressions = array(), $global_modifier = '') {
    if(!preg_match('/^((?:-?[eimsuxADJSUX])+)$/', $global_modifier)) {
        $global_modifier = '';
    }

    $expression = '/(?:';
    $sub_expressions = array();
    foreach($expressions as $sub_expression) {
        $active_modifiers = array();
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1((?:-?[eimsuxADJSUX])+)$/', $sub_expression, $matches)) {
            $active_modifiers = preg_split('/(-?[eimsuxADJSUX])/',
                $matches[3], -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
        }

        // If expression has modifiers, include them in sub-expression
        if(count($active_modifiers) > 0) {
            $replacement = '(?';
            $replacement .= implode('', $active_modifiers);
            $replacement .= ':$2)';
        } else {
            $replacement = '$2';
        }

        $sub_expression = preg_replace('/^(.)(.*)\1(?:(?:-?[eimsuxADJSUX])*)$/',
            $replacement, $sub_expression);

        // Properly escape slashes if another delimiter was used
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/' . $global_modifier;
    return $expression;
}

Ahora usa (? modifiers: sub-expression) en lugar de (? modifiers) sub-expresión | (? cancel-modifiers) sub-expresión pero yo He notado que ambos tienen algunos efectos secundarios modificadores extraños. Por ejemplo, en ambos casos, si una sub-expresión tiene un modificador / u , no podrá coincidir (pero si pasa 'u' como el segundo argumento de la nueva función, que coincidirá con la multa).

Otros consejos

  1. Eliminar delimitadores y banderas de cada uno. Este regex debería hacerlo:

    /^(.)(.*)\1([imsxeADSUXJu]*)$/
    
  2. Unir expresiones. Necesitará paréntesis no capturadores para inyectar marcas:

    "(?$flags1:$regexp1)|(?$flags2:$regexp2)"
    
  3. Si hay referencias anteriores, cuente el paréntesis de captura y actualice las referencias anteriores en consecuencia (p. ej., /(.)x\1/ y / (.) y \ 1 / es /(.)x\1|(.)y\2/ ).

EDITAR

¡He reescrito el código! Ahora contiene los cambios que se enumeran a continuación. Además, he realizado pruebas exhaustivas (que no he publicado aquí porque son demasiadas) para buscar errores. Hasta ahora, no he encontrado ninguno.

  • La función ahora se divide en dos partes: existe & # 8217; una función separada preg_split que toma una expresión regular y devuelve una matriz que contiene la expresión simple (sin delimitadores) y una matriz de los modificadores. Esto podría ser útil (ya lo ha hecho, de hecho, es por eso que hice este cambio).

  • El código ahora maneja correctamente las referencias inversas. Después de todo, esto era necesario para mi propósito. No fue difícil de agregar, la expresión regular utilizada para capturar las referencias pasadas simplemente parece extraña (y en realidad puede ser extremadamente ineficiente, me parece NP-difícil para mí & # 8211; pero eso es solo una la intuición y sólo se aplica en casos de borde extraño). Por cierto, ¿alguien sabe una mejor manera de verificar un número desigual de coincidencias que mi forma de ver? Las miradas negativas no funcionarán aquí porque solo aceptan cadenas de longitud fija en lugar de expresiones regulares. Sin embargo, necesito la expresión regular aquí para probar si la barra diagonal inversa anterior se ha escapado realmente.

    Además, no sé qué tan bueno es PHP en el uso anónimo de create_function . En lo que respecta al rendimiento, esta podría no ser la mejor solución, pero parece lo suficientemente buena.

  • He corregido un error en la comprobación de validez.

  • He eliminado la cancelación de modificadores obsoletos ya que mis pruebas muestran que no es necesario.

Por cierto, este código es uno de los componentes centrales de un resaltador de sintaxis para varios idiomas en los que estoy trabajando en PHP ya que no estoy satisfecho con las alternativas enumeradas en otra parte .

¡Gracias!

porneL , falta de párpados , ¡increíble trabajo! Muchas muchas gracias. En realidad me había rendido.

He construido sobre su solución y me gustaría compartirla aquí. No implementé referencias numéricas de re-numeración ya que esto no es relevante en mi caso (creo que & # 8230;). Sin embargo, quizás esto sea necesario más tarde.

Algunas preguntas & # 8230;

Una cosa, @eyelidlessness : ¿Por qué sientes la necesidad de cancelar modificadores antiguos? Por lo que veo, esto no es necesario ya que los modificadores solo se aplican localmente de todos modos. Ah sí, otra cosa. Su escape del delimitador parece demasiado complicado. ¿Te importa explicar por qué crees que esto es necesario? Creo que mi versión también debería funcionar pero podría estar muy equivocada.

También, he cambiado la firma de su función para que coincida con mis necesidades. También creo que mi versión es más útil en general. Una vez más, podría estar equivocado.

Por cierto, ahora deberías darte cuenta de la importancia de los nombres reales en SO. ;-) No puedo darte crédito real en el código. : - /

El Código

De todos modos, me gustaría compartir mi resultado hasta ahora porque no puedo creer que nadie más necesite algo así. El código parece para funcionar muy bien. Sin embargo, aún no se han realizado pruebas exhaustivas. ¡Por favor, comenta!

Y sin más demora & # 8230;

/**
 * Merges several regular expressions into one, using the indicated 'glue'.
 *
 * This function takes care of individual modifiers so it's safe to use
 * <em>different</em> modifiers on the individual expressions. The order of
 * sub-matches is preserved as well. Numbered back-references are adapted to
 * the new overall sub-match count. This means that it's safe to use numbered
 * back-refences in the individual expressions!
 * If {@link $names} is given, the individual expressions are captured in
 * named sub-matches using the contents of that array as names.
 * Matching pair-delimiters (e.g. <code>"{…}"</code>) are currently
 * <strong>not</strong> supported.
 *
 * The function assumes that all regular expressions are well-formed.
 * Behaviour is undefined if they aren't.
 *
 * This function was created after a {@link https://stackoverflow.com/questions/244959/
 * StackOverflow discussion}. Much of it was written or thought of by
 * “porneL” and “eyelidlessness”. Many thanks to both of them.
 *
 * @param string $glue  A string to insert between the individual expressions.
 *      This should usually be either the empty string, indicating
 *      concatenation, or the pipe (<code>|</code>), indicating alternation.
 *      Notice that this string might have to be escaped since it is treated
 *      like a normal character in a regular expression (i.e. <code>/</code>)
 *      will end the expression and result in an invalid output.
 * @param array $expressions    The expressions to merge. The expressions may
 *      have arbitrary different delimiters and modifiers.
 * @param array $names  Optional. This is either an empty array or an array of
 *      strings of the same length as {@link $expressions}. In that case,
 *      the strings of this array are used to create named sub-matches for the
 *      expressions.
 * @return string An string representing a regular expression equivalent to the
 *      merged expressions. Returns <code>FALSE</code> if an error occurred.
 */
function preg_merge($glue, array $expressions, array $names = array()) {
    // … then, a miracle occurs.

    // Sanity check …

    $use_names = ($names !== null and count($names) !== 0);

    if (
        $use_names and count($names) !== count($expressions) or
        !is_string($glue)
    )
        return false;

    $result = array();
    // For keeping track of the names for sub-matches.
    $names_count = 0;
    // For keeping track of *all* captures to re-adjust backreferences.
    $capture_count = 0;

    foreach ($expressions as $expression) {
        if ($use_names)
            $name = str_replace(' ', '_', $names[$names_count++]);

        // Get delimiters and modifiers:

        $stripped = preg_strip($expression);

        if ($stripped === false)
            return false;

        list($sub_expr, $modifiers) = $stripped;

        // Re-adjust backreferences:

        // We assume that the expression is correct and therefore don't check
        // for matching parentheses.

        $number_of_captures = preg_match_all('/\([^?]|\(\?[^:]/', $sub_expr, 

EDITAR

¡He reescrito el código! Ahora contiene los cambios que se enumeran a continuación. Además, he realizado pruebas exhaustivas (que no he publicado aquí porque son demasiadas) para buscar errores. Hasta ahora, no he encontrado ninguno.

  • La función ahora se divide en dos partes: existe & # 8217; una función separada preg_split que toma una expresión regular y devuelve una matriz que contiene la expresión simple (sin delimitadores) y una matriz de los modificadores. Esto podría ser útil (ya lo ha hecho, de hecho, es por eso que hice este cambio).

  • El código ahora maneja correctamente las referencias inversas. Después de todo, esto era necesario para mi propósito. No fue difícil de agregar, la expresión regular utilizada para capturar las referencias pasadas simplemente parece extraña (y en realidad puede ser extremadamente ineficiente, me parece NP-difícil para mí & # 8211; pero eso es solo una la intuición y sólo se aplica en casos de borde extraño). Por cierto, ¿alguien sabe una mejor manera de verificar un número desigual de coincidencias que mi forma de ver? Las miradas negativas no funcionarán aquí porque solo aceptan cadenas de longitud fija en lugar de expresiones regulares. Sin embargo, necesito la expresión regular aquí para probar si la barra diagonal inversa anterior se ha escapado realmente.

    Además, no sé qué tan bueno es PHP en el uso anónimo de create_function . En lo que respecta al rendimiento, esta podría no ser la mejor solución, pero parece lo suficientemente buena.

  • He corregido un error en la comprobación de validez.

  • He eliminado la cancelación de modificadores obsoletos ya que mis pruebas muestran que no es necesario.

Por cierto, este código es uno de los componentes centrales de un resaltador de sintaxis para varios idiomas en los que estoy trabajando en PHP ya que no estoy satisfecho con las alternativas enumeradas en otra parte .

¡Gracias!

porneL , falta de párpados , ¡increíble trabajo! Muchas muchas gracias. En realidad me había rendido.

He construido sobre su solución y me gustaría compartirla aquí. No implementé referencias numéricas de re-numeración ya que esto no es relevante en mi caso (creo que & # 8230;). Sin embargo, quizás esto sea necesario más tarde.

Algunas preguntas & # 8230;

Una cosa, @eyelidlessness : ¿Por qué sientes la necesidad de cancelar modificadores antiguos? Por lo que veo, esto no es necesario ya que los modificadores solo se aplican localmente de todos modos. Ah sí, otra cosa. Su escape del delimitador parece demasiado complicado. ¿Te importa explicar por qué crees que esto es necesario? Creo que mi versión también debería funcionar pero podría estar muy equivocada.

También, he cambiado la firma de su función para que coincida con mis necesidades. También creo que mi versión es más útil en general. Una vez más, podría estar equivocado.

Por cierto, ahora deberías darte cuenta de la importancia de los nombres reales en SO. ;-) No puedo darte crédito real en el código. : - /

El Código

De todos modos, me gustaría compartir mi resultado hasta ahora porque no puedo creer que nadie más necesite algo así. El código parece para funcionar muy bien. Sin embargo, aún no se han realizado pruebas exhaustivas. ¡Por favor, comenta!

Y sin más demora & # 8230;

<*>

PD: He hecho que esta wiki de la comunidad de publicaciones sea editable. ¡Sabes lo que esto significa & # 8230 ;!

); if ($number_of_captures === false) return false; if ($number_of_captures > 0) { // NB: This looks NP-hard. Consider replacing. $backref_expr = '/ ( # Only match when not escaped: [^\\\\] # guarantee an even number of backslashes (\\\\*?)\\2 # (twice n, preceded by something else). ) \\\\ (\d) # Backslash followed by a digit. /x'; $sub_expr = preg_replace_callback( $backref_expr, create_function( '$m', 'return $m[1] . "\\\\" . ((int)$m[3] + ' . $capture_count . ');' ), $sub_expr ); $capture_count += $number_of_captures; } // Last, construct the new sub-match: $modifiers = implode('', $modifiers); $sub_modifiers = "(?$modifiers)"; if ($sub_modifiers === '(?)') $sub_modifiers = ''; $sub_name = $use_names ? "?<$name>" : '?:'; $new_expr = "($sub_name$sub_modifiers$sub_expr)"; $result[] = $new_expr; } return '/' . implode($glue, $result) . '/'; } /** * Strips a regular expression string off its delimiters and modifiers. * Additionally, normalize the delimiters (i.e. reformat the pattern so that * it could have used '/' as delimiter). * * @param string $expression The regular expression string to strip. * @return array An array whose first entry is the expression itself, the * second an array of delimiters. If the argument is not a valid regular * expression, returns <code>FALSE</code>. * */ function preg_strip($expression) { if (preg_match('/^(.)(.*)\\1([imsxeADSUXJu]*)$/s', $expression, $matches) !== 1) return false; $delim = $matches[1]; $sub_expr = $matches[2]; if ($delim !== '/') { // Replace occurrences by the escaped delimiter by its unescaped // version and escape new delimiter. $sub_expr = str_replace("\\$delim", $delim, $sub_expr); $sub_expr = str_replace('/', '\\/', $sub_expr); } $modifiers = $matches[3] === '' ? array() : str_split(trim($matches[3])); return array($sub_expr, $modifiers); }

PD: He hecho que esta wiki de la comunidad de publicaciones sea editable. ¡Sabes lo que esto significa & # 8230 ;!

Estoy bastante seguro de que no es posible juntar expresiones regulares de esa manera en ningún idioma, podrían tener modificadores incompatibles.

Probablemente los coloque en una matriz y los recorra, o los combine a mano.

Editar: si los está haciendo uno a la vez como se describe en su edición, es posible que pueda ejecutar la segunda en una subcadena (desde el inicio hasta la primera coincidencia). Eso podría ayudar a las cosas.

function preg_magic_coalasce($split, $re1, $re2) {
  $re1 = rtrim($re1, "\/#is");
  $re2 = ltrim($re2, "\/#");
  return $re1.$split.$re2;
}

Podrías hacerlo de la siguiente manera:

$a = '# /[a-z] #i';
$b = '/ Moo /x';

$a_matched = preg_match($a, $text, $a_matches);
$b_matched = preg_match($b, $text, $b_matches);

if ($a_matched && $b_matched) {
    $a_pos = strpos($text, $a_matches[1]);
    $b_pos = strpos($text, $b_matches[1]);

    if ($a_pos == $b_pos) {
        if (strlen($a_matches[1]) == strlen($b_matches[1])) {
            // $a and $b matched the exact same string
        } else if (strlen($a_matches[1]) > strlen($b_matches[1])) {
            // $a and $b started matching at the same spot but $a is longer
        } else {
            // $a and $b started matching at the same spot but $b is longer
        }
    } else if ($a_pos < $b_pos) {
        // $a matched first
    } else {
        // $b matched first
    }
} else if ($a_matched) {
    // $a matched, $b didn't
} else if ($b_matched) {
    // $b matched, $a didn't
} else {
    // neither one matched
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top