Initial commit

This commit is contained in:
github-classroom[bot] 2025-04-24 07:02:41 +00:00 committed by GitHub
commit 1be1863b20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 6449 additions and 0 deletions

3551
.editorconfig Normal file

File diff suppressed because it is too large Load diff

583
.gitignore vendored Normal file
View file

@ -0,0 +1,583 @@
# Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio
# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio ###
# User-specific files
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
# Build results
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
# MSTest test Results
# NUnit
# Build Results of an ATL Project
# Benchmark Results
# .NET Core
# ASP.NET Scaffolding
# StyleCop
# Files built by Visual Studio
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
# Coverlet is a free, cross platform Code Coverage Tool
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
# NuGet Packages
# NuGet Symbol Packages
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
# Uncomment if necessary however generally it will be regenerated when needed
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
# SQL Server files
# Business Intelligence projects
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
# Telerik's JustMock configuration file
# BizTalk build output
# OpenCover UI analysis results
# Azure Stream Analytics local run output
# MSBuild Binary and Structured Log
# NVidia Nsight GPU debugger configuration file
# MFractors (Xamarin productivity tool) working folder
# Local History for Visual Studio
# Visual Studio History (VSHistory) files
# BeatPulse healthcheck temp database
# Backup folder for Package Reference Convert tool in Visual Studio 2017
# Ionide (cross platform F# VS Code tools) working folder
# Fody - auto-generated XML schema
# VS Code files for those working on multiple tools
# Local History for Visual Studio Code
# Windows Installer files from build outputs
# JetBrains Rider
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio

View file

@ -0,0 +1,32 @@
12240;Baar Erich
12242;Bauer Peter
12244;Baumann Daniel
12246;Braun Matthias
12249;Bucek Michael
12252;Dilly Franz
12254;Eder Lisa
12256;Ehrenmüller Markus
12257;Eisserer Clemens
12265;Fürlinger Josef
12281;Holzleitner Jürgen
12284;Huemer Martin
12287;Kainerstorfer Richard
12288;Kaiser Alexander
12289;Karpowicz Michal
12292;Keck-Jordan Andrea
12294;Kerschner Martin
12295;Kiesenhofer Maximilian
12297;Knogler Kerstin
12303;Kurz Matthias
12305;Leitner Helmut
12324;Pachschwöll Cornelia
12331;Raschhofer Robert
12332;Reder Robert
12336;Reisinger Hannes
12341;Schmidt Silke
12350;Steiner Dietmar
12351;Steiner Martin
12357;Stütz Thomas
12363;Venzl Bernhard
12364;Wagner Michael
12368;Wiedermann Alfred
1 12240 Baar Erich
2 12242 Bauer Peter
3 12244 Baumann Daniel
4 12246 Braun Matthias
5 12249 Bucek Michael
6 12252 Dilly Franz
7 12254 Eder Lisa
8 12256 Ehrenmüller Markus
9 12257 Eisserer Clemens
10 12265 Fürlinger Josef
11 12281 Holzleitner Jürgen
12 12284 Huemer Martin
13 12287 Kainerstorfer Richard
14 12288 Kaiser Alexander
15 12289 Karpowicz Michal
16 12292 Keck-Jordan Andrea
17 12294 Kerschner Martin
18 12295 Kiesenhofer Maximilian
19 12297 Knogler Kerstin
20 12303 Kurz Matthias
21 12305 Leitner Helmut
22 12324 Pachschwöll Cornelia
23 12331 Raschhofer Robert
24 12332 Reder Robert
25 12336 Reisinger Hannes
26 12341 Schmidt Silke
27 12350 Steiner Dietmar
28 12351 Steiner Martin
29 12357 Stütz Thomas
30 12363 Venzl Bernhard
31 12364 Wagner Michael
32 12368 Wiedermann Alfred

View file

@ -0,0 +1,2 @@
12240;Baar Erich
12242;Bauer Peter
1 12240 Baar Erich
2 12242 Bauer Peter

140
Data/teachers.csv Normal file
View file

@ -0,0 +1,140 @@
Aberger Christian;Tuesday;6;12:45 - 13:35;E74
Aistleitner Gerald;Thursday;6;12:45 - 13:35;206
Aitenbichler Herbert;Wednesday;2;08:55 - 09:45;254
Andraschko Benjamin;Wednesday;5;11:50 - 12:40;T4
Anzenberger Peter;Thursday;6;12:45 - 13:35;E31
Apollonio Eva-Maria;Monday;5;11:50 - 12:40;223
Arzt Miriam;Friday;3;10:00 - 10:50;223
Atak Mehmet;;;;
Auernig Franz;Thursday;5;11:50 - 12:40;E31
Baar Erich;Friday;4;10:55 - 11:45;E05
Bärnthaler Andreas;Wednesday;4;10:55 - 11:45;237
Bauer Peter;Tuesday;6;12:45 - 13:35;E52
Bauernfeind Raphael;;;;
Baumann Daniel;Wednesday;6;12:45 - 13:35;148
Bodenstorfer Bernhard;Tuesday;3;10:00 - 10:50;254
Braun Matthias;Friday;3;10:00 - 10:50;E31
Brenn Rosemarie;Tuesday;6;12:45 - 13:35;E05
Brückner Margit;;;;
Bucek Michael;Wednesday;5;11:50 - 12:40;103
Dellinger Franz;Tuesday;2;08:55 - 09:45;E08
Denkmair Rainer;Tuesday;7;13:40 - 14:30;206
Dilly Franz;Wednesday;2;08:55 - 09:45;223
Draxlbauer Josef;Thursday;5;11:50 - 12:40;237
Eder Lisa;Wednesday;6;12:45 - 13:35;E08
Ehrenberger Günther Ferdinand;Wednesday;2;08:55 - 09:45;E51
Ehrenmüller Markus;;;;
Eisserer Clemens;;;;
Engleitner Patricia;Tuesday;3;10:00 - 10:50;254
Enzenhofer Mario;;;;
Eppich Christina;Thursday;3;10:00 - 10:50;T07
Ernecker Björn;Friday;8;14:35 - 15:25;E06
Fallmann-Raupach Gerhild;Tuesday;5;11:50 - 12:40;T07
Felsner Anja;Friday;4;10:55 - 11:45;E06
Frey Peter;Thursday;5;11:50 - 12:40;103
Fürlinger Josef;Thursday;7;13:40 - 14:30;E31
Gallistl Andreas;Wednesday;5;11:50 - 12:40;U92
Gallner-Holzmann Katharina;Wednesday;5;11:50 - 12:40;241
Gehrer Gerhard;Thursday;4;10:55 - 11:45;254
Giritzhofer Rudolf;Wednesday;4;10:55 - 11:45;218
Gruber-Leitner Franz;Monday;6;12:45 - 13:35;E31
Günther Harald;Wednesday;5;11:50 - 12:40;U08
Hackenbuchner Franz;Wednesday;5;11:50 - 12:40;U92
Hammer Hans-Christian;Wednesday;6;12:45 - 13:35;148
Hannesschläger Jürgen;;;;
Haslinger Klaus;Tuesday;5;11:50 - 12:40;U92
Haslinger Markus;Monday;3;10:00 - 10:50;103
Haunschmid Wilfried;;;;
Hinterramskogler Britta;Thursday;6;12:45 - 13:35;237
Höfer Gerhard;Monday;3;10:00 - 10:50;254
Hofer Gerhard;;;;
Holzleitner Jürgen;Monday;5;11:50 - 12:40;222
Holzmann Michael;Wednesday;5;11:50 - 12:40;E08
Huemer Elke;;;;
Huemer Martin;Tuesday;4;10:55 - 11:45;148
Jäger Christian;Thursday;3;10:00 - 10:50;T13
Jakob Franz;Wednesday;3;10:00 - 10:50;E08
Kainerstorfer Richard;Friday;3;10:00 - 10:50;E38
Kaiser Alexander;Tuesday;5;11:50 - 12:40;U87
Karpowicz Michal;Monday;5;11:50 - 12:40;E32
Kasberger Johannes;Tuesday;3;10:00 - 10:50;222
Kaser Christof;Friday;4;10:55 - 11:45;E31
Keck-Jordan Andrea;Tuesday;7;13:40 - 14:30;E08
Keplinger Edith;Monday;8;14:35 - 15:25;221
Kerschner Martin;Tuesday;3;10:00 - 10:50;T04
Kiesenhofer Maximilian;Thursday;3;10:00 - 10:50;E05
Klewein David;Tuesday;10;16:25 - 17:15;206
Knogler Kerstin;Tuesday;4;10:55 - 11:45;203
Kodre Christina;Thursday;3;10:00 - 10:50;226
Köck Gerald;Tuesday;4;10:55 - 11:45;E74
Köck Johann;Thursday;4;10:55 - 11:45;E32
Köfler Andreas;;;;
Kornfellner Alexander;Monday;4;10:55 - 11:45;E32
Kurz Matthias;Thursday;4;10:55 - 11:45;E53
Lehenbauer Christian;Monday;4;10:55 - 11:45;142
Leitner Helmut;Wednesday;11;17:20 - 18:05;E32
Lüftner Markus;Tuesday;5;11:50 - 12:40;E51
Luger Angelika;Wednesday;5;11:50 - 12:40;223
Lugmayr Karin;Tuesday;3;10:00 - 10:50;E08
Luttinger Christian;Monday;7;13:40 - 14:30;U92
Lutz Wolfgang;Thursday;5;11:50 - 12:40;E53
Mader Cornelia;Tuesday;5;11:50 - 12:40;226
Matzinger Barbara;Tuesday;5;11:50 - 12:40;218
Mischitz Gernot;Tuesday;6;12:45 - 13:35;E05
Mistelberger Wolfgang;Thursday;6;12:45 - 13:35;E51
Mitsea Eirini;;;;
Möschl Edith;Thursday;3;10:00 - 10:50;E32
Moser Tamara;Friday;4;10:55 - 11:45;142
Mühleder Regina;Thursday;11;17:20 - 18:05;237
Mühlehner Philipp;Tuesday;3;10:00 - 10:50;254
Niedermaier Maren;;;;
Nimmervoll Raimund;Monday;5;11:50 - 12:40;E51
Nöbauer Klaus;Tuesday;5;11:50 - 12:40;103
Oswald Alois;Tuesday;4;10:55 - 11:45;U92
Pachschwöll Cornelia;Wednesday;4;10:55 - 11:45;241
Palitsch-Infanger Michael;Friday;2;08:55 - 09:45;222
Povacz Katharina;Friday;3;10:00 - 10:50;203
Prantl Anton;Thursday;3;10:00 - 10:50;E53
Rager Franz;Thursday;2;08:55 - 09:45;E74
Rammelmüller Natascha;Thursday;3;10:00 - 10:50;148
Rammer Alfred;Thursday;2;08:55 - 09:45;241
Raschhofer Robert;Monday;5;11:50 - 12:40;E53
Reder Robert;Monday;6;12:45 - 13:35;E74
Reichinger Josef;Tuesday;3;10:00 - 10:50;E08
Reisenberger Martina;Tuesday;5;11:50 - 12:40;103
Reisinger Elisabeth;;;;
Reisinger Hannes;Wednesday;6;12:45 - 13:35;U91
Reiter Riccarda;Monday;6;12:45 - 13:35;218
Rumetshofer Elisabeth;Monday;9;15:30 - 16:20;203
Scheinecker Silke;Wednesday;3;10:00 - 10:50;203
Schlesinger Ralf;Friday;4;10:55 - 11:45;E53
Schmidt Silke;Thursday;2;08:55 - 09:45;241
Schmidtgrabmer Doris;Thursday;4;10:55 - 11:45;E74
Schnalzenberger Mario;;;;
Schneider Klaus Michael;;;;
Schraml Stefan;Tuesday;7;13:40 - 14:30;148
Schröder Birgit;Thursday;4;10:55 - 11:45;103
Schwaiger Barbara;Wednesday;4;10:55 - 11:45;E06
Sokoli Anila;Thursday;2;08:55 - 09:45;E08
Sonnleitner Erik;Wednesday;3;10:00 - 10:50;E31
Steiner Dietmar;Wednesday;6;12:45 - 13:35;E32
Steiner Martin;Tuesday;5;11:50 - 12:40;U92
Sternath Martin;Wednesday;3;10:00 - 10:50;221
Stöpp Gerhard;;;;
Stöttinger Robert;Friday;2;08:55 - 09:45;206
Strecker Alexander;;;;
Stropek Rainer;;;;
Stütz Thomas;Monday;5;11:50 - 12:40;E74
Täubel Michael;Monday;2;08:55 - 09:45;T13
Thieme Linda;;;;
Traunmüller Günter;Tuesday;3;10:00 - 10:50;206
Tumfart Johannes;Tuesday;2;08:55 - 09:45;E74
Unterrainer Gerald;Tuesday;4;10:55 - 11:45;222
Venzl Bernhard;Tuesday;5;11:50 - 12:40;U92
Wagner Michael;Wednesday;5;11:50 - 12:40;U92
Weißmair Rudolf;Monday;5;11:50 - 12:40;E08
Wellisch Alexandra;Monday;3;10:00 - 10:50;226
Wenigwieser Siegfried;Wednesday;5;11:50 - 12:40;U92
Wiedermann Alfred;Friday;4;10:55 - 11:45;E55
Willmann Renate;Wednesday;5;11:50 - 12:40;218
Wingert Edwin;Wednesday;7;13:40 - 14:30;T09
1 Aberger Christian Tuesday 6 12:45 - 13:35 E74
2 Aistleitner Gerald Thursday 6 12:45 - 13:35 206
3 Aitenbichler Herbert Wednesday 2 08:55 - 09:45 254
4 Andraschko Benjamin Wednesday 5 11:50 - 12:40 T4
5 Anzenberger Peter Thursday 6 12:45 - 13:35 E31
6 Apollonio Eva-Maria Monday 5 11:50 - 12:40 223
7 Arzt Miriam Friday 3 10:00 - 10:50 223
8 Atak Mehmet
9 Auernig Franz Thursday 5 11:50 - 12:40 E31
10 Baar Erich Friday 4 10:55 - 11:45 E05
11 Bärnthaler Andreas Wednesday 4 10:55 - 11:45 237
12 Bauer Peter Tuesday 6 12:45 - 13:35 E52
13 Bauernfeind Raphael
14 Baumann Daniel Wednesday 6 12:45 - 13:35 148
15 Bodenstorfer Bernhard Tuesday 3 10:00 - 10:50 254
16 Braun Matthias Friday 3 10:00 - 10:50 E31
17 Brenn Rosemarie Tuesday 6 12:45 - 13:35 E05
18 Brückner Margit
19 Bucek Michael Wednesday 5 11:50 - 12:40 103
20 Dellinger Franz Tuesday 2 08:55 - 09:45 E08
21 Denkmair Rainer Tuesday 7 13:40 - 14:30 206
22 Dilly Franz Wednesday 2 08:55 - 09:45 223
23 Draxlbauer Josef Thursday 5 11:50 - 12:40 237
24 Eder Lisa Wednesday 6 12:45 - 13:35 E08
25 Ehrenberger Günther Ferdinand Wednesday 2 08:55 - 09:45 E51
26 Ehrenmüller Markus
27 Eisserer Clemens
28 Engleitner Patricia Tuesday 3 10:00 - 10:50 254
29 Enzenhofer Mario
30 Eppich Christina Thursday 3 10:00 - 10:50 T07
31 Ernecker Björn Friday 8 14:35 - 15:25 E06
32 Fallmann-Raupach Gerhild Tuesday 5 11:50 - 12:40 T07
33 Felsner Anja Friday 4 10:55 - 11:45 E06
34 Frey Peter Thursday 5 11:50 - 12:40 103
35 Fürlinger Josef Thursday 7 13:40 - 14:30 E31
36 Gallistl Andreas Wednesday 5 11:50 - 12:40 U92
37 Gallner-Holzmann Katharina Wednesday 5 11:50 - 12:40 241
38 Gehrer Gerhard Thursday 4 10:55 - 11:45 254
39 Giritzhofer Rudolf Wednesday 4 10:55 - 11:45 218
40 Gruber-Leitner Franz Monday 6 12:45 - 13:35 E31
41 Günther Harald Wednesday 5 11:50 - 12:40 U08
42 Hackenbuchner Franz Wednesday 5 11:50 - 12:40 U92
43 Hammer Hans-Christian Wednesday 6 12:45 - 13:35 148
44 Hannesschläger Jürgen
45 Haslinger Klaus Tuesday 5 11:50 - 12:40 U92
46 Haslinger Markus Monday 3 10:00 - 10:50 103
47 Haunschmid Wilfried
48 Hinterramskogler Britta Thursday 6 12:45 - 13:35 237
49 Höfer Gerhard Monday 3 10:00 - 10:50 254
50 Hofer Gerhard
51 Holzleitner Jürgen Monday 5 11:50 - 12:40 222
52 Holzmann Michael Wednesday 5 11:50 - 12:40 E08
53 Huemer Elke
54 Huemer Martin Tuesday 4 10:55 - 11:45 148
55 Jäger Christian Thursday 3 10:00 - 10:50 T13
56 Jakob Franz Wednesday 3 10:00 - 10:50 E08
57 Kainerstorfer Richard Friday 3 10:00 - 10:50 E38
58 Kaiser Alexander Tuesday 5 11:50 - 12:40 U87
59 Karpowicz Michal Monday 5 11:50 - 12:40 E32
60 Kasberger Johannes Tuesday 3 10:00 - 10:50 222
61 Kaser Christof Friday 4 10:55 - 11:45 E31
62 Keck-Jordan Andrea Tuesday 7 13:40 - 14:30 E08
63 Keplinger Edith Monday 8 14:35 - 15:25 221
64 Kerschner Martin Tuesday 3 10:00 - 10:50 T04
65 Kiesenhofer Maximilian Thursday 3 10:00 - 10:50 E05
66 Klewein David Tuesday 10 16:25 - 17:15 206
67 Knogler Kerstin Tuesday 4 10:55 - 11:45 203
68 Kodre Christina Thursday 3 10:00 - 10:50 226
69 Köck Gerald Tuesday 4 10:55 - 11:45 E74
70 Köck Johann Thursday 4 10:55 - 11:45 E32
71 Köfler Andreas
72 Kornfellner Alexander Monday 4 10:55 - 11:45 E32
73 Kurz Matthias Thursday 4 10:55 - 11:45 E53
74 Lehenbauer Christian Monday 4 10:55 - 11:45 142
75 Leitner Helmut Wednesday 11 17:20 - 18:05 E32
76 Lüftner Markus Tuesday 5 11:50 - 12:40 E51
77 Luger Angelika Wednesday 5 11:50 - 12:40 223
78 Lugmayr Karin Tuesday 3 10:00 - 10:50 E08
79 Luttinger Christian Monday 7 13:40 - 14:30 U92
80 Lutz Wolfgang Thursday 5 11:50 - 12:40 E53
81 Mader Cornelia Tuesday 5 11:50 - 12:40 226
82 Matzinger Barbara Tuesday 5 11:50 - 12:40 218
83 Mischitz Gernot Tuesday 6 12:45 - 13:35 E05
84 Mistelberger Wolfgang Thursday 6 12:45 - 13:35 E51
85 Mitsea Eirini
86 Möschl Edith Thursday 3 10:00 - 10:50 E32
87 Moser Tamara Friday 4 10:55 - 11:45 142
88 Mühleder Regina Thursday 11 17:20 - 18:05 237
89 Mühlehner Philipp Tuesday 3 10:00 - 10:50 254
90 Niedermaier Maren
91 Nimmervoll Raimund Monday 5 11:50 - 12:40 E51
92 Nöbauer Klaus Tuesday 5 11:50 - 12:40 103
93 Oswald Alois Tuesday 4 10:55 - 11:45 U92
94 Pachschwöll Cornelia Wednesday 4 10:55 - 11:45 241
95 Palitsch-Infanger Michael Friday 2 08:55 - 09:45 222
96 Povacz Katharina Friday 3 10:00 - 10:50 203
97 Prantl Anton Thursday 3 10:00 - 10:50 E53
98 Rager Franz Thursday 2 08:55 - 09:45 E74
99 Rammelmüller Natascha Thursday 3 10:00 - 10:50 148
100 Rammer Alfred Thursday 2 08:55 - 09:45 241
101 Raschhofer Robert Monday 5 11:50 - 12:40 E53
102 Reder Robert Monday 6 12:45 - 13:35 E74
103 Reichinger Josef Tuesday 3 10:00 - 10:50 E08
104 Reisenberger Martina Tuesday 5 11:50 - 12:40 103
105 Reisinger Elisabeth
106 Reisinger Hannes Wednesday 6 12:45 - 13:35 U91
107 Reiter Riccarda Monday 6 12:45 - 13:35 218
108 Rumetshofer Elisabeth Monday 9 15:30 - 16:20 203
109 Scheinecker Silke Wednesday 3 10:00 - 10:50 203
110 Schlesinger Ralf Friday 4 10:55 - 11:45 E53
111 Schmidt Silke Thursday 2 08:55 - 09:45 241
112 Schmidtgrabmer Doris Thursday 4 10:55 - 11:45 E74
113 Schnalzenberger Mario
114 Schneider Klaus Michael
115 Schraml Stefan Tuesday 7 13:40 - 14:30 148
116 Schröder Birgit Thursday 4 10:55 - 11:45 103
117 Schwaiger Barbara Wednesday 4 10:55 - 11:45 E06
118 Sokoli Anila Thursday 2 08:55 - 09:45 E08
119 Sonnleitner Erik Wednesday 3 10:00 - 10:50 E31
120 Steiner Dietmar Wednesday 6 12:45 - 13:35 E32
121 Steiner Martin Tuesday 5 11:50 - 12:40 U92
122 Sternath Martin Wednesday 3 10:00 - 10:50 221
123 Stöpp Gerhard
124 Stöttinger Robert Friday 2 08:55 - 09:45 206
125 Strecker Alexander
126 Stropek Rainer
127 Stütz Thomas Monday 5 11:50 - 12:40 E74
128 Täubel Michael Monday 2 08:55 - 09:45 T13
129 Thieme Linda
130 Traunmüller Günter Tuesday 3 10:00 - 10:50 206
131 Tumfart Johannes Tuesday 2 08:55 - 09:45 E74
132 Unterrainer Gerald Tuesday 4 10:55 - 11:45 222
133 Venzl Bernhard Tuesday 5 11:50 - 12:40 U92
134 Wagner Michael Wednesday 5 11:50 - 12:40 U92
135 Weißmair Rudolf Monday 5 11:50 - 12:40 E08
136 Wellisch Alexandra Monday 3 10:00 - 10:50 226
137 Wenigwieser Siegfried Wednesday 5 11:50 - 12:40 U92
138 Wiedermann Alfred Friday 4 10:55 - 11:45 E55
139 Willmann Renate Wednesday 5 11:50 - 12:40 218
140 Wingert Edwin Wednesday 7 13:40 - 14:30 T09

View file

@ -0,0 +1,4 @@
Aberger Christian;Tuesday;6;12:45 - 13:35;E74
Atak Mehmet;;;;
Auernig Franz;Thursday;5;11:50 - 12:40;E31
Baar Erich;Friday;4;10:55 - 11:45;E05
1 Aberger Christian Tuesday 6 12:45 - 13:35 E74
2 Atak Mehmet
3 Auernig Franz Thursday 5 11:50 - 12:40 E31
4 Baar Erich Friday 4 10:55 - 11:45 E05

View file

@ -0,0 +1,52 @@
using MeetTheTeacher.Export;
namespace MeetTheTeacher.Test.Export;
public sealed class CsvDataExporterTests : ExporterTestBase, IDisposable
{
private readonly CsvDataExporter _csvDataExporter;
public CsvDataExporterTests()
{
_csvDataExporter = new CsvDataExporter();
}
protected override string FileExtension => "csv";
public void Dispose()
{
// Clean up the test file.
if (File.Exists(FilePath))
{
File.Delete(FilePath);
}
}
[Fact]
public void Export_Valid()
{
SimpleCheck();
}
[Fact]
public void Export_FileExisted()
{
File.WriteAllText(FilePath, "some content");
// still has to work
SimpleCheck();
}
private void SimpleCheck()
{
_csvDataExporter.Export(SampleData, FileName);
File.Exists(FilePath).Should().BeTrue();
string[] rows = File.ReadAllLines(FilePath);
rows.Length.Should().Be(3, "Header + 2 data rows");
rows[0].Should().Be("Name;Age");
rows[1].Should().Be("John;30");
rows[2].Should().Be("Jane;25");
}
}

View file

@ -0,0 +1,39 @@
using MeetTheTeacher.Export;
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Export;
public sealed class DefaultExporterProviderTests
{
private readonly DefaultExporterProvider _provider;
public DefaultExporterProviderTests()
{
_provider = new DefaultExporterProvider();
}
[Fact]
public void GetExporter_CsvFormat()
{
_provider.GetExporter(ExportFormat.Csv)
.Should().BeOfType<CsvDataExporter>();
}
[Fact]
public void GetExporter_HtmlFormat()
{
_provider.GetExporter(ExportFormat.Html)
.Should().BeOfType<HtmlDataExporter>();
}
[Fact]
public void GetExporter_InvalidFormat()
{
var exporterProvider = new DefaultExporterProvider();
Action act = () => exporterProvider.GetExporter((ExportFormat) (-1));
act.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("*exportFormat*");
}
}

View file

@ -0,0 +1,26 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Export;
public abstract class ExporterTestBase
{
protected static string FileName => "testFile";
protected string FilePath => Path.Combine(Directory.GetCurrentDirectory(), $"{FileName}.{FileExtension}");
protected abstract string FileExtension { get; }
protected static IEnumerable<ICsvRepresentable> SampleData =>
new List<ICsvRepresentable>
{
new TestData { Name = "John", Age = 30 },
new TestData { Name = "Jane", Age = 25 }
};
protected class TestData : ICsvRepresentable
{
public required string Name { get; init; }
public int Age { get; init; }
public CsvData ToCsvData() =>
new(new List<string> { "Name", "Age" }, new List<string> { Name, Age.ToString() });
}
}

View file

@ -0,0 +1,67 @@
using System.Reflection;
using MeetTheTeacher.Export;
using MeetTheTeacher.Model;
using MeetTheTeacher.Model.Comparison;
namespace MeetTheTeacher.Test.Export;
public sealed class ExporterTests
{
[Theory]
[MemberData(nameof(UseProperComparerData))]
public void Comparer(SortOrder order, Type expectedComparer, bool? expectedAscendingFlag)
{
// Note: not a pretty test, private stuff is only tested if it comes from students ;)
var exporter = new Exporter(order, ExportFormat.Html, Substitute.For<IExporterProvider>());
CheckComparerField(exporter, expectedComparer, expectedAscendingFlag);
}
[Fact]
public void Export()
{
const string FileName = "test";
string? capturedFileName = null;
var exporterMock = Substitute.For<IDataExporter>();
exporterMock.Export(Arg.Any<List<Teacher>>(),
Arg.Do<string>(fn => capturedFileName = fn));
var providerMock = Substitute.For<IExporterProvider>();
providerMock.GetExporter(Arg.Is(ExportFormat.Html)).Returns(exporterMock);
var exporter = new Exporter(SortOrder.ByHourDesc, ExportFormat.Html, providerMock);
exporter.Export(FileName, new());
capturedFileName.Should().NotBeNull().And.Be(FileName);
}
public static TheoryData<SortOrder, Type, bool?> UseProperComparerData =>
new()
{
{SortOrder.ByNameAsc, typeof(ByNameComparer), true},
{SortOrder.ByNameDesc, typeof(ByNameComparer), false},
{SortOrder.ByHourAsc, typeof(ByHourComparer), true},
{SortOrder.ByHourDesc, typeof(ByHourComparer), false},
{SortOrder.Room, typeof(ByRoomComparer), null}
};
private static void CheckComparerField(Exporter exporter, Type expectedType, bool? ascending)
{
var field = typeof(Exporter).GetField("_comparer",
BindingFlags.Instance | BindingFlags.NonPublic);
field?.GetValue(exporter).Should().NotBeNull()
.And.BeOfType(expectedType);
if (ascending.HasValue && expectedType.IsSubclassOf(typeof(ComparerBase)))
{
var ascendingField = expectedType.GetProperty("Ascending",
BindingFlags.Instance | BindingFlags.NonPublic);
ascendingField?.GetValue(field!.GetValue(exporter))
.Should().NotBeNull()
.And.Be(ascending.Value);
}
}
}

View file

@ -0,0 +1,49 @@
using MeetTheTeacher.Export;
namespace MeetTheTeacher.Test.Export;
public sealed class HtmlDataExporterTests : ExporterTestBase, IDisposable
{
private readonly HtmlDataExporter _htmlDataExporter;
public HtmlDataExporterTests()
{
_htmlDataExporter = new HtmlDataExporter();
}
protected override string FileExtension => "html";
public void Dispose()
{
// Clean up the test file.
if (File.Exists(FilePath))
{
File.Delete(FilePath);
}
}
[Fact]
public void Export()
{
_htmlDataExporter.Export(SampleData, FileName);
File.Exists(FilePath).Should().BeTrue();
string[]? rows = File.ReadAllLines(FilePath);
void Check(int rowIdx, string expected)
{
rows[rowIdx].Trim().Should().Be(expected);
}
Check(0, "<table>");
Check(1, "<thead>");
Check(2, "<tr><th>Name</th><th>Age</th></tr>");
Check(3, "</thead>");
Check(4, "<tbody>");
Check(5, "<tr><td>John</td><td>30</td></tr>");
Check(6, "<tr><td>Jane</td><td>25</td></tr>");
Check(7, "</tbody>");
Check(8, "</table>");
}
}

View file

@ -0,0 +1,30 @@
using MeetTheTeacher.Export;
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Export;
public sealed class SpectreTableExporterTests: ExporterTestBase
{
private readonly SpectreTableExporter _spectreTableExporter;
public SpectreTableExporterTests()
{
_spectreTableExporter = new SpectreTableExporter();
}
[Fact]
public void Export()
{
_spectreTableExporter.Export(SampleData, FileName);
var table = _spectreTableExporter.Table;
table.Should().NotBeNull();
table.Columns.Should().HaveCount(2);
table.Rows.Should().HaveCount(2);
// actual (string) values cannot be checked without reflection on an external library which will
// inevitably break and is a super bad idea, so we'll leave it at this
}
protected override string FileExtension => null!;
}

View file

@ -0,0 +1,52 @@
using MeetTheTeacher.Import;
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Import;
public sealed class TeacherDataCsvImporterTests
{
[Fact]
public void DataLoading_Simple()
{
var importer = new TeacherDataCsvImporter(GetPath("teachers_minimal.csv"),
GetPath("teachers-with-business-card_minimal.csv"));
IEnumerable<Teacher> data = importer.LoadTeacherData();
// wrapping in list, because enumerating multiple times
var dataAsList = new List<Teacher>(data);
dataAsList.Should().NotBeEmpty().And.HaveCount(4);
CheckTeacher(dataAsList[0], "Aberger Christian", DayOfWeek.Tuesday, SchoolUnit.UE06,
new TimeFrame(new(12,45), new(13,35)),
"E74", null);
CheckTeacher(dataAsList[1], "Atak Mehmet", null, null, null, null, null);
CheckTeacher(dataAsList[2], "Auernig Franz", DayOfWeek.Thursday, SchoolUnit.UE05,
new TimeFrame(new(11,50), new(12,40)),
"E31", null);
CheckTeacher(dataAsList[3], "Baar Erich", DayOfWeek.Friday, SchoolUnit.UE04,
new TimeFrame(new(10,55), new(11,45)),
"E05", 12240);
}
private static void CheckTeacher(Teacher teacher, string name, DayOfWeek? dayOfWeek, SchoolUnit? unit,
TimeFrame? time, string? room, int? imageId)
{
teacher.Name.Should().Be(name);
teacher.ConsultingHourWeekDay.Should().Be(dayOfWeek);
teacher.ConsultingHourUnit.Should().Be(unit);
teacher.ConsultingHour.Should().Be(time);
teacher.Room.Should().Be(room);
if (imageId.HasValue)
{
teacher.Should().BeOfType<TeacherWithBusinessCard>();
}
else
{
teacher.Should().NotBeOfType<TeacherWithBusinessCard>();
}
}
private static string GetPath(string fileName) => Path.Combine(Directory.GetCurrentDirectory(), $"Data/{fileName}");
}

View file

@ -0,0 +1,41 @@
using MeetTheTeacher.Import;
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Import;
// such a class would rarely exist in production code
// so this test is rather silly - doing it for completeness anyway
public sealed class TeacherDataFakeImporterTests
{
private readonly TeacherDataFakeImporter _teacherDataFakeImporter;
public TeacherDataFakeImporterTests()
{
_teacherDataFakeImporter = new TeacherDataFakeImporter();
}
[Fact]
public void LoadTeacherData_ReturnsExpectedData()
{
var result = _teacherDataFakeImporter.LoadTeacherData().ToList();
result.Should().HaveCount(6);
result[0].Name.Should().Be("John Doe");
result[0].ConsultingHour.Should().Be(new TimeFrame(new TimeOnly(8, 0), new TimeOnly(8, 50)));
result[0].ConsultingHourUnit.Should().Be(SchoolUnit.UE01);
result[0].ConsultingHourWeekDay.Should().Be(DayOfWeek.Monday);
result[0].Room.Should().Be("A101");
// Add similar assertions for the other Teacher instances
result[4].Name.Should().Be("Emily Davis");
result[4].ConsultingHour.Should().Be(new TimeFrame(new TimeOnly(12, 40), new TimeOnly(13, 35)));
result[4].ConsultingHourUnit.Should().Be(SchoolUnit.UE06);
result[4].ConsultingHourWeekDay.Should().Be(DayOfWeek.Thursday);
result[4].Room.Should().Be("A101");
result[4].Should().BeOfType<TeacherWithBusinessCard>();
((TeacherWithBusinessCard) result[4]).Id.Should().Be(1001);
// Add similar assertions for the other TeacherWithBusinessCard instances
}
}

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="FluentAssertions" />
<Using Include="Xunit" />
<Using Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<Content Include="..\Data\*.csv" Link="Data\%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="8.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MeetTheTeacher\MeetTheTeacher.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,142 @@
using MeetTheTeacher.Model;
using MeetTheTeacher.Model.Comparison;
namespace MeetTheTeacher.Test.Model.Comparison;
public sealed class ByHourComparerTests
{
[Fact]
public void Compare_Ascending_BothNull()
{
var comparer = new ByHourComparer(true);
comparer.Compare(null, null).Should().Be(0);
}
[Fact]
public void Compare_Ascending_FirstNull()
{
var comparer = new ByHourComparer(true);
var teacher2 = new Teacher("Teacher2") {
ConsultingHourWeekDay = DayOfWeek.Tuesday,
ConsultingHourUnit = SchoolUnit.UE02
};
comparer.Compare(null, teacher2).Should().Be(1);
}
[Fact]
public void Compare_Ascending_SecondNull()
{
var comparer = new ByHourComparer(true);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
comparer.Compare(teacher1, null).Should().Be(-1);
}
[Fact]
public void Compare_Ascending_DifferentWeekDays()
{
var comparer = new ByHourComparer(true);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
var teacher2 = new Teacher("Teacher2")
{
ConsultingHourWeekDay = DayOfWeek.Tuesday,
ConsultingHourUnit = SchoolUnit.UE01
};
comparer.Compare(teacher1, teacher2).Should().Be(-1);
}
[Fact]
public void Compare_Ascending_DifferentConsultingHours()
{
var comparer = new ByHourComparer(true);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE02
};
var teacher2 = new Teacher("Teacher2")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
int result = comparer.Compare(teacher1, teacher2);
result.Should().Be(1);
}
[Fact]
public void Compare_Descending_FirstNull()
{
var comparer = new ByHourComparer(false);
var teacher2 = new Teacher("Teacher2")
{
ConsultingHourWeekDay = DayOfWeek.Tuesday,
ConsultingHourUnit = SchoolUnit.UE02
};
comparer.Compare(null, teacher2).Should().Be(-1);
}
[Fact]
public void Compare_Descending_SecondNull()
{
var comparer = new ByHourComparer(false);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
comparer.Compare(teacher1, null).Should().Be(1);
}
[Fact]
public void Compare_Descending_DifferentWeekDays()
{
var comparer = new ByHourComparer(false);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
var teacher2 = new Teacher("Teacher2")
{
ConsultingHourWeekDay = DayOfWeek.Tuesday,
ConsultingHourUnit = SchoolUnit.UE01
};
comparer.Compare(teacher1, teacher2).Should().Be(1);
}
[Fact]
public void Compare_Descending_DifferentConsultingHours()
{
var comparer = new ByHourComparer(false);
var teacher1 = new Teacher("Teacher1")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE02
};
var teacher2 = new Teacher("Teacher2")
{
ConsultingHourWeekDay = DayOfWeek.Monday,
ConsultingHourUnit = SchoolUnit.UE01
};
int result = comparer.Compare(teacher1, teacher2);
result.Should().Be(-1);
}
}

View file

@ -0,0 +1,70 @@
using MeetTheTeacher.Model;
using MeetTheTeacher.Model.Comparison;
namespace MeetTheTeacher.Test.Model.Comparison;
public sealed class ByNameComparerTests
{
[Fact]
public void Compare_Ascending_BothNull()
{
var comparer = new ByNameComparer(true);
comparer.Compare(null, null).Should().Be(0);
}
[Fact]
public void Compare_Ascending_FirstNull()
{
var teacher2 = new Teacher("Teacher2");
var comparer = new ByNameComparer(true);
comparer.Compare(null, teacher2).Should().Be(1);
}
[Fact]
public void Compare_Ascending_SecondNull()
{
var teacher1 = new Teacher("Teacher1");
var comparer = new ByNameComparer(true);
comparer.Compare(teacher1, null).Should().Be(-1);
}
[Fact]
public void Compare_Ascending_DifferentNames()
{
var teacher1 = new Teacher("Teacher1");
var teacher2 = new Teacher("Teacher2");
var comparer = new ByNameComparer(true);
comparer.Compare(teacher1, teacher2).Should().Be(-1);
}
[Fact]
public void Compare_Descending_FirstNull()
{
var teacher2 = new Teacher("Teacher2");
var comparer = new ByNameComparer(false);
comparer.Compare(null, teacher2).Should().Be(-1);
}
[Fact]
public void Compare_Descending_SecondNull()
{
var teacher1 = new Teacher("Teacher1");
var comparer = new ByNameComparer(false);
comparer.Compare(teacher1, null).Should().Be(1);
}
[Fact]
public void Compare_Descending_DifferentNames()
{
var teacher1 = new Teacher("Teacher1");
var teacher2 = new Teacher("Teacher2");
var comparer = new ByNameComparer(false);
comparer.Compare(teacher1, teacher2).Should().Be(1);
}
}

View file

@ -0,0 +1,64 @@
using MeetTheTeacher.Model;
using MeetTheTeacher.Model.Comparison;
namespace MeetTheTeacher.Test.Model.Comparison;
public sealed class ByRoomComparerTests
{
private readonly IComparer<Teacher> _comparer;
public ByRoomComparerTests()
{
_comparer = new ByRoomComparer();
}
[Fact]
public void Compare_BothNull()
{
_comparer.Compare(null, null).Should().Be(0);
}
[Fact]
public void Compare_FirstNull()
{
_comparer.Compare(null, new Teacher("Teacher2") { Room = "B" })
.Should().Be(1);
}
[Fact]
public void Compare_SecondNull()
{
_comparer.Compare(new Teacher("Teacher1") { Room = "A" }, null)
.Should().Be(-1);
}
[Fact]
public void Compare_SameRoom()
{
var teacher1 = new Teacher("Teacher1") { Room = "A" };
var teacher2 = new Teacher("Teacher2") { Room = "A" };
_comparer.Compare(teacher1, teacher2)
.Should().Be(0);
}
[Fact]
public void Compare_FirstIsLess()
{
var teacher1 = new Teacher("Teacher1") { Room = "A" };
var teacher2 = new Teacher("Teacher2") { Room = "B" };
_comparer.Compare(teacher1, teacher2)
.Should().Be(-1);
}
[Fact]
public void Compare_FirstIsGreater()
{
var teacher1 = new Teacher("Teacher1") { Room = "B" };
var teacher2 = new Teacher("Teacher2") { Room = "A" };
_comparer.Compare(teacher1, teacher2)
.Should().Be(1);
}
}

View file

@ -0,0 +1,62 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Model;
public sealed class CsvDataTests
{
[Fact]
public void GetHeader_Valid()
{
var headerNames = new List<string> { "Name", "Day", "ConsultingHour", "Room" };
var data = new List<string> { "Teacher1", "Monday", "10:00-12:00", "A1" };
var csvData = new CsvData(headerNames, data);
csvData.GetHeader().Should().Be("Name;Day;ConsultingHour;Room");
}
[Fact]
public void GetData_Valid()
{
var headerNames = new List<string> { "Name", "Day", "ConsultingHour", "Room" };
var data = new List<string> { "Teacher1", "Monday", "10:00-12:00", "A1" };
var csvData = new CsvData(headerNames, data);
csvData.GetData().Should().Be("Teacher1;Monday;10:00-12:00;A1");
}
[Fact]
public void Deconstruct_Valid()
{
var headerNames = new List<string> { "Name", "Day", "ConsultingHour", "Room" };
var data = new List<string> { "Teacher1", "Monday", "10:00-12:00", "A1" };
var csvData = new CsvData(headerNames, data);
var (deconstructedHeaderNames, deconstructedData) = csvData;
deconstructedHeaderNames.Should().BeEquivalentTo(headerNames);
deconstructedData.Should().BeEquivalentTo(data);
}
[Theory]
[MemberData(nameof(EmptyInputData))]
public void GetHeaderAndData_EmptyInputData_ReturnsEmptyString(string[] headerNames, string[] data)
{
var csvData = new CsvData(headerNames, data);
string header = csvData.GetHeader();
string rowData = csvData.GetData();
header.Should().Be(string.Join(CsvData.Separator, headerNames));
rowData.Should().Be(string.Join(CsvData.Separator, data));
}
public static TheoryData<string[], string[]> EmptyInputData => new()
{
{ Array.Empty<string>(), new[] { "Teacher1", "Monday", "10:00-12:00", "A1" } },
{ new[] { "Name", "Day", "ConsultingHour", "Room" }, Array.Empty<string>() },
{ Array.Empty<string>(), Array.Empty<string>() }
};
}

View file

@ -0,0 +1,41 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Model;
public sealed class TeacherTests
{
[Fact]
public void ToCsvData_Valid()
{
var teacher = new Teacher("John Doe")
{
ConsultingHour = new TimeFrame(new TimeOnly(10, 0),
new TimeOnly(12, 0)),
ConsultingHourUnit = SchoolUnit.UE01,
ConsultingHourWeekDay = DayOfWeek.Monday,
Room = "A1"
};
var csvData = teacher.ToCsvData();
csvData.HeaderNames.Should().Equal("Name", "Day", "ConsultingHour", "Room");
csvData.Data.Should().ContainInOrder("John Doe", "Monday", "10:00-12:00 (UE01)", "A1");
}
[Fact]
public void ToCsvData_OptionalPropertiesNull()
{
var teacher = new Teacher("Jane Doe")
{
ConsultingHour = null,
ConsultingHourUnit = null,
ConsultingHourWeekDay = null,
Room = null
};
var csvData = teacher.ToCsvData();
csvData.HeaderNames.Should().Equal("Name", "Day", "ConsultingHour", "Room");
csvData.Data.Should().ContainInOrder("Jane Doe", string.Empty, string.Empty, string.Empty);
}
}

View file

@ -0,0 +1,43 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Model;
public sealed class TeacherWithBusinessCardTests
{
[Fact]
public void ToCsvData_Valid()
{
var teacher = new TeacherWithBusinessCard("John Doe", 1)
{
ConsultingHour = new TimeFrame(new TimeOnly(10, 0),
new TimeOnly(12, 0)),
ConsultingHourUnit = SchoolUnit.UE07,
ConsultingHourWeekDay = DayOfWeek.Monday,
Room = "A1"
};
var csvData = teacher.ToCsvData();
csvData.HeaderNames.Should().Equal("Name", "Day", "ConsultingHour", "Room", "Image");
csvData.Data.Should().Equal("John Doe", "Monday", "10:00-12:00 (UE07)", "A1",
"https://www.htl-leonding.at/media/teacher-avatar/1");
}
[Fact]
public void ToCsvData_OptionalPropertiesNull()
{
var teacher = new TeacherWithBusinessCard("Jane Doe", 2)
{
ConsultingHour = null,
ConsultingHourUnit = null,
ConsultingHourWeekDay = null,
Room = null
};
var csvData = teacher.ToCsvData();
csvData.HeaderNames.Should().Equal("Name", "Day", "ConsultingHour", "Room", "Image");
csvData.Data.Should().Equal("Jane Doe", string.Empty, string.Empty, string.Empty,
"https://www.htl-leonding.at/media/teacher-avatar/2");
}
}

View file

@ -0,0 +1,67 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Test.Model;
public sealed class TimeFrameTests
{
[Fact]
public void Construction_Simple()
{
var start = new TimeOnly(14, 51);
var end = new TimeOnly(17, 28);
var tf = new TimeFrame(start, end);
tf.Start.Should().Be(start);
tf.End.Should().Be(end);
}
[Theory]
[InlineData("10:00-12:00", "10:00", "12:00")]
[InlineData("14:30-15:00", "14:30", "15:00")]
[InlineData("09:15-09:45", "09:15", "09:45")]
public void TryParse_Valid(string input, string expectedStart, string expectedEnd)
{
TimeFrame.TryParse(input, out TimeFrame? result).Should().BeTrue();
result.Should().NotBeNull();
result?.Start.Should().Be(TimeOnly.Parse(expectedStart));
result?.End.Should().Be(TimeOnly.Parse(expectedEnd));
}
[Theory]
[InlineData("12:00-10:00", "10:00", "12:00")]
[InlineData("15:00-14:30", "14:30", "15:00")]
public void TryParse_StartEndSwapped(string input, string expectedStart, string expectedEnd)
{
TimeFrame.TryParse(input, out TimeFrame? result).Should().BeTrue();
result.Should().NotBeNull();
result?.Start.Should().Be(TimeOnly.Parse(expectedStart));
result?.End.Should().Be(TimeOnly.Parse(expectedEnd));
}
[Theory]
[InlineData("invalid-12:00")]
[InlineData("10:00-invalid")]
[InlineData("25:00-12:00")]
[InlineData("10:00-28:00")]
[InlineData("10:62-22:00")]
[InlineData("10:a2-22:00")]
[InlineData(" ")]
[InlineData("")]
public void TryParse_InvalidInputs_ReturnsFalse(string input)
{
TimeFrame.TryParse(input, out TimeFrame? result).Should().BeFalse();
result.Should().BeNull();
}
[Fact]
public void StringRepresentation()
{
var timeFrame = new TimeFrame(new TimeOnly(10, 0), new TimeOnly(12, 0));
timeFrame.ToString().Should().Be("10:00-12:00");
}
}

31
MeetTheTeacher.sln Normal file
View file

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33103.184
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeetTheTeacher", "MeetTheTeacher\MeetTheTeacher.csproj", "{7F30E637-BAFE-42FA-A173-F42B3902ED3B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeetTheTeacher.Test", "MeetTheTeacher.Test\MeetTheTeacher.Test.csproj", "{161D9B2A-4E8B-43B6-A77E-40BED559521F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F30E637-BAFE-42FA-A173-F42B3902ED3B}.Release|Any CPU.Build.0 = Release|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{161D9B2A-4E8B-43B6-A77E-40BED559521F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D322E690-379C-4802-8F7E-1AC6AD090D59}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,18 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Export;
/// <summary>
/// Allows to export data of <see cref="ICsvRepresentable"/> type to a CSV file
/// </summary>
/// <inheritdoc cref="FileExporterBase" />
/// <inheritdoc cref="IDataExporter" />
public sealed class CsvDataExporter : FileExporterBase, IDataExporter
{
public void Export(IEnumerable<ICsvRepresentable> items, string? fileName)
{
// TODO
}
protected override string FileExtension => ".csv";
}

View file

@ -0,0 +1,12 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Export;
/// <summary>
/// Default implementation of <see cref="IExporterProvider"/> which returns the following exporters:
/// <see cref="CsvDataExporter"/> and <see cref="HtmlDataExporter"/>
/// </summary>
public sealed class DefaultExporterProvider : IExporterProvider
{
public IDataExporter GetExporter(ExportFormat exportFormat) => null!; // TODO
}

View file

@ -0,0 +1,37 @@
using MeetTheTeacher.Model;
using MeetTheTeacher.Model.Comparison;
namespace MeetTheTeacher.Export;
/// <summary>
/// A utility class which allows to export data in a given format and order
/// </summary>
public sealed class Exporter
{
// TODO
//private readonly IComparer<Teacher> _comparer;
private readonly IDataExporter _exporter;
/// <summary>
/// Creates a new instance of <see cref="Exporter" /> initialized with the provided export options
/// </summary>
/// <param name="sortOrder">Order in which data should be sorted before export</param>
/// <param name="exportFormat">Desired export format</param>
/// <param name="exporterProvider">Export provider which supports exporting in the desired format</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if you manage to pass an unexpected <see cref="SortOrder" /> value</exception>
public Exporter(SortOrder sortOrder, ExportFormat exportFormat, IExporterProvider exporterProvider)
{
_exporter = exporterProvider.GetExporter(exportFormat);
//_comparer = ... // TODO
}
/// <summary>
/// First sorts the provided data based on the previous settings, then performs the actual export to a file
/// </summary>
/// <param name="fileName">Name of the file to export to; without extension</param>
/// <param name="teachers">Data to export - has to be list due to multiple enumeration</param>
public void Export(string fileName, List<Teacher> teachers)
{
// TODO
}
}

View file

@ -0,0 +1,36 @@
namespace MeetTheTeacher.Export;
/// <summary>
/// Base class for exporters which write data to a file
/// </summary>
public abstract class FileExporterBase
{
/// <summary>
/// File extension of the exported file including the dot
/// </summary>
protected abstract string FileExtension { get; }
/// <summary>
/// Exports the given content to a file with the given name.
/// If the file already exists, it will be overwritten.
/// </summary>
/// <param name="fileName">Name of the file to export without the extension</param>
/// <param name="content">Content to write into the file</param>
protected void Export(string fileName, string content)
{
// TODO (don't forget to delete)
}
/// <summary>
/// Joins several lines into one text, separated by line breaks
/// </summary>
/// <param name="lines">Lines to merge</param>
/// <returns>A text containing all lines</returns>
protected static string LinesToText(IEnumerable<string> lines) => null!; // TODO (remember to use proper line break)
private string GetFilePath(string fileName)
{
// TODO (remember to use the current directory)
return null!;
}
}

View file

@ -0,0 +1,27 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Export;
/// <summary>
/// Allows to export data of <see cref="ICsvRepresentable"/> type to a file in HTML table syntax.
/// Be careful: does not create valid HTML page, just a fragment!
/// </summary>
/// <inheritdoc cref="FileExporterBase" />
/// <inheritdoc cref="IDataExporter" />
public sealed class HtmlDataExporter : FileExporterBase, IDataExporter
{
public void Export(IEnumerable<ICsvRepresentable> items, string? fileName)
{
// TODO (use a raw string literal with interpolation!)
}
private static string CreateTableRow(IEnumerable<string> values, bool isHeader = false)
{
// TODO
return null!;
}
private static string WrapInTag(string content, string tag) => $"<{tag}>{content}</{tag}>";
protected override string FileExtension => ".html";
}

View file

@ -0,0 +1,17 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Export;
/// <summary>
/// Allows to export data of <see cref="ICsvRepresentable"/> type
/// </summary>
public interface IDataExporter
{
/// <summary>
/// Exports the given data.
/// File based exporters will use the provided file name as the name of the file they export.
/// </summary>
/// <param name="data">Data to export</param>
/// <param name="fileName">File name to be used; without the extension</param>
void Export(IEnumerable<ICsvRepresentable> data, string? fileName);
}

View file

@ -0,0 +1,16 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Export;
/// <summary>
/// Provides an <see cref="IDataExporter"/> for the given <see cref="ExportFormat"/>
/// </summary>
public interface IExporterProvider
{
/// <summary>
/// Returns an <see cref="IDataExporter"/> for the given <see cref="ExportFormat"/>
/// </summary>
/// <param name="exportFormat">Requested export format</param>
/// <returns><see cref="IDataExporter"/> which supports exporting in the desired format</returns>
IDataExporter GetExporter(ExportFormat exportFormat);
}

View file

@ -0,0 +1,51 @@
using MeetTheTeacher.Model;
using Spectre.Console;
namespace MeetTheTeacher.Export;
/// <summary>
/// Allows to export data of <see cref="ICsvRepresentable" /> type to a <see cref="Table" /> in-memory data structure
/// </summary>
/// <inheritdoc cref="IDataExporter" />
public sealed class SpectreTableExporter : IDataExporter
{
/// <summary>
/// Creates a new instance of the exporter with an empty <see cref="Table" />
/// </summary>
public SpectreTableExporter()
{
Table = new Table();
}
/// <summary>
/// Gets the table containing the exported data
/// </summary>
public Table Table { get; private set; }
/// <summary>
/// Exports the given data to a <see cref="Table" />.
/// Does not write a file.
/// </summary>
/// <param name="data">Data to export</param>
/// <param name="fileName">Unused</param>
public void Export(IEnumerable<ICsvRepresentable> data, string? fileName)
{
var dataRows = new List<CsvData>();
foreach (var csvRepresentable in data)
{
dataRows.Add(csvRepresentable.ToCsvData());
}
var table = new Table();
// TODO
Table = table;
}
private static IEnumerable<string> FindMaxHeaderColumns(IEnumerable<CsvData> rows)
{
// TODO
return null!;
}
}

View file

@ -0,0 +1,16 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Import;
/// <summary>
/// Allows to import data of <see cref="Teacher" /> type
/// </summary>
public interface ITeacherDataImporter
{
/// <summary>
/// Loads the data from various sources.
/// Mind that instances of <see cref="Teacher"/> and <see cref="TeacherWithBusinessCard"/> can be returned.
/// </summary>
/// <returns>A collection of <see cref="Teacher"/> data</returns>
IEnumerable<Teacher> LoadTeacherData();
}

View file

@ -0,0 +1,54 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Import;
/// <summary>
/// Imports <see cref="Teacher" /> data from CSV files
/// </summary>
/// <inheritdoc cref="ITeacherDataImporter" />
public sealed class TeacherDataCsvImporter : ITeacherDataImporter
{
private readonly string _businessCardDataFilePath;
private readonly string _generalDataFilePath;
/// <summary>
/// Creates a new instance of <see cref="TeacherDataCsvImporter" /> configured to read from the given files
/// </summary>
/// <param name="generalDataFilePath">Path to file containing general <see cref="Teacher" /> data for all teachers</param>
/// <param name="businessCardDataFilePath">
/// Path to file containing additional data for <see cref="TeacherWithBusinessCard" /> teachers
/// </param>
public TeacherDataCsvImporter(string generalDataFilePath, string businessCardDataFilePath)
{
_generalDataFilePath = generalDataFilePath;
_businessCardDataFilePath = businessCardDataFilePath;
}
public IEnumerable<Teacher> LoadTeacherData()
{
// TODO
return null!;
}
private static Dictionary<string, Teacher> CombineData(Dictionary<string, Teacher> generalData,
IEnumerable<(string name, int id)> businessCardData)
{
// Note: this method is designed specifically to force you to deal with the dictionary and
// different teacher classes as value. It could be done in different ways as well, of course.
// TODO
return null!;
}
private IEnumerable<(string name, int id)>? ReadTeacherBusinessCardData()
{
// TODO - CSV import (mind the return type: ValueTuple!)
return null!;
}
private Dictionary<string, Teacher>? ReadTeacherData()
{
// TODO - CSV import
return null!;
}
}

View file

@ -0,0 +1,57 @@
using MeetTheTeacher.Model;
namespace MeetTheTeacher.Import;
/// <summary>
/// Fake implementation of <see cref="ITeacherDataImporter" /> which returns a fixed set of data.
/// </summary>
/// <inheritdoc cref="ITeacherDataImporter" />
public sealed class TeacherDataFakeImporter : ITeacherDataImporter
{
public IEnumerable<Teacher> LoadTeacherData() =>
new List<Teacher>
{
new("John Doe")
{
ConsultingHour = new TimeFrame(new TimeOnly(8, 0), new TimeOnly(8, 50)),
ConsultingHourUnit = SchoolUnit.UE01,
ConsultingHourWeekDay = DayOfWeek.Monday,
Room = "A101"
},
new("Jane Smith")
{
ConsultingHour = new TimeFrame(new TimeOnly(14, 35), new TimeOnly(15, 25)),
ConsultingHourUnit = SchoolUnit.UE08,
ConsultingHourWeekDay = DayOfWeek.Tuesday,
Room = "A102"
},
new("Alice Johnson")
{
ConsultingHour = new TimeFrame(new TimeOnly(10, 0), new TimeOnly(10, 50)),
ConsultingHourUnit = SchoolUnit.UE03,
ConsultingHourWeekDay = DayOfWeek.Wednesday,
Room = "A102"
},
new("Bob Brown")
{
ConsultingHour = new TimeFrame(new TimeOnly(10, 55), new TimeOnly(11, 45)),
ConsultingHourUnit = SchoolUnit.UE04,
ConsultingHourWeekDay = DayOfWeek.Friday,
Room = "A104"
},
new TeacherWithBusinessCard("Emily Davis", 1001)
{
ConsultingHour = new TimeFrame(new TimeOnly(12, 40), new TimeOnly(13, 35)),
ConsultingHourUnit = SchoolUnit.UE06,
ConsultingHourWeekDay = DayOfWeek.Thursday,
Room = "A101"
},
new TeacherWithBusinessCard("Tom Wilson", 1002)
{
ConsultingHour = new TimeFrame(new TimeOnly(8, 50), new TimeOnly(9, 45)),
ConsultingHourUnit = SchoolUnit.UE02,
ConsultingHourWeekDay = DayOfWeek.Monday,
Room = "A102"
}
};
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.49.2-preview.0.77" />
<PackageReference Include="Spectre.Console.ImageSharp" Version="0.49.2-preview.0.77" />
</ItemGroup>
<ItemGroup>
<Content Include="..\Data\*.csv" Link="Data\%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="happy-dog.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,22 @@
namespace MeetTheTeacher.Model.Comparison;
/// <summary>
/// Compares <see cref="Teacher" /> instances by their consulting hour, sorting in ascending order.
/// First compares by <see cref="DayOfWeek" />, then by <see cref="SchoolUnit" />.
/// </summary>
/// <inheritdoc cref="ComparerBase" />
/// <inheritdoc cref="IComparer{Teacher}" />
public sealed class ByHourComparer : ComparerBase, IComparer<Teacher>
{
/// <summary>
/// Creates a new instance of <see cref="ByHourComparer" /> configured to sort in ascending or descending order
/// </summary>
/// <param name="ascending">Flag indicating if items should be sorted in ascending order; descending if false</param>
public ByHourComparer(bool ascending) : base(ascending) { }
public int Compare(Teacher? x, Teacher? y)
{
// TODO
return -1;
}
}

View file

@ -0,0 +1,22 @@
namespace MeetTheTeacher.Model.Comparison;
/// <summary>
/// A comparer which allows sorting <see cref="Teacher" /> instances by their name in either ascending or descending
/// order
/// </summary>
/// <inheritdoc cref="ComparerBase" />
/// <inheritdoc cref="IComparer{Teacher}"/>
public sealed class ByNameComparer : ComparerBase, IComparer<Teacher>
{
/// <summary>
/// Creates a new instance of <see cref="ByNameComparer" /> configured to sort in ascending or descending order
/// </summary>
/// <param name="ascending">Flag indicating if items should be sorted in ascending order; descending if false</param>
public ByNameComparer(bool ascending) : base(ascending) { }
public int Compare(Teacher? x, Teacher? y)
{
// TODO
return -1;
}
}

View file

@ -0,0 +1,14 @@
namespace MeetTheTeacher.Model.Comparison;
/// <summary>
/// Compares <see cref="Teacher"/> instances by their room, sorting in ascending order
/// </summary>
/// <inheritdoc cref="IComparer{Teacher}"/>
public sealed class ByRoomComparer : IComparer<Teacher>
{
public int Compare(Teacher? x, Teacher? y)
{
// TODO
return -1;
}
}

View file

@ -0,0 +1,24 @@
namespace MeetTheTeacher.Model.Comparison;
/// <summary>
/// Base class for comparers which allow switching between ascending and descending mode
/// </summary>
public abstract class ComparerBase
{
/// <summary>
/// Creates a new instance of <see cref="ComparerBase" /> configured to sort in ascending or descending order
/// </summary>
/// <param name="ascending">Flag indicating if items should be sorted in ascending order; descending if false</param>
protected ComparerBase(bool ascending)
{
Ascending = ascending;
}
/// <summary>
/// Gets if the comparer is configured to sort in ascending order; descending if false
/// </summary>
protected bool Ascending { get; }
// TODO: consider moving the basic RefEquals code of CompareTo from the subclasses here
// maybe also rename this to SortOrderComparerBase or something and introduce another, common base class for all three?
}

View file

@ -0,0 +1,57 @@
namespace MeetTheTeacher.Model;
/// <summary>
/// Represents a single entity as a CSV processable object.
/// </summary>
public sealed class CsvData
{
/// <summary>
/// Separator used in CSV files
/// </summary>
public const char Separator = ';';
/// <summary>
/// Creates a new instance of <see cref="CsvData" />
/// </summary>
/// <param name="headerNames">Header column names for the entity which has its data represented by this object</param>
/// <param name="data">Data column values for the entity which has its data represented by this object; stringly typed</param>
public CsvData(IReadOnlyList<string> headerNames, IReadOnlyList<string> data)
{
HeaderNames = headerNames;
Data = data;
}
/// <summary>
/// Gets the header column names for the entity which has its data represented by this object
/// </summary>
public IReadOnlyList<string> HeaderNames { get; }
/// <summary>
/// Gets the data column values for the entity which has its data represented by this object; stringly typed
/// </summary>
public IReadOnlyList<string> Data { get; }
/// <summary>
/// Gets a CSV header based on <see cref="HeaderNames" />
/// </summary>
/// <returns>A CSV header line</returns>
public string GetHeader() => null!; // TODO
/// <summary>
/// Gets a CSV data line based on <see cref="Data" />
/// </summary>
/// <returns>A CSV data line</returns>
public string GetData() => null!; // TODO
/// <summary>
/// Allows to deconstruct the object into its header and data parts
/// </summary>
/// <param name="headerNames">Set to <see cref="HeaderNames" /></param>
/// <param name="data">Set to <see cref="Data" /></param>
public void Deconstruct(out IReadOnlyList<string> headerNames, out IReadOnlyList<string> data)
{
// TODO
headerNames = null!;
data = null!;
}
}

View file

@ -0,0 +1,13 @@
namespace MeetTheTeacher.Model;
/// <summary>
/// Allows to represent the entity in a CSV compatible way
/// </summary>
public interface ICsvRepresentable
{
/// <summary>
/// Converts the entity to a <see cref="CsvData" /> instance
/// </summary>
/// <returns>A <see cref="CsvData" /> instance representing this entity</returns>
CsvData ToCsvData();
}

View file

@ -0,0 +1,54 @@
namespace MeetTheTeacher.Model;
/// <summary>
/// Represents a teacher at a vocational college
/// </summary>
/// <inheritdoc cref="ICsvRepresentable" />
public class Teacher : ICsvRepresentable
{
/// <summary>
/// Creates a new instance of <see cref="Teacher" />.
/// The name is the only required property.
/// </summary>
/// <param name="name">Name of the teacher</param>
public Teacher(string name)
{
Name = name;
}
/// <summary>
/// Gets the name of the teacher
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the time frame in which the teacher is available for consulting
/// </summary>
public TimeFrame? ConsultingHour { get; init; }
/// <summary>
/// Gets the school unit in which the teacher is available for consulting
/// </summary>
public SchoolUnit? ConsultingHourUnit { get; init; }
/// <summary>
/// Gets the day of the week the teacher is available for consulting
/// </summary>
public DayOfWeek? ConsultingHourWeekDay { get; init; }
/// <summary>
/// Gets the room in which the teacher is available for consulting
/// </summary>
public string? Room { get; init; }
private string? ConsultingHourTime
{
get
{
// TODO
return null!;
}
}
public virtual CsvData ToCsvData() => null!; // TODO
}

View file

@ -0,0 +1,32 @@
namespace MeetTheTeacher.Model;
/// <summary>
/// Represents a teacher with a business card which means they have an id for their picture available as well
/// </summary>
/// <inheritdoc cref="Teacher" />
public sealed class TeacherWithBusinessCard : Teacher
{
private const string BaseUrl = "https://www.htl-leonding.at/media/teacher-avatar";
/// <summary>
/// Creates a new instance of <see cref="TeacherWithBusinessCard" />.
/// In addition to the name, the id of the picture is required as well.
/// </summary>
/// <param name="name">Name of the teacher</param>
/// <param name="id">Id of the teacher's picture</param>
public TeacherWithBusinessCard(string name, int id) : base(name)
{
Id = id;
}
/// <summary>
/// Gets the picture id of the teacher
/// </summary>
public int Id { get; }
public override CsvData ToCsvData()
{
// TODO (remember to maybe use the base implementation...)
return null!;
}
}

View file

@ -0,0 +1,38 @@
using System.Globalization;
namespace MeetTheTeacher.Model;
/// <summary>
/// Represents a time frame between a start and end time.
/// Start time is always before end time.
/// </summary>
/// <param name="Start">Start time</param>
/// <param name="End">End time</param>
public readonly record struct TimeFrame(TimeOnly Start, TimeOnly End)
{
private static readonly CultureInfo culture = new ("de-AT");
/// <summary>
/// Tries to parse a time frame from a string in format HH:MM-HH:MM.
/// Additional whitespace is trimmed and ignored.
/// </summary>
/// <param name="timeFrame">Timeframe string to parse</param>
/// <param name="result">Set to the parsed time frame if possible</param>
/// <returns>True if time frame could be parsed successfully; false otherwise</returns>
public static bool TryParse(string timeFrame, out TimeFrame? result)
{
// TODO
result = null;
return false;
}
/// <summary>
/// Returns a string representation of the time frame in format HH:MM-HH:MM
/// </summary>
/// <returns></returns>
public override string ToString()
{
// TODO
return null!;
}
}

View file

@ -0,0 +1,39 @@
namespace MeetTheTeacher.Model;
/// <summary>
/// Represents school units, each taking 50 minutes
/// </summary>
public enum SchoolUnit
{
UE01 = 01,
UE02 = 02,
UE03 = 03,
UE04 = 04,
UE05 = 05,
UE06 = 06,
UE07 = 07,
UE08 = 08,
UE09 = 09,
UE10 = 10
}
/// <summary>
/// Represents possible data export formats
/// </summary>
public enum ExportFormat
{
Csv,
Html
}
/// <summary>
/// Represents possible sort orders for the teacher data
/// </summary>
public enum SortOrder
{
ByNameAsc,
ByNameDesc,
ByHourAsc,
ByHourDesc,
Room
}

154
MeetTheTeacher/Program.cs Normal file
View file

@ -0,0 +1,154 @@
using System.Text;
using MeetTheTeacher.Export;
using MeetTheTeacher.Import;
using MeetTheTeacher.Model;
using Spectre.Console;
Console.OutputEncoding = Encoding.UTF8;
Clear();
AnsiConsole.Markup("[orange3]Press any key to load HTL-Leonding teacher data[/] ");
Console.ReadKey();
// data loading
bool loadRealData = AskForDataToUse();
// wrapping IEnumerable in List, because we will iterate multiple times
var data = new List<Teacher>(LoadData(loadRealData));
var tableGenerator = new SpectreTableExporter();
tableGenerator.Export(data, string.Empty);
// fake loading
Clear();
await DisplayFakeProgress();
Clear();
AnsiConsole.Write(tableGenerator.Table);
PrintStatistics(data);
// user input
WriteRule("Sort Order");
var order = AskForSortOrder();
WriteRule("Export Format");
var format = AskForExportFormat();
WriteRule("File Name");
string fileName = AnsiConsole.Ask<string>("Which [blue]file name[/] should be used?");
// export
var exporter = new Exporter(order, format, new DefaultExporterProvider());
exporter.Export(fileName, data);
// done
WriteRule(string.Empty);
AnsiConsole.Write(new FigletText("All Done!").Centered().Color(Color.Green));
DrawDog();
return;
static void Clear()
{
Console.Clear();
AnsiConsole.Write(new FigletText("Teacher Data").Centered().Color(Color.Blue));
}
static void WriteRule(string text)
{
AnsiConsole.Write(new Rule($"[blue]{text}[/]").LeftJustified());
}
static IEnumerable<Teacher> LoadData(bool realData)
{
// TODO
// ITeacherDataImporter importer ...
// files: "Data/teachers.csv" & "Data/teachers-with-business-card.csv"
return null!;
}
static Task DisplayFakeProgress()
{
const double Increase = 4.5;
const double ReducedIncrease = Increase * 0.8D;
return AnsiConsole.Progress()
.StartAsync(async ctx =>
{
// we are actually already done, but want to fake some complex operation
// to have the user appreciate the hard work our application is doing :)
var task1 = ctx.AddTask("[green]Loading Data[/]");
var task2 = ctx.AddTask("[green]Processing Data[/]");
while (!ctx.IsFinished)
{
task1.Increment(Increase);
task2.Increment(ReducedIncrease);
await Task.Delay(TimeSpan.FromMilliseconds(125));
}
});
}
static void PrintStatistics(IReadOnlyCollection<Teacher> teachers)
{
var hasConsultingHours = 0;
var hasBusinessCard = 0;
foreach (var teacher in teachers)
{
// TODO
}
double total = teachers.Count;
var cPercentage = (int) (hasConsultingHours / total * 100);
int ncPercentage = 100 - cPercentage;
var bPercentage = (int) (hasBusinessCard / total * 100);
AnsiConsole.Write(new BarChart()
.Width(60)
.Label($"[blue underline]Statistics ({total:F0} teachers)[/]")
.CenterLabel()
.AddItem("Has consulting hour", cPercentage, Color.Green)
.AddItem("Has no consulting hour", ncPercentage, Color.Red)
.AddItem("Has business card", bPercentage, Color.Purple));
}
static SortOrder AskForSortOrder()
{
var options = new Dictionary<string, SortOrder>
{
["Name Asc."] = SortOrder.ByNameAsc,
["Name Desc."] = SortOrder.ByNameDesc,
["Hour Asc."] = SortOrder.ByHourAsc,
["Hour Desc."] = SortOrder.ByHourDesc,
["Room"] = SortOrder.Room
};
string order = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("How do you wish to [blue]sort[/] the data?")
.PageSize(3)
.MoreChoicesText("[grey](Move up and down to reveal more options)[/]")
.AddChoices(options.Keys));
AnsiConsole.MarkupLine($"Selected: [green]{order}[/]");
return options[order];
}
static ExportFormat AskForExportFormat()
{
// TODO (similar to AskForDataToUse)
return default;
}
static bool AskForDataToUse()
{
const string Real = "Real";
string dataToUse = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("Do you want to load [blue]real[/] or [blue]sample[/] data?")
.AddChoices(Real, "Sample"));
AnsiConsole.MarkupLine($"Selected: [green]{dataToUse}[/]");
return dataToUse == Real;
}
static void DrawDog()
{
var image = new CanvasImage("happy-dog.png");
image.MaxWidth(36);
AnsiConsole.Write(image);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
pics/sample_run.mp4 Normal file

Binary file not shown.

BIN
pics/spectre_examples.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

381
readme.adoc Normal file
View file

@ -0,0 +1,381 @@
:sectnums:
:nofooter:
:toc: left
:icons: font
:data-uri:
:source-highlighter: highlightjs
:stem: latexmath
= Int.02 -- Meet the Teacher
This is an *extensive* assignment with the goal of processing the consultation hours of teachers (real data from a few years ago).
== Introduction
The application is split into several parts:
. Importing Data
** From multiple files and different sources
. Performing Sort Operations
** With a multitude of options
. Exporting Data
** In two different formats
. User Interaction
** A _modern_ console application
NOTE: You _really_ should not wait to the last minute before starting to work on this assignment! It _will_ take you some time to understand the relations between the multitude of components.
=== Sample Run [[sec:sample]]
Before reading more about the individual parts and their requirements watch a sample run of the application to get a feeling for what you are going to implement.
video::pics/sample_run.mp4[Sample Run,width=1000]
=== Tests
* Basic coverage with unit tests has been provided for you
** But _not_ complete, especially for edge cases => do not rely only on the tests
*** You are allowed to add tests, if you want to, of course
* Take a look at how we can make use of interfaces when writing unit tests
** Especially for _mocking_
** => you will write such tests _yourself_ in the future!
== Data Model
[plantuml]
----
@startuml
hide empty fields
hide empty methods
interface ICsvRepresentable {
+CsvData ToCsvData()
}
class CsvData <<sealed>> {
+char Separator [const]
+IReadOnlyList<string> HeaderNames [readonly]
+IReadOnlyList<string> Data [readonly]
+CsvData(IReadOnlyList<string> headerNames, IReadOnlyList<string> data)
+string GetHeader()
+string GetData()
+void Deconstruct(out IReadOnlyList<string> headerNames, out IReadOnlyList<string> data)
}
class Teacher {
+string Name [readonly]
+TimeFrame? ConsultingHour [init only]
+SchoolUnit? ConsultingHourUnit [init only]
+DayOfWeek? ConsultingHourWeekDay [init only]
+string? Room [init only]
-string? ConsultingHourTime [readonly]
+Teacher(string name)
+CsvData ToCsvData() [virtual]
}
class TeacherWithBusinessCard <<sealed>> {
-string BaseUrl [const]
+int Id [readonly]
+TeacherWithBusinessCard(string name, int id)
+CsvData ToCsvData() [override]
}
struct TimeFrame {
+TimeOnly Start [readonly]
+TimeOnly End [readonly]
+TimeFrame(TimeOnly start, TimeOnly end)
{static} +bool TryParse(string timeFrame, out TimeFrame? result)
+string ToString() [override]
}
enum SchoolUnit {
UE01
UE02
UE03
UE04
UE05
UE06
UE07
UE08
UE09
UE10
}
Teacher <|-- TeacherWithBusinessCard
Teacher o-- TimeFrame
Teacher o-- SchoolUnit
ICsvRepresentable <|.. Teacher
@enduml
----
The data model combines inheritance with interface implementation as it is common in modern applications.
* `ICsvRepresentable`
** Defines the _capability_ of a class to provide its own data in a format which can be easily processed to create a CSV file
* `CsvData`
** Contains data of a _single_ object in addition to the headers which define this data
** Some properties are of type `IReadOnlyList` -- *that's something new* => https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlylist-1?view=net-8.0[learn about it]
*** We'll need it quite often from now on
** We implement a `Deconstruct` method
*** That allows the compiler to perform a https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct[_deconstruct_ operation]
**** We can write things like `var (a, b) = objectWithAAndB`
*** *This is _not_ the same as a _destructor_! Do _not_ use destructors!*
* `Teacher`
** _All_ teachers are of type `Teacher`
** The name is required
*** Other properties are _optional_ (some colleagues do not offer a consulting hour)
** `DayOfWeek` is an `enum` defined in the framework -- https://learn.microsoft.com/en-us/dotnet/api/system.dayofweek?view=net-8.0[read, if you don't know it]
** `ConsultingHourTime` combines information from `ConsultingHour` & `ConsultingHourUnit`
*** Has to return `null` if components are `null`
*** Otherwise, returns format `HH:MM-HH:MM (UEXX)`
**** Example: `12:40-13:35 (UE06)`
* `TeacherWithBusinessCard`
** _Some_ teachers also want to display a business card, which requires a portrait picture, in addition to the regular information
** This picture is stored on a webserver, so we only have to deal with the id
** Such an image URI could look like this: `https://www.htl-leonding.at/media/teacher-avatar/1234`
*** _Fictional address, doesn't work!_
* `TimeFrame`
** Defines a time frame with a start and end time
*** Start time has to be before end time
** Implemented as a https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct[struct]
*** It's been some time since we last talked about structs.
Do you still remember the guidelines when to use one and when better to use a class?
If not, ask me to explain it again!
TIP: If you find the class diagrams hard to read (due to size) remember that you don't have images but those are gnerated from PlantUML code => you can look at individual parts, zoom in, use different editors or even simply look at the diagram source code
== Import
[plantuml]
----
@startuml
hide empty fields
hide empty methods
interface ITeacherDataImporter {
+IEnumerable<Teacher> LoadTeacherData()
}
class TeacherDataCsvImporter <<sealed>> {
-string _businessCardDataFilePath [readonly]
-string _generalDataFilePath [readonly]
+TeacherDataCsvImporter(string generalDataFilePath, string businessCardDataFilePath)
{static} -Dictionary<string, Teacher> CombineData(Dictionary<string, Teacher> generalData, IEnumerable<(string name, int id)> businessCardData)
-IEnumerable<(string name, int id)>? ReadTeacherBusinessCardData()
-Dictionary<string, Teacher>? ReadTeacherData()
}
class TeacherDataFakeImporter <<sealed>> {
}
ITeacherDataImporter <|.. TeacherDataCsvImporter
ITeacherDataImporter <|.. TeacherDataFakeImporter
@enduml
----
Data can be imported from various, different sources.
In the main part of the application we are only interested in getting the data, not how or from where.
To showcase this abstraction (and separation of concerns) we implement two 'data sources': one will read data from two CSV files and combine it to build the required format, while the other will simply fake importing and return sample data.
* `ITeacherDataImporter`
** Defines the capability to load `Teacher` data from a source
* `TeacherDataCsvImporter`
** Reads data from two CSV files
... One file contains general `Teacher` information
... The second file contains data only for `TeacherWithBusinessCard` teachers
** Reading data from a CSV file is easy for you by now, but be aware that some properties are _optional_
** The data from the two files has to be intelligently combined to create the required output
*** Hint: a collection which holds `Teacher` objects can _at the same time_ contain instances of _subclasses_
** Here you'll deal with `Dictionary` again -- we haven't used this collection extensively so far, maybe https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-8.0[you want to refresh your memory]
* `TeacherDataFakeImporter`
** This is just an example how different import providers could be used in this application, thanks to the interfaces
** Implementation has already been provided for you -- make sure you understand what's going on!
== Sorting
[plantuml]
----
@startuml
hide empty methods
hide empty fields
enum SortOrder
{
ByNameAsc
ByNameDesc
ByHourAsc
ByHourDesc
Room
}
abstract class ComparerBase {
#ComparerBase(bool ascending)
#bool Ascending [readonly]
}
interface IComparer<Teacher>
class ByHourComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
class ByNameComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
class ByRoomComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
IComparer <|.. ByHourComparer
IComparer <|.. ByNameComparer
IComparer <|.. ByRoomComparer
ComparerBase <|-- ByHourComparer
ComparerBase <|-- ByNameComparer
@enduml
----
Our application offers various sorting options.
These are implemented by different `Comparer` implementation.
TIP: Do you still remember the difference between https://learn.microsoft.com/en-us/dotnet/api/system.icomparable?view=net-8.0[`IComparable`] and https://learn.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=net-8.0[`IComparer`]?
* `ComparerBase`
** Base class for those comparers which support changing the sorting order from ascending to descending
* `ByHourComparer`
** Allows sorting of the teacher data by time of the consulting hour
** Sorting order can be ascending or descending
** _First_ the day of the week, and _then_ the unit is compared
** Hint: some properties are optional, remember how to deal with `null` values when sorting/comparing
* `ByNameComparer`
** Allows sorting of the teacher data by name
** Sorting order can be ascending or descending
* `ByRoomComparer`
** Allows sorting of the teacher data by room
** Sorting order is _always_ ascending
** Hint: `Room` can be `null`
== Export
CAUTION: The interactions between the individual components and the number of those is much more _complex_ than anything we did before -- do not hesitate to ask if you have trouble understanding!
[plantuml]
----
@startuml
hide empty methods
hide empty fields
enum ExportFormat
{
Csv
Html
}
class CsvDataExporter <<sealed>> {}
class DefaultExporterProvider <<sealed>> {}
class Exporter <<sealed>> {
-IComparer<Teacher> _comparer [readonly]
-IDataExporter _exporter [readonly]
+Exporter(SortOrder sortOrder, ExportFormat exportFormat, IExporterProvider exporterProvider)
+void Export(string fileName, List<Teacher> teachers)
}
abstract class FileExporterBase {
{abstract} #string FileExtension [readonly]
#void Export(string fileName, string content)
{static} #string LinesToText(IEnumerable<string> lines)
-string GetFilePath(string fileName)
}
class HtmlDataExporter <<sealed>> {
{static} -string CreateTableRow(IEnumerable<string> values, bool isHeader = false)
{static} -string WrapInTag(string content, string tag)
}
interface IDataExporter {
+void Export(IEnumerable<ICsvRepresentable> data, string? fileName)
}
interface IExporterProvider {
+IDataExporter GetExporter(ExportFormat exportFormat)
}
class SpectreTableExporter <<sealed>> {
+Table Table [private set]
+public SpectreTableExporter()
{static} -IEnumerable<string> FindMaxHeaderColumns(IEnumerable<CsvData> rows)
}
IDataExporter <|.. SpectreTableExporter
IDataExporter <|.. HtmlDataExporter
IDataExporter <|.. CsvDataExporter
FileExporterBase <|-- CsvDataExporter
FileExporterBase <|-- HtmlDataExporter
IExporterProvider <|.. DefaultExporterProvider
Exporter *-- IDataExporter
@enduml
----
Our application has a very _flexible_ export system.
Not only are two different file exports (CSV & HTML) supported, but an export to a specific in-memory data structure as well.
This requires a very sophisticated _decoupling_ of components => _many_ classes and interfaces.
Some parts also improve the _testability_ of the export functionality.
* `CsvDataExporter`
** Exports data to a CSV file
** This is very easy, because we are receiving `ICsvRepresentable` objects to export
** Several features of the base class help as well
* `DefaultExporterProvider`
** The default provider for exporters used in the application
* `Exporter`
** Can be configured to export data in a specific format and with a specific sorting order
** Before (finally) exporting, the data is sorted accordingly
*** For this task use the https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.sort?view=net-8.0[`Sort`] method of `List<T>`
* `FileExporterBase`
** Base class for those exporters which write data to a file
* `HtmlDataExporter`
** Exports data _encoded_ as an HTML table
*** Does _not_ create a full, valid HTML document -- only the data as HTML table
*** We use `.html` as file extension anyway
*** You know how to create such a table from WMC 😉
** You _have to_ use a https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/raw-string-literal[raw string literal]
*** This is useful when defining semi-structured data like JSON or HTML
*** You can (and should) even use indentation
** `CreateTableRow` will create either a `<tr><th>...</tr>` or `<tr><td>...</tr>` depending on the `isHeader` flag
* `IDataExporter`
** Defines the capability to export data of type `ICsvRepresentable`
* `IExporterProvider`
** Defines the capability to provide an exporter based on a specific, requested export format
** Primarily used for testing here -- could be used at runtime as well if the application expanded
* `SpectreTableExporter`
** Exports data to a `Table` as used by `Spectre.Console`
** We need this for our user interface
** To make it easier for you, these two methods have to be used:
... `table.AddColumn`
... `table.AddRow`
** In addition, you are _allowed_ (this time only, for now) to use the `ToArray` of `IReadOnlyList<string>`
** `FindMaxHeaderColumns`
*** Remember that the data to export can contain `Teacher` & `TeacherWithBusinessCard` objects at the same time
*** The table has to be set to the _max._ number of columns
**** `TeacherWithBusinessCard` has one more, which will simply stay empty for `Teacher` objects
*** We don't know if there are `TeacherWithBusinessCard` contained, so we have to _check_ to find the max. set of headers
TIP: Consider the provided, detailed XMLDoc as well, it offers additional insights
== User Interaction
To conclude this extensive assignment we want to give it the exciting user interface it deserves.
After all, the interface is what customers will see of your product, so we cannot ignore the importance of making a good impression altogether (despite concentrating on clean and *correct* code primarily).
So far, you are limited to console applications.
But those don't have to be boring!
This time we will be using the cross-platform, free & open-source https://www.nuget.org/packages/Spectre.Console[`Spectre.Console`] NuGet package.
It has a https://spectreconsole.net/[good documentation] which I implore you to check out.
There are many features, such as structure (tables, grids, rules,...), interactivity (prompts, loading indicators,...) and design (colors, fonts,...) -- see the examples below (taken from the package website).
.`Spectre.Console` Examples
[%collapsible]
====
image::pics/spectre_examples.png[Spectre.Console Examples]
====
Since this is the first time you have to work with this package, and because some features require callbacks -- which you know from WMC, but we haven't discussed in POSE so far -- most of the implementation has already been provided for you.
Still, some things need to be implemented -- look for the `TODO`.
A <<sec:sample,sample run>> has already been shown at the beginning of the document.
🎉 Congratulations -- you completed the assignment, learnt new things and had a lot of programming exercise 🎉