aoc_cli 0.1.1

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