Skip to content

Commit f545a16

Browse files
committed
Added init script
1 parent e8e691b commit f545a16

File tree

1 file changed

+241
-0
lines changed

1 file changed

+241
-0
lines changed

2025/init.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#! /usr/bin/env python3
2+
"""init.py.
3+
4+
Initializer script for Advent of Code 2025. Initializes a new day from
5+
the template in apps/day00_elixir_template.
6+
"""
7+
from __future__ import annotations
8+
9+
import os
10+
import re
11+
import sys
12+
import argparse
13+
from pathlib import Path
14+
15+
description = """
16+
Initializes a new day from the template in apps/day00_elixir_template."""
17+
18+
help = """\
19+
----------------------------------------------------------------------
20+
Example uses:
21+
22+
init.py --name <name>
23+
Initializes the next uninitialized day.
24+
25+
init.py --name <name> --day <day>
26+
Initializes a given day.
27+
28+
"""
29+
30+
TEMPLATE_DAY = 0
31+
TEMPLATE_NAME = "Elixir Template"
32+
33+
# -------------------------------------------------------------------- #
34+
35+
36+
def get_day(day: int) -> str:
37+
"""Get a two-digit day string.
38+
39+
Args:
40+
day (int): Given or the next day.
41+
42+
Returns:
43+
str: The two-digit day string.
44+
"""
45+
day_str = "0" if day <= 9 else ""
46+
day_str += str(day)
47+
return day_str
48+
49+
50+
def snake_case(name: str) -> str:
51+
"""Get snake case string.
52+
53+
Args:
54+
name (str): A string.
55+
56+
Returns:
57+
str: A snake case version of the given string.
58+
"""
59+
name_parts = name.lower().split(" ")
60+
return "_".join(name_parts).replace("-", "_")
61+
62+
63+
def pascal_case(name: str) -> str:
64+
"""Get PascalCase string.
65+
66+
Args:
67+
name (str): A string.
68+
69+
Returns:
70+
str: A PascalCase version of the given string.
71+
"""
72+
name_parts = name.lower().split(" ")
73+
for i in range(len(name_parts)):
74+
name_parts[i] = name_parts[i].capitalize()
75+
return "".join(name_parts)
76+
77+
78+
def camel_case(name: str) -> str:
79+
"""Get camelCase string.
80+
81+
Args:
82+
name (str): A string.
83+
84+
Returns:
85+
str: A camelCase version of the given string.
86+
"""
87+
name_parts = name.lower().split(" ")
88+
for i in range(len(name_parts)):
89+
if i > 0:
90+
name_parts[i] = name_parts[i].capitalize()
91+
return "".join(name_parts)
92+
93+
94+
def get_day_folder(day: int, name: str | None = None) -> Path:
95+
"""Get the folder name for a given day.
96+
97+
Args:
98+
day (int): The new day.
99+
name (str | None, optional): The project name. Defaults to None.
100+
101+
Returns:
102+
Path: The path to the subproject.
103+
"""
104+
day_folder = Path("apps") / Path(
105+
"day" + get_day(day) + "_" + (snake_case(name) if name else "")
106+
)
107+
108+
return Path(__file__).parent / day_folder
109+
110+
111+
def find_next_day() -> int:
112+
"""Find the next day if no day was given.
113+
114+
Returns:
115+
int: The next uninitialized day.
116+
"""
117+
day = 1
118+
while True:
119+
day_pattern = f"day{get_day_folder(day)}*"
120+
if not any(Path(__file__).parent.glob(str(day_pattern))):
121+
break
122+
day += 1
123+
return day
124+
125+
126+
# -------------------------------------------------------------------- #
127+
128+
# Command line options
129+
parser = argparse.ArgumentParser(
130+
formatter_class=argparse.RawDescriptionHelpFormatter,
131+
description=description,
132+
epilog=help,
133+
)
134+
135+
parser.add_argument(
136+
"-n",
137+
"--name",
138+
action="store",
139+
help="Name of the subproject",
140+
required=True,
141+
)
142+
parser.add_argument(
143+
"-d",
144+
"--day",
145+
action="store",
146+
help="The number of the day",
147+
type=int,
148+
default=find_next_day(),
149+
)
150+
opts = parser.parse_args()
151+
152+
# -------------------------------------------------------------------- #
153+
154+
155+
def search_and_replace(day: int, name: str, contents: str) -> str:
156+
"""Search and Replace day and project name.
157+
158+
Searches for the template project name and the day in different
159+
naming styles and replaces them with the given / next day and given
160+
project name.
161+
162+
Args:
163+
day (int): The new day (replacement for 00 in the template)
164+
name (str): The project name (replacement for the template name)
165+
contents (str): The template project file contents
166+
167+
Returns:
168+
str: The file contents of the new subproject
169+
"""
170+
contents = re.sub(" " + get_day(TEMPLATE_DAY), " " + get_day(day), contents)
171+
contents = re.sub("day" + get_day(TEMPLATE_DAY), "day" + get_day(day), contents)
172+
contents = re.sub(TEMPLATE_NAME, name, contents)
173+
contents = re.sub(snake_case(TEMPLATE_NAME), snake_case(name), contents)
174+
contents = re.sub(pascal_case(TEMPLATE_NAME), pascal_case(name), contents)
175+
contents = re.sub(camel_case(TEMPLATE_NAME), camel_case(name), contents)
176+
return contents
177+
178+
179+
def init_project(
180+
day: int,
181+
name: str,
182+
) -> None:
183+
"""Initialize a new subproject from the template.
184+
185+
Walk through each file in the template project and replace the day
186+
and project name.
187+
188+
Args:
189+
day (int): The new day (replacement for 00 in the template)
190+
name (str): The project name (replacement for the template name)
191+
"""
192+
source = get_day_folder(TEMPLATE_DAY, TEMPLATE_NAME).as_posix()
193+
destination = get_day_folder(day, name).as_posix()
194+
195+
print(f"Initializing new project in '{destination}' from template '{source}'.")
196+
197+
for dirpath, _, files in os.walk(source):
198+
output_dir = re.sub(source, destination, Path(dirpath).as_posix())
199+
output_dir = search_and_replace(day, name, output_dir)
200+
os.makedirs(output_dir)
201+
202+
for filename in files:
203+
input = Path(dirpath) / filename
204+
with open(input, "r") as file:
205+
contents = file.read()
206+
207+
output = Path(output_dir) / search_and_replace(day, name, filename)
208+
with open(output, "w") as file:
209+
file.write(search_and_replace(day, name, contents))
210+
211+
212+
# -------------------------------------------------------------------- #
213+
214+
215+
def main() -> int:
216+
"""Execute subproject initialization.
217+
218+
Raises:
219+
RuntimeError: If the day is not between 1 and 12.
220+
RuntimeError: If there is already a project for the given day.
221+
222+
Returns:
223+
int: System exit code.
224+
"""
225+
# Check day
226+
if opts.day < 1 or opts.day > 12:
227+
raise RuntimeError("Day needs to be between 1 and 12.")
228+
229+
# Test if folder already exists
230+
folder = get_day_folder(opts.day)
231+
if folder.exists():
232+
raise RuntimeError("Day '{0}' already exists.".format(folder))
233+
234+
# Initialize project
235+
init_project(opts.day, opts.name)
236+
237+
return 0
238+
239+
240+
if __name__ == "__main__":
241+
sys.exit(main())

0 commit comments

Comments
 (0)