aoc_cli 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|