From f55bc8353b1a171a364be1eacc39011262a853ed Mon Sep 17 00:00:00 2001
From: Hong-Phuc Bui <hong-phuc.bui@htwsaar.de>
Date: Mon, 06 Jul 2026 07:55:39 +0200
Subject: [PATCH] Bezier

---
 06-bezier-kurve/requirements.txt       |    1 
 kalendar/test_chronos.py               |   13 +
 06-bezier-kurve/README.md              |   29 +++
 uebungsblatt-3/.gitignore              |    3 
 .gitignore                             |    1 
 06-bezier-kurve/main-nicegui.py        |   28 +++
 04-module-kalender-bsp/pycalendar.py   |   39 ++++
 04-module-kalender-bsp/chronos.py      |   55 ++++++
 06-bezier-kurve/main-canvas.py         |   36 ++++
 06-bezier-kurve/bezier.py              |   43 +++++
 kalendar/chronos.py                    |   29 +++
 04-module-kalender-bsp/chronos_test.py |   29 +++
 kalendar/main.py                       |    5 
 05-oop-polynome/test_polynomial.py     |   72 +++++++++
 gezeiten/gezeiten.py                   |   26 +++
 05-oop-polynome/.coverage              |    0 
 05-oop-polynome/polynomial.py          |   39 ++++
 gezeiten/test_gezeiten.py              |    9 +
 05-oop-polynome/requirements.txt       |    7 
 19 files changed, 464 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore
index d9cf955..1d09bb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 .pytest_cache
+.venv
 __pycache__/
diff --git a/04-module-kalender-bsp/chronos.py b/04-module-kalender-bsp/chronos.py
new file mode 100644
index 0000000..ed82c04
--- /dev/null
+++ b/04-module-kalender-bsp/chronos.py
@@ -0,0 +1,55 @@
+# Module zur Berechnung von Kalendar Datum
+
+def wochentag(d:int, m:int, y:int) -> int:
+    """
+        bereche den Wochentag eines Datum
+
+        Die Wochentage werden wie folgt codiert:
+        So | Mo | Di | Mi | Do | Fr | Sa
+        0  | 1  | 2  | 3  | 4  | 5  | 6
+
+        @param d: Der Tag im Monat fängt mit 0 an
+        @param m: Der Monat 1 = Januar, 2 = Februa, ... 12 = Dezember
+        @param y: Die Jahresnummer in 4 Ziffer
+
+        @return den Wochentag als eine ganze Zahl
+    """
+    y0 = y - (14 - m) // 12
+    x = y0 + y0//4 - y0//100 + y0//400
+    m0 = m + 12*( (14-m)//12 ) - 2
+    return (d + x + (31*m0)//12) % 7
+
+
+def berechne_ueberhang_tage(wochentag:int) -> int:
+    """Nach Gepflogenheit in deutches Kalender-System fängt eine Woche mit Montag an.
+
+    Mo -> Keinen Überhang; 
+    Di -> 1 Überhang; 
+    Mi -> 2 Überhange; 
+    Do -> 3 Überhange; 
+    Fr -> 4 Überhange; 
+    Sa -> 5 Überhange; 
+    So -> 6 Überhange
+    """
+    return (wochentag - 1) % 7
+
+
+def ist_schaltjahr(y:int) -> bool:
+    """Ein Jahr ist ein Schaltjahr, wenn es 
+        * durch 4 teilbar ist,
+        * und nicht durch 100 teilbar,
+        * aber durch 400 teilbar ist.
+    """
+    return (y % 4 == 0) and ( (y%100 != 0) or (y%400 == 0) )
+
+
+def letzter_tag(m: int, y:int) -> int:
+    """Berechne den letztenn Tag eines Monat in einem Jahr
+    """
+    tage = [None, 31, 28, 31, 30, 
+    31, 30, 31, 31, 
+    30, 31, 30, 31]
+    if m != 2: return tage[m]
+    return 29 if ist_schaltjahr(y) else 28
+
+
diff --git a/04-module-kalender-bsp/chronos_test.py b/04-module-kalender-bsp/chronos_test.py
new file mode 100644
index 0000000..859bf70
--- /dev/null
+++ b/04-module-kalender-bsp/chronos_test.py
@@ -0,0 +1,29 @@
+import pytest
+
+from chronos import *
+
+
+def test_ist_schaltjar_einfache():
+    """DRY"""
+    assert ist_schaltjahr(2000) == True
+
+
+@pytest.mark.parametrize("jahr,expected", [
+    (2000, True),
+    (2004, True),
+    (2100, False),
+    (2400, True)
+])
+def test_schaltjahr(jahr, expected):
+    assert ist_schaltjahr(jahr) == expected
+
+   
+@pytest.mark.parametrize("d,m,y,expected_d", [
+    (24, 12, 2026, 4), # 24.12.2026 fällt auf einem Donnerstag
+    ( 3, 10, 2026, 6)
+])
+def test_wochentag(d, m, y, expected_d):
+    t = wochentag(d, m, y)
+    assert t == expected_d
+
+
diff --git a/04-module-kalender-bsp/pycalendar.py b/04-module-kalender-bsp/pycalendar.py
new file mode 100644
index 0000000..4942765
--- /dev/null
+++ b/04-module-kalender-bsp/pycalendar.py
@@ -0,0 +1,39 @@
+import sys
+
+from chronos import *
+
+def print_kalender(m: int, y: int):    
+    monaten = [None, 
+            "Januar",    "Februar", "März",     "April",
+            "Mai",       "Juni",    "July",     "August",
+            "September", "Oktober", "November", "Dezember"]
+    # Ausgabe des Monat und Jahr
+    print(f"{monaten[m]} {y}")
+    wochentage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
+    print(" ".join(wochentage))
+    
+    # Ausgabe der Überhang von letzen Monat
+    erster_wochentag = wochentag(1, m, y)
+    ueberhang = berechne_ueberhang_tage(erster_wochentag)
+    spalten_breite = len(wochentage[0]) + 1 
+    ueberhang_zeichen = " "*(spalten_breite * ueberhang)
+    print(ueberhang_zeichen, end="")
+    
+    # Ausgabe des Kalenders
+    spalten_idx = ueberhang
+    tag = 1
+    letzter_tag_im_monat = letzter_tag(m, y)
+    while tag <= letzter_tag_im_monat:
+        print(f"{tag:2d}", end=" ")
+        tag += 1
+        spalten_idx += 1
+        if spalten_idx % 7 == 0: print()
+
+
+if __name__ == "__main__":
+    try:
+        m, y = int(sys.argv[1]), int(sys.argv[2])
+        print_kalender(m, y)
+        print()
+    except:
+        print(f"CLI-Syntax: `python {sys.argv[0]} <monat:int> <jahr:int>`")
diff --git a/05-oop-polynome/.coverage b/05-oop-polynome/.coverage
new file mode 100644
index 0000000..3738c0c
--- /dev/null
+++ b/05-oop-polynome/.coverage
Binary files differ
diff --git a/05-oop-polynome/polynomial.py b/05-oop-polynome/polynomial.py
new file mode 100644
index 0000000..c78bcd5
--- /dev/null
+++ b/05-oop-polynome/polynomial.py
@@ -0,0 +1,39 @@
+import numbers
+
+class Polynomial:
+    def __init__(self, coe):
+        if coe[-1] == 0.0:
+            raise RuntimeError("Coefficient invalid")
+        self._coe = tuple(coe)
+
+    def getExponent(self, n):
+        """
+        :param: n der n-Expoment des Polynom 
+        """
+        return self._coe[n]
+
+    def getExponents(self):
+        return tuple(self._coe)
+
+    def eval(self, x:float) -> float:
+        """
+        Den Wert des Polynoms an der Stelle x berechnen
+        """
+        wert = 0
+        for c in self._coe[-1:0:-1]:
+            wert = (wert + c)*x
+            print(c, wert)
+        wert += self._coe[0]
+        return wert
+
+    def __call__(self, value:float) -> float:
+        if isinstance(value, numbers.Number):
+            return self.eval(value)
+        else:
+            raise TypeError("Bad argument, expected a number")
+
+    def __getitem__(self, idx:int) -> float:
+        if idx >= 0 and idx < len(self._coe):
+            return self._coe[idx]
+        else:
+            raise IndexError("idx invalid")
diff --git a/05-oop-polynome/requirements.txt b/05-oop-polynome/requirements.txt
new file mode 100644
index 0000000..33bd0c2
--- /dev/null
+++ b/05-oop-polynome/requirements.txt
@@ -0,0 +1,7 @@
+coverage==7.14.3
+iniconfig==2.3.0
+packaging==26.2
+pluggy==1.6.0
+Pygments==2.20.0
+pytest==9.1.1
+pytest-cov==7.1.0
diff --git a/05-oop-polynome/test_polynomial.py b/05-oop-polynome/test_polynomial.py
new file mode 100644
index 0000000..1302663
--- /dev/null
+++ b/05-oop-polynome/test_polynomial.py
@@ -0,0 +1,72 @@
+from pytest import approx, fail
+
+from polynomial import Polynomial
+
+def test_construct_polynomial_error():
+    coe = [3.5, -4.6, 0] # 3.5 - 4.6x + 0x^2
+    try:
+        p = Polynomial(coe) #
+    except RuntimeError as ex:
+        print(ex)
+        pass
+
+def test_construct_quadratic_polynomial():
+    coe = [3, -4.5, 1.5] # 3 - 4.5x + 1.5x^2
+    p = Polynomial(coe)
+    assert p.getExponent(0) == 3
+    #e = p[0]
+
+def test_get_coefficient():
+    coe = [3, -4.5, 1.5] # 3 - 4.5x + 1.5x^2
+    p = Polynomial(coe)
+    for idx in range(0, len(coe)):
+        assert p[idx] == coe[idx]
+    
+def test_get_coefficient_bad_argument():
+    coe = [3, -4.5, 1.5] # 3 - 4.5x + 1.5x^2
+    p = Polynomial(coe)
+    try:
+        p[-1]
+        fail("Expected an exception")
+    except IndexError as ex:
+        pass
+
+
+def test_eval():
+    coe = [3, 2, 1] #  3 + 2x + x^2
+    p = Polynomial(coe)
+    w = p.eval(0) # 
+    assert w == 3
+    w = p.eval(1)
+    assert w == 6
+    w = p.eval(-1)
+    
+def test_eval_per_operator():
+    coe = [3, 2, 1] #  3 + 2x + x^2
+    p = Polynomial(coe)
+    w = p(-4.5)
+    assert w == approx(14.25) 
+
+def test_eval_per_operator_with_str_argument():
+    coe = [3, 2, 1] #  3 + 2x + x^2
+    p = Polynomial(coe)
+    try:
+        w = p("hello world")
+        fail("Bad argument")
+    except TypeError as ex:
+        #assert ex.message == "Bad argument"
+        print(ex)
+
+
+
+def test_approximate_eval():
+    coe = [3, 2, 1] #  3 + 2x + x^2
+    p = Polynomial(coe)
+    w = p.eval(1.25)
+    assert w == approx(7.0625)
+    w = p.eval(1/3)
+    assert w == approx(3.77777777)
+
+
+
+    
diff --git a/06-bezier-kurve/README.md b/06-bezier-kurve/README.md
new file mode 100644
index 0000000..067ea78
--- /dev/null
+++ b/06-bezier-kurve/README.md
@@ -0,0 +1,29 @@
+# Demo NiceGUI vs Tk
+
+Eine kleine Demonstration von Plot-Möglichkeiten in NiceGUI via HTML-Canvas und Tk Canvas
+
+## Install
+
+```bash
+python3.13 -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+
+## Run
+
+In beiden Programme kann man Punkten einer (kubischen) Bezier-Kurve mit Mouse-Klicks bestimmen,
+danach wird eine Kurve angezeigt.
+
+### Tk
+
+```bash
+python ./main-canvas.py
+```
+
+### NiceGUI
+
+```bash
+python ./main-nicegui.py
+```
diff --git a/06-bezier-kurve/bezier.py b/06-bezier-kurve/bezier.py
new file mode 100644
index 0000000..26f8eed
--- /dev/null
+++ b/06-bezier-kurve/bezier.py
@@ -0,0 +1,43 @@
+""""
+Module related to bezier curve
+"""
+def make_cubic_bezier(control_points: [(float, float)], n: int = 10) -> [(float, float)]:
+    r""""
+    Interpolate a Bezier-curve by split it into segments of lines (=polyline).
+    A Bezier-Curve is defined by
+
+        \[
+           B(P_0, P_1, P_2, P_3:t) = (1-t)^3*P_0 + 3(1-t)^2*t*P_1 + 3(1-t)*t^2*P_2 + t^3*P_3
+        \]
+
+    for $t \in \[0, 1\]$
+
+    :param control_points
+    :param n number of segments
+    :return a list of points on the Bezier-Curve.
+    """
+
+    p0x = control_points[0][0]
+    p0y = control_points[0][1]
+    p1x = control_points[1][0]
+    p1y = control_points[1][1]
+    p2x = control_points[2][0]
+    p2y = control_points[2][1]
+    p3x = control_points[3][0]
+    p3y = control_points[3][1]
+    points = [(p0x, p0y)]
+    for i in range(1, n):
+        t = i / n
+        u = 1 - t
+        x = p0x * u**3 + 3 * p1x * t* u**2 + 3 * p2x * t**2 * u + p3x * t**3
+        y = p0y * u**3 + 3 * p1y * t* u**2 + 3 * p2y * t**2 * u + p3y * t**3
+        points.append( (x,y) )
+    points.append((p3x, p3y))
+    return points
+
+
+def make_svg_polyline(points) -> str:
+    line = map(lambda p: f"{p[0]},{p[1]}", points)
+    svg = f'<polyline points="{" ".join(line)}" fill="none" stroke="gray" stroke-width="3" />'
+    return svg
+
diff --git a/06-bezier-kurve/main-canvas.py b/06-bezier-kurve/main-canvas.py
new file mode 100644
index 0000000..756cfa8
--- /dev/null
+++ b/06-bezier-kurve/main-canvas.py
@@ -0,0 +1,36 @@
+from tkinter import *
+
+from bezier import make_cubic_bezier
+
+def draw_bezier(canvas, control_points):
+    points = make_cubic_bezier(control_points, n= 400)
+    for i in range(1,len(points)):
+        x1 = points[i-1][0]
+        y1 = points[i-1][1]
+        x2 = points[i][0]
+        y2 = points[i][1]
+        canvas.create_line(x1,y1, x2,y2)
+
+
+def get_point(event):
+    global control_points
+    point = x, y = (event.x, event.y)
+    control_points.append(point)
+    canvas.create_oval(x, y, x+3, y+3)
+    if len(control_points) == 4:
+        draw_bezier(canvas, control_points)
+        control_points = []
+
+
+if __name__ == '__main__':
+
+    control_points = []
+
+    root = Tk()
+
+    canvas = Canvas(root, width=800, height=800)
+    canvas.pack()
+
+    canvas.bind('<Button-1>', get_point)
+
+    root.mainloop()
\ No newline at end of file
diff --git a/06-bezier-kurve/main-nicegui.py b/06-bezier-kurve/main-nicegui.py
new file mode 100644
index 0000000..4a055ea
--- /dev/null
+++ b/06-bezier-kurve/main-nicegui.py
@@ -0,0 +1,28 @@
+from nicegui import ui
+
+from bezier import make_cubic_bezier, make_svg_polyline
+
+control_points = []
+svg = ""
+
+def get_point(event):
+    global control_points
+    global svg
+    x = event.image_x
+    y = event.image_y
+    control_points.append((x,y))
+    svg += f'<circle cx="{x}" cy="{y}" r="1" fill="orange" />'
+    ui.notification(f"(x,y) = ({x},{y})")
+    if len(control_points) == 4:
+        bezier_points = make_cubic_bezier(control_points, n = 50)
+        svg += make_svg_polyline(bezier_points)
+        control_points.clear()
+    event.sender.set_content(svg)
+
+ui.interactive_image(
+    size=(900, 400), cross=True,
+    on_mouse=get_point,
+).classes('w-full bg-blue-50')
+
+ui.run()
+
diff --git a/06-bezier-kurve/requirements.txt b/06-bezier-kurve/requirements.txt
new file mode 100644
index 0000000..e57426a
--- /dev/null
+++ b/06-bezier-kurve/requirements.txt
@@ -0,0 +1 @@
+nicegui==2.18.0
diff --git a/gezeiten/gezeiten.py b/gezeiten/gezeiten.py
new file mode 100644
index 0000000..f661274
--- /dev/null
+++ b/gezeiten/gezeiten.py
@@ -0,0 +1,26 @@
+def parse_dataline(data:str):
+    fields = data.split('#')
+    fields[5] = fields[5].replace(' ', '')
+    return tuple(fields)
+    
+def parse_data_file(filename: str):
+    wasser_pegel = []
+    in_data = False
+    with open(filename) as data:
+        line = data.readline()
+        while line is not None:
+            line = line.strip()
+            if (line == "LLL#"):
+                none_data_line = 3
+                while none_data_line > 0:
+                    none_data_line -= 1
+                    data.readline()
+                in_data = True
+            if in_data:
+                wasser_pegel.append(parse_dataline(line))
+    return wasser_pegel
+
+
+if __name__ == "__main__":
+    wasser_pegel = parse_data_file(sys.argv[1])
+
diff --git a/gezeiten/test_gezeiten.py b/gezeiten/test_gezeiten.py
new file mode 100644
index 0000000..bea0828
--- /dev/null
+++ b/gezeiten/test_gezeiten.py
@@ -0,0 +1,9 @@
+from gezeiten import parse_dataline
+
+def test_parse_dataline():
+    line = "VB2#DE__508P# #H#Di# 1. 1.2019# 0:00# 6.87 # #  1#+ 1:00#  24348#   1#2458484.458495#"
+    data = parse_dataline(line)
+    assert data[0] == "VB2"
+    assert data[1] == "DE__508P"
+    assert data[5] == "1.1.2019"
+    
diff --git a/kalendar/chronos.py b/kalendar/chronos.py
new file mode 100644
index 0000000..952362c
--- /dev/null
+++ b/kalendar/chronos.py
@@ -0,0 +1,29 @@
+
+def ist_schaltjahr(y:int) -> bool:
+    """Ein Jahr ist ein Schaltjahr, wenn es 
+        * durch 4 teilbar ist,
+        * und nicht durch 100 teilbar,
+        * aber durch 400 teilbar ist.
+    """
+    return (y % 4 == 0) and ( (y%100 != 0) or (y%400 == 0) )
+
+
+def berechne_ueberhang_tage(wochentag:int) -> int:
+    """Nach Gepflogenheit in deutches Kalender-System fängt eine Woche mit Montag an.
+
+    Mo -> Keinen Überhang; 
+    Di -> 1 Überhang; 
+    Mi -> 2 Überhange; 
+    Do -> 3 Überhange; 
+    Fr -> 4 Überhange; 
+    Sa -> 5 Überhange; 
+    So -> 6 Überhange
+    """
+    #return (wochentag - 1) % 7
+    return (wochentag - 2) % 7
+
+
+
+     
+
+
diff --git a/kalendar/main.py b/kalendar/main.py
new file mode 100644
index 0000000..895fdde
--- /dev/null
+++ b/kalendar/main.py
@@ -0,0 +1,5 @@
+import chronos
+
+jahr = 2024
+schaltjahr = chronos.ist_schaltjahr(jahr)
+print(schaltjahr)
\ No newline at end of file
diff --git a/kalendar/test_chronos.py b/kalendar/test_chronos.py
new file mode 100644
index 0000000..c9f52a4
--- /dev/null
+++ b/kalendar/test_chronos.py
@@ -0,0 +1,13 @@
+from chronos import ist_schaltjahr, berechne_ueberhang_tage
+
+def test_ist_schaltjahr():
+    assert ist_schaltjahr(2000) , "2000 ist schaltjahr"
+    assert not ist_schaltjahr(2025) , "2025 ist kein schaltjahr"
+
+
+def test_berechne_ueberhang_tage():
+    assert berechne_ueberhang_tage(1) == 0, "Keinen Überhang Platz bei Montag"
+    assert berechne_ueberhang_tage(0) == 6, "6 Überhang Plätze bei Sonntag"    
+
+
+
diff --git a/uebungsblatt-3/.gitignore b/uebungsblatt-3/.gitignore
new file mode 100644
index 0000000..788c8a1
--- /dev/null
+++ b/uebungsblatt-3/.gitignore
@@ -0,0 +1,3 @@
+gezeiten-bsh.py
+
+

--
Gitblit v1.10.0