Frecuencia de palabras con api Stream

Un simple ejemplo donde se usa el api Stream de java, para extraer las palabras y contar el numero de repeticiones, para ello usamos una función para eliminar los caracteres en este caso serán, [.,!] y caracter de espaciado uno solo así " " más los Strings que sean empty así "", que debemos filtrar si o si.

La expresión regular que se me ocurrió es esta [\\.\\,\\!] usando doble backslash, todas los matches de puntos, comas, exclamación, también las convertiremos en minúsculas, con toLowerCase junto con el método replaceAll

private String normalize(final String word) {
    return word.toLowerCase().replaceAll("[\\.\\,\\!]", "");
}

Nuestro texto objetivo es el siguiente:

Hola que tal, bienvenidos a BettaTech, Si os está gustando este video, suscribiros y darle a la campanita para ver los nuevos videos que vaya subiendo!

Este String lo introducimos en un array de String e invocamos al método:

split(" ");

con un caracter en blanco como parámetro, para que después de cada espacio almacene cada palabra en una posición de nuestro array.

final String[] words = "Hola que tal, bienvenidos a BettaTech, Si os está gustando este vídeo, suscribiros y darle a la campanita para ver los nuevos videos que vaya subiendo!".split(" ");

Imprimimos con printf un formateo de 15 caracteres de espaciado

System.out.printf("%-15s%s%n","Word","Frecuency");

Invocamos al método filter del Stream, la fase de filtrado para los carácteres empty y null de nuestro String, para compactar más el código, pasaremos el propio método por referencia method reference ::

.filter(this::filterEmptyAndNull)
private boolean filterEmptyAndNull(final String word) {
    return Objects.nonNull(word) && !"".equals(word);
}

La función map, que es la fase de mapping del stream, en este caso hacemos un method::reference ya que el método normalize es una función con un parámetro y retorna un objeto, encajando en nuestro map perfectamente.

.map(this::normalize)

Hacemos una reducción mutable a este Stream con collect, donde el método groupingBy le dice al collect que debe agrupar todos los elementos en un Map, el primer parámetro es que retorne el mismo valor pasado en la expresión lambda siendo una operación muy común podríamos usar tanto e -> e como Function.identity(), el segundo parámetro es un downstream collector Collectors.counting()que nos permitirá contar los elementos repetidos, permitiendo crear el value de cada key, como lo siguiente:

.collect(Collectors.groupingBy(e -> e,Collectors.counting()))

Por ultimo un forEach, pero como ya nuestro collect nos retorna un Map<String,Long> el forEach de la interface Map, tiene un BiConsumer como parámetro, y esta a su vez, el método accept, que tiene 2 parámetros genéricos, uno para la key y otro para el value, forEach((key,value) -> {})

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 
 */
@Service
public class FrecuencyWordsService {

    private static final String REG_EXP = "[\\.\\,\\!]";
    private static final String SPLIT_ESPACE_CARACTER = " ";
    
    /**
     *
     * Process with parallelStream()
     *
     * @param words to count
     * @return Map<String,Long> with key(word), and value(frecuency)
     */
    public Map<String,Long> frecuencyWordsParallel(final String words) {
        final String[] splitWords = words.split(SPLIT_ESPACE_CARACTER);
        return Arrays.stream(splitWords)
                .parallel()
                .filter(this::filterEmptyAndNull)
                .map(this::normalize)           
                .collect(Collectors.groupingBy(Function.identity(),Collectors.counting()));
    }

    /**
     *
     * Process with stream()
     *
     * @param words to count
     * @return Map<String,Long> with key(word), and value(frecuency)
     */
    public Map<String,Long> frecuencyWords(final String words) {
        final String[] splitWords = words.split(SPLIT_ESPACE_CARACTER);
        return Arrays.stream(splitWords)
                .filter(this::filterEmptyAndNull)
                .map(this::normalize)
                .collect(Collectors.groupingBy(Function.identity(),Collectors.counting()));
    }

    /**
     *
     * @param input path
     * @return Map<String,Long> with key(word), and value(frecuency)
     */
    public Map<String,Long> frecuencyWordsFromFile(final Path input) {
        try (final Stream<String> lines = Files.lines(input)) {

            return Arrays.stream(lines.collect(Collectors.joining())
                    .split(SPLIT_ESPACE_CARACTER))
                    .filter(this::filterEmptyAndNull)
                    .map(this::normalize)
                    .collect(Collectors.groupingBy(Function.identity(),Collectors.counting()));

        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private boolean filterEmptyAndNull(final String word) {
        return Objects.nonNull(word) && !"".equals(word);
    }

    private String normalize(final String word) {
        return word.toLowerCase().replaceAll(REG_EXP, "");
    }

}

Test final y también con lectura de archivo que contiene el mismo texto target

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Simple test for frecuency words
 */
@Log4j2 
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {FrecuencyWordsService.class,Memory.class})
class FrecuencyTest {

    private static final String FORMAT_PRINTF_15 = "%-15s%s%n";
    private static final String WORD = "Word";
    private static final String HOLA = "hola";
    private static final String FRECUENCY = "Frecuency";

    private static final Path PATH_TEXT_FILE = Path.of("src/test/resources/textoSimple.txt");

    @Autowired
    private FrecuencyWordsService frecuencyWordsService;

    @Autowired
    private Memory memory;

    private final String WORDS = "Hola que tal, bienvenidos a BettaTech, Si os está gustando este vídeo, suscribiros y darle a la campanita para ver los nuevos videos que vaya subiendo!";

    @Test
    void testFrecuencyWordsParallel() {
        System.out.println("Run Frecuency holaParallel");
        System.out.printf(FORMAT_PRINTF_15, WORD, FRECUENCY);
        final Map<String, Long> mapWordsParallel = frecuencyWordsService.frecuencyWordsParallel(WORDS);
        Assertions.assertNotNull(mapWordsParallel);

        mapWordsParallel.forEach((word, frecuency) -> {
            log.info(String.format(FORMAT_PRINTF_15, word, frecuency));
        });

        final long result = mapWordsParallel.get(HOLA);

        final long actualHola = 1;
        MatcherAssert.assertThat(actualHola, Matchers.is(result));
        /*
         * total memory consumptionhola
         */
        log.info(memory.getTotalMemory());
    }

    @Test
    void testFrecuencyWordsFromTextFile() {
        try (final InputStream inputStream = Files.newInputStream(PATH_TEXT_FILE);
             final Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             final BufferedReader br = new BufferedReader(reader)) {

            final StringBuilder sb = new StringBuilder();
            //read lines with Stream api
            sb.append(br.lines().collect(Collectors.joining()));

            final Map<String, Long> mapWordsParallel = frecuencyWordsService.frecuencyWords(sb.toString());
            mapWordsParallel.forEach((word, frecuency) -> {
                log.info(String.format(FORMAT_PRINTF_15, word, frecuency));
            });

            final long result = mapWordsParallel.get(HOLA);
            final long actualHola = 1;

            MatcherAssert.assertThat(actualHola, Matchers.is(result));

        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
        /*
         * total memory consumption
         */
        log.info(memory.getTotalMemory());
    }

    @Test
    void frecuencyFromPath() {

        final Map<String, Long> mapWordsParallel = frecuencyWordsService.frecuencyWordsFromFile(PATH_TEXT_FILE);
        mapWordsParallel.forEach((word, frecuency) -> {
            log.info(String.format(FORMAT_PRINTF_15, word, frecuency));
        });

        final long result = mapWordsParallel.get(HOLA);
        final long actualHola = 1;

        MatcherAssert.assertThat(actualHola, Matchers.is(result));
        /*
         * total memory consumption
         */
        log.info(memory.getTotalMemory());
    }

}    

Salida en consola

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
'  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::                (v2.4.1)

2021-01-03 05:23:42.129  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Starting FrecuencyTest using Java 11.0.9.1 on rubn0x52e with PID 20326 (started by rubn in /home/rubn/Documentos/frencuencywords)
2021-01-03 05:23:42.132  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : No active profile set, falling back to default profiles: default
2021-01-03 05:23:42.420  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Started FrecuencyTest in 0.452 seconds (JVM running for 1.152)
2021-01-03 05:23:42.618  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Run Frecuency Parallel
2021-01-03 05:23:42.621  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Word           Frecuency

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : darle          1

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : que            2

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : a              2

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vaya           1

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : os             1

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : nuevos         1

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : videos         1

2021-01-03 05:23:42.622  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bienvenidos    1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : hola           1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : verlos         1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : suscribiros    1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : subiendo       1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bettatech      1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : este           1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : campanita      1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : para           1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : está           1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : la             1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : si             1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : gustando       1

2021-01-03 05:23:42.623  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : y              1

2021-01-03 05:23:42.624  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vídeo          1

2021-01-03 05:23:42.624  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : tal            1

2021-01-03 05:23:42.626  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Total Memory 14MB
2021-01-03 05:23:42.634  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Run Frecuency Parallel
2021-01-03 05:23:42.635  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Word           Frecuency

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : darle          1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : que            2

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : a              2

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vaya           1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : os             1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : nuevos         1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : videos         1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bienvenidos    1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : hola           1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : verlos         1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : suscribiros    1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : subiendo       1

2021-01-03 05:23:42.636  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bettatech      1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : este           1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : campanita      1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : para           1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : está           1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : la             1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : si             1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : gustando       1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : y              1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vídeo          1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : tal            1

2021-01-03 05:23:42.637  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Total Memory 16MB
2021-01-03 05:23:42.640  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Run Frecuency Parallel
2021-01-03 05:23:42.640  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Word           Frecuency

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : darle          1

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : que            2

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : a              2

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : ver            1

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vaya           1

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : os             1

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : nuevos         1

2021-01-03 05:23:42.647  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : videos         1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bienvenidos    1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : hola           1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : suscribiros    1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : subiendo       1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : bettatech      1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : este           1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : campanita      1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : los            1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : para           1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : está           1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : la             1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : si             1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : gustando       1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : y              1

2021-01-03 05:23:42.648  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : vídeo          1

2021-01-03 05:23:42.649  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : tal            1

2021-01-03 05:23:42.649  INFO 20326 --- [           main] com.test.frencuencywords.FrecuencyTest   : Total Memory 22MB


Inpiración

Según esta pregunta se la hicieron a un doc para un entrevista, donde resuelve este caso con TypeScript, nada fácil para resolver así a primeras, porque siempre hay soluciones más eficientes que otras, y este caso no es la excepción, porque mi ejemplo tengo 2 maneras más de hacerlo con switch y for, en donde ambas soluciones incremento posiciones.


source

Comments