Feedback

C# - Klasse Bruchrechnung / Class Fraction

Veröffentlicht von am 12.11.2015
(0 Bewertungen)
Many students (like me) get confronted with this Fraction class Tasks if they are learning C#.

I want to provide one solution (out of a million others) to understand how things are working.
There is still room for improvement (e.g. Exceptions etc.) but I think it´s easier to get a first impression for the functionalities if I leave it beside. For the same reasons I reduced the comments in Fraction class.
If you want me to add theese, I will do so.
I thought tapping through it in Debug Modus will show everything you need to know.

If you have any suggestions to make it better, I´d appreciate that!

Thanks to Koopakiller, I changed the method based approach to overloaded operators.
I found this link to MSDN useful to learn how overloaded operator work.
https://msdn.microsoft.com/de-de/library/6fbs5e2h.aspx
For compatibilitie reasons the class should contain a method based solution too.

You´ll find an example for the IFormattable intergration too. The web sources are pasted as a comment in the specific regions.

Here is some sample data for your Programm Main():
Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(1, 4);
Fraction c = new Fraction(7, 2);
Fraction d = new Fraction(1, 2);
Fraction x = new Fraction(0.25);
Fraction y = new Fraction(0.255845121);

Console.WriteLine("Addition");
Console.WriteLine("{0} + {1} = {2}", a, b, a + b);
Console.WriteLine("{0} + 2 = {1}", a, a + 2);
Console.WriteLine("{0} + 0,25 = {1}", a, a + 0.25);

Console.WriteLine();
Console.WriteLine("Substraction / Subtrahieren");
Console.WriteLine("{0} - {1} = {2}", a, b, a - b);
Console.WriteLine("{0} - 2 = {1}", c, c - 2);
Console.WriteLine("{0} - 0,25 = {1}", a, a - 0.25);


Console.WriteLine();
Console.WriteLine("Multiplication / Multiplizieren");
Console.WriteLine("{0} * {1} = {2}", a, b, a * b);
Console.WriteLine("{0} * 2 = {1}", c, c * 2);
Console.WriteLine("{0} * 0,25 = {1}", a, a * 0.25);

Console.WriteLine();
Console.WriteLine("Divison / Dividieren");
Console.WriteLine("{0} / {1} = {2}", a, b, a / b);
Console.WriteLine("{0} / 2 = {1}", a, a / 2);
Console.WriteLine("{0} / 0,25 = {1}", a, a / 0.25);
Console.WriteLine();

Console.WriteLine(c +" after Reciprocal() = " + c.Reciprocal());
Console.WriteLine();

Console.WriteLine("Evaluate / Evaluieren");
Console.WriteLine(x + " as decimal number = " + x.Evaluate());
Console.WriteLine(y + " with 5 decimal places = " + y.Evaluate(5));
Console.WriteLine();

Console.WriteLine("ToString() with IFromattable");
Console.WriteLine("ToString() for "+ c + " without parameters, displayed as a Fraction > " + c.ToString());
Console.WriteLine("ToString() for " + c + " with parameter F, displayed as a Fraction > " + c.ToString("F"));
Console.WriteLine("ToString() for " + c + " with parameter M, displayed as a Mixed Numbers > " + c.ToString("M"));
Console.ReadKey();
using System;
using System.Globalization;

namespace Fraction 
{
    class Fraction : IFormattable
    {            
            // numerator = Zähler / denominator =  Nenner
        public long numerator { get; private set;}
        public long denominator { get; private set; }
        private int exponent { get; set; }
            
        public Fraction(long numerator)
        {
            this.numerator = numerator;
            this.denominator = 1;
        }

        public Fraction(double adouble)            
        {
            this.exponent = (Decimal.GetBits((decimal)adouble)[3] & 0x0ff0000) >> 0x10;
            this.denominator = (long)System.Math.Pow(10, exponent);
            this.numerator = (long)(adouble * denominator);
            Reduce();
        }
                
        public Fraction(long numerator = 0, long denominator = 1)
        {
            this.numerator = numerator;
            this.denominator = denominator;
            Reduce();
        }

        public Fraction(Fraction aFraction)
        {
            numerator = aFraction.numerator;
            denominator = aFraction.denominator;
        }
            
        // get Greatest Common Divisor(Greatest Common Divisor)
        static public long GetGCD(long a, long b)
        {
            if (a == b || b == 0)
            {
                return a;
            }
            else return GetGCD(b, a % b);
        }

        protected void Reduce()
        {
            if (denominator == 0)
            {
                numerator = 0;
                denominator = 1;
            }
            if (denominator < 0)
            {
                denominator *= -1;
                numerator *= -1;
            }
            long gcd = GetGCD(numerator, denominator);
            if (gcd != 0)
            {
                numerator /= gcd;
                denominator /= gcd;
            }
        }
  
        #region Calculating Methods Add / Substract / Multiply / Divide

        #region Method Add
        public Fraction Add(Fraction f1)
        {
            return new Fraction(this.numerator * f1.denominator + this.denominator * f1.numerator, this.denominator * f1.denominator);
        }

        public Fraction Add(int i)
        {
            return this.Add(new Fraction(i));
        }

        public Fraction Add(double i)
        {
            return this.Add(new Fraction(i));
        }
        #endregion

        #region Method Substract
        public Fraction Substract(Fraction f1)
        {
            return new Fraction(this.numerator * f1.denominator - this.denominator * f1.numerator, this.denominator * f1.denominator);
        }

        public Fraction Substract(int i)
        {
            return this.Substract(new Fraction(i));
        }

        public Fraction Substract(double i)
        {
            return this.Substract(new Fraction(i));
        }
        #endregion

        #region Method Multiply
        public Fraction Multiply(Fraction f1)
        {
            return new Fraction(this.numerator * f1.numerator, this.denominator * f1.denominator);
        }

        public Fraction Multiply(int i)
        {
            return this.Multiply(new Fraction(i));
        }

        public Fraction Multiply(double i)
        {
            return this.Multiply(new Fraction(i));
        }
        #endregion

        #region Method Divide

        public Fraction Divide(Fraction f1)
        {
            return this.Multiply(f1.Reciprocal());
        }

        public Fraction Divide(int i)
        {
            return this.Divide(new Fraction(i));
        }

        public Fraction Divide(double i)
        {
            return this.Divide(new Fraction(i));
        }
        #endregion
        
        #endregion
                      
        #region overloaded operators
        // Readmore about overloading operators @ https://msdn.microsoft.com/de-de/library/6fbs5e2h.aspx
        #region operator +
        public static Fraction operator +(Fraction f1, Fraction f2)
        {
            return new Fraction((f1.numerator * f2.denominator + f1.denominator * f2.numerator), f1.denominator * f2.denominator);
        }

        public static Fraction operator +(Fraction f1, int i)
        {
            return f1 + new Fraction(i);
        }

        public static Fraction operator +(int i, Fraction f1)
        {
            return f1 + i;
        }

        public static Fraction operator +(Fraction f1, double i)
        {
            return f1 + new Fraction(i);
        }

        public static Fraction operator +(double i, Fraction f1)
        {
            return f1 + new Fraction(i);
        } 
        #endregion

        #region operator -
        public static Fraction operator -(Fraction f1, Fraction f2)
        {
            return new Fraction(((f1.numerator * f2.denominator) - (f1.denominator * f2.numerator)), f1.denominator * f2.denominator);
        }

        public static Fraction operator -(Fraction f1, int i)
        {
            return f1 - new Fraction(i);
        }

        public static Fraction operator -(int i, Fraction f1)
        {
            return f1 - new Fraction(i);
        }

        public static Fraction operator -(Fraction f1, double i)
        {
            return f1 - new Fraction(i);
        }

        public static Fraction operator -(double i, Fraction f1)
        {
            return f1 - new Fraction(i);
        } 
        #endregion

        #region operator *
        public static Fraction operator *(Fraction f1, Fraction f2)
        {
            return new Fraction(f1.numerator * f2.numerator, f1.denominator * f2.denominator);
        }

        public static Fraction operator *(Fraction f1, int i)
        {
            return new Fraction(f1 * new Fraction(i));
        }

        public static Fraction operator *(int i, Fraction f1)
        {
            return new Fraction(f1 * new Fraction(i));
        }

        public static Fraction operator *(Fraction f1, double i)
        {
            return f1 * new Fraction(i);
        }

        public static Fraction operator *(double i, Fraction f1)
        {
            return f1 * new Fraction(i);
        } 
        #endregion

        #region operator /
        public static Fraction operator /(Fraction f1, Fraction f2)
        {
            return new Fraction(f1 * f2.Reciprocal());
        }

        public static Fraction operator /(Fraction f1, int i)
        {
            return new Fraction(f1 / new Fraction(i));
        }

        public static Fraction operator /(int i, Fraction f1)
        {
            return new Fraction(f1 / new Fraction(i));
        }

        public static Fraction operator /(Fraction f1, double i)
        {
            return f1 / new Fraction(i);
        }

        public static Fraction operator /(double i, Fraction f1)
        {
            return f1 / new Fraction(i);
        }  
        #endregion
        #endregion

        public Fraction Reciprocal()
        {
            return new Fraction(this.denominator, this.numerator);
        }

        public double Evaluate()
        {                   
            return (double)this.numerator / this.denominator;
        }
            
        public double Evaluate(int lenght)
        {
            if (lenght <= exponent)
                return Math.Round((double)this.numerator / this.denominator, lenght);
            return Math.Round((double)this.numerator / this.denominator, lenght = exponent);
        }

        #region overide ToString()
        // Read more about ToString with IFromattable at https://msdn.microsoft.com/de-de/library/system.iformattable%28v=vs.110%29.aspx
        
        public override string ToString()
        {
            return this.ToString("F");
        }

        public string ToString(string format)
        {
            if (String.IsNullOrEmpty(format)) format = "F";
            return this.ToString(format, CultureInfo.CurrentCulture);
        }
                    
        public string ToString(string format, IFormatProvider formatProvider)
        {

            if (String.IsNullOrEmpty(format)) format = "F";
            if (formatProvider == null) formatProvider = CultureInfo.CurrentCulture;

            switch (format.ToUpperInvariant())
            {
                case "F":
                    if (denominator != 1)
                        return numerator + "/" + denominator;
                    return this.numerator + "";

                case "M":
                    if (numerator / denominator != 0)
                        return numerator / denominator + " " + numerator % denominator + "/" + this.denominator;
                    return this.ToString();

                default:
                    throw new FormatException(String.Format("The {0} format string is not supported. Please choose F (Fraction Format) or M (Mixed Numbers Fromat)", format));
            }
        }

        #endregion

    }
}

8 Kommentare zum Snippet

Koopakiller schrieb am 12.11.2015:
Einige Teile deines Codes ergeben für mich nicht wirklich Sinn bzw. arbeiten vollkommen anders als typische .NET Klassen. Warum beispielsweise erzeugst du in der Divide Methode 2mal eine Instanz der Fraction Klasse? Weiterhin, gibt die Reciprocal Methode den alten Wert zurück und ändert die Instanz. In .NET wäre es typischerweise anders herum.
Auch solltest du Eigenschaften statt Get-Methoden in .NET schreiben.

Um für den Start beim Programmieren etwas wichtiges zu zeigen wäre es hier schön zu sehen wenn du noch die Operatoren überlädst.

Übrigens nutzt man für solche Typen eher Strukturen als Klassen (vgl. BigInteger etc.).
johndoe schrieb am 13.11.2015:
Hallo Koopakiller,
ja, das stimmt. Ursprünglich war das mal ein Java Programm, welches ich ziemlich direkt übersetzt habe. :D Sah ziemlich messy aus...
Ich habe die Klasse nochmal überarbeitet und deine Tipps eingepflegt.

Wie kann ich das jetzt noch "elegant" lösen, dass ich nicht nur
a + 1 schreiben kann, sondern auch 1 + a. Muss man für diese Variante wieder eine Überladung schreiben?

Vielen Dank erstmal für de Tipps!
Koopakiller schrieb am 13.11.2015:
Ja, Operatoren sind in der Hinsicht manchmal etwas nervig mit den vielen benötigten Überladungen. Du musst also wirklich 2 machen. Wobei ich es bei Fällen wie dem immer so handhabe das Einer einfach den Anderen aufruft.
In C# 6 ist die 2. Überladung zum Glück recht schlank:
public static Fraction operator +(double x, Fraction t) => t + x;


Um voll kompatibel zu allen Programmiersprachen zu ein soll man laut MS aber trotzdem noch eine Add-Methode (usw.) bereitstellen. Es könnte schließlich sein dass die andere Sprache die Operatoren nicht hat.

Eines noch, warum rundest du in Evaluate? Ich würde das Ergebnis ungerundet zurück geben, so kann man von außen bestimmen wie exakt es sein soll. Wenn man dagegen eine theoretisch beliebig exakt funktionierende Fraction-Klasse schreieben wöllte, so würde ich einen Parameter angeben wie viele Dezimalstellen ermittelt werden sollen. Das wäre dann aber wirklich viel Arbeit (ich weiß wovon ich rede...)

Übrigens, warum versteckst du den Zähler/Nenner? Ich würde die als öffentliche Eigenschaft (dann aber groß geschrieben) deklarieren. So kann man auch von außen zugreifen wer das mag/braucht.

Und wenn du lange Weile hast, dann kannst du dein bereits sehr solides Grundgerüst um Kompatibilität zu Double erweitern. Also das Addieren etc. ermöglichen und auch die Ex-/Impliziten Konvertierungen.
Das musst du natürlich nicht machen, ist nur eine Idee wie man die Klasse erweitern kann.
johndoe schrieb am 15.11.2015:
Super, ich werde deine Vorschläge bei Zeiten ergänzen!
Stimmt, bei Evaluate wäre es ein Mehrwert, wenn man selber entscheiden könnte. Ich habe mir überlegt, dass für den Standardfall vermutlich zwei Stellen reichen. Wenn man mit den Werten weiterrechnen will, ist man da aber schnell am Ende. Warum ist das soviel Arbeit, das wie von dir vorgeschlagen umzusetzen? Weil die Anzahl an Nachkommmastellen bei Double begrenzt ist?
Die Zähler und Nenner habe ich private gemacht, weil ich gelernt habe, das man grundsätzlich alles so privat wie möglich macht um Manipulation zu vermeiden. Durch die ToString Methode kann man sich die Sachen ja ausgeben lassen. Und neue Werte kann man auch einfach zuweisen. Oder gibt es andere Gründe, die gegen "Private" sprechen?! Ich habe leider nicht viel Ahnung von Standards in der Programmierung.
Ich habe mir überlegt, das es auch sinnvoll sein könnte, wenn man bei der Ausgabe wählen könnte, wie der Bruch dargestellt wird. Also zum Beispiel könnte man 7/3 auch als 2 1/3 ausgeben. Vielleicht finden einige Menschen diese Schreibsweise besser lesbar?!
Koopakiller schrieb am 15.11.2015:
Das entgegen nehmen eines Paramaters in Evaluate der die Anzahl an Nachkommastellen angibt ist erstmal nicht weiter schwer. Wenn du allerdings eine Höhere Genauigkeit erreichen willst, wird es mit Double schwierig. Double hat eine Genauigkeit von 15 bis 16 Stellen, Decimal bis zu 27. Mit Zwei Integern kannst du dagegen auch noch mehr Nachkommastellen darstellen, wenn auch bei weitem nicht alle möglichen Kombinationen.
Es gibt einige Hersteller solcher Bruch-Klassen, die intern BigInteger verwenden. Dort hast du dann wirklich eine beliebige Genauigkeit gegeben. Evaluate gibt in diesen Fällen häufig eine String-Darstellung zurück. Diese wird in etwa so ermittelt, wie man es beim schriftlichen Dividieren tun würde. Das ist zwar ein machbares verfahren, aber auch nicht wenig Code. Fehleranfällig ist er so oder so.
Das Verwenden von BigInteger und das Implementieren des Evaluate Algorithmus empfand ich als gute Übung, es war aber einige Arbeit es effizient zu lösen. (Ich habe selbst viele solche Typen geschrieben.)

Das alles erstmal so privat wie möglich sein sollte ist richtig. Ich habe oben aber auch schon mal angedeutet dass man für Fraction vermutlich eher eine Struktur anstelle einer Klasse verwenden würde.
Strukturen sind per Definition unveränderlich, das heißt dass du im Konstruktor Daten entgegen nehmen kannst, diese dann aber nicht mehr per Eigenschaft oder Methode von außen veränderbar sind. Deine Eigenschaften könnten dann noch einen public Getter, aber nur einen privaten Setter haben. Im Umkehrschluss heißt die Unveränderlichkeit aber auch dass Reciprocal und alle anderen Methoden nicht die Eigenschaften verändern, sondern jeweils eine neue Instanz von Fraction erzeugen und zurück geben müssen. Es verhält sich dann alles wie beim String-Typ.

Verschiedene Ausgabeformate für ToString zu haben ist immer gut. Gucke die mal die Schnittstelle IFormattable an. Diese schreibt eine ToString-Überladung mit einem String-Parameter vor. Wenn du beispielsweise "f" übergibst könntest du den Bruch zurück geben, bei "d" das Dezimale Ergebnis usw.
johndoe schrieb am 18.11.2015:
Alles klar, das erklärt die Aufwendigkeit! Danke, für die ausführliche Erklärung, dass verdeutlicht die Komplexität sehr schön.
Den Getter public und den Setter Private zu machen, hat mein Professor auch in seiner Vorlesung umgesetzt. Ich dachte nur, dass mehr private dann auch "mehr" Sicherheit bietet, weil ich ja alles bekomme, was ich brauche. Aber vor dem von Dir geschilderten Hintergrund, macht das auf jedenfall Sinn, das so zu machen.
Ich werde vermutlich erst am Donnerstag wieder Zeit haben, die Klasse weiter auszubauen.
Nochmal Danke für die ausführlichen Schilderungen. Hat mich voran gebracht und vermutlich auch viele andere, die sich mit dem Fraction Problem beschäftigen!
johndoe schrieb am 20.11.2015:
Ich habe die Fraction Klasse (im Rahmen meiner Möglichkeiten :D) um double Erweitert und IFormattable eingebunden.
Die Methoden sind jetzt auch wieder eingefügt. Ich habe das jetzt so verstanden, dass ich die überladenen Operatoren in z.B. der Add Methode nicht aufrufen kann, weil es einige Sprachen, das nicht unterstützen. Ist das richtig?
Die Operatoren habe ich auch noch ergänzt. Leider hat die Schreibweise
public static Fraction operator +(double x, Fraction t) => t + x;

nicht funktioniert. Ich habe mich über diese Lambda Expression gelesen und auch mal die System.Linq eingebunden, leider ohne Erfolg. Ich hab´s dann letztenendes einzeln aufgeschrieben. Vielleicht hat ja jemand einen Tipp, was ich beachten sollte!?
Evaluate habe ich auch um den Kommastellen Parameter ergänzt. Das mit dem beliebig genau habe ich aber erstmal aufgeschoben... :D

Koopakiller schrieb am 03.12.2015:
Entschuldige bitte meine Späte Rückmeldung. Deine Antwortkommentare gingen leider etwas unter.
Das warum du die Methoden und die Operatoren haben solltest ist richtig. Moderne Sprachen dürften wohl immer die Grundlegendsten Operatoren unterstützen, aber man weiß ja nie.
Wenn die Syntax mit => nicht funktioniert, dann nutzt du wahrscheinlich noch keinen C# 6 Compiler. Daher muss hier dann wirklich die klassische Syntax her.
 

Logge dich ein, um hier zu kommentieren!