├── .github ├── CODEOWNERS └── workflows │ ├── dependency-update.yml │ ├── maven-manual-release.yml │ ├── maven-release.yml │ ├── release.yml │ ├── stale.yml │ └── verify.yml ├── .gitignore ├── .mvn └── jvm.config ├── LICENSE ├── LICENSE-THIRD-PARTY ├── NOTICE ├── README.md ├── examples ├── Example.pdf ├── Example.tex ├── Example_global.json ├── Example_month.json ├── Latex_Logo.pdf ├── schemas │ ├── global.json │ └── month.json └── ui │ ├── ui_add_entry.png │ ├── ui_default.png │ ├── ui_file_dialog.png │ ├── ui_global_settings.png │ └── ui_open_file_dialog.png ├── formatter.xml ├── license-header.txt ├── pom.xml ├── preset.pdf ├── renovate.json └── src ├── main ├── java │ ├── checker │ │ ├── CheckerError.java │ │ ├── CheckerException.java │ │ ├── CheckerReturn.java │ │ ├── IChecker.java │ │ ├── MiLoGChecker.java │ │ └── holiday │ │ │ ├── GermanState.java │ │ │ ├── GermanyHolidayChecker.java │ │ │ ├── Holiday.java │ │ │ ├── HolidayFetchException.java │ │ │ └── IHolidayChecker.java │ ├── data │ │ ├── Employee.java │ │ ├── Entry.java │ │ ├── Profession.java │ │ ├── TimeSheet.java │ │ ├── TimeSpan.java │ │ └── WorkingArea.java │ ├── etc │ │ └── ContextStringReplacer.java │ ├── i18n │ │ ├── DateFormatWrapper.java │ │ └── ResourceHandler.java │ ├── io │ │ ├── FileController.java │ │ ├── IGenerator.java │ │ └── LatexGenerator.java │ ├── main │ │ ├── Main.java │ │ ├── UserInput.java │ │ ├── UserInputFile.java │ │ └── UserInputOption.java │ ├── parser │ │ ├── IGlobalParser.java │ │ ├── IHolidayParser.java │ │ ├── IMonthParser.java │ │ ├── ParseException.java │ │ ├── Parser.java │ │ └── json │ │ │ ├── GlobalJson.java │ │ │ ├── HolidayJson.java │ │ │ ├── HolidayMapJson.java │ │ │ ├── JsonGlobalParser.java │ │ │ ├── JsonHolidayParser.java │ │ │ ├── JsonMonthParser.java │ │ │ ├── MonthEntryJson.java │ │ │ └── MonthJson.java │ └── ui │ │ ├── ActionBar.java │ │ ├── DialogHelper.java │ │ ├── DragDropJFrame.java │ │ ├── ErrorHandler.java │ │ ├── GlobalSettingsDialog.java │ │ ├── JTimeField.java │ │ ├── MonthlySettingsBar.java │ │ ├── SaveOnClosePrompt.java │ │ ├── Time.java │ │ ├── TimesheetEntry.java │ │ ├── UserInterface.java │ │ ├── export │ │ ├── FileExporter.java │ │ ├── PDFCompiler.java │ │ ├── TempFiles.java │ │ └── TexCompiler.java │ │ ├── fileexplorer │ │ ├── FileChooser.java │ │ └── FileChooserType.java │ │ └── json │ │ ├── EntrySerializer.java │ │ ├── Global.java │ │ ├── JSONHandler.java │ │ ├── Month.java │ │ ├── MonthSerializer.java │ │ └── UISettings.java └── resources │ ├── MiLoG_Template.tex │ ├── i18n │ └── MessageBundle.properties │ ├── maven │ └── project.properties │ └── pdf │ └── template.pdf └── test ├── java ├── checker │ ├── MiLoGCheckerCheckTest.java │ ├── MiLoGCheckerDayPauseTimeTest.java │ ├── MiLoGCheckerDayTimeBoundsTest.java │ ├── MiLoGCheckerDayTimeExceedanceTest.java │ ├── MiLoGCheckerDepartmentNameTest.java │ ├── MiLoGCheckerRowNumExceedanceTest.java │ ├── MiLoGCheckerTimeOverlapTest.java │ ├── MiLoGCheckerTotalTimeExceedanceTest.java │ ├── MiLoGCheckerValidWorkingDaysTest.java │ └── holiday │ │ ├── GermanyHolidayCheckerTest.java │ │ └── HolidayEqualsDateTest.java ├── data │ ├── EntryArithmeticTest.java │ ├── EntryCommonTest.java │ ├── EntryCompareToTest.java │ ├── TimeSheetArithmeticTest.java │ ├── TimeSheetCommonTest.java │ ├── TimeSpanArithmeticTest.java │ ├── TimeSpanCommonTest.java │ ├── TimeSpanCompareTest.java │ ├── TimeSpanParseTest.java │ └── WorkingAreaParseTest.java ├── etc │ └── ContextStringReplacerTest.java ├── i18n │ ├── DateFormatWrapperTest.java │ └── ResourceHandlerTest.java ├── io │ ├── LatexGeneratorEscapeTest.java │ ├── LatexGeneratorPlaceholderTest.java │ └── LatexGeneratorTest.java └── parser │ ├── ParserJsonTest.java │ └── json │ ├── JsonGlobalParserTest.java │ ├── JsonHolidayParserTest.java │ ├── JsonMonthParserEntryTest.java │ └── JsonMonthParserTest.java └── resources └── i18n_test ├── MessageBundle.properties ├── MessageBundle_de.properties └── MessageBundle_en.properties /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kit-sdq/programming-lecture @kit-sdq/maintainer 2 | -------------------------------------------------------------------------------- /.github/workflows/dependency-update.yml: -------------------------------------------------------------------------------- 1 | name: Maven Dependency Updates 2 | 3 | on: 4 | schedule: 5 | - cron: "00 11 * * 2" 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | update: 12 | uses: kit-sdq/actions/.github/workflows/maven-update.yml@main 13 | secrets: 14 | PAT: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /.github/workflows/maven-manual-release.yml: -------------------------------------------------------------------------------- 1 | name: Maven Release (Manual) 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | inputs: 7 | release-version: 8 | type: string 9 | description: The version for release. E.g., "1.2.3" 10 | required: true 11 | next-version: 12 | type: string 13 | description: The version after release. E.g., "2.0.0-SNAPSHOT" 14 | required: true 15 | jobs: 16 | release: 17 | uses: kit-sdq/actions/.github/workflows/maven-manual-release.yml@main 18 | secrets: 19 | # Needs to be a personal access token to push as a certain user; otherwise actions won't be triggered. 20 | PAT: ${{ secrets.PAT }} 21 | with: 22 | release-version: ${{ github.event.inputs.release-version }} 23 | next-version: ${{ github.event.inputs.next-version }} 24 | -------------------------------------------------------------------------------- /.github/workflows/maven-release.yml: -------------------------------------------------------------------------------- 1 | name: Maven Release 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | update: 9 | uses: kit-sdq/actions/.github/workflows/maven-release.yml@main 10 | secrets: 11 | # Needs to be a personal access token to push as a certain user; otherwise actions won't be triggered. 12 | PAT: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | # Publish `v1.2.3` tags as releases. 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Set up JDK 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'temurin' 22 | java-version: 21 23 | - name: Build with Maven 24 | run: mvn -U -B package 25 | 26 | - name: Create Changelog 27 | shell: bash 28 | run: | 29 | DIFF=$(git log $(git tag --sort=-creatordate | sed '2q;d')..HEAD --oneline) 30 | if [ -z "$DIFF" ]; then 31 | echo "Defaulting to full log" 32 | DIFF=$(git log --oneline) 33 | fi 34 | echo "# Commits since last release" > CHANGELOG.txt 35 | echo "$DIFF" | sed 's/^/* /' | sed '/Auto-Update Dependencies/{s/Updated/\n\t* Updated/g}' | sed '/\[maven-release-plugin\]/d' | sed -r '/.*\(#\w+\)$/d' >> CHANGELOG.txt 36 | 37 | - uses: softprops/action-gh-release@v2 38 | if: startsWith(github.ref, 'refs/tags/v') 39 | with: 40 | body_path: CHANGELOG.txt 41 | generate_release_notes: true 42 | files: | 43 | target/timesheetgenerator.jar 44 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale PRs 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "1 12 * * *" 6 | 7 | jobs: 8 | stale: 9 | if: github.repository_owner == 'kit-sdq' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check for stale PRs 13 | uses: actions/stale@v9 14 | with: 15 | days-before-stale: 7 16 | days-before-close: 14 17 | days-before-issue-stale: -1 18 | remove-stale-when-updated: true 19 | stale-pr-label: "stale" 20 | exempt-pr-labels: "no-stale" 21 | stale-pr-message: 'This PR is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 7 days.' 22 | close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' 23 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Maven Verify 2 | 3 | on: 4 | push: # Ignore releases and main dev branch 5 | tags-ignore: 6 | - 'v*' 7 | branches: 8 | - '**' 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | verify: 17 | uses: kit-sdq/actions/.github/workflows/maven.yml@main 18 | with: 19 | deploy: false 20 | secrets: 21 | CENTRAL_USER: "" 22 | CENTRAL_TOKEN: "" 23 | GPG_KEY: "" 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | out/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | .mvn/wrapper/maven-wrapper.jar 12 | .classpath 13 | .project 14 | .settings 15 | .checkstyle 16 | .idea/ 17 | *.iml 18 | .DS_Store 19 | .vscode 20 | MANIFEST.MF -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yves R. Schneider 4 | Copyright (c) 2024 Benjamin Claus 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This product includes software developed by The Apache Software Foundation (http://www.apache.org/). 2 | 3 | Apache PDFBox is licensed under the Apache License 2.0. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeSheetGenerator 2 | 3 | TimeSheetGenerator is an application that checks and builds time sheet documents. 4 | 5 | ## UI for Timesheet Generator 6 | 7 | This is a UI for the kit-sdq timesheet generator. 8 | Global settings can be edited via the UI, saving files only saves the month settings. 9 | These saved JSON files are also compatible with the original timesheet generator, so the CLI 10 | can be used as well. 11 | 12 | Yes, you can drag and drop your month.json files into the generator. 13 | 14 | The files can be compiled to tex or directly compiled to a pdf. 15 | 16 | #### To open a file with the UI from the command line, you can type `$ java -jar TimeSheetGenerator.jar /path/to/month.json` to open the specified file with the TimesheetGenerator UI. 17 | 18 | ### User Interface: Images 19 | 20 | The User Interface: 21 | 22 | 23 | The User Interface with File options selected: 24 | 25 | 26 | Opening a JSON file with the TimeSheetGenerator: 27 | 28 | 29 | Adding a new entry to the timesheet: 30 | 31 | 32 | Editing the global settings: 33 | 34 | 35 | ## Command Line Execution 36 | 37 | Run TimeSheetGenerator (requires Java 21 or higher): 38 | 39 | `$ java -jar TimeSheetGenerator.jar [--help] [--version] [--gui] [--file ]` 40 | 41 | ### Command Line Options 42 | 43 | | Option | Long Option | Arguments | Description | 44 | | ------ | ----------- | --------------------------------------- | -------------------------------------------------------------- | 45 | | `-h` | `--help` | _none_ | Print a help dialog. | 46 | | `-v` | `--version` | _none_ | Print the version of the application. | 47 | | `-g` | `--gui` | _none_ | Generate an output file based on files chosen in a file dialog.| 48 | | `-f` | `--file` |` `| Generate an output file based on the given files. | 49 | 50 | ### Third-Party Libraries 51 | 52 | This project uses the following third-party libraries: 53 | 54 | - **Apache PDFBox** 55 | - Website: https://pdfbox.apache.org/ 56 | - License: Apache License 2.0 (See `LICENSE` and `NOTICE` files) 57 | -------------------------------------------------------------------------------- /examples/Example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/Example.pdf -------------------------------------------------------------------------------- /examples/Example.tex: -------------------------------------------------------------------------------- 1 | \documentclass[]{scrartcl} 2 | \usepackage[a4paper,top=0.3in,bottom=0.2in,left=0.3in,right=0.3in]{geometry} 3 | \usepackage{graphicx} 4 | \usepackage{amssymb} 5 | \usepackage{array} 6 | \usepackage{background} 7 | \usepackage[utf8]{inputenc} 8 | \usepackage{eurosym} 9 | \thispagestyle{empty} 10 | 11 | \newcommand{\headentry}[1]{\parbox{18.6cm}{#1}} 12 | \newcolumntype{P}[1]{>{\centering\arraybackslash}p{#1}} %uses package 'array' 13 | 14 | \begin{document} 15 | \includegraphics[width=140pt]{Latex_Logo.pdf} \par \smallskip 16 | \sffamily 17 | 18 | %HEADER 19 | \vspace{0.2cm} 20 | \headentry{\huge \textbf{Arbeitszeitdokumentation} \hspace*{\fill} \Large \textbf{Monat / Jahr:} \underline{\parbox{5.0cm}{\centering 11 / 2019 }}} \par \medskip 21 | \headentry{\Large \textbf{Name des Mitarbeiters/der Mitarbeiterin:} \hspace*{\fill} \underline{\parbox{8.5cm}{\mbox{Max Mustermann}}}} \par \medskip 22 | \headentry{\Large \textbf{Personalnummer:} \hspace*{\fill} \underline{\parbox{4.5cm}{\mbox{1234567}} \parbox{3.85cm}{\centering \textbf{GF:} $\Box$ \textbf{UB:} $\boxtimes$}}} \par \medskip %This is the KIT style for workingArea => GF: $\Box$ UB: $\boxtimes$ 23 | \headentry{\Large \textbf{Institut/Organisationseinheit:} \hspace*{\fill} \underline{\parbox{8.5cm}{\mbox{Fakultät für Informatik}}}} \par \medskip 24 | \headentry{\Large \textbf{Vertraglich vereinbarte Arbeitszeit:} \hspace*{\fill} \parbox{8.5cm}{\underline{\parbox{2.35cm}{\centering 40:00}} \parbox{6cm}{\centering \raggedleft \textbf{Stundensatz:} \underline{\parbox{2.25cm}{\centering 10.31 \euro} } } } } \par \medskip 25 | 26 | %BODY 27 | \large 28 | \begin{center} 29 | \begin{tabular}{| P{6.7cm} | P{2cm} | P{1.8cm} | P{1.8cm} | P{1.8cm} | P{2.4cm} |} 30 | \hline 31 | %Line 1 32 | \textbf{T\"atigkeit (Stichwort, Projekt)} 33 | & \textbf{Datum} 34 | & \textbf{Beginn} 35 | & \textbf{Ende} 36 | & \textbf{Pause} 37 | & \textbf{Arbeitszeit\textsuperscript{1}}\\ 38 | \hline 39 | %Line 2 40 | %empty 41 | & \textbf{(tt.mm.jj)} 42 | & \textbf{(hh:mm)} 43 | & \textbf{(hh:mm)} 44 | & \textbf{(hh:mm)} 45 | & \textbf{(hh:mm)}\\ 46 | \hline 47 | %Line 3 48 | \mbox{Korrektur} 49 | & \mbox{02.11.19} 50 | & \mbox{10:00} 51 | & \mbox{11:00} 52 | & \mbox{00:00} 53 | & \mbox{01:00}\\ 54 | \hline 55 | %Line 4 56 | \mbox{Fragen beantworten} 57 | & \mbox{04.11.19} 58 | & \mbox{11:31} 59 | & \mbox{15:11} 60 | & \mbox{00:30} 61 | & \mbox{03:10}\\ 62 | \hline 63 | %Line 5 64 | \mbox{Urlaub in Italien} 65 | & \mbox{11.11.19} 66 | & \mbox{08:00} 67 | & \mbox{16:00} 68 | & \mbox{00:00} 69 | & \mbox{08:00 U}\\ 70 | \hline 71 | %Line 6 72 | \mbox{} 73 | & \mbox{} 74 | & \mbox{} 75 | & \mbox{} 76 | & \mbox{} 77 | & \mbox{}\\ 78 | \hline 79 | %Line 7 80 | \mbox{} 81 | & \mbox{} 82 | & \mbox{} 83 | & \mbox{} 84 | & \mbox{} 85 | & \mbox{}\\ 86 | \hline 87 | %Line 8 88 | \mbox{} 89 | & \mbox{} 90 | & \mbox{} 91 | & \mbox{} 92 | & \mbox{} 93 | & \mbox{}\\ 94 | \hline 95 | %Line 9 96 | \mbox{} 97 | & \mbox{} 98 | & \mbox{} 99 | & \mbox{} 100 | & \mbox{} 101 | & \mbox{}\\ 102 | \hline 103 | %Line 10 104 | \mbox{} 105 | & \mbox{} 106 | & \mbox{} 107 | & \mbox{} 108 | & \mbox{} 109 | & \mbox{}\\ 110 | \hline 111 | %Line 11 112 | \mbox{} 113 | & \mbox{} 114 | & \mbox{} 115 | & \mbox{} 116 | & \mbox{} 117 | & \mbox{}\\ 118 | \hline 119 | %Line 12 120 | \mbox{} 121 | & \mbox{} 122 | & \mbox{} 123 | & \mbox{} 124 | & \mbox{} 125 | & \mbox{}\\ 126 | \hline 127 | %Line 13 128 | \mbox{} 129 | & \mbox{} 130 | & \mbox{} 131 | & \mbox{} 132 | & \mbox{} 133 | & \mbox{}\\ 134 | \hline 135 | %Line 14 136 | \mbox{} 137 | & \mbox{} 138 | & \mbox{} 139 | & \mbox{} 140 | & \mbox{} 141 | & \mbox{}\\ 142 | \hline 143 | %Line 15 144 | \mbox{} 145 | & \mbox{} 146 | & \mbox{} 147 | & \mbox{} 148 | & \mbox{} 149 | & \mbox{}\\ 150 | \hline 151 | %Line 16 152 | \mbox{} 153 | & \mbox{} 154 | & \mbox{} 155 | & \mbox{} 156 | & \mbox{} 157 | & \mbox{}\\ 158 | \hline 159 | %Line 17 160 | \mbox{} 161 | & \mbox{} 162 | & \mbox{} 163 | & \mbox{} 164 | & \mbox{} 165 | & \mbox{}\\ 166 | \hline 167 | %Line 18 168 | \mbox{} 169 | & \mbox{} 170 | & \mbox{} 171 | & \mbox{} 172 | & \mbox{} 173 | & \mbox{}\\ 174 | \hline 175 | %Line 19 176 | \mbox{} 177 | & \mbox{} 178 | & \mbox{} 179 | & \mbox{} 180 | & \mbox{} 181 | & \mbox{}\\ 182 | \hline 183 | %Line 20 184 | \mbox{} 185 | & \mbox{} 186 | & \mbox{} 187 | & \mbox{} 188 | & \mbox{} 189 | & \mbox{}\\ 190 | \hline 191 | %Line 21 192 | \mbox{} 193 | & \mbox{} 194 | & \mbox{} 195 | & \mbox{} 196 | & \mbox{} 197 | & \mbox{}\\ 198 | \hline 199 | %Line 22 200 | \mbox{} 201 | & \mbox{} 202 | & \mbox{} 203 | & \mbox{} 204 | & \mbox{} 205 | & \mbox{}\\ 206 | \hline 207 | %Leerzeile 208 | \multicolumn{6}{c}{\thinspace}\\ 209 | %Urlaub 210 | \cline{3-6} 211 | \multicolumn{2}{c}{\thinspace} 212 | & \multicolumn{3}{|c|}{\centering \textbf{Urlaub anteilig:}} 213 | & 08:00\\ 214 | \cline{3-6} 215 | %Summe 216 | \multicolumn{2}{c}{\thinspace} 217 | & \multicolumn{3}{|c|}{\centering \textbf{Summe:}} 218 | & 12:10\\ 219 | \cline{3-6} 220 | %SollArbeitszeit 221 | \multicolumn{2}{c}{\thinspace} 222 | & \multicolumn{3}{|c|}{\centering \textbf{monatliche Soll-Arbeitszeit:}} 223 | & 40:00\\ 224 | \cline{3-6} 225 | %Übertrag Vormonat 226 | \multicolumn{2}{c}{\thinspace} 227 | & \multicolumn{3}{|c|}{\centering \textbf{Übertrag vom Vormonat:}} 228 | & 00:00\\ 229 | \cline{3-6} 230 | %Übertrag Folgemonat 231 | \multicolumn{2}{c}{\thinspace} 232 | & \multicolumn{3}{|c|}{\centering \textbf{Übertrag in den Folgemonat:}} 233 | & 00:00\\ 234 | \cline{3-6} 235 | \end{tabular} 236 | \end{center} 237 | 238 | %FOOTER 239 | \par \bigskip \bigskip \medskip 240 | \headentry{\large Ich bestätige die Richtigkeit der Angaben: \hspace*{\fill} $\overline{{\parbox{5.75cm}{\normalsize Datum, Beschäftigte/r} } }$ } \par \medskip 241 | \headentry{\normalsize Nach \textbf{$\S$17 Mindestlohngesetz (MiLoG)} müssen für geringfügig entlohnte und kurzfristig beschäftigte Arbeitnehmer/innen u.a. Beginn, Ende und Dauer der täglichen Arbeitszeit aufgezeichnet und für Kontrollzwecke mindestens 2 Jahre am Ort der Beschäftigung aufbewahrt werden!} \par \bigskip \bigskip 242 | \headentry{\hspace*{\fill} geprüft: $\overline{{\parbox{5.75cm}{\normalsize Datum, Dienstvorgesetzte/r} } }$} \par \medskip 243 | \rule{6cm}{0.2pt} \par \smallskip 244 | \headentry{\textsuperscript{1} Summe in vollen Stunden und Minuten ohne Pause (Std:Min); bei Abwesenheit können auch folgende Kürzel eingetragen werden: U=Urlaub, K=Krankheit, F=Feiertag, S=Sonstiges} 245 | 246 | %BACKGROUND 247 | \SetBgContents{K\_IPD\_AZDoku\_01\_01-20} 248 | \SetBgPosition{-2.4cm, -29.2cm} 249 | \SetBgColor{black} 250 | \SetBgOpacity{1.0} 251 | \SetBgAngle{90.0} 252 | \SetBgScale{0.8} 253 | \end{document} 254 | -------------------------------------------------------------------------------- /examples/Example_global.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/master/examples/schemas/global.json", 3 | "name": "Max Mustermann", 4 | "staffId": 1234567, 5 | "department": "Fakultät für Informatik", 6 | "workingTime": "40:00", 7 | "wage": 10.31, 8 | "workingArea": "ub" 9 | } -------------------------------------------------------------------------------- /examples/Example_month.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/master/examples/schemas/month.json", 3 | "year": 2019, 4 | "month": 11, 5 | "pred_transfer": "0:00", 6 | "succ_transfer": "0:00", 7 | "entries": [ 8 | {"action": "Korrektur", "day": 2, "start": "10:00", "end": "11:00"}, 9 | {"action": "Fragen beantworten", "day": 4, "start": "11:31", "end": "15:11", "pause": "00:30"}, 10 | {"action": "Urlaub in Italien", "day": 11, "start": "08:00", "end": "16:00", "vacation": true} 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/Latex_Logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/Latex_Logo.pdf -------------------------------------------------------------------------------- /examples/schemas/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "definitions": { 5 | "time_span": { 6 | "type": "string", 7 | "pattern": "^[0-9]+:[0-5]?[0-9]$" 8 | } 9 | }, 10 | "properties": { 11 | "name": { 12 | "type": "string" 13 | }, 14 | "staffId": { 15 | "type": "integer" 16 | }, 17 | "department": { 18 | "type": "string" 19 | }, 20 | "workingTime": { 21 | "$ref": "#/definitions/time_span" 22 | }, 23 | "wage": { 24 | "type": "number", 25 | "minimum": 0.0 26 | }, 27 | "workingArea": { 28 | "type": "string", 29 | "enum": ["ub", "gf"] 30 | } 31 | }, 32 | "patternProperties": { 33 | "^\\$.*$": {} 34 | }, 35 | "required": ["name", "staffId", "department", "workingTime", "wage", "workingArea"], 36 | "additionalProperties": false 37 | } 38 | -------------------------------------------------------------------------------- /examples/schemas/month.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "definitions": { 5 | "time_span": { 6 | "type": "string", 7 | "pattern": "^[0-9]+:[0-5]?[0-9]$" 8 | }, 9 | "day_time": { 10 | "type": "string", 11 | "pattern": "^(([0-1]?[0-9])|(2[0-3])):[0-5]?[0-9]$" 12 | }, 13 | "entry": { 14 | "type": "object", 15 | "properties": { 16 | "action": { 17 | "type": "string" 18 | }, 19 | "day": { 20 | "type": "integer", 21 | "minimum": 1, 22 | "maximum": 31 23 | }, 24 | "start": { 25 | "$ref": "#/definitions/day_time" 26 | }, 27 | "end": { 28 | "$ref": "#/definitions/day_time" 29 | }, 30 | "pause": { 31 | "$ref": "#/definitions/time_span" 32 | }, 33 | "vacation": { 34 | "type": "boolean", 35 | "default": false 36 | } 37 | }, 38 | "required": ["action", "day", "start", "end"], 39 | "not": { "required": ["pause", "vacation"] }, 40 | "additionalProperties": false 41 | } 42 | }, 43 | "properties": { 44 | "year": { 45 | "type": "integer", 46 | "minimum": 1000, 47 | "maximum": 9999 48 | }, 49 | "month": { 50 | "type": "integer", 51 | "minimum": 1, 52 | "maximum": 12 53 | }, 54 | "pred_transfer": { 55 | "$ref": "#/definitions/time_span" 56 | }, 57 | "succ_transfer": { 58 | "$ref": "#/definitions/time_span" 59 | }, 60 | "entries": { 61 | "type": "array", 62 | "items": { 63 | "$ref": "#/definitions/entry" 64 | } 65 | } 66 | }, 67 | "patternProperties": { 68 | "^\\$.*$": {} 69 | }, 70 | "required": ["year", "month", "entries"], 71 | "additionalProperties": false 72 | } 73 | -------------------------------------------------------------------------------- /examples/ui/ui_add_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/ui/ui_add_entry.png -------------------------------------------------------------------------------- /examples/ui/ui_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/ui/ui_default.png -------------------------------------------------------------------------------- /examples/ui/ui_file_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/ui/ui_file_dialog.png -------------------------------------------------------------------------------- /examples/ui/ui_global_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/ui/ui_global_settings.png -------------------------------------------------------------------------------- /examples/ui/ui_open_file_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/examples/ui/ui_open_file_dialog.png -------------------------------------------------------------------------------- /license-header.txt: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT $YEAR. */ -------------------------------------------------------------------------------- /preset.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/365fade8ee2504f25585bebda1be50466f0a0438/preset.pdf -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "description": "Automerge non-major updates", 9 | "matchUpdateTypes": ["minor", "patch"], 10 | "automerge": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/checker/CheckerError.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023. */ 2 | package checker; 3 | 4 | import data.TimeSheet; 5 | 6 | /** 7 | * A CheckerError is used when an expected or user induced error occurs while 8 | * checking a {@link TimeSheet} with an {@link IChecker} instance. Therefore it 9 | * is not insoluble and should not be treated like an {@link Exception}, 10 | * especially the {@link CheckerException}. 11 | */ 12 | public class CheckerError { 13 | 14 | private final CheckerErrorMessageProvider errorMessageProvider; 15 | private final Object[] args; 16 | 17 | /** 18 | * Constructs a new {@link CheckerError} instance. 19 | * 20 | * @param errorMessageProvider - Provider for the error message. 21 | * @param args - Arguments referenced by the format specifiers 22 | * in the errorMsg format string. 23 | */ 24 | public CheckerError(CheckerErrorMessageProvider errorMessageProvider, Object... args) { 25 | this.errorMessageProvider = errorMessageProvider; 26 | this.args = args; 27 | } 28 | 29 | /** 30 | * Gets the error message of an {@link CheckerError}. 31 | * 32 | * @return The error message. 33 | */ 34 | public String getErrorMessage() { 35 | return errorMessageProvider.getErrorMessage(args); 36 | } 37 | 38 | /** 39 | * Gets the {@link CheckerErrorMessageProvider} used to create this 40 | * {@link CheckerError}. 41 | * 42 | * @return The checker error message provider. 43 | */ 44 | public CheckerErrorMessageProvider getErrorMessageProvider() { 45 | return errorMessageProvider; 46 | } 47 | 48 | /** 49 | * Gets the arguments used to create this {@link CheckerError}. 50 | * 51 | * @return The arguments. 52 | */ 53 | public Object[] getArgs() { 54 | return args; 55 | } 56 | 57 | /** 58 | * A CheckerErrorMessageProvider provides a message for given arguments that can 59 | * be used in a CheckerError. 60 | */ 61 | public interface CheckerErrorMessageProvider { 62 | 63 | /** 64 | * Gets the error message. 65 | * 66 | * @param args Arguments that will be inserted in the error message 67 | * @return Error message including the formatted arguments 68 | */ 69 | String getErrorMessage(Object... args); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/checker/CheckerException.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker; 3 | 4 | import java.io.Serial; 5 | 6 | /** 7 | * A CheckerException is thrown by a {@link IChecker} instance when an 8 | * unexpected or insoluble error occurs. 9 | */ 10 | public class CheckerException extends Exception { 11 | 12 | /** 13 | * Auto-generated serial version UID 14 | */ 15 | @Serial 16 | private static final long serialVersionUID = 4362647380313599066L; 17 | 18 | /** 19 | * Constructs a new {@link CheckerException}. 20 | * 21 | * @param message - message of the error that occurred. 22 | */ 23 | public CheckerException(String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/checker/CheckerReturn.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker; 3 | 4 | import data.TimeSheet; 5 | 6 | /** 7 | * Represents the possible return values of the checker class. 8 | */ 9 | public enum CheckerReturn { 10 | /** 11 | * Returned if the {@link TimeSheet} to be checked is invalid. 12 | */ 13 | INVALID, 14 | 15 | /** 16 | * Returned if the {@link TimeSheet} to be checked is valid. 17 | */ 18 | VALID 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/checker/IChecker.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker; 3 | 4 | import data.TimeSheet; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * A checker is able to check the validity of an {@link TimeSheet}. 10 | */ 11 | public interface IChecker { 12 | 13 | /** 14 | * Checks the validity of a {@link TimeSheet}. 15 | * 16 | * @return If the {@link TimeSheet} is valid {@link CheckerReturn}.Valid is 17 | * returned. Invalid otherwise. 18 | * @throws CheckerException if an error occurs while checking the 19 | * {@link TimeSheet}. 20 | */ 21 | CheckerReturn check() throws CheckerException; 22 | 23 | /** 24 | * Returns a {@link Collection} of {@link CheckerError} elements that occurred 25 | * while checking a {@link TimeSheet}. 26 | * 27 | * @return A {@link Collection} of {@link CheckerError CheckerErrors}. 28 | */ 29 | Collection getErrors(); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/checker/holiday/GermanState.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | /** 5 | * Elements of this enumeration represent a state of Germany. 6 | */ 7 | public enum GermanState { 8 | 9 | /** 10 | * Baden-Württemberg 11 | */ 12 | BW, 13 | 14 | /** 15 | * Bayern 16 | */ 17 | BY, 18 | 19 | /** 20 | * Berlin 21 | */ 22 | BE, 23 | 24 | /** 25 | * Brandenburg 26 | */ 27 | BB, 28 | 29 | /** 30 | * Bremen 31 | */ 32 | HB, 33 | 34 | /** 35 | * Hamburg 36 | */ 37 | HH, 38 | 39 | /** 40 | * Hessen 41 | */ 42 | HE, 43 | 44 | /** 45 | * Mecklenburg-Vorpommern 46 | */ 47 | MV, 48 | 49 | /** 50 | * Niedersachsen 51 | */ 52 | NI, 53 | 54 | /** 55 | * Nordrhein-Westfalen 56 | */ 57 | NW, 58 | 59 | /** 60 | * Rheinland-Pfalz 61 | */ 62 | RP, 63 | 64 | /** 65 | * Saarland 66 | */ 67 | SL, 68 | 69 | /** 70 | * Sachsen 71 | */ 72 | SN, 73 | 74 | /** 75 | * Sachsen-Anhalt 76 | */ 77 | ST, 78 | 79 | /** 80 | * Schleswig-Holstein 81 | */ 82 | SH, 83 | 84 | /** 85 | * Thüringen 86 | */ 87 | TH, 88 | 89 | /** 90 | * Deutschland 91 | */ 92 | NATIONAL 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/checker/holiday/GermanyHolidayChecker.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import io.FileController; 5 | import parser.IHolidayParser; 6 | import parser.ParseException; 7 | import parser.json.JsonHolidayParser; 8 | 9 | import javax.net.ssl.SSLHandshakeException; 10 | import java.io.IOException; 11 | import java.net.URL; 12 | import java.time.LocalDate; 13 | import java.time.Year; 14 | import java.util.Collection; 15 | 16 | /** 17 | * A GermanyHolidayChecker is a holiday checker implementing 18 | * {@link IHolidayChecker} that is able to check for legal {@link Holiday 19 | * holidays} of all different {@link GermanState GermanStates}. 20 | */ 21 | public class GermanyHolidayChecker implements IHolidayChecker { 22 | 23 | private final Year year; 24 | private final GermanState state; 25 | private Collection holidays; 26 | private static final String HOLIDAY_FETCH_ADDRESS_HTTPS = "https://feiertage-api.de/api/?jahr=$year$&nur_land=$state$"; 27 | private static final String HOLIDAY_FETCH_ADDRESS_HTTP = "http://feiertage-api.de/api/?jahr=$year$&nur_land=$state$"; 28 | 29 | /** 30 | * Constructs a new {@link GermanyHolidayChecker} instance. 31 | * 32 | * @param year - in which the {@link Holiday holidays} take place. 33 | * @param state - of Germany to check for possible {@link Holiday holidays}. 34 | */ 35 | public GermanyHolidayChecker(int year, GermanState state) { 36 | this.year = Year.of(year); 37 | this.state = state; 38 | } 39 | 40 | @Override 41 | public boolean isHoliday(LocalDate date) throws HolidayFetchException { 42 | if (!hasHolidays()) { 43 | fetchHolidays(); 44 | } 45 | 46 | for (Holiday holiday : holidays) { 47 | if (holiday.equalsDate(date)) { 48 | return true; 49 | } 50 | } 51 | return false; 52 | } 53 | 54 | @Override 55 | public Collection getHolidays() throws HolidayFetchException { 56 | if (!hasHolidays()) { 57 | fetchHolidays(); 58 | } 59 | return holidays; 60 | } 61 | 62 | /** 63 | * Fetches the occurring holidays from a specific source. 64 | * 65 | * @throws HolidayFetchException if an error occurs fetching the holidays. 66 | */ 67 | private void fetchHolidays() throws HolidayFetchException { 68 | String stringHolidays; 69 | try { 70 | stringHolidays = fetchHolidaysJSONString(); 71 | } catch (IOException e) { 72 | throw new HolidayFetchException(e.getMessage()); 73 | } 74 | 75 | try { 76 | IHolidayParser holidayParser = new JsonHolidayParser(stringHolidays); 77 | 78 | holidays = holidayParser.getHolidays(); 79 | } catch (ParseException e) { 80 | throw new HolidayFetchException(e.getMessage()); 81 | } 82 | 83 | } 84 | 85 | /** 86 | * Reads holidays formatted as JSON string and retries with fallback http 87 | * address if https is not available. 88 | * 89 | * @return Holidays formatted as JSON string 90 | * @throws IOException if an I/O error occurs. 91 | */ 92 | private String fetchHolidaysJSONString() throws IOException { 93 | try { 94 | return readHolidayJSONStringFromAddress(HOLIDAY_FETCH_ADDRESS_HTTPS); 95 | } catch (SSLHandshakeException e) { 96 | return readHolidayJSONStringFromAddress(HOLIDAY_FETCH_ADDRESS_HTTP); 97 | } 98 | } 99 | 100 | /** 101 | * Reads holidays formatted as JSON string from address given. 102 | * 103 | * @param address - to fetch holidays from 104 | * @return Holidays formatted as JSON string 105 | * @throws SSLHandshakeException if an SSL handshake error occurs. 106 | * @throws IOException if an I/O error occurs. 107 | */ 108 | private String readHolidayJSONStringFromAddress(String address) throws SSLHandshakeException, IOException { 109 | String filledAddress = address.replace("$year$", Integer.toString(year.getValue())).replace("$state$", state.name()); 110 | 111 | return FileController.readURLToString(new URL(filledAddress)); 112 | } 113 | 114 | /** 115 | * Checks whether the {@link Holiday holidays} are already fetched. 116 | * 117 | * @return {@code True} if the holidays are already fetched, {@code False} 118 | * otherwise. 119 | */ 120 | private boolean hasHolidays() { 121 | return holidays != null && !holidays.isEmpty(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/checker/holiday/Holiday.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import java.time.LocalDate; 5 | 6 | /** 7 | * Represents a certain holiday used by a class implementing 8 | * {@link IHolidayChecker}. 9 | */ 10 | public class Holiday { 11 | 12 | private final LocalDate date; 13 | private final String description; 14 | 15 | /** 16 | * Constructs a new {@link Holiday} instance. 17 | * 18 | * @param date - on which the holiday takes place. 19 | * @param description - of the holiday. 20 | */ 21 | public Holiday(LocalDate date, String description) { 22 | this.date = date; 23 | this.description = description; 24 | } 25 | 26 | /** 27 | * Gets the description of a {@link Holiday}. 28 | * 29 | * @return The description. 30 | */ 31 | public String getDescription() { 32 | return this.description; 33 | } 34 | 35 | /** 36 | * Gets the date of a {@link Holiday}. 37 | * 38 | * @return The date. 39 | */ 40 | public LocalDate getDate() { 41 | return this.date; 42 | } 43 | 44 | /** 45 | * Checks whether a {@link Holiday} takes place on a given date. 46 | * 47 | * @param otherDate - to check if the holiday takes place on. 48 | * @return True if the {@link Holiday} takes place on this day, false otherwise. 49 | */ 50 | public boolean equalsDate(LocalDate otherDate) { 51 | return date.equals(otherDate); 52 | } 53 | 54 | @Override 55 | public boolean equals(Object other) { 56 | if (!(other instanceof Holiday otherHoliday)) { 57 | return false; 58 | } 59 | 60 | return this.date.equals(otherHoliday.date) && this.description.equals(otherHoliday.description); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/checker/holiday/HolidayFetchException.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import java.io.Serial; 5 | 6 | /** 7 | * This exception is thrown by an {@link IHolidayChecker} instance if an error 8 | * occurs while fetching the possible {@link Holiday holidays}. 9 | */ 10 | public class HolidayFetchException extends Exception { 11 | 12 | /** 13 | * Auto-generated serialVersionUID 14 | */ 15 | @Serial 16 | private static final long serialVersionUID = -4763109415356430991L; 17 | 18 | /** 19 | * Constructs a new {@link HolidayFetchException}. 20 | * 21 | * @param message - message of the error that occurred. 22 | */ 23 | public HolidayFetchException(String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/checker/holiday/IHolidayChecker.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import java.time.LocalDate; 5 | import java.util.Collection; 6 | 7 | /** 8 | * A HolidayChecker instance is able to check whether a given date is a holiday. 9 | */ 10 | public interface IHolidayChecker { 11 | 12 | /** 13 | * Checks whether a given date is a {@link Holiday holiday}. 14 | * 15 | * @param date - to be checked. 16 | * @return True if the date is a holiday, false otherwise. 17 | * @throws HolidayFetchException if an error occurs while fetching possible 18 | * holidays. 19 | */ 20 | boolean isHoliday(LocalDate date) throws HolidayFetchException; 21 | 22 | /** 23 | * Returns a {@link Collection} of all {@link Holiday holidays} associated with 24 | * a specific implementation of {@link IHolidayChecker}. 25 | * 26 | * @return A {@link Collection} of all holidays. 27 | * @throws HolidayFetchException if an error occurs while fetching possible 28 | * holidays. 29 | */ 30 | Collection getHolidays() throws HolidayFetchException; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/data/Employee.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | /** 5 | * Represents an employee. 6 | */ 7 | public class Employee { 8 | 9 | private final String name; 10 | private final int id; 11 | 12 | /** 13 | * Constructs a new employee instance. 14 | * 15 | * @param name - The full name of the employee 16 | * @param id - The employee id 17 | */ 18 | public Employee(String name, int id) { 19 | this.name = name; 20 | this.id = id; 21 | } 22 | 23 | /** 24 | * Gets the name of an employee. 25 | * 26 | * @return The name of the employee. 27 | */ 28 | public String getName() { 29 | return this.name; 30 | } 31 | 32 | /** 33 | * Gets the id of an employee. 34 | * 35 | * @return The id of the employee. 36 | */ 37 | public int getId() { 38 | return this.id; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object other) { 43 | if (!(other instanceof Employee otherEmployee)) { 44 | return false; 45 | } 46 | return this.name.equals(otherEmployee.name) && this.id == otherEmployee.id; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/data/Entry.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import i18n.ResourceHandler; 5 | 6 | import java.time.LocalDate; 7 | 8 | /** 9 | * An entry represents a continuous work interval of an {@link Employee}. 10 | */ 11 | public class Entry implements Comparable { 12 | 13 | private static final int MAX_HOUR_PER_DAY = 23; 14 | 15 | private final String action; 16 | private final LocalDate date; 17 | private final TimeSpan start; 18 | private final TimeSpan end; 19 | private final TimeSpan pause; 20 | private final boolean vacation; 21 | 22 | /** 23 | * Constructs a new instance of {@link Entry} 24 | * 25 | * @param action - the activity or title of the work done in this period of 26 | * time 27 | * @param date - the date on which this {@link Entry} took place 28 | * @param start - the starting time of the work interval 29 | * @param end - the end time of the work interval 30 | * @param pause - the breaks taken in this work interval 31 | * @param vacation - if the entry is a vacation entry (may not contain a pause 32 | * if true) 33 | */ 34 | public Entry(String action, LocalDate date, TimeSpan start, TimeSpan end, TimeSpan pause, boolean vacation) { 35 | if (start.getHour() > MAX_HOUR_PER_DAY || end.getHour() > MAX_HOUR_PER_DAY) { 36 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.entry.timeOverUpperLimit")); 37 | } else if (end.compareTo(start) < 0) { 38 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.entry.startGreaterThanEnd")); 39 | } 40 | 41 | if (!pause.equals(new TimeSpan(0, 0)) && vacation) { 42 | throw new IllegalArgumentException("Vacation entries may not contain a pause."); 43 | } 44 | 45 | this.action = action; 46 | this.date = date; 47 | this.start = start; 48 | this.end = end; 49 | this.pause = pause; 50 | this.vacation = vacation; 51 | } 52 | 53 | /** 54 | * Gets the action of an {@link Entry}. 55 | * 56 | * @return The action. 57 | */ 58 | public String getAction() { 59 | return action; 60 | } 61 | 62 | /** 63 | * Gets the date of an {@link Entry}. 64 | * 65 | * @return The date. 66 | */ 67 | public LocalDate getDate() { 68 | return date; 69 | } 70 | 71 | /** 72 | * Gets the start of an {@link Entry}. 73 | * 74 | * @return The start. 75 | */ 76 | public TimeSpan getStart() { 77 | return start; 78 | } 79 | 80 | /** 81 | * Gets the end of an {@link Entry}. 82 | * 83 | * @return The end. 84 | */ 85 | public TimeSpan getEnd() { 86 | return end; 87 | } 88 | 89 | /** 90 | * Gets the pause of an {@link Entry}. 91 | * 92 | * @return The pause. 93 | */ 94 | public TimeSpan getPause() { 95 | return pause; 96 | } 97 | 98 | /** 99 | * If the entry is a vacation entry. 100 | * 101 | * @return True if the entry represents vacation time, False otherwise. 102 | */ 103 | public boolean isVacation() { 104 | return vacation; 105 | } 106 | 107 | /** 108 | * Calculates the working time. Working time defines the difference between 109 | * start time and end time without break time. 110 | * 111 | * @return The working time 112 | */ 113 | public TimeSpan getWorkingTime() { 114 | TimeSpan workingTime = this.getEnd(); 115 | 116 | workingTime = workingTime.subtract(this.getStart()); 117 | workingTime = workingTime.subtract(this.getPause()); 118 | 119 | return workingTime; 120 | } 121 | 122 | /** 123 | * Compare by date and start time. 124 | */ 125 | @Override 126 | public int compareTo(Entry o) { 127 | if (!this.date.isEqual(o.getDate())) { 128 | return this.date.compareTo(o.getDate()); 129 | } else { 130 | return this.start.compareTo(o.getStart()); 131 | } 132 | } 133 | 134 | @Override 135 | public boolean equals(Object other) { 136 | if (!(other instanceof Entry otherEntry)) { 137 | return false; 138 | } 139 | 140 | return this.action.equals(otherEntry.action) && this.date.equals(otherEntry.date) && this.start.equals(otherEntry.start) 141 | && this.end.equals(otherEntry.end) && this.pause.equals(otherEntry.pause) && this.vacation == otherEntry.vacation; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/data/Profession.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | /** 5 | * Represents the profession of an {@link Employee employee}. 6 | */ 7 | public class Profession { 8 | 9 | private final String departmentName; 10 | private final WorkingArea workingArea; 11 | private final TimeSpan maxWorkingTime; 12 | private final double wage; 13 | 14 | /** 15 | * Constructs a new {@link Profession profession} instance. 16 | * 17 | * @param departmentName - Name of the department this profession is associated 18 | * with. 19 | * @param workingArea - {@link WorkingArea} of this profession. 20 | * @param maxWorkingTime - Maximum number of hours an {@link Employee employee} 21 | * may work per month. 22 | * @param wage - Wage per hour an {@link Employee employee} earns. 23 | */ 24 | public Profession(String departmentName, WorkingArea workingArea, TimeSpan maxWorkingTime, double wage) { 25 | this.departmentName = departmentName; 26 | this.workingArea = workingArea; 27 | this.maxWorkingTime = maxWorkingTime; 28 | this.wage = wage; 29 | } 30 | 31 | /** 32 | * Gets the name of the department this profession is associated with. 33 | * 34 | * @return The name of the department. 35 | */ 36 | public String getDepartmentName() { 37 | return departmentName; 38 | } 39 | 40 | /** 41 | * Gets the {@link WorkingArea} of this profession. 42 | * 43 | * @return The {@link WorkingArea}. 44 | */ 45 | public WorkingArea getWorkingArea() { 46 | return workingArea; 47 | } 48 | 49 | /** 50 | * Gets the maximum number of hours an {@link Employee employee} may work per 51 | * month. 52 | * 53 | * @return The maximum number of hours. 54 | */ 55 | public TimeSpan getMaxWorkingTime() { 56 | return maxWorkingTime; 57 | } 58 | 59 | /** 60 | * Gets the wage per hour an {@link Employee employee} earns. 61 | * 62 | * @return The wage per hour. 63 | */ 64 | public double getWage() { 65 | return wage; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object other) { 70 | if (!(other instanceof Profession otherProfession)) { 71 | return false; 72 | } 73 | 74 | return (this.departmentName.equals(otherProfession.departmentName) && this.workingArea.equals(otherProfession.workingArea)) 75 | && this.maxWorkingTime.equals(otherProfession.maxWorkingTime) && this.wage == otherProfession.wage; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/data/TimeSheet.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import i18n.ResourceHandler; 5 | 6 | import java.time.Month; 7 | import java.time.YearMonth; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * A time sheet represents a whole month of work done by an {@link Employee}. 14 | */ 15 | public class TimeSheet { 16 | private final Employee employee; 17 | private final Profession profession; 18 | private final YearMonth yearMonth; 19 | private final TimeSpan succTransfer, predTransfer; 20 | private final List entries; 21 | 22 | /** 23 | * Constructs a new instance of {@code TimeSheet}. 24 | * 25 | * @param employee - The {@link Employee employee} this time sheet is 26 | * associated with. 27 | * @param profession - The {@link Profession profession} of the 28 | * {@link Employee employee}. 29 | * @param yearMonth - The year and month this time sheet is associated with. 30 | * @param entries - The {@link Entry entries} this time sheet should 31 | * consist of. 32 | * @param succTransfer - The time that should be carried over to the next time 33 | * sheet. 34 | * @param predTransfer - The time that got carried over from the last time 35 | * sheet. 36 | */ 37 | public TimeSheet(Employee employee, Profession profession, YearMonth yearMonth, Entry[] entries, TimeSpan succTransfer, TimeSpan predTransfer) { 38 | 39 | this.employee = employee; 40 | this.profession = profession; 41 | this.yearMonth = yearMonth; 42 | this.succTransfer = succTransfer; 43 | this.predTransfer = predTransfer; 44 | 45 | List entryList = Arrays.asList(entries); 46 | Collections.sort(entryList); 47 | this.entries = Collections.unmodifiableList(entryList); 48 | 49 | /* 50 | * This check has to be done in order to guarantee that the corrected max 51 | * working time (corrected => taking vacation and transfer into account) is not 52 | * negative. 53 | * 54 | * TODO: I don't think this belongs here, should probably be in some checker. 55 | */ 56 | if (profession.getMaxWorkingTime().add(succTransfer).compareTo(predTransfer.add(getTotalVacationTime())) < 0) { 57 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.timesheet.sumOfTimeNegative")); 58 | } 59 | } 60 | 61 | /** 62 | * Gets the year of a {@link TimeSheet}. 63 | * 64 | * @return The year. 65 | */ 66 | public int getYear() { 67 | return yearMonth.getYear(); 68 | } 69 | 70 | /** 71 | * Gets the {@link Month} of a {@link TimeSheet}. 72 | * 73 | * @return The month. 74 | */ 75 | public Month getMonth() { 76 | return yearMonth.getMonth(); 77 | } 78 | 79 | /** 80 | * Gets all entries associated with a {@link TimeSheet}. The list of entries is 81 | * sorted as specified in {@link Entry}. 82 | * 83 | * @return The entries. 84 | */ 85 | public List getEntries() { 86 | return entries; 87 | } 88 | 89 | /** 90 | * Gets the transfered time from the predecessor month of a {@link TimeSheet}. 91 | * 92 | * @return The transfered time from the predecessor month. 93 | */ 94 | public TimeSpan getPredTransfer() { 95 | return this.predTransfer; 96 | } 97 | 98 | /** 99 | * Gets the transfered time from the successor month of a {@link TimeSheet}. 100 | * 101 | * @return The transfered time from the successor month. 102 | */ 103 | public TimeSpan getSuccTransfer() { 104 | return this.succTransfer; 105 | } 106 | 107 | /** 108 | * Gets the {@link Employee} associated with a {@link TimeSheet}. 109 | * 110 | * @return The employee. 111 | */ 112 | public Employee getEmployee() { 113 | return this.employee; 114 | } 115 | 116 | /** 117 | * Gets the {@link Profession} associated with a {@link TimeSheet}. 118 | * 119 | * @return The profession. 120 | */ 121 | public Profession getProfession() { 122 | return this.profession; 123 | } 124 | 125 | /** 126 | * Calculates the overall working time of all entries. 127 | * 128 | * @return The overall, summed up working time. 129 | */ 130 | public TimeSpan getTotalWorkTime() { 131 | TimeSpan totalWorkTime = new TimeSpan(0, 0); 132 | 133 | for (Entry entry : this.getEntries()) { 134 | if (!entry.isVacation()) { 135 | totalWorkTime = totalWorkTime.add(entry.getWorkingTime()); 136 | } 137 | } 138 | 139 | return totalWorkTime; 140 | } 141 | 142 | /** 143 | * Calculates the overall vacation time of all entries. 144 | * 145 | * @return The overall, summed up vacation time. 146 | */ 147 | public TimeSpan getTotalVacationTime() { 148 | TimeSpan totalVacationTime = new TimeSpan(0, 0); 149 | 150 | for (Entry entry : this.getEntries()) { 151 | if (entry.isVacation()) { 152 | totalVacationTime = totalVacationTime.add(entry.getWorkingTime()); 153 | } 154 | } 155 | 156 | return totalVacationTime; 157 | } 158 | 159 | @Override 160 | public boolean equals(Object other) { 161 | if (!(other instanceof TimeSheet otherTimeSheet)) { 162 | return false; 163 | } 164 | 165 | return this.employee.equals(otherTimeSheet.employee) && this.profession.equals(otherTimeSheet.profession) 166 | && this.yearMonth.equals(otherTimeSheet.yearMonth) && this.succTransfer.equals(otherTimeSheet.succTransfer) 167 | && this.predTransfer.equals(otherTimeSheet.predTransfer) && this.entries.equals(otherTimeSheet.entries); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/data/TimeSpan.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import i18n.ResourceHandler; 5 | 6 | /** 7 | * An immutable time span consisting of hours and minutes as well as basic 8 | * arithmetic for it. 9 | */ 10 | public class TimeSpan implements Comparable { 11 | 12 | public static final int MIN_HOUR = 0; 13 | public static final int MIN_MINUTE = 0; 14 | public static final int MAX_MINUTE = 59; 15 | 16 | private final int minute; 17 | private final int hour; 18 | 19 | /** 20 | * Constructs a new TimeSpan instance. 21 | * 22 | * @param hour - Non-negative amount of hours 23 | * @param minute - Number of minutes between 0 and 59 24 | */ 25 | public TimeSpan(int hour, int minute) { 26 | if (hour < MIN_HOUR || minute < MIN_MINUTE) { 27 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.timespan.timeNegative")); 28 | } else if (minute > MAX_MINUTE) { 29 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.timespan.minuteOverUpperBound", MAX_MINUTE)); 30 | } 31 | this.minute = minute; 32 | this.hour = hour; 33 | } 34 | 35 | /** 36 | * Gets the minutes of a TimeSpan. 37 | * 38 | * @return - The minutes. 39 | */ 40 | public int getMinute() { 41 | return minute; 42 | } 43 | 44 | /** 45 | * Gets the hours of a TimeSpan. 46 | * 47 | * @return - The hours. 48 | */ 49 | public int getHour() { 50 | return hour; 51 | } 52 | 53 | /** 54 | * Sums up hours and minutes taking carryover into account. 55 | * 56 | * @param addend - TimeSpan that should be added 57 | * @return The {@link TimeSpan} representing the sum 58 | */ 59 | public TimeSpan add(TimeSpan addend) { 60 | int hourSum = this.hour + addend.getHour(); 61 | int minuteSum = this.minute + addend.getMinute(); 62 | int carry = minuteSum / (MAX_MINUTE + 1); 63 | 64 | return new TimeSpan(hourSum + carry, minuteSum % (MAX_MINUTE + 1)); 65 | } 66 | 67 | /** 68 | * Subtracts hours and minutes taking carryover into account. 69 | * 70 | * @param subtrahend - TimeSpan that should be subtracted 71 | * @return The {@link TimeSpan} representing the difference 72 | * @throws IllegalArgumentException thrown if the subtrahend is greater than the 73 | * minuend 74 | */ 75 | public TimeSpan subtract(TimeSpan subtrahend) throws IllegalArgumentException { 76 | if (this.compareTo(subtrahend) < 0) { 77 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.timespan.subtrahendGreaterThanMinuend")); 78 | } 79 | 80 | int hourDiff = this.hour - subtrahend.getHour(); 81 | int minuteDiff = this.minute - subtrahend.getMinute(); 82 | 83 | return new TimeSpan(hourDiff - ((MAX_MINUTE - minuteDiff) / (MAX_MINUTE + 1)), ((MAX_MINUTE + 1) + minuteDiff) % (MAX_MINUTE + 1)); 84 | } 85 | 86 | /** 87 | * Attempts to interpret a string as a representation of a {@link TimeSpan}. 88 | * 89 | * @param s - the string to be parsed. 90 | * @return A {@link TimeSpan} representing the input string 91 | */ 92 | public static TimeSpan parse(String s) { 93 | if (!s.matches(ResourceHandler.getMessage("locale.timespan.parseRegex"))) { 94 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.timespan.invalidParseInput")); 95 | } 96 | String[] splittedString = s.split(ResourceHandler.getMessage("locale.timespan.separatorHourMinute")); 97 | 98 | int hours; 99 | int minutes; 100 | try { 101 | hours = Integer.parseInt(splittedString[0]); 102 | minutes = Integer.parseInt(splittedString[1]); 103 | } catch (NumberFormatException e) { 104 | throw new IllegalArgumentException(e.getMessage()); 105 | } 106 | 107 | return new TimeSpan(hours, minutes); 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return ResourceHandler.getMessage("locale.timespan.stringFormat", hour, minute); 113 | } 114 | 115 | @Override 116 | public int compareTo(TimeSpan other) { 117 | if (this.hour > other.getHour()) { 118 | return 1; 119 | } else if (this.hour == other.getHour()) { 120 | return Integer.compare(this.minute, other.getMinute()); 121 | } else { 122 | return -1; 123 | } 124 | } 125 | 126 | @Override 127 | public boolean equals(Object other) { 128 | if (!(other instanceof TimeSpan otherTimeSpan)) { 129 | return false; 130 | } 131 | 132 | return this.hour == otherTimeSpan.hour && this.minute == otherTimeSpan.minute; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/data/WorkingArea.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import i18n.ResourceHandler; 5 | 6 | /** 7 | * Represents an area, environment respectively, an {@link Employee employee} 8 | * works in. 9 | */ 10 | public enum WorkingArea { 11 | /** 12 | * GF represents "Grossforschungsbereich" 13 | */ 14 | GF("gf"), 15 | /** 16 | * UB represents "Universitaetsbereich" 17 | */ 18 | UB("ub"); 19 | 20 | private final String stringValue; 21 | 22 | WorkingArea(String stringValue) { 23 | this.stringValue = stringValue; 24 | } 25 | 26 | /** 27 | * Returns the string value of the working area 28 | * 29 | * @return String value 30 | */ 31 | public String getStringValue() { 32 | return stringValue; 33 | } 34 | 35 | /** 36 | * Parses a given {@link String} to a {@link WorkingArea} element. 37 | * 38 | * @param s - the string to be parsed. 39 | * @return A {@link WorkingArea} element parsed from a {@link String}. 40 | */ 41 | public static WorkingArea parse(String s) { 42 | if (s.equalsIgnoreCase(GF.getStringValue())) { 43 | return GF; 44 | } else if (s.equalsIgnoreCase(UB.getStringValue())) { 45 | return UB; 46 | } 47 | throw new IllegalArgumentException(ResourceHandler.getMessage("error.workingarea.invalidParseInput")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/i18n/DateFormatWrapper.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package i18n; 3 | 4 | import java.io.Serial; 5 | import java.text.DateFormat; 6 | import java.text.FieldPosition; 7 | import java.text.Format; 8 | import java.text.ParsePosition; 9 | import java.time.*; 10 | import java.util.Date; 11 | 12 | /** 13 | * Wrapper for the DateFormat class to add support for 14 | * LocalDateTime, LocalDate and LocalTime. 15 | */ 16 | class DateFormatWrapper extends Format { 17 | 18 | @Serial 19 | private static final long serialVersionUID = -7295077661315897883L; 20 | 21 | /** 22 | * Create a new DateFormatWrapper to wrap the given 23 | * DateFormat date format. 24 | * 25 | * @param dateFormat Wrapped DateFormat object 26 | */ 27 | public DateFormatWrapper(DateFormat dateFormat) { 28 | this.innerFormat = dateFormat; 29 | } 30 | 31 | private final DateFormat innerFormat; 32 | 33 | /** 34 | * Convert a LocalDateTime object to a Date object 35 | * using the zone ID from the wrapped date format. 36 | * 37 | * @param localDateTime LocalDateTime object 38 | * @return Date object 39 | */ 40 | private Date convertLocalDateTimeToDate(LocalDateTime localDateTime) { 41 | ZoneId zoneId = innerFormat.getTimeZone().toZoneId(); 42 | Instant instant = localDateTime.atZone(zoneId).toInstant(); 43 | 44 | return Date.from(instant); 45 | } 46 | 47 | @Override 48 | public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { 49 | if (obj instanceof LocalDateTime) { 50 | obj = convertLocalDateTimeToDate((LocalDateTime) obj); 51 | } else if (obj instanceof LocalDate) { 52 | obj = convertLocalDateTimeToDate(((LocalDate) obj).atStartOfDay()); 53 | } else if (obj instanceof LocalTime) { 54 | obj = convertLocalDateTimeToDate(((LocalTime) obj).atDate(LocalDate.now())); 55 | } 56 | 57 | return innerFormat.format(obj, toAppendTo, pos); 58 | } 59 | 60 | @Override 61 | public Object parseObject(String source, ParsePosition pos) { 62 | return innerFormat.parseObject(source, pos); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/i18n/ResourceHandler.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package i18n; 3 | 4 | import java.text.DateFormat; 5 | import java.text.Format; 6 | import java.text.MessageFormat; 7 | import java.util.Locale; 8 | import java.util.ResourceBundle; 9 | 10 | /** 11 | * Static class providing localized messages from the i18n message bundles. 12 | */ 13 | public class ResourceHandler { 14 | 15 | private static final String DEFAULT_MESSAGE_BUNDLE_PATH = "i18n/MessageBundle"; 16 | 17 | protected static class ResourceHandlerInstance { 18 | 19 | protected ResourceHandlerInstance(String messageBundlePath) { 20 | this.messageBundlePath = messageBundlePath; 21 | 22 | locale = Locale.getDefault(); 23 | 24 | loadResourceBundle(); 25 | } 26 | 27 | private final String messageBundlePath; 28 | 29 | private Locale locale; 30 | private ResourceBundle resourceBundle; 31 | 32 | protected Locale getLocale() { 33 | return locale; 34 | } 35 | 36 | protected void setLocale(final Locale locale) { 37 | this.locale = locale; 38 | 39 | loadResourceBundle(); 40 | } 41 | 42 | protected String getMessage(final String key, final Object... args) { 43 | String message = resourceBundle.getString(key); 44 | 45 | MessageFormat format = new MessageFormat(""); 46 | format.setLocale(locale); 47 | format.applyPattern(message); 48 | replaceUnsupportedFormats(format); 49 | 50 | return format.format(args); 51 | } 52 | 53 | private void loadResourceBundle() { 54 | resourceBundle = ResourceBundle.getBundle(messageBundlePath, locale); 55 | } 56 | 57 | private static void replaceUnsupportedFormats(MessageFormat format) { 58 | Format[] subformats = format.getFormats(); 59 | 60 | for (int i = 0; i < subformats.length; i++) { 61 | if (subformats[i] instanceof DateFormat) { 62 | format.setFormat(i, new DateFormatWrapper((DateFormat) subformats[i])); 63 | } 64 | } 65 | } 66 | 67 | } 68 | 69 | private static final ResourceHandlerInstance instance = new ResourceHandlerInstance(DEFAULT_MESSAGE_BUNDLE_PATH); 70 | 71 | /** 72 | * Get the currently used locale. 73 | * 74 | * @return Currently used locale 75 | */ 76 | public static Locale getLocale() { 77 | return instance.getLocale(); 78 | } 79 | 80 | /** 81 | * Set the used locale. 82 | * The set locale will be the first choice for the i18n message bundle loaded. 83 | * 84 | * @param locale Locale to use for the loading of i18n message bundles 85 | */ 86 | public static void setLocale(final Locale locale) { 87 | instance.setLocale(locale); 88 | } 89 | 90 | /** 91 | * Get a message string from the i18n message bundles. 92 | * The key will be searched in the i18n message bundle specified by the 93 | * currently used locale and all parent message bundles ("fallback"). 94 | * The objects provided in args will be inserted in the loaded 95 | * message string with the format specified in the message string. 96 | * 97 | * @param key Message key 98 | * @param args Objects to insert in the loaded message string 99 | * 100 | * @return Loaded message string containing a string representation of the 101 | * provided objects 102 | */ 103 | public static String getMessage(final String key, final Object... args) { 104 | return instance.getMessage(key, args); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/FileController.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package io; 3 | 4 | import java.io.*; 5 | import java.net.URL; 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | 10 | /** 11 | * The FileController class provides functionality for file handling. 12 | */ 13 | public class FileController { 14 | 15 | private static final Charset CHARSET = StandardCharsets.UTF_8; 16 | 17 | private FileController() { 18 | } 19 | 20 | /** 21 | * This method returns a {@link String} read from an {@link InputStream}. 22 | * 23 | * @param inStream - The stream the {@link String} is read from. 24 | * @return a {@link String} read from the {@link InputStream} 25 | * @throws IOException if an I/O error occurs. 26 | */ 27 | public static String readInputStreamToString(InputStream inStream) throws IOException { 28 | StringBuilder stringBuilder = new StringBuilder(); 29 | String ls = System.getProperty("line.separator", "\n"); 30 | 31 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inStream, CHARSET))) { 32 | String line; 33 | while ((line = reader.readLine()) != null) { 34 | stringBuilder.append(line); 35 | stringBuilder.append(ls); 36 | } 37 | 38 | return stringBuilder.toString(); 39 | } 40 | } 41 | 42 | /** 43 | * This method returns a {@link String} read from a {@link File}. 44 | * 45 | * @param file - The file the {@link String} is read from. 46 | * @return a {@link String} read from the {@link File} 47 | * @throws IOException if an I/O error occurs. 48 | */ 49 | public static String readFileToString(File file) throws IOException { 50 | return readInputStreamToString(new FileInputStream(file)); 51 | } 52 | 53 | /** 54 | * This method returns a {@link String} read from an {@link URL}. 55 | * 56 | * @param url - The url the {@link String} is read from. 57 | * @return a {@link String} read from the {@link URL} 58 | * @throws IOException if an I/O error occurs. 59 | */ 60 | public static String readURLToString(URL url) throws IOException { 61 | return readInputStreamToString(url.openStream()); 62 | } 63 | 64 | /** 65 | * This method saves a {@link String} to a {@link File}. 66 | * 67 | * @param content - The {@link String} to be saved. 68 | * @param file - The {@link File} to save the content to. 69 | * @throws IOException if an I/O error occurs. 70 | */ 71 | public static void saveStringToFile(String content, File file) throws IOException { 72 | try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), CHARSET)) { 73 | writer.write(content); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/IGenerator.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package io; 3 | 4 | import data.Entry; 5 | import data.TimeSheet; 6 | 7 | import javax.swing.filechooser.FileNameExtensionFilter; 8 | 9 | /** 10 | * A generator is able to generate a document filled with values coming from a 11 | * {@link TimeSheet} and the associated {@link Entry Entries}. 12 | */ 13 | public interface IGenerator { 14 | 15 | /** 16 | * Generates a document with information from a {@link TimeSheet} and the 17 | * associated {@link Entry entries}. 18 | * 19 | * @return The generated document. 20 | */ 21 | String generate(); 22 | 23 | /** 24 | * Returns the {@link FileNameExtensionFilter} associated with the generated 25 | * file. This can be used if the {@link String} given by {@link #generate()} 26 | * should be saved. 27 | * 28 | * @return The {@link FileNameExtensionFilter} associated to the generated file. 29 | */ 30 | FileNameExtensionFilter getFileNameExtensionFilter(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/main/Main.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package main; 3 | 4 | import checker.*; 5 | import data.TimeSheet; 6 | import i18n.ResourceHandler; 7 | import io.FileController; 8 | import io.IGenerator; 9 | import io.LatexGenerator; 10 | import main.UserInput.Request; 11 | import parser.ParseException; 12 | import parser.Parser; 13 | import ui.UserInterface; 14 | 15 | import javax.swing.*; 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.util.Optional; 19 | 20 | /** 21 | * Main class of the application containing the main method as entry point for 22 | * the application 23 | */ 24 | public class Main { 25 | 26 | /** 27 | * Main entry point for the application 28 | * 29 | * @param args command line arguments that are passed to the apache cli library 30 | */ 31 | public static void main(String[] args) { 32 | 33 | // If no arguments or a valid file was given, run UI instead 34 | if (args.length == 0 || (args.length == 1 && new File(args[0]).exists())) { 35 | UserInterface.main(args); 36 | return; 37 | } 38 | 39 | // Initialize and parse user input 40 | UserInput userInput = new UserInput(args); 41 | Request request; 42 | try { 43 | request = userInput.parse(); 44 | } catch (org.apache.commons.cli.ParseException e) { 45 | System.out.println(e.getMessage()); 46 | System.exit(1); 47 | return; 48 | } 49 | 50 | // If requested: Print help and return 51 | if (request == Request.HELP) { 52 | userInput.printHelp(); 53 | return; 54 | } 55 | // If requested: Print version and return 56 | if (request == Request.VERSION) { 57 | userInput.printVersion(); 58 | return; 59 | } 60 | 61 | // Get content of input files 62 | String global; 63 | String month; 64 | try { 65 | global = FileController.readFileToString(userInput.getFile(UserInputFile.JSON_GLOBAL)); 66 | month = FileController.readFileToString(userInput.getFile(UserInputFile.JSON_MONTH)); 67 | } catch (IOException e) { 68 | System.out.println(e.getMessage()); 69 | System.exit(1); 70 | return; 71 | } 72 | 73 | // Initialize time sheet 74 | TimeSheet timeSheet; 75 | try { 76 | timeSheet = Parser.parseTimeSheetJson(global, month); 77 | } catch (ParseException e) { 78 | System.out.println(e.getMessage()); 79 | System.exit(1); 80 | return; 81 | } 82 | 83 | // Check time sheet 84 | IChecker checker = new MiLoGChecker(timeSheet); 85 | CheckerReturn checkerReturn; 86 | try { 87 | checkerReturn = checker.check(); 88 | } catch (CheckerException e) { // exception does not mean that the time sheet is invalid, but that the process 89 | // of checking failed 90 | System.out.println(e.getMessage()); 91 | System.exit(1); 92 | return; 93 | } 94 | // Print all errors in case the time sheet is invalid 95 | if (checkerReturn == CheckerReturn.INVALID) { 96 | handleInvalidTimesheet(checker, userInput); 97 | return; 98 | } 99 | 100 | // Generate and save output file 101 | ClassLoader classLoader = Main.class.getClassLoader(); 102 | try { 103 | String latexTemplate = FileController.readInputStreamToString(classLoader.getResourceAsStream("MiLoG_Template.tex")); 104 | IGenerator generator = new LatexGenerator(timeSheet, latexTemplate); 105 | FileController.saveStringToFile(generator.generate(), userInput.getFile(UserInputFile.OUTPUT)); 106 | } catch (IOException e) { 107 | System.out.println(e.getMessage()); 108 | System.exit(1); 109 | } 110 | } 111 | 112 | private static void handleInvalidTimesheet(IChecker checker, UserInput userInput) { 113 | for (CheckerError error : checker.getErrors()) { 114 | System.out.println(error.getErrorMessage()); 115 | } 116 | 117 | if (userInput.isGui()) { 118 | StringBuilder errorList = new StringBuilder(); 119 | for (CheckerError error : checker.getErrors()) { 120 | errorList.append(error.getErrorMessage()).append(System.lineSeparator()); 121 | } 122 | 123 | JOptionPane.showMessageDialog(null, errorList.toString(), ResourceHandler.getMessage("gui.errorListWindowTitle"), JOptionPane.ERROR_MESSAGE); 124 | } 125 | } 126 | 127 | /** 128 | * Addendum to the timesheet generator. This method only validated the contents 129 | * of a given timesheet file. If the given file, for any reason, is not a valid 130 | * timesheet, this method returns an optional containing the error message. If 131 | * it is, this method will return an empty optional. 132 | * 133 | * @param globalFile The global.json file. 134 | * @param monthFile The month.json file. 135 | * @return An optional of the error message. 136 | */ 137 | public static Optional validateTimesheet(File globalFile, File monthFile) { 138 | if (globalFile == null || monthFile == null) 139 | return Optional.of("The global or month file were null. Try saving."); 140 | String globalStr; 141 | String monthStr; 142 | try { 143 | globalStr = FileController.readFileToString(globalFile); 144 | monthStr = FileController.readFileToString(monthFile); 145 | } catch (IOException e) { 146 | return Optional.of(e.getMessage()); 147 | } 148 | 149 | // Validation code from above. 150 | 151 | // Initialize time sheet 152 | TimeSheet timeSheet; 153 | try { 154 | timeSheet = Parser.parseTimeSheetJson(globalStr, monthStr); 155 | } catch (ParseException e) { 156 | return Optional.of(e.getMessage()); 157 | } 158 | 159 | // Check time sheet 160 | IChecker checker = new MiLoGChecker(timeSheet); 161 | CheckerReturn checkerReturn; 162 | try { 163 | checkerReturn = checker.check(); 164 | } catch (CheckerException e) { 165 | return Optional.of(e.getMessage()); 166 | } 167 | 168 | if (checkerReturn == CheckerReturn.INVALID) { 169 | StringBuilder errorList = new StringBuilder(); 170 | for (CheckerError error : checker.getErrors()) { 171 | errorList.append(error.getErrorMessage()).append(System.lineSeparator()); 172 | } 173 | return Optional.of(errorList.toString()); 174 | } 175 | 176 | return Optional.empty(); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/main/UserInputFile.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package main; 3 | 4 | import i18n.ResourceHandler; 5 | 6 | import javax.swing.filechooser.FileNameExtensionFilter; 7 | 8 | /** 9 | * Represents a type of file that is specified by the user 10 | */ 11 | public enum UserInputFile { 12 | 13 | /** 14 | * [INPUT] Global JSON file containing information that are the same on all time 15 | * sheets 16 | */ 17 | JSON_GLOBAL("file.json.global.description", "file.json.description", "file.json.extension", FileOperation.OPEN), 18 | /** 19 | * [INPUT] JSON file containing information that is specific to one time sheet 20 | * (= one month) 21 | */ 22 | JSON_MONTH("file.json.month.description", "file.json.description", "file.json.extension", FileOperation.OPEN), 23 | /** 24 | * [OUTPUT] TeX file in which the generated time sheet is written. 25 | */ 26 | OUTPUT("file.tex.output.description", "file.tex.description", "file.tex.extension", FileOperation.SAVE); 27 | 28 | private final String dialogTitel; 29 | private final FileNameExtensionFilter fileFilter; 30 | private final FileOperation operation; 31 | 32 | /** 33 | * Create a new type of user input file 34 | * 35 | * @param operation Operation associated with this user file type (OPEN or SAVE) 36 | */ 37 | UserInputFile(String dialogTitleKey, String fileDescriptionKey, String fileExtensionKey, FileOperation operation) { 38 | this.dialogTitel = ResourceHandler.getMessage(dialogTitleKey); 39 | this.fileFilter = new FileNameExtensionFilter(ResourceHandler.getMessage(fileDescriptionKey), ResourceHandler.getMessage(fileExtensionKey)); 40 | this.operation = operation; 41 | } 42 | 43 | /** 44 | * Get the title of the dialog that is used to request the file from the user 45 | * 46 | * @return Dialog title 47 | */ 48 | public String getDialogTitel() { 49 | return this.dialogTitel; 50 | } 51 | 52 | /** 53 | * Get the filter for allowed file extensions 54 | * 55 | * @return File extension filter 56 | */ 57 | public FileNameExtensionFilter getFileFilter() { 58 | return this.fileFilter; 59 | } 60 | 61 | /** 62 | * Get the operation associated with this user file type 63 | * 64 | * @return File operation (OPEN or SAVE) 65 | */ 66 | public FileOperation getFileOperation() { 67 | return this.operation; 68 | } 69 | 70 | /** 71 | * Represents the operation that is executed with a file type (either OPEN or 72 | * SAVE) 73 | */ 74 | public enum FileOperation { 75 | OPEN, SAVE 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/main/UserInputOption.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package main; 3 | 4 | import i18n.ResourceHandler; 5 | import org.apache.commons.cli.Option; 6 | import org.apache.commons.cli.Options; 7 | 8 | /** 9 | * Represents the input option the user has on the command line 10 | */ 11 | public enum UserInputOption { 12 | 13 | /** 14 | * Print the command line help 15 | */ 16 | HELP(Option.builder("h").longOpt("help").desc(ResourceHandler.getMessage("command.input.help.description")).hasArg(false).build()), 17 | /** 18 | * Print the version of the application 19 | */ 20 | VERSION(Option.builder("v").longOpt("version").desc(ResourceHandler.getMessage("command.input.version.description")).hasArg(false).build()), 21 | /** 22 | * Show the GUI for choosing the files 23 | */ 24 | GUI(Option.builder("g").longOpt("gui").desc(ResourceHandler.getMessage("command.input.gui.description")).hasArg(false).build()), 25 | /** 26 | * Specify the files in the arguments of this command 27 | */ 28 | FILE(Option.builder("f").longOpt("file").desc(ResourceHandler.getMessage("command.input.file.description")).numberOfArgs(3) 29 | .argName(ResourceHandler.getMessage("command.input.file.arguments")).build()); 30 | 31 | private final Option option; 32 | 33 | /** 34 | * Create a user input option 35 | * 36 | * @param option Apache CLI option for the user input option 37 | */ 38 | UserInputOption(Option option) { 39 | this.option = option; 40 | } 41 | 42 | /** 43 | * Get the Apache CLI option of this user input option 44 | * 45 | * @return Apache CLI option 46 | */ 47 | public Option getOption() { 48 | return this.option; 49 | } 50 | 51 | /** 52 | * Get the Apache CLI options 53 | * 54 | * @return Apache CLI options 55 | */ 56 | public static Options getOptions() { 57 | Options options = new Options(); 58 | for (UserInputOption uio : UserInputOption.values()) { 59 | options.addOption(uio.getOption()); 60 | } 61 | return options; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/parser/IGlobalParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import data.Employee; 5 | import data.Profession; 6 | 7 | /** 8 | * A global parser provides the functionality of parsing an {@link Employee} and 9 | * {@link Profession} out of given data. 10 | */ 11 | public interface IGlobalParser { 12 | 13 | /** 14 | * Returns an {@link Employee} that got parsed from data. 15 | * 16 | * @return An employee. 17 | * @throws ParseException if an error occurs while parsing. 18 | */ 19 | Employee getEmployee() throws ParseException; 20 | 21 | /** 22 | * Returns a {@link Profession} that got parsed from data. 23 | * 24 | * @return A profession. 25 | * @throws ParseException if an error occurs while parsing. 26 | */ 27 | Profession getProfession() throws ParseException; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/parser/IHolidayParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import checker.holiday.Holiday; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * A holiday parser provides the functionality of parsing {@link Holiday 10 | * holidays} from given data. 11 | */ 12 | public interface IHolidayParser { 13 | 14 | /** 15 | * Returns a {@link Collection} of {@link Holiday holidays} parsed from data. 16 | * 17 | * @return A collection of holidays. 18 | * @throws ParseException if an error occurs while parsing. 19 | */ 20 | Collection getHolidays() throws ParseException; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/parser/IMonthParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import data.Entry; 5 | import data.TimeSheet; 6 | import data.TimeSpan; 7 | 8 | import java.time.YearMonth; 9 | 10 | /** 11 | * A month parser provides the functionality of parsing month-related 12 | * {@link TimeSheet} information from given data. 13 | */ 14 | public interface IMonthParser { 15 | 16 | /** 17 | * Returns a {@link YearMonth} parsed from data. 18 | * 19 | * @return A YearMonth. 20 | * @throws ParseException if an error occurs while parsing. 21 | */ 22 | YearMonth getYearMonth() throws ParseException; 23 | 24 | /** 25 | * Returns an array of {@link Entry entries} parsed from data. 26 | * 27 | * @return An array of entries. 28 | * @throws ParseException if an error occurs while parsing. 29 | */ 30 | Entry[] getEntries() throws ParseException; 31 | 32 | /** 33 | * Returns {@link TimeSpan} representing the transfered time from the successor 34 | * month parsed from data. 35 | * 36 | * @return A transfered time {@link TimeSpan}. 37 | * @throws ParseException if an error occurs while parsing. 38 | */ 39 | TimeSpan getSuccTransfer() throws ParseException; 40 | 41 | /** 42 | * Returns {@link TimeSpan} representing the transfered time from the 43 | * predecessor month parsed from data. 44 | * 45 | * @return A transferred time {@link TimeSpan}. 46 | * @throws ParseException if an error occurs while parsing. 47 | */ 48 | TimeSpan getPredTransfer() throws ParseException; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/parser/ParseException.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import java.io.Serial; 5 | 6 | /** 7 | * Exception thrown by parser classes to signal that an error occurred while 8 | * parsing. 9 | */ 10 | public class ParseException extends Exception { 11 | 12 | @Serial 13 | private static final long serialVersionUID = -644519857973827281L; 14 | 15 | /** 16 | * Constructs a new {@link ParseException}. 17 | * 18 | * @param error - message of the error that occurred. 19 | */ 20 | public ParseException(String error) { 21 | super(error); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/parser/Parser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import data.*; 5 | import parser.json.JsonGlobalParser; 6 | import parser.json.JsonMonthParser; 7 | 8 | import java.time.YearMonth; 9 | 10 | /** 11 | * A Parser provides the functionality to construct a {@link TimeSheet} with the 12 | * data coming from different types of files getting parsed by 13 | * {@link IGlobalParser} and {@link IMonthParser} instances. 14 | */ 15 | public class Parser { 16 | 17 | private Parser() { 18 | } 19 | 20 | /** 21 | * Returns a new {@link TimeSheet} constructed out of data coming from two json 22 | * strings. 23 | * 24 | * @param globalJson - json to get global data from. 25 | * @param monthJson - json to get month data from. 26 | * @return A new {@link TimeSheet} instances. 27 | * @throws ParseException if an error occurs while parsing the json strings. 28 | */ 29 | public static TimeSheet parseTimeSheetJson(String globalJson, String monthJson) throws ParseException { 30 | IGlobalParser globalParser = new JsonGlobalParser(globalJson); 31 | 32 | Employee employee = globalParser.getEmployee(); 33 | Profession profession = globalParser.getProfession(); 34 | 35 | IMonthParser monthParser = new JsonMonthParser(monthJson); 36 | 37 | YearMonth yearMonth = monthParser.getYearMonth(); 38 | Entry[] entries = monthParser.getEntries(); 39 | TimeSpan succTransfer = monthParser.getSuccTransfer(); 40 | TimeSpan predTransfer = monthParser.getPredTransfer(); 41 | 42 | return new TimeSheet(employee, profession, yearMonth, entries, succTransfer, predTransfer); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/parser/json/GlobalJson.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import data.TimeSpan; 8 | import data.WorkingArea; 9 | 10 | @JsonIgnoreProperties({ "$schema" }) 11 | class GlobalJson { 12 | 13 | private final String name; 14 | private final int staffId; 15 | private final String department; 16 | private final TimeSpan workingTime; 17 | private final double wage; 18 | private final WorkingArea workingArea; 19 | 20 | @JsonCreator 21 | GlobalJson(@JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "staffId", required = true) int staffId, 22 | @JsonProperty(value = "department", required = true) String department, @JsonProperty(value = "workingTime", required = true) String workingTime, 23 | @JsonProperty(value = "wage", required = true) double wage, @JsonProperty(value = "workingArea", required = true) String workingArea) { 24 | this.name = name; 25 | this.staffId = staffId; 26 | this.department = department; 27 | this.workingTime = TimeSpan.parse(workingTime); 28 | this.wage = wage; 29 | this.workingArea = WorkingArea.parse(workingArea); 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public int getStaffId() { 37 | return staffId; 38 | } 39 | 40 | public String getDepartment() { 41 | return department; 42 | } 43 | 44 | public TimeSpan getWorkingTime() { 45 | return workingTime; 46 | } 47 | 48 | public double getWage() { 49 | return wage; 50 | } 51 | 52 | public WorkingArea getWorkingArea() { 53 | return workingArea; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/parser/json/HolidayJson.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import java.time.LocalDate; 8 | import java.time.format.DateTimeFormatter; 9 | import java.time.format.DateTimeFormatterBuilder; 10 | 11 | class HolidayJson { 12 | 13 | private final static String DATE_PATTERN = "yyyy-MM-dd"; 14 | 15 | private final LocalDate date; 16 | private final String note; 17 | 18 | @JsonCreator 19 | HolidayJson(@JsonProperty(value = "datum", required = true) String date, @JsonProperty(value = "hinweis") String note) { 20 | DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern(DATE_PATTERN).toFormatter(); 21 | 22 | this.date = LocalDate.parse(date, dateFormatter); 23 | this.note = note; 24 | } 25 | 26 | public LocalDate getDate() { 27 | return date; 28 | } 29 | 30 | public String getNote() { 31 | return note; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/parser/json/HolidayMapJson.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonAnySetter; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | class HolidayMapJson { 10 | 11 | private final Map holidays; 12 | 13 | HolidayMapJson() { 14 | this.holidays = new HashMap<>(); 15 | } 16 | 17 | @JsonAnySetter 18 | public void addHoliday(String key, HolidayJson value) { 19 | holidays.put(key, value); 20 | } 21 | 22 | public Map getHolidays() { 23 | return holidays; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/parser/json/JsonGlobalParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.json.JsonMapper; 8 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 11 | import data.Employee; 12 | import data.Profession; 13 | import parser.IGlobalParser; 14 | import parser.ParseException; 15 | 16 | /** 17 | * A JsonGlobalParser provides the functionality to parse the elements specified 18 | * by {@link IGlobalParser} from a json string. 19 | */ 20 | public class JsonGlobalParser implements IGlobalParser { 21 | 22 | private final String json; 23 | 24 | private GlobalJson globalJson; // caching 25 | 26 | /** 27 | * Constructs a new {@link JsonGlobalParser} instance. 28 | * 29 | * @param json - to parse the data from. 30 | */ 31 | public JsonGlobalParser(String json) { 32 | this.json = json; 33 | } 34 | 35 | private GlobalJson parseJson() throws JsonProcessingException { 36 | if (globalJson == null) { 37 | ObjectMapper mapper = JsonMapper.builder().addModule(new ParameterNamesModule()).addModule(new Jdk8Module()).addModule(new JavaTimeModule()) 38 | .build(); 39 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); 40 | globalJson = mapper.readValue(json, GlobalJson.class); 41 | } 42 | 43 | return globalJson; 44 | } 45 | 46 | @Override 47 | public Employee getEmployee() throws ParseException { 48 | Employee employee; 49 | try { 50 | GlobalJson global = parseJson(); 51 | 52 | employee = new Employee(global.getName(), global.getStaffId()); 53 | } catch (JsonProcessingException e) { 54 | throw new ParseException(e.getMessage()); 55 | } 56 | 57 | return employee; 58 | } 59 | 60 | @Override 61 | public Profession getProfession() throws ParseException { 62 | Profession profession; 63 | try { 64 | GlobalJson global = parseJson(); 65 | 66 | profession = new Profession(global.getDepartment(), global.getWorkingArea(), global.getWorkingTime(), global.getWage()); 67 | } catch (JsonProcessingException e) { 68 | throw new ParseException(e.getMessage()); 69 | } 70 | 71 | return profession; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/parser/json/JsonHolidayParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import checker.holiday.Holiday; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.json.JsonMapper; 8 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 11 | import parser.IHolidayParser; 12 | import parser.ParseException; 13 | 14 | import java.util.Collection; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * A JsonHolidayParser provides the functionality to parse the elements 19 | * specified by {@link IHolidayParser} from a json string. 20 | */ 21 | public class JsonHolidayParser implements IHolidayParser { 22 | 23 | private static final String SCHOOL_HOLIDAY_NOTE = "schulfrei"; 24 | private final String json; 25 | 26 | private HolidayMapJson holidayMap; // caching 27 | 28 | /** 29 | * Constructs a new {@link JsonHolidayParser} instance. 30 | * 31 | * @param json - to parse the data from. 32 | */ 33 | public JsonHolidayParser(String json) { 34 | this.json = json; 35 | } 36 | 37 | private HolidayMapJson parseJson() throws JsonProcessingException { 38 | if (holidayMap == null) { 39 | ObjectMapper mapper = JsonMapper.builder().addModule(new ParameterNamesModule()).addModule(new Jdk8Module()).addModule(new JavaTimeModule()) 40 | .build(); 41 | holidayMap = mapper.readValue(json, HolidayMapJson.class); 42 | } 43 | 44 | return holidayMap; 45 | } 46 | 47 | @Override 48 | public Collection getHolidays() throws ParseException { 49 | try { 50 | HolidayMapJson holidayMap = parseJson(); 51 | 52 | return holidayMap.getHolidays().entrySet().stream() 53 | .filter(e -> e.getValue().getNote() == null || !e.getValue().getNote().contains(SCHOOL_HOLIDAY_NOTE)) 54 | .map(e -> new Holiday(e.getValue().getDate(), e.getKey())).collect(Collectors.toList()); 55 | } catch (JsonProcessingException e) { 56 | throw new ParseException(e.getMessage()); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/parser/json/JsonMonthParser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.json.JsonMapper; 8 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 9 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 10 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 11 | import data.Entry; 12 | import data.TimeSpan; 13 | import parser.IMonthParser; 14 | import parser.ParseException; 15 | 16 | import java.time.LocalDate; 17 | import java.time.YearMonth; 18 | import java.util.List; 19 | 20 | /** 21 | * A JsonMonthParser provides the functionality to parse the elements specified 22 | * by {@link IMonthParser} from a json string. 23 | */ 24 | public class JsonMonthParser implements IMonthParser { 25 | 26 | private final String json; 27 | 28 | private MonthJson monthJson; // caching 29 | 30 | /** 31 | * Constructs a new {@link JsonMonthParser} instance. 32 | * 33 | * @param json - to parse the data from. 34 | */ 35 | public JsonMonthParser(String json) { 36 | this.json = json; 37 | } 38 | 39 | private MonthJson parse() throws JsonProcessingException { 40 | if (monthJson == null) { 41 | ObjectMapper mapper = JsonMapper.builder().addModule(new ParameterNamesModule()).addModule(new Jdk8Module()).addModule(new JavaTimeModule()) 42 | .build(); 43 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); 44 | monthJson = mapper.readValue(json, MonthJson.class); 45 | } 46 | 47 | return monthJson; 48 | } 49 | 50 | @Override 51 | public YearMonth getYearMonth() throws ParseException { 52 | try { 53 | return parse().getYearMonth(); 54 | } catch (JsonProcessingException e) { 55 | throw new ParseException(e.getMessage()); 56 | } 57 | } 58 | 59 | @Override 60 | public Entry[] getEntries() throws ParseException { 61 | List entries; 62 | try { 63 | MonthJson month = parse(); 64 | 65 | // catching the ParseException is necessary, because Java complains about a 66 | // unhandled exception otherwise 67 | // declaring the exception is not possible in a lambda function 68 | // a workaround is to encapsulate the exception in a RuntimeException, which 69 | // doesn't have to be declared 70 | // since we expect the RuntimeException caught outside the lambda function to 71 | // include the actual exception as the cause, 72 | // we need to encapsulate RuntimeExceptions thrown in parseEntry(..) as well 73 | // (note that IllegalArgumentException is a RuntimeException as well) 74 | entries = month.getEntries().stream().map(entry -> { 75 | try { 76 | return parseEntry(entry); 77 | } catch (ParseException | RuntimeException e) { 78 | throw new RuntimeException(e); 79 | } 80 | }).toList(); 81 | } catch (RuntimeException e) { 82 | throw new ParseException(e.getCause().getMessage()); 83 | } catch (JsonProcessingException e) { 84 | throw new ParseException(e.getMessage()); 85 | } 86 | 87 | return entries.toArray(new Entry[0]); 88 | } 89 | 90 | @Override 91 | public TimeSpan getSuccTransfer() throws ParseException { 92 | try { 93 | return parse().getSuccTransfer(); 94 | } catch (JsonProcessingException e) { 95 | throw new ParseException(e.getMessage()); 96 | } 97 | } 98 | 99 | @Override 100 | public TimeSpan getPredTransfer() throws ParseException { 101 | try { 102 | return parse().getPredTransfer(); 103 | } catch (JsonProcessingException e) { 104 | throw new ParseException(e.getMessage()); 105 | } 106 | } 107 | 108 | /** 109 | * Parses an {@link Entry} from an {@link MonthEntryJson}. 110 | * 111 | * @param entry - to parse {@link Entry} from 112 | * @return The entry parsed from the {@link MonthEntryJson}. 113 | * @throws ParseException if an error occurs while fetching the 114 | * {@link YearMonth}. 115 | */ 116 | private Entry parseEntry(MonthEntryJson entry) throws ParseException { 117 | // LocalDate construction 118 | YearMonth yearMonth = getYearMonth(); 119 | LocalDate date = LocalDate.of(yearMonth.getYear(), yearMonth.getMonth(), entry.getDay()); 120 | 121 | return new Entry(entry.getAction(), date, entry.getStart(), entry.getEnd(), entry.getPause(), entry.getVacation()); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/parser/json/MonthEntryJson.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import data.TimeSpan; 7 | 8 | class MonthEntryJson { 9 | 10 | private final String action; 11 | private final int day; 12 | private final TimeSpan start; 13 | private final TimeSpan end; 14 | private TimeSpan pause; 15 | private boolean vacation; 16 | 17 | @JsonCreator 18 | MonthEntryJson(@JsonProperty(value = "action", required = true) String action, @JsonProperty(value = "day", required = true) int day, 19 | @JsonProperty(value = "start", required = true) String start, @JsonProperty(value = "end", required = true) String end) { 20 | this.action = action; 21 | this.day = day; 22 | this.start = TimeSpan.parse(start); 23 | this.end = TimeSpan.parse(end); 24 | this.pause = new TimeSpan(0, 0); // default 25 | this.vacation = false; // default 26 | } 27 | 28 | public String getAction() { 29 | return action; 30 | } 31 | 32 | public int getDay() { 33 | return day; 34 | } 35 | 36 | public TimeSpan getStart() { 37 | return start; 38 | } 39 | 40 | public TimeSpan getEnd() { 41 | return end; 42 | } 43 | 44 | public TimeSpan getPause() { 45 | return pause; 46 | } 47 | 48 | @JsonProperty("pause") 49 | public void setPause(String pause) { 50 | this.pause = TimeSpan.parse(pause); 51 | } 52 | 53 | public boolean getVacation() { 54 | return vacation; 55 | } 56 | 57 | @JsonProperty("vacation") 58 | public void setVacation(boolean vacation) { 59 | this.vacation = vacation; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/parser/json/MonthJson.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import data.TimeSpan; 8 | 9 | import java.time.YearMonth; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @JsonIgnoreProperties({ "$schema" }) 14 | class MonthJson { 15 | 16 | private final YearMonth yearMonth; 17 | private TimeSpan predTransfer; 18 | private TimeSpan succTransfer; 19 | private final List entries; 20 | 21 | @JsonCreator 22 | MonthJson(@JsonProperty(value = "year", required = true) int year, @JsonProperty(value = "month", required = true) int month, 23 | @JsonProperty(value = "entries", required = true) List entries) { 24 | this.yearMonth = YearMonth.of(year, month); 25 | this.predTransfer = new TimeSpan(0, 0); // default 26 | this.succTransfer = new TimeSpan(0, 0); // default 27 | this.entries = new ArrayList<>(entries); 28 | } 29 | 30 | public YearMonth getYearMonth() { 31 | return yearMonth; 32 | } 33 | 34 | public TimeSpan getPredTransfer() { 35 | return predTransfer; 36 | } 37 | 38 | @JsonProperty("pred_transfer") 39 | public void setPredTransfer(String predTransfer) { 40 | this.predTransfer = TimeSpan.parse(predTransfer); 41 | } 42 | 43 | public TimeSpan getSuccTransfer() { 44 | return succTransfer; 45 | } 46 | 47 | @JsonProperty("succ_transfer") 48 | public void setSuccTransfer(String succTransfer) { 49 | this.succTransfer = TimeSpan.parse(succTransfer); 50 | } 51 | 52 | public List getEntries() { 53 | return entries; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/ui/ActionBar.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui; 3 | 4 | import ui.export.FileExporter; 5 | import ui.json.JSONHandler; 6 | import ui.json.UISettings; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | 11 | public class ActionBar extends JPanel { 12 | 13 | private static final String HOURS_FORMAT = "Total Time: %s/%s "; 14 | 15 | private final transient UserInterface parentUi; 16 | 17 | private final JLabel hoursWorkedLabel; 18 | private final Font fontNormal; 19 | private final Font fontBold; 20 | 21 | public ActionBar(UserInterface parentUi) { 22 | this.parentUi = parentUi; 23 | this.setPreferredSize(new Dimension(this.parentUi.getWidth(), 70)); 24 | this.setLayout(new BorderLayout()); 25 | 26 | JPanel buttonPanel = new JPanel(); 27 | 28 | JButton addButton = new JButton("+"); 29 | addButton.setPreferredSize(new Dimension(50, 50)); 30 | buttonPanel.add(addButton); 31 | 32 | JButton duplicateButton = new JButton("Duplicate"); 33 | duplicateButton.setPreferredSize(new Dimension(100, 50)); 34 | buttonPanel.add(duplicateButton); 35 | 36 | JButton editButton = new JButton("Edit"); 37 | editButton.setPreferredSize(new Dimension(70, 50)); 38 | buttonPanel.add(editButton); 39 | 40 | JButton removeButton = new JButton("Remove"); 41 | removeButton.setPreferredSize(new Dimension(100, 50)); 42 | buttonPanel.add(removeButton); 43 | 44 | JButton compileButton = new JButton("Compile to Tex"); 45 | compileButton.setPreferredSize(new Dimension(125, 50)); 46 | buttonPanel.add(compileButton); 47 | 48 | JButton printButton = new JButton("Print to PDF"); 49 | printButton.setPreferredSize(new Dimension(105, 50)); 50 | buttonPanel.add(printButton); 51 | 52 | this.add(buttonPanel, BorderLayout.WEST); 53 | 54 | addButton.addActionListener(e -> { 55 | if (this.parentUi.isSpaceForNewEntry()) { 56 | DialogHelper.showEntryDialog(this.parentUi, "Add Entry"); 57 | } else { 58 | ErrorHandler.showError("Entry limit reached", "You have reached the maximum of %d entries".formatted(UserInterface.MAX_ENTRIES)); 59 | } 60 | }); 61 | duplicateButton.addActionListener(l -> this.parentUi.duplicateSelectedListEntry()); 62 | removeButton.addActionListener(l -> this.parentUi.removeSelectedListEntry()); 63 | editButton.addActionListener(l -> this.parentUi.editSelectedListEntry()); 64 | 65 | compileButton.addActionListener(l -> { 66 | if (hourMismatchCheck()) { 67 | return; 68 | } 69 | FileExporter.printTex(this.parentUi); 70 | }); 71 | printButton.addActionListener(l -> { 72 | if (hourMismatchCheck()) { 73 | return; 74 | } 75 | FileExporter.printPDF(this.parentUi); 76 | }); 77 | 78 | hoursWorkedLabel = new JLabel(); 79 | fontNormal = hoursWorkedLabel.getFont().deriveFont(18f); 80 | fontBold = fontNormal.deriveFont(Font.BOLD); 81 | hoursWorkedLabel.setFont(fontNormal); 82 | this.add(hoursWorkedLabel, BorderLayout.EAST); 83 | updateHours(new Time()); 84 | } 85 | 86 | private boolean hourMismatchCheck() { 87 | return (JSONHandler.getUISettings().isWarnOnHoursMismatch() && parentUi.hasWorkedHoursMismatch() 88 | && !parentUi.showOKCancelDialog("Hours mismatch", "Warning: The worked hours do not match the target working hours. Do you want to continue?")); 89 | } 90 | 91 | /** 92 | * Returns overflowing hours to be entered in the succ hours label. 93 | * 94 | * @param workedHours The hours worked, in sum. 95 | * @return Overflowing hours. 96 | */ 97 | public Time updateHours(Time workedHours) { 98 | UISettings uiSettings = JSONHandler.getUISettings(); 99 | String totalHoursStr = JSONHandler.getGlobalSettings().getWorkingTime(); 100 | Time totalHours = Time.parseTime(totalHoursStr); 101 | 102 | workedHours.addTime(this.parentUi.getPredTime()); 103 | 104 | String displayedWorkedHours; 105 | Time successorHours; 106 | 107 | if (workedHours.isLongerThan(totalHours)) { 108 | displayedWorkedHours = totalHours + "+"; 109 | successorHours = new Time(workedHours); 110 | successorHours.subtractTime(totalHours); 111 | hoursWorkedLabel.setFont(fontBold); 112 | 113 | if (uiSettings.isWarnOnHoursMismatch()) { 114 | hoursWorkedLabel.setForeground(Color.RED); 115 | } 116 | } else { 117 | displayedWorkedHours = workedHours.toString(); 118 | successorHours = new Time(0, 0); 119 | hoursWorkedLabel.setForeground(Color.BLACK); 120 | 121 | // Same Length 122 | if (!totalHours.isLongerThan(workedHours)) 123 | hoursWorkedLabel.setFont(fontBold); 124 | else 125 | hoursWorkedLabel.setFont(fontNormal); 126 | } 127 | 128 | hoursWorkedLabel.setText(HOURS_FORMAT.formatted(displayedWorkedHours, totalHours)); 129 | return successorHours; 130 | } 131 | 132 | public void reset() { 133 | updateHours(new Time()); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/ui/DragDropJFrame.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui; 3 | 4 | import javax.swing.*; 5 | import java.awt.*; 6 | import java.awt.datatransfer.DataFlavor; 7 | import java.awt.datatransfer.Transferable; 8 | import java.awt.datatransfer.UnsupportedFlavorException; 9 | import java.awt.dnd.*; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.List; 13 | 14 | public class DragDropJFrame extends JFrame { 15 | 16 | private static final Color HOVER_COLOR = new Color(160, 160, 160); 17 | private static final Color DEFAULT_COLOR = UIManager.getColor("Panel.background"); 18 | 19 | private final transient UserInterface parentUi; 20 | 21 | public DragDropJFrame(UserInterface parentUi) { 22 | this.parentUi = parentUi; 23 | 24 | // Setting up drag-and-drop functionality 25 | new DropTarget(this, new DropTargetListener() { 26 | @Override 27 | public void dragEnter(DropTargetDragEvent dtde) { 28 | if (isDragAcceptable(dtde)) { 29 | setColor(HOVER_COLOR); 30 | dtde.acceptDrag(DnDConstants.ACTION_COPY); 31 | } else { 32 | dtde.rejectDrag(); 33 | } 34 | } 35 | 36 | @Override 37 | public void dragOver(DropTargetDragEvent dtde) { 38 | // Needs to be implemented, but we're not doing anything here 39 | } 40 | 41 | @Override 42 | public void dropActionChanged(DropTargetDragEvent dtde) { 43 | // Needs to be implemented, but we're not doing anything here 44 | } 45 | 46 | @Override 47 | public void dragExit(DropTargetEvent dte) { 48 | setColor(DEFAULT_COLOR); 49 | } 50 | 51 | @Override 52 | public void drop(DropTargetDropEvent dtde) { 53 | handleDroppedFileEvent(dtde); 54 | } 55 | }); 56 | } 57 | 58 | @SuppressWarnings("unchecked") // :) 59 | private void handleDroppedFileEvent(DropTargetDropEvent dtde) { 60 | if (isDropAcceptable(dtde)) { 61 | dtde.acceptDrop(DnDConstants.ACTION_COPY); 62 | Transferable transferable = dtde.getTransferable(); 63 | 64 | try { 65 | List droppedFiles = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); 66 | 67 | if (!droppedFiles.isEmpty()) { 68 | File jsonFile = droppedFiles.getFirst(); 69 | if (jsonFile.getName().toLowerCase().endsWith(".json")) { 70 | performActionWithJSON(jsonFile); 71 | } else { 72 | JOptionPane.showMessageDialog(DragDropJFrame.this, "Only JSON files are accepted.", "Invalid File", JOptionPane.WARNING_MESSAGE); 73 | } 74 | } 75 | } catch (UnsupportedFlavorException | IOException ignored) { 76 | // Catch exception, but just ignore it. Nothing happens, we just go back to 77 | // normal 78 | } finally { 79 | setColor(DEFAULT_COLOR); 80 | dtde.dropComplete(true); 81 | } 82 | } else { 83 | dtde.rejectDrop(); 84 | } 85 | } 86 | 87 | private boolean isDragAcceptable(DropTargetDragEvent dtde) { 88 | return dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor); 89 | } 90 | 91 | private boolean isDropAcceptable(DropTargetDropEvent dtde) { 92 | return dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor); 93 | } 94 | 95 | private void performActionWithJSON(File jsonFile) { 96 | parentUi.openFile(jsonFile); 97 | } 98 | 99 | private void setColor(Color color) { 100 | getContentPane().setBackground(color); 101 | parentUi.setBackgroundColor(color); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/ui/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui; 3 | 4 | import javax.swing.*; 5 | import java.awt.*; 6 | import java.util.Objects; 7 | 8 | public final class ErrorHandler { 9 | private static Component parentComponent; 10 | 11 | private ErrorHandler() { 12 | throw new IllegalAccessError(); 13 | } 14 | 15 | public static synchronized void setParentComponent(Component parentComponent) { 16 | ErrorHandler.parentComponent = Objects.requireNonNull(parentComponent); 17 | } 18 | 19 | public static void showError(String title, String error) { 20 | if (parentComponent == null) { 21 | System.err.printf("Error (%s): %s%n", title, error); 22 | return; 23 | } 24 | JOptionPane.showMessageDialog(parentComponent, error, title, JOptionPane.ERROR_MESSAGE); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ui/JTimeField.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui; 3 | 4 | import javax.swing.*; 5 | import java.awt.*; 6 | import java.awt.event.FocusAdapter; 7 | import java.awt.event.FocusEvent; 8 | import java.util.regex.Pattern; 9 | 10 | public class JTimeField extends JTextField { 11 | 12 | private static final String PLACEHOLDER = "00:00"; 13 | static final Pattern TIME_PATTERN_SEMI_SMALL_2 = Pattern.compile("^(\\d):(\\d{2})$"); 14 | 15 | public JTimeField(UserInterface parentUi) { 16 | this(parentUi, null); 17 | } 18 | 19 | public JTimeField(UserInterface parentUi, String text) { 20 | super(4); 21 | this.setHorizontalAlignment(CENTER); 22 | 23 | DialogHelper.addPlaceholderText(this, PLACEHOLDER, text); 24 | 25 | this.addFocusListener(new FocusAdapter() { 26 | @Override 27 | public void focusLost(FocusEvent e) { 28 | if (getText().isEmpty()) 29 | return; 30 | validateField(); 31 | parentUi.updateTotalTimeWorkedUI(); 32 | } 33 | }); 34 | } 35 | 36 | private void validateField() { 37 | String text = this.getText().trim().replace('.', ':'); 38 | 39 | if (DialogHelper.TIME_PATTERN_SMALL.matcher(text).matches()) { 40 | text += ":00"; 41 | } else if (DialogHelper.TIME_PATTERN_SEMI_SMALL.matcher(text).matches()) { 42 | text += "0"; 43 | } 44 | if (TIME_PATTERN_SEMI_SMALL_2.matcher(text).matches()) { 45 | text = "0" + text; 46 | } 47 | super.setText(text); 48 | 49 | if (!text.isBlank() && !DialogHelper.TIME_PATTERN.matcher(text).matches()) { 50 | setForeground(Color.RED); 51 | } else { 52 | setForeground(getText().equals(PLACEHOLDER) ? Color.GRAY : Color.BLACK); 53 | } 54 | } 55 | 56 | public void clear() { 57 | // Prevent auto-focus on clear 58 | super.setFocusable(false); 59 | super.setText(PLACEHOLDER); 60 | setForeground(Color.GRAY); 61 | super.setFocusable(true); 62 | } 63 | 64 | @Override 65 | public boolean isValid() { 66 | return getForeground() != Color.RED; 67 | } 68 | 69 | @Override 70 | public void setText(String text) { 71 | super.setText(text); 72 | validateField(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/ui/SaveOnClosePrompt.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui; 3 | 4 | import javax.swing.*; 5 | import java.awt.*; 6 | import java.awt.event.ActionEvent; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | 9 | public final class SaveOnClosePrompt { 10 | 11 | private SaveOnClosePrompt() { 12 | // Don't allow instances of this class 13 | } 14 | 15 | static boolean showDialog(UserInterface parentUi) { 16 | final AtomicBoolean proceed = new AtomicBoolean(false); 17 | 18 | // Create the dialog 19 | JDialog dialog = new JDialog((Frame) null, "Save Changes?", true); 20 | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 21 | dialog.setLayout(new BorderLayout(10, 10)); 22 | dialog.setSize(400, 150); 23 | dialog.setLocationRelativeTo(null); 24 | 25 | // Add message 26 | JLabel messageLabel = new JLabel("You have unsaved changes. Save?", SwingConstants.CENTER); 27 | dialog.add(messageLabel, BorderLayout.CENTER); 28 | 29 | // Create buttons 30 | JPanel buttonPanel = createButtons(parentUi, proceed, dialog); 31 | 32 | // Add button panel to dialog 33 | dialog.add(buttonPanel, BorderLayout.SOUTH); 34 | 35 | // Display dialog 36 | dialog.setVisible(true); 37 | 38 | return proceed.get(); 39 | } 40 | 41 | private static JPanel createButtons(UserInterface parentUi, AtomicBoolean proceed, JDialog dialog) { 42 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); 43 | JButton saveButton = new JButton("Save"); 44 | JButton saveAsButton = new JButton("Save as"); 45 | JButton discardButton = new JButton("Discard"); 46 | JButton cancelButton = new JButton("Cancel"); 47 | 48 | // Button actions 49 | saveButton.addActionListener((ActionEvent e) -> { 50 | parentUi.saveFile(null); // Gets the current open file to save 51 | proceed.set(true); 52 | dialog.dispose(); 53 | }); 54 | 55 | saveAsButton.addActionListener((ActionEvent e) -> { 56 | parentUi.saveFileAs(); 57 | proceed.set(true); 58 | dialog.dispose(); 59 | }); 60 | 61 | discardButton.addActionListener((ActionEvent e) -> { 62 | proceed.set(true); 63 | dialog.dispose(); 64 | }); 65 | 66 | cancelButton.addActionListener((ActionEvent e) -> { 67 | proceed.set(false); 68 | dialog.dispose(); 69 | }); 70 | 71 | // Add buttons to panel 72 | buttonPanel.add(saveButton); 73 | buttonPanel.add(saveAsButton); 74 | buttonPanel.add(discardButton); 75 | buttonPanel.add(cancelButton); 76 | return buttonPanel; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/ui/Time.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui; 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.util.regex.Matcher; 8 | 9 | @Getter 10 | public class Time { 11 | @Setter 12 | private int hours; 13 | private int minutes; 14 | 15 | public Time() { 16 | this(0, 0); 17 | } 18 | 19 | public Time(int hours, int minutes) { 20 | setHours(hours); 21 | setMinutes(minutes); 22 | } 23 | 24 | public Time(Time time) { 25 | setHours(time.getHours()); 26 | setMinutes(time.getMinutes()); 27 | } 28 | 29 | public void addHours(int hours) { 30 | this.hours += hours; 31 | } 32 | 33 | public void setMinutes(int minutes) { 34 | this.hours += minutes / 60; 35 | this.minutes = minutes % 60; 36 | } 37 | 38 | public void addMinutes(int minutes) { 39 | this.minutes += minutes; 40 | this.hours += this.minutes / 60; 41 | this.minutes %= 60; 42 | if (this.minutes < 0) { 43 | this.minutes += 60; 44 | hours--; 45 | } 46 | } 47 | 48 | public void addTime(Time time) { 49 | addHours(time.getHours()); 50 | addMinutes(time.getMinutes()); 51 | } 52 | 53 | public void subtractTime(Time time) { 54 | addHours(-time.getHours()); 55 | addMinutes(-time.getMinutes()); 56 | } 57 | 58 | public boolean sameLengthAs(Time other) { 59 | return this.hours == other.hours && this.minutes == other.minutes; 60 | } 61 | 62 | public boolean isLongerThan(Time other) { 63 | if (this.hours > other.hours) 64 | return true; 65 | if (this.hours < other.hours) 66 | return false; 67 | return this.minutes > other.minutes; 68 | } 69 | 70 | public boolean isNotZero() { 71 | return hours > 0 || minutes > 0; 72 | } 73 | 74 | public static Time parseTime(String string) { 75 | if (string == null) 76 | return new Time(0, 0); 77 | Matcher matcher = DialogHelper.TIME_PATTERN.matcher(string); 78 | if (!matcher.matches()) 79 | return new Time(); 80 | return new Time(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))); 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return String.format("%02d:%02d", hours, minutes); 86 | } 87 | 88 | /** 89 | * Returns a new time object with 0 hours and 0 minutes. 90 | * 91 | * @return Time object with 0 time stored. 92 | */ 93 | public static Time none() { 94 | return new Time(0, 0); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/ui/export/FileExporter.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.export; 3 | 4 | import ui.ErrorHandler; 5 | import ui.UserInterface; 6 | import ui.fileexplorer.FileChooser; 7 | import ui.json.JSONHandler; 8 | 9 | import java.io.File; 10 | import java.util.Optional; 11 | 12 | public final class FileExporter { 13 | 14 | private FileExporter() { 15 | throw new IllegalAccessError(); 16 | } 17 | 18 | public static void printTex(UserInterface parentUi) { 19 | try (TempFiles tempFiles = TempFiles.generateNewTemp(parentUi)) { 20 | if (tempFiles == null) { 21 | ErrorHandler.showError("Failed to create temporary file", "Could not create month.json file. If you have unsaved changes, try saving."); 22 | return; 23 | } 24 | 25 | Optional error = TexCompiler.validateContents(tempFiles); 26 | if (error.isPresent()) { 27 | error("Validation error", error.get()); 28 | return; 29 | } 30 | 31 | File texFile = FileChooser.chooseCreateTexFile(parentUi, "Compile to Tex"); 32 | if (texFile == null) 33 | return; // Cancelled 34 | 35 | TexCompiler.compileToTex(tempFiles.getMonthFile(), texFile); 36 | 37 | if (!texFile.exists()) { 38 | error("Latex compiler error", "Tex file creation failed!"); 39 | } 40 | } 41 | } 42 | 43 | public static void printPDF(UserInterface parentUi) { 44 | try (TempFiles tempFiles = TempFiles.generateNewTemp(parentUi)) { 45 | if (tempFiles == null) 46 | return; 47 | 48 | Optional error = TexCompiler.validateContents(tempFiles); 49 | if (error.isPresent()) { 50 | error("Validation error", error.get()); 51 | return; 52 | } 53 | 54 | File pdfFile = FileChooser.chooseCreatePDFFile(parentUi, "Print to PDF"); 55 | if (pdfFile == null) { 56 | return; // Cancelled 57 | } 58 | 59 | error = PDFCompiler.compileToPDF(JSONHandler.getGlobalSettings(), parentUi.getCurrentMonth(), pdfFile, JSONHandler.getUISettings()); 60 | 61 | if (error.isPresent()) { 62 | error("PDF compiler error", error.get()); 63 | return; 64 | } 65 | 66 | if (!pdfFile.exists()) { 67 | error("Failed to create PDF", "PDF file creation failed! Perhaps try to compile to tex?"); 68 | } 69 | } 70 | } 71 | 72 | private static void error(String title, String error) { 73 | ErrorHandler.showError(title, "%s%s%s".formatted("Error: Invalid Timesheet:", System.lineSeparator(), error)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/ui/export/PDFCompiler.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.export; 3 | 4 | import org.apache.pdfbox.Loader; 5 | import org.apache.pdfbox.pdmodel.PDDocument; 6 | import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; 7 | import ui.Time; 8 | import ui.json.Global; 9 | import ui.json.JSONHandler; 10 | import ui.json.Month; 11 | import ui.json.UISettings; 12 | 13 | import java.io.EOFException; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.time.LocalDateTime; 18 | import java.time.format.DateTimeFormatter; 19 | import java.util.Optional; 20 | import java.util.logging.Logger; 21 | 22 | public class PDFCompiler { 23 | private static final String DATE_FORMAT_2_DIGITS = "dd.MM.yy"; 24 | private static final String DATE_FORMAT_4_DIGITS = "dd.MM.yyyy"; 25 | 26 | private PDFCompiler() { 27 | throw new IllegalAccessError(); 28 | } 29 | 30 | public static Optional compileToPDF(Global global, Month month, File targetFile, UISettings uiSettings) { 31 | try (InputStream templateStream = PDFCompiler.class.getResourceAsStream("/pdf/template.pdf")) { 32 | if (templateStream == null) { 33 | return Optional.of("Template PDF not found in resources."); 34 | } 35 | 36 | PDDocument document = Loader.loadPDF(templateStream.readAllBytes()); 37 | return writeToPDF(document, global, month, targetFile, uiSettings); 38 | 39 | } catch (IOException e) { 40 | return Optional.of(e.getMessage()); 41 | } 42 | } 43 | 44 | private static Optional writeToPDF(PDDocument document, Global global, Month month, File targetFile, UISettings uiSettings) throws IOException { 45 | PDAcroForm form = document.getDocumentCatalog().getAcroForm(); 46 | if (form == null) { 47 | return Optional.of("No form found in the document. Nothing we can do, sorry."); 48 | } 49 | 50 | form.getField("GF").setValue(global.getNameFormalFormat()); // Name 51 | form.getField("abc").setValue(getMonth(month, uiSettings)); // Month 52 | form.getField("abdd").setValue(String.valueOf(month.getYear())); // Year 53 | form.getField("Personalnummer").setValue(String.valueOf(global.getStaffId())); // Personalnummer 54 | if (global.getWorkingArea().equals("gf")) { 55 | form.getField("GFB").setValue("On"); 56 | form.getField("UB").setValue("Off"); 57 | } else if (global.getWorkingArea().equals("ub")) { 58 | form.getField("GFB").setValue("Off"); 59 | form.getField("UB").setValue("On"); 60 | } 61 | form.getField("OE").setValue(global.getDepartment()); // Probably department 62 | form.getField("Std").setValue(global.getWorkingTime()); // Total hours 63 | form.getField("Stundensatz").setValue(String.valueOf(global.getWage()));// Wage 64 | 65 | form.getField("Übertrag vom Vormonat").setValue(month.getPredTransfer()); // Pred Übertrag 66 | form.getField("Übertrag in den Folgemonat").setValue(month.getSuccTransfer()); // Succ Übertrag 67 | 68 | Time timeSum = Time.parseTime(month.getPredTransfer()); 69 | Time timeVacation = new Time(); 70 | form.getField("monatliche SollArbeitszeit").setValue(global.getWorkingTime()); // Again hours probably 71 | 72 | try { 73 | form.getField("Ich bestätige die Richtigkeit der Angaben") 74 | .setValue("%s, %s".formatted(DateTimeFormatter.ofPattern(DATE_FORMAT_4_DIGITS).format(LocalDateTime.now()), 75 | JSONHandler.getUISettings().isAddSignature() ? global.getName() : "")); 76 | } catch (EOFException ignored) { 77 | Logger.getGlobal().warning("Could not load font for signature field when exporting to PDF. Proceeding with default."); 78 | } 79 | 80 | final DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern(uiSettings.isUseYYYY() ? DATE_FORMAT_4_DIGITS : DATE_FORMAT_2_DIGITS); 81 | 82 | int fieldIndex = 1; 83 | for (int i = 0; i < month.getEntries().size(); i++) { 84 | Month.Entry entry = month.getEntries().get(i); 85 | Time time = Time.parseTime(entry.getEnd()); 86 | time.subtractTime(Time.parseTime(entry.getStart())); 87 | time.subtractTime(Time.parseTime(entry.getPause())); 88 | timeSum.addTime(time); 89 | 90 | if (entry.isVacation()) { 91 | timeVacation.addTime(time); 92 | if (!uiSettings.isAddVacationEntry()) 93 | continue; 94 | } 95 | 96 | form.getField("Tätigkeit Stichwort ProjektRow%d".formatted(fieldIndex)).setValue(entry.getAction()); 97 | form.getField("ttmmjjRow%d".formatted(fieldIndex)) 98 | .setValue(dayFormatter.format(LocalDateTime.of(month.getYear(), month.getMonth(), entry.getDay(), 0, 0))); 99 | form.getField("hhmmRow%d".formatted(fieldIndex)).setValue(entry.getStart()); 100 | form.getField("hhmmRow%d_2".formatted(fieldIndex)).setValue(entry.getEnd()); 101 | form.getField("hhmmRow%d_3".formatted(fieldIndex)).setValue(entry.getPause()); 102 | 103 | String timeFieldValue = time.toString(); 104 | if (entry.isVacation()) 105 | timeFieldValue += " U"; 106 | form.getField("hhmmRow%d_4".formatted(fieldIndex)).setValue(timeFieldValue); 107 | fieldIndex++; 108 | } 109 | 110 | form.getField("Summe").setValue(timeSum.toString()); // Total time worked 111 | form.getField("Urlaub anteilig").setValue(timeVacation.toString()); // Total time of Vacation 112 | 113 | // Save the filled document 114 | document.save(targetFile); 115 | document.close(); 116 | 117 | return Optional.empty(); 118 | } 119 | 120 | private static String getMonth(Month month, UISettings uiSettings) { 121 | return uiSettings.isUseGermanMonths() ? month.getGermanName() : "%02d".formatted(month.getMonth()); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/ui/export/TempFiles.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.export; 3 | 4 | import lombok.Getter; 5 | import ui.UserInterface; 6 | import ui.json.JSONHandler; 7 | 8 | import java.io.File; 9 | 10 | class TempFiles implements AutoCloseable { 11 | 12 | @Getter 13 | private final File globalFile; 14 | @Getter 15 | private final File monthFile; 16 | 17 | private final boolean isTempMonthFile; 18 | 19 | private TempFiles(File globalFile, File monthFile, boolean isTempMonthFile) { 20 | this.globalFile = globalFile; 21 | this.monthFile = monthFile; 22 | this.isTempMonthFile = isTempMonthFile; 23 | } 24 | 25 | @Override 26 | public void close() { 27 | if (isTempMonthFile && !monthFile.delete()) { 28 | monthFile.deleteOnExit(); 29 | } 30 | } 31 | 32 | public static TempFiles generateNewTemp(UserInterface parentUi) { 33 | File monthFile; 34 | boolean tempMonth = false; 35 | if (parentUi.hasUnsavedChanges() || parentUi.getCurrentOpenFile() == null) { 36 | monthFile = parentUi.generateTempMonthFile(); 37 | tempMonth = true; 38 | } else { 39 | monthFile = parentUi.getCurrentOpenFile(); 40 | } 41 | if (monthFile == null) 42 | return null; 43 | return new TempFiles(JSONHandler.getConfigFile(), monthFile, tempMonth); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/ui/export/TexCompiler.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui.export; 3 | 4 | import ui.json.JSONHandler; 5 | 6 | import java.io.File; 7 | import java.util.Optional; 8 | 9 | public final class TexCompiler { 10 | 11 | private TexCompiler() { 12 | throw new IllegalAccessError(); 13 | } 14 | 15 | public static void compileToTex(File monthFile, File texFile) { 16 | File globalFile = JSONHandler.getConfigFile(); 17 | main.Main.main(new String[] { "-f", globalFile.getAbsolutePath(), monthFile.getAbsolutePath(), texFile.getAbsolutePath() }); 18 | } 19 | 20 | /** 21 | * Wrapper method for the timesheet generator module timesheet compiler. 22 | * 23 | * @param tempFiles The temporary files. 24 | * @return An optional of the error message, empty if success. 25 | */ 26 | static Optional validateContents(TempFiles tempFiles) { 27 | return main.Main.validateTimesheet(tempFiles.getGlobalFile(), tempFiles.getMonthFile()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/ui/fileexplorer/FileChooser.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.fileexplorer; 3 | 4 | import ui.UserInterface; 5 | import ui.json.Global; 6 | import ui.json.JSONHandler; 7 | import ui.json.UISettings; 8 | 9 | import javax.swing.*; 10 | import javax.swing.filechooser.FileNameExtensionFilter; 11 | import java.io.File; 12 | 13 | public final class FileChooser { 14 | 15 | private FileChooser() { 16 | // Don't allow instances of this class 17 | } 18 | 19 | /** 20 | * Returns the default file name for the PDF export based on the users settings. 21 | * 22 | * @param parentUI The UserInterface instance to get the current month and year 23 | * from. 24 | * @return The default file name for the PDF export. 25 | */ 26 | public static String getDefaultFileName(UserInterface parentUI) { 27 | UISettings uiSettings = JSONHandler.getUISettings(); 28 | Global global = JSONHandler.getGlobalSettings(); 29 | return uiSettings.getExportPdfNameFormat().replace("%FIRST_U%", global.getFirstnameUnderscoreFormat()).replace("%FIRST%", global.getFirstname()) 30 | .replace("%LAST%", global.getLastname()).replace("%MM%", "%02d".formatted(parentUI.getCurrentMonthNumber())) 31 | .replace("%MM_GER%", parentUI.getCurrentMonth().getGermanName()).replace("%MM_ENG%", parentUI.getCurrentMonthName()) 32 | .replace("%YY%", parentUI.getYear()).replace("%YYYY%", parentUI.getFullYear()); 33 | } 34 | 35 | public static File chooseFile(String title, FileChooserType chooserType) { 36 | // Later: Implement Windows Version ? 37 | // -> Later, I tried doing it simple and failed, and I don't have the time 38 | // (currently) to figure it out 39 | // Once I do, I'll make another pull request. It might not be as pretty, but 40 | // because it remembers the 41 | // folders, you don't need to go through the navigation hassle each time, so I 42 | // think it's a fair 43 | // compromise for now. 44 | // - JustOneDeveloper 45 | return chooseFileSwing(title, chooserType); 46 | 47 | } 48 | 49 | private static File chooseFileSwing(String title, FileChooserType chooserType) { 50 | JFileChooser fileChooser = getFileChooser(chooserType); 51 | 52 | fileChooser.setDialogTitle(title); 53 | 54 | int userSelection = fileChooser.showOpenDialog(null); 55 | 56 | if (userSelection == JFileChooser.APPROVE_OPTION) { 57 | File file = fileChooser.getSelectedFile(); 58 | JSONHandler.getUISettings().setPath(chooserType, file); 59 | return file; 60 | } else { 61 | return null; 62 | } 63 | } 64 | 65 | public static File chooseCreateJSONFile(UserInterface parentUi, String title) { 66 | String month = parentUi.getCurrentMonthName(); 67 | if (month.isBlank()) 68 | month = "month"; 69 | return chooseCreateFile(title, FileChooserType.MONTH_PATH, "%s%s".formatted(month, parentUi.getYear()), "json", "JSON Files (*.json)"); 70 | } 71 | 72 | public static File chooseCreateTexFile(UserInterface parentUi, String title) { 73 | return chooseCreateFile(title, FileChooserType.TEX_PATH, getDefaultFileName(parentUi), "tex", "LaTeX Files (*.tex)"); 74 | } 75 | 76 | public static File chooseCreatePDFFile(UserInterface parentUi, String title) { 77 | return chooseCreateFile(title, FileChooserType.PDF_PATH, getDefaultFileName(parentUi), "pdf", "PDF Files (*.pdf)"); 78 | } 79 | 80 | public static File chooseCreateFile(String title, FileChooserType chooserType, String defaultFileName, String extension, String extensionDescription) { 81 | JFileChooser fileChooser = getFileChooser(chooserType); 82 | fileChooser.setDialogTitle(title); 83 | 84 | // Suggest a default file name 85 | fileChooser.setSelectedFile(new File("%s.%s".formatted(defaultFileName, extension == null ? "" : extension))); 86 | 87 | // Set up file filter for .json files 88 | FileNameExtensionFilter jsonFilter = new FileNameExtensionFilter(extensionDescription, extension); 89 | fileChooser.setFileFilter(jsonFilter); 90 | 91 | int userSelection = fileChooser.showSaveDialog(null); 92 | 93 | if (userSelection == JFileChooser.APPROVE_OPTION) { 94 | File fileToSave = fileChooser.getSelectedFile(); 95 | if (fileToSave.exists()) { 96 | int result = JOptionPane.showConfirmDialog(null, "The file already exists. Do you want to override it?", "Existing file", 97 | JOptionPane.YES_NO_OPTION); 98 | 99 | if (result != JOptionPane.YES_OPTION) { 100 | return null; 101 | } 102 | } 103 | 104 | JSONHandler.getUISettings().setPath(chooserType, fileToSave); 105 | return fileToSave; 106 | } else { 107 | return null; 108 | } 109 | } 110 | 111 | private static JFileChooser getFileChooser(FileChooserType chooserType) { 112 | JFileChooser fileChooser; 113 | File parent = JSONHandler.getUISettings().getPath(chooserType); 114 | if (parent == null) 115 | fileChooser = new JFileChooser(); 116 | else 117 | fileChooser = new JFileChooser(parent); 118 | return fileChooser; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/ui/fileexplorer/FileChooserType.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui.fileexplorer; 3 | 4 | public enum FileChooserType { 5 | MONTH_PATH, TEX_PATH, PDF_PATH 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/ui/json/EntrySerializer.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui.json; 3 | 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Used to ensure XOR when entering entries, such that pause is only printed for 12 | * an entry if and only if vacation is false. 13 | */ 14 | public class EntrySerializer extends JsonSerializer { 15 | @Override 16 | public void serialize(Month.Entry entry, JsonGenerator gen, SerializerProvider serializers) throws IOException { 17 | gen.writeStartObject(); 18 | gen.writeStringField("action", entry.getAction()); 19 | gen.writeNumberField("day", entry.getDay()); 20 | gen.writeStringField("start", entry.getStart()); 21 | gen.writeStringField("end", entry.getEnd()); 22 | 23 | // Serialize only one: pause or vacation 24 | if (entry.isVacation()) { 25 | gen.writeBooleanField("vacation", true); 26 | } else { 27 | gen.writeStringField("pause", entry.getPause()); 28 | } 29 | 30 | gen.writeEndObject(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/ui/json/Global.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | /** 10 | * Global Settings stored in AppData files. 11 | */ 12 | @Getter 13 | @Setter 14 | public class Global { 15 | @JsonProperty("$schema") 16 | private String schema; 17 | private String name; 18 | private int staffId; 19 | private String department; 20 | private String workingTime; 21 | private double wage; 22 | private String workingArea; 23 | 24 | public Global() { 25 | schema = "https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/main/examples/schemas/global.json"; 26 | } 27 | 28 | public Global(Global global) { 29 | this(); 30 | this.name = global.name; 31 | this.staffId = global.staffId; 32 | this.department = global.department; 33 | this.workingTime = global.workingTime; 34 | this.wage = global.wage; 35 | this.workingArea = global.workingArea; 36 | } 37 | 38 | // Constructors, Getters, and Setters 39 | 40 | /** 41 | * Formats the name, e.g. "Firstname-middle Lastname", to be "Lastname, 42 | * Firstname-middle" by splitting the name at the spaces and rotating the last 43 | * String to the front. Everything else is interpreted as first- and middle 44 | * names. 45 | * If the name does not contain any spaces, returns the regular name. 46 | * 47 | * @return The name, formatted to start with the lastname. 48 | */ 49 | @JsonIgnore 50 | public String getNameFormalFormat() { 51 | if (!getName().contains(" ")) { 52 | return getName(); 53 | } 54 | return "%s, %s".formatted(getLastname(), getFirstname()); 55 | } 56 | 57 | /** 58 | * Formats the name, e.g. "Firstname Middlename Lastname", to be "Firstname 59 | * middle" by splitting the name at the spaces and removing the last String, as 60 | * this is probably the lastname. Everything else is interpreted as first- and 61 | * middle names. 62 | * For separated by underscores instead of spaces, see 63 | * {@link Global#getFirstnameUnderscoreFormat()} This is used when determining 64 | * the filename when exporting to a PDF. 65 | * 66 | * If the name does not contain any spaces, returns the regular name. 67 | * 68 | * @return the firstname of the employee, separated by spaces. 69 | */ 70 | @JsonIgnore 71 | public String getFirstname() { 72 | if (!getName().contains(" ")) { 73 | return getName(); 74 | } 75 | 76 | String[] nameParts = getName().split(" "); 77 | String[] onlyFirstnameParts = new String[nameParts.length - 1]; 78 | System.arraycopy(nameParts, 0, onlyFirstnameParts, 0, onlyFirstnameParts.length); 79 | 80 | return String.join(" ", onlyFirstnameParts); 81 | } 82 | 83 | /** 84 | * Formats the name, e.g. "Firstname Middlename Lastname", to be "Firstname 85 | * middle" by splitting the name at the spaces and removing the last String, as 86 | * this is probably the lastname. Everything else is interpreted as first- and 87 | * middle names. 88 | * 89 | * For separated by spaces instead of underscores, see 90 | * {@link Global#getFirstname()} This is used when determining the filename when 91 | * exporting to a PDF. 92 | * 93 | * 94 | * This method literally does {@code Global#getFirstname().replace(' ', '_')}. 95 | * 96 | * 97 | * If the name does not contain any spaces, returns the regular name. 98 | * 99 | * @return the firstname of the employee, separated by underscores. 100 | */ 101 | @JsonIgnore 102 | public String getFirstnameUnderscoreFormat() { 103 | return getFirstname().replace(' ', '_'); 104 | } 105 | 106 | /** 107 | * Gets the lastname of the employee. 108 | * This method formats the name, e.g. "Firstname Middlename Lastname", to be 109 | * "Lastname" by splitting the name at the spaces and returning the last String. 110 | * Everything else is interpreted as first- and middle names. 111 | * 112 | * If the name does not contain any spaces, returns the regular name. 113 | * 114 | * @return the lastname of the employee. 115 | */ 116 | @JsonIgnore 117 | public String getLastname() { 118 | if (!getName().contains(" ")) { 119 | return getName(); 120 | } 121 | String[] nameParts = getName().split(" "); 122 | return nameParts[nameParts.length - 1]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/ui/json/Month.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.json; 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | 11 | @JsonSerialize(using = MonthSerializer.class) 12 | public class Month { 13 | 14 | @JsonProperty("$schema") 15 | @Getter 16 | @Setter 17 | private String schema; 18 | 19 | @Getter 20 | @Setter 21 | private int year; 22 | 23 | @JsonProperty("month") 24 | private int monthNr; 25 | 26 | @JsonProperty("pred_transfer") 27 | @Getter 28 | @Setter 29 | private String predTransfer; 30 | 31 | @JsonProperty("succ_transfer") 32 | @Getter 33 | @Setter 34 | private String succTransfer; 35 | 36 | @Getter 37 | @Setter 38 | private List entries; 39 | 40 | // Constructors, Getters, and Setters 41 | public Month() { 42 | schema = "https://raw.githubusercontent.com/kit-sdq/TimeSheetGenerator/master/examples/schemas/month.json"; 43 | } 44 | 45 | // Nested class for individual entries 46 | @Setter 47 | @Getter 48 | @JsonSerialize(using = EntrySerializer.class) 49 | public static class Entry { 50 | private String action; 51 | private int day; 52 | private String start; 53 | private String end; 54 | private String pause; 55 | private boolean vacation; 56 | } 57 | 58 | // Getters and Setters for Month class fields 59 | 60 | public void setMonth(int month) { 61 | this.monthNr = month; 62 | } 63 | 64 | public int getMonth() { 65 | return monthNr; 66 | } 67 | 68 | public String getGermanName() { 69 | return switch (monthNr) { 70 | case 1 -> "Januar"; 71 | case 2 -> "Februar"; 72 | case 3 -> "März"; 73 | case 4 -> "April"; 74 | case 5 -> "Mai"; 75 | case 6 -> "Juni"; 76 | case 7 -> "Juli"; 77 | case 8 -> "August"; 78 | case 9 -> "September"; 79 | case 10 -> "Oktober"; 80 | case 11 -> "November"; 81 | case 12 -> "Dezember"; 82 | default -> "null"; 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/ui/json/MonthSerializer.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024. */ 2 | package ui.json; 3 | 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | 8 | import java.io.IOException; 9 | 10 | public class MonthSerializer extends JsonSerializer { 11 | 12 | @Override 13 | public void serialize(Month month, JsonGenerator gen, SerializerProvider serializers) throws IOException { 14 | gen.writeStartObject(); 15 | gen.writeStringField("$schema", month.getSchema()); 16 | gen.writeNumberField("year", month.getYear()); 17 | gen.writeNumberField("month", month.getMonth()); 18 | gen.writeStringField("pred_transfer", month.getPredTransfer()); 19 | gen.writeStringField("succ_transfer", month.getSuccTransfer()); 20 | 21 | gen.writeFieldName("entries"); 22 | JsonSerializer entrySerializer = serializers.findValueSerializer(Month.Entry.class); 23 | gen.writeStartArray(); 24 | for (Month.Entry entry : month.getEntries()) { 25 | entrySerializer.serialize(entry, gen, serializers); 26 | } 27 | gen.writeEndArray(); 28 | 29 | gen.writeEndObject(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/ui/json/UISettings.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2024-2025. */ 2 | package ui.json; 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import ui.fileexplorer.FileChooserType; 7 | 8 | import java.io.File; 9 | 10 | @Getter 11 | @Setter 12 | public class UISettings { 13 | private boolean addSignature = true; 14 | private boolean addVacationEntry = false; 15 | private boolean useYYYY = false; 16 | private boolean useGermanMonths = false; 17 | private boolean warnOnHoursMismatch = true; 18 | private String monthPath; 19 | private String texPath; 20 | private String pdfPath; 21 | /** 22 | * Specifies the naming format for exported PDF files. 23 | * 24 | * The format can include placeholders for dynamic values from other current 25 | * settings, such as the name, month and year. The default value is defined by 26 | * {@link JSONHandler#DEFAULT_PDF_NAME_FORMAT_ALGO}. 27 | * 28 | */ 29 | private String exportPdfNameFormat = JSONHandler.DEFAULT_PDF_NAME_FORMAT_ALGO; 30 | 31 | public UISettings() { 32 | // Default Constructor is required 33 | } 34 | 35 | public UISettings(UISettings uiSettings) { 36 | this.addSignature = uiSettings.addSignature; 37 | this.addVacationEntry = uiSettings.addVacationEntry; 38 | this.useYYYY = uiSettings.useYYYY; 39 | this.useGermanMonths = uiSettings.useGermanMonths; 40 | this.warnOnHoursMismatch = uiSettings.warnOnHoursMismatch; 41 | this.monthPath = uiSettings.monthPath; 42 | this.texPath = uiSettings.texPath; 43 | this.pdfPath = uiSettings.pdfPath; 44 | this.exportPdfNameFormat = uiSettings.exportPdfNameFormat; 45 | } 46 | 47 | // Constructors, Getters, and Setters 48 | 49 | public void setMonthPath(String openMonthPath) { 50 | this.monthPath = openMonthPath; 51 | save(); 52 | } 53 | 54 | public void setTexPath(String texPath) { 55 | this.texPath = texPath; 56 | save(); 57 | } 58 | 59 | public void setPdfPath(String pdfPath) { 60 | this.pdfPath = pdfPath; 61 | save(); 62 | } 63 | 64 | public void setPath(FileChooserType type, File selectedFile) { 65 | String folderPath; 66 | if (selectedFile == null) 67 | folderPath = ""; 68 | else 69 | folderPath = selectedFile.getParent(); 70 | switch (type) { 71 | case MONTH_PATH -> setMonthPath(folderPath); 72 | case TEX_PATH -> setTexPath(folderPath); 73 | case PDF_PATH -> setPdfPath(folderPath); 74 | } 75 | } 76 | 77 | public File getPath(FileChooserType type) { 78 | String pathStr = switch (type) { 79 | case MONTH_PATH -> getMonthPath(); 80 | case TEX_PATH -> getTexPath(); 81 | case PDF_PATH -> getPdfPath(); 82 | }; 83 | // If Pdf path is empty but tex isn't, use tex path for pdf default and vice 84 | // versa 85 | if (pathStr == null || pathStr.isEmpty()) { 86 | switch (type) { 87 | case PDF_PATH: 88 | pathStr = getTexPath(); 89 | if (pathStr == null || pathStr.isEmpty()) 90 | return null; 91 | break; 92 | case TEX_PATH: 93 | pathStr = getPdfPath(); 94 | if (pathStr == null || pathStr.isEmpty()) 95 | return null; 96 | break; 97 | default: 98 | return null; 99 | } 100 | } 101 | File pathFile = new File(pathStr); 102 | if (!pathFile.exists()) 103 | return null; 104 | return pathFile; 105 | } 106 | 107 | public void save() { 108 | JSONHandler.saveUISettings(this); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/resources/i18n/MessageBundle.properties: -------------------------------------------------------------------------------- 1 | application.name = TimeSheetGenerator 2 | 3 | command.input.help.description = Prints helping information 4 | command.input.version.description = Prints the version 5 | command.input.gui.description = Enables load/save dialogs 6 | command.input.file.description = Passes file paths via console 7 | command.input.file.arguments = global.json> item.getErrorMessage().equals(error))); 61 | } 62 | 63 | @Test 64 | public void testSingleEntryValidButSunday() throws CheckerException { 65 | //// Test values 66 | TimeSpan start = new TimeSpan(8, 0); 67 | TimeSpan end = new TimeSpan(12, 0); 68 | TimeSpan pause = zeroTs; 69 | LocalDate date = LocalDate.of(2024, 12, 1); // Sunday 70 | 71 | //// Checker initialization 72 | Entry entry = new Entry("Test", date, start, end, pause, false); 73 | Entry[] entries = { entry }; 74 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 75 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 76 | 77 | //// Expectation 78 | String error = MiLoGChecker.MiLoGCheckerErrorMessageProvider.TIME_SUNDAY.getErrorMessage(date); 79 | 80 | //// Assertions 81 | assertEquals(CheckerReturn.INVALID, checker.check()); 82 | assertTrue(checker.getErrors().stream().anyMatch(item -> item.getErrorMessage().equals(error))); 83 | } 84 | 85 | @Test 86 | public void testSingleEntryValidButSundayAndHoliday() throws CheckerException { 87 | //// Test values 88 | TimeSpan start = new TimeSpan(8, 0); 89 | TimeSpan end = new TimeSpan(12, 0); 90 | TimeSpan pause = zeroTs; 91 | LocalDate date = LocalDate.of(2022, 12, 25); // Sunday and holiday 92 | 93 | //// Checker initialization 94 | Entry entry = new Entry("Test", date, start, end, pause, false); 95 | Entry[] entries = { entry }; 96 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 97 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 98 | 99 | //// Expectation 100 | String error = MiLoGChecker.MiLoGCheckerErrorMessageProvider.TIME_SUNDAY.getErrorMessage(date); 101 | 102 | //// Assertions 103 | assertEquals(CheckerReturn.INVALID, checker.check()); 104 | assertTrue(checker.getErrors().stream().anyMatch(item -> item.getErrorMessage().equals(error))); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/checker/MiLoGCheckerDepartmentNameTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker; 3 | 4 | import data.*; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | import java.time.Month; 9 | import java.time.YearMonth; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class MiLoGCheckerDepartmentNameTest { 15 | 16 | private static final Entry[] ENTRIES = { new Entry("Test", LocalDate.of(2019, 11, 22), new TimeSpan(0, 0), new TimeSpan(0, 0), new TimeSpan(0, 0), false) }; 17 | 18 | //// Placeholder for time sheet construction 19 | private static final Employee EMPLOYEE = new Employee("Max Mustermann", 1234567); 20 | private static final YearMonth YEAR_MONTH = YearMonth.of(2019, Month.NOVEMBER); 21 | private static final TimeSpan zeroTs = new TimeSpan(0, 0); 22 | 23 | @Test 24 | public void testEmptyName() { 25 | //// Test values 26 | String departmentName = ""; 27 | 28 | //// Checker initialization 29 | Profession profession = new Profession(departmentName, WorkingArea.UB, new TimeSpan(40, 0), 10.31); 30 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, profession, YEAR_MONTH, ENTRIES, zeroTs, zeroTs); 31 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 32 | 33 | //// Execution 34 | checker.checkDepartmentName(); 35 | 36 | //// Assertions 37 | assertEquals(CheckerReturn.INVALID, checker.getResult()); 38 | assertTrue(checker.getErrors().stream() 39 | .anyMatch(item -> item.getErrorMessage().equals(MiLoGChecker.MiLoGCheckerErrorMessageProvider.NAME_MISSING.getErrorMessage()))); 40 | } 41 | 42 | @Test 43 | public void testValidName() { 44 | //// Test values 45 | String departmentName = "validName Test Word"; 46 | 47 | //// Checker initialization 48 | Profession profession = new Profession(departmentName, WorkingArea.UB, new TimeSpan(40, 0), 10.31); 49 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, profession, YEAR_MONTH, ENTRIES, zeroTs, zeroTs); 50 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 51 | 52 | //// Execution 53 | checker.checkDepartmentName(); 54 | 55 | //// Assertions 56 | assertEquals(CheckerReturn.VALID, checker.getResult()); 57 | assertTrue(checker.getErrors().isEmpty()); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/checker/MiLoGCheckerRowNumExceedanceTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker; 3 | 4 | import data.*; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | import java.time.Month; 9 | import java.time.YearMonth; 10 | import java.util.Random; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | public class MiLoGCheckerRowNumExceedanceTest { 16 | 17 | // TODO Out source entry generator 18 | private static final int RANDOM_ENTRY_BOUND = 50; 19 | private static final int CHECKER_ENTRY_MAX = MiLoGChecker.getMaxEntries(); 20 | 21 | //// Placeholder for time sheet construction 22 | private static final Employee EMPLOYEE = new Employee("Max Mustermann", 1234567); 23 | private static final Profession PROFESSION = new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31); 24 | private static final YearMonth YEAR_MONTH = YearMonth.of(2019, Month.NOVEMBER); 25 | private static final TimeSpan zeroTs = new TimeSpan(0, 0); 26 | 27 | @Test 28 | public void testNoExceedanceLowerBound() { 29 | //// Checker initialization 30 | Entry entry = new Entry("Test", LocalDate.of(2019, 11, 22), new TimeSpan(0, 0), new TimeSpan(0, 0), new TimeSpan(0, 0), false); 31 | Entry[] entries = { entry }; 32 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 33 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 34 | 35 | //// Execution 36 | checker.checkRowNumExceedance(); 37 | 38 | //// Assertions 39 | assertEquals(CheckerReturn.VALID, checker.getResult()); 40 | assertTrue(checker.getErrors().isEmpty()); 41 | } 42 | 43 | @Test 44 | public void testNoExceedanceUpperBound() { 45 | //// Test values 46 | int numberOfEntries = CHECKER_ENTRY_MAX; 47 | 48 | //// Entry generator 49 | Entry[] entries = new Entry[numberOfEntries]; 50 | for (int i = 0; i < numberOfEntries; i++) { 51 | TimeSpan start = new TimeSpan(0, 0); 52 | TimeSpan end = new TimeSpan(0, 0); 53 | TimeSpan pause = new TimeSpan(0, 0); 54 | 55 | Entry entry = new Entry("Test", LocalDate.of(2019, 11, 22), start, end, pause, false); 56 | entries[i] = entry; 57 | } 58 | 59 | //// Checker initialization 60 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 61 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 62 | 63 | //// Execution 64 | checker.checkRowNumExceedance(); 65 | 66 | //// Assertions 67 | assertEquals(numberOfEntries, timeSheet.getEntries().size()); 68 | assertEquals(CheckerReturn.VALID, checker.getResult()); 69 | assertTrue(checker.getErrors().isEmpty()); 70 | } 71 | 72 | @Test 73 | public void testExceedanceLowerBound() { 74 | //// Test values 75 | int numberOfEntries = CHECKER_ENTRY_MAX + 1; 76 | 77 | //// Entry generator 78 | Entry[] entries = new Entry[numberOfEntries]; 79 | for (int i = 0; i < numberOfEntries; i++) { 80 | TimeSpan start = new TimeSpan(0, 0); 81 | TimeSpan end = new TimeSpan(0, 0); 82 | TimeSpan pause = new TimeSpan(0, 0); 83 | 84 | Entry entry = new Entry("Test", LocalDate.of(2019, 11, 22), start, end, pause, false); 85 | entries[i] = entry; 86 | } 87 | 88 | //// Checker initialization 89 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 90 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 91 | 92 | //// Execution 93 | checker.checkRowNumExceedance(); 94 | 95 | //// Assertions 96 | assertEquals(numberOfEntries, timeSheet.getEntries().size()); 97 | assertEquals(CheckerReturn.INVALID, checker.getResult()); 98 | assertTrue(checker.getErrors().stream().anyMatch(item -> item.getErrorMessage().equals( 99 | MiLoGChecker.MiLoGCheckerErrorMessageProvider.ROWNUM_EXCEEDENCE.getErrorMessage(CHECKER_ENTRY_MAX, numberOfEntries - CHECKER_ENTRY_MAX)))); 100 | } 101 | 102 | @Test 103 | public void testExceedanceRandom() { 104 | //// Random 105 | Random rand = new Random(); 106 | 107 | //// Test values 108 | int numberOfEntries = rand.nextInt(RANDOM_ENTRY_BOUND) + 1; // May not be zero! 109 | 110 | //// Entry generator 111 | Entry[] entries = new Entry[numberOfEntries]; 112 | for (int i = 0; i < numberOfEntries; i++) { 113 | TimeSpan start = new TimeSpan(0, 0); 114 | TimeSpan end = new TimeSpan(0, 0); 115 | TimeSpan pause = new TimeSpan(0, 0); 116 | 117 | Entry entry = new Entry("Test", LocalDate.of(2019, 11, 22), start, end, pause, false); 118 | entries[i] = entry; 119 | } 120 | 121 | //// Checker initialization 122 | TimeSheet timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, zeroTs, zeroTs); 123 | MiLoGChecker checker = new MiLoGChecker(timeSheet); 124 | 125 | //// Execution 126 | checker.checkRowNumExceedance(); 127 | 128 | //// Assertions 129 | assertEquals(numberOfEntries, timeSheet.getEntries().size()); 130 | if (timeSheet.getEntries().size() > MiLoGChecker.getMaxEntries()) { 131 | assertEquals(CheckerReturn.INVALID, checker.getResult()); 132 | assertTrue(checker.getErrors().stream().anyMatch(item -> item.getErrorMessage().equals( 133 | MiLoGChecker.MiLoGCheckerErrorMessageProvider.ROWNUM_EXCEEDENCE.getErrorMessage(CHECKER_ENTRY_MAX, numberOfEntries - CHECKER_ENTRY_MAX)))); 134 | } else { 135 | assertEquals(CheckerReturn.VALID, checker.getResult()); 136 | assertTrue(checker.getErrors().isEmpty()); 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/checker/holiday/GermanyHolidayCheckerTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.Random; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | public class GermanyHolidayCheckerTest { 14 | 15 | // Exclusively. Refer to 16 | // https://docs.oracle.com/javase/8/docs/api/java/util/Random.html 17 | private static final int RANDOM_YEAR_BOUND = 200; 18 | private static final int RANDOM_MONTH_BOUND = 11; 19 | private static final int RANDOM_DAY_BOUND = 28; 20 | private static final int MULTIPLE_TEST_ITERATIONS = 3650; 21 | 22 | @Test 23 | public void testChristmas2024() throws HolidayFetchException { 24 | //// Test values 25 | LocalDate localDate = LocalDate.of(2024, 12, 25); 26 | GermanState state = GermanState.BW; 27 | 28 | //// HolidayChecker initialization 29 | IHolidayChecker holidayChecker = new GermanyHolidayChecker(localDate.getYear(), state); 30 | 31 | //// Assertions 32 | assertTrue(holidayChecker.isHoliday(localDate)); 33 | } 34 | 35 | @Test 36 | public void testNewYearsDay2024() throws HolidayFetchException { 37 | //// Test values 38 | LocalDate localDate = LocalDate.of(2024, 1, 1); 39 | GermanState state = GermanState.BW; 40 | 41 | //// HolidayChecker initialization 42 | IHolidayChecker holidayChecker = new GermanyHolidayChecker(localDate.getYear(), state); 43 | 44 | //// Assertions 45 | assertTrue(holidayChecker.isHoliday(localDate)); 46 | } 47 | 48 | @Test 49 | public void testNewYearsDay2100() throws HolidayFetchException { 50 | //// Test values 51 | LocalDate localDate = LocalDate.of(2100, 1, 1); 52 | GermanState state = GermanState.BW; 53 | 54 | //// HolidayChecker initialization 55 | IHolidayChecker holidayChecker = new GermanyHolidayChecker(localDate.getYear(), state); 56 | 57 | //// Assertions 58 | assertTrue(holidayChecker.isHoliday(localDate)); 59 | } 60 | 61 | @Test 62 | public void testIsHolidayRandom() throws HolidayFetchException { 63 | // Random 64 | Random rand = new Random(); 65 | 66 | //// Test values 67 | GermanState state = GermanState.BW; 68 | int year = 2024; 69 | IHolidayChecker holidayChecker = new GermanyHolidayChecker(year, state); 70 | 71 | for (int i = 0; i < MULTIPLE_TEST_ITERATIONS; i++) { 72 | int month = rand.nextInt(RANDOM_MONTH_BOUND) + 1; 73 | int day = rand.nextInt(RANDOM_DAY_BOUND) + 1; 74 | LocalDate localDate = LocalDate.of(year, month, day); 75 | 76 | //// HolidayChecker initialization 77 | Collection holidays = holidayChecker.getHolidays(); 78 | Collection holidayDates = new ArrayList(); 79 | for (Holiday holiday : holidays) { 80 | holidayDates.add(holiday.getDate()); 81 | } 82 | 83 | //// Assertions 84 | assertFalse(holidays.isEmpty()); 85 | assertEquals(holidayDates.contains(localDate), holidayChecker.isHoliday(localDate)); 86 | } 87 | } 88 | 89 | @Test 90 | public void testIsHolidayHolidays() throws HolidayFetchException { 91 | // Random 92 | Random rand = new Random(); 93 | 94 | //// Test values 95 | GermanState state = GermanState.BW; 96 | int year = 2024; 97 | IHolidayChecker holidayChecker = new GermanyHolidayChecker(year, state); 98 | 99 | //// HolidayChecker initialization 100 | Collection holidays = holidayChecker.getHolidays(); 101 | Collection holidayDates = new ArrayList(); 102 | for (Holiday holiday : holidays) { 103 | holidayDates.add(holiday.getDate()); 104 | } 105 | 106 | for (LocalDate holidayDate : holidayDates) { 107 | //// Assertions 108 | assertFalse(holidays.isEmpty()); 109 | assertTrue(holidayChecker.isHoliday(holidayDate)); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/checker/holiday/HolidayEqualsDateTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package checker.holiday; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | import java.util.Random; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class HolidayEqualsDateTest { 12 | 13 | private static final int RANDOM_YEAR_BOUND = 200; 14 | private static final int RANDOM_MONTH_BOUND = 11; 15 | private static final int RANDOM_DAY_BOUND = 28; 16 | private static final int MULTIPLE_TEST_ITERATIONS = 10000; 17 | 18 | @Test 19 | public void testEqualDates() { 20 | //// Test values 21 | LocalDate localDate = LocalDate.of(2019, 11, 28); 22 | LocalDate holidayDate = LocalDate.of(2019, 11, 28); 23 | 24 | //// Holiday initialization 25 | Holiday holiday = new Holiday(holidayDate, ""); 26 | 27 | //// Assertions 28 | assertTrue(holiday.equalsDate(localDate)); 29 | } 30 | 31 | @Test 32 | public void testNotEqualDates() { 33 | //// Test values 34 | LocalDate localDate = LocalDate.of(2019, 11, 29); 35 | LocalDate holidayDate = LocalDate.of(2019, 11, 28); 36 | 37 | //// Holiday initialization 38 | Holiday holiday = new Holiday(holidayDate, ""); 39 | 40 | //// Assertions 41 | assertFalse(holiday.equalsDate(localDate)); 42 | } 43 | 44 | @Test 45 | public void testFixpointRandom() { 46 | //// Random 47 | Random rand = new Random(); 48 | 49 | //// Test values 50 | int year = rand.nextInt(RANDOM_YEAR_BOUND) + 1950; 51 | int fixMonth = rand.nextInt(RANDOM_MONTH_BOUND) + 1; 52 | int fixDay = rand.nextInt(RANDOM_DAY_BOUND) + 1; 53 | LocalDate fixDate = LocalDate.of(year, fixMonth, fixDay); 54 | 55 | for (int i = 0; i < MULTIPLE_TEST_ITERATIONS; i++) { 56 | int month = rand.nextInt(RANDOM_MONTH_BOUND) + 1; 57 | int day = rand.nextInt(RANDOM_DAY_BOUND) + 1; 58 | LocalDate dynDate = LocalDate.of(year, month, day); 59 | 60 | //// Holiday initialization 61 | Holiday holiday = new Holiday(dynDate, ""); 62 | 63 | //// Assertions 64 | assertEquals(fixDate.equals(dynDate), holiday.equalsDate(fixDate)); 65 | } 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/data/EntryArithmeticTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class EntryArithmeticTest { 11 | 12 | private static final LocalDate DATE_NOW = LocalDate.now(); 13 | 14 | @Test 15 | public void testGetWorkingTime1() { 16 | TimeSpan start = new TimeSpan(14, 0); 17 | TimeSpan end = new TimeSpan(18, 0); 18 | TimeSpan pause = new TimeSpan(0, 30); 19 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 20 | 21 | TimeSpan workingTime = entry.getWorkingTime(); 22 | assertEquals(workingTime.getHour(), 3); 23 | assertEquals(workingTime.getMinute(), 30); 24 | } 25 | 26 | @Test 27 | public void testGetWorkingTime2() { 28 | TimeSpan start = new TimeSpan(17, 0); 29 | TimeSpan end = new TimeSpan(20, 30); 30 | TimeSpan pause = new TimeSpan(0, 0); 31 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 32 | 33 | TimeSpan workingTime = entry.getWorkingTime(); 34 | assertEquals(workingTime.getHour(), 3); 35 | assertEquals(workingTime.getMinute(), 30); 36 | } 37 | 38 | @Test 39 | public void testGetWorkingTime3() { 40 | TimeSpan start = new TimeSpan(12, 30); 41 | TimeSpan end = new TimeSpan(21, 0); 42 | TimeSpan pause = new TimeSpan(0, 30); 43 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 44 | 45 | TimeSpan workingTime = entry.getWorkingTime(); 46 | assertEquals(workingTime.getHour(), 8); 47 | assertEquals(workingTime.getMinute(), 0); 48 | } 49 | 50 | @Test 51 | public void testGetWorkingTime4() { 52 | TimeSpan start = new TimeSpan(13, 0); 53 | TimeSpan end = new TimeSpan(21, 25); 54 | TimeSpan pause = new TimeSpan(0, 30); 55 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 56 | 57 | TimeSpan workingTime = entry.getWorkingTime(); 58 | assertEquals(workingTime.getHour(), 7); 59 | assertEquals(workingTime.getMinute(), 55); 60 | } 61 | 62 | @Test 63 | public void testGetWorkingTime5() { 64 | TimeSpan start = new TimeSpan(19, 30); 65 | TimeSpan end = new TimeSpan(20, 0); 66 | TimeSpan pause = new TimeSpan(0, 0); 67 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 68 | 69 | TimeSpan workingTime = entry.getWorkingTime(); 70 | assertEquals(workingTime.getHour(), 0); 71 | assertEquals(workingTime.getMinute(), 30); 72 | } 73 | 74 | @Test 75 | public void testGetWorkingTime6() { 76 | TimeSpan start = new TimeSpan(13, 0); 77 | TimeSpan end = new TimeSpan(23, 0); 78 | TimeSpan pause = new TimeSpan(5, 0); 79 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, false); 80 | 81 | TimeSpan workingTime = entry.getWorkingTime(); 82 | assertEquals(workingTime.getHour(), 5); 83 | assertEquals(workingTime.getMinute(), 0); 84 | } 85 | 86 | @Test 87 | public void testGetWorkingTimeVacation() { 88 | TimeSpan start = new TimeSpan(9, 0); 89 | TimeSpan end = new TimeSpan(12, 0); 90 | TimeSpan pause = new TimeSpan(0, 0); 91 | Entry entry = new Entry("Test", DATE_NOW, start, end, pause, true); 92 | 93 | TimeSpan workingTime = entry.getWorkingTime(); 94 | assertEquals(workingTime.getHour(), 3); 95 | assertEquals(workingTime.getMinute(), 0); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/data/EntryCommonTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class EntryCommonTest { 12 | 13 | @Test 14 | public void testConstructor() { 15 | String action = "Test"; 16 | TimeSpan start = new TimeSpan(14, 0); 17 | TimeSpan end = new TimeSpan(18, 0); 18 | TimeSpan pause = new TimeSpan(0, 30); 19 | 20 | LocalDate date = LocalDate.of(2019, 11, 16); 21 | 22 | Entry entry = new Entry(action, date, start, end, pause, false); 23 | 24 | TimeSpan workingTime = entry.getWorkingTime(); 25 | assertEquals(workingTime.getHour(), 3); 26 | assertEquals(workingTime.getMinute(), 30); 27 | 28 | assertEquals(entry.getAction(), action); 29 | assertEquals(entry.getDate(), date); 30 | assertEquals(entry.getStart().compareTo(start), 0); 31 | assertEquals(entry.getEnd().compareTo(end), 0); 32 | assertEquals(entry.getPause().compareTo(pause), 0); 33 | } 34 | 35 | @Test 36 | public void testConstructorIllegalArgument1() { 37 | String action = "Test"; 38 | TimeSpan start = new TimeSpan(14, 0); 39 | TimeSpan end = new TimeSpan(42, 0); 40 | TimeSpan pause = new TimeSpan(0, 30); 41 | 42 | LocalDate date = LocalDate.of(2019, 11, 16); 43 | 44 | Assertions.assertThrows(IllegalArgumentException.class, () -> new Entry(action, date, start, end, pause, false)); 45 | } 46 | 47 | @Test 48 | public void testConstructorIllegalArgument2() { 49 | String action = "Test"; 50 | TimeSpan start = new TimeSpan(23, 59); 51 | TimeSpan end = new TimeSpan(24, 0); 52 | TimeSpan pause = new TimeSpan(0, 30); 53 | 54 | LocalDate date = LocalDate.of(2019, 11, 16); 55 | 56 | Assertions.assertThrows(IllegalArgumentException.class, () -> new Entry(action, date, start, end, pause, false)); 57 | } 58 | 59 | @Test 60 | public void testConstructorIllegalArgument3() { 61 | String action = "Test"; 62 | TimeSpan start = new TimeSpan(23, 00); 63 | TimeSpan end = new TimeSpan(22, 0); 64 | TimeSpan pause = new TimeSpan(0, 30); 65 | 66 | LocalDate date = LocalDate.of(2019, 11, 16); 67 | 68 | Assertions.assertThrows(IllegalArgumentException.class, () -> new Entry(action, date, start, end, pause, false)); 69 | } 70 | 71 | @Test 72 | public void testConstructorIllegalArgument4() { 73 | String action = "Test"; 74 | TimeSpan start = new TimeSpan(25, 00); 75 | TimeSpan end = new TimeSpan(26, 0); 76 | TimeSpan pause = new TimeSpan(0, 30); 77 | 78 | LocalDate date = LocalDate.of(2019, 11, 16); 79 | 80 | Assertions.assertThrows(IllegalArgumentException.class, () -> new Entry(action, date, start, end, pause, false)); 81 | } 82 | 83 | @Test 84 | public void testConstructorIllegalArgumentPauseAndVacation() { 85 | String action = "Test"; 86 | TimeSpan start = new TimeSpan(14, 0); 87 | TimeSpan end = new TimeSpan(18, 0); 88 | TimeSpan pause = new TimeSpan(0, 30); 89 | 90 | LocalDate date = LocalDate.of(2019, 11, 16); 91 | 92 | Assertions.assertThrows(IllegalArgumentException.class, () -> new Entry(action, date, start, end, pause, true)); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/data/EntryCompareToTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | public class EntryCompareToTest { 12 | 13 | @Test 14 | public void testCompareToSmallerDate() { 15 | String action0 = "Test"; 16 | TimeSpan start0 = new TimeSpan(14, 0); 17 | TimeSpan end0 = new TimeSpan(18, 0); 18 | TimeSpan pause0 = new TimeSpan(0, 30); 19 | LocalDate date0 = LocalDate.of(2019, 11, 16); 20 | 21 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 22 | 23 | String action1 = "Test"; 24 | TimeSpan start1 = new TimeSpan(14, 0); 25 | TimeSpan end1 = new TimeSpan(18, 0); 26 | TimeSpan pause1 = new TimeSpan(0, 30); 27 | LocalDate date1 = LocalDate.of(2019, 11, 17); 28 | 29 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 30 | 31 | assertTrue(entry0.compareTo(entry1) < 0); 32 | } 33 | 34 | @Test 35 | public void testCompareToGreaterDate() { 36 | String action0 = "Test"; 37 | TimeSpan start0 = new TimeSpan(14, 0); 38 | TimeSpan end0 = new TimeSpan(18, 0); 39 | TimeSpan pause0 = new TimeSpan(0, 30); 40 | LocalDate date0 = LocalDate.of(2019, 11, 16); 41 | 42 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 43 | 44 | String action1 = "Test"; 45 | TimeSpan start1 = new TimeSpan(14, 0); 46 | TimeSpan end1 = new TimeSpan(18, 0); 47 | TimeSpan pause1 = new TimeSpan(0, 30); 48 | LocalDate date1 = LocalDate.of(2019, 11, 15); 49 | 50 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 51 | 52 | assertTrue(entry0.compareTo(entry1) > 0); 53 | } 54 | 55 | @Test 56 | public void testCompareToSmallerHour() { 57 | String action0 = "Test"; 58 | TimeSpan start0 = new TimeSpan(14, 0); 59 | TimeSpan end0 = new TimeSpan(18, 0); 60 | TimeSpan pause0 = new TimeSpan(0, 30); 61 | LocalDate date0 = LocalDate.of(2019, 11, 16); 62 | 63 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 64 | 65 | String action1 = "Test"; 66 | TimeSpan start1 = new TimeSpan(15, 0); 67 | TimeSpan end1 = new TimeSpan(18, 0); 68 | TimeSpan pause1 = new TimeSpan(0, 30); 69 | LocalDate date1 = LocalDate.of(2019, 11, 16); 70 | 71 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 72 | 73 | assertTrue(entry0.compareTo(entry1) < 0); 74 | } 75 | 76 | @Test 77 | public void testCompareToGreaterHour() { 78 | String action0 = "Test"; 79 | TimeSpan start0 = new TimeSpan(14, 0); 80 | TimeSpan end0 = new TimeSpan(18, 0); 81 | TimeSpan pause0 = new TimeSpan(0, 30); 82 | LocalDate date0 = LocalDate.of(2019, 11, 16); 83 | 84 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 85 | 86 | String action1 = "Test"; 87 | TimeSpan start1 = new TimeSpan(13, 0); 88 | TimeSpan end1 = new TimeSpan(18, 0); 89 | TimeSpan pause1 = new TimeSpan(0, 30); 90 | LocalDate date1 = LocalDate.of(2019, 11, 16); 91 | 92 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 93 | 94 | assertTrue(entry0.compareTo(entry1) > 0); 95 | } 96 | 97 | @Test 98 | public void testCompareToSmallerMinute() { 99 | String action0 = "Test"; 100 | TimeSpan start0 = new TimeSpan(14, 0); 101 | TimeSpan end0 = new TimeSpan(18, 0); 102 | TimeSpan pause0 = new TimeSpan(0, 30); 103 | LocalDate date0 = LocalDate.of(2019, 11, 16); 104 | 105 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 106 | 107 | String action1 = "Test"; 108 | TimeSpan start1 = new TimeSpan(14, 10); 109 | TimeSpan end1 = new TimeSpan(18, 0); 110 | TimeSpan pause1 = new TimeSpan(0, 30); 111 | LocalDate date1 = LocalDate.of(2019, 11, 16); 112 | 113 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 114 | 115 | assertTrue(entry0.compareTo(entry1) < 0); 116 | } 117 | 118 | @Test 119 | public void testCompareToGreaterMinute() { 120 | String action0 = "Test"; 121 | TimeSpan start0 = new TimeSpan(14, 10); 122 | TimeSpan end0 = new TimeSpan(18, 0); 123 | TimeSpan pause0 = new TimeSpan(0, 30); 124 | LocalDate date0 = LocalDate.of(2019, 11, 16); 125 | 126 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 127 | 128 | String action1 = "Test"; 129 | TimeSpan start1 = new TimeSpan(14, 0); 130 | TimeSpan end1 = new TimeSpan(18, 0); 131 | TimeSpan pause1 = new TimeSpan(0, 30); 132 | LocalDate date1 = LocalDate.of(2019, 11, 16); 133 | 134 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 135 | 136 | assertTrue(entry0.compareTo(entry1) > 0); 137 | } 138 | 139 | @Test 140 | public void testCompareToEqual() { 141 | String action0 = "Test"; 142 | TimeSpan start0 = new TimeSpan(14, 0); 143 | TimeSpan end0 = new TimeSpan(18, 0); 144 | TimeSpan pause0 = new TimeSpan(0, 30); 145 | LocalDate date0 = LocalDate.of(2019, 11, 16); 146 | 147 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 148 | 149 | String action1 = "Test"; 150 | TimeSpan start1 = new TimeSpan(14, 0); 151 | TimeSpan end1 = new TimeSpan(18, 0); 152 | TimeSpan pause1 = new TimeSpan(0, 30); 153 | LocalDate date1 = LocalDate.of(2019, 11, 16); 154 | 155 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 156 | 157 | assertEquals(0, entry0.compareTo(entry1)); 158 | } 159 | 160 | @Test 161 | public void testCompareToEqualDateHourMinute() { 162 | String action0 = "Test 1"; 163 | TimeSpan start0 = new TimeSpan(14, 0); 164 | TimeSpan end0 = new TimeSpan(19, 0); 165 | TimeSpan pause0 = new TimeSpan(0, 30); 166 | LocalDate date0 = LocalDate.of(2019, 11, 16); 167 | 168 | Entry entry0 = new Entry(action0, date0, start0, end0, pause0, false); 169 | 170 | String action1 = "Test 2"; 171 | TimeSpan start1 = new TimeSpan(14, 0); 172 | TimeSpan end1 = new TimeSpan(18, 0); 173 | TimeSpan pause1 = new TimeSpan(0, 15); 174 | LocalDate date1 = LocalDate.of(2019, 11, 16); 175 | 176 | Entry entry1 = new Entry(action1, date1, start1, end1, pause1, false); 177 | 178 | assertEquals(0, entry0.compareTo(entry1)); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/test/java/data/TimeSheetArithmeticTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | import java.time.Month; 8 | import java.time.YearMonth; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | public class TimeSheetArithmeticTest { 13 | 14 | @Test 15 | public void testGetTotalWorkTime() { 16 | Employee employee = new Employee("Moritz Gstür", 1234567); 17 | Profession profession = new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31); 18 | TimeSpan zeroTs = new TimeSpan(0, 0); 19 | Entry[] entries = new Entry[7]; 20 | entries[0] = new Entry("Test1", LocalDate.now(), new TimeSpan(10, 0), new TimeSpan(18, 30), new TimeSpan(3, 15), false); 21 | entries[1] = new Entry("Test2", LocalDate.now(), new TimeSpan(12, 0), new TimeSpan(16, 30), new TimeSpan(0, 0), false); 22 | entries[2] = new Entry("Test3", LocalDate.now(), new TimeSpan(10, 0), new TimeSpan(12, 0), new TimeSpan(0, 15), false); 23 | entries[3] = new Entry("Test4", LocalDate.now(), new TimeSpan(15, 0), new TimeSpan(16, 30), new TimeSpan(0, 0), false); 24 | entries[4] = new Entry("Test5", LocalDate.now(), new TimeSpan(20, 0), new TimeSpan(20, 30), new TimeSpan(0, 0), false); 25 | entries[5] = new Entry("Test6", LocalDate.now(), new TimeSpan(9, 30), new TimeSpan(18, 30), new TimeSpan(1, 0), false); 26 | entries[6] = new Entry("Test7", LocalDate.now(), new TimeSpan(9, 0), new TimeSpan(12, 0), new TimeSpan(0, 0), true); 27 | TimeSheet timeSheet = new TimeSheet(employee, profession, YearMonth.of(2019, Month.NOVEMBER), entries, zeroTs, zeroTs); 28 | 29 | assertEquals(timeSheet.getTotalWorkTime(), new TimeSpan(21, 30)); 30 | } 31 | 32 | @Test 33 | public void testGetTotalVacationTime() { 34 | Employee employee = new Employee("Moritz Gstür", 1234567); 35 | Profession profession = new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31); 36 | TimeSpan zeroTs = new TimeSpan(0, 0); 37 | Entry[] entries = new Entry[4]; 38 | entries[0] = new Entry("Test1", LocalDate.now(), new TimeSpan(10, 0), new TimeSpan(18, 30), new TimeSpan(3, 15), false); 39 | entries[1] = new Entry("Test2", LocalDate.now(), new TimeSpan(12, 0), new TimeSpan(16, 30), new TimeSpan(0, 0), true); 40 | entries[2] = new Entry("Test3", LocalDate.now(), new TimeSpan(10, 0), new TimeSpan(12, 0), new TimeSpan(0, 0), true); 41 | entries[3] = new Entry("Test4", LocalDate.now(), new TimeSpan(15, 0), new TimeSpan(16, 30), new TimeSpan(0, 0), true); 42 | TimeSheet timeSheet = new TimeSheet(employee, profession, YearMonth.of(2019, Month.NOVEMBER), entries, zeroTs, zeroTs); 43 | 44 | assertEquals(timeSheet.getTotalVacationTime(), new TimeSpan(8, 0)); 45 | } 46 | 47 | // TODO Create random tests 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/data/TimeSheetCommonTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | import java.time.YearMonth; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | public class TimeSheetCommonTest { 13 | 14 | @Test 15 | public void testInvalidTransferVacation() { 16 | //// Test values 17 | TimeSpan maxWorkingTime = new TimeSpan(40, 0); 18 | TimeSpan predTransfer = new TimeSpan(60, 0); 19 | TimeSpan succTransfer = new TimeSpan(20, 0); 20 | 21 | //// TimeSheet initialization 22 | Employee employee = new Employee("Max Mustermann", 1234567); 23 | Profession profession = new Profession("IPD", WorkingArea.UB, maxWorkingTime, 10.31); 24 | YearMonth yearMonth = YearMonth.of(2019, 11); 25 | Entry[] entries = new Entry[] { new Entry("Vacation", LocalDate.of(2019, 11, 29), new TimeSpan(10, 0), new TimeSpan(20, 0), new TimeSpan(0, 0), true) }; 26 | Assertions.assertThrows(IllegalArgumentException.class, () -> new TimeSheet(employee, profession, yearMonth, entries, succTransfer, predTransfer)); 27 | } 28 | 29 | @Test 30 | public void testValidTransferVacationUpperBound() { 31 | //// Test values 32 | TimeSpan maxWorkingTime = new TimeSpan(40, 0); 33 | TimeSpan predTransfer = new TimeSpan(50, 0); 34 | TimeSpan succTransfer = new TimeSpan(20, 0); 35 | 36 | //// TimeSheet initialization 37 | Employee employee = new Employee("Max Mustermann", 1234567); 38 | Profession profession = new Profession("IPD", WorkingArea.UB, maxWorkingTime, 10.31); 39 | YearMonth yearMonth = YearMonth.of(2019, 11); 40 | Entry[] entries = new Entry[] { new Entry("Vacation", LocalDate.of(2019, 11, 29), new TimeSpan(10, 0), new TimeSpan(20, 0), new TimeSpan(0, 0), true) }; 41 | TimeSheet timeSheet = new TimeSheet(employee, profession, yearMonth, entries, succTransfer, predTransfer); 42 | 43 | //// Assertions 44 | assertNotNull(timeSheet); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/data/TimeSpanArithmeticTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class TimeSpanArithmeticTest { 10 | 11 | @Test 12 | public void testAddWithoutCarry1() { 13 | TimeSpan ts1 = new TimeSpan(0, 20); 14 | TimeSpan ts2 = new TimeSpan(0, 5); 15 | 16 | ts1 = ts1.add(ts2); 17 | assertEquals(ts1.getHour(), 0); 18 | assertEquals(ts1.getMinute(), 25); 19 | } 20 | 21 | @Test 22 | public void testAddWithoutCarry2() { 23 | TimeSpan ts1 = new TimeSpan(0, 0); 24 | TimeSpan ts2 = new TimeSpan(0, 37); 25 | 26 | ts1 = ts1.add(ts2); 27 | assertEquals(ts1.getHour(), 0); 28 | assertEquals(ts1.getMinute(), 37); 29 | } 30 | 31 | @Test 32 | public void testAddWithoutCarry3() { 33 | TimeSpan ts1 = new TimeSpan(0, 0); 34 | TimeSpan ts2 = new TimeSpan(0, 0); 35 | 36 | ts1 = ts1.add(ts2); 37 | assertEquals(ts1.getHour(), 0); 38 | assertEquals(ts1.getMinute(), 0); 39 | } 40 | 41 | @Test 42 | public void testAddWithCarry1() { 43 | TimeSpan ts1 = new TimeSpan(11, 26); 44 | TimeSpan ts2 = new TimeSpan(2, 56); 45 | 46 | ts1 = ts1.add(ts2); 47 | assertEquals(ts1.getHour(), 14); 48 | assertEquals(ts1.getMinute(), 22); 49 | } 50 | 51 | @Test 52 | public void testAddWithCarry2() { 53 | TimeSpan ts1 = new TimeSpan(0, 56); 54 | TimeSpan ts2 = new TimeSpan(0, 5); 55 | 56 | ts1 = ts1.add(ts2); 57 | assertEquals(ts1.getHour(), 1); 58 | assertEquals(ts1.getMinute(), 1); 59 | } 60 | 61 | @Test 62 | public void testAddWithCarry3() { 63 | TimeSpan ts1 = new TimeSpan(0, 59); 64 | TimeSpan ts2 = new TimeSpan(0, 1); 65 | 66 | ts1 = ts1.add(ts2); 67 | assertEquals(ts1.getHour(), 1); 68 | assertEquals(ts1.getMinute(), 0); 69 | } 70 | 71 | @Test 72 | public void testSubtractWithoutCarry1() { 73 | TimeSpan ts1 = new TimeSpan(0, 59); 74 | TimeSpan ts2 = new TimeSpan(0, 1); 75 | 76 | ts1 = ts1.subtract(ts2); 77 | assertEquals(ts1.getHour(), 0); 78 | assertEquals(ts1.getMinute(), 58); 79 | } 80 | 81 | @Test 82 | public void testSubtractWithoutCarry2() { 83 | TimeSpan ts1 = new TimeSpan(17, 22); 84 | TimeSpan ts2 = new TimeSpan(12, 16); 85 | 86 | ts1 = ts1.subtract(ts2); 87 | assertEquals(ts1.getHour(), 5); 88 | assertEquals(ts1.getMinute(), 6); 89 | } 90 | 91 | @Test 92 | public void testSubtractWithoutCarry3() { 93 | TimeSpan ts1 = new TimeSpan(57, 2); 94 | TimeSpan ts2 = new TimeSpan(2, 2); 95 | 96 | ts1 = ts1.subtract(ts2); 97 | assertEquals(ts1.getHour(), 55); 98 | assertEquals(ts1.getMinute(), 0); 99 | } 100 | 101 | @Test 102 | public void testSubtractWithoutCarry4() { 103 | TimeSpan ts1 = new TimeSpan(16, 5); 104 | TimeSpan ts2 = new TimeSpan(16, 5); 105 | 106 | ts1 = ts1.subtract(ts2); 107 | assertEquals(ts1.getHour(), 0); 108 | assertEquals(ts1.getMinute(), 0); 109 | } 110 | 111 | @Test 112 | public void testSubtractWithCarry1() { 113 | TimeSpan ts1 = new TimeSpan(16, 5); 114 | TimeSpan ts2 = new TimeSpan(15, 6); 115 | 116 | ts1 = ts1.subtract(ts2); 117 | assertEquals(ts1.getHour(), 0); 118 | assertEquals(ts1.getMinute(), 59); 119 | } 120 | 121 | @Test 122 | public void testSubtractWithCarry2() { 123 | TimeSpan ts1 = new TimeSpan(11, 26); 124 | TimeSpan ts2 = new TimeSpan(2, 56); 125 | 126 | ts1 = ts1.subtract(ts2); 127 | assertEquals(ts1.getHour(), 8); 128 | assertEquals(ts1.getMinute(), 30); 129 | } 130 | 131 | @Test 132 | public void testSubtractWithCarry3() { 133 | TimeSpan ts1 = new TimeSpan(5, 0); 134 | TimeSpan ts2 = new TimeSpan(4, 59); 135 | 136 | ts1 = ts1.subtract(ts2); 137 | assertEquals(ts1.getHour(), 0); 138 | assertEquals(ts1.getMinute(), 1); 139 | } 140 | 141 | @Test 142 | public void testSubtractWithCarry4() { 143 | TimeSpan ts1 = new TimeSpan(40, 58); 144 | TimeSpan ts2 = new TimeSpan(30, 59); 145 | 146 | ts1 = ts1.subtract(ts2); 147 | assertEquals(ts1.getHour(), 9); 148 | assertEquals(ts1.getMinute(), 59); 149 | } 150 | 151 | @Test 152 | public void testSubtractIllegalArgument1() { 153 | TimeSpan ts1 = new TimeSpan(0, 0); 154 | TimeSpan ts2 = new TimeSpan(2, 7); 155 | 156 | Assertions.assertThrows(IllegalArgumentException.class, () -> ts1.subtract(ts2)); 157 | } 158 | 159 | @Test 160 | public void testSubtractIllegalArgument2() { 161 | TimeSpan ts1 = new TimeSpan(17, 0); 162 | TimeSpan ts2 = new TimeSpan(17, 5); 163 | 164 | Assertions.assertThrows(IllegalArgumentException.class, () -> ts1.subtract(ts2)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/java/data/TimeSpanCommonTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class TimeSpanCommonTest { 10 | 11 | @Test 12 | public void testConstructor1() { 13 | TimeSpan ts = new TimeSpan(22, 17); 14 | 15 | assertEquals(ts.getHour(), 22); 16 | assertEquals(ts.getMinute(), 17); 17 | } 18 | 19 | @Test 20 | public void testConstructor2() { 21 | TimeSpan ts = new TimeSpan(0, 0); 22 | 23 | assertEquals(ts.getHour(), 0); 24 | assertEquals(ts.getMinute(), 0); 25 | } 26 | 27 | @Test 28 | public void testConstructor3() { 29 | Assertions.assertThrows(IllegalArgumentException.class, () -> new TimeSpan(-1, 3)); 30 | } 31 | 32 | @Test 33 | public void testConstructor4() { 34 | Assertions.assertThrows(IllegalArgumentException.class, () -> new TimeSpan(1, -3)); 35 | } 36 | 37 | @Test 38 | public void testConstructor5() { 39 | Assertions.assertThrows(IllegalArgumentException.class, () -> new TimeSpan(2, 60)); 40 | } 41 | 42 | @Test 43 | public void testConstructor6() { 44 | Assertions.assertThrows(IllegalArgumentException.class, () -> new TimeSpan(0, 322)); 45 | } 46 | 47 | @Test 48 | public void testToString1() { 49 | TimeSpan ts = new TimeSpan(0, 0); 50 | 51 | assertEquals(ts.toString(), "00:00"); 52 | } 53 | 54 | @Test 55 | public void testToString2() { 56 | TimeSpan ts = new TimeSpan(2, 0); 57 | 58 | assertEquals(ts.toString(), "02:00"); 59 | } 60 | 61 | @Test 62 | public void testToString3() { 63 | TimeSpan ts = new TimeSpan(0, 4); 64 | 65 | assertEquals(ts.toString(), "00:04"); 66 | } 67 | 68 | @Test 69 | public void testToString4() { 70 | TimeSpan ts = new TimeSpan(20, 9); 71 | 72 | assertEquals(ts.toString(), "20:09"); 73 | } 74 | 75 | @Test 76 | public void testToString5() { 77 | TimeSpan ts = new TimeSpan(7, 36); 78 | 79 | assertEquals(ts.toString(), "07:36"); 80 | } 81 | 82 | @Test 83 | public void testToString6() { 84 | TimeSpan ts = new TimeSpan(56, 13); 85 | 86 | assertEquals(ts.toString(), "56:13"); 87 | } 88 | 89 | @Test 90 | public void testToString7() { 91 | TimeSpan ts = new TimeSpan(102, 7); 92 | 93 | assertEquals(ts.toString(), "102:07"); 94 | } 95 | 96 | @Test 97 | public void testToString8() { 98 | TimeSpan ts = new TimeSpan(1923, 0); 99 | 100 | assertEquals(ts.toString(), "1923:00"); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/data/TimeSpanCompareTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Random; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class TimeSpanCompareTest { 11 | 12 | private static final int RANDOM_HOURS_BOUND = 9999; 13 | 14 | // Exclusively. Refer to 15 | // https://docs.oracle.com/javase/8/docs/api/java/util/Random.html 16 | private static final int RANDOM_MINUTES_BOUND = 60; 17 | 18 | @Test 19 | public void testIdentical1() { 20 | 21 | TimeSpan ts1 = new TimeSpan(0, 0); 22 | TimeSpan ts2 = new TimeSpan(0, 0); 23 | 24 | assertEquals(ts1.compareTo(ts2), 0); 25 | } 26 | 27 | @Test 28 | public void testIdentical2() { 29 | 30 | TimeSpan ts1 = new TimeSpan(26, 33); 31 | TimeSpan ts2 = new TimeSpan(26, 33); 32 | 33 | assertEquals(ts1.compareTo(ts2), 0); 34 | } 35 | 36 | @Test 37 | public void testIdenticalRandom() { 38 | 39 | Random rand = new Random(); 40 | int hours = rand.nextInt(RANDOM_HOURS_BOUND); 41 | int minutes = rand.nextInt(RANDOM_MINUTES_BOUND); 42 | 43 | TimeSpan ts1 = new TimeSpan(hours, minutes); 44 | TimeSpan ts2 = new TimeSpan(hours, minutes); 45 | 46 | assertEquals(ts1.compareTo(ts2), 0); 47 | } 48 | 49 | @Test 50 | public void testRandom() { 51 | 52 | Random rand = new Random(); 53 | int hoursFst = rand.nextInt(RANDOM_HOURS_BOUND); 54 | int minutesFst = rand.nextInt(RANDOM_MINUTES_BOUND); 55 | int hoursSnd = rand.nextInt(RANDOM_HOURS_BOUND); 56 | int minutesSnd = rand.nextInt(RANDOM_MINUTES_BOUND); 57 | 58 | TimeSpan ts1 = new TimeSpan(hoursFst, minutesFst); 59 | TimeSpan ts2 = new TimeSpan(hoursSnd, minutesSnd); 60 | 61 | if (hoursFst > hoursSnd) { 62 | assertEquals(ts1.compareTo(ts2), 1); 63 | } else if (hoursFst == hoursSnd) { 64 | assertEquals(ts1.compareTo(ts2), Integer.compare(minutesFst, minutesSnd)); 65 | } else { 66 | assertEquals(ts1.compareTo(ts2), -1); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/data/WorkingAreaParseTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package data; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class WorkingAreaParseTest { 10 | 11 | @Test 12 | public void testValidUB_1() { 13 | //// Test values 14 | String toParse = "UB"; 15 | 16 | //// WorkingArea initialization 17 | WorkingArea fromString = WorkingArea.parse(toParse); 18 | 19 | //// Assertions 20 | assertEquals(WorkingArea.UB, fromString); 21 | } 22 | 23 | @Test 24 | public void testValidUB_2() { 25 | //// Test values 26 | String toParse = "uB"; 27 | 28 | //// WorkingArea initialization 29 | WorkingArea fromString = WorkingArea.parse(toParse); 30 | 31 | //// Assertions 32 | assertEquals(WorkingArea.UB, fromString); 33 | } 34 | 35 | @Test 36 | public void testValidUB_3() { 37 | //// Test values 38 | String toParse = "Ub"; 39 | 40 | //// WorkingArea initialization 41 | WorkingArea fromString = WorkingArea.parse(toParse); 42 | 43 | //// Assertions 44 | assertEquals(WorkingArea.UB, fromString); 45 | } 46 | 47 | @Test 48 | public void testValidUB_4() { 49 | //// Test values 50 | String toParse = "ub"; 51 | 52 | //// WorkingArea initialization 53 | WorkingArea fromString = WorkingArea.parse(toParse); 54 | 55 | //// Assertions 56 | assertEquals(WorkingArea.UB, fromString); 57 | } 58 | 59 | @Test 60 | public void testValidGF_1() { 61 | //// Test values 62 | String toParse = "GF"; 63 | 64 | //// WorkingArea initialization 65 | WorkingArea fromString = WorkingArea.parse(toParse); 66 | 67 | //// Assertions 68 | assertEquals(WorkingArea.GF, fromString); 69 | } 70 | 71 | @Test 72 | public void testValidGF_2() { 73 | //// Test values 74 | String toParse = "gF"; 75 | 76 | //// WorkingArea initialization 77 | WorkingArea fromString = WorkingArea.parse(toParse); 78 | 79 | //// Assertions 80 | assertEquals(WorkingArea.GF, fromString); 81 | } 82 | 83 | @Test 84 | public void testValidGF_3() { 85 | //// Test values 86 | String toParse = "Gf"; 87 | 88 | //// WorkingArea initialization 89 | WorkingArea fromString = WorkingArea.parse(toParse); 90 | 91 | //// Assertions 92 | assertEquals(WorkingArea.GF, fromString); 93 | } 94 | 95 | @Test 96 | public void testValidGF_4() { 97 | //// Test values 98 | String toParse = "gf"; 99 | 100 | //// WorkingArea initialization 101 | WorkingArea fromString = WorkingArea.parse(toParse); 102 | 103 | //// Assertions 104 | assertEquals(WorkingArea.GF, fromString); 105 | } 106 | 107 | @Test 108 | public void testInvalidSubstring_1() { 109 | //// Test values 110 | String toParse = "gfbutNotCorrect"; 111 | 112 | //// WorkingArea initialization 113 | Assertions.assertThrows(IllegalArgumentException.class, () -> WorkingArea.parse(toParse)); 114 | } 115 | 116 | @Test 117 | public void testInvalidSubstring_2() { 118 | //// Test values 119 | String toParse = "gf "; 120 | 121 | //// WorkingArea initialization 122 | Assertions.assertThrows(IllegalArgumentException.class, () -> WorkingArea.parse(toParse)); 123 | } 124 | 125 | @Test 126 | public void testInvalidSubstring_3() { 127 | //// Test values 128 | String toParse = "gfub"; 129 | 130 | //// WorkingArea initialization 131 | Assertions.assertThrows(IllegalArgumentException.class, () -> WorkingArea.parse(toParse)); 132 | } 133 | 134 | @Test 135 | public void testInvalidEmpty() { 136 | //// Test values 137 | String toParse = ""; 138 | 139 | //// WorkingArea initialization 140 | Assertions.assertThrows(IllegalArgumentException.class, () -> WorkingArea.parse(toParse)); 141 | } 142 | 143 | @Test 144 | public void testInvalidSpace() { 145 | //// Test values 146 | String toParse = " "; 147 | 148 | //// WorkingArea initialization 149 | Assertions.assertThrows(IllegalArgumentException.class, () -> WorkingArea.parse(toParse)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/test/java/i18n/DateFormatWrapperTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package i18n; 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.text.DateFormat; 8 | import java.text.MessageFormat; 9 | import java.text.SimpleDateFormat; 10 | import java.time.LocalDate; 11 | import java.time.LocalDateTime; 12 | import java.time.LocalTime; 13 | import java.util.Calendar; 14 | import java.util.Date; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | public class DateFormatWrapperTest { 19 | 20 | @Test 21 | public void testPassthrough() { 22 | // data 23 | Calendar calendar = Calendar.getInstance(); 24 | calendar.set(2019, Calendar.JULY, 11, 9, 30, 17); 25 | Date date = calendar.getTime(); 26 | 27 | DateFormatWrapper wrapper = new DateFormatWrapper(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")); 28 | // execute 29 | String result = wrapper.format(date); 30 | // assert 31 | assertEquals("2019/07/11 09:30:17", result); 32 | } 33 | 34 | @Test 35 | public void testDateFormatException() { 36 | // data 37 | LocalDateTime date = LocalDateTime.of(2019, 7, 11, 9, 30, 17); 38 | DateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); 39 | // execute 40 | Assertions.assertThrows(IllegalArgumentException.class, () -> format.format(date)); 41 | } 42 | 43 | @Test 44 | public void testMessageFormatUsesDateFormat() { 45 | // execute 46 | MessageFormat messageFormat = new MessageFormat("{0, date}"); 47 | // assert 48 | assertInstanceOf(DateFormat.class, messageFormat.getFormats()[0]); 49 | } 50 | 51 | @Test 52 | public void testLocalDateTime() { 53 | // data 54 | LocalDateTime date = LocalDateTime.of(2019, 7, 11, 9, 30, 17); 55 | DateFormatWrapper wrapper = new DateFormatWrapper(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")); 56 | // execute 57 | String result = wrapper.format(date); 58 | // assert 59 | assertEquals("2019/07/11 09:30:17", result); 60 | } 61 | 62 | @Test 63 | public void testLocalDate() { 64 | // data 65 | LocalDate date = LocalDate.of(2019, 7, 11); 66 | DateFormatWrapper wrapper = new DateFormatWrapper(new SimpleDateFormat("yyyy/MM/dd")); 67 | // execute 68 | String result = wrapper.format(date); 69 | // assert 70 | assertEquals("2019/07/11", result); 71 | } 72 | 73 | @Test 74 | public void testLocalTime() { 75 | // data 76 | LocalTime date = LocalTime.of(9, 30, 17); 77 | DateFormatWrapper wrapper = new DateFormatWrapper(new SimpleDateFormat("HH:mm:ss")); 78 | // execute 79 | String result = wrapper.format(date); 80 | // assert 81 | assertEquals("09:30:17", result); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/i18n/ResourceHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package i18n; 3 | 4 | import i18n.ResourceHandler.ResourceHandlerInstance; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.time.LocalDate; 9 | import java.util.Locale; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class ResourceHandlerTest { 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | resourceHandler = new ResourceHandlerInstance("i18n_test/MessageBundle"); 18 | } 19 | 20 | private ResourceHandlerInstance resourceHandler; 21 | 22 | @Test 23 | public void testChangeLocale() { 24 | // execute 25 | resourceHandler.setLocale(Locale.GERMAN); 26 | // assert 27 | assertEquals(Locale.GERMAN, resourceHandler.getLocale()); 28 | // execute 29 | resourceHandler.setLocale(Locale.ENGLISH); 30 | // assert 31 | assertEquals(Locale.ENGLISH, resourceHandler.getLocale()); 32 | } 33 | 34 | @Test 35 | public void testGetMessage() { 36 | // data 37 | resourceHandler.setLocale(Locale.ROOT); 38 | // execute 39 | String result = resourceHandler.getMessage("test"); 40 | // assert 41 | assertEquals("Hello Fallback!", result); 42 | } 43 | 44 | @Test 45 | public void testGetMessageEN() { 46 | // data 47 | resourceHandler.setLocale(Locale.ENGLISH); 48 | // execute 49 | String result = resourceHandler.getMessage("test"); 50 | // assert 51 | assertEquals("Hello World!", result); 52 | } 53 | 54 | @Test 55 | public void testGetMessageDE() { 56 | // data 57 | resourceHandler.setLocale(Locale.GERMAN); 58 | // execute 59 | String result = resourceHandler.getMessage("test"); 60 | // assert 61 | assertEquals("Hallo Welt!", result); 62 | } 63 | 64 | @Test 65 | public void testGetMessageCountryDE() { 66 | // data 67 | resourceHandler.setLocale(Locale.GERMANY); 68 | // execute 69 | String result = resourceHandler.getMessage("test"); 70 | // assert 71 | assertEquals("Hallo Welt!", result); 72 | } 73 | 74 | @Test 75 | public void testGetMessageFallback() { 76 | // data 77 | resourceHandler.setLocale(Locale.GERMAN); 78 | // execute 79 | String result = resourceHandler.getMessage("fallback"); 80 | // assert 81 | assertEquals("This is the fallback", result); 82 | } 83 | 84 | @Test 85 | public void testGetMessageArgs() { 86 | // data 87 | resourceHandler.setLocale(Locale.ENGLISH); 88 | // execute 89 | String result = resourceHandler.getMessage("args", "here"); 90 | // assert 91 | assertEquals("Insert > here <", result); 92 | } 93 | 94 | @Test 95 | public void testGetMessageArgsMultiple() { 96 | // data 97 | resourceHandler.setLocale(Locale.ENGLISH); 98 | // execute 99 | String result = resourceHandler.getMessage("multiArgs", 1, 2); 100 | // assert 101 | assertEquals("1 + 1 = 2", result); 102 | } 103 | 104 | @Test 105 | public void testGetMessageWithoutFormat() { 106 | // data 107 | resourceHandler.setLocale(Locale.ENGLISH); 108 | LocalDate date = LocalDate.of(2019, 7, 21); 109 | // execute 110 | String result = resourceHandler.getMessage("dateWithoutFormat", date); 111 | // assert 112 | assertEquals("On 2019-07-21", result); 113 | } 114 | 115 | @Test 116 | public void testGetMessageWithFormatLocaleEN() { 117 | // data 118 | resourceHandler.setLocale(Locale.ENGLISH); 119 | LocalDate date = LocalDate.of(2019, 7, 21); 120 | // execute 121 | String result = resourceHandler.getMessage("dateWithFormat", date); 122 | // assert 123 | assertEquals("On Jul 21, 2019", result); 124 | } 125 | 126 | @Test 127 | public void testGetMessageWithFormatLocaleDE() { 128 | // data 129 | resourceHandler.setLocale(Locale.GERMAN); 130 | LocalDate date = LocalDate.of(2019, 7, 21); 131 | // execute 132 | String result = resourceHandler.getMessage("dateWithFormat", date); 133 | // assert 134 | assertEquals("Am 21.07.2019", result); 135 | } 136 | 137 | @Test 138 | public void testGetMessageWithFormatStyleLocalEN() { 139 | // data 140 | resourceHandler.setLocale(Locale.ENGLISH); 141 | LocalDate date = LocalDate.of(2019, 7, 21); 142 | // execute 143 | String result = resourceHandler.getMessage("dateWithLongFormat", date); 144 | // assert 145 | assertEquals("On July 21, 2019", result); 146 | } 147 | 148 | @Test 149 | public void testGetMessageWithFormatStyleLocaleDE() { 150 | // data 151 | resourceHandler.setLocale(Locale.GERMAN); 152 | LocalDate date = LocalDate.of(2019, 7, 21); 153 | // execute 154 | String result = resourceHandler.getMessage("dateWithLongFormat", date); 155 | // assert 156 | assertEquals("Am 21. Juli 2019", result); 157 | } 158 | 159 | @Test 160 | public void testGetMessageWithFormatStyleLocaleFallback() { 161 | // data 162 | resourceHandler.setLocale(Locale.GERMAN); 163 | LocalDate date = LocalDate.of(2019, 7, 21); 164 | // execute 165 | String result = resourceHandler.getMessage("dateWithLongFormatFallback", date); 166 | // assert 167 | assertEquals("Fallback to 21. Juli 2019", result); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/io/LatexGeneratorEscapeTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package io; 3 | 4 | import data.*; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | import java.time.YearMonth; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | public class LatexGeneratorEscapeTest { 13 | 14 | @Test 15 | public void testEscapeWithoutSpecialCharacters() { 16 | // execute 17 | String result = LatexGenerator.escapeText("Hello World"); 18 | // assert 19 | assertEquals("Hello World", result); 20 | } 21 | 22 | @Test 23 | public void testEscapeAllSpecialCharactersWithoutSpace() { 24 | // execute 25 | String result = LatexGenerator.escapeText("&%$#_{}\\~^"); 26 | // assert 27 | assertEquals("\\&\\%\\$\\#\\_\\{\\}\\textbackslash \\textasciitilde \\textasciicircum ", result); 28 | } 29 | 30 | @Test 31 | public void testEscapeAllSpecialCharactersWithSpace() { 32 | // execute 33 | String result = LatexGenerator.escapeText(" & % $ # _ { } \\ ~ ^ "); 34 | // assert 35 | assertEquals(" \\& \\% \\$ \\# \\_ \\{ \\} \\textbackslash\\ \\textasciitilde\\ \\textasciicircum\\ ", result); 36 | } 37 | 38 | @Test 39 | public void testEscapeWithinNormalText() { 40 | // execute 41 | String result = LatexGenerator.escapeText("He__o ~ World"); 42 | // assert 43 | assertEquals("He\\_\\_o \\textasciitilde\\ World", result); 44 | } 45 | 46 | @Test 47 | public void testEscapeTimesheetWithoutSpecialCharacters() { 48 | // data 49 | Employee employee = new Employee("Max Mustermann", 1234567); 50 | Profession profession = new Profession("Institut für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 23.71); 51 | Entry[] entries = new Entry[] { 52 | new Entry("Fragen und Antworten", LocalDate.of(2020, 3, 21), new TimeSpan(9, 0), new TimeSpan(12, 0), new TimeSpan(0, 30), false) }; 53 | TimeSheet timeSheet = new TimeSheet(employee, profession, YearMonth.of(2020, 3), entries, new TimeSpan(0, 0), new TimeSpan(0, 0)); 54 | // execute 55 | LatexGenerator generator = new LatexGenerator(timeSheet, "\\begin !employeeName !department !action \\end"); 56 | String result = generator.generate(); 57 | // assert 58 | assertEquals("\\begin Max Mustermann Institut für Informatik Fragen und Antworten \\end", result); 59 | } 60 | 61 | @Test 62 | public void testEscapeTimesheetWithSpecialCharacters() { 63 | // data 64 | Employee employee = new Employee("Max #Mustermann", 1234567); 65 | Profession profession = new Profession("Institut f~r Informatik", WorkingArea.UB, new TimeSpan(40, 0), 23.71); 66 | Entry[] entries = new Entry[] { 67 | new Entry("Fragen & Antworten", LocalDate.of(2020, 3, 21), new TimeSpan(9, 0), new TimeSpan(12, 0), new TimeSpan(0, 30), false) }; 68 | TimeSheet timeSheet = new TimeSheet(employee, profession, YearMonth.of(2020, 3), entries, new TimeSpan(0, 0), new TimeSpan(0, 0)); 69 | // execute 70 | LatexGenerator generator = new LatexGenerator(timeSheet, "\\begin !employeeName !department !action \\end"); 71 | String result = generator.generate(); 72 | // assert 73 | assertEquals("\\begin Max \\#Mustermann Institut f\\textasciitilde r Informatik Fragen \\& Antworten \\end", result); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/io/LatexGeneratorPlaceholderTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package io; 3 | 4 | import data.*; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.Arguments; 8 | import org.junit.jupiter.params.provider.MethodSource; 9 | 10 | import java.time.LocalDate; 11 | import java.time.Month; 12 | import java.time.YearMonth; 13 | import java.time.format.DateTimeFormatter; 14 | import java.util.stream.Stream; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | 18 | public class LatexGeneratorPlaceholderTest { 19 | 20 | private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yy"); 21 | 22 | private static final Employee EMPLOYEE = new Employee("Max Mustermann", 1234567); 23 | private static final Profession PROFESSION = new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31); 24 | private static final YearMonth YEAR_MONTH = YearMonth.of(2019, Month.NOVEMBER); 25 | 26 | private static TimeSheet timeSheet; 27 | 28 | @BeforeAll 29 | public static void beforeAll() { 30 | Entry entry0 = new Entry("Test Action 1", YEAR_MONTH.atDay(12), new TimeSpan(10, 0), new TimeSpan(14, 0), new TimeSpan(0, 30), false); 31 | Entry entry1 = new Entry("Test Action 2", YEAR_MONTH.atDay(14), new TimeSpan(8, 0), new TimeSpan(12, 0), new TimeSpan(0, 0), false); 32 | Entry entry2 = new Entry("Test Vacation", YEAR_MONTH.atDay(19), new TimeSpan(9, 0), new TimeSpan(10, 0), new TimeSpan(0, 0), true); 33 | Entry entry3 = new Entry("Test 2 Vacation", YEAR_MONTH.atDay(21), new TimeSpan(15, 0), new TimeSpan(16, 30), new TimeSpan(0, 0), true); 34 | Entry[] entries = new Entry[] { entry0, entry1, entry2, entry3 }; 35 | 36 | timeSheet = new TimeSheet(EMPLOYEE, PROFESSION, YEAR_MONTH, entries, new TimeSpan(1, 0), new TimeSpan(2, 0)); 37 | } 38 | 39 | @ParameterizedTest 40 | @MethodSource("getTemplateAndExpected") 41 | public void testGenerate(final String template, final String expected) { 42 | LatexGenerator generator = new LatexGenerator(timeSheet, template); 43 | String latex = generator.generate(); 44 | assertEquals(expected, latex); 45 | } 46 | 47 | private static Stream getTemplateAndExpected() { 48 | return Stream.of(Arguments.of("Year: !year", "Year: 2019"), Arguments.of("Month: !month", "Month: 11"), 49 | Arguments.of("Employee Name: !employeeName", "Employee Name: Max Mustermann"), Arguments.of("Employee ID: !employeeID", "Employee ID: 1234567"), 50 | Arguments.of("GF / UB: !workingArea", "GF / UB: \\textbf{GF:} $\\Box$ \\textbf{UB:} $\\boxtimes$"), 51 | Arguments.of("Department: !department", "Department: Fakultät für Informatik"), Arguments.of("Max Hours: !workingTime", "Max Hours: 40:00"), 52 | Arguments.of("Wage: !wage", "Wage: 10.31"), Arguments.of("Vacation: !vacation", "Vacation: 02:30"), Arguments.of("Hours: !sum", "Hours: 10:00"), 53 | Arguments.of("Transfer Pred: !carryPred", "Transfer Pred: 02:00"), Arguments.of("Transfer Succ: !carrySucc", "Transfer Succ: 01:00"), 54 | Arguments.of("Action 1: !action, Action 2: !action, Action 3: !action, Action 4: !action", 55 | "Action 1: Test Action 1, Action 2: Test Action 2, Action 3: Test Vacation, Action 4: Test 2 Vacation"), 56 | Arguments.of("Date 1: !date, Date 2: !date, Date 3: !date, Date 4: !date", 57 | "Date 1: " + LocalDate.of(2019, 11, 12).format(DATE_TIME_FORMATTER) + ", Date 2: " 58 | + LocalDate.of(2019, 11, 14).format(DATE_TIME_FORMATTER) + ", Date 3: " + LocalDate.of(2019, 11, 19).format(DATE_TIME_FORMATTER) 59 | + ", Date 4: " + LocalDate.of(2019, 11, 21).format(DATE_TIME_FORMATTER)), 60 | Arguments.of("Start 1: !begin, Start 2: !begin, Start 3: !begin, Start 4: !begin", 61 | "Start 1: 10:00, Start 2: 08:00, Start 3: 09:00, Start 4: 15:00"), 62 | Arguments.of("End 1: !end, End 2: !end, End 3: !end, End 4: !end", "End 1: 14:00, End 2: 12:00, End 3: 10:00, End 4: 16:30"), 63 | Arguments.of("Pause 1: !break, Pause 2: !break, Pause 3: !break, Pause 4: !break", 64 | "Pause 1: 00:30, Pause 2: 00:00, Pause 3: 00:00, Pause 4: 00:00"), 65 | Arguments.of("Time 1: !dayTotal, Time 2: !dayTotal, Time 3: !dayTotal, Time 4: !dayTotal", 66 | "Time 1: 03:30, Time 2: 04:00, Time 3: 01:00 U, Time 4: 01:30 U")); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/test/java/parser/ParserJsonTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser; 3 | 4 | import data.*; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.time.LocalDate; 9 | import java.time.YearMonth; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class ParserJsonTest { 14 | 15 | private static final String JSON_EMPTY = "{}"; 16 | private static final String JSON_GLOBAL_EXAMPLE = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," 17 | + "\"department\": \"Fakultät für Informatik\"," + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," + "\"workingArea\": \"ub\"" + "}"; 18 | private static final String JSON_MONTH_EXAMPLE = "{" + "\"year\": 2019," + "\"month\": 11," + "\"pred_transfer\": \"2:00\"," 19 | + "\"succ_transfer\": \"1:00\"," + "\"entries\": [" + "{\"action\": \"Korrektur\", \"day\": 2, \"start\": \"10:00\", \"end\": \"11:00\"}," 20 | + "{\"action\": \"Fragen beantworten\", \"day\": 4, \"start\": \"11:31\", \"end\": \"15:11\", \"pause\": \"00:30\"}," 21 | + "{\"action\": \"Urlaub in Italien\", \"day\": 11, \"start\": \"09:00\", \"end\": \"12:00\", \"vacation\": true}" + "]" + "}"; 22 | 23 | @Test 24 | public void testParseTimeSheetJsonBothJsonEmpty() throws ParseException { 25 | // execute 26 | Assertions.assertThrows(ParseException.class, () -> Parser.parseTimeSheetJson(JSON_EMPTY, JSON_EMPTY)); 27 | } 28 | 29 | @Test 30 | public void testParseTimeSheetJsonGlobalJsonEmpty() throws ParseException { 31 | // execute 32 | Assertions.assertThrows(ParseException.class, () -> Parser.parseTimeSheetJson(JSON_EMPTY, JSON_MONTH_EXAMPLE)); 33 | } 34 | 35 | @Test 36 | public void testParseTimeSheetJsonMonthJsonEmpty() throws ParseException { 37 | // execute 38 | Assertions.assertThrows(ParseException.class, () -> Parser.parseTimeSheetJson(JSON_GLOBAL_EXAMPLE, JSON_EMPTY)); 39 | } 40 | 41 | @Test 42 | public void testParseTimeSheetJson() throws ParseException { 43 | // data 44 | Employee expectedEmployee = new Employee("Max Mustermann", 1234567); 45 | Profession expectedProfession = new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31); 46 | Entry[] expectedEntries = new Entry[] { 47 | new Entry("Korrektur", LocalDate.of(2019, 11, 2), new TimeSpan(10, 0), new TimeSpan(11, 0), new TimeSpan(0, 0), false), 48 | new Entry("Fragen beantworten", LocalDate.of(2019, 11, 4), new TimeSpan(11, 31), new TimeSpan(15, 11), new TimeSpan(0, 30), false), 49 | new Entry("Urlaub in Italien", LocalDate.of(2019, 11, 11), new TimeSpan(9, 0), new TimeSpan(12, 0), new TimeSpan(0, 0), true) }; 50 | TimeSheet expectedTimeSheet = new TimeSheet(expectedEmployee, expectedProfession, YearMonth.of(2019, 11), expectedEntries, new TimeSpan(1, 0), 51 | new TimeSpan(2, 0)); 52 | 53 | // execute 54 | TimeSheet timeSheet = Parser.parseTimeSheetJson(JSON_GLOBAL_EXAMPLE, JSON_MONTH_EXAMPLE); 55 | 56 | // assert 57 | assertEquals(expectedTimeSheet, timeSheet); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/parser/json/JsonGlobalParserTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import data.Employee; 5 | import data.Profession; 6 | import data.TimeSpan; 7 | import data.WorkingArea; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import parser.IGlobalParser; 11 | import parser.ParseException; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertNotNull; 15 | 16 | public class JsonGlobalParserTest { 17 | 18 | private static final String JSON_EMPTY = "{}"; 19 | private static final String JSON_EXAMPLE = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 20 | + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," + "\"workingArea\": \"ub\"" + "}"; 21 | 22 | @Test 23 | public void testCreate() { 24 | IGlobalParser parser = new JsonGlobalParser(JSON_EXAMPLE); 25 | 26 | assertNotNull(parser); 27 | } 28 | 29 | @Test 30 | public void testParseEmployeeEmptyJson() throws ParseException { 31 | // data 32 | IGlobalParser parser = new JsonGlobalParser(JSON_EMPTY); 33 | 34 | // execute 35 | Assertions.assertThrows(ParseException.class, parser::getEmployee); 36 | } 37 | 38 | @Test 39 | public void testParseEmployeeMissingName() throws ParseException { 40 | // data 41 | String json = "{" + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," 42 | + "\"workingArea\": \"ub\"" + "}"; 43 | IGlobalParser parser = new JsonGlobalParser(json); 44 | 45 | // execute 46 | Assertions.assertThrows(ParseException.class, parser::getEmployee); 47 | } 48 | 49 | @Test 50 | public void testParseEmployeeMissingStaffId() throws ParseException { 51 | // data 52 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"department\": \"Fakultät für Informatik\"," + "\"workingTime\": \"40:00\"," 53 | + "\"wage\": 10.31," + "\"workingArea\": \"ub\"" + "}"; 54 | IGlobalParser parser = new JsonGlobalParser(json); 55 | 56 | // execute 57 | Assertions.assertThrows(ParseException.class, parser::getEmployee); 58 | } 59 | 60 | @Test 61 | public void testParseEmployeeAdditionalProperty() throws ParseException { 62 | // data 63 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 64 | + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," + "\"workingArea\": \"ub\"," + "\"something\": \"else\"" + "}"; 65 | IGlobalParser parser = new JsonGlobalParser(json); 66 | 67 | // execute 68 | Assertions.assertThrows(ParseException.class, parser::getEmployee); 69 | } 70 | 71 | @Test 72 | public void testParseEmployee() throws ParseException { 73 | // data 74 | IGlobalParser parser = new JsonGlobalParser(JSON_EXAMPLE); 75 | 76 | // execute 77 | Employee employee = parser.getEmployee(); 78 | 79 | // assert 80 | assertEquals(new Employee("Max Mustermann", 1234567), employee); 81 | } 82 | 83 | @Test 84 | public void testParseProfessionEmptyJson() throws ParseException { 85 | // data 86 | IGlobalParser parser = new JsonGlobalParser(JSON_EMPTY); 87 | 88 | // execute 89 | Assertions.assertThrows(ParseException.class, parser::getProfession); 90 | } 91 | 92 | @Test 93 | public void testParseProfessionMissingDepartment() throws ParseException { 94 | // data 95 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," 96 | + "\"workingArea\": \"ub\"" + "}"; 97 | IGlobalParser parser = new JsonGlobalParser(json); 98 | 99 | // execute 100 | Assertions.assertThrows(ParseException.class, parser::getProfession); 101 | } 102 | 103 | @Test 104 | public void testParseProfessionMissingWorkingTime() throws ParseException { 105 | // data 106 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," + "\"wage\": 10.31," 107 | + "\"workingArea\": \"ub\"" + "}"; 108 | IGlobalParser parser = new JsonGlobalParser(json); 109 | 110 | // execute 111 | Assertions.assertThrows(ParseException.class, parser::getProfession); 112 | } 113 | 114 | @Test 115 | public void testParseProfessionMissingWage() throws ParseException { 116 | // data 117 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 118 | + "\"workingTime\": \"40:00\"," + "\"workingArea\": \"ub\"" + "}"; 119 | IGlobalParser parser = new JsonGlobalParser(json); 120 | 121 | // execute 122 | Assertions.assertThrows(ParseException.class, parser::getProfession); 123 | } 124 | 125 | @Test 126 | public void testParseProfessionMissingWorkingArea() throws ParseException { 127 | // data 128 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 129 | + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31" + "}"; 130 | IGlobalParser parser = new JsonGlobalParser(json); 131 | 132 | // execute 133 | Assertions.assertThrows(ParseException.class, parser::getProfession); 134 | } 135 | 136 | @Test 137 | public void testParseProfessionAdditionalProperty() throws ParseException { 138 | // data 139 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 140 | + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," + "\"workingArea\": \"ub\"," + "\"something\": \"else\"" + "}"; 141 | IGlobalParser parser = new JsonGlobalParser(json); 142 | 143 | // execute 144 | Assertions.assertThrows(ParseException.class, parser::getProfession); 145 | } 146 | 147 | @Test 148 | public void testParseProfessionWrongWorkingTimeFormat() throws ParseException { 149 | // data 150 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 151 | + "\"workingTime\": \"40 Stunden 0 Minuten\"," + "\"wage\": 10.31," + "\"workingArea\": \"ub\"" + "}"; 152 | IGlobalParser parser = new JsonGlobalParser(json); 153 | 154 | // execute 155 | Assertions.assertThrows(ParseException.class, parser::getProfession); 156 | } 157 | 158 | @Test 159 | public void testParseProfessionWrongWorkingAreaFormat() throws ParseException { 160 | // data 161 | String json = "{" + "\"name\": \"Max Mustermann\"," + "\"staffId\": 1234567," + "\"department\": \"Fakultät für Informatik\"," 162 | + "\"workingTime\": \"40:00\"," + "\"wage\": 10.31," + "\"workingArea\": \"Univ.-Be.\"" + "}"; 163 | IGlobalParser parser = new JsonGlobalParser(json); 164 | 165 | // execute 166 | Assertions.assertThrows(ParseException.class, parser::getProfession); 167 | } 168 | 169 | @Test 170 | public void testParseProfession() throws ParseException { 171 | // data 172 | IGlobalParser parser = new JsonGlobalParser(JSON_EXAMPLE); 173 | 174 | // execute 175 | Profession profession = parser.getProfession(); 176 | 177 | // assert 178 | assertEquals(new Profession("Fakultät für Informatik", WorkingArea.UB, new TimeSpan(40, 0), 10.31), profession); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/test/java/parser/json/JsonHolidayParserTest.java: -------------------------------------------------------------------------------- 1 | /* Licensed under MIT 2023-2024. */ 2 | package parser.json; 3 | 4 | import checker.holiday.Holiday; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import parser.IHolidayParser; 8 | import parser.ParseException; 9 | 10 | import java.time.LocalDate; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertNotNull; 16 | 17 | public class JsonHolidayParserTest { 18 | 19 | private static final String JSON_EMPTY = "{}"; 20 | private static final String JSON_EXAMPLE = "{" + "\"1. Weihnachtstag\": {" + "\"datum\": \"2019-12-25\"," + "\"hinweis\": \"Weihnachten\"" 21 | + "}, \"2. Weihnachtstag\": {" + "\"datum\": \"2019-12-26\"" + "}" + "}"; 22 | 23 | @Test 24 | public void testCreate() { 25 | IHolidayParser parser = new JsonHolidayParser(JSON_EXAMPLE); 26 | 27 | assertNotNull(parser); 28 | } 29 | 30 | @Test 31 | public void testGetHolidaysEmptyJson() throws ParseException { 32 | // data 33 | IHolidayParser parser = new JsonHolidayParser(JSON_EMPTY); 34 | 35 | // execute 36 | Collection holidays = parser.getHolidays(); 37 | 38 | // assert 39 | assertEquals(0, holidays.size()); 40 | } 41 | 42 | @Test 43 | public void testGetHolidaysMissingDate() throws ParseException { 44 | // data 45 | String json = "{" + "\"1. Weihnachtstag\": {" + "\"hinweis\": \"Weihnachten\"" + "}" + "}"; 46 | IHolidayParser parser = new JsonHolidayParser(json); 47 | 48 | // execute 49 | Assertions.assertThrows(ParseException.class, parser::getHolidays); 50 | } 51 | 52 | @Test 53 | public void testGetHolidaysAdditionalProperty() throws ParseException { 54 | // data 55 | String json = "{" + "\"2. Weihnachtstag\": {" + "\"datum\": \"2019-12-26\"," + "\"something\": \"else\"" + "}" + "}"; 56 | IHolidayParser parser = new JsonHolidayParser(json); 57 | 58 | // execute 59 | Assertions.assertThrows(ParseException.class, parser::getHolidays); 60 | } 61 | 62 | @Test 63 | public void testGetHolidaysWrongDateFormat() throws ParseException { 64 | // data 65 | String json = "{" + "\"2. Weihnachtstag\": {" + "\"datum\": \"26.12.2019\"" + "}" + "}"; 66 | IHolidayParser parser = new JsonHolidayParser(json); 67 | 68 | // execute 69 | Assertions.assertThrows(ParseException.class, parser::getHolidays); 70 | } 71 | 72 | @Test 73 | public void testGetHolidays() throws ParseException { 74 | // data 75 | Collection expectedHolidays = new ArrayList(); 76 | expectedHolidays.add(new Holiday(LocalDate.of(2019, 12, 25), "1. Weihnachtstag")); 77 | expectedHolidays.add(new Holiday(LocalDate.of(2019, 12, 26), "2. Weihnachtstag")); 78 | 79 | IHolidayParser parser = new JsonHolidayParser(JSON_EXAMPLE); 80 | 81 | // execute 82 | Collection holidays = parser.getHolidays(); 83 | 84 | // assert 85 | assertEquals(expectedHolidays.size(), holidays.size()); 86 | 87 | for (Holiday expectedHoliday : expectedHolidays) { 88 | assert (holidays.contains(expectedHoliday)); 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/resources/i18n_test/MessageBundle.properties: -------------------------------------------------------------------------------- 1 | test = Hello Fallback! 2 | fallback = This is the fallback 3 | args = Insert > {0} < 4 | multiArgs = {0} + {0} = {1} 5 | dateWithoutFormat = On {0} 6 | dateWithLongFormatFallback = Fallback to {0,date,long} 7 | -------------------------------------------------------------------------------- /src/test/resources/i18n_test/MessageBundle_de.properties: -------------------------------------------------------------------------------- 1 | test = Hallo Welt! 2 | dateWithFormat = Am {0,date} 3 | dateWithLongFormat = Am {0,date,long} 4 | -------------------------------------------------------------------------------- /src/test/resources/i18n_test/MessageBundle_en.properties: -------------------------------------------------------------------------------- 1 | test = Hello World! 2 | dateWithFormat = On {0,date} 3 | dateWithLongFormat = On {0,date,long} 4 | --------------------------------------------------------------------------------
DateFormat
DateFormatWrapper
LocalDateTime
Date
args
94 | * This method literally does {@code Global#getFirstname().replace(' ', '_')}. 95 | *
24 | * The format can include placeholders for dynamic values from other current 25 | * settings, such as the name, month and year. The default value is defined by 26 | * {@link JSONHandler#DEFAULT_PDF_NAME_FORMAT_ALGO}. 27 | *