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.
@@ -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