29
29
from PIL .ImageDraw import ImageDraw
30
30
from PIL import ImageFont
31
31
32
- # The year and day pattern to detect directories. For example, if your day folders are
33
- # called "day1" to "day25" then set the pattern to r"day\d{1,2}". The script extracts
34
- # a number from the folder and tries to guess its day that way.
35
- YEAR_PATTERN = r"\d{4}"
36
- DAY_PATTERN = r"\d{2}"
37
-
38
32
# This results in the parent directory of the script directory, the year directories should be here
39
33
AOC_DIR = Path (__file__ ).absolute ().parent .parent
40
34
56
50
# it was completed with a checkmark
57
51
SHOW_CHECKMARK_INSTEAD_OF_TIME_RANK = False
58
52
53
+ # The year and day pattern to detect directories. For example, if your day folders are
54
+ # called "day1" to "day25" then set the pattern to r"day\d{1,2}". The script extracts
55
+ # a number from the folder and tries to guess its day that way.
56
+ YEAR_PATTERN = r"\d{4}"
57
+ DAY_PATTERN = r"\d{2}"
58
+
59
+
60
+ # You can change this code entirely, or just change patterns above. You get more control if you change the code.
61
+ def get_solution_paths_dict_for_years () -> dict [int , dict [int , list [str ]]]:
62
+ """Returns a dictionary which maps years to days to a list of solution paths,
63
+
64
+ E.g.: {2022: {1: [Path("2022/01/01.py"), Path("2022/01/01.kt")], ...}}
65
+
66
+ This functions gives you more control of which solutions should be shown in the tiles. For example, you
67
+ can filter by extension, or only show a single solution, or show tiles for days that have been completed
68
+ but do not have a solution.
69
+
70
+ These can also be links to external solutions, e.g. if you want to show a solution from a different repository.
71
+ (Untested however)
72
+
73
+ """
74
+ solution_paths_dict : dict [int , dict [int , list [str ]]] = {}
75
+
76
+ # If you use a new repo for years you might just remove this if, and assign the year manually
77
+ for year_dir in sorted (get_paths_matching_regex (AOC_DIR , YEAR_PATTERN ), reverse = True ):
78
+ year = find_first_number (year_dir .name )
79
+ solution_paths_dict [year ] = {}
80
+ # If you have a deep structure then you can adjust the year dir as well:
81
+ # year_dir = year_dir / "src/main/kotlin/com/example/aoc"
82
+ for day_dir in get_paths_matching_regex (year_dir , DAY_PATTERN ):
83
+ day = find_first_number (day_dir .name )
84
+ solutions = sorted (find_recursive_solution_files (day_dir ))
85
+
86
+ # To filter by extension:
87
+ # solutions = [s for s in solutions if s.suffix == ".py"]
88
+
89
+ # To only show a single solution:
90
+ # solutions = [solutions[0]]
91
+
92
+ # To show tiles for days that have been completed but do not have a solution:
93
+ # if len(solutions) == 0:
94
+ # solutions = [Path("dummy.kt")]
95
+
96
+ solutions = [solution .relative_to (AOC_DIR ) for solution in solutions ]
97
+
98
+ solution_paths_dict [year ][day ] = [str (s ) for s in solutions ]
99
+ return solution_paths_dict
100
+
101
+
59
102
# ======================================================
60
103
# === The following likely do not need to be changed ===
61
104
# ======================================================
@@ -116,7 +159,15 @@ def get_paths_matching_regex(path: Path, pattern: str):
116
159
return sorted ([p for p in path .iterdir () if re .fullmatch (pattern , p .name )])
117
160
118
161
119
- def parse_leaderboard (leaderboard_path : Path ) -> dict [str , DayScores ]:
162
+ def find_recursive_solution_files (directory : Path ) -> list [Path ]:
163
+ solution_paths = []
164
+ for path in directory .rglob ('*' ):
165
+ if path .is_file () and path .suffix in extension_to_color :
166
+ solution_paths .append (path )
167
+ return solution_paths
168
+
169
+
170
+ def parse_leaderboard (leaderboard_path : Path ) -> dict [int , DayScores ]:
120
171
no_stars = "You haven't collected any stars... yet."
121
172
start = '<span class="leaderboard-daydesc-both"> *Time *Rank *Score</span>\n '
122
173
end = "</pre>"
@@ -134,11 +185,11 @@ def parse_leaderboard(leaderboard_path: Path) -> dict[str, DayScores]:
134
185
scores = [s if s != "-" else None for s in scores ]
135
186
assert len (scores ) in (
136
187
3 , 6 ), f"Number scores for { day = } ({ scores } ) are not 3 or 6."
137
- leaderboard [day ] = DayScores (* scores )
188
+ leaderboard [int ( day ) ] = DayScores (* scores )
138
189
return leaderboard
139
190
140
191
141
- def request_leaderboard (year : int ) -> dict [str , DayScores ]:
192
+ def request_leaderboard (year : int ) -> dict [int , DayScores ]:
142
193
leaderboard_path = CACHE_DIR / f"leaderboard{ year } .html"
143
194
if leaderboard_path .exists ():
144
195
leaderboard = parse_leaderboard (leaderboard_path )
@@ -278,49 +329,48 @@ def generate_day_tile_image(day: str, year: str, languages: list[str], day_score
278
329
return path
279
330
280
331
281
- def handle_day (day : int , year : int , day_path : Path , html : HTML , day_scores : DayScores | None ):
332
+ def handle_day (day : int , year : int , solutions : list [ str ] , html : HTML , day_scores : DayScores | None ):
282
333
languages = []
283
- solution_file_path = None
284
- if day_path is not None :
285
- for file_path in day_path .glob ("*" ):
286
- if file_path .is_file ():
287
- if file_path .suffix .lower () in extension_to_color :
288
- if solution_file_path is None :
289
- solution_file_path = file_path .relative_to (AOC_DIR )
290
- languages .append (file_path .suffix .lower ())
291
- languages = sorted (set (languages ))
334
+ for solution in solutions :
335
+ extension = "." + solution .split ("." )[- 1 ]
336
+ if extension in extension_to_color and extension not in languages :
337
+ languages .append (extension )
338
+ solution_link = solutions [0 ] if solutions else None
292
339
if DEBUG :
293
340
if day == 25 :
294
341
languages = []
295
342
day_graphic_path = generate_day_tile_image (f"{ day :02} " , f"{ year :04} " , languages , day_scores )
296
343
day_graphic_path = day_graphic_path .relative_to (AOC_DIR )
297
- with html .tag ("a" , href = str (solution_file_path )):
344
+ with html .tag ("a" , href = str (solution_link )):
298
345
html .tag ("img" , closing = False , src = str (day_graphic_path ), width = TILE_WIDTH_PX )
299
346
300
347
301
348
def find_first_number (string : str ) -> int :
302
349
return int (re .findall (r"\d+" , string )[0 ])
303
350
304
351
305
- def handle_year (year_path : Path , year : int ):
352
+ def fill_empty_days_in_dict (day_to_solutions : dict [int , list [str ]], max_day ) -> None :
353
+ if not CREATE_ALL_DAYS and len (day_to_solutions ) == 0 :
354
+ print (f"Current year has no solutions!" )
355
+ for day in range (1 , max_day + 1 ):
356
+ if day not in day_to_solutions :
357
+ day_to_solutions [day ] = []
358
+
359
+
360
+ def handle_year (year : int , day_to_solutions : dict [int , list [str ]]):
306
361
leaderboard = request_leaderboard (year )
307
362
if DEBUG :
308
- leaderboard ["25" ] = None
309
- leaderboard ["24" ] = DayScores ("22:22:22" , "12313" , "0" )
363
+ leaderboard [25 ] = None
364
+ leaderboard [24 ] = DayScores ("22:22:22" , "12313" , "0" )
365
+ day_to_solutions [23 ] = []
310
366
html = HTML ()
311
367
with html .tag ("h1" , align = "center" ):
312
368
stars = sum ((ds .time1 is not None ) + (ds .time2 is not None ) for ds in leaderboard .values () if ds is not None )
313
369
html .push (f"{ year } - { stars } ⭐" )
314
- days_with_filled_gaps = {find_first_number (p .name ): p for p in get_paths_matching_regex (year_path , DAY_PATTERN )}
315
- if not CREATE_ALL_DAYS and len (days_with_filled_gaps ) == 0 :
316
- print (f"Year { year } is empty!" )
317
- return
318
- max_day = 25 if CREATE_ALL_DAYS else max (* days_with_filled_gaps , * map (int , leaderboard ))
319
- for day in range (1 , max_day + 1 ):
320
- if day not in days_with_filled_gaps :
321
- days_with_filled_gaps [day ] = None
322
- for day , day_path in days_with_filled_gaps .items ():
323
- handle_day (day , year , day_path , html , leaderboard .get (str (day ), None ))
370
+ max_day = 25 if CREATE_ALL_DAYS else max (* day_to_solutions , * leaderboard )
371
+ fill_empty_days_in_dict (day_to_solutions , max_day )
372
+ for day , solutions in day_to_solutions .items ():
373
+ handle_day (day , year , solutions , html , leaderboard .get (day , None ))
324
374
325
375
with open (README_PATH , "r" ) as file :
326
376
text = file .read ()
@@ -334,10 +384,9 @@ def handle_year(year_path: Path, year: int):
334
384
335
385
336
386
def main ():
337
- for year_path in sorted (get_paths_matching_regex (AOC_DIR , YEAR_PATTERN ), reverse = True ):
338
- year = find_first_number (year_path .name )
387
+ for year , day_to_solutions_list in get_solution_paths_dict_for_years ().items ():
339
388
print (f"=== Generating table for year { year } ===" )
340
- handle_year (year_path , year )
389
+ handle_year (year , day_to_solutions_list )
341
390
342
391
343
392
if __name__ == "__main__" :
0 commit comments