From e5c3b094424194754630d1b08db9f3e668c0fd8b Mon Sep 17 00:00:00 2001
From: Hong-Phuc Bui <hong-phuc.bui@htwsaar.de>
Date: Tue, 24 Jun 2025 10:16:54 +0200
Subject: [PATCH] stundenplan

---
 stundenplan/src/pygraph/__init__.py                       |    0 
 stundenplan/src/pygraph/graphdemo.py                      |  117 ++++++++++++++++
 stundenplan/tests/pygraph/__init__.py                     |    0 
 stundenplan/tests/pygraph/graphdemo_tests.py              |   88 ++++++++++++
 stundenplan/src/stundenplan.egg-info/top_level.txt        |    1 
 stundenplan/tests/__init__.py                             |    0 
 stundenplan/src/stundenplan.egg-info/PKG-INFO             |    3 
 stundenplan/src/stundenplan.egg-info/dependency_links.txt |    1 
 stundenplan/src/pygraph/shortestpath.py                   |   79 +++++++++++
 stundenplan/tests/pygraph/shortestpath_tests.py           |   57 ++++++++
 stundenplan/pyproject.toml                                |   17 ++
 stundenplan/Readme.md                                     |   21 +++
 stundenplan/src/stundenplan.egg-info/SOURCES.txt          |    8 +
 13 files changed, 392 insertions(+), 0 deletions(-)

diff --git a/stundenplan/Readme.md b/stundenplan/Readme.md
new file mode 100644
index 0000000..00ba398
--- /dev/null
+++ b/stundenplan/Readme.md
@@ -0,0 +1,21 @@
+# Beispielanwendung von Graph
+
+## Installation
+
+Installation das Projekt auf einem virtuellen Python-Umgebung:
+
+````shell
+pip install -e .
+````
+
+## Run Unittest
+
+```shell
+python3 -m unittest discover --pattern *tests.py
+```
+
+## Run programm
+
+```shell
+echo "TODO: to be done by student"
+```
\ No newline at end of file
diff --git a/stundenplan/pyproject.toml b/stundenplan/pyproject.toml
new file mode 100644
index 0000000..8fd56e0
--- /dev/null
+++ b/stundenplan/pyproject.toml
@@ -0,0 +1,17 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "stundenplan"
+version = "0.0.1"
+
+[tool.setuptools.packages.find]
+# All the following settings are optional:
+where = ["src"]
+
+# [project.scripts]
+# To be done
+# studenplan = "TODO: to-be-done"
+
+
diff --git a/stundenplan/src/pygraph/__init__.py b/stundenplan/src/pygraph/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/stundenplan/src/pygraph/__init__.py
diff --git a/stundenplan/src/pygraph/graphdemo.py b/stundenplan/src/pygraph/graphdemo.py
new file mode 100644
index 0000000..d8669be
--- /dev/null
+++ b/stundenplan/src/pygraph/graphdemo.py
@@ -0,0 +1,117 @@
+from typing import Any, Callable
+
+
+class Graph:
+    """
+    represents an undirected and unweighted Graph
+    """
+    def __init__(self):
+        self._adjacent: dict[int, set] = {}
+        self._vertex_attribute: dict[int, dict] = {}
+
+    def add_vertex(self, vertex:int):
+        if vertex not in self._adjacent:
+            if not isinstance(vertex, int):
+                raise TypeError(f"Argument {vertex} is not a valid vertex")
+            self._adjacent[vertex] = set()
+            self._vertex_attribute[vertex] = {}
+        pass
+
+    def add_edge(self, u: int, v: int):
+        self.add_vertex(u)
+        self.add_vertex(v)
+        v_neighbor = self._adjacent[v]
+        if u not in v_neighbor:
+            v_neighbor.add(u)
+        u_neighbor = self._adjacent[u]
+        if v not in u_neighbor:
+            u_neighbor.add(v)
+        pass
+
+    def add_edges(self, u: int, adjacent: list[int]):
+        for v in adjacent:
+            self.add_edge(u, v)
+        pass
+
+    def merge_attribute(self, vertex: int, properties: dict):
+        """
+        merge a dict of attribute to a vertex. Usage example:
+        ```
+        g = Graph();
+        g.add_vertex(1);
+        g.mergeAttribute(1, {'color': 0});
+        ```
+        :param vertex:
+        :param properties:
+        :return: self
+        """
+        if vertex not in self._adjacent:
+            raise ValueError(f"Graph does not include vertex {vertex}")
+        old_attributes = self._vertex_attribute[vertex]
+        self._vertex_attribute[vertex] = old_attributes | properties
+        return self
+        pass
+
+    def get_attribute(self, vertex: int):
+        if vertex in self._adjacent:
+            return self._vertex_attribute[vertex]
+        else:
+            raise ValueError(f"Graph does not include vertex {vertex}")
+        pass
+
+    def vertices(self):
+        """
+        returns the set of vertices in not defined order.
+        :return:
+        """
+        return self._adjacent.keys()
+
+    def for_each_vertices(self, action: Callable[[int], Any]):
+        """
+        iterates over all vertices in this graph in ascending order.
+        :param action:
+        :return:
+        """
+        for v in sorted( self.vertices() ):
+            action(v)
+
+    def adjacent_of(self, vertex: int):
+        if vertex not in self._adjacent:
+            raise ValueError(f"Graph does not include vertex {vertex}")
+        return sorted( self._adjacent[vertex] )
+
+    def for_each_edges(self, action: Callable[[int, int], Any]):
+        """
+        iterates over all edges of the graph, executes the action on each edge.
+        :param action: a binary function, its parameters are start and end vertices of the visited edges
+        """
+        visited_edges: dict[int, set] = {}
+        for start in sorted( self.vertices() ):
+            for end in self.adjacent_of(start):
+                (first, second) = (start, end) if (start >= end) else (end, start)
+            if first in visited_edges:
+                #visited_adjacent = visited_edges.get(first)
+                visited_adjacent = visited_edges[first]
+                if second not in visited_adjacent:
+                    visited_adjacent.add(second)
+                    action(start, end)
+            else:
+                adjacent: set[int] = set()
+                visited_edges[first] = adjacent
+                adjacent.add(second)
+                action(start, end)
+
+
+
+    def __repr__(self):
+        text = ""
+        sorted_key = sorted(self._adjacent.keys())
+        last_key = sorted_key.pop()
+        def adjacent_fn(vertex): return "[" + ", ".join([str(v) for v in self._adjacent[vertex]]) + "]"
+        for key in sorted_key:
+            text += f"{key} → {adjacent_fn(key)}\n"
+        text += f"{last_key} → {adjacent_fn(last_key)}"
+        return text
+
+    def to_dot(self) -> str:
+        pass
diff --git a/stundenplan/src/pygraph/shortestpath.py b/stundenplan/src/pygraph/shortestpath.py
new file mode 100644
index 0000000..5d3e2af
--- /dev/null
+++ b/stundenplan/src/pygraph/shortestpath.py
@@ -0,0 +1,79 @@
+import collections
+from typing import Mapping
+
+from pygraph.graphdemo import Graph
+
+
+def bfs(g: Graph, start: int) -> Graph:
+    """
+
+    :param g:
+    :param start:
+    :return:
+    """
+    tree = Graph()
+    queue = collections.deque()
+    VISITED = "visited"
+    not_visited = {VISITED: False}
+    visited = {VISITED: True}
+    g.for_each_vertices(lambda v : g.merge_attribute(v, not_visited))
+    queue.append(start)  # append() is the enqueue() operation of a Queue
+    g.merge_attribute(start, visited)
+    while queue:
+        u = queue.popleft()  # popleft() is the dequeue() operation of a Queue
+        for v in g.adjacent_of(u):
+            if not g.get_attribute(v)[VISITED]:
+                tree.add_edge(u, v)
+                g.merge_attribute(v, visited)
+                queue.append(v)
+    return tree
+
+
+def bfs_dict(g: Graph, start: int) -> Mapping[int, int|None]:
+    tree = {start:None}
+    queue = collections.deque()
+    VISITED = "visited"
+    not_visited = {VISITED: False}
+    visited = {VISITED: True}
+    g.for_each_vertices(lambda v : g.merge_attribute(v, not_visited))
+    queue.append(start)  # append() is the enqueue() operation of a Queue
+    g.merge_attribute(start, visited)
+    while queue:
+        u = queue.popleft()  # popleft() is the dequeue() operation of a Queue
+        for v in g.adjacent_of(u):
+            if not g.get_attribute(v)[VISITED]:
+                tree[v] = u
+                g.merge_attribute(v, visited)
+                queue.append(v)
+    return tree
+    pass
+
+
+def find_path(g: Graph, u: int, v: int) -> list[int]:
+    tree = bfs_dict(g, v)
+    path = [u]
+    while (current := path[-1]) != v:
+        precedent = tree[current]
+        path.append(precedent)
+    return path
+
+
+def find_path_gpt(g: Graph, u: int, v: int) -> list[int]:
+    # Erstelle einen BFS-Baum, beginnend bei Knoten v
+    bfs_tree = bfs_dict(g, v)
+
+    # Wenn u nicht in bfs_tree ist, gibt es keinen Pfad von u nach v
+    if u not in bfs_tree:
+        return []  # Kein Pfad gefunden
+
+    # Den Pfad von u nach v rekonstruieren
+    path = []
+    current = u
+    while current is not None:
+        path.append(current)
+        current = bfs_tree[current]
+
+    # Den Pfad umkehren, da wir rückwärts von u nach v gehen
+    path.reverse()
+
+    return path
\ No newline at end of file
diff --git a/stundenplan/src/stundenplan.egg-info/PKG-INFO b/stundenplan/src/stundenplan.egg-info/PKG-INFO
new file mode 100644
index 0000000..121e906
--- /dev/null
+++ b/stundenplan/src/stundenplan.egg-info/PKG-INFO
@@ -0,0 +1,3 @@
+Metadata-Version: 2.1
+Name: stundenplan
+Version: 0.0.1
diff --git a/stundenplan/src/stundenplan.egg-info/SOURCES.txt b/stundenplan/src/stundenplan.egg-info/SOURCES.txt
new file mode 100644
index 0000000..a47d17f
--- /dev/null
+++ b/stundenplan/src/stundenplan.egg-info/SOURCES.txt
@@ -0,0 +1,8 @@
+pyproject.toml
+src/pygraph/__init__.py
+src/pygraph/graphdemo.py
+src/pygraph/shortestpath.py
+src/stundenplan.egg-info/PKG-INFO
+src/stundenplan.egg-info/SOURCES.txt
+src/stundenplan.egg-info/dependency_links.txt
+src/stundenplan.egg-info/top_level.txt
\ No newline at end of file
diff --git a/stundenplan/src/stundenplan.egg-info/dependency_links.txt b/stundenplan/src/stundenplan.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/stundenplan/src/stundenplan.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/stundenplan/src/stundenplan.egg-info/top_level.txt b/stundenplan/src/stundenplan.egg-info/top_level.txt
new file mode 100644
index 0000000..ba98b63
--- /dev/null
+++ b/stundenplan/src/stundenplan.egg-info/top_level.txt
@@ -0,0 +1 @@
+pygraph
diff --git a/stundenplan/tests/__init__.py b/stundenplan/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/stundenplan/tests/__init__.py
diff --git a/stundenplan/tests/pygraph/__init__.py b/stundenplan/tests/pygraph/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/stundenplan/tests/pygraph/__init__.py
diff --git a/stundenplan/tests/pygraph/graphdemo_tests.py b/stundenplan/tests/pygraph/graphdemo_tests.py
new file mode 100644
index 0000000..d8d1ba5
--- /dev/null
+++ b/stundenplan/tests/pygraph/graphdemo_tests.py
@@ -0,0 +1,88 @@
+import unittest
+
+from pygraph.graphdemo import Graph
+
+
+class GraphTestCase(unittest.TestCase):
+    def test_construct_a_graph(self):
+        g = Graph()
+        g.add_edge(1, 2)
+        g.add_edge(1, 3)
+        g.add_edge(2, 3)
+        g.add_edge(2, 4)
+        txt = str(g)
+        expected = '''1 → [2, 3]
+2 → [1, 3, 4]
+3 → [1, 2]
+4 → [2]'''
+        self.assertEqual(expected, txt)
+        vertices = [1, 2, 3, 4]
+        # def action(v): self.assertTrue(v in vertices)
+        g.for_each_vertices(lambda v : self.assertTrue(v in vertices))
+
+    def test_iterate_all_edges(self):
+        g = Graph()
+        g.add_edge(1, 2)
+        g.add_edge(1, 3)
+        g.add_edge(2, 3)
+        g.add_edge(2, 4)
+        edges = [
+            "1-2", "1-3", "2-3", "2-4"
+        ]
+        def action(a, b):
+            (s,e) = (a,b) if a < b else (b,a)
+            edge = f"{s}-{e}"
+            self.assertTrue(edge in edges)
+        g.for_each_edges(action)
+
+    def test_iterate_all_vertices(self):
+        g = Graph()
+        g.add_edge(1, 2)
+        g.add_edge(1, 3)
+        g.add_edge(2, 3)
+        g.add_edge(2, 4)
+        vertices = []
+        for v in g.vertices():
+            vertices.append(v)
+        expected = [1, 2, 3, 4]
+        self.assertListEqual(sorted(vertices), expected)
+        pass
+
+    def test_iterate_neighbor_vertex(self):
+        g = Graph()
+        g.add_edge(1, 2)
+        g.add_edge(1, 3)
+        g.add_edge(2, 3)
+        g.add_edge(2, 4)
+        g.add_edge(4, 1)
+        vertices = []
+        for v in g.adjacent_of(1):
+            vertices.append(v)
+        expected = [2, 3, 4]
+        self.assertListEqual(sorted(vertices), expected)
+        pass
+
+    def test_not_add_float_to_vertex(self):
+        g = Graph()
+        try:
+            g.add_vertex(1.2)
+            pass
+        except TypeError as ex:
+            expected_msg = "Argument 1.2 is not a valid vertex"
+            self.assertEqual(str(ex), expected_msg)
+            pass
+        pass
+
+    def test_not_add_string_to_vertex(self):
+        g = Graph()
+        try:
+            g.add_vertex("add")
+        except TypeError as ex:
+            expected_msg = 'Argument add is not a valid vertex'
+            self.assertEqual(str(ex), expected_msg)
+            pass
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/stundenplan/tests/pygraph/shortestpath_tests.py b/stundenplan/tests/pygraph/shortestpath_tests.py
new file mode 100644
index 0000000..dc914a1
--- /dev/null
+++ b/stundenplan/tests/pygraph/shortestpath_tests.py
@@ -0,0 +1,57 @@
+import unittest
+
+from pygraph.graphdemo import Graph
+from pygraph.shortestpath import bfs, bfs_dict, find_path, find_path_gpt
+
+
+class ShortestPathTestCase(unittest.TestCase):
+    def test_bfs(self):
+        g = Graph()
+        g.add_edges(0, [1, 2])
+        g.add_edges(1, [3, 4])
+        g.add_edges(2, [4, 6, 7])
+        g.add_edges(4, [5])
+        print(g)
+        print("#")
+        tree = bfs(g, 0)
+        print(tree)
+
+    def test_bfs_dict(self):
+        g = Graph()
+        g.add_edges(0, [1, 2])
+        g.add_edges(1, [3, 4])
+        g.add_edges(2, [4, 6, 7])
+        g.add_edges(4, [5])
+        print(g)
+        tree = bfs_dict(g, 6)
+        print("Tree from Vertex 6")
+        print(tree)
+
+    def test_find_path(self):
+        g = Graph()
+        g.add_edges(0, [1, 2])
+        g.add_edges(1, [3, 4])
+        g.add_edges(2, [4, 6, 7])
+        g.add_edges(4, [5])
+        print(g)
+        (u, v) = (6, 5)
+        path = find_path(g, 6, 5)
+        print(f"path from {u} to {v}")
+        print(path)
+
+    def test_find_path_gpt(self):
+        g = Graph()
+        g.add_edges(0, [1, 2])
+        g.add_edges(1, [3, 4])
+        g.add_edges(2, [4, 6, 7])
+        g.add_edges(4, [5])
+        print(g)
+        (u, v) = (6, 5)
+        path = find_path_gpt(g, 6, 5)
+        print(f"path from {u} to {v}")
+        print(path)
+
+
+
+if __name__ == '__main__':
+    unittest.main()

--
Gitblit v1.10.0