advent_of_ruby 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|