Najlepszy Java Developer – Zadanie 5: Text Histogram

Zadanie

Kolejny raz zgłosił się do mnie Pablo z pewnym problemem. Ma problem ze zliczaniem znaków w tekście, chciałby móc robić to automatycznie. Postanowił napisać aplikację, stworzył szablon projektu, ale nie wie co ma zrobić dalej.

Pomożesz mu w tym zadaniu? Już przekazuję co mi powiedział o jego wymaganiach i o tym co już zrobił.

Cały szablon znajdziesz tutaj – otwórz go, a może szybciej zrozumiesz to co tłumaczę. 😉

Głównym punktem wejścia jest fasada – HistogramFacade 

  • generateHistogram – ma zwracać histogram w postaci mapy,
  • generateHistogramCSV – ma zwracać Stringa, w którym jest histogram w formie CSV
  • saveHistogramToCSV – tworzy histogram i automatycznie zapisuje go do podanego pliku

Fasada korzysta z HistogramFactory:

  • metoda createHistogram ma za zadanie zwrócić histogram w formie Mapy
  • przed każdym utworzeniem histogramu powinna być wczytana konfiguracja z pliku

HistogramConfigurationLoader ma za zadanie wczytywać konfigurację z pliku – o niej powiem za chwilę. 😉

HistogramCSVGenerator ma metodę convertHistogramToCSV, która zwraca histogram w postaci CSV w Stringu na podstawie podanego histograma w formie mapy.

Wrócmy do konfiguracji, plik konfiguracji znajduje się w src/main/resources/histogram.properties. Wygląda on tak:

histogram.ignore.white-spaces=true
histogram.ignore.characters=.,:;\/?!'"<>-()*%$@#&{}[]+=^~`|

Mamy tylko dwa klucze:

  • histogram.ignore.white-spaces – odpowiada za to czy w histogramie mają być uwzględnianie białe znaki – spacje, tabulatory i nowe linie
  • histogram.ignore.characters – zawiera wszystkie znaki, które mają nie być zliczane w histogramie

Pablo przygotował również 4 testy, które może pomogą Ci podczas developowania aplikacji. 😉

Gdybyś do końca nie wiedział czym jest histogram – mówi on tam dokładnie o tym ile razy mamy doczynienia z danym kryterium (u nas kryterium jest dany znak) – to więcej możesz poczytać np. tutaj.

Klucze histogramu muszą być posortowane, w tym celu musisz skorzystać z TreeMap zamiast HashMap.

Jeśli coś jest niejasne to pytaj! – w komentarzu lub na grupie. 😉

Punktacja

Przygotowane jest 10 testów, za każdy można uzyskać 80 expa – czyli łącznie 800 expa.

Porady

  • Wykorzystanie enum do identyfikowania property może być dobrym pomysłem – możesz w nim zapisywać klucz i później porównywać podczas parsowania pliku. Z pewnością będzie to dużo bardziej czytelne rozwiązanie niż zwykłe porównywanie Stringów.
  • Większe bloki zamykaj w metody i odpowiednio nazywaj – kod będzie czytelniejszy
  • Podczas tworzenia CSV wykorzystaj StringBuilder – jeśli nie wiesz po co, to możesz poczytać o tym tutaj.
  • Pytaj, gdy coś nie jest jasne
  • Napisz sobie proste testy – np. do ładowania konfiguracji, fasady, generatora – zobaczysz, że dużo szybciej napiszesz swój kod. 😉

Czas

Zadanie zostało opublikowane 15 grudnia, a jego rozwiązania można przesyłać do 23 grudnia do godziny 23.59. Zadania wysłane później będą automatycznie usuwane.

Format

Projekt powinien wykonany na podstawie tego szablonu. Czemu tak? Ułatwia mi to sprawdzanie, proszę nie zmieniaj sygnatur metod oraz położenia pliku, ponieważ spowoduje to dużo zmian w moich testach. Problemu nie ma, gdy sprawdzam 1-2 pracę, problem jest, gdy liczba jest większa. 😉

Zadanie należy wysłać na email: njd@1024kb.pl z tematem: TWÓJ-NICK_HISTOGRAM_TEXT.

Zadanie przesyłamy jako repozytorium Git – może być hostowany na GitHub, GitLab, Bitbucket – gdzie tylko chcesz. W wiadomości podajemy tylko link do repozytorium projektu. 😉

Pamiętajcie, aby do pliku .gitignore dodać:

  • /target
  • /out
  • /.idea
  • *.iml

I inne pliki/katalogi, które nie powinny być na zdalnym repozytorium.

W razie jakichkolwiek wątpliwości pytajcie jak ma wyglądać finalna aplikacja. 😉

Rozwiązanie

Zacznijmy od klasy odpowiedzialnej za wczytywanie konfiguracji – HistogramConfigurationLoader.

package histogram.config;

import histogram.config.HistogramProperty;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class HistogramConfigurationLoader {
    public final static List<Character> WHITE_SPACPES = Arrays.asList(
            '\n', '\t', '\r', ' ');

    public HistogramConfiguration loadProperties(String propertyFileName) {
        HistogramConfiguration config = new HistogramConfiguration();

        try {
            List<String> lines = readFile(propertyFileName);

            for (String line: lines) {
                parseProperty(config, line);
            }

        } catch (IOException e) {
            System.out.println("Histogram properties file does not exist. Factory will use default configuration.");
        }

        return config;
    }

    private List<String> readFile(String fileName) throws IOException {
        List<String> lines = new LinkedList<>();
        BufferedReader br = new BufferedReader(new FileReader(fileName));

        String line = br.readLine();
        while (line != null) {
            lines.add(line);
            line = br.readLine();
        }

        br.close();

        return lines;

    }

    private void parseProperty(HistogramConfiguration config, String line) {
        String value = line.split("=")[1];

        if (isIgnoreWhiteSpacesProperty(line)) {
            config.setShouldIgnoreWhiteSpaces(Boolean.valueOf(value));

        } else if (isIgnoreCharactersProperty(line)) {
            Set<Character> chars = value.chars()
                    .mapToObj(word -> (char) word)
                    .collect(Collectors.toSet());

            config.setIgnoreCharacters(chars);
        }
    }

    private boolean isIgnoreCharactersProperty(String line) {
        return line.contains(HistogramProperty.IGNORE_CHARACTERS.toString());
    }

    private boolean isIgnoreWhiteSpacesProperty(String line) {
        return line.contains(HistogramProperty.IGNORE_WHITE_SPACES.toString());
    }
}

Logika tej klasy jest napisana bardzo prosto, ale za równo niezbyt ładnie – już teraz pisząc podsumowanie zadania widzę, że m.in część odpowiedzialną za parsowanie klucza można napisać lepiej (bez użycia if-ów). 😉

Jedyna publiczną metodą jest loadProperties – na jej początku tworzymy obiekt nowej konfiguracji, wczytujemy cały plik – linia po linii i dalej przechodzimy do metody parseProperty.

Metoda parseProperty rozdziela wczytaną linię na podstawie separatora równa się „=”. Sprawdza klucz, jeśli jest wpisany odpowiednio to zapisujemy wartość pod znaku równa się do konfiguracji.

Po przejściu całej listy linii zwracamy konfigurację z metody.

Następnie mamy klasę HistogramFactory, która ma za zadanie stworzyć histogram (mapę) na podstawie podanego Stringa.

package histogram.factory;

import histogram.config.HistogramConfiguration;
import histogram.config.HistogramConfigurationLoader;

import java.util.Map;
import java.util.TreeMap;

public class HistogramFactory {
    private String propertyFileName = "src/main/resources/histogram.properties";
    private final HistogramConfigurationLoader histogramConfigurationLoader = new HistogramConfigurationLoader();

    public HistogramFactory() {
    }

    public HistogramFactory(String propertyFileName) {
        this.propertyFileName = propertyFileName;
    }

    public Map<Character,Long> createHistogram(String text) {
        HistogramConfiguration config = histogramConfigurationLoader.loadProperties(propertyFileName);
        Map<Character, Long> histogram = new TreeMap<>();

        for(Character character : text.toLowerCase().toCharArray()) {
            if (!isWhiteSpace(config, character) && !isForbiddenChar(config, character)) {
                histogram.merge(character, 1L, Long::sum);
            }
        }

        return histogram;
    }


    private boolean isWhiteSpace(HistogramConfiguration config, Character character) {
        return config.shouldIgnoreWhiteSpaces()
               && HistogramConfigurationLoader.WHITE_SPACPES.contains(character);
    }

    private boolean isForbiddenChar(HistogramConfiguration config, Character character) {
        return config.getIgnoreCharacters().contains(character);
    }
}

Interesuje nas tutaj głównie metoda createHistogram, która  na początku wczytuje konfigurację z pliku i na znak po znaku sprawdza wyznaczone kryteria -> czy jest zakazanym znakiem lub czy jest znakiem biały (oczywiście jeśli w konfiguracja flaga jest ustawiona na true).

Jeśli owy warunek jest sprawdzony to inkrementujemy wartość w mapie dla danego chara o 1 (jeśli nie istnieje to ustawiamy na 1) – i to dla nas robi magiczna metoda merge.

histogram.merge(character, 1L, Long::sum);

Tworzenie pliku CSV nie jest bardzo skomplikowanym zadaniem – iterujemy po całej mapie klucz -> wartość i zapisujemy linia po linii klucze i wartość oddzielając je tylko przecinkami.

package histogram.CSV;

import java.util.Map;

public class HistogramCSVGenerator {
    private static final char SEPARATOR = ',';
    private static final char NEW_LINE = '\n';

    public String convertHistogramToCSV(Map<Character, Long> histogram) {
        StringBuilder sb = new StringBuilder();

        for (Map.Entry<Character, Long> entry : histogram.entrySet()) {
            sb.append(entry.getKey());
            sb.append(SEPARATOR);
            sb.append(entry.getValue());
            sb.append(NEW_LINE);
        }

        return sb.toString();
    }


}

Na koniec zwracamy zbudowanego Stringa, który jest gotowy do zapisu do pliku.

Klasa HistogramFacade zamyka w sobie możliwość tworzenie histogramu i generowanie CSV i udostępnia nam metody do:

  • tworzenia histogramu w formacie CSV
public String generateHistogramCSV(String text) {
    Map<Character, Long> histogram = histogramFactory.createHistogram(text);
    return histogramCSVGenerator.convertHistogramToCSV(histogram);
}

I nie jest to nic innego jak wywołanie już stworzonych wcześniej metod.

  • generowanie histogramu
public Map<Character, Long> generateHistogram(String text) {
    return histogramFactory.createHistogram(text);
}

Również korzystamy już z wcześniej napisanych metod

  • zapis histogramu do pliku CSV
public void saveHistogramToCSV(String text, String fileName) {
    Map<Character, Long> histogram = histogramFactory.createHistogram(text);
    String histogramCSV = histogramCSVGenerator.convertHistogramToCSV(histogram);

    try {
        BufferedWriter bw = new BufferedWriter(new FileWriter(fileName));
        bw.write(histogramCSV);
        bw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Ostatnia metoda musi od siebie dołożyć również zapis do pliku utworzonego wcześniej histogramu (Stringa).

Podsumowanie

Zadanie samo w sobie nie było aż tak bardzo skomplikowane, lecz myślę, że samemu je za bardzo skomplikowałem. Prawdopodobnie właśnie to odbiło się na małej ilości uczestników – tym razem tylko 3. Dzielni rycerze, właśnie takich nagroda czeka. 😉

Ewentualnie może kogoś przycisnął okres świąt – tak samo jak i mnie, w końcu dopiero po 11 dniach udało sprawdzić mi się zadanie.

Najważniejsze, że wyciągne z tego wnioski i postaram się kolejne zadanie przekazywać w prostszej formie – pamiętajcie, że nie mogę dać wam swodoby, ponieważ wtedy sprawdzanie zadań będzie trwało wieki. A tak to tylko wklejam swoje testy, wpisuję jedną komendę i mam wyniki. 😉

Sporo się również przy Was uczę, dzisiaj m.in. poznałem klasę Properties, która za nas załatwia wczytywanie plików properties – świetna sprawa, oprócz jednego problemu: backslasha wczytuje tylko w takiej formie: „\\”, niestety nie rozpoznaje, gdy podamy w takiej: „\”. Mały problem, ale i tak ze względu na takie działanie biblioteki testy zaliczałem.

Moje przykładowe rozwiązanie można zobaczyć tutaj,

Na koniec gratuluję trzem programistom, każdy z nich uzyskał 100% punktów. Wielkie gratulację i życzę każdemu powodzenia w kolejnych zadaniach NJD. 😉