aoc_cli 0.1.1
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/.gitignore +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +282 -0
- data/Rakefile +3 -0
- data/aoc_cli.gemspec +30 -0
- data/bin/aoc +4 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/aoc_cli.rb +22 -0
- data/lib/aoc_cli/commands.rb +215 -0
- data/lib/aoc_cli/database.rb +159 -0
- data/lib/aoc_cli/day.rb +153 -0
- data/lib/aoc_cli/db/reddit.db +0 -0
- data/lib/aoc_cli/errors.rb +171 -0
- data/lib/aoc_cli/files.rb +93 -0
- data/lib/aoc_cli/help.rb +53 -0
- data/lib/aoc_cli/interface.rb +137 -0
- data/lib/aoc_cli/paths.rb +93 -0
- data/lib/aoc_cli/solve.rb +109 -0
- data/lib/aoc_cli/tables.rb +138 -0
- data/lib/aoc_cli/tools.rb +91 -0
- data/lib/aoc_cli/version.rb +5 -0
- data/lib/aoc_cli/year.rb +98 -0
- metadata +157 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Database
|
3
|
+
class Query
|
4
|
+
require 'sqlite3'
|
5
|
+
attr_reader :db
|
6
|
+
def initialize(path:)
|
7
|
+
@db = SQLite3::Database.open(path)
|
8
|
+
end
|
9
|
+
def select(t:, cols:"*", data:)
|
10
|
+
db.execute(
|
11
|
+
"SELECT #{cols} FROM #{t} "\
|
12
|
+
"WHERE #{data.map{|k, v| "#{k} = #{v}"}.join(" AND ")}")
|
13
|
+
end
|
14
|
+
def table(t:, cols:)
|
15
|
+
db.execute(
|
16
|
+
"CREATE TABLE IF NOT EXISTS "\
|
17
|
+
"#{t}(#{cols.map{|c, t| "#{c} #{t}"}.join(", ")})")
|
18
|
+
self
|
19
|
+
end
|
20
|
+
def insert(t:, val:)
|
21
|
+
db.execute(
|
22
|
+
"INSERT INTO #{t} "\
|
23
|
+
"VALUES(#{val.join(", ")})")
|
24
|
+
self
|
25
|
+
end
|
26
|
+
def update(t:, val:, where:)
|
27
|
+
db.execute(
|
28
|
+
"UPDATE #{t} "\
|
29
|
+
"SET #{val.map{|c, v| "#{c} = #{v}"}.join(", ")} "\
|
30
|
+
"WHERE #{where.map{|c, v| "#{c} = #{v}"}.join(" AND ")}")
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
class Log
|
35
|
+
attr_reader :attempt, :db
|
36
|
+
def initialize(attempt:)
|
37
|
+
@attempt = attempt
|
38
|
+
@db = Query.new(path:Paths::Database
|
39
|
+
.cfg("#{attempt.user}"))
|
40
|
+
.table(t:"attempts", cols:cols)
|
41
|
+
end
|
42
|
+
def correct
|
43
|
+
db.insert(t:"attempts", val:data << 1 << 0 << 0)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
def incorrect(high:, low:)
|
47
|
+
db.insert(t:"attempts", val:data.push(0)
|
48
|
+
.push(parse_hint(high:high, low:low)))
|
49
|
+
self
|
50
|
+
end
|
51
|
+
def parse_hint(high:, low:)
|
52
|
+
[ high ? 1 : 0, low ? 1 : 0 ]
|
53
|
+
end
|
54
|
+
def data
|
55
|
+
[ "'#{Time.now}'",
|
56
|
+
"'#{attempt.year}'",
|
57
|
+
"'#{attempt.day}'",
|
58
|
+
"'#{attempt.part}'",
|
59
|
+
"'#{attempt.answer}'" ]
|
60
|
+
end
|
61
|
+
def cols
|
62
|
+
{ time: :TEXT,
|
63
|
+
year: :INT,
|
64
|
+
day: :INT,
|
65
|
+
part: :INT,
|
66
|
+
answer: :TEXT,
|
67
|
+
correct: :INT,
|
68
|
+
low: :INT,
|
69
|
+
high: :INT }
|
70
|
+
end
|
71
|
+
def count_attempts
|
72
|
+
db.select(t:"attempts", data:where).count
|
73
|
+
end
|
74
|
+
def where
|
75
|
+
{ year:attempt.year,
|
76
|
+
day:attempt.day,
|
77
|
+
part:attempt.part }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
module Stats
|
81
|
+
class Init
|
82
|
+
attr_reader :user, :year, :day, :part, :now, :db
|
83
|
+
def initialize(u:Metafile.get(:user),
|
84
|
+
y:Metafile.get(:year),
|
85
|
+
d:Metafile.get(:day),
|
86
|
+
p:Metafile.get(:part))
|
87
|
+
@user = Validate.user(u)
|
88
|
+
@year = Validate.year(y)
|
89
|
+
@day = Validate.day(d)
|
90
|
+
@part = p
|
91
|
+
@now = Time.now
|
92
|
+
@db = Query.new(path:Paths::Database.cfg(user))
|
93
|
+
.table(t:"stats", cols:cols)
|
94
|
+
end
|
95
|
+
def cols
|
96
|
+
{ year: :INT,
|
97
|
+
day: :INT,
|
98
|
+
part: :INT,
|
99
|
+
dl_time: :TEXT,
|
100
|
+
end_time: :TEXT,
|
101
|
+
elapsed: :TEXT,
|
102
|
+
attempts: :INT,
|
103
|
+
correct: :INT }
|
104
|
+
end
|
105
|
+
def init
|
106
|
+
db.insert(t:"stats", val:data)
|
107
|
+
end
|
108
|
+
def data
|
109
|
+
[ "'#{year}'",
|
110
|
+
"'#{day}'",
|
111
|
+
"'#{part}'",
|
112
|
+
"'#{now}'",
|
113
|
+
"NULL",
|
114
|
+
"NULL",
|
115
|
+
"'0'",
|
116
|
+
"'0'" ]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
class Complete < Init
|
120
|
+
attr_reader :n_attempts
|
121
|
+
def initialize(u:Metafile.get(:user),
|
122
|
+
y:Metafile.get(:year),
|
123
|
+
d:Metafile.get(:day),
|
124
|
+
p:Metafile.get(:part),
|
125
|
+
n:)
|
126
|
+
super(u:u, y:y, d:d, p:p)
|
127
|
+
@n_attempts = n
|
128
|
+
end
|
129
|
+
def update
|
130
|
+
db.update(t:"stats", val:val, where:where)
|
131
|
+
end
|
132
|
+
def val
|
133
|
+
{ elapsed: "'#{elapsed}'",
|
134
|
+
end_time: "'#{now}'",
|
135
|
+
attempts: "'#{n_attempts}'",
|
136
|
+
correct: "'1'" }
|
137
|
+
end
|
138
|
+
def where
|
139
|
+
{ year: year,
|
140
|
+
day: day,
|
141
|
+
part: part }
|
142
|
+
end
|
143
|
+
def elapsed
|
144
|
+
@elapsed ||= hms(now - dl_time)
|
145
|
+
end
|
146
|
+
def hms(seconds)
|
147
|
+
[seconds/3600, seconds/60 % 60, seconds % 60]
|
148
|
+
.map{|t| t.to_i.to_s.rjust(2, "0")}.join(":")
|
149
|
+
end
|
150
|
+
def dl_time
|
151
|
+
@dl_time ||= Time
|
152
|
+
.parse(db
|
153
|
+
.select(t:"stats", cols:"dl_time", data:where)
|
154
|
+
.flatten.first)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/aoc_cli/day.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Day
|
3
|
+
def self.refresh
|
4
|
+
puts "- Updating puzzle...".yellow
|
5
|
+
Init.new.write
|
6
|
+
Data::Puzzle.new.write
|
7
|
+
end
|
8
|
+
class Init
|
9
|
+
attr_reader :year, :day, :user, :paths, :part
|
10
|
+
def initialize(u:Metafile.get(:user),
|
11
|
+
y:Metafile.get(:year),
|
12
|
+
d:Metafile.get(:day))
|
13
|
+
@user = Validate.user(u)
|
14
|
+
@year = Validate.year(y)
|
15
|
+
@day = Validate.day(d)
|
16
|
+
@paths = Paths::Day.new(u:user, y:year, d:day)
|
17
|
+
end
|
18
|
+
def mkdir
|
19
|
+
Dir.mkdir(Validate.day_dir(paths.day_dir))
|
20
|
+
self
|
21
|
+
end
|
22
|
+
def write
|
23
|
+
File.write(paths.local(f:"meta"),
|
24
|
+
Metafile.day(u:user, y:year, d:day))
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
class Pages < Init
|
29
|
+
attr_reader :cache, :files
|
30
|
+
def initialize(u:Metafile.get(:user),
|
31
|
+
y:Metafile.get(:year),
|
32
|
+
d:Metafile.get(:day),
|
33
|
+
f:[:Input, :Puzzle])
|
34
|
+
super(u:u, y:y, d:d)
|
35
|
+
@files = f
|
36
|
+
end
|
37
|
+
def write
|
38
|
+
cache.each{|page, data| data ?
|
39
|
+
File.write(paths.local(f:page), data) :
|
40
|
+
download(page:page)}
|
41
|
+
end
|
42
|
+
private
|
43
|
+
def cache
|
44
|
+
@cache ||= Cache
|
45
|
+
.new(u:user, y:year, d:day, f:files).load
|
46
|
+
end
|
47
|
+
def download(page:, to:paths.cache_and_local(f:page))
|
48
|
+
dl = Object.const_get("AocCli::Day::Data::#{page}")
|
49
|
+
.new(u:user, y:year, d:day)
|
50
|
+
.write(to:to)
|
51
|
+
dl.init_db if dl.class
|
52
|
+
.instance_methods
|
53
|
+
.include?(:init_db)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
module Data
|
57
|
+
class DayObject < Init
|
58
|
+
attr_reader :user, :year, :day, :data, :paths, :part
|
59
|
+
def initialize(u:Metafile.get(:user),
|
60
|
+
y:Metafile.get(:year),
|
61
|
+
d:Metafile.get(:day))
|
62
|
+
super(u:u, y:y, d:d)
|
63
|
+
@part = Metafile.part(d:day)
|
64
|
+
@data = parse(raw: fetch)
|
65
|
+
end
|
66
|
+
def write(to:paths.cache_and_local(f:page))
|
67
|
+
to.each{|path| File.write(path, data)}
|
68
|
+
self
|
69
|
+
end
|
70
|
+
private
|
71
|
+
def fetch
|
72
|
+
Tools::Get.new(u:user, y:year, d:day, p:page)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
class Puzzle < DayObject
|
76
|
+
def page
|
77
|
+
:Puzzle
|
78
|
+
end
|
79
|
+
def parse(raw:)
|
80
|
+
raw.chunk(f:"<article", t:"<\/article", f_off:2)
|
81
|
+
.md
|
82
|
+
.gsub(/(?<=\])\[\]/, "")
|
83
|
+
.gsub(/\n.*<!--.*-->.*\n/, "")
|
84
|
+
end
|
85
|
+
def init_db
|
86
|
+
Database::Stats::Init
|
87
|
+
.new(d:day, p:part)
|
88
|
+
.init if part < 3
|
89
|
+
self
|
90
|
+
end
|
91
|
+
end
|
92
|
+
class Input < DayObject
|
93
|
+
def page
|
94
|
+
:Input
|
95
|
+
end
|
96
|
+
def parse(raw:)
|
97
|
+
raw.raw
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
class Cache < Pages
|
102
|
+
require 'fileutils'
|
103
|
+
def initialize(u:Metafile.get(:user),
|
104
|
+
y:Metafile.get(:year),
|
105
|
+
d:Metafile.get(:day),
|
106
|
+
f:[:Input, :Puzzle])
|
107
|
+
super(u:u, y:y, d:d, f:f)
|
108
|
+
FileUtils.mkdir_p(paths.cache_dir) unless Dir
|
109
|
+
.exist?(paths.cache_dir)
|
110
|
+
end
|
111
|
+
def load
|
112
|
+
files.map{|f| [f, read_file(f:f)]}.to_h
|
113
|
+
end
|
114
|
+
private
|
115
|
+
def read_file(f:)
|
116
|
+
cached?(f:f) ? read(f:f) : nil
|
117
|
+
end
|
118
|
+
def cached?(f:)
|
119
|
+
File.exist?(paths.cache_path(f:f))
|
120
|
+
end
|
121
|
+
def read(f:)
|
122
|
+
File.read(paths.cache_path(f:f))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
class Reddit
|
126
|
+
attr_reader :year, :day, :uniq, :browser
|
127
|
+
def initialize(y:Metafile.get(:year),
|
128
|
+
d:Metafile.get(:day),
|
129
|
+
b:false)
|
130
|
+
@year = Validate.year(y)
|
131
|
+
@day = Validate.day(d)
|
132
|
+
@uniq = Database::Query
|
133
|
+
.new(path:Paths::Database.root("reddit"))
|
134
|
+
.select(t:"'#{year}'", data:{day:"'#{day}'"})
|
135
|
+
.flatten[1]
|
136
|
+
@browser = b
|
137
|
+
end
|
138
|
+
def open
|
139
|
+
system("#{browser ? "open" : cmd} #{link}")
|
140
|
+
end
|
141
|
+
def cmd
|
142
|
+
["ttrv", "rtv"]
|
143
|
+
.map{|cli| cli unless `which #{cli}`.empty?}
|
144
|
+
.reject{|cmd| cmd.nil?}&.first || "open"
|
145
|
+
end
|
146
|
+
def link
|
147
|
+
"https://www.reddit.com/r/"\
|
148
|
+
"adventofcode/comments/#{uniq}/"\
|
149
|
+
"#{year}_day_#{day}_solutions"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
Binary file
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Errors
|
3
|
+
ERROR = "Error".bold.red
|
4
|
+
class UserNil < StandardError
|
5
|
+
def message
|
6
|
+
<<~error
|
7
|
+
#{ERROR}: No user alias value.
|
8
|
+
Specify an alias to use when passing the #{"-u".yellow} or #{"--user".yellow} flag
|
9
|
+
error
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class UserInv < StandardError
|
13
|
+
def initialize(user)
|
14
|
+
@user = user
|
15
|
+
end
|
16
|
+
def message
|
17
|
+
<<~error
|
18
|
+
#{ERROR}: Invalid user: #{@user.to_s.red}
|
19
|
+
No key was found under this alias
|
20
|
+
error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
class UserDup < StandardError
|
24
|
+
attr_reader :user
|
25
|
+
def initialize(user)
|
26
|
+
@user = user
|
27
|
+
end
|
28
|
+
def message
|
29
|
+
<<~error
|
30
|
+
#{ERROR}: There is already a key set for the user #{user.yellow}
|
31
|
+
Either check your config file or set a new username for this key using the #{"-u".yellow} or #{"--user".yellow} flags
|
32
|
+
error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
class YearNil < StandardError
|
36
|
+
def message
|
37
|
+
<<~error
|
38
|
+
#{ERROR}: No year value.
|
39
|
+
Set the year using the #{"-y".yellow} or #{"--year".yellow} flags.
|
40
|
+
error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
class YearInv < StandardError
|
44
|
+
attr_reader :year
|
45
|
+
def initialize(year)
|
46
|
+
@year = year
|
47
|
+
end
|
48
|
+
def message
|
49
|
+
<<~error
|
50
|
+
#{ERROR}: Invalid year: #{year.to_s.red}
|
51
|
+
Advent of Code currently spans 2015 - 2020.
|
52
|
+
error
|
53
|
+
end
|
54
|
+
end
|
55
|
+
class DayNil < StandardError
|
56
|
+
def message
|
57
|
+
<<~error
|
58
|
+
#{ERROR}: No day value.
|
59
|
+
Specify the day using the #{"-d".yellow} or #{"--day".yellow} flags.
|
60
|
+
error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
class DayInv < StandardError
|
64
|
+
def initialize(day)
|
65
|
+
@day = day
|
66
|
+
end
|
67
|
+
def message
|
68
|
+
<<~error
|
69
|
+
#{ERROR}: Invalid day: #{@day.to_s.red}
|
70
|
+
Valid days are between 1 and 25
|
71
|
+
error
|
72
|
+
end
|
73
|
+
end
|
74
|
+
class DayExist < StandardError
|
75
|
+
def initialize(day)
|
76
|
+
@day = day.to_s
|
77
|
+
end
|
78
|
+
def message
|
79
|
+
<<~error
|
80
|
+
#{ERROR}: Day #{@day.red} is already initialised!
|
81
|
+
error
|
82
|
+
end
|
83
|
+
end
|
84
|
+
class PartNil < StandardError
|
85
|
+
def message
|
86
|
+
<<~error
|
87
|
+
#{ERROR}: No part value.
|
88
|
+
Check the .meta file or pass it manually with the #{"-p".yellow} or #{"--part".yellow} flags
|
89
|
+
error
|
90
|
+
end
|
91
|
+
end
|
92
|
+
class PartInv < StandardError
|
93
|
+
attr_reader :part
|
94
|
+
def initialize(part)
|
95
|
+
@part = part
|
96
|
+
end
|
97
|
+
def message
|
98
|
+
<<~error
|
99
|
+
#{ERROR}: Invalid part: #{part.red}
|
100
|
+
Part refers to the part of the puzzle and can either be 1 or 2.
|
101
|
+
error
|
102
|
+
end
|
103
|
+
end
|
104
|
+
class AnsNil < StandardError
|
105
|
+
def message
|
106
|
+
<<~error
|
107
|
+
#{ERROR}: No answer value.
|
108
|
+
error
|
109
|
+
end
|
110
|
+
end
|
111
|
+
class KeyNil < StandardError
|
112
|
+
def message
|
113
|
+
<<~error
|
114
|
+
#{ERROR}: No session key value.
|
115
|
+
Use the #{"-k".yellow} or #{"--key".yellow} flags
|
116
|
+
error
|
117
|
+
end
|
118
|
+
end
|
119
|
+
class KeyDup < StandardError
|
120
|
+
attr_reader :key
|
121
|
+
def initialize(key)
|
122
|
+
@key = key
|
123
|
+
end
|
124
|
+
def message
|
125
|
+
<<~error
|
126
|
+
#{ERROR}: The key #{user.yellow} already exists in your config file
|
127
|
+
error
|
128
|
+
end
|
129
|
+
end
|
130
|
+
class AlrInit < StandardError
|
131
|
+
def message
|
132
|
+
<<~error
|
133
|
+
#{ERROR}: This directory is already initialised.
|
134
|
+
error
|
135
|
+
end
|
136
|
+
end
|
137
|
+
class NotInit < StandardError
|
138
|
+
def message
|
139
|
+
<<~error
|
140
|
+
#{ERROR}: You must initialise the directory first
|
141
|
+
error
|
142
|
+
end
|
143
|
+
end
|
144
|
+
class PuzzComp < StandardError
|
145
|
+
def message
|
146
|
+
<<~error
|
147
|
+
#{ERROR}: This puzzle is already complete!
|
148
|
+
error
|
149
|
+
end
|
150
|
+
end
|
151
|
+
class FlagInv < StandardError
|
152
|
+
attr_reader :flag
|
153
|
+
def initialize(flag)
|
154
|
+
@flag = flag
|
155
|
+
end
|
156
|
+
def message
|
157
|
+
<<~error
|
158
|
+
#{ERROR}: Invalid flag: #{flag.red}
|
159
|
+
Use the #{"-h".yellow} or #{"--help".yellow} flags for a list of commands
|
160
|
+
error
|
161
|
+
end
|
162
|
+
end
|
163
|
+
class NoCmd < StandardError
|
164
|
+
def message
|
165
|
+
<<~error
|
166
|
+
#{ERROR}: Flags passed but no command specified
|
167
|
+
error
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|