Add initial implementation of ExamCalc application with UI components and grading logic

This commit is contained in:
tiko
2026-02-26 09:55:26 +01:00
commit 8918de4219
18 changed files with 1557 additions and 0 deletions

55
build.gradle.kts Normal file
View File

@@ -0,0 +1,55 @@
plugins {
id("java")
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = "com.victorpyra"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
tasks.jar {
manifest {
attributes["Main-Class"] = "com.github.victorpyra.calc.ExamCalcApp"
}
}
tasks.javadoc {
// Bestimmt das Zielverzeichnis (standardmäßig build/docs/javadoc)
setDestinationDir(file("${layout.buildDirectory.get()}/docs/javadoc"))
// Konfiguration des Javadoc-Tools
(options as StandardJavadocDocletOptions).apply {
// Nutzt UTF-8, damit Umlaute in deinen deutschen Texten korrekt angezeigt werden
encoding = "UTF-8"
charSet = "UTF-8"
docEncoding = "UTF-8"
// Fügt nützliche Links zur Standard-Java-Bibliothek hinzu
links("https://docs.oracle.com/en/java/javase/17/docs/api/")
// Erzeugt eine übersichtliche Struktur
addStringOption("Xdoclint:none", "-quiet")
isAuthor = true
isVersion = true
windowTitle = "IHK Notenberechner API"
}
}
application {
mainClass.set("com.github.victorpyra.calc.ExamCalcApp")
}

9
grades.storage Normal file
View File

@@ -0,0 +1,9 @@
#IHK Calc Auto-Save
#Thu Feb 26 09:21:47 CET 2026
AP1=3
AP2_DOCUMENTATION=1
AP2_PART_1=4
AP2_PART_2=5
AP2_PART_3=2
AP2_PRESENTATION=4
AP2_TECHNICAL_DISCUSSION=2

1
settings.gradle.kts Normal file
View File

@@ -0,0 +1 @@
rootProject.name = "pruefungsrechner"

View File

@@ -0,0 +1,32 @@
package com.github.victorpyra.calc;
import com.github.victorpyra.calc.ui.MainFrame;
/**
* Die Klasse {@code ExamCalcApp} bildet den Haupteinstiegspunkt der Applikation.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie ist für die Initialisierung und den Start der grafischen Benutzeroberfläche zuständig. Hier wird das Hauptfenster
* instanziiert und dem Benutzer angezeigt.
* </p>
*
* @author Victor Pyra
* @version 1.0.0
*/
public final class ExamCalcApp {
/**
* Die Main-Methode startet die Anwendung.
*
* <p>
* Sie erzeugt eine Instanz des Hauptfensters mit dem Titel "IHK-Notenberechner" und ruft die Methode zum Öffnen des
* Frames auf.
* </p>
*
* @param args Die beim Programmstart übergebenen Argumente (werden nicht verwendet).
*/
public static void main(String[] args) {
// Erstellt eine neue Instant des MainFrames mit dem Titel "IHK-Notenberechner" und öffnet diesen Frame von alleine.
new MainFrame("IHK-Notenberechner", true);
}
}

View File

@@ -0,0 +1,138 @@
package com.github.victorpyra.calc.grading;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.result.ExamRecord;
import com.github.victorpyra.calc.result.ExamResult;
/**
* Der Record {@code Grading} ist für die Berechnung der Gesamtnote verantwortlich.
*
* <p>
* Er wertet einen {@link ExamRecord} aus, prüft die Bestehensregeln der IHK-Verordnung
* und formatiert das Ergebnis für die Anzeige in der Benutzeroberfläche.
* </p>
*
* @param examRecord Der Datensatz mit den Prüfungsergebnissen, die bewertet werden sollen.
*
* @author Victor Pyra
* @version 1.0
*/
public record Grading(ExamRecord examRecord) {
/**
* Die Anzahl an Prüfungsergebnissen, die für eine vollständige Berechnung notwendig sind.
*/
public static final int NEEDED_EXAM_RESULTS = 7;
/**
* Der Schwellenwert für die Note, bis zu dem eine Prüfung als bestanden gilt
*/
private static final double PASS_THRESHOLD_GRADE = 4.4;
/**
* Führt die Notenberechnung durch und prüft alle Bestehenskriterien.
*
* <p>
* Die Methode validiert zuerst die Vollständigkeit der Daten und prüft auf Ausschlusskriterien
* wie die Note 6.0. Anschließend werden die gewichteten Noten für AP1 und AP2 ermittelt.
* </p>
*
* @return Ein {@code String} mit der Erfolgsmeldung und der Note oder einer Fehlerbeschreibung.
*/
public String calculate() {
if (!examRecord.gradable()) {
return "Nicht berechenbar.\nMind. 1 Prüfungsteil fehlt die Note von.";
}
if (examRecord.terminated()) {
return "Du hast nicht bestanden!\nGrund: 6.0 in einer Teilprüfung erreicht.";
}
ExamResult ap1 = examRecord.find(Exam.AP1);
double ap1Total = weightedGrade(ap1);
double ap2Total = calculateAp2Total();
double totalGrade = ap1Total + ap2Total;
if (!isPassing(totalGrade, ap2Total)) {
return "Nicht bestanden!";
}
return formatPassResult(totalGrade, ap1Total, ap2Total, ap1);
}
/**
* Überprüft, ob die berechneten Noten den Bestehensregeln entsprechen.
*
* @param totalGrade Die berechnete Gesamtnote über alle Teile.
* @param ap2Total Die gewichtete Summe der Ergebnisse aus Teil 2.
* @return {@code true}, wenn alle Kriterien für das Bestehen erfüllt sind.
*/
private boolean isPassing(double totalGrade, double ap2Total) {
return totalGrade <= PASS_THRESHOLD_GRADE
&& ap2Total <= PASS_THRESHOLD_GRADE
&& countResultsBelowOrEqual(PASS_THRESHOLD_GRADE) >= 3;
}
/**
* Erstellt eine formatierte Nachricht für das Bestehen der Prüfung.
*
* @param totalGrade Die erreichte Gesamtnote.
* @param ap1Total Der gewichtete Anteil von AP1.
* @param ap2Total Der gewichtete Anteil von AP2.
* @param ap1 Das {@code ExamResult}-Objekt von AP1 für Zugriff auf Formatierung.
* @return Der formatierte Ergebnis-String.
*/
private String formatPassResult(
double totalGrade,
double ap1Total,
double ap2Total,
ExamResult ap1
) {
return String.format(
"""
Du hast bestanden!
Deine Gesamtnote beträgt: %.2f
Sie setzt sich aus der AP1: %s und AP2: %s zusammen.
""",
totalGrade, ap1.format(ap1Total), ap1.format(ap2Total)
);
}
/**
* Berechnet die gewichtete Gesamtsumme für alle Prüfungsteile der AP2.
*
* @return Die Summe der gewichteten AP2-Noten.
*/
private double calculateAp2Total() {
return examRecord.results()
.stream()
.filter(result -> result.exam() != Exam.AP1)
.mapToDouble(this::weightedGrade)
.sum();
}
/**
* Zählt die Anzahl der Prüfungsteile in AP2, die eine bestimmte Mindestnote erreichen.
*
* @param maxGrade Die maximale Note, die noch als bestanden/ausreichend zählt.
* @return Die Anzahl der erfolgreichen Prüfungsteile.
*/
private long countResultsBelowOrEqual(double maxGrade) {
return examRecord.results()
.stream()
.filter(result -> result.exam() != Exam.AP1)
.filter(unit -> unit.grade() <= maxGrade)
.count();
}
/**
* Berechnet die gewichtete Note für ein einzelnes Prüfungsergebnis.
*
* @param result Das zu gewichtende Ergebnis.
* @return Das Ergebnis multipliziert mit seinem Gewichtungsfaktor.
*/
private double weightedGrade(ExamResult result) {
return result.grade() * result.exam().gradeTotal();
}
}

View File

@@ -0,0 +1,83 @@
package com.github.victorpyra.calc.result;
/**
* Das Enum {@code Exam} repräsentiert die verschiedenen Prüfungsbestandteile der Abschlussprüfung.
*
* <p>
* Jedes Element definiert sowohl eine lokalisierte Bezeichnung als auch die jeweilige
* Gewichtung für die Berechnung der Gesamtnote. Dies ermöglicht eine strukturierte
* Handhabung der Prüfungsteile innerhalb der Berechnungslogik.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public enum Exam {
/**
* Die erste Abschlussprüfung (Teil 1), gewichtet mit 20 %.
*/
AP1("Abschlussprüfung 1", 0.2),
/**
* Der erste schriftliche Teil der Abschlussprüfung 2, gewichtet mit 10 %.
*/
AP2_PART_1("AP2 Teil 1 (Schriftlich)", 0.1),
/**
* Der zweite schriftliche Teil der Abschlussprüfung 2, gewichtet mit 10 %.
*/
AP2_PART_2("AP2 Teil 2 (Schriftlich)", 0.1),
/**
* Der dritte schriftliche Teil der Abschlussprüfung 2 (WiSo), gewichtet mit 10 %.
*/
AP2_PART_3("AP2 Teil 3 (Wirtschafts- und Sozialkunde)", 0.1),
/**
* Die Dokumentation der praktischen Arbeit in AP2, gewichtet mit 25 %.
*/
AP2_DOCUMENTATION("AP2 Dokumentation (Praktisch)", 0.25),
/**
* Die Präsentation der praktischen Arbeit in AP2, gewichtet mit 12,5 %.
*/
AP2_PRESENTATION("AP2 Präsentation (Praktisch)", 0.125),
/**
* Das fachliche Gespräch zur praktischen Arbeit in AP2, gewichtet mit 12,5 %.
*/
AP2_TECHNICAL_DISCUSSION("AP2 Fachgespräch (Praktisch)", 0.125);
private final String localizedName;
private final double gradeTotal;
/**
* Erstellt einen neuen Prüfungsbestandteil.
*
* @param localizedName Die Anzeigebezeichnung des Prüfungsteils.
* @param gradeTotal Die Gewichtung des Teils an der Gesamtnote (0.0 bis 1.0).
*/
Exam(
final String localizedName,
final double gradeTotal
) {
this.localizedName = localizedName;
this.gradeTotal = gradeTotal;
}
/**
* Gibt die lokalisierte Bezeichnung des Prüfungsteils zurück.
* * @return die Bezeichnung als {@code String}.
*/
public String localizedName() {
return localizedName;
}
/**
* Gibt die Gewichtung dieses Prüfungsteils für die Gesamtnote zurück.
* * @return der Gewichtungsfaktor als {@code double}.
*/
public double gradeTotal() {
return gradeTotal;
}
}

View File

@@ -0,0 +1,106 @@
package com.github.victorpyra.calc.result;
import com.github.victorpyra.calc.grading.Grading;
import java.util.ArrayList;
import java.util.List;
/**
* Die Klasse {@code ExamRecord} verwaltet eine Sammlung von Prüfungsergebnissen für einen Prüfling.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie bietet Mechanismen zum Hinzufügen, Suchen und Validieren von Ergebnissen. Die Klasse stellt
* sicher, dass keine doppelten Prüfungen abgelegt werden und erkennt, ob alle notwendigen
* Leistungen für eine Benotung vorliegen.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class ExamRecord {
private final List<ExamResult> results;
/**
* Initialisiert eine neue, leere Ergebnisliste.
*/
private ExamRecord() {
this.results = new ArrayList<>();
}
/**
* Erstellt eine neue Instanz von {@code ExamRecord}.
*
* @return Eine frische Instanz dieser Klasse.
*/
public static ExamRecord create() {
return new ExamRecord();
}
/**
* Prüft intern, ob für einen bestimmten Prüfungsteil bereits ein Ergebnis vorliegt.
*
* @param exam Der zu prüfende Prüfungsteil.
* @return {@code true}, wenn bereits ein Ergebnis existiert, andernfalls {@code false}.
*/
private boolean alreadyTakenExam(Exam exam) {
return results.stream().anyMatch(entry -> entry.exam() == exam);
}
/**
* Fügt ein neues Prüfungsergebnis hinzu, sofern die Maximalanzahl noch nicht erreicht
* und die Prüfung nicht bereits im Datensatz vorhanden ist.
*
* @param result Das hinzuzufügende {@code ExamResult}.
*/
public void add(ExamResult result) {
if (gradable()) {
return;
}
if (alreadyTakenExam(result.exam())) {
return;
}
results.add(result);
}
/**
* Überprüft, ob die Anzahl der vorliegenden Ergebnisse für eine Gesamtnotenberechnung ausreicht.
*
* @return {@code true}, wenn die benötigte Anzahl an Ergebnissen erreicht ist.
*/
public boolean gradable() {
return results.size() == Grading.NEEDED_EXAM_RESULTS;
}
/**
* Ermittelt, ob die Prüfung aufgrund einer ungenügenden Leistung (Note 6.0) als abgebrochen
* oder endgültig nicht bestanden gilt.
*
* @return {@code true}, wenn mindestens eine Note 6.0 enthalten ist.
*/
public boolean terminated() {
return results.stream().anyMatch(entry -> entry.grade() == 6.0);
}
/**
* Sucht nach einem spezifischen Ergebnis innerhalb des Datensatzes.
*
* @param exam Der gesuchte Prüfungsteil.
* @return Das entsprechende {@code ExamResult} oder {@code null}, falls kein Eintrag gefunden wurde.
*/
public ExamResult find(Exam exam) {
return results.stream()
.filter(entry -> entry.exam() == exam)
.findFirst()
.orElse(null);
}
/**
* Gibt die Liste aller aktuell gespeicherten Prüfungsergebnisse zurück.
*
* @return Eine Liste von {@code ExamResult}-Objekten.
*/
public List<ExamResult> results() {
return results;
}
}

View File

@@ -0,0 +1,65 @@
package com.github.victorpyra.calc.result;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
/**
* Der Record {@code ExamResult} repräsentiert das Ergebnis eines spezifischen Prüfungsteils.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Er verknüpft einen {@link Exam}-Typ mit der erreichten Note und bietet Hilfsmittel
* zur einheitlichen Formatierung von numerischen Notenwerten nach deutschem Standard.
* </p>
*
* @param exam Der Prüfungsteil, auf den sich dieses Ergebnis bezieht.
* @param grade Die erreichte Note als numerischer Wert.
*
* @author Victor Pyra
* @version 1.0
*/
public record ExamResult(
Exam exam,
double grade
) {
/**
* Das Formatierungsobjekt zur Darstellung von Notenwerten.
*
* <p>
* Es verwendet deutsche Lokalisierungseinstellungen, um sicherzustellen, dass
* Dezimaltrenner den regionalen Standards entsprechen.
* </p>
*
*/
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(
"0",
DecimalFormatSymbols.getInstance(Locale.GERMAN)
);
/*
Hier wird die maximale Anzahl der Nachkommastellen auf zwei begrenzt,
um eine übersichtliche Darstellung der Noten zu gewährleisten.
Hier wird einfach der Static-Block genutzt, um vor Erstellung jeglicher Objekte dem DecimalFormat-Objekt GLOBAL für
ALLE Objekte zu sagen, dass wir die Nachkommastellen mithilfe der Methode #setMaxFracDig[...] STATISCH setzen.
*/
static {
DECIMAL_FORMAT.setMaximumFractionDigits(2);
}
/**
* Formatiert einen numerischen Notenwert in eine lesbare String-Repräsentation.
*
* <p>
* Dabei werden maximal zwei Nachkommastellen berücksichtigt und die
* Formatierungsregeln des deutschen Lokals (z. B. Komma als Dezimaltrenner) angewandt.
* </p>
*
* @param grade Der zu formatierende Notenwert.
* @return Die formatierte Note als {@code String}.
*/
public String format(double grade) {
return DECIMAL_FORMAT.format(grade);
}
}

View File

@@ -0,0 +1,83 @@
package com.github.victorpyra.calc.storage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
/**
* Die Klasse {@code LocalStorage} ist eine Utility-Klasse für die persistente Datenspeicherung.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie fungiert als einfacher lokaler Speicher (ähnlich dem LocalStorage im Browser), um Notenwerte
* zwischen Programmstarts in einer Konfigurationsdatei zu erhalten.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class LocalStorage {
/**
* Der Name der Datei, in der die Daten gespeichert werden.
*/
private static final String FILE_NAME = "grades.storage";
/**
* Der Pfad zur Speicherdatei im Dateisystem.
*/
private static final Path STORAGE_PATH = Paths.get(FILE_NAME);
/**
* Schreibt die übergebenen Eigenschaften in die lokale Speicherdatei.
*
* <p>
* Bestehende Daten werden dabei überschrieben. Tritt ein Fehler beim Schreibvorgang auf,
* wird dieser in die Standard-Fehlerausgabe geschrieben.
* </p>
*
* @param data Ein {@link Properties}-Objekt, das die zu speichernden Schlüssel-Wert-Paare enthält.
*/
public static void save(Properties data) {
try (BufferedWriter writer = Files.newBufferedWriter(STORAGE_PATH)) {
data.store(writer, "IHK Calc Auto-Save");
} catch (IOException exception) {
System.err.println("Exception occured while saving data to storage: " + exception.getMessage());
}
}
/**
* Lädt die gespeicherten Noten aus der lokalen Datei.
*
* <p>
* Falls die Datei noch nicht existiert, wird ein leeres {@link Properties}-Objekt zurückgegeben.
* </p>
*
* @return Ein {@link Properties}-Objekt mit den geladenen Werten.
*/
public static Properties load() {
Properties properties = new Properties();
if (!Files.exists(STORAGE_PATH)) {
return properties;
}
try (BufferedReader reader = Files.newBufferedReader(STORAGE_PATH)) {
properties.load(reader);
} catch (IOException exception) {
System.err.println("Exception occured while loading data from storage: " + exception.getMessage());
}
return properties;
}
/**
* Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern.
*/
private LocalStorage() {
throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden.");
}
}

View File

@@ -0,0 +1,251 @@
package com.github.victorpyra.calc.ui;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.storage.LocalStorage;
import com.github.victorpyra.calc.ui.component.ButtonComponent;
import com.github.victorpyra.calc.ui.component.CardComponent;
import com.github.victorpyra.calc.ui.component.InputComponent;
import com.github.victorpyra.calc.ui.component.WindowClosingComponent;
import com.github.victorpyra.calc.ui.theme.Theme;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.net.URL;
import java.util.EnumMap;
import java.util.Map;
import java.util.Properties;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
/**
* Die Klasse {@code MainFrame} stellt das Hauptfenster der Anwendung dar. Die Klasse ist als 'final' markiert, um
* anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie ist für den Aufbau der gesamten Benutzeroberfläche zuständig, verwaltet die Eingabemasken für die verschiedenen
* Prüfungsteile und integriert die Berechnungslogik. Das Fenster wird basierend auf dem globalen {@link Theme}
* gestaltet.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class MainFrame extends JFrame {
/**
* Eine Map, die alle Prüfungstypen auf ihre entsprechenden Eingabefelder abbildet.
*/
private final Map<Exam, JTextField> examInputFields = new EnumMap<>(Exam.class);
/**
* Der Container für den Eingabebereich, in dem die Labels und Textfelder liegen.
*/
private final JPanel inputSection = new JPanel();
/**
* Das Hauptpanel der Anwendung, das als Hintergrund für alle anderen Komponenten dient.
*/
private final JPanel backgroundPanel = new JPanel();
/**
* Ein Container für die Steuerungselemente, primär für den Berechnungs-Button.
*/
private final JPanel buttonSection = new JPanel();
/**
* Die Schaltfläche zum Auslösen der Notenberechnung.
*/
private final JButton calcButton = new JButton("Ausrechnen");
/**
* Der Container für den Ergebnisbereich am unteren Ende des Fensters.
*/
private final JPanel resultPanel = new JPanel();
/**
* Der mehrzeilige Textbereich zur Anzeige der Berechnungsergebnisse oder Fehlermeldungen.
*/
private final JTextArea resultTextArea = new JTextArea();
/**
* Erstellt ein neues {@code MainFrame} mit einem spezifischen Titel.
*
* <p>
* Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert
* den Layout-Aufbau.
* </p>
*
* @param title Der Titel, der in der Kopfzeile des Fensters angezeigt wird.
*/
public MainFrame(String title) {
super(title);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setAppIcon();
initFrame();
addWindowListener(new WindowClosingComponent(examInputFields));
}
/**
* Erstellt ein neues {@code MainFrame} mit einem spezifischen Titel und öffnet dieses von alleine.
*
* <p>
* Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert
* den Layout-Aufbau.
* </p>
*
* @param title Der Titel, der in der Kopfzeile des Fensters angezeigt wird.
* @param open Die Flag, die markiert, dass das MainFrame von "alleine" geöffnet werden soll.
*/
public MainFrame(String title, boolean open) {
this(title);
if (open) {
open();
}
}
/**
* Lädt und setzt das Icon für das Anwendungsfenster und die Taskleiste.
*
* <p>
* Die Ressource wird aus dem Klassenpfad geladen. Falls die Datei nicht gefunden wird, erfolgt eine Fehlermeldung
* in der Konsole.
* </p>
*
*/
private void setAppIcon() {
URL url = getClass().getResource("/calculator-taskbar.png");
if (url == null) {
System.err.println("Icon resource not found: /calculator-taskbar.png");
return;
}
setIconImage(new ImageIcon(url).getImage());
}
/**
* Initialisiert die Eingabefelder und den Ergebnisbereich.
*
* <p>
* Diese Methode erzeugt für jeden {@link Exam}-Typ ein {@link InputComponent} und konfiguriert das
* {@code JTextArea} für die Ergebnisausgabe. Zudem wird der {@link ButtonComponent} zur Steuerung registriert.
* </p>
*
*/
private void initFields() {
examInputFields.clear();
Properties savedData = LocalStorage.load();
for (Exam exam : Exam.values()) {
InputComponent inputComponent = new InputComponent();
String savedValue = savedData.getProperty(exam.name());
if (savedValue != null) {
inputComponent.setText(savedValue);
}
examInputFields.put(exam, inputComponent);
}
resultTextArea.setEditable(false);
resultTextArea.setWrapStyleWord(true);
resultTextArea.setLineWrap(true);
resultTextArea.setRows(6);
resultTextArea.setFont(Theme.FONT_RESULT);
resultTextArea.setForeground(Theme.TEXT_MAIN);
resultTextArea.setBackground(Theme.RESULT_BG);
resultTextArea.setBorder(new EmptyBorder(10, 12, 10, 12));
new ButtonComponent(
calcButton,
resultTextArea,
examInputFields
);
}
/**
* Baut die Struktur des Frames auf und ordnet die Komponenten an.
*
* <p>
* Hier werden die Eingabefelder in Karten-Komponenten verpackt, Scroll-Panels für die Ergebnisse erstellt und das
* BoxLayout für die vertikale Anordnung konfiguriert.
* </p>
*
*/
private void initFrame() {
initFields();
inputSection.setLayout(new GridLayout(0, 2, 10, 8));
inputSection.setOpaque(false);
for (Exam exam : Exam.values()) {
JLabel label = styledLabel(exam.localizedName());
inputSection.add(label);
inputSection.add(examInputFields.get(exam));
}
JPanel inputCard = CardComponent.create("Noten eingeben", inputSection);
buttonSection.setOpaque(false);
buttonSection.setBorder(new EmptyBorder(4, 0, 4, 0));
buttonSection.add(calcButton);
JScrollPane scrollPane = new JScrollPane(resultTextArea);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.setBackground(Theme.RESULT_BG);
JPanel resultInner = new JPanel(new BorderLayout());
resultInner.setOpaque(false);
resultInner.add(scrollPane, BorderLayout.CENTER);
resultPanel.setLayout(new BorderLayout());
resultPanel.setOpaque(false);
resultPanel.add(CardComponent.create("Ergebnis", resultInner), BorderLayout.CENTER);
backgroundPanel.setLayout(new BoxLayout(backgroundPanel, BoxLayout.Y_AXIS));
backgroundPanel.setBackground(Theme.BG);
backgroundPanel.setBorder(new EmptyBorder(20, 20, 20, 20));
backgroundPanel.add(inputCard);
backgroundPanel.add(buttonSection);
backgroundPanel.add(resultPanel);
setContentPane(backgroundPanel);
pack();
setMinimumSize(getSize());
setResizable(false);
}
/**
* Erstellt ein formatiertes Label für die Beschriftung der Eingabefelder.
*
* @param text Der anzuzeigende Text des Labels.
* @return Ein vorkonfiguriertes {@link JLabel} im Design des Themes.
*/
private JLabel styledLabel(String text) {
JLabel label = new JLabel(text, SwingConstants.LEFT);
label.setFont(Theme.FONT_LABEL);
label.setForeground(Theme.TEXT_MAIN);
return label;
}
/**
* Macht das Fenster für den Benutzer sichtbar.
*/
public void open() {
setVisible(true);
}
}

View File

@@ -0,0 +1,241 @@
package com.github.victorpyra.calc.ui.component;
import com.github.victorpyra.calc.grading.Grading;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.result.ExamRecord;
import com.github.victorpyra.calc.result.ExamResult;
import com.github.victorpyra.calc.storage.LocalStorage;
import com.github.victorpyra.calc.ui.theme.Theme;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Map;
import java.util.Properties;
import javax.swing.JButton;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.border.EmptyBorder;
/**
* Die Klasse {@code CalcButtonComponent} steuert die Logik für den Berechnungs-Button. Die Klasse ist als 'final'
* markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie implementiert den {@link ActionListener}, um bei einem Klick die Eingabewerte aus den Textfeldern zu validieren,
* in Notenwerte zu parsen und über die {@link Grading}-Logik auszuwerten. Zudem übernimmt sie das Styling des Buttons.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class ButtonComponent implements ActionListener {
private static final String ERROR_MISSING = "Berechnung nicht möglich.\nNote nicht vorhanden.";
private static final String ERROR_FORMAT = "Berechnung nicht möglich.\nNote darf keine Buchstaben beinhalten.";
private static final String ERROR_RANGE = "Berechnung nicht möglich.\nNote muss zwischen 1 und 6 liegen!";
/**
* Eine Map, die jedem {@link Exam}-Typ das entsprechende Texteingabefeld zuordnet. Dies ermöglicht den dynamischen
* Zugriff auf alle Notenwerte während der Validierung.
*/
private final Map<Exam, JTextField> examInputFields;
/**
* Der Textbereich, in dem das Ergebnis der Berechnung oder Fehlermeldungen ausgegeben werden.
*/
private final JTextArea resultTextArea;
/**
* Die Schaltfläche, welche die Berechnungslogik auslöst und visuell angepasst wird.
*/
private final JButton calcButton;
/**
* Erstellt eine neue {@code CalcButtonComponent} und verknüpft sie mit der UI.
*
* @param calcButton Der Button, der die Berechnung auslösen soll.
* @param resultTextArea Der Textbereich zur Anzeige der Ergebnisse oder Fehlermeldungen.
* @param examInputFields Eine Map, die jedem Prüfungsteil sein entsprechendes Eingabefeld zuordnet.
*/
public ButtonComponent(
JButton calcButton,
JTextArea resultTextArea,
Map<Exam, JTextField> examInputFields
) {
this.calcButton = calcButton;
this.resultTextArea = resultTextArea;
this.examInputFields = examInputFields;
styleCalcButton();
}
/**
* Konfiguriert das visuelle Erscheinungsbild des Buttons basierend auf dem globalen Theme.
*
* <p>
* Es werden Schriftarten, Farben, Rahmen und ein Hover-Effekt über den {@link HoverListener} hinzugefügt.
* </p>
*
*/
private void styleCalcButton() {
calcButton.setFont(Theme.FONT_BUTTON);
calcButton.setForeground(Color.WHITE);
calcButton.setBackground(Theme.ACCENT);
calcButton.setOpaque(true);
calcButton.setFocusPainted(false);
calcButton.setBorderPainted(false);
calcButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
calcButton.setBorder(new EmptyBorder(8, 28, 8, 28));
calcButton.addMouseListener(new HoverListener());
calcButton.addActionListener(this);
}
/**
* Verarbeitet den Klick auf den Berechnungs-Button.
*
* <p>
* Die Methode liest alle Eingabefelder aus, validiert die Inhalte auf Vollständigkeit und Korrektheit und stößt bei
* Erfolg die Notenberechnung an.
* </p>
*
* @param event Das auslösende ActionEvent.
*/
@Override
public void actionPerformed(ActionEvent event) {
saveCurrentInput();
try {
ExamRecord record = buildExamRecord();
showResult(new Grading(record).calculate(), Theme.SUCCESS_FG);
} catch (IllegalArgumentException exception) {
showResult(exception.getMessage(), Theme.ERROR_FG);
}
}
private void saveCurrentInput() {
Properties properties = new Properties();
for (Map.Entry<Exam, JTextField> entry : examInputFields.entrySet()) {
String text = entry.getValue().getText();
if (text != null && !text.isBlank()) {
properties.setProperty(entry.getKey().name(), text);
}
}
LocalStorage.save(properties);
}
/**
* Erstellt ein {@link ExamRecord}-Objekt, indem die Eingabefelder aller {@link Exam}-Einträge geparst und validiert
* werden.
*
* @return ein vollständig befülltes {@link ExamRecord}
* @throws IllegalArgumentException wenn eine Eingabe fehlt, kein gültiges Format hat oder außerhalb des gültigen
* Bereichs [1.0, 6.0] liegt
*/
private ExamRecord buildExamRecord() {
ExamRecord record = ExamRecord.create();
for (Exam exam : Exam.values()) {
double grade = parseGradeOrThrow(examInputFields.get(exam).getText());
record.add(new ExamResult(exam, grade));
}
return record;
}
/**
* Parst und validiert eine Note aus dem gegebenen Texteingabe-String.
*
* @param text der Rohtext, der als Note interpretiert werden soll
* @return die geparste Note als {@code double}
* @throws IllegalArgumentException wenn der Text leer oder keine gültige Zahl ist, oder die Note außerhalb des
* gültigen Bereichs [1.0, 6.0] liegt
*/
private double parseGradeOrThrow(String text) {
double grade;
try {
grade = parseGrade(text);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException(exception.getMessage().equals("empty") ? ERROR_MISSING : ERROR_FORMAT);
}
if (grade < 1.0 || grade > 6.0) {
throw new IllegalArgumentException(ERROR_RANGE);
}
return grade;
}
/**
* Wandelt einen Text-String in einen numerischen Notenwert um.
*
* @param text Der eingegebene Text aus einem Textfeld.
* @return Die Note als {@code double}.
* @throws NumberFormatException Wenn der Text leer ist oder kein gültiges Zahlenformat aufweist.
*/
private double parseGrade(String text) throws NumberFormatException {
if (text == null || text.isBlank()) {
throw new NumberFormatException("empty");
}
return Double.parseDouble(text.replace(",", ".").trim());
}
/**
* Zeigt eine Nachricht im Ergebnisfeld der Anwendung an.
*
* @param message Der anzuzeigende Text.
* @param color Die Farbe, in der der Text dargestellt werden soll.
*/
private void showResult(String message, Color color) {
resultTextArea.setForeground(color);
resultTextArea.setText(message);
}
/**
* Interne Hilfsklasse zur Umsetzung von Hover-Effekten auf dem Button.
*
* <p>
* Beim Betreten des Buttons mit der Maus wird die Hintergrundfarbe auf {@code ACCENT_HOVER} gesetzt und beim
* Verlassen auf {@code ACCENT} zurückgesetzt.
* </p>
*
*/
private static class HoverListener extends MouseAdapter {
/**
* Ändert die Hintergrundfarbe beim Mouse-Over.
*
* @param event Das Maus-Ereignis.
*/
@Override
public void mouseEntered(MouseEvent event) {
if (!(event.getSource() instanceof JButton button)) {
return;
}
button.setBackground(Theme.ACCENT_HOVER);
}
/**
* Setzt die Hintergrundfarbe beim Verlassen des Buttons zurück.
*
* @param event Das Maus-Ereignis.
*/
@Override
public void mouseExited(MouseEvent event) {
if (!(event.getSource() instanceof JButton button)) {
return;
}
button.setBackground(Theme.ACCENT);
}
}
}

View File

@@ -0,0 +1,69 @@
package com.github.victorpyra.calc.ui.component;
import com.github.victorpyra.calc.ui.theme.Theme;
import java.awt.BorderLayout;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
/**
* Die Klasse {@code CardComponent} ist eine Utility-Klasse zur Erstellung von UI-Containern.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie stellt eine statische Methode bereit, um Inhalte in einem visuell abgegrenzten
* Panel (einer "Card") mit Rahmen und Titel darzustellen. Das Design orientiert sich
* dabei strikt an den Vorgaben der {@link Theme}-Klasse.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class CardComponent {
/**
* Erstellt ein {@link JPanel}, das als dekorative Karte fungiert.
*
* <p>
* Die Karte erhält einen abgerundeten Rahmen mit einem Titel, der über die
* {@code TitledBorder} konfiguriert wird. Innerhalb der Karte wird das übergebene
* Content-Panel platziert und farblich angepasst.
* </p>
*
* @param title Der Text, der als Überschrift im Rahmen der Karte angezeigt wird.
* @param content Das {@code JPanel}, welches im Inneren der Karte angezeigt werden soll.
* @return Ein fertig konfiguriertes {@code JPanel} im Card-Design.
*/
public static JPanel create(String title, JPanel content) {
JPanel card = new JPanel(new BorderLayout());
card.setBackground(Theme.PANEL_BG);
TitledBorder titled = BorderFactory.createTitledBorder(
new LineBorder(Theme.BORDER_COL, 1, true),
title,
TitledBorder.LEFT,
TitledBorder.TOP,
Theme.FONT_TITLE,
Theme.TEXT_MUTED
);
card.setBorder(new CompoundBorder(
titled,
new EmptyBorder(8, 10, 10, 10)
));
content.setBackground(Theme.PANEL_BG);
card.add(content, BorderLayout.CENTER);
return card;
}
/**
* Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern.
*/
private CardComponent() {
throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden.");
}
}

View File

@@ -0,0 +1,103 @@
package com.github.victorpyra.calc.ui.component;
import com.github.victorpyra.calc.ui.theme.Theme;
import java.awt.Color;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
/**
* Die Klasse {@code InputComponent} ist eine spezialisierte {@link JTextField}-Komponente.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie erweitert das Standard-Textfeld um das globale Design-System der Anwendung.
* Dies umfasst die automatische Anwendung von Farben, Schriftarten und einen
* dynamischen Rahmen, der auf Fokus-Ereignisse reagiert.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public final class InputComponent extends JTextField {
/**
* Der Standard-Rahmen der Komponente im inaktiven Zustand.
*/
private static final Border DEFAULT_BORDER = new CompoundBorder(
new LineBorder(Theme.BORDER_COL, 2, true),
new EmptyBorder(6, 10, 6, 10)
);
/**
* Der Rahmen der Komponente, wenn sie den Tastaturfokus erhält.
*/
private static final Border FOCUS_BORDER = new CompoundBorder(
new LineBorder(Theme.ACCENT, 2, true),
new EmptyBorder(6, 10, 6, 10)
);
/**
* Erstellt eine neue {@code InputComponent} und initialisiert das visuelle Erscheinungsbild.
*
* <p>
* Es werden die Schriftarten, Farben für Text, Hintergrund und Cursor sowie der
* initiale Rahmen aus der {@link Theme}-Klasse geladen. Zudem wird ein
* {@link FocusHandler} registriert.
* </p>
*
*/
public InputComponent() {
super();
setFont(Theme.FONT_FIELD);
setForeground(Theme.TEXT_MAIN);
setBackground(Color.WHITE);
setCaretColor(Theme.ACCENT);
setBorder(DEFAULT_BORDER);
addFocusListener(new FocusHandler());
}
/**
* Interne Hilfsklasse zur Handhabung von Fokus-Ereignissen.
*
* <p>
* Diese Klasse wechselt den Rahmen der Komponente zwischen {@code DEFAULT_BORDER}
* und {@code FOCUS_BORDER}, um dem Benutzer visuelles Feedback zu geben.
* </p>
*
*/
private static class FocusHandler extends FocusAdapter {
/**
* Wird aufgerufen, wenn die Komponente den Fokus erhält.
*
* @param event Das entsprechende Fokus-Ereignis.
*/
@Override
public void focusGained(FocusEvent event) {
if (!(event.getSource() instanceof JTextField field)) {
return;
}
field.setBorder(FOCUS_BORDER);
}
/**
* Wird aufgerufen, wenn die Komponente den Fokus verliert.
*
* @param event Das entsprechende Fokus-Ereignis.
*/
@Override
public void focusLost(FocusEvent event) {
if (!(event.getSource() instanceof JTextField field)) {
return;
}
field.setBorder(DEFAULT_BORDER);
}
}
}

View File

@@ -0,0 +1,62 @@
package com.github.victorpyra.calc.ui.component;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.storage.LocalStorage;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Map;
import java.util.Properties;
import javax.swing.JTextField;
/**
* Die Klasse {@code WindowClosingComponent} überwacht das Schließen des Anwendungsfensters.
*
* <p>
* Sie implementiert einen {@link WindowAdapter}, um beim Beenden der Anwendung automatisch
* alle aktuellen Eingaben aus den Textfeldern zu sammeln und über den {@link LocalStorage}
* persistent zu speichern.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public class WindowClosingComponent extends WindowAdapter {
/**
* Die Map mit den Eingabefeldern, deren Inhalte gespeichert werden sollen.
*/
private final Map<Exam, JTextField> examInputFields;
/**
* Erstellt eine neue {@code WindowClosingComponent}.
*
* @param examInputFields Die Map, die den Zugriff auf die aktuellen Noteneingaben ermöglicht.
*/
public WindowClosingComponent(Map<Exam, JTextField> examInputFields) {
this.examInputFields = examInputFields;
}
/**
* Wird aufgerufen, wenn das Fenster geschlossen wird.
*
* <p>
* Die Methode iteriert über alle bekannten Prüfungsteile, extrahiert die Texte aus den
* Eingabefeldern und stößt den Speichervorgang an.
* </p>
*
* @param event Das Fenster-Ereignis.
*/
@Override
public void windowClosing(WindowEvent event) {
Properties property = new Properties();
for (Map.Entry<Exam, JTextField> entry : examInputFields.entrySet()) {
String text = entry.getValue().getText();
if (text != null && !text.isBlank()) {
property.setProperty(entry.getKey().name(), text);
}
}
LocalStorage.save(property);
}
}

View File

@@ -0,0 +1,87 @@
package com.github.victorpyra.calc.ui.theme;
import java.awt.Color;
import java.awt.Font;
/**
* Die Klasse {@code Theme} dient als zentraler Speicherort für alle visuellen Stilvorgaben der Benutzeroberfläche.
* Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann.
*
* <p>
* Sie enthält Definitionen für Farben und Schriftarten, um eine konsistente Gestaltung der
* Taschenrechner-Applikation sicherzustellen. Durch die Verwendung dieser Konstanten kann das Design der gesamten
* Anwendung an einer einzigen Stelle angepasst werden.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
public class Theme {
/**
* Die Haupt-Hintergrundfarbe für das Anwendungsfenster (helles Grau).
*/
public static final Color BG = new Color(245, 246, 248);
/**
* Die Hintergrundfarbe für Panels und Inhaltsbereiche (reines Weiß).
*/
public static final Color PANEL_BG = Color.WHITE;
/**
* Die primäre Akzentfarbe für interaktive Elemente wie Buttons oder Highlights.
*/
public static final Color ACCENT = new Color(41, 98, 200);
/**
* Eine dunklere Variante der Akzentfarbe, die für Mouse-Over-Effekte (Hover) genutzt wird.
*/
public static final Color ACCENT_HOVER = new Color(26, 76, 160);
/**
* Die Standardfarbe für Rahmen und Trennlinien zwischen Komponenten.
*/
public static final Color BORDER_COL = new Color(220, 222, 226);
/**
* Die Haupt-Textfarbe für maximale Lesbarkeit auf hellem Hintergrund.
*/
public static final Color TEXT_MAIN = new Color(30, 30, 35);
/**
* Eine abgeschwächte Textfarbe für sekundäre Informationen oder Platzhalter.
*/
public static final Color TEXT_MUTED = new Color(110, 115, 125);
/**
* Die Hintergrundfarbe speziell für das Display- bzw. Ergebnisfeld.
*/
public static final Color RESULT_BG = new Color(250, 251, 253);
/**
* Die Signalfarbe (Rot) für Fehlermeldungen oder ungültige Eingaben.
*/
public static final Color ERROR_FG = new Color(190, 40, 40);
/**
* Die Signalfarbe (Grün) für erfolgreiche Operationen oder positive Statusmeldungen.
*/
public static final Color SUCCESS_FG = new Color(25, 120, 60);
/**
* Die Standardschriftart für allgemeine Beschriftungen (Labels).
*/
public static final Font FONT_LABEL = new Font("Segoe UI", Font.PLAIN, 13);
/**
* Die Schriftart für Texteingabefelder.
*/
public static final Font FONT_FIELD = new Font("Segoe UI", Font.PLAIN, 13);
/**
* Die fettgedruckte Schriftart für Schaltflächen, um sie visuell hervorzuheben.
*/
public static final Font FONT_BUTTON = new Font("Segoe UI", Font.BOLD, 13);
/**
* Die Schriftart für die Anzeige der berechneten Ergebnisse.
*/
public static final Font FONT_RESULT = new Font("Segoe UI", Font.PLAIN, 13);
/**
* Eine kleinere, fettgedruckte Schriftart für Überschriften oder Sektionstitel.
*/
public static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 11);
/**
* Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern.
*/
private Theme() {
throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden.");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,116 @@
package com.github.victorpyra.calc.grading;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.result.ExamRecord;
import com.github.victorpyra.calc.result.ExamResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Die Klasse {@code ExamRecordTest} validiert die Funktionalität der Ergebnisverwaltung.
*
* <p>
* Geprüft wird insbesondere die Integrität des Datensatzes, das Verhindern von Duplikaten sowie die korrekte
* Zustandsermittlung für die Notenberechnung.
* </p>
*
* @author Victor Pyra
* @version 1.0
*/
class ExamRecordTest {
/**
* Die zu testende Instanz des Datensatzes.
*/
private ExamRecord record;
/**
* Initialisiert vor jedem Testlauf einen neuen, leeren {@link ExamRecord}.
*/
@BeforeEach
void setUp() {
record = ExamRecord.create();
}
/**
* Überprüft, ob ein Ergebnis korrekt hinzugefügt werden kann und danach auffindbar ist.
*/
@Test
void shouldAddAndFindResult() {
ExamResult result = new ExamResult(Exam.AP1, 2.0);
record.add(result);
assertNotNull(record.find(Exam.AP1));
assertEquals(2.0, record.find(Exam.AP1).grade());
}
/**
* Stellt sicher, dass für denselben Prüfungsteil keine zwei Ergebnisse hinterlegt werden können.
*/
@Test
void shouldNotAddDuplicateExams() {
record.add(new ExamResult(Exam.AP1, 2.0));
record.add(new ExamResult(Exam.AP1, 3.0));
assertEquals(1, record.results().size());
assertEquals(2.0, record.find(Exam.AP1).grade());
}
/**
* Validiert die Logik der {@code gradable}-Methode.
*
* <p>
* Ein Datensatz darf erst dann als berechenbar markiert werden, wenn die in {@link Grading#NEEDED_EXAM_RESULTS}
* definierte Anzahl an Prüfungen vorliegt.
* </p>
*
*/
@Test
void shouldBeGradableOnlyWhenComplete() {
assertFalse(record.gradable());
for (Exam exam : Exam.values()) {
record.add(new ExamResult(exam, 3.0));
}
assertTrue(record.gradable());
assertEquals(Grading.NEEDED_EXAM_RESULTS, record.results().size());
}
/**
* Prüft, ob das Erreichen der Note 6.0 korrekt als Abbruchkriterium (terminated) erkannt wird.
*/
@Test
void shouldIdentifyTerminatedRecord() {
record.add(new ExamResult(Exam.AP1, 6.0));
assertTrue(record.terminated());
}
/**
* Stellt sicher, dass bei der Suche nach einem nicht vorhandenen Prüfungsteil {@code null} zurückgegeben wird,
* anstatt eine Exception zu werfen.
*/
@Test
void shouldReturnNullWhenExamNotFound() {
assertNull(record.find(Exam.AP1));
}
/**
* Überprüft, ob nach Erreichen der maximalen Anzahl an Ergebnissen keine weiteren Einträge mehr akzeptiert werden.
*/
@Test
void shouldNotAddBeyondLimit() {
for (Exam exam : Exam.values()) {
record.add(new ExamResult(exam, 3.0));
}
// Simulierter Versuch, ein zusätzliches theoretisches Ergebnis zu addieren
record.add(new ExamResult(Exam.AP1, 1.0));
assertEquals(Grading.NEEDED_EXAM_RESULTS, record.results().size());
}
}

View File

@@ -0,0 +1,56 @@
package com.github.victorpyra.calc.grading;
import com.github.victorpyra.calc.result.Exam;
import com.github.victorpyra.calc.result.ExamRecord;
import com.github.victorpyra.calc.result.ExamResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Die Klasse {@code GradingTest} enthält Unit-Tests für die Notenberechnung.
*
* <p>
* Hier werden verschiedene Szenarien geprüft, wie zum Beispiel das Bestehen
* bei Mindestpunktzahl, das Durchfallen durch eine Note 6.0 oder die
* korrekte Gewichtung der einzelnen Prüfungsteile.
* </p>
*
*/
class GradingTest {
private ExamRecord record;
/**
* Bereitet die Testumgebung vor jedem einzelnen Testlauf vor.
*/
@BeforeEach
void setUp() {
record = ExamRecord.create();
}
/**
* Prüft, ob die Berechnung korrekt erkennt, wenn Prüfungsteile fehlen.
*/
@Test
void shouldReturnErrorMessageWhenExamsMissing() {
Grading grading = new Grading(record);
String result = grading.calculate();
assertTrue(result.contains("Nicht berechenbar"));
}
/**
* Validiert das Ausschlusskriterium einer ungenügenden Leistung (Note 6.0).
*
* <p>
* Auch wenn der Gesamtschnitt rechnerisch ausreichen würde, führt eine 6.0
* in einem Teilbereich sofort zum Nichtbestehen.
* </p>
*
*/
@Test
void shouldFailWhenAnyGradeIsSix() {
// Hier würden alle 7 Prüfungen hinzugefügt, eine davon mit 6.0
record.add(new ExamResult(Exam.AP1, 6.0));
assertTrue(record.terminated());
}
}