advent_of_ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/arb +46 -0
- data/lib/arb/api/aoc.rb +51 -0
- data/lib/arb/api/other_solutions.rb +101 -0
- data/lib/arb/arb.rb +23 -0
- data/lib/arb/cli/bootstrap.rb +36 -0
- data/lib/arb/cli/commit.rb +36 -0
- data/lib/arb/cli/progress.rb +33 -0
- data/lib/arb/cli/run_day.rb +140 -0
- data/lib/arb/cli/shared/git.rb +82 -0
- data/lib/arb/cli/shared/runner.rb +42 -0
- data/lib/arb/cli/shared/working_directory.rb +157 -0
- data/lib/arb/cli/shared/year_day_validator.rb +85 -0
- data/lib/arb/files/input.rb +25 -0
- data/lib/arb/files/instructions.rb +40 -0
- data/lib/arb/files/other_solutions.rb +28 -0
- data/lib/arb/files/source.rb +44 -0
- data/lib/arb/files/spec.rb +45 -0
- data/lib/arb/version.rb +3 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a387ce043139b5090d50ac18e76a0687f248fa75dc7e4ba098780f9e739b8687
|
4
|
+
data.tar.gz: e805613b4a188fafa0a0d17499893abb2edd26e5e33c2367fa717a9aa8a827c9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a731b8c4449af8c8d89a542bc4979ed96edb8972802177709425e62ac8270c1c89fddf71f2c3239ebc430693fdc917b633f53f2006d59c5ce16e027530621203
|
7
|
+
data.tar.gz: 1c87883119e89399b0c7dc6c41a7472142c863d605bca45eafeb2d8b32a51fae11f77474655bef263d1e0696d9ad49d1e8bb84cb406dfb8cf1554390e7b99641
|
data/bin/arb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require_relative "../lib/arb/arb"
|
5
|
+
|
6
|
+
# The CLI application
|
7
|
+
class ArbApp < Thor
|
8
|
+
desc "bootstrap [YEAR] [DAY]", "Creates puzzle files for the next day, or " \
|
9
|
+
"for a given year and optionally day (defaults to Day 1)."
|
10
|
+
def bootstrap(year = nil, day = nil)
|
11
|
+
Arb::Cli.bootstrap(year:, day:)
|
12
|
+
end
|
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
|
+
desc "run [YEAR] [DAY]", "Runs the puzzle that's untracked in Git, or the " \
|
21
|
+
"puzzle of the given day and year. `-s` runs only specs, `-o` part 1, `-t` part 2."
|
22
|
+
method_option :spec, type: :boolean, aliases: "-s"
|
23
|
+
method_option :real_part_1, type: :boolean, aliases: "-o"
|
24
|
+
method_option :real_part_2, type: :boolean, aliases: "-t"
|
25
|
+
def run_day(year = nil, day = nil)
|
26
|
+
Arb::Cli.run_day(year:, day:, options:)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "progress", "Shows progress based on the number of your solutions " \
|
30
|
+
"committed in Git."
|
31
|
+
def progress
|
32
|
+
Arb::Cli.progress
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO extract/abstract run_day contents (without terminal output) to a class,
|
36
|
+
# then use it here to silently run each untracked puzzle before committing it,
|
37
|
+
# and if anything is incorrect then show confirmation prompt to user.
|
38
|
+
desc "commit", "Commits new and modified solutions to Git."
|
39
|
+
def commit
|
40
|
+
Arb::Cli.commit
|
41
|
+
end
|
42
|
+
|
43
|
+
default_task :run_day
|
44
|
+
end
|
45
|
+
|
46
|
+
ArbApp.start
|
data/lib/arb/api/aoc.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Arb
|
2
|
+
module Api
|
3
|
+
class Aoc
|
4
|
+
private attr_reader :connection
|
5
|
+
|
6
|
+
def initialize(cookie)
|
7
|
+
@connection = Faraday.new(
|
8
|
+
url: "https://adventofcode.com",
|
9
|
+
headers: {
|
10
|
+
"Cookie" => "session=#{cookie}",
|
11
|
+
"User-Agent" => "github.com/fpsvogel/advent_of_ruby by fps.vogel@gmail.com",
|
12
|
+
}
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def input(year, day)
|
17
|
+
logged_in {
|
18
|
+
connection.get("/#{year}/day/#{day}/input")
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def instructions(year, day)
|
23
|
+
logged_in {
|
24
|
+
connection.get("/#{year}/day/#{day}")
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def submit(year, day, part, answer)
|
29
|
+
logged_in {
|
30
|
+
connection.post(
|
31
|
+
"/#{year}/day/#{day}/answer",
|
32
|
+
"level=#{part}&answer=#{answer}",
|
33
|
+
)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
LOGGED_OUT_RESPONSE = "Puzzle inputs differ by user. Please log in to get your puzzle input.\n"
|
40
|
+
|
41
|
+
def logged_in(&block)
|
42
|
+
while (response = block.call.body) == LOGGED_OUT_RESPONSE
|
43
|
+
Config.refresh_aoc_cookie!
|
44
|
+
initialize(ENV["AOC_COOKIE"])
|
45
|
+
end
|
46
|
+
|
47
|
+
response
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Arb
|
2
|
+
module Api
|
3
|
+
class OtherSolutions
|
4
|
+
UI_URI = "https://github.com"
|
5
|
+
|
6
|
+
private attr_reader :connection
|
7
|
+
|
8
|
+
PATHS = {
|
9
|
+
"eregon" => ->(year, day, part) {
|
10
|
+
if part == "1"
|
11
|
+
[
|
12
|
+
"adventofcode/tree/master/#{year}/#{day}.rb",
|
13
|
+
"adventofcode/tree/master/#{year}/#{day}a.rb",
|
14
|
+
]
|
15
|
+
elsif part == "2"
|
16
|
+
["adventofcode/tree/master/#{year}/#{day}b.rb"]
|
17
|
+
end
|
18
|
+
},
|
19
|
+
"gchan" => ->(year, day, part) {
|
20
|
+
["advent-of-code-ruby/tree/main/#{year}/day-#{day.rjust(2, "0")}/day-#{day.rjust(2, "0")}-part-#{part}.rb"]
|
21
|
+
},
|
22
|
+
"ahorner" => ->(year, day, part) {
|
23
|
+
return [] if part == "1"
|
24
|
+
["advent-of-code/tree/main/lib/#{year}/#{day.rjust(2, "0")}.rb"]
|
25
|
+
},
|
26
|
+
"ZogStriP" => ->(year, day, part) {
|
27
|
+
return [] if part == "1"
|
28
|
+
|
29
|
+
puzzle_name = File.read(Files::Instructions.path(year, day))
|
30
|
+
.match(/## --- Day \d\d?: (.+)(?= ---)/)
|
31
|
+
.captures
|
32
|
+
.first
|
33
|
+
.downcase
|
34
|
+
.gsub(" ", "_")
|
35
|
+
|
36
|
+
["adventofcode/tree/master/#{year}/#{day.rjust(2, "0")}_#{puzzle_name}.rb"]
|
37
|
+
},
|
38
|
+
"erikw" => ->(year, day, part) {
|
39
|
+
["advent-of-code-solutions/tree/main/#{year}/#{day.rjust(2, "0")}/part#{part}.rb"]
|
40
|
+
},
|
41
|
+
}
|
42
|
+
|
43
|
+
EDITS = {
|
44
|
+
"gchan" => ->(file_str) {
|
45
|
+
# Remove the first 5 lines (boilerplate).
|
46
|
+
file_str.lines[5..].join
|
47
|
+
},
|
48
|
+
"ZogStriP" => ->(file_str) {
|
49
|
+
# Remove input at the end of the file.
|
50
|
+
file_str.split("\n__END__").first
|
51
|
+
},
|
52
|
+
"erikw" => ->(file_str) {
|
53
|
+
# Remove the first 3 lines (boilerplate).
|
54
|
+
file_str.lines[3..].join
|
55
|
+
},
|
56
|
+
}
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@connection = Faraday.new(
|
60
|
+
url: "https://raw.githubusercontent.com",
|
61
|
+
headers: {
|
62
|
+
"User-Agent" => "github.com/fpsvogel/advent_of_ruby by fps.vogel@gmail.com",
|
63
|
+
}
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def other_solutions(year, day, part)
|
68
|
+
"# #{year} Day #{day} Part #{part}\n\n" +
|
69
|
+
PATHS
|
70
|
+
.map { |username, path_builder|
|
71
|
+
actual_path = nil
|
72
|
+
solution = nil
|
73
|
+
paths = path_builder.call(year, day, part)
|
74
|
+
|
75
|
+
paths.each do |path|
|
76
|
+
next if solution
|
77
|
+
response = connection.get("/#{username}/#{path.sub("/tree/", "/")}")
|
78
|
+
next if response.status == 404
|
79
|
+
|
80
|
+
actual_path = path
|
81
|
+
solution = (EDITS[username] || :itself.to_proc).call(response.body)
|
82
|
+
end
|
83
|
+
|
84
|
+
if solution
|
85
|
+
<<~TPL
|
86
|
+
# ------------------------------------------------------------------------------
|
87
|
+
# #{username}: #{UI_URI}/#{username}/#{actual_path}
|
88
|
+
# ------------------------------------------------------------------------------
|
89
|
+
|
90
|
+
#{solution}
|
91
|
+
|
92
|
+
TPL
|
93
|
+
end
|
94
|
+
}
|
95
|
+
.compact
|
96
|
+
.join
|
97
|
+
.strip + "\n"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/arb/arb.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "benchmark"
|
2
|
+
require "date"
|
3
|
+
require "debug"
|
4
|
+
require "dotenv"
|
5
|
+
require "faraday"
|
6
|
+
require "pastel"
|
7
|
+
require "reverse_markdown"
|
8
|
+
require "rspec/core"
|
9
|
+
|
10
|
+
class AppError < StandardError; end
|
11
|
+
class InputError < AppError; end
|
12
|
+
class ConfigError < AppError; end
|
13
|
+
|
14
|
+
PASTEL = Pastel.new
|
15
|
+
|
16
|
+
Dir[File.join(__dir__, "**", "*.rb")].each do |file|
|
17
|
+
require file
|
18
|
+
end
|
19
|
+
|
20
|
+
solution_files = File.join(Dir.pwd, "src", "**", "*.rb")
|
21
|
+
Dir[solution_files].each do |file|
|
22
|
+
require file
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
# @param year [String, Integer]
|
4
|
+
# @param day [String, Integer]
|
5
|
+
def self.bootstrap(year: nil, day: nil)
|
6
|
+
WorkingDirectory.prepare!
|
7
|
+
|
8
|
+
year, day = YearDayValidator.validate_year_and_day(year:, day:)
|
9
|
+
|
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
|
+
source_path = Files::Source.create(year, day)
|
14
|
+
spec_path = Files::Spec.create(year, day)
|
15
|
+
|
16
|
+
puts "🤘 Bootstrapped #{year}##{day}\n\n"
|
17
|
+
|
18
|
+
# Open the new files.
|
19
|
+
`#{ENV["EDITOR_COMMAND"]} #{others_1_path}`
|
20
|
+
`#{ENV["EDITOR_COMMAND"]} #{others_2_path}`
|
21
|
+
`#{ENV["EDITOR_COMMAND"]} #{input_path}`
|
22
|
+
`#{ENV["EDITOR_COMMAND"]} #{source_path}`
|
23
|
+
`#{ENV["EDITOR_COMMAND"]} #{spec_path}`
|
24
|
+
`#{ENV["EDITOR_COMMAND"]} #{instructions_path}`
|
25
|
+
|
26
|
+
if Git.commit_count <= 1
|
27
|
+
puts "Now fill in the spec for Part One with an example from the instructions, " \
|
28
|
+
"then run it with `#{PASTEL.blue.bold("arb run")}` (or just `arb`) as " \
|
29
|
+
"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.\n"
|
31
|
+
end
|
32
|
+
rescue AppError => e
|
33
|
+
puts Pastel.new.red(e.message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
def self.commit
|
4
|
+
WorkingDirectory.prepare!
|
5
|
+
|
6
|
+
change_year, change_day = Git.new_solutions.first
|
7
|
+
unless change_year
|
8
|
+
files_modified = true
|
9
|
+
change_year, change_day = Git.modified_solutions.first
|
10
|
+
end
|
11
|
+
|
12
|
+
unless change_year
|
13
|
+
puts "Nothing to commit."
|
14
|
+
|
15
|
+
if Git.commit_count <= 2
|
16
|
+
puts "\nRun `#{PASTEL.blue.bold("arb bootstrap")}` (or `arb b`) to start the next puzzle."
|
17
|
+
end
|
18
|
+
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
message = "#{files_modified ? "Improve" : "Solve"} #{change_year}##{change_day}"
|
23
|
+
Git.commit_all!(message:)
|
24
|
+
|
25
|
+
# TODO less naive check: ensure prev. days are finished too
|
26
|
+
if !files_modified && change_day == "25"
|
27
|
+
puts "\n🎉 You've finished #{change_year}!\n\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
if Git.commit_count <= 1
|
31
|
+
puts "Solution committed! When you're ready to start the next puzzle, run " \
|
32
|
+
"`#{PASTEL.blue.bold("arb bootstrap")}` (or `arb b`)."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
def self.progress
|
4
|
+
WorkingDirectory.prepare!
|
5
|
+
|
6
|
+
committed = Git.committed_by_year
|
7
|
+
|
8
|
+
total_count = committed.values.sum(&:count)
|
9
|
+
my_counts_by_year = committed
|
10
|
+
.transform_values { _1.values.count(&:itself) }
|
11
|
+
.reject { |k, v| v.zero? }
|
12
|
+
my_total_count = my_counts_by_year.values.sum
|
13
|
+
|
14
|
+
total_percent = (my_total_count.to_f / total_count * 100).round(1)
|
15
|
+
total_percent = total_percent == total_percent.to_i ? total_percent.to_i : total_percent
|
16
|
+
|
17
|
+
puts "You have completed:\n\n"
|
18
|
+
puts PASTEL.bold "#{PASTEL.blue("All:")}\t#{total_percent}% \t#{my_total_count}/#{total_count} puzzles\n"
|
19
|
+
|
20
|
+
my_counts_by_year.each do |year, my_count|
|
21
|
+
if year.to_i == Date.today.year
|
22
|
+
year_count = this_year_count
|
23
|
+
else
|
24
|
+
year_count = 25
|
25
|
+
end
|
26
|
+
|
27
|
+
year_percent = (my_count.to_f / year_count * 100).round
|
28
|
+
|
29
|
+
puts "#{PASTEL.blue("#{year}:")}\t#{year_percent}%\t#{my_count}/#{year_count}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,140 @@
|
|
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
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
class Git
|
4
|
+
# Years and days of uncommitted new solutions, or an empty array.
|
5
|
+
# @return [Array<Array(String, String)>]
|
6
|
+
def self.new_solutions
|
7
|
+
output = `git status -su | grep -e "^?? src" -e "^?? spec" -e "^A src" -e "^A spec"`
|
8
|
+
output.scan(/(?<year>\d{4})\/(?<day>\d\d)(?:_spec)?.rb$/).uniq
|
9
|
+
end
|
10
|
+
|
11
|
+
# Years and days of modified existing solutions, or an empty array.
|
12
|
+
# @return [Array<Array(String, String)>]
|
13
|
+
def self.modified_solutions
|
14
|
+
output = `git status -su | grep -e "^ M src" -e "^ M spec" -e "^M src" -e "^M spec"`
|
15
|
+
output.scan(/(?<year>\d{4})\/(?<day>\d\d)(?:_spec)?.rb$/).uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
# Year and day of the latest-date solution in the most recent commit, or nil.
|
19
|
+
# @return [Array(String, String), nil]
|
20
|
+
def self.last_committed_solution(year: nil, exclude_year: nil)
|
21
|
+
if exclude_year
|
22
|
+
output = `git log -n 1 --diff-filter=A --name-only --pretty=format: -- src spec ':!src/#{exclude_year}' ':!spec/#{exclude_year}'`
|
23
|
+
else
|
24
|
+
if year && !Dir.exist?(File.join("src", year))
|
25
|
+
return nil
|
26
|
+
else
|
27
|
+
output = `git log -n 1 --diff-filter=A --name-only --pretty=format: #{File.join("src", year || "")} #{File.join("spec", year || "")}`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return nil if output.empty?
|
32
|
+
|
33
|
+
output.scan(/(?<year>\d{4})\/(?<day>\d\d)(?:_spec)?.rb$/).last
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Integer]
|
37
|
+
def self.commit_count
|
38
|
+
`git rev-list HEAD --count`.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.committed_by_year
|
42
|
+
committed_solution_files = `git log --diff-filter=A --name-only --pretty=format: src`
|
43
|
+
|
44
|
+
previous_days = (2015..Date.today.year - 1).map { [_1, (1..25).to_a] }.to_h
|
45
|
+
previous_days.merge!(Date.today.year => (1..Date.today.day)) if Date.today.month == 12
|
46
|
+
|
47
|
+
committed_days = committed_solution_files
|
48
|
+
.split("\n")
|
49
|
+
.reject(&:empty?)
|
50
|
+
.map {
|
51
|
+
match = _1.match(/(?<year>\d{4})\/(?<day>\d\d).rb$/)
|
52
|
+
year, day = match[:year], match[:day]
|
53
|
+
[year.to_i, day.to_i]
|
54
|
+
}
|
55
|
+
.group_by(&:first)
|
56
|
+
.transform_values { _1.map(&:last) }
|
57
|
+
|
58
|
+
previous_days.map { |year, days|
|
59
|
+
days_hash = days.map { |day|
|
60
|
+
[day, committed_days.has_key?(year) && committed_days[year].include?(day)]
|
61
|
+
}.to_h
|
62
|
+
|
63
|
+
[year, days_hash]
|
64
|
+
}.to_h
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Boolean]
|
68
|
+
def self.repo_exists?
|
69
|
+
`git rev-parse --is-inside-work-tree 2> /dev/null`.chomp == "true"
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.init!
|
73
|
+
`git init`
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.commit_all!(message:)
|
77
|
+
`git add -A`
|
78
|
+
`git commit -m "#{message}"`
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
class Runner
|
4
|
+
def self.load_solution(year, day)
|
5
|
+
padded_day = day.rjust(2, "0")
|
6
|
+
Module.const_get("Year#{year}").const_get("Day#{padded_day}").new
|
7
|
+
rescue NameError
|
8
|
+
puts "There is no solution for this puzzle"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.run_part(part_name, correct_answer)
|
12
|
+
answer = nil
|
13
|
+
t = Benchmark.realtime do
|
14
|
+
answer = yield
|
15
|
+
if answer
|
16
|
+
puts "Result for #{part_name}:"
|
17
|
+
|
18
|
+
if correct_answer
|
19
|
+
if answer.to_s == correct_answer
|
20
|
+
puts PASTEL.green.bold("✅ #{answer}")
|
21
|
+
else
|
22
|
+
puts PASTEL.red.bold("❌ #{answer} -- should be #{correct_answer}")
|
23
|
+
end
|
24
|
+
else
|
25
|
+
puts PASTEL.bright_white.bold(answer)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
puts "❌ No result for #{part_name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if answer
|
33
|
+
puts "(obtained in #{t.round(10)} seconds)"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts
|
37
|
+
|
38
|
+
answer
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
class WorkingDirectory
|
4
|
+
FILES = {
|
5
|
+
gitignore: <<~FILE,
|
6
|
+
input/**/*
|
7
|
+
instructions/**/*
|
8
|
+
others/**/*
|
9
|
+
.env
|
10
|
+
|
11
|
+
FILE
|
12
|
+
ruby_version: "3.3.0\n",
|
13
|
+
gemfile: <<~FILE,
|
14
|
+
source "https://rubygems.org"
|
15
|
+
ruby file: ".ruby-version"
|
16
|
+
|
17
|
+
FILE
|
18
|
+
spec_helper_addition: <<~FILE
|
19
|
+
require "debug"
|
20
|
+
|
21
|
+
Dir[File.join(__dir__, "..", "src", "**", "*.rb")].each do |file|
|
22
|
+
require file
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
FILE
|
27
|
+
}
|
28
|
+
|
29
|
+
def self.env_keys = ["EDITOR_COMMAND", "AOC_COOKIE"]
|
30
|
+
def self.default_editor_command = "code"
|
31
|
+
|
32
|
+
def self.prepare!
|
33
|
+
files_created = []
|
34
|
+
|
35
|
+
existing_dotenv = Dotenv.parse(".env")
|
36
|
+
unless env_keys.all? { existing_dotenv.has_key?(_1) }
|
37
|
+
create_dotenv!(existing_dotenv)
|
38
|
+
files_created << :dotenv
|
39
|
+
end
|
40
|
+
Dotenv.load
|
41
|
+
Dotenv.require_keys(*env_keys)
|
42
|
+
|
43
|
+
files_created += create_other_files!
|
44
|
+
|
45
|
+
if files_created.any?
|
46
|
+
puts "✅ Initial files created and committed to a new Git repository.\n\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.refresh_aoc_cookie!
|
51
|
+
print "Uh oh, your Advent of Code session cookie has expired or was " \
|
52
|
+
"incorrectly entered. "
|
53
|
+
ENV["AOC_COOKIE"] = input_aoc_cookie
|
54
|
+
File.write(".env", generate_dotenv)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.generate_dotenv(new_dotenv)
|
60
|
+
new_dotenv.slice(*env_keys).map { |k, v|
|
61
|
+
"#{k}=#{v}"
|
62
|
+
}.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.create_dotenv!(existing_dotenv)
|
66
|
+
new_dotenv = existing_dotenv.dup
|
67
|
+
|
68
|
+
puts "🎄 Welcome to Advent of Code in Ruby! 🎄\n\n"
|
69
|
+
puts "Let's start with some configuration.\n\n"
|
70
|
+
|
71
|
+
unless existing_dotenv.has_key?("EDITOR_COMMAND")
|
72
|
+
puts "What's the shell command to start your editor? (default: #{default_editor_command})"
|
73
|
+
print PASTEL.green("> ")
|
74
|
+
editor_command = STDIN.gets.strip
|
75
|
+
editor_command = default_editor_command if editor_command.empty?
|
76
|
+
new_dotenv["EDITOR_COMMAND"] = editor_command
|
77
|
+
end
|
78
|
+
|
79
|
+
puts
|
80
|
+
|
81
|
+
unless existing_dotenv.has_key?("AOC_COOKIE")
|
82
|
+
new_dotenv["AOC_COOKIE"] = input_aoc_cookie
|
83
|
+
end
|
84
|
+
|
85
|
+
File.write(".env", generate_dotenv(new_dotenv))
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.input_aoc_cookie
|
89
|
+
aoc_cookie = nil
|
90
|
+
|
91
|
+
loop do
|
92
|
+
puts "What's your Advent of Code session cookie?"
|
93
|
+
puts PASTEL.dark.white("To find it, log in to [Advent of Code](https://adventofcode.com/) and then:")
|
94
|
+
puts PASTEL.dark.white(" Firefox: Developer Tools ⇨ Storage tab ⇨ Cookies")
|
95
|
+
puts PASTEL.dark.white(" Chrome: Developer Tools ⇨ Application tab ⇨ Cookies")
|
96
|
+
print PASTEL.green("> ")
|
97
|
+
|
98
|
+
aoc_cookie = STDIN.gets.strip
|
99
|
+
|
100
|
+
puts
|
101
|
+
|
102
|
+
break unless aoc_cookie.strip.empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
aoc_cookie
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.create_other_files!
|
109
|
+
other_files_created = []
|
110
|
+
|
111
|
+
unless Dir.exist?("src")
|
112
|
+
Dir.mkdir("src")
|
113
|
+
other_files_created << :src_dir
|
114
|
+
end
|
115
|
+
|
116
|
+
unless Dir.exist?("spec")
|
117
|
+
Dir.mkdir("spec")
|
118
|
+
other_files_created << :spec_dir
|
119
|
+
end
|
120
|
+
|
121
|
+
unless File.exist?(".gitignore")
|
122
|
+
File.write(".gitignore", FILES.fetch(:gitignore))
|
123
|
+
other_files_created << :gitignore
|
124
|
+
end
|
125
|
+
|
126
|
+
unless File.exist?(".ruby-version")
|
127
|
+
File.write(".ruby-version", FILES.fetch(:ruby_version))
|
128
|
+
other_files_created << :ruby_version
|
129
|
+
end
|
130
|
+
|
131
|
+
unless File.exist?("Gemfile")
|
132
|
+
File.write("Gemfile", FILES.fetch(:gemfile))
|
133
|
+
other_files_created << :gemfile
|
134
|
+
end
|
135
|
+
|
136
|
+
spec_helper_path = File.join("spec", "spec_helper.rb")
|
137
|
+
unless File.exist?(".rspec") && File.exist?(spec_helper_path)
|
138
|
+
rspec_init_output = `rspec --init`
|
139
|
+
unless rspec_init_output.match?(/exist\s+spec.spec_helper.rb/)
|
140
|
+
original_spec_helper = File.read(spec_helper_path)
|
141
|
+
File.write(spec_helper_path, FILES.fetch(:spec_helper_addition) + original_spec_helper)
|
142
|
+
other_files_created << :spec_helper
|
143
|
+
end
|
144
|
+
other_files_created << :rspec
|
145
|
+
end
|
146
|
+
|
147
|
+
unless Git.repo_exists?
|
148
|
+
Git.init!
|
149
|
+
Git.commit_all!(message: "Initial commit")
|
150
|
+
other_files_created << :git
|
151
|
+
end
|
152
|
+
|
153
|
+
other_files_created
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Arb
|
2
|
+
module Cli
|
3
|
+
class YearDayValidator
|
4
|
+
def self.validate_year_and_day(year:, day:, default_untracked_or_done: false)
|
5
|
+
year, day = year&.to_s, day&.to_s
|
6
|
+
|
7
|
+
# The first two digits of the year may be omitted.
|
8
|
+
year = "20#{year}" if year && year.length == 2
|
9
|
+
|
10
|
+
if day && !year
|
11
|
+
raise InputError, "If you specify the day, specify the year also."
|
12
|
+
elsif !day
|
13
|
+
if default_untracked_or_done
|
14
|
+
year, day = Git.new_solutions.last
|
15
|
+
end
|
16
|
+
|
17
|
+
unless day
|
18
|
+
if year && !Dir.exist?(File.join("src", year))
|
19
|
+
Dir.mkdir(File.join("src", year))
|
20
|
+
day = "1"
|
21
|
+
else
|
22
|
+
year, day = Git.last_committed_solution(year:)
|
23
|
+
|
24
|
+
if day && !default_untracked_or_done
|
25
|
+
if day == "25"
|
26
|
+
day = :end
|
27
|
+
else
|
28
|
+
day = day.next
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if !day || day == :end
|
33
|
+
default_year = "2015"
|
34
|
+
default_day = "1"
|
35
|
+
bootstrap_year_prompt = nil
|
36
|
+
|
37
|
+
committed = Git.committed_by_year
|
38
|
+
total_committed = committed.values.sum { _1.values.count(&:itself) }
|
39
|
+
if total_committed.zero?
|
40
|
+
bootstrap_year_prompt = "What year's puzzles do you want to start with? (default: #{default_year})"
|
41
|
+
else
|
42
|
+
earliest_year_with_fewest_committed = committed
|
43
|
+
.transform_values { _1.values.count(&:itself) }
|
44
|
+
.sort_by(&:last).first.first
|
45
|
+
default_year = earliest_year_with_fewest_committed
|
46
|
+
default_day = committed[default_year].values.index(false)
|
47
|
+
|
48
|
+
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}])"
|
50
|
+
end
|
51
|
+
|
52
|
+
loop do
|
53
|
+
puts bootstrap_year_prompt
|
54
|
+
print PASTEL.green("> ")
|
55
|
+
year_input = STDIN.gets.strip
|
56
|
+
puts
|
57
|
+
if year_input.strip.empty?
|
58
|
+
year = default_year
|
59
|
+
day = default_day
|
60
|
+
else
|
61
|
+
year = year_input.strip.match(/\A\d{4}\z/)&.to_s
|
62
|
+
day = "1"
|
63
|
+
end
|
64
|
+
break if year
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
year = Integer(year, exception: false) || (raise InputError, "Year must be a number.")
|
72
|
+
day = Integer(day, exception: false) || (raise InputError, "Day must be a number.")
|
73
|
+
|
74
|
+
unless year.between?(2015, Date.today.year)
|
75
|
+
raise InputError, "Year must be between 2015 and this year."
|
76
|
+
end
|
77
|
+
unless day.between?(1, 25) && Date.new(year, 12, day) <= Date.today
|
78
|
+
raise InputError, "Day must be between 1 and 25, and <= today."
|
79
|
+
end
|
80
|
+
|
81
|
+
[year.to_s, day.to_s]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Arb
|
2
|
+
module Files
|
3
|
+
class Input
|
4
|
+
def self.download(year, day, notify_exists: true)
|
5
|
+
year_directory = File.join("input", year)
|
6
|
+
Dir.mkdir("input") unless Dir.exist?("input")
|
7
|
+
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")
|
11
|
+
|
12
|
+
if File.exist?(file_path)
|
13
|
+
puts "Already exists: #{file_path}" if notify_exists
|
14
|
+
else
|
15
|
+
aoc_api = Api::Aoc.new(ENV["AOC_COOKIE"])
|
16
|
+
response = aoc_api.input(year, day)
|
17
|
+
|
18
|
+
File.write(file_path, response)
|
19
|
+
end
|
20
|
+
|
21
|
+
file_path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Arb
|
2
|
+
module Files
|
3
|
+
class Instructions
|
4
|
+
def self.path(year, day)
|
5
|
+
year_directory = File.join("instructions", year)
|
6
|
+
Dir.mkdir("instructions") unless Dir.exist?("instructions")
|
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")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.download(year, day, notify_exists: true, overwrite: false)
|
15
|
+
file_path = path(year, day)
|
16
|
+
|
17
|
+
if File.exist?(file_path) && !overwrite
|
18
|
+
puts "Already exists: #{file_path}" if notify_exists
|
19
|
+
else
|
20
|
+
url = "https://adventofcode.com/#{year}/day/#{day}"
|
21
|
+
|
22
|
+
aoc_api = Api::Aoc.new(ENV["AOC_COOKIE"])
|
23
|
+
response = aoc_api.instructions(year, day)
|
24
|
+
instructions = response.match(/(?<=<main>).+(?=<\/main>)/m).to_s
|
25
|
+
markdown_instructions = ReverseMarkdown.convert(instructions).strip
|
26
|
+
markdown_instructions = markdown_instructions
|
27
|
+
.sub(/\nTo play, please identify yourself via one of these services:.+/m, "")
|
28
|
+
.sub(/\nTo begin, \[get your puzzle input\].+/m, "")
|
29
|
+
.sub(/\n\<form method="post".+/m, "")
|
30
|
+
.sub(/\nAt this point, you should \[return to your Advent calendar\].+/m, "")
|
31
|
+
.concat("\n#{url}\n")
|
32
|
+
|
33
|
+
File.write(file_path, markdown_instructions)
|
34
|
+
end
|
35
|
+
|
36
|
+
file_path
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Arb
|
2
|
+
module Files
|
3
|
+
class OtherSolutions
|
4
|
+
def self.download(year, day)
|
5
|
+
year_directory = File.join("others", year)
|
6
|
+
Dir.mkdir("others") unless Dir.exist?("others")
|
7
|
+
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
8
|
+
|
9
|
+
padded_day = day.rjust(2, "0")
|
10
|
+
|
11
|
+
file_paths = %w[1 2].map do |part|
|
12
|
+
file_path = File.join(year_directory, "#{padded_day}_#{part}.rb")
|
13
|
+
|
14
|
+
if File.exist?(file_path)
|
15
|
+
puts "Already exists: #{file_path}"
|
16
|
+
else
|
17
|
+
other_solutions = Api::OtherSolutions.new.other_solutions(year, day, part)
|
18
|
+
File.write(file_path, other_solutions)
|
19
|
+
end
|
20
|
+
|
21
|
+
file_path
|
22
|
+
end
|
23
|
+
|
24
|
+
file_paths
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Arb
|
2
|
+
module Files
|
3
|
+
class Source
|
4
|
+
def self.create(year, day)
|
5
|
+
year_directory = File.join("src", year)
|
6
|
+
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
7
|
+
|
8
|
+
padded_day = day.rjust(2, "0")
|
9
|
+
file_path = File.join(year_directory, "#{padded_day}.rb")
|
10
|
+
|
11
|
+
if File.exist?(file_path)
|
12
|
+
puts "Already exists: #{file_path}"
|
13
|
+
else
|
14
|
+
File.write(file_path, source(year, day))
|
15
|
+
end
|
16
|
+
|
17
|
+
file_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.source(year, day)
|
21
|
+
padded_day = day.rjust(2, "0")
|
22
|
+
|
23
|
+
<<~TPL
|
24
|
+
# https://adventofcode.com/#{year}/day/#{day}
|
25
|
+
module Year#{year}
|
26
|
+
class Day#{padded_day}
|
27
|
+
def part_1(input_file)
|
28
|
+
lines = input_file.readlines(chomp: true)
|
29
|
+
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def part_2(input_file)
|
34
|
+
lines = input_file.readlines(chomp: true)
|
35
|
+
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
TPL
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Arb
|
2
|
+
module Files
|
3
|
+
class Spec
|
4
|
+
def self.create(year, day, notify_exists: true)
|
5
|
+
year_directory = File.join("spec", year)
|
6
|
+
Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
|
7
|
+
|
8
|
+
padded_day = day.rjust(2, "0")
|
9
|
+
file_path = File.join(year_directory, "#{padded_day}_spec.rb")
|
10
|
+
|
11
|
+
if File.exist?(file_path)
|
12
|
+
puts "Already exists: #{file_path}" if notify_exists
|
13
|
+
else
|
14
|
+
File.write(file_path, source(year, day))
|
15
|
+
end
|
16
|
+
|
17
|
+
file_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.source(year, day)
|
21
|
+
padded_day = day.rjust(2, "0")
|
22
|
+
|
23
|
+
<<~TPL
|
24
|
+
RSpec.describe Year#{year}::Day#{padded_day} do
|
25
|
+
let(:input) {
|
26
|
+
StringIO.new(
|
27
|
+
<<~IN
|
28
|
+
something
|
29
|
+
IN
|
30
|
+
)
|
31
|
+
}
|
32
|
+
|
33
|
+
it "solves Part One" do
|
34
|
+
expect(subject.part_1(input)).to eq(:todo)
|
35
|
+
end
|
36
|
+
|
37
|
+
xit "solves Part Two" do
|
38
|
+
expect(subject.part_2(input)).to eq(:todo)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
TPL
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/arb/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: advent_of_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Felipe Vogel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: debug
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dotenv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pastel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.8'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: reverse_markdown
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: thor
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: vcr
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '6.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '6.0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- fps.vogel@gmail.com
|
128
|
+
executables:
|
129
|
+
- arb
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- bin/arb
|
134
|
+
- lib/arb/api/aoc.rb
|
135
|
+
- lib/arb/api/other_solutions.rb
|
136
|
+
- lib/arb/arb.rb
|
137
|
+
- lib/arb/cli/bootstrap.rb
|
138
|
+
- lib/arb/cli/commit.rb
|
139
|
+
- lib/arb/cli/progress.rb
|
140
|
+
- lib/arb/cli/run_day.rb
|
141
|
+
- lib/arb/cli/shared/git.rb
|
142
|
+
- lib/arb/cli/shared/runner.rb
|
143
|
+
- lib/arb/cli/shared/working_directory.rb
|
144
|
+
- lib/arb/cli/shared/year_day_validator.rb
|
145
|
+
- lib/arb/files/input.rb
|
146
|
+
- lib/arb/files/instructions.rb
|
147
|
+
- lib/arb/files/other_solutions.rb
|
148
|
+
- lib/arb/files/source.rb
|
149
|
+
- lib/arb/files/spec.rb
|
150
|
+
- lib/arb/version.rb
|
151
|
+
homepage: https://github.com/fpsvogel/advent_of_ruby
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata:
|
155
|
+
allowed_push_host: https://rubygems.org
|
156
|
+
source_code_uri: https://github.com/fpsvogel/advent_of_ruby
|
157
|
+
changelog_uri: https://github.com/fpsvogel/avent_of_ruby/blob/main/CHANGELOG.md
|
158
|
+
post_install_message:
|
159
|
+
rdoc_options: []
|
160
|
+
require_paths:
|
161
|
+
- lib
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 3.3.0
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
requirements: []
|
173
|
+
rubygems_version: 3.5.10
|
174
|
+
signing_key:
|
175
|
+
specification_version: 4
|
176
|
+
summary: CLI for Advent of Code in Ruby, via the `arb` command.
|
177
|
+
test_files: []
|