¿Cómo creo un flujo de coincidencias de expresiones regulares?

Estoy intentando analizar la entrada estándar y extraer cada cadena que coincida con un patrón específico, contar el número de apariciones de cada coincidencia e imprimir los resultados alfabéticamente. Este problema parece una buena coincidencia para la API de Streams, pero no puedo encontrar una forma concisa de crear una secuencia de coincidencias desde un Matcher.

Trabajé alrededor de este problema implementando un iterador sobre las coincidencias y envolviéndolo en un Stream, pero el resultado no es muy legible. ¿Cómo puedo crear una secuencia de coincidencias de expresiones regulares sin introducir clases adicionales?

public class PatternCounter { static private class MatcherIterator implements Iterator { private final Matcher matcher; public MatcherIterator(Matcher matcher) { this.matcher = matcher; } public boolean hasNext() { return matcher.find(); } public String next() { return matcher.group(0); } } static public void main(String[] args) throws Throwable { Pattern pattern = Pattern.compile("[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)"); new TreeMap(new BufferedReader(new InputStreamReader(System.in)) .lines().map(line -> { Matcher matcher = pattern.matcher(line); return StreamSupport.stream( Spliterators.spliteratorUnknownSize(new MatcherIterator(matcher), Spliterator.ORDERED), false); }).reduce(Stream.empty(), Stream::concat).collect(groupingBy(o -> o, counting())) ).forEach((k, v) -> { System.out.printf("%s\t%s\n",k,v); }); } } 

Bueno, en Java 8, hay Pattern.splitAsStream que proporcionará una secuencia de elementos divididos por un patrón delimitador , pero desafortunadamente no hay un método de soporte para obtener una secuencia de coincidencias .

Si va a implementar tal Stream , le recomiendo implementar Spliterator directamente en lugar de implementar y empaquetar un Iterator . Puede que esté más familiarizado con Iterator pero implementar un Spliterator simple es sencillo:

 final class MatchItr extends Spliterators.AbstractSpliterator { private final Matcher matcher; MatchItr(Matcher m) { super(m.regionEnd()-m.regionStart(), ORDERED|NONNULL); matcher=m; } public boolean tryAdvance(Consumer action) { if(!matcher.find()) return false; action.accept(matcher.group()); return true; } } 

Sin forEachRemaining puede considerar forEachRemaining con un bucle directo.


Si entiendo tu bash correctamente, la solución debería verse más como:

 Pattern pattern = Pattern.compile( "[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)"); try(BufferedReader br=new BufferedReader(System.console().reader())) { br.lines() .flatMap(line -> StreamSupport.stream(new MatchItr(pattern.matcher(line)), false)) .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v)); } 

Java 9 proporciona un método Stream results() directamente en el Matcher . Pero para encontrar coincidencias dentro de una transmisión, hay un método aún más conveniente en Scanner . Con eso, la implementación se simplifica

 try(Scanner s = new Scanner(System.console().reader())) { s.findAll(pattern) .collect(Collectors.groupingBy(MatchResult::group,TreeMap::new,Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v)); } 

Esta respuesta contiene un puerto de respaldo de Scanner.findAll que se puede usar con Java 8.

Saliendo de la solución de Holger, podemos admitir operaciones de Matcher arbitrarias (como obtener el n- ésimo grupo) haciendo que el usuario proporcione una operación Function . También podemos ocultar el Spliterator como un detalle de implementación, para que las personas que llaman puedan trabajar directamente con Stream . Como regla general, StreamSupport debe ser utilizado por el código de la biblioteca, en lugar de los usuarios.

 public class MatcherStream { private MatcherStream() {} public static Stream find(Pattern pattern, CharSequence input) { return findMatches(pattern, input).map(MatchResult::group); } public static Stream findMatches( Pattern pattern, CharSequence input) { Matcher matcher = pattern.matcher(input); Spliterator spliterator = new Spliterators.AbstractSpliterator( Long.MAX_VALUE, Spliterator.ORDERED|Spliterator.NONNULL) { @Override public boolean tryAdvance(Consumer action) { if(!matcher.find()) return false; action.accept(matcher.toMatchResult()); return true; }}; return StreamSupport.stream(spliterator, false); } } 

Puede usarlo así:

 MatcherStream.find(Pattern.compile("\\w+"), "foo bar baz").forEach(System.out::println); 

O para su tarea específica (tomando prestado nuevamente de Holger):

 try(BufferedReader br = new BufferedReader(System.console().reader())) { br.lines() .flatMap(line -> MatcherStream.find(pattern, line)) .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting())) .forEach((k, v) -> System.out.printf("%s\t%s\n", k, v)); } 

Si desea utilizar un Scanner junto con expresiones regulares utilizando el método findWithinHorizon , también puede convertir una expresión regular en una secuencia de cadenas. Aquí usamos un generador de flujo que es muy conveniente para usar durante un ciclo while convencional.

Aquí hay un ejemplo:

 private Stream extractRulesFrom(String text, Pattern pattern, int group) { Stream.Builder builder = Stream.builder(); try(Scanner scanner = new Scanner(text)) { while (scanner.findWithinHorizon(pattern, 0) != null) { builder.accept(scanner.match().group(group)); } } return builder.build(); }