advent_of_ruby 0.1.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 +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: []
|