A simple Kotlin library for Advent of Code.
After my second year of completing AoC in Kotlin, I extracted common functionality into a library to reduce redundancy. Two packages are published;
The main utility classes and datastructures to be compiled in the implementation
scope.
Test utility classes for supporting unit tests and benchmarking for the testImplementation
scope.
The AdventLogger
provides a simple companion-object class that wraps the
SLF4J abstraction. The class exposes the usual levels which are as follows
Level | Description |
---|---|
Error | Designates error events that might still allow the application to continue running. |
Warn | Designates potentially harmful situations. |
Info | Designates information messages that highlight the progress of the application at a coarse-grained level. |
Debug | Designates fine-grained informational events that are most useful to debug the application. |
Trace | Designates finer-grained informational events than the debug level. |
The InputReader
provides an easy way to read the puzzle inputs from the resources
directory
of the main source set. The reader looks for a file named input.txt
in a sub-folder named dayX
where X
is the day
number. The read()
function also expects a generic type parameter which changes the type of collection that is returned.
The currently supported types are Integer
and String
.
A common theme in the Advent of Code is maps, which are usually represented on a 2D cartesian grid. Thus,
Point2D
is a wrapper class for co-ordinates that provides useful translation, transformation
and interacting with other points. Likewise, Point3D
does a similar thing for 3-Dimensions.
To accompany the Point
classes, AdventMap2D
and AdventMap3D
have
been created for Point2D
and Point3D
respectively. These classes are essentially cartesian grids backed by a Map
of the respective Point
class against a MapTile
which accepts a generic type.
This is usually a Char
due to the nature of Advent of Code puzzle inputs.
An example puzzle input of a map that could be read and stored in an AdventMap2D
;
..##.......
#...#...#..
.#....#..#.
..#.#...#.#
.#...##..#.
..#.##.....
.#.#.#....#
.#........#
#.##...#...
#...##....#
.#..#...#.#
A Solution
interface is provided so that the solution classes can be passed to the SolutionRunner
.
The runner executes all the solution implementations and measures their runtimes.
A benchmark is created that produces a report that is appended to the SystemOut.
The benchmark utility stores the last run in benchmark.xml
in the root of the project directory.
The format and verbosity of the report can be changed via a JVM arg called report
.
It can have the values verbose
or compact
. E.g. -Dreport=verbose
.
A snippet from a verbose runtime delta report;
- Advent of Code 2020 Solution Report -
[Day 1]
Part 1: 802011
Execution Time: 9ms (+3ms)
Part 2: 248607374
Execution Time: 54ms (+22ms)
[Day 2]
Part 1: 660
Execution Time: 10ms (+4ms)
Part 2: 530
Execution Time: 6ms (+2ms)
...
Average Execution Time: 654ms (+21ms)
Total Execution Time: 16s 365ms (+535ms)
If a previous run cannot be found (I.e. the benchmark.xml
is not present), then a delta report cannot be produced.
There will be an info log printed, and the report will have the deltas omitted like the following;
Cannot find previous run. Deltas will not be reported.
- Advent of Code 2020 Solution Report -
[Day 1]
Part 1: 802011
Execution Time: 7ms
Part 2: 248607374
Execution Time: 41ms
...
If your terminal supports ANSI escape codes then the deltas will be green or red for increased and decreased runtimes respectively.
Running unit tests with JUnit via VisualVM can be difficult due to the time taken to launch the VisualVM process, attach to the running Java thread, and configure sampling/profiling. There doesn't seem to be a particularly elegant process for this other than telling the thread to wait.
One problem I ran into (and seemingly many others online too) was that JUnit5 bootstrapped and ran my test so quickly
that it finished before VisualVM even had a chance to boot-up. A common solution I'd seen was to simply add a
Thread.sleep(x)
line at the start of the test method. Although this is the solution I technically went with, I
abstracted it into a @WaitForVisualVM
annotation and created a custom implementation
of the Jupiter APIs BeforeTestExecutionCallback
interface called SupportsVisualVM.kt
which can be added to a test-suite class using the
@ExtendWith
annotation.
This kept things inline with the 'enterprise-style' aspect of my codebase as it did the following;
- Wrapped the dubious
Thread.sleep()
call with a self-explanatory annotation (and is also documented). - Removed noise from the test method and ensured that it always runs before test-execution.
- Allows developers to easily disable all waiting for tests in a suite by simply removing the support extension.
- Makes it easier to refactor in the future the as implementation specifics are encapsulated in the annotation.
- Make your code changes in the master branch (or feature branch)
- Update the release version in the build.gradle
- Rebase off of the release branch to ensure a fast-forward merge
- Merge to release branch once ready
- Create a new release on GitHub, set the new version tag and draft the release
- This will trigger GitHub actions release pipeline, wait for run to complete
- Add error handling for input not found for solution runner
- Investigate and fix big integer printing, seems to be truncating
- Add init method to Map classes so you can pass a data set and have it parse (generic type for tile + predicate for mapping)
- A test support class for testing solutions. Accepts a solution and expected answers for p1, p2
- Added graph/node objets for graphing algos like Djikstra
- Added a findTile() function to the AdventMaps so you can pass a predicate to find or null