advent_of_ruby 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/arb +4 -10
- data/lib/arb/api/aoc.rb +7 -7
- data/lib/arb/api/other_solutions.rb +17 -17
- data/lib/arb/arb.rb +1 -5
- data/lib/arb/cli/bootstrap.rb +9 -8
- data/lib/arb/cli/commit.rb +20 -11
- data/lib/arb/cli/progress.rb +4 -2
- data/lib/arb/cli/run.rb +171 -0
- data/lib/arb/cli/shared/git.rb +31 -10
- data/lib/arb/cli/shared/runner.rb +55 -15
- data/lib/arb/cli/shared/working_directory.rb +27 -34
- data/lib/arb/cli/shared/year_day_validator.rb +14 -13
- data/lib/arb/files/input.rb +9 -6
- data/lib/arb/files/instructions.rb +10 -12
- data/lib/arb/files/other_solutions.rb +3 -5
- data/lib/arb/files/{source.rb → solution.rb} +9 -12
- data/lib/arb/files/spec.rb +7 -10
- data/lib/arb/version.rb +1 -1
- metadata +4 -4
- data/lib/arb/cli/run_day.rb +0 -140
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed16b9db7d2113dc310e48d6fc3792c673dfdfeab2259ad4c8af67797b4c7c6b
|
4
|
+
data.tar.gz: 6cde22c6e469c4c774d0ba336a6f37dfacedf2f98e421480153ddc9d875369b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77e2f6d78f40aec3b30f625fce9367af74612c015111cab5f11ee207ff150b4feadbe21cff287e8f372b68fa711c3d81f17ac5b923be8bef9b991bf0781514d8
|
7
|
+
data.tar.gz: 1274475154d99500503228440120a0322d83832f0c30f39ffdbbc56bcc29d54d258ef0c6e4f8914617818183682b4a2f1c5525581c1803d3f611b90a35f00f11
|
data/bin/arb
CHANGED
@@ -11,19 +11,13 @@ class ArbApp < Thor
|
|
11
11
|
Arb::Cli.bootstrap(year:, day:)
|
12
12
|
end
|
13
13
|
|
14
|
-
# TODO add a [VARIANT] arg, so that additional versions of the same solution
|
15
|
-
# can co-exist as additional methods, e.g.:
|
16
|
-
#
|
17
|
-
# #part_1_first # for my initial attempt
|
18
|
-
# #part_1_concise # for a code-golf type solution
|
19
|
-
# #part_1_alt # for an alternative approach
|
20
14
|
desc "run [YEAR] [DAY]", "Runs the puzzle that's untracked in Git, or the " \
|
21
15
|
"puzzle of the given day and year. `-s` runs only specs, `-o` part 1, `-t` part 2."
|
22
16
|
method_option :spec, type: :boolean, aliases: "-s"
|
23
|
-
method_option :
|
24
|
-
method_option :
|
25
|
-
def run_day(year = nil, day = nil)
|
26
|
-
Arb::Cli.
|
17
|
+
method_option :one, type: :boolean, aliases: "-o"
|
18
|
+
method_option :two, type: :boolean, aliases: "-t"
|
19
|
+
def run_day(year = nil, day = nil) # can't be named `run` due to Thor conflict
|
20
|
+
Arb::Cli.run(year:, day:, options:)
|
27
21
|
end
|
28
22
|
|
29
23
|
desc "progress", "Shows progress based on the number of your solutions " \
|
data/lib/arb/api/aoc.rb
CHANGED
@@ -3,7 +3,7 @@ module Arb
|
|
3
3
|
class Aoc
|
4
4
|
private attr_reader :connection
|
5
5
|
|
6
|
-
def initialize(cookie)
|
6
|
+
def initialize(cookie:)
|
7
7
|
@connection = Faraday.new(
|
8
8
|
url: "https://adventofcode.com",
|
9
9
|
headers: {
|
@@ -13,22 +13,22 @@ module Arb
|
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
16
|
-
def input(year
|
16
|
+
def input(year:, day:)
|
17
17
|
logged_in {
|
18
|
-
connection.get("/#{year}/day/#{day}/input")
|
18
|
+
connection.get("/#{year}/day/#{day.delete_prefix("0")}/input")
|
19
19
|
}
|
20
20
|
end
|
21
21
|
|
22
|
-
def instructions(year
|
22
|
+
def instructions(year:, day:)
|
23
23
|
logged_in {
|
24
|
-
connection.get("/#{year}/day/#{day}")
|
24
|
+
connection.get("/#{year}/day/#{day.delete_prefix("0")}")
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
|
-
def submit(year
|
28
|
+
def submit(year:, day:, part:, answer:)
|
29
29
|
logged_in {
|
30
30
|
connection.post(
|
31
|
-
"/#{year}/day/#{day}/answer",
|
31
|
+
"/#{year}/day/#{day.delete_prefix("0")}/answer",
|
32
32
|
"level=#{part}&answer=#{answer}",
|
33
33
|
)
|
34
34
|
}
|
@@ -6,37 +6,37 @@ module Arb
|
|
6
6
|
private attr_reader :connection
|
7
7
|
|
8
8
|
PATHS = {
|
9
|
-
"eregon" => ->(year
|
9
|
+
"eregon" => ->(year:, day:, part:) {
|
10
10
|
if part == "1"
|
11
11
|
[
|
12
|
-
"adventofcode/tree/master/#{year}/#{day}.rb",
|
13
|
-
"adventofcode/tree/master/#{year}/#{day}a.rb",
|
12
|
+
"adventofcode/tree/master/#{year}/#{day.delete_prefix("0")}.rb",
|
13
|
+
"adventofcode/tree/master/#{year}/#{day.delete_prefix("0")}a.rb",
|
14
14
|
]
|
15
15
|
elsif part == "2"
|
16
|
-
["adventofcode/tree/master/#{year}/#{day}b.rb"]
|
16
|
+
["adventofcode/tree/master/#{year}/#{day.delete_prefix("0")}b.rb"]
|
17
17
|
end
|
18
18
|
},
|
19
|
-
"gchan" => ->(year
|
20
|
-
["advent-of-code-ruby/tree/main/#{year}/day-#{day
|
19
|
+
"gchan" => ->(year:, day:, part:) {
|
20
|
+
["advent-of-code-ruby/tree/main/#{year}/day-#{day}/day-#{day}-part-#{part}.rb"]
|
21
21
|
},
|
22
|
-
"ahorner" => ->(year
|
22
|
+
"ahorner" => ->(year:, day:, part:) {
|
23
23
|
return [] if part == "1"
|
24
|
-
["advent-of-code/tree/main/lib/#{year}/#{day
|
24
|
+
["advent-of-code/tree/main/lib/#{year}/#{day}.rb"]
|
25
25
|
},
|
26
|
-
"ZogStriP" => ->(year
|
26
|
+
"ZogStriP" => ->(year:, day:, part:) {
|
27
27
|
return [] if part == "1"
|
28
28
|
|
29
|
-
puzzle_name = File.read(Files::Instructions.path(year
|
29
|
+
puzzle_name = File.read(Files::Instructions.path(year:, day:))
|
30
30
|
.match(/## --- Day \d\d?: (.+)(?= ---)/)
|
31
31
|
.captures
|
32
32
|
.first
|
33
33
|
.downcase
|
34
34
|
.gsub(" ", "_")
|
35
35
|
|
36
|
-
["adventofcode/tree/master/#{year}/#{day
|
36
|
+
["adventofcode/tree/master/#{year}/#{day}_#{puzzle_name}.rb"]
|
37
37
|
},
|
38
|
-
"erikw" => ->(year
|
39
|
-
["advent-of-code-solutions/tree/main/#{year}/#{day
|
38
|
+
"erikw" => ->(year:, day:, part:) {
|
39
|
+
["advent-of-code-solutions/tree/main/#{year}/#{day}/part#{part}.rb"]
|
40
40
|
},
|
41
41
|
}
|
42
42
|
|
@@ -64,13 +64,13 @@ module Arb
|
|
64
64
|
)
|
65
65
|
end
|
66
66
|
|
67
|
-
def other_solutions(year
|
67
|
+
def other_solutions(year:, day:, part:)
|
68
68
|
"# #{year} Day #{day} Part #{part}\n\n" +
|
69
69
|
PATHS
|
70
70
|
.map { |username, path_builder|
|
71
71
|
actual_path = nil
|
72
72
|
solution = nil
|
73
|
-
paths = path_builder.call(year
|
73
|
+
paths = path_builder.call(year:, day:, part:)
|
74
74
|
|
75
75
|
paths.each do |path|
|
76
76
|
next if solution
|
@@ -82,14 +82,14 @@ module Arb
|
|
82
82
|
end
|
83
83
|
|
84
84
|
if solution
|
85
|
-
<<~
|
85
|
+
<<~SOLUTION
|
86
86
|
# ------------------------------------------------------------------------------
|
87
87
|
# #{username}: #{UI_URI}/#{username}/#{actual_path}
|
88
88
|
# ------------------------------------------------------------------------------
|
89
89
|
|
90
90
|
#{solution}
|
91
91
|
|
92
|
-
|
92
|
+
SOLUTION
|
93
93
|
end
|
94
94
|
}
|
95
95
|
.compact
|
data/lib/arb/arb.rb
CHANGED
@@ -3,6 +3,7 @@ require "date"
|
|
3
3
|
require "debug"
|
4
4
|
require "dotenv"
|
5
5
|
require "faraday"
|
6
|
+
require "open3"
|
6
7
|
require "pastel"
|
7
8
|
require "reverse_markdown"
|
8
9
|
require "rspec/core"
|
@@ -16,8 +17,3 @@ PASTEL = Pastel.new
|
|
16
17
|
Dir[File.join(__dir__, "**", "*.rb")].each do |file|
|
17
18
|
require file
|
18
19
|
end
|
19
|
-
|
20
|
-
solution_files = File.join(Dir.pwd, "src", "**", "*.rb")
|
21
|
-
Dir[solution_files].each do |file|
|
22
|
-
require file
|
23
|
-
end
|
data/lib/arb/cli/bootstrap.rb
CHANGED
@@ -7,19 +7,20 @@ module Arb
|
|
7
7
|
|
8
8
|
year, day = YearDayValidator.validate_year_and_day(year:, day:)
|
9
9
|
|
10
|
-
instructions_path = Files::Instructions.download(year
|
11
|
-
others_1_path, others_2_path = Files::OtherSolutions.download(year
|
12
|
-
input_path = Files::Input.download(year
|
13
|
-
|
14
|
-
spec_path = Files::Spec.create(year
|
10
|
+
instructions_path = Files::Instructions.download(year:, day:)
|
11
|
+
others_1_path, others_2_path = Files::OtherSolutions.download(year:, day:)
|
12
|
+
input_path = Files::Input.download(year:, day:)
|
13
|
+
solution_path = Files::Solution.create(year:, day:)
|
14
|
+
spec_path = Files::Spec.create(year:, day:)
|
15
15
|
|
16
|
-
puts "🤘 Bootstrapped #{year}##{day}
|
16
|
+
puts "🤘 Bootstrapped #{year}##{day}"
|
17
|
+
puts
|
17
18
|
|
18
19
|
# Open the new files.
|
19
20
|
`#{ENV["EDITOR_COMMAND"]} #{others_1_path}`
|
20
21
|
`#{ENV["EDITOR_COMMAND"]} #{others_2_path}`
|
21
22
|
`#{ENV["EDITOR_COMMAND"]} #{input_path}`
|
22
|
-
`#{ENV["EDITOR_COMMAND"]} #{
|
23
|
+
`#{ENV["EDITOR_COMMAND"]} #{solution_path}`
|
23
24
|
`#{ENV["EDITOR_COMMAND"]} #{spec_path}`
|
24
25
|
`#{ENV["EDITOR_COMMAND"]} #{instructions_path}`
|
25
26
|
|
@@ -27,7 +28,7 @@ module Arb
|
|
27
28
|
puts "Now fill in the spec for Part One with an example from the instructions, " \
|
28
29
|
"then run it with `#{PASTEL.blue.bold("arb run")}` (or just `arb`) as " \
|
29
30
|
"you implement the solution. When the spec passes, your solution will " \
|
30
|
-
"be run with the real input and you'll be prompted to submit your solution
|
31
|
+
"be run with the real input and you'll be prompted to submit your solution."
|
31
32
|
end
|
32
33
|
rescue AppError => e
|
33
34
|
puts Pastel.new.red(e.message)
|
data/lib/arb/cli/commit.rb
CHANGED
@@ -3,32 +3,41 @@ module Arb
|
|
3
3
|
def self.commit
|
4
4
|
WorkingDirectory.prepare!
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
puzzles = Git.modified_puzzles
|
7
|
+
if puzzles.any?
|
8
8
|
files_modified = true
|
9
|
-
|
9
|
+
else
|
10
|
+
puzzles = Git.uncommitted_puzzles
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
+
if puzzles.empty?
|
13
14
|
puts "Nothing to commit."
|
14
15
|
|
15
16
|
if Git.commit_count <= 2
|
16
|
-
puts
|
17
|
+
puts
|
18
|
+
puts "Run `#{PASTEL.blue.bold("arb bootstrap")}` (or `arb b`) to start the next puzzle."
|
17
19
|
end
|
18
20
|
|
19
21
|
return
|
20
22
|
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
puzzles.each do |(year, day), filenames|
|
25
|
+
message = "#{files_modified ? "Improve" : "Solve"} #{year}##{day}"
|
26
|
+
Git.commit!(filenames:, message:)
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
# TODO less naive check: ensure prev. days are finished too
|
29
|
+
if !files_modified && day == "25"
|
30
|
+
puts
|
31
|
+
puts "🎉 You've finished #{year}!"
|
32
|
+
puts
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "Puzzle #{year}##{day}#{" (modified)" if files_modified} committed 🎉"
|
28
36
|
end
|
29
37
|
|
30
38
|
if Git.commit_count <= 1
|
31
|
-
puts
|
39
|
+
puts
|
40
|
+
puts "When you're ready to start the next puzzle, run " \
|
32
41
|
"`#{PASTEL.blue.bold("arb bootstrap")}` (or `arb b`)."
|
33
42
|
end
|
34
43
|
end
|
data/lib/arb/cli/progress.rb
CHANGED
@@ -14,8 +14,10 @@ module Arb
|
|
14
14
|
total_percent = (my_total_count.to_f / total_count * 100).round(1)
|
15
15
|
total_percent = total_percent == total_percent.to_i ? total_percent.to_i : total_percent
|
16
16
|
|
17
|
-
puts "You have completed
|
18
|
-
puts
|
17
|
+
puts "You have completed:"
|
18
|
+
puts
|
19
|
+
puts PASTEL.bold "#{PASTEL.blue("All:")}\t#{total_percent}% \t#{my_total_count}/#{total_count} puzzles"
|
20
|
+
puts
|
19
21
|
|
20
22
|
my_counts_by_year.each do |year, my_count|
|
21
23
|
if year.to_i == Date.today.year
|
data/lib/arb/cli/run.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
# @param year [String, Integer]
|
4
|
+
# @param day [String, Integer]
|
5
|
+
# @param options [Hash] see `method_option`s above ArbApp#run_day.
|
6
|
+
def self.run(year:, day:, options:)
|
7
|
+
WorkingDirectory.prepare!
|
8
|
+
|
9
|
+
if options[:spec] && (options[:one] || options[:two])
|
10
|
+
raise InputError, "Don't use --spec (-s) with --one (-o) or --two (-t)"
|
11
|
+
end
|
12
|
+
|
13
|
+
year, day = YearDayValidator.validate_year_and_day(year:, day:, default_to_untracked_or_last_committed: true)
|
14
|
+
|
15
|
+
if Git.uncommitted_puzzles.empty? && !Git.last_committed_puzzle(year:)
|
16
|
+
bootstrap(year:, day:)
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
instructions_path = Files::Instructions.download(year:, day:, notify_exists: false, overwrite: false)
|
21
|
+
instructions = File.read(instructions_path)
|
22
|
+
correct_answer_1, correct_answer_2 = instructions.scan(/Your puzzle answer was `([^`]+)`./).flatten
|
23
|
+
skip_count = 0
|
24
|
+
|
25
|
+
if options[:spec]
|
26
|
+
run_specs_only(year:, day:)
|
27
|
+
return
|
28
|
+
elsif !(options[:one] || options[:two])
|
29
|
+
specs_passed, skip_count = run_specs_before_real(year:, day:)
|
30
|
+
return unless specs_passed
|
31
|
+
puts "👍 Specs passed!"
|
32
|
+
if skip_count > 1 || (skip_count == 1 && correct_answer_1)
|
33
|
+
puts PASTEL.yellow.bold("🤐 #{skip_count} skipped, however")
|
34
|
+
end
|
35
|
+
puts
|
36
|
+
end
|
37
|
+
|
38
|
+
Files::Input.download(year:, day:, notify_exists: false)
|
39
|
+
answers_1, answers_2 = [], []
|
40
|
+
|
41
|
+
begin
|
42
|
+
if options[:one] || (!options[:two] && ((correct_answer_1.nil? && skip_count <= 1) || correct_answer_2))
|
43
|
+
answers_1 = Runner.run_part(year:, day:, part: "1", correct_answer: correct_answer_1)
|
44
|
+
end
|
45
|
+
if options[:two] || (!options[:one] && ((correct_answer_1 && !correct_answer_2 && skip_count.zero?) || correct_answer_2))
|
46
|
+
if answers_1.count > 1
|
47
|
+
puts "------------"
|
48
|
+
puts
|
49
|
+
end
|
50
|
+
|
51
|
+
answers_2 = Runner.run_part(year:, day:, part: "2",correct_answer: correct_answer_2)
|
52
|
+
end
|
53
|
+
rescue Runner::SolutionNotFoundError
|
54
|
+
puts PASTEL.red("Solution class not found. Make sure this class exists: #{PASTEL.bold("Year#{year}::Day#{day}")}")
|
55
|
+
rescue Runner::SolutionArgumentError
|
56
|
+
puts PASTEL.red("ArgumentError when running your solution. Make sure every method has a one parameter (the input file).")
|
57
|
+
rescue Runner::SolutionArgumentError
|
58
|
+
end
|
59
|
+
|
60
|
+
answer_1, answer_2 = answers_1.compact.first, answers_2.compact.first
|
61
|
+
|
62
|
+
return unless answer_1 || answer_2
|
63
|
+
|
64
|
+
if correct_answer_2
|
65
|
+
puts "🙌 You've already submitted the answers to both parts.\n"
|
66
|
+
|
67
|
+
if Git.commit_count <= 1
|
68
|
+
puts "When you're done with this puzzle, run " \
|
69
|
+
"`#{PASTEL.blue.bold("arb commit")}` (or `arb c`) commit your solution to Git.\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
return
|
73
|
+
elsif options[:one] && correct_answer_1
|
74
|
+
puts "🙌 You've already submitted the answer to this part.\n\n"
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
puts "Submit solution? (Y/n)"
|
79
|
+
print PASTEL.green("> ")
|
80
|
+
submit = STDIN.gets.strip.downcase
|
81
|
+
puts
|
82
|
+
|
83
|
+
if submit == "y" || submit == ""
|
84
|
+
options_part = options[:one] ? "1" : (options[:two] ? "2" : nil)
|
85
|
+
inferred_part = correct_answer_1.nil? ? "1" : "2"
|
86
|
+
aoc_api = Api::Aoc.new(cookie: ENV["AOC_COOKIE"])
|
87
|
+
|
88
|
+
response = aoc_api.submit(year:, day:, part: options_part || inferred_part, answer: answer_2 || answer_1)
|
89
|
+
message = response.match(/(?<=<article>).+(?=<\/article>)/m).to_s.strip
|
90
|
+
markdown_message = ReverseMarkdown.convert(message)
|
91
|
+
short_message = markdown_message
|
92
|
+
.sub(/\n\n.+/m, "")
|
93
|
+
.sub(/ \[\[.+/, "")
|
94
|
+
|
95
|
+
if short_message.start_with?("That's the right answer!")
|
96
|
+
puts
|
97
|
+
puts "⭐ #{short_message}"
|
98
|
+
|
99
|
+
# TODO don't re-download if the instructions file already contains the next part
|
100
|
+
instructions_path = Files::Instructions.download(year:, day:, overwrite: true)
|
101
|
+
|
102
|
+
if (options_part || inferred_part) == "1"
|
103
|
+
puts "Downloaded instructions for Part Two."
|
104
|
+
`#{ENV["EDITOR_COMMAND"]} #{instructions_path}`
|
105
|
+
|
106
|
+
spec_path = Files::Spec.create(year:, day:, notify_exists: false)
|
107
|
+
spec = File.read(spec_path)
|
108
|
+
spec_without_skips = spec.gsub(" xit ", " it ")
|
109
|
+
File.write(spec_path, spec_without_skips)
|
110
|
+
end
|
111
|
+
|
112
|
+
if Git.commit_count <= 1
|
113
|
+
puts
|
114
|
+
puts "Now it's time to improve your solution! Be sure to look " \
|
115
|
+
"at other people's solutions (in the \"others\" directory). When " \
|
116
|
+
"you're done, run `#{PASTEL.blue.bold("arb commit")}` (or `arb c`) " \
|
117
|
+
"to commit your solution to Git.\n"
|
118
|
+
end
|
119
|
+
else
|
120
|
+
puts "❌ #{short_message}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
rescue AppError => e
|
124
|
+
puts PASTEL.red(e.message)
|
125
|
+
end
|
126
|
+
|
127
|
+
private_class_method def self.run_specs_only(year:, day:)
|
128
|
+
spec_filename = File.join("spec", year, "#{day}_spec.rb")
|
129
|
+
|
130
|
+
# Running RSpec from within RSpec causes problems, so in the test environment
|
131
|
+
# run RSpec in a subprocess.
|
132
|
+
if ENV["TEST_ENV"]
|
133
|
+
system "rspec #{spec_filename}"
|
134
|
+
else
|
135
|
+
RSpec::Core::Runner.run([spec_filename])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private_class_method def self.run_specs_before_real(year:, day:)
|
140
|
+
spec_filename = File.join("spec", year, "#{day}_spec.rb")
|
141
|
+
|
142
|
+
# Running RSpec from within RSpec causes problems, so in the test environment
|
143
|
+
# run RSpec in a subprocess.
|
144
|
+
if ENV["TEST_ENV"]
|
145
|
+
stdout, _status = Open3.capture2("rspec #{spec_filename} --color --tty")
|
146
|
+
else
|
147
|
+
stdout = StringIO.new
|
148
|
+
|
149
|
+
RSpec.configure do |config|
|
150
|
+
config.color = true
|
151
|
+
config.tty = true
|
152
|
+
config.output_stream = stdout
|
153
|
+
end
|
154
|
+
|
155
|
+
RSpec::Core::Runner.run([spec_filename])
|
156
|
+
stdout = stdout.string
|
157
|
+
end
|
158
|
+
|
159
|
+
if stdout.include? "Failures:"
|
160
|
+
puts stdout
|
161
|
+
|
162
|
+
[false, nil]
|
163
|
+
elsif stdout.include?("Finished in ") && stdout.include?("0 failures")
|
164
|
+
[true, stdout.scan("skipped with xit").count]
|
165
|
+
else
|
166
|
+
uncolorized_stdout = stdout.gsub(/\e\[\d+m/, "")
|
167
|
+
raise uncolorized_stdout
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/lib/arb/cli/shared/git.rb
CHANGED
@@ -1,23 +1,36 @@
|
|
1
1
|
module Arb
|
2
2
|
module Cli
|
3
3
|
class Git
|
4
|
-
#
|
4
|
+
# A hash whose keys are ["<year>", "<day>"] of new solutions or specs, and
|
5
|
+
# and whose values are the names of the modified files (solution, spec, or both).
|
5
6
|
# @return [Array<Array(String, String)>]
|
6
|
-
def self.
|
7
|
+
def self.uncommitted_puzzles
|
7
8
|
output = `git status -su | grep -e "^?? src" -e "^?? spec" -e "^A src" -e "^A spec"`
|
8
|
-
output.scan(/(
|
9
|
+
filenames = output.scan(/(?:src|spec)\/\d{4}\/\d\d(?:_spec)?.rb/).uniq
|
10
|
+
filenames.group_by { |filename|
|
11
|
+
filename
|
12
|
+
.match(/(?<year>\d{4})\/(?<day>\d\d)(?:_spec)?.rb$/)
|
13
|
+
.captures
|
14
|
+
}
|
9
15
|
end
|
10
16
|
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
17
|
+
# A hash whose keys are ["<year>", "<day>"] of modified existing solutions
|
18
|
+
# or specs, and whose values are the names of the modified files
|
19
|
+
# (solution, spec, or both).
|
20
|
+
# @return [Hash{Array(String, String), Array<String>}]
|
21
|
+
def self.modified_puzzles
|
14
22
|
output = `git status -su | grep -e "^ M src" -e "^ M spec" -e "^M src" -e "^M spec"`
|
15
|
-
output.scan(/(
|
23
|
+
filenames = output.scan(/(?:src|spec)\/\d{4}\/\d\d(?:_spec)?.rb/).uniq
|
24
|
+
filenames.group_by { |filename|
|
25
|
+
filename
|
26
|
+
.match(/(?<year>\d{4})\/(?<day>\d\d)(?:_spec)?.rb$/)
|
27
|
+
.captures
|
28
|
+
}
|
16
29
|
end
|
17
30
|
|
18
31
|
# Year and day of the latest-date solution in the most recent commit, or nil.
|
19
32
|
# @return [Array(String, String), nil]
|
20
|
-
def self.
|
33
|
+
def self.last_committed_puzzle(year: nil, exclude_year: nil)
|
21
34
|
if exclude_year
|
22
35
|
output = `git log -n 1 --diff-filter=A --name-only --pretty=format: -- src spec ':!src/#{exclude_year}' ':!spec/#{exclude_year}'`
|
23
36
|
else
|
@@ -57,10 +70,13 @@ module Arb
|
|
57
70
|
|
58
71
|
previous_days.map { |year, days|
|
59
72
|
days_hash = days.map { |day|
|
60
|
-
[
|
73
|
+
[
|
74
|
+
day.to_s.rjust(2, "0"),
|
75
|
+
committed_days.has_key?(year) && committed_days[year].include?(day),
|
76
|
+
]
|
61
77
|
}.to_h
|
62
78
|
|
63
|
-
[year, days_hash]
|
79
|
+
[year.to_s, days_hash]
|
64
80
|
}.to_h
|
65
81
|
end
|
66
82
|
|
@@ -73,6 +89,11 @@ module Arb
|
|
73
89
|
`git init`
|
74
90
|
end
|
75
91
|
|
92
|
+
def self.commit!(filenames:, message:)
|
93
|
+
`git add #{filenames.join(" ")}`
|
94
|
+
`git commit -m "#{message}"`
|
95
|
+
end
|
96
|
+
|
76
97
|
def self.commit_all!(message:)
|
77
98
|
`git add -A`
|
78
99
|
`git commit -m "#{message}"`
|
@@ -1,17 +1,41 @@
|
|
1
1
|
module Arb
|
2
2
|
module Cli
|
3
3
|
class Runner
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
SolutionNotFoundError = Class.new(StandardError)
|
5
|
+
SolutionArgumentError = Class.new(StandardError)
|
6
|
+
|
7
|
+
def self.run_part(year:, day:, part:, correct_answer:)
|
8
|
+
solution_class = solution_class(year:, day:)
|
9
|
+
base_method_name = "part_#{part}"
|
10
|
+
variant_method_names = solution_class
|
11
|
+
.instance_methods(false)
|
12
|
+
.filter { _1.to_s.start_with?(base_method_name)}
|
13
|
+
.sort
|
14
|
+
|
15
|
+
if variant_method_names.empty?
|
16
|
+
puts PASTEL.red("🤔 Couldn't find the method #{PASTEL.bold("Year#{year}::Day#{day}##{base_method_name}")}.")
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
answers = []
|
21
|
+
|
22
|
+
variant_method_names.each do |variant_method_name|
|
23
|
+
solution = solution_class.new
|
24
|
+
input_file = File.new(Files::Input.path(year:, day:))
|
25
|
+
answer = nil
|
26
|
+
|
27
|
+
begin
|
28
|
+
run_time = Benchmark.realtime do
|
29
|
+
answer = solution.send(variant_method_name, input_file)
|
30
|
+
end
|
31
|
+
variant = variant_method_name.to_s.delete_prefix(base_method_name).delete_prefix("_")
|
32
|
+
variant_note = " `#{PASTEL.blue.bold(variant)}`" unless variant.empty?
|
33
|
+
rescue ArgumentError
|
34
|
+
raise SolutionArgumentError
|
35
|
+
end
|
36
|
+
|
37
|
+
part_name = "#{year}##{day}.#{part}#{variant_note}"
|
10
38
|
|
11
|
-
def self.run_part(part_name, correct_answer)
|
12
|
-
answer = nil
|
13
|
-
t = Benchmark.realtime do
|
14
|
-
answer = yield
|
15
39
|
if answer
|
16
40
|
puts "Result for #{part_name}:"
|
17
41
|
|
@@ -27,15 +51,31 @@ module Arb
|
|
27
51
|
else
|
28
52
|
puts "❌ No result for #{part_name}"
|
29
53
|
end
|
30
|
-
end
|
31
54
|
|
32
|
-
|
33
|
-
|
55
|
+
if answer && run_time
|
56
|
+
if variant_method_names.count > 1
|
57
|
+
puts "(obtained in #{PASTEL.cyan("%.6f" % run_time)} seconds)"
|
58
|
+
else
|
59
|
+
puts "(obtained in #{"%.6f" % run_time} seconds)"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
answers << answer
|
64
|
+
|
65
|
+
puts
|
34
66
|
end
|
35
67
|
|
36
|
-
|
68
|
+
answers
|
69
|
+
end
|
37
70
|
|
38
|
-
|
71
|
+
private_class_method def self.solution_class(year:, day:)
|
72
|
+
require "#{Dir.pwd}/src/#{year}/#{day}"
|
73
|
+
|
74
|
+
Module
|
75
|
+
.const_get("Year#{year}")
|
76
|
+
.const_get("Day#{day}")
|
77
|
+
rescue NameError
|
78
|
+
raise SolutionNotFoundError
|
39
79
|
end
|
40
80
|
end
|
41
81
|
end
|
@@ -2,20 +2,18 @@ module Arb
|
|
2
2
|
module Cli
|
3
3
|
class WorkingDirectory
|
4
4
|
FILES = {
|
5
|
-
gitignore
|
5
|
+
".gitignore" => <<~FILE,
|
6
6
|
input/**/*
|
7
7
|
instructions/**/*
|
8
8
|
others/**/*
|
9
9
|
.env
|
10
|
-
|
11
10
|
FILE
|
12
|
-
|
13
|
-
|
11
|
+
".ruby-version" => "3.3.0\n",
|
12
|
+
"Gemfile" => <<~FILE,
|
14
13
|
source "https://rubygems.org"
|
15
14
|
ruby file: ".ruby-version"
|
16
|
-
|
17
15
|
FILE
|
18
|
-
|
16
|
+
File.join("spec", "spec_helper.rb") => <<~FILE
|
19
17
|
require "debug"
|
20
18
|
|
21
19
|
Dir[File.join(__dir__, "..", "src", "**", "*.rb")].each do |file|
|
@@ -43,7 +41,8 @@ module Arb
|
|
43
41
|
files_created += create_other_files!
|
44
42
|
|
45
43
|
if files_created.any?
|
46
|
-
puts "✅ Initial files created and committed to a new Git repository
|
44
|
+
puts "✅ Initial files created and committed to a new Git repository."
|
45
|
+
puts
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
@@ -54,19 +53,19 @@ module Arb
|
|
54
53
|
File.write(".env", generate_dotenv)
|
55
54
|
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
def self.generate_dotenv(new_dotenv)
|
56
|
+
private_class_method def self.generate_dotenv(new_dotenv)
|
60
57
|
new_dotenv.slice(*env_keys).map { |k, v|
|
61
58
|
"#{k}=#{v}"
|
62
59
|
}.join("\n")
|
63
60
|
end
|
64
61
|
|
65
|
-
def self.create_dotenv!(existing_dotenv)
|
62
|
+
private_class_method def self.create_dotenv!(existing_dotenv)
|
66
63
|
new_dotenv = existing_dotenv.dup
|
67
64
|
|
68
|
-
puts "🎄 Welcome to Advent of Code in Ruby!
|
69
|
-
puts
|
65
|
+
puts "🎄 Welcome to Advent of Code in Ruby! 🎄"
|
66
|
+
puts
|
67
|
+
puts "Let's start with some configuration."
|
68
|
+
puts
|
70
69
|
|
71
70
|
unless existing_dotenv.has_key?("EDITOR_COMMAND")
|
72
71
|
puts "What's the shell command to start your editor? (default: #{default_editor_command})"
|
@@ -76,6 +75,7 @@ module Arb
|
|
76
75
|
new_dotenv["EDITOR_COMMAND"] = editor_command
|
77
76
|
end
|
78
77
|
|
78
|
+
puts
|
79
79
|
puts
|
80
80
|
|
81
81
|
unless existing_dotenv.has_key?("AOC_COOKIE")
|
@@ -85,7 +85,7 @@ module Arb
|
|
85
85
|
File.write(".env", generate_dotenv(new_dotenv))
|
86
86
|
end
|
87
87
|
|
88
|
-
def self.input_aoc_cookie
|
88
|
+
private_class_method def self.input_aoc_cookie
|
89
89
|
aoc_cookie = nil
|
90
90
|
|
91
91
|
loop do
|
@@ -97,6 +97,7 @@ module Arb
|
|
97
97
|
|
98
98
|
aoc_cookie = STDIN.gets.strip
|
99
99
|
|
100
|
+
puts
|
100
101
|
puts
|
101
102
|
|
102
103
|
break unless aoc_cookie.strip.empty?
|
@@ -105,32 +106,24 @@ module Arb
|
|
105
106
|
aoc_cookie
|
106
107
|
end
|
107
108
|
|
108
|
-
def self.create_other_files!
|
109
|
+
private_class_method def self.create_other_files!
|
109
110
|
other_files_created = []
|
110
111
|
|
111
112
|
unless Dir.exist?("src")
|
112
113
|
Dir.mkdir("src")
|
113
|
-
other_files_created <<
|
114
|
+
other_files_created << "src"
|
114
115
|
end
|
115
116
|
|
116
117
|
unless Dir.exist?("spec")
|
117
118
|
Dir.mkdir("spec")
|
118
|
-
other_files_created <<
|
119
|
-
end
|
120
|
-
|
121
|
-
unless File.exist?(".gitignore")
|
122
|
-
File.write(".gitignore", FILES.fetch(:gitignore))
|
123
|
-
other_files_created << :gitignore
|
119
|
+
other_files_created << "spec"
|
124
120
|
end
|
125
121
|
|
126
|
-
|
127
|
-
File.
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
unless File.exist?("Gemfile")
|
132
|
-
File.write("Gemfile", FILES.fetch(:gemfile))
|
133
|
-
other_files_created << :gemfile
|
122
|
+
FILES.slice(".gitignore", ".ruby-version", "Gemfile").each do |filename, contents|
|
123
|
+
unless File.exist?(filename)
|
124
|
+
File.write(filename, contents)
|
125
|
+
other_files_created << filename
|
126
|
+
end
|
134
127
|
end
|
135
128
|
|
136
129
|
spec_helper_path = File.join("spec", "spec_helper.rb")
|
@@ -138,16 +131,16 @@ module Arb
|
|
138
131
|
rspec_init_output = `rspec --init`
|
139
132
|
unless rspec_init_output.match?(/exist\s+spec.spec_helper.rb/)
|
140
133
|
original_spec_helper = File.read(spec_helper_path)
|
141
|
-
File.write(spec_helper_path, FILES.fetch(
|
142
|
-
other_files_created <<
|
134
|
+
File.write(spec_helper_path, FILES.fetch(spec_helper_path) + original_spec_helper)
|
135
|
+
other_files_created << spec_helper_path
|
143
136
|
end
|
144
|
-
other_files_created <<
|
137
|
+
other_files_created << ".rspec"
|
145
138
|
end
|
146
139
|
|
147
140
|
unless Git.repo_exists?
|
148
141
|
Git.init!
|
149
142
|
Git.commit_all!(message: "Initial commit")
|
150
|
-
other_files_created <<
|
143
|
+
other_files_created << ".git"
|
151
144
|
end
|
152
145
|
|
153
146
|
other_files_created
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Arb
|
2
2
|
module Cli
|
3
3
|
class YearDayValidator
|
4
|
-
def self.validate_year_and_day(year:, day:,
|
5
|
-
year, day = year&.to_s, day&.to_s
|
4
|
+
def self.validate_year_and_day(year:, day:, default_to_untracked_or_last_committed: false)
|
5
|
+
year, day = year&.to_s, day&.to_s&.rjust(2, "0")
|
6
6
|
|
7
7
|
# The first two digits of the year may be omitted.
|
8
8
|
year = "20#{year}" if year && year.length == 2
|
@@ -10,18 +10,17 @@ module Arb
|
|
10
10
|
if day && !year
|
11
11
|
raise InputError, "If you specify the day, specify the year also."
|
12
12
|
elsif !day
|
13
|
-
if
|
14
|
-
year, day = Git.
|
13
|
+
if !year && default_to_untracked_or_last_committed
|
14
|
+
year, day = Git.uncommitted_puzzles.keys.last
|
15
15
|
end
|
16
16
|
|
17
17
|
unless day
|
18
|
-
if year &&
|
19
|
-
|
20
|
-
day = "1"
|
18
|
+
if year && Git.committed_by_year[year].values.none?
|
19
|
+
day = "01"
|
21
20
|
else
|
22
|
-
year, day = Git.
|
21
|
+
year, day = Git.last_committed_puzzle(year:)
|
23
22
|
|
24
|
-
if day && !
|
23
|
+
if day && !default_to_untracked_or_last_committed
|
25
24
|
if day == "25"
|
26
25
|
day = :end
|
27
26
|
else
|
@@ -31,7 +30,7 @@ module Arb
|
|
31
30
|
|
32
31
|
if !day || day == :end
|
33
32
|
default_year = "2015"
|
34
|
-
default_day = "
|
33
|
+
default_day = "01"
|
35
34
|
bootstrap_year_prompt = nil
|
36
35
|
|
37
36
|
committed = Git.committed_by_year
|
@@ -46,7 +45,7 @@ module Arb
|
|
46
45
|
default_day = committed[default_year].values.index(false)
|
47
46
|
|
48
47
|
puts "You've recently finished #{year}. Yay!"
|
49
|
-
bootstrap_year_prompt = "What year do you want to bootstrap next? (default: #{default_year} [at Day #{default_day}])"
|
48
|
+
bootstrap_year_prompt = "What year do you want to bootstrap next? (default: #{default_year} [at Day #{default_day.to_s.delete_prefix("0")}])"
|
50
49
|
end
|
51
50
|
|
52
51
|
loop do
|
@@ -54,12 +53,14 @@ module Arb
|
|
54
53
|
print PASTEL.green("> ")
|
55
54
|
year_input = STDIN.gets.strip
|
56
55
|
puts
|
56
|
+
puts
|
57
|
+
|
57
58
|
if year_input.strip.empty?
|
58
59
|
year = default_year
|
59
60
|
day = default_day
|
60
61
|
else
|
61
62
|
year = year_input.strip.match(/\A\d{4}\z/)&.to_s
|
62
|
-
day = "
|
63
|
+
day = "01"
|
63
64
|
end
|
64
65
|
break if year
|
65
66
|
end
|
@@ -78,7 +79,7 @@ module Arb
|
|
78
79
|
raise InputError, "Day must be between 1 and 25, and <= today."
|
79
80
|
end
|
80
81
|
|
81
|
-
[year.to_s, day.to_s]
|
82
|
+
[year.to_s, day.to_s.rjust(2, "0")]
|
82
83
|
end
|
83
84
|
end
|
84
85
|
end
|
data/lib/arb/files/input.rb
CHANGED
@@ -1,19 +1,22 @@
|
|
1
1
|
module Arb
|
2
2
|
module Files
|
3
3
|
class Input
|
4
|
-
def self.
|
4
|
+
def self.path(year:, day:)
|
5
5
|
year_directory = File.join("input", year)
|
6
|
+
File.join(year_directory, "#{day}.txt")
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.download(year:, day:, notify_exists: true)
|
6
10
|
Dir.mkdir("input") unless Dir.exist?("input")
|
11
|
+
year_directory = File.join("input", year)
|
7
12
|
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
8
|
-
|
9
|
-
padded_day = day.rjust(2, "0")
|
10
|
-
file_path = File.join(year_directory, "#{padded_day}.txt")
|
13
|
+
file_path = File.join(year_directory, "#{day}.txt")
|
11
14
|
|
12
15
|
if File.exist?(file_path)
|
13
16
|
puts "Already exists: #{file_path}" if notify_exists
|
14
17
|
else
|
15
|
-
aoc_api = Api::Aoc.new(ENV["AOC_COOKIE"])
|
16
|
-
response = aoc_api.input(year
|
18
|
+
aoc_api = Api::Aoc.new(cookie: ENV["AOC_COOKIE"])
|
19
|
+
response = aoc_api.input(year:, day:)
|
17
20
|
|
18
21
|
File.write(file_path, response)
|
19
22
|
end
|
@@ -1,26 +1,24 @@
|
|
1
1
|
module Arb
|
2
2
|
module Files
|
3
3
|
class Instructions
|
4
|
-
def self.path(year
|
4
|
+
def self.path(year:, day:)
|
5
5
|
year_directory = File.join("instructions", year)
|
6
|
-
|
7
|
-
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
8
|
-
|
9
|
-
padded_day = day.rjust(2, "0")
|
10
|
-
|
11
|
-
File.join(year_directory, "#{padded_day}.md")
|
6
|
+
File.join(year_directory, "#{day}.md")
|
12
7
|
end
|
13
8
|
|
14
|
-
def self.download(year
|
15
|
-
|
9
|
+
def self.download(year:, day:, notify_exists: true, overwrite: false)
|
10
|
+
Dir.mkdir("instructions") unless Dir.exist?("instructions")
|
11
|
+
year_directory = File.join("instructions", year)
|
12
|
+
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
13
|
+
file_path = File.join(year_directory, "#{day}.md")
|
16
14
|
|
17
15
|
if File.exist?(file_path) && !overwrite
|
18
16
|
puts "Already exists: #{file_path}" if notify_exists
|
19
17
|
else
|
20
|
-
url = "https://adventofcode.com/#{year}/day/#{day}"
|
18
|
+
url = "https://adventofcode.com/#{year}/day/#{day.delete_prefix("0")}"
|
21
19
|
|
22
|
-
aoc_api = Api::Aoc.new(ENV["AOC_COOKIE"])
|
23
|
-
response = aoc_api.instructions(year
|
20
|
+
aoc_api = Api::Aoc.new(cookie: ENV["AOC_COOKIE"])
|
21
|
+
response = aoc_api.instructions(year:, day:)
|
24
22
|
instructions = response.match(/(?<=<main>).+(?=<\/main>)/m).to_s
|
25
23
|
markdown_instructions = ReverseMarkdown.convert(instructions).strip
|
26
24
|
markdown_instructions = markdown_instructions
|
@@ -1,20 +1,18 @@
|
|
1
1
|
module Arb
|
2
2
|
module Files
|
3
3
|
class OtherSolutions
|
4
|
-
def self.download(year
|
4
|
+
def self.download(year:, day:)
|
5
5
|
year_directory = File.join("others", year)
|
6
6
|
Dir.mkdir("others") unless Dir.exist?("others")
|
7
7
|
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
8
8
|
|
9
|
-
padded_day = day.rjust(2, "0")
|
10
|
-
|
11
9
|
file_paths = %w[1 2].map do |part|
|
12
|
-
file_path = File.join(year_directory, "#{
|
10
|
+
file_path = File.join(year_directory, "#{day}_#{part}.rb")
|
13
11
|
|
14
12
|
if File.exist?(file_path)
|
15
13
|
puts "Already exists: #{file_path}"
|
16
14
|
else
|
17
|
-
other_solutions = Api::OtherSolutions.new.other_solutions(year
|
15
|
+
other_solutions = Api::OtherSolutions.new.other_solutions(year:, day:, part:)
|
18
16
|
File.write(file_path, other_solutions)
|
19
17
|
end
|
20
18
|
|
@@ -1,29 +1,26 @@
|
|
1
1
|
module Arb
|
2
2
|
module Files
|
3
|
-
class
|
4
|
-
def self.create(year
|
3
|
+
class Solution
|
4
|
+
def self.create(year:, day:)
|
5
5
|
year_directory = File.join("src", year)
|
6
6
|
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
7
7
|
|
8
|
-
|
9
|
-
file_path = File.join(year_directory, "#{padded_day}.rb")
|
8
|
+
file_path = File.join(year_directory, "#{day}.rb")
|
10
9
|
|
11
10
|
if File.exist?(file_path)
|
12
11
|
puts "Already exists: #{file_path}"
|
13
12
|
else
|
14
|
-
File.write(file_path,
|
13
|
+
File.write(file_path, template(year:, day:))
|
15
14
|
end
|
16
15
|
|
17
16
|
file_path
|
18
17
|
end
|
19
18
|
|
20
|
-
def self.
|
21
|
-
|
22
|
-
|
23
|
-
<<~TPL
|
24
|
-
# https://adventofcode.com/#{year}/day/#{day}
|
19
|
+
def self.template(year:, day:)
|
20
|
+
<<~SRC
|
21
|
+
# https://adventofcode.com/#{year}/day/#{day.delete_prefix("0")}
|
25
22
|
module Year#{year}
|
26
|
-
class Day#{
|
23
|
+
class Day#{day}
|
27
24
|
def part_1(input_file)
|
28
25
|
lines = input_file.readlines(chomp: true)
|
29
26
|
|
@@ -37,7 +34,7 @@ module Arb
|
|
37
34
|
end
|
38
35
|
end
|
39
36
|
end
|
40
|
-
|
37
|
+
SRC
|
41
38
|
end
|
42
39
|
end
|
43
40
|
end
|
data/lib/arb/files/spec.rb
CHANGED
@@ -1,27 +1,24 @@
|
|
1
1
|
module Arb
|
2
2
|
module Files
|
3
3
|
class Spec
|
4
|
-
def self.create(year
|
4
|
+
def self.create(year:, day:, notify_exists: true)
|
5
5
|
year_directory = File.join("spec", year)
|
6
6
|
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
7
7
|
|
8
|
-
|
9
|
-
file_path = File.join(year_directory, "#{padded_day}_spec.rb")
|
8
|
+
file_path = File.join(year_directory, "#{day}_spec.rb")
|
10
9
|
|
11
10
|
if File.exist?(file_path)
|
12
11
|
puts "Already exists: #{file_path}" if notify_exists
|
13
12
|
else
|
14
|
-
File.write(file_path,
|
13
|
+
File.write(file_path, template(year:, day:))
|
15
14
|
end
|
16
15
|
|
17
16
|
file_path
|
18
17
|
end
|
19
18
|
|
20
|
-
def self.
|
21
|
-
|
22
|
-
|
23
|
-
<<~TPL
|
24
|
-
RSpec.describe Year#{year}::Day#{padded_day} do
|
19
|
+
def self.template(year:, day:)
|
20
|
+
<<~SPECS
|
21
|
+
RSpec.describe Year#{year}::Day#{day} do
|
25
22
|
let(:input) {
|
26
23
|
StringIO.new(
|
27
24
|
<<~IN
|
@@ -38,7 +35,7 @@ module Arb
|
|
38
35
|
expect(subject.part_2(input)).to eq(:todo)
|
39
36
|
end
|
40
37
|
end
|
41
|
-
|
38
|
+
SPECS
|
42
39
|
end
|
43
40
|
end
|
44
41
|
end
|
data/lib/arb/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: advent_of_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felipe Vogel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: debug
|
@@ -137,7 +137,7 @@ files:
|
|
137
137
|
- lib/arb/cli/bootstrap.rb
|
138
138
|
- lib/arb/cli/commit.rb
|
139
139
|
- lib/arb/cli/progress.rb
|
140
|
-
- lib/arb/cli/
|
140
|
+
- lib/arb/cli/run.rb
|
141
141
|
- lib/arb/cli/shared/git.rb
|
142
142
|
- lib/arb/cli/shared/runner.rb
|
143
143
|
- lib/arb/cli/shared/working_directory.rb
|
@@ -145,7 +145,7 @@ files:
|
|
145
145
|
- lib/arb/files/input.rb
|
146
146
|
- lib/arb/files/instructions.rb
|
147
147
|
- lib/arb/files/other_solutions.rb
|
148
|
-
- lib/arb/files/
|
148
|
+
- lib/arb/files/solution.rb
|
149
149
|
- lib/arb/files/spec.rb
|
150
150
|
- lib/arb/version.rb
|
151
151
|
homepage: https://github.com/fpsvogel/advent_of_ruby
|
data/lib/arb/cli/run_day.rb
DELETED
@@ -1,140 +0,0 @@
|
|
1
|
-
module Arb
|
2
|
-
module Cli
|
3
|
-
# @param year [String, Integer]
|
4
|
-
# @param day [String, Integer]
|
5
|
-
# @param options [Thor::CoreExt::HashWithIndifferentAccess] see `method_option`s
|
6
|
-
# above ArbApp#run_day.
|
7
|
-
def self.run_day(year:, day:, options:)
|
8
|
-
WorkingDirectory.prepare!
|
9
|
-
|
10
|
-
if options.spec? && (options.real_part_1? || options.real_part_2?)
|
11
|
-
raise InputError, "Don't use --spec (-s) with --real_part_1 (-o) or --real_part_2 (-t)"
|
12
|
-
end
|
13
|
-
|
14
|
-
year, day = YearDayValidator.validate_year_and_day(year:, day:, default_untracked_or_done: true)
|
15
|
-
|
16
|
-
if Git.new_solutions.none? && !Git.last_committed_solution(year:)
|
17
|
-
bootstrap(year:, day:)
|
18
|
-
return
|
19
|
-
end
|
20
|
-
|
21
|
-
solution = Runner.load_solution(year, day)
|
22
|
-
input_path = Files::Input.download(year, day, notify_exists: false)
|
23
|
-
answer_1, answer_2 = nil, nil
|
24
|
-
|
25
|
-
instructions_path = Files::Instructions.download(year, day, notify_exists: false, overwrite: false)
|
26
|
-
instructions = File.read(instructions_path)
|
27
|
-
correct_answer_1, correct_answer_2 = instructions.scan(/Your puzzle answer was `([^`]+)`./).flatten
|
28
|
-
skip_count = 0
|
29
|
-
|
30
|
-
if options.spec?
|
31
|
-
run_specs_only(year, day)
|
32
|
-
return
|
33
|
-
elsif !(options.real_part_1? || options.real_part_2?)
|
34
|
-
specs_passed, skip_count = run_specs_before_real(year, day)
|
35
|
-
return unless specs_passed
|
36
|
-
puts "👍 Specs passed!"
|
37
|
-
if skip_count > 1 || (skip_count == 1 && correct_answer_1)
|
38
|
-
puts PASTEL.yellow.bold("🤐 #{skip_count} skipped, however")
|
39
|
-
end
|
40
|
-
puts "\n"
|
41
|
-
end
|
42
|
-
|
43
|
-
if options.real_part_1? || (!options.real_part_2? && ((correct_answer_1.nil? && skip_count <= 1) || correct_answer_2))
|
44
|
-
answer_1 = Runner.run_part("#{year}##{day}.1", correct_answer_1) do
|
45
|
-
solution.part_1(File.new(input_path))
|
46
|
-
end
|
47
|
-
end
|
48
|
-
if options.real_part_2? || (!options.real_part_1? && ((correct_answer_1 && !correct_answer_2 && skip_count.zero?) || correct_answer_2))
|
49
|
-
answer_2 = Runner.run_part("#{year}##{day}.2", correct_answer_2) do
|
50
|
-
solution.part_2(File.new(input_path))
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
return unless answer_1 || answer_2
|
55
|
-
|
56
|
-
if correct_answer_2
|
57
|
-
puts "🙌 You've already submitted the answers to both parts.\n\n"
|
58
|
-
|
59
|
-
if Git.commit_count <= 1
|
60
|
-
puts "When you're done with this puzzle, run " \
|
61
|
-
"`#{PASTEL.blue.bold("arb commit")}` (or `arb c`) commit your solution to Git.\n"
|
62
|
-
end
|
63
|
-
|
64
|
-
return
|
65
|
-
elsif options.real_part_1? && correct_answer_1
|
66
|
-
puts "🙌 You've already submitted the answer to this part.\n\n"
|
67
|
-
return
|
68
|
-
end
|
69
|
-
|
70
|
-
puts "Submit solution? (Y/n)"
|
71
|
-
print PASTEL.green("> ")
|
72
|
-
submit = STDIN.gets.strip.downcase
|
73
|
-
|
74
|
-
if submit == "y" || submit == ""
|
75
|
-
options_part = options.real_part_1? ? "1" : (options.real_part_2? ? "2" : nil)
|
76
|
-
inferred_part = correct_answer_1.nil? ? "1" : "2"
|
77
|
-
aoc_api = Api::Aoc.new(ENV["AOC_COOKIE"])
|
78
|
-
|
79
|
-
response = aoc_api.submit(year, day, options_part || inferred_part, answer_2 || answer_1)
|
80
|
-
message = response.match(/(?<=<article>).+(?=<\/article>)/m).to_s.strip
|
81
|
-
markdown_message = ReverseMarkdown.convert(message)
|
82
|
-
short_message = markdown_message
|
83
|
-
.sub(/\n\n.+/m, "")
|
84
|
-
.sub(/ \[\[.+/, "")
|
85
|
-
|
86
|
-
if short_message.start_with?("That's the right answer!")
|
87
|
-
puts "⭐ #{short_message}\n"
|
88
|
-
|
89
|
-
# TODO don't re-download if the instructions file already contains the next part
|
90
|
-
instructions_path = Files::Instructions.download(year, day, overwrite: true)
|
91
|
-
|
92
|
-
if (options_part || inferred_part) == "1"
|
93
|
-
puts "\nDownloaded instructions for Part Two.\n\n"
|
94
|
-
`#{ENV["EDITOR_COMMAND"]} #{instructions_path}`
|
95
|
-
|
96
|
-
spec_path = Files::Spec.create(year, day, notify_exists: false)
|
97
|
-
spec = File.read(spec_path)
|
98
|
-
spec_without_skips = spec.gsub(" xit ", " it ")
|
99
|
-
File.write(spec_path, spec_without_skips)
|
100
|
-
end
|
101
|
-
|
102
|
-
if Git.commit_count <= 1
|
103
|
-
puts "\n\nNow it's time to improve your solution! Be sure to look " \
|
104
|
-
"at other people's solutions (in the \"others\" directory). When " \
|
105
|
-
"you're done, run `#{PASTEL.blue.bold("arb commit")}` (or `arb c`) " \
|
106
|
-
"to commit your solution to Git.\n"
|
107
|
-
end
|
108
|
-
else
|
109
|
-
puts "❌ #{short_message}"
|
110
|
-
end
|
111
|
-
end
|
112
|
-
rescue AppError => e
|
113
|
-
puts PASTEL.red(e.message)
|
114
|
-
end
|
115
|
-
|
116
|
-
private_class_method def self.run_specs_only(year, day)
|
117
|
-
padded_day = day.rjust(2, "0")
|
118
|
-
spec_filename = [File.join("spec", year, "#{padded_day}_spec.rb")]
|
119
|
-
|
120
|
-
RSpec::Core::Runner.run(spec_filename)
|
121
|
-
end
|
122
|
-
|
123
|
-
private_class_method def self.run_specs_before_real(year, day)
|
124
|
-
padded_day = day.rjust(2, "0")
|
125
|
-
spec_filename = [File.join("spec", year, "#{padded_day}_spec.rb")]
|
126
|
-
|
127
|
-
out = StringIO.new
|
128
|
-
RSpec::Core::Runner.run(spec_filename, $stderr, out)
|
129
|
-
|
130
|
-
if out.string.match?(/Failures:/)
|
131
|
-
RSpec.reset
|
132
|
-
RSpec::Core::Runner.run(spec_filename)
|
133
|
-
|
134
|
-
[false, nil]
|
135
|
-
else
|
136
|
-
[true, out.string.scan("skipped with xit").count]
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|