diff --git a/aufgaben/sheet4/README.md b/aufgaben/sheet4/README.md new file mode 100755 index 0000000..19cb4a9 --- /dev/null +++ b/aufgaben/sheet4/README.md @@ -0,0 +1,10 @@ +Blatt 4 +======= + +Wie immer soll dieses Blatt in einem eigenen Branch, `sheet4`, bearbeitet +werden. + +Eine weitere generelle Aufgabe: Euer erster Commit soll *nur* die Dateien +beinhalten, die euch schon (in diesem Repository) gegeben werden. So fällt es +den Tutoren und mir deutlich leichter, den vorgegebenen Teil und eure eigenen +Änderungen zu unterscheiden! diff --git a/aufgaben/sheet4/task1/README.md b/aufgaben/sheet4/task1/README.md new file mode 100755 index 0000000..5657bb8 --- /dev/null +++ b/aufgaben/sheet4/task1/README.md @@ -0,0 +1,15 @@ +Aufgabe 4.1: Programm schöner machen +==================================== + +In dieser Aufgabe dürft *ihr* mal die Aufgabe der Tutoren übernehmen. Gegeben +ist ein Programm, das funktioniert und das tut, was es soll. Es gibt alle +Zahlen zwischen 1 und 20, die sowohl eine Primzahl als auch eine ["fröhliche" +Zahl](https://en.wikipedia.org/wiki/Happy_number) sind, aus. + +Jedoch ist der Quellcode alles andere als hübsch. Eure Aufgabe ist es, den +Quellcode zu verbessern. D.h., ihr sollt die Funktionsweise des Programms gar +nicht verändern, sondern nur den Quellcode. + +Versucht, den Code möglichst kurz, gut lesbar und idiomatisch zu gestalten. +So viel sei gesagt: Es gibt *sehr viele* Stellen, an denen etwas verbessert +werden kann! Versucht möglichst, alle dieser Stellen zu finden. diff --git a/aufgaben/sheet4/task1/bad.rs b/aufgaben/sheet4/task1/bad.rs new file mode 100755 index 0000000..ea37906 --- /dev/null +++ b/aufgaben/sheet4/task1/bad.rs @@ -0,0 +1,69 @@ +// print if they are both +fn main() -> () { + for iterationnumber in &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] { + let iterationnumber = *iterationnumber; + if !happy_prime(iterationnumber) {} else { + print!("{}", iterationnumber); + println!(" is a happy prime!"); + } + } + +} + +// is it botH? +fn happy_prime(n: i32) -> bool { + match check_if_number_is_happy(n) { + false => return false, + _ => {} + } + + if check_if_number_is_prime(n) == true { + return true; + } else { + return false; + } +} + +// Is it a happy number? https://en.wikipedia.org/wiki/Happy_number +fn check_if_number_is_happy(number: i32) -> bool { + let mut number: i32= number; + + while number > 1 { + let mut tmp = 0; + while number > 0 { + tmp = tmp + (number %10) * (number%10); + number = number / 10; + } + number = tmp; + + // We ended up in a cycle -> not happy + if (number == 4) { + return false; + } + } + + return true; +} + +// is it priem? +fn check_if_number_is_prime(n: i32) -> bool { + if n == 1 { + return false; + } + + if n == 2 { + return false; + } + + let mut teilerGefunden:bool = false; + + let mut teiler:i32= 2; + while (teiler < n) { + if (n % teiler == 0) { + teilerGefunden = true; + } + teiler = teiler + 1; + } + + return !teilerGefunden; +} diff --git a/aufgaben/sheet4/task2/README.md b/aufgaben/sheet4/task2/README.md new file mode 100755 index 0000000..37e1151 --- /dev/null +++ b/aufgaben/sheet4/task2/README.md @@ -0,0 +1,219 @@ +Aufgaben 4.2: Taschenrechner +============================ + +In dieser Aufgabe sollt ihr ein Programm schreiben, welches einfache +Rechenaufgaben lösen kann. +Zwar kann man sich einen sehr einfachen Rechner schnell manuell +zusammenschreiben. +Das Programm dieser Aufgabe soll jedoch nicht quick'n'dirty geschrieben werden, +sondern später einfach zu erweitern sein. + +Ein Teil des Codes ist bereits in `calculator.rs` vorgegeben. +Das gegebene Programm liest in einer Endlosschleife Eingaben vom Nutzer ein, +tut damit aber bislang noch nichts, außer sie auszugeben. +Das Programm kann man bekanntlich mit Strg+C beenden. + +Das Programm soll später ungefähr folgende Aufgaben lösen können: + +``` +calc > 3 + 3 +6 +calc > 2-3 +-1 +calc > 9 + (7 - 2) +14 +calc > 2 +2 +calc > (3 - 2) + ((3 - 2) + 9) +11 +``` + + +a) Tokenization +--------------- + +Die ersten beiden Schritte fast aller Compiler jeglicher Art sind: +*Tokenization* (auch *Lexing*) und *Parsing*. +Gerade Compiler haben einen extrem komplizierten und komplexen Job. +Um dieser Komplexität Herr zu werden, werden eine Reihe von unterschiedlichen +Abstraktionsstufen genutzt. +Der Quellcode wird permanent von einer Darstellung in die nächste überführt; +Analysen, wie Typechecking ("werden alle Typen korrekt benutzt?"), werden nie +direkt auf dem Quellcode-String, sondern auf abstrakteren Darstellungen +ausgeführt. + +Glücklicherweise müsst ihr keinen Compiler schreiben, sondern nur einen +Taschenrechner. +Aber auch dabei müssen Nutzereingaben mit einer bestimmten Syntax analysiert +werden. +Daher werden wir die Funktionalität des Rechners ebenfalls in drei Schritte +unterteilen: + +1. Tokenization/Lexing +2. Parsing +3. Errechnen des Ergebnisses + +Im ersten Schritt wird der Eingabestring in eine Liste von sog. *Tokens* +umgewandelt. Ein Token kann ein Zeichen repräsentieren (wie z.B. `"+"`) oder +auch mehrere (wie z.B. `"127"`). +Anstatt also eine Liste von Zeichen zu betrachten, betrachtet man eine Liste +von atomaren Elementen im Code. +Um mal ein Beispiel zu geben: + +``` +Eingabe: "23 + 7" + + | | | +Tokens: [Number(23), Plus, Number(7)] +``` + +Eure erste Aufgabe ist es, einen Typen `Token `zu definieren, der einen Token +darstellt. +Der Tokenization-Schritt produziert nämlich dann `Vec`. +Wie oben schon gezeigt, muss der Rechner in der Lage sein, Addition und +Subtraktion von beliebigen positiven Ganzzahlen auszuführen (das Ergebnis kann +negativ sein, nur die einzelnen Zahlen in der Eingabe nicht). +Außerdem müssen Klammerausdrücke behandelt werden. +Entwerft entsprechend den `Token` Typ. + +--- + +Nachdem der Typ definiert ist, müsst ihr jetzt eine Funktion `tokenize()` +schreiben. +Diese Funktion bekommt einen String und erzeugt daraus eine Liste von Tokens. +Natürlich können schon bei der Tokenization Fehler im Input auffallen. +Diese Fehler sollten nicht einfach ignoriert werden. + +Die Funktion soll außerdem Whitespaces in der Eingabe komplett ignorieren, dafür +also keinen Token erstellen (das würde in späteren Schritten nur ablenken). + +*Wichtig*: Ihr müsst lediglich mit Zahlen umgehen können, die nur +*eine* Ziffer besitzen. +Zahlen bestehend aus mehreren Ziffern zu lexen, ist -- selbst mit Hilfe von +der `parse()` Funktion aus der Standardbibliothek -- nicht ganz trivial. +Wer das trotzdem schafft, wird natürlich vom Tutor gelobt ;-) + + +Schließlich soll die Nutzereingabe an `tokenize()` gegeben werden und das +Ergebnis zu Debugzwecken in `main()` ausgegeben werden. +Folgende Eingaben können getestet werden: + +- `3` +- `3 + 4` +- `3 - (7 - 7)` +- `3 + ) - (` -> (in diesem Schritt führt das *noch nicht* zum Fehler!) +- `peter` -> Hier sollte ein Fehler ausgegeben werden (nicht panic!) + +**Tipps**: + +- Es lohnt sich, die Dokumentation von + [`char`](https://doc.rust-lang.org/std/primitive.char.html) durchzulesen +- Um Instanzen selbstdefinierter Typen miteinander vergleichen zu können, kann + man `#[derive(PartialEq)]` über die Typdefinition schreiben. Dann kann der + Typ per `==` und `!=` verglichen werden. + +b) Definition des Parsetrees +---------------------------- + +Bisher haben wir aus einem String einen Tokenstream gemacht. +Im Parsing-Schritt wird aus dem Tokenstream ein sog. Parsetree erzeugt. +Doch bevor wir uns an das Parsing wagen, müssen wir erstmal Typen definieren, +um den Parsetree darzustellen. + +Hier ist mal ein Beispiel eines Parsetrees zu der Eingabe `3 + (6 - 1)` zu +sehen: + +``` + +---------+ + | Summe | + +---------+ + / \ + / \ + +-----+ +-----------+ + | 3 | | Differenz | + +-----+ +-----------+ + / \ + / \ + +-----+ +-----+ + | 6 | | 1 | + +-----+ +-----+ +``` + +Wir sehen also: + +- Blätter des Baumes stellen die Literale/eingegebenen Zahlen dar +- Innere Knoten des Baumes wissen, welche Art von Rechenoperation sie + darstellen und welche Kinder sie besitzen + +Erstellt zunächst einen Typ `Op`, welcher eine Rechenoperation darstellt. +Dieser Typ soll dann eine Methode `apply()` bekommen. +Diese Methode bekommt zwei Zahlen und gibt das Ergebnis der Operation +angewendet auf diese beiden Zahlen zurück. + +Danach soll der Typ `Expr` erstellt werden, der letztendlich den Parsetree +darstellt. +*Tipp*: Auch wenn wir immer nur zwei Kinder pro Knoten erwarten, könnt ihr hier +gerne `Vec<_>` benutzen! + +Dieser Typ soll eine Methode `evaluate()` bekommen, welche den ganzen Baum +auswertet (also das Ergebnis ausrechnet). +Die Methode würde also auf dem oben gezeigten Baum "8" zurückgeben. +Das ist also Schritt 3 aus der oben genannten Liste von Schritten. + +Baut euch in einer kleinen Hilfsfunktion manuell eine Instanz (z.B. den oben +gezeigten Baum) von `Expr` zusammen und testet `evaluate()` auf dieser Instanz. +Ruft diese Hilfsfunktion in `main()` auf, um das Ergebnis zu prüfen. +Löscht diese Hilfsfunktion auch nicht, sodass euer Tutor sie später nutzen +kann. + + +c) Parsing +---------- + +Dieser zweite Schritt ist der komplizierteste. +Ihr müsst diese Teilaufgabe nicht komplett lösen, da die komplette Lösung +nicht gerade trivial ist. +Mindestanforderung ist jedoch, dass ihr die folgenden Eingaben parsen könnt: + +- `3` +- `1 + 2` +- `9 - 6` + +Das Parsing soll in einer Konstruktorfunktion von `Expr` geschehen. +Nennen wir diese Funktion einfach `parse()`. +Die Funktion bekommt natürlich den vorher generierten Tokenstream und soll +bei Erfolg eine `Expr`-Instanz zurückgeben, welche den Parsetree darstellt. + +Aber natürlich kann das Parsing auch auf Fehler in der Eingabe stoßen. +Zum Beispiel sollten Fehler wie eine leere Eingabe und eine falsche Syntax +gemeldet werden. + +Ein Hinweis zur Syntax: Wir gehen davon aus, dass jeder Knoten im Baum nur zwei +Kinder hat und Rechnungen immer geklammert sind. +Eine Eingabe von `3 + 4 + 5` ist also ungültig und müsste als `3 + (4 + 5)` +oder `(3 + 4) + 5` eingegeben werden. +Das vereinfacht das Parsing stark. +Unsere Grammatik ist also: + +``` +expr := ⟨operand⟩ [⟨op⟩ ⟨operand⟩] +operand := ⟨num⟩ | "(" ⟨expr⟩ ")" +op := "+" | "-" +num := "0" | "1" | ... | "9" +``` + +*Tipps*: + +- Wenn ihr versucht, den kompletten Parser zu implementieren, bietet + sich natürlich eine Art von Rekursion an. +- Es gibt recht viele Wege, diesen Parser zu implementieren. Ein Weg, den ich + recht bequem finde: Man speichert sich den aktuellen Index im Tokenstream + und erhöht diesen immer, wenn man weitere Tokens verarbeitet hat. +- Kleine Hilfsfunktionen können doppelten Code vermeiden. + + +--- + +Zuletzt (unabhängig davon, ob ihr nur den halben oder ganzen Parser +implementiert habt) soll alles in `main()` benutzt werden, um den +Taschenrechner funktionsfähig zu machen. diff --git a/aufgaben/sheet4/task2/calculator.rs b/aufgaben/sheet4/task2/calculator.rs new file mode 100755 index 0000000..e2d75b2 --- /dev/null +++ b/aufgaben/sheet4/task2/calculator.rs @@ -0,0 +1,35 @@ + +fn main() { + loop { + // Read input from the user and just do nothing when the input is empty + let input = read_string(); + if input.is_empty() { + continue; + } + + // Debug output + println!("{}", input); + } +} + + +/// Reads a string from the user (with a nice prompt). +fn read_string() -> String { + use std::io::Write; + + // Print prompt + print!("calc > "); + std::io::stdout().flush().unwrap(); + + // Read line + let mut buffer = String::new(); + std::io::stdin() + .read_line(&mut buffer) + .expect("something went horribly wrong..."); + + // Discard trailing newline + let new_len = buffer.trim_right().len(); + buffer.truncate(new_len); + + buffer +}