Wyzwanie Java #3: Rozwiązanie

 

W trzecim wyzwaniu poprosiliśmy Was o stworzenie kalkulatora, który będzie pracował na własnym typie (klasie) Fraction reprezentującym ułamek. Problem jaki mogliśmy spotkać w poprzednim rozwiązaniu, to operacje typu 2 / 3 były rzutowane na typ double i przez to zaokrąglane do postaci 0.66(6), co będzie skutkować błędami obliczeń.

By wyjaśnić lepiej zadanie, a tym samym rozwiać wątpliwości związane z programowaniem obiektowym, zadanie będzie rozwiązane krok po kroku.

Rozwiązanie

Zaczynamy od stworzenia klasy Fraction z trzema polami wymienionymi w zadaniu, czyli licznika, mianownika i części całkowitej:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

}

Żeby móc stworzyć ten obiekt wraz z polami, dodamy konstruktor, czyli specjalną “metodę” tworzącą obiekty:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

    public Fraction(int fractionInteger, int numerator, int denominator) {
        this.fractionInteger = fractionInteger;
        this.numerator = numerator;
        this.denominator = denominator;
    }

}

Na razie idzie prosto. Jednak klasa jeszcze nic nie potrafi, pola są prywatne (hermetyzacja), więc tworząc taką klasę nie mamy nawet do nich dostępu… Dlatego stwórzmy sobie dodatkowo funkcję która zwraca nam ułamek w postaci dziesiętnej, czyli po prostu typ double. Dostajemy zatem taki kod:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

    public Fraction(int fractionInteger, int numerator, int denominator) {
        this.fractionInteger = fractionInteger;
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public double getFractionAsDecimal() {
        return fractionInteger + (double) numerator / denominator;
    }

}

W powyższym kodzie trudność może sprawić konieczność rzutowania, czyli wymuszenia by liczba numerator będąca typem int była jednak potraktowana jako double. Gdybyśmy tego nie zrobili, wynik działania zostałby “obcięty” do części całkowitej, w końcu typ int jest typem całkowitym. Sam wynik byłby zwrócony jako double, tak robi to nasza metoda, jednak wtedy będzie już “za późno”, końcówka zostanie ucięta wcześniej (zgodnie z kolejnością wykonywania obliczeń).

Teraz możemy pójść dalej i zaimplementować podstawowe operacje matematyczne na ułamkach, czyli dodawanie, odejmowanie, mnożenie i dzielenie.

Nasza klasa zaczyna wyglądać tak:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

    public Fraction(int fractionInteger, int numerator, int denominator) {
        this.fractionInteger = fractionInteger;
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Fraction(int numerator, int denominator) {
        if (numerator == denominator) { // gdy licznik i mianownik równe, wynik to liczba 1
            this.fractionInteger = 1;
            this.numerator = 0;
            this.denominator = 1;
        } else if (numerator > denominator) { // ułamek niewłaściwy, wyciągamy część całkowitą
            this.fractionInteger = numerator / denominator;
            this.numerator = numerator % denominator;
            this.denominator = denominator;
        } else { // ułamek właściwy
            this.fractionInteger = 0;
            this.numerator = numerator;
            this.denominator = denominator;
        }
    }

    public double getFractionAsDecimal() {
        return fractionInteger + (double) numerator / denominator;
    }

    public Fraction add(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 + n2 * d1, d1 * d2);
    }

    public Fraction subtract(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 - n2 * d1, d1 * d2);
    }

    public Fraction multiply(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * n2, d1 * d2);
    }

    public Fraction divide(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2, d1 * n2);
    }

}

Dla ułatwienia czytelności działań matematycznych, zostały użyte dodatkowe zmienne n1, d1, n2, d2. Należy zauważyć, że obok czterech metod odpowiadających za podstawowe operacje matematyczne został dodany nowy konstruktor który przyjmuje jedynie licznik i mianownik i w razie potrzeby wyciąga część całkowitą gdy ułamek jest niewłaściwy, tworzymy wtedy tak zwaną liczbę mieszaną.

Jesteśmy już praktycznie “w domu”. Taka postać klasy Fraction spełnia nasze podstawowe wymagania, czyli możliwość pracy na ułamkach. Jednak pokusimy się tutaj o dodanie czegoś bardziej zaawansowanego, mianowicie skracanie ułamków. Powyższy kod potrafi nam wyprodukować wynik w postaci 10/8, więc warto by było rozszerzyć klasę o algorytm skracający ułamek do postaci 5/4. By skrócić ułamek, potrzebujemy wyliczyć największy wspólny dzielnik (NWD) (ang. GCD - Greatest Common Divisor). Tutaj jednak zaczynają się schody, bo nie jest to wcale tak trywialne zadanie, by go obliczyć. Można oczywiście poszukiwać NWD iterując się w pętli po wszystkich możliwych liczbach z zakresu od zera do mniejszej z nich, dzielić obydwie i sprawdzać czy reszta z dzielenia dla obydwu jest zerowa, ale nie będzie to zbyt optymalne. Euklides odkrył, że największy wspólny dzielnik dwóch liczb, dzieli także ich różnicę, dlatego wystarczy, że dopóki dwie liczby są identyczne, odejmujemy od większej mniejszą. Gdy liczby staną się równe, dowolna z nich to nasz NWD.

Nasza klasa po przeróbce może wyglądać tak:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

    public Fraction(int fractionInteger, int numerator, int denominator) {
        // UWAGA, ten kod nie zabezpiecza nas przed mianownikiem równym zero!

        int gcd = gcd(numerator, denominator);

        // skracanie ułamka
        numerator = numerator / gcd;
        denominator = denominator / gcd;

        this.fractionInteger = fractionInteger;
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Fraction(int numerator, int denominator) {
        // UWAGA, ten kod nie zabezpiecza nas przed mianownikiem równym zero!

        int gcd = gcd(numerator, denominator);

        // skracanie ułamka
        numerator = numerator / gcd;
        denominator = denominator / gcd;

        if (numerator == denominator) { // gdy licznik i mianownik równe, wynik to liczba 1
            this.fractionInteger = 1;
            this.numerator = 0;
            this.denominator = 1;
        } else if (numerator > denominator) { // ułamek niewłaściwy, wyciągamy część całkowitą
            this.fractionInteger = numerator / denominator;
            this.numerator = numerator % denominator;
            this.denominator = denominator;
        } else { // ułamek właściwy
            this.fractionInteger = 0;
            this.numerator = numerator;
            this.denominator = denominator;
        }
    }

    public double getFractionAsDecimal() {
        return fractionInteger + (double) numerator / denominator;
    }

    public Fraction add(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 + n2 * d1, d1 * d2);
    }

    public Fraction subtract(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 - n2 * d1, d1 * d2);
    }

    public Fraction multiply(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * n2, d1 * d2);
    }

    public Fraction divide(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2, d1 * n2);
    }

    /**
     * Największy wspólny dzielnik
     */
    public static int gcd(int a, int b) {
        if (a < 0) a = -a; // zmieniamy na wartości dodatnie
        if (b < 0) b = -b;

        while (a != b)
            if (a > b) a -= b;
            else b -= a;

        return a;
    }

}

Skracanie zostało użyte w obydwu konstruktorach.

Powyższą modyfikacja wymagała większej wiedzy matematycznej, dlatego potraktujmy to jako wersję z gwiazdką.

To wszystko co należało wykonać w klasie Fraction. Zostało już jedynie napisanie samego kalkulatora, czyli klasy z metodą main. Ten kod stworzyliśmy już w poprzednim wyzwaniu, teraz jedynie go zmodyfikujemy o użycie klasy Fraction:

package pl.kodolamacz.calc;

import pl.kodolamacz.math.Fraction;

import java.util.Scanner;

public class FractionCalculator {

    public static void main(String[] args) {

        var scanner = new Scanner(System.in);

        System.out.println("Proszę podaj w oddzielnych linijkach jakąś liczbę, operację matematyczną +,-,*,/ a następnie kolejną liczbę:");

        String runAgain;
        do {

            var numerator = scanner.nextInt();
            scanner.next();
            var denominator = scanner.nextInt();
            scanner.nextLine();
            var x = new Fraction(numerator, denominator);

            var operation = scanner.nextLine();

            numerator = scanner.nextInt();
            scanner.next();
            denominator = scanner.nextInt();
            scanner.nextLine();
            var y = new Fraction(numerator, denominator);

            Fraction result;
            if (operation.equals("+")) {
                result = x.add(y);
            } else if (operation.equals("-")) {
                result = x.subtract(y);
            } else if (operation.equals("*")) {
                result = x.multiply(y);
            } else if (operation.equals("/")) {
                result = x.divide(y);
            } else {
                System.out.println("Nieznana operacja!");
                result = new Fraction(0);
            }
            System.out.println("Twój wynik to: " + result.getFractionAsString());

            System.out.println("Chcesz wykonać kolejne działanie? Wpisz literę t lub n.");
            runAgain = scanner.nextLine();

        } while (runAgain.equals("t"));

        scanner.close();
    }

}

W powyższym kodzie zostało przyjęte, że użytkownik podaje liczby w postaci: licznik / mianownik. Aby ułatwić sobie napisanie powyższego kodu, dodaliśmy dodatkowo nowy konstruktor przyjmujący tylko część całkowitą oraz metodę getFractionAsString zwracającą ułamek jako String do wyświetlenia.

Ostateczna wersja klasy Fraction wygląda tak:

package pl.kodolamacz.math;

public class Fraction {

    private int fractionInteger;
    private int numerator;
    private int denominator;

    public Fraction(int value) {
        fractionInteger = value;
        numerator = 0;
        denominator = 1;
    }

    public Fraction(int fractionInteger, int numerator, int denominator) {
        // UWAGA, ten kod nie zabezpiecza nas przed mianownikiem równym zero!

        int gcd = gcd(numerator, denominator);

        // skracanie ułamka
        numerator = numerator / gcd;
        denominator = denominator / gcd;

        this.fractionInteger = fractionInteger;
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Fraction(int numerator, int denominator) {
        // UWAGA, ten kod nie zabezpiecza nas przed mianownikiem równym zero!

        int gcd = gcd(numerator, denominator);

        // skracanie ułamka
        numerator = numerator / gcd;
        denominator = denominator / gcd;

        if (numerator == denominator) { // gdy licznik i mianownik równe, wynik to liczba 1
            this.fractionInteger = 1;
            this.numerator = 0;
            this.denominator = 1;
        } else if (numerator > denominator) { // ułamek niewłaściwy, wyciągamy część całkowitą
            this.fractionInteger = numerator / denominator;
            this.numerator = numerator % denominator;
            this.denominator = denominator;
        } else { // ułamek właściwy
            this.fractionInteger = 0;
            this.numerator = numerator;
            this.denominator = denominator;
        }
    }

    public double getFractionAsDecimal() {
        return fractionInteger + (double) numerator / denominator;
    }

    public String getFractionAsString() {
        if (denominator == 1) { // jeśli mianownik równy 1, to w naszej klasie powinniśmy mieć liczbę całkowitą
            return String.valueOf(fractionInteger);
        } else if (fractionInteger == 0) { // jeśli nie ma części całkowitej zwracamy sam ułamek
            return numerator + "/" + denominator;
        } else { // jeśli jest część całkowita to zwracamy jako liczbę mieszaną
            return fractionInteger + " " + numerator + "/" + denominator;
        }
    }

    public Fraction add(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 + n2 * d1, d1 * d2);
    }

    public Fraction subtract(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2 - n2 * d1, d1 * d2);
    }

    public Fraction multiply(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * n2, d1 * d2);
    }

    public Fraction divide(Fraction fraction) {
        int n1 = this.numerator + this.fractionInteger * this.denominator;
        int d1 = this.denominator;
        int n2 = fraction.numerator + fraction.fractionInteger * fraction.denominator;
        int d2 = fraction.denominator;
        return new Fraction(n1 * d2, d1 * n2);
    }

    /**
     * Największy wspólny dzielnik
     */
    public static int gcd(int a, int b) {
        if (a < 0) a = -a; // zmieniamy na wartości dodatnie
        if (b < 0) b = -b;

        while (a != b)
            if (a > b) a -= b;
            else b -= a;

        return a;
    }

}

zaś samo wykonanie programu:

Proszę podaj w oddzielnych linijkach jakąś liczbę, operację matematyczną +,-,*,/ a następnie kolejną liczbę:
1 / 2
+
3 / 4
Twój wynik to: 1 1/4
Chcesz wykonać kolejne działanie? Wpisz literę t lub n.
t
5 / 4
+
7 / 8
Twój wynik to: 2 1/8
Chcesz wykonać kolejne działanie? Wpisz literę t lub n.
n

Process finished with exit code 0

Czy można to zrobić jeszcze lepiej?

Podobnie jak poprzednio, warto by było w przyszłości rozszerzyć ten program o lepszą obsługę błędów w przypadku sytuacji wyjątkowych, takich jak np. dzielenie przez zero. O walidacji i obsłudze błędów w Javie wspomnimy jednak w innym wyzwaniu, dlatego teraz jeszcze to pomijamy. Jeśli byście chcieli zobaczyć, jak wygląda taka klasa po uwzględnieniu wszystkich sytuacji wyjątkowych z zastosowaniem bardziej zaawansowanych algorytmów niż powyższe, polecam spojrzeć na kod źródłowy klasy Fraction z otwarto źródłowej biblioteki Apache Commons.

Jeśli macie więcej pytań lub problemów, piszcie do nas śmiało na Facebooku, chętnie pomożemy. :) Do zobaczenia w poniedziałek na kolejnym wyzwaniu!

Radosław Szmit

Opiekun bootcampu Full-stack w Kodołamacz.pl. Inżynier oprogramowania, specjalista Big Data, trener IT. Absolwent Politechniki Warszawskiej aktualnie pracujący nad rozprawą doktorską z zakresu Big Data i NLP. Twórca polskiej wyszukiwarki internetowej NEKST stworzonej przez Instytut Podstaw Informatyki Polskiej Akademii Nauk oraz Otwartego Systemu Antyplagiatowego realizowanego przez Międzyuniwersyteckie Centrum Informatyzacji. Zawodowo konsultant IT specjalizujący się w rozwiązaniach Java Enterprise Edition, Big Data oraz Business Intelligence, trener IT w firmie Sages.
Komentarze
Ostatnie posty
Data Science News #202
Data Science News #201
Data Science News #200