aoc_cli 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,20 @@
1
1
  module AocCli
2
2
  module Database
3
+ def self.correct(attempt:)
4
+ attempt = Attempt.new(attempt:attempt).correct
5
+ Stats::Complete.new(n:attempt.count_attempts).update
6
+ Calendar::Part.new.increment
7
+ end
3
8
  class Query
4
9
  require 'sqlite3'
5
10
  attr_reader :db
6
11
  def initialize(path:)
7
12
  @db = SQLite3::Database.open(path)
8
13
  end
9
- def select(t:, cols:"*", data:)
14
+ def select(t:, cols:"*", where:)
10
15
  db.execute(
11
16
  "SELECT #{cols} FROM #{t} "\
12
- "WHERE #{data.map{|k, v| "#{k} = #{v}"}.join(" AND ")}")
17
+ "WHERE #{where.map{|k, v| "#{k} = #{v}"}.join(" AND ")}")
13
18
  end
14
19
  def table(t:, cols:)
15
20
  db.execute(
@@ -31,25 +36,24 @@ module AocCli
31
36
  self
32
37
  end
33
38
  end
34
- class Log
39
+ class Attempt
35
40
  attr_reader :attempt, :db
36
41
  def initialize(attempt:)
37
42
  @attempt = attempt
38
- @db = Query.new(path:Paths::Database
39
- .cfg("#{attempt.user}"))
43
+ @db = Query.new(path:Paths::Database.cfg(attempt.user))
40
44
  .table(t:"attempts", cols:cols)
41
45
  end
42
46
  def correct
43
47
  db.insert(t:"attempts", val:data << 1 << 0 << 0)
44
48
  self
45
49
  end
46
- def incorrect(high:, low:)
47
- db.insert(t:"attempts", val:data.push(0)
48
- .push(parse_hint(high:high, low:low)))
50
+ def incorrect(low:, high:)
51
+ db.insert(t:"attempts",
52
+ val:data << 0 << parse_hint(low:low, high:high))
49
53
  self
50
54
  end
51
- def parse_hint(high:, low:)
52
- [ high ? 1 : 0, low ? 1 : 0 ]
55
+ def parse_hint(low:, high:)
56
+ [ low ? 1 : 0, high ? 1 : 0 ]
53
57
  end
54
58
  def data
55
59
  [ "'#{Time.now}'",
@@ -69,7 +73,7 @@ module AocCli
69
73
  high: :INT }
70
74
  end
71
75
  def count_attempts
72
- db.select(t:"attempts", data:where).count
76
+ db.select(t:"attempts", where:where).count
73
77
  end
74
78
  def where
75
79
  { year:attempt.year,
@@ -148,12 +152,69 @@ module AocCli
148
152
  .map{|t| t.to_i.to_s.rjust(2, "0")}.join(":")
149
153
  end
150
154
  def dl_time
151
- @dl_time ||= Time
152
- .parse(db
153
- .select(t:"stats", cols:"dl_time", data:where)
155
+ @dl_time ||= Time.parse(db
156
+ .select(t:"stats", cols:"dl_time", where:where)
154
157
  .flatten.first)
155
158
  end
156
159
  end
157
160
  end
161
+ module Calendar
162
+ class Init
163
+ attr_reader :user, :year, :db, :stars
164
+ def initialize(u:Metafile.get(:user),
165
+ y:Metafile.get(:year),
166
+ stars:)
167
+ @user = Validate.user(u)
168
+ @year = Validate.year(y)
169
+ @stars = stars
170
+ @db = Query
171
+ .new(path:Paths::Database.cfg(user))
172
+ .table(t:"calendar", cols:cols)
173
+ end
174
+ def cols
175
+ { year: :INT,
176
+ day: :INT,
177
+ stars: :TEXT }
178
+ end
179
+ def n_stars(day)
180
+ stars.keys.include?(day) ? stars[day] : 0
181
+ end
182
+ def day_data(day)
183
+ ["'#{year}'", "'#{day}'", "'#{n_stars(day)}'"]
184
+ end
185
+ def table_exist?
186
+ db.select(t:"calendar", where:{year:"'#{year}'"}).count > 0
187
+ end
188
+ def insert
189
+ unless table_exist?
190
+ 1.upto(25){|day|
191
+ db.insert(t:"calendar",
192
+ val:day_data(day))}
193
+ end
194
+ end
195
+ end
196
+ class Part
197
+ attr_reader :user, :year, :day, :db
198
+ def initialize(u:Metafile.get(:user),
199
+ y:Metafile.get(:year),
200
+ d:Metafile.get(:day))
201
+ @user = Validate.user(u)
202
+ @year = Validate.year(y)
203
+ @day = Validate.day(d)
204
+ @db = Query.new(path:Paths::Database.cfg(user))
205
+ end
206
+ def get
207
+ db.select(t:"calendar", cols:"stars", where:where)
208
+ .flatten.first.to_i + 1
209
+ end
210
+ def increment
211
+ db.update(t:"calendar", val:{stars:get}, where:where)
212
+ end
213
+ def where
214
+ { year:"'#{year}'",
215
+ day:"'#{day}'" }
216
+ end
217
+ end
218
+ end
158
219
  end
159
220
  end
data/lib/aoc_cli/day.rb CHANGED
@@ -1,12 +1,7 @@
1
1
  module AocCli
2
2
  module Day
3
- def self.refresh
4
- puts "- Updating puzzle...".yellow
5
- Init.new.write
6
- Data::Puzzle.new.write
7
- end
8
3
  class Init
9
- attr_reader :year, :day, :user, :paths, :part
4
+ attr_reader :user, :year, :day, :paths
10
5
  def initialize(u:Metafile.get(:user),
11
6
  y:Metafile.get(:year),
12
7
  d:Metafile.get(:day))
@@ -19,135 +14,111 @@ module AocCli
19
14
  Dir.mkdir(Validate.day_dir(paths.day_dir))
20
15
  self
21
16
  end
22
- def write
17
+ def meta
23
18
  File.write(paths.local(f:"meta"),
24
- Metafile.day(u:user, y:year, d:day))
19
+ Metafile.day(u:user, y:year, d:day))
25
20
  self
26
21
  end
27
22
  end
28
23
  class Pages < Init
29
- attr_reader :cache, :files
24
+ attr_reader :files, :use_cache
30
25
  def initialize(u:Metafile.get(:user),
31
26
  y:Metafile.get(:year),
32
27
  d:Metafile.get(:day),
33
- f:[:Input, :Puzzle])
28
+ f:[:Input, :Puzzle],
29
+ use_cache:true)
34
30
  super(u:u, y:y, d:d)
35
31
  @files = f
32
+ @use_cache = use_cache
36
33
  end
37
- def write
38
- cache.each{|page, data| data ?
39
- File.write(paths.local(f:page), data) :
40
- download(page:page)}
34
+ def load
35
+ files.each do |file| use_cache && cache[file] ?
36
+ copy(file:file) : download(page:file) end
41
37
  end
42
- private
43
38
  def cache
44
- @cache ||= Cache
45
- .new(u:user, y:year, d:day, f:files).load
39
+ @cache ||= Cache.new(d:day, f:files).query
40
+ end
41
+ def copy(file:)
42
+ File.write(paths.local(f:file), cache[file])
46
43
  end
47
- def download(page:, to:paths.cache_and_local(f:page))
48
- dl = Object.const_get("AocCli::Day::Data::#{page}")
44
+ def download(page:)
45
+ req = Requests.const_get(page)
49
46
  .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)
47
+ .write(to:paths.cache_and_local(f:page))
48
+ req.init_stats if page == :Puzzle
49
+ end
50
+ end
51
+ class Cache < Pages
52
+ def initialize(u:Metafile.get(:user),
53
+ y:Metafile.get(:year),
54
+ d:Metafile.get(:day),
55
+ f:[:Input, :Puzzle])
56
+ super(u:u, y:y, d:d, f:f)
57
+ paths.create_cache
58
+ end
59
+ def query
60
+ files.map{|file| [file, read(file:file)]}.to_h
61
+ end
62
+ private
63
+ def read(file:)
64
+ File.exist?(paths.cache_path(f:file)) ?
65
+ File.read(paths.cache_path(f:file)) : nil
54
66
  end
55
67
  end
56
- module Data
57
- class DayObject < Init
58
- attr_reader :user, :year, :day, :data, :paths, :part
68
+ module Requests
69
+ class Request < Init
70
+ attr_reader :data, :part
59
71
  def initialize(u:Metafile.get(:user),
60
72
  y:Metafile.get(:year),
61
73
  d:Metafile.get(:day))
62
74
  super(u:u, y:y, d:d)
75
+ @data = parse(raw:fetch)
63
76
  @part = Metafile.part(d:day)
64
- @data = parse(raw: fetch)
65
77
  end
66
- def write(to:paths.cache_and_local(f:page))
67
- to.each{|path| File.write(path, data)}
68
- self
78
+ def write(to:)
79
+ to.each{|path| File.write(path, data)}; self
69
80
  end
70
81
  private
71
82
  def fetch
72
83
  Tools::Get.new(u:user, y:year, d:day, p:page)
73
84
  end
74
85
  end
75
- class Puzzle < DayObject
86
+ class Puzzle < Request
76
87
  def page
77
88
  :Puzzle
78
89
  end
79
- def parse(raw:)
90
+ def parse(raw:fetch)
80
91
  raw.chunk(f:"<article", t:"<\/article", f_off:2)
81
92
  .md
82
93
  .gsub(/(?<=\])\[\]/, "")
83
94
  .gsub(/\n.*<!--.*-->.*\n/, "")
84
95
  end
85
- def init_db
96
+ def init_stats
86
97
  Database::Stats::Init
87
98
  .new(d:day, p:part)
88
- .init if part < 3
89
- self
99
+ .init if part < 3 && !stats_exist?
100
+ end
101
+ def stats_exist?
102
+ Database::Query
103
+ .new(path:Paths::Database.cfg(user))
104
+ .select(t:"stats",
105
+ where:{year:year, day:day, part:part})
106
+ .count > 0
90
107
  end
91
108
  end
92
- class Input < DayObject
109
+ class Input < Request
93
110
  def page
94
111
  :Input
95
112
  end
96
- def parse(raw:)
113
+ def parse(raw:fetch)
97
114
  raw.raw
98
115
  end
99
116
  end
100
117
  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
118
+ def self.refresh(files:[:Input, :Puzzle])
119
+ puts "- Updating puzzle...".blue
120
+ Init.new.meta
121
+ Pages.new(f:files, use_cache:false).load
151
122
  end
152
123
  end
153
124
  end
@@ -112,7 +112,7 @@ module AocCli
112
112
  def message
113
113
  <<~error
114
114
  #{ERROR}: No session key value.
115
- Use the #{"-k".yellow} or #{"--key".yellow} flags
115
+ Use the #{"-k".yellow} or #{"--key".yellow} flags to store the key
116
116
  error
117
117
  end
118
118
  end
@@ -123,7 +123,7 @@ module AocCli
123
123
  end
124
124
  def message
125
125
  <<~error
126
- #{ERROR}: The key #{user.yellow} already exists in your config file
126
+ #{ERROR}: The key #{key.yellow} already exists in your config file
127
127
  error
128
128
  end
129
129
  end
@@ -160,12 +160,27 @@ module AocCli
160
160
  error
161
161
  end
162
162
  end
163
- class NoCmd < StandardError
163
+ class CmdNil < StandardError
164
164
  def message
165
165
  <<~error
166
166
  #{ERROR}: Flags passed but no command specified
167
167
  error
168
168
  end
169
169
  end
170
+ class KeyInv < StandardError
171
+ def message
172
+ <<~error
173
+ #{ERROR}: Invalid key
174
+ Double check your session key. It should start with "session="
175
+ error
176
+ end
177
+ end
178
+ class ConfigExist < StandardError
179
+ def message
180
+ <<~error
181
+ #{ERROR}: A config file already exists in #{Paths::Config.path.blue}
182
+ error
183
+ end
184
+ end
170
185
  end
171
186
  end
data/lib/aoc_cli/files.rb CHANGED
@@ -1,52 +1,106 @@
1
1
  module AocCli
2
2
  module Files
3
- class Config
4
- require 'fileutils'
5
- def initialize
6
- Paths::Config.create
7
- end
8
- def def_acc
9
- get_line(key:"default") || "main"
10
- end
11
- def is_set?(key:nil, val:nil)
12
- read.split("\n").grep(/#{key}=>#{val}/).any?
13
- end
14
- def mod_line(key:, val:)
15
- is_set?(key:key) ?
16
- write(f:read.gsub(/(?<=^#{key}=>).*$/,
17
- val.to_s)) :
3
+ module Config
4
+ class Tools
5
+ def self.is_set?(key:nil, val:nil)
6
+ read.split("\n").grep(/(?<!\/\/)#{key}=>#{val}/).any?
7
+ end
8
+ def self.get_all(key:)
9
+ read.scan(/(?:(?<=(?<!\/\/)#{key}=>)).*$/)
10
+ end
11
+ def self.get_line(key:)
12
+ get_all(key:key)&.first
13
+ #read.scan(/(?:(?<=(?<!\/\/)#{key}=>)).*$/)&.first
14
+ end
15
+ def self.get_bool(key:)
16
+ get_line(key:key) == "true" ? true : false
17
+ end
18
+ def self.mod_line(key:, val:)
19
+ is_set?(key:key) ?
20
+ write(f:read.gsub(/(?<=^#{key}=>).*$/, val.to_s)) :
21
+ write(f:"#{key}=>#{val}\n", m:"a")
22
+ end
23
+ def self.set_line(key:, val:)
18
24
  write(f:"#{key}=>#{val}\n", m:"a")
19
- end
20
- def get_line(key:)
21
- read.scan(/(?<=#{key}=>).*$/)&.first
22
- end
23
- def get_bool(key:)
24
- get_line(key:key) == "true" ? true : false
25
- end
26
- protected
27
- def set_line(key:, val:)
28
- write(f:"#{key}=>#{val}\n", m:"a")
29
- end
30
- private
31
- def read
32
- File.read(Paths::Config.path)
33
- end
34
- def write(f:, m:"w")
35
- File.write(Paths::Config.path, f, mode:m)
36
- end
37
- end
38
- class Cookie < Config
39
- attr_reader :user
40
- def initialize(u:)
41
- @user = u
42
- super()
43
- end
44
- def store(key:)
45
- set_line(key:"cookie=>#{Validate.set_user(user)}",
46
- val:Validate.set_key(key))
47
- end
48
- def key
49
- get_line(key:"cookie=>#{user}")
25
+ end
26
+ private
27
+ def self.read
28
+ Paths::Config.create
29
+ File.read(Paths::Config.path)
30
+ end
31
+ def self.write(f:, m:"w")
32
+ Paths::Config.create
33
+ File.write(Paths::Config.path, f, mode:m)
34
+ end
35
+ end
36
+ class Prefs < Tools
37
+ def self.default_alias
38
+ is_set?(key:"default") ? get_line(key:"default") :
39
+ is_set?(key:"cookie=>main") ? "main" :
40
+ list_aliases.first || "main"
41
+ end
42
+ def self.list_aliases
43
+ get_all(key:"cookie")&.map{|a| a.gsub(/=>.*/, "")}
44
+ end
45
+ def self.bool(key:)
46
+ is_set?(key:key) ?
47
+ get_bool(key:key) : defaults[key.to_sym]
48
+ end
49
+ def self.string(key:)
50
+ is_set?(key:key) ?
51
+ get_line(key:key) : defaults[key.to_sym]
52
+ end
53
+ private
54
+ def self.defaults
55
+ { calendar_file:true,
56
+ ignore_md_files:true,
57
+ ignore_meta_files:true,
58
+ init_git:false,
59
+ lb_in_calendar:true,
60
+ reddit_in_browser:false,
61
+ unicode_tables:true
62
+ }
63
+ end
64
+ end
65
+ class Cookie < Tools
66
+ def self.store(user:, key:)
67
+ set_line(key:"cookie=>#{Validate.set_user(user)}",
68
+ val:Validate.set_key(key))
69
+ end
70
+ def self.key(user:)
71
+ Validate.key(get_line(key:"cookie=>#{Validate
72
+ .user(user)}"))
73
+ end
74
+ end
75
+ class Example
76
+ def self.write
77
+ File.write(Validate.no_config, file)
78
+ end
79
+ def self.file
80
+ <<~file
81
+ //aoc-cli example config
82
+ //See the github repo for more information on configuring aoc-cli
83
+ //https://github.com/apexatoll/aoc-cli
84
+
85
+ [General]
86
+ //Print table in unicode rather than ascii
87
+ unicode_tables=>true
88
+ //Open Reddit in browser rather than use a Reddit CLI
89
+ reddit_in_browser=>false
90
+
91
+ [Initialise Year]
92
+ //Create a calendar file
93
+ calendar_file=>true
94
+ //Initialise git repo on year initialisation
95
+ init_git=>false
96
+ //Add calendar and puzzle files to gitignore
97
+ ignore_md_files=>true
98
+ //Add .meta files to gitignore
99
+ ignore_meta_files=>true
100
+ //Include leaderboard stats in calendar file
101
+ lb_in_calendar=>true
102
+ file
103
+ end
50
104
  end
51
105
  end
52
106
  class Metafile
@@ -56,22 +110,13 @@ module AocCli
56
110
  def self.type
57
111
  get("dir").to_sym
58
112
  end
59
- def self.add(hash:, path:".meta")
60
- hash.map {|k, v| "#{k}=>#{v}\n"}
61
- .each{|l| File.write(path, l, mode:"a")}
62
- end
63
113
  def self.part(d:)
64
- JSON.parse(read(dir:root_dir)
65
- .scan(/(?<=stars=>).*$/)&.first)[d.to_s]
66
- .to_i + 1
114
+ Database::Calendar::Part.new(d:d).get
67
115
  end
68
116
  private
69
117
  def self.read(dir:".")
70
118
  File.read("#{Validate.init(dir)}/.meta")
71
119
  end
72
- def self.root_dir
73
- type == :ROOT ? "." : ".."
74
- end
75
120
  def self.year(u:, y:)
76
121
  <<~meta
77
122
  dir=>ROOT
@@ -89,5 +134,29 @@ module AocCli
89
134
  meta
90
135
  end
91
136
  end
137
+ class Calendar
138
+ attr_reader :cal, :stats, :year
139
+ def initialize(y:Metafile.get(:year), cal:, stats:)
140
+ @year, @cal, @stats = Validate.year(y), cal, stats
141
+ end
142
+ def include_leaderboard?
143
+ Prefs.bool(key:"lb_in_calendar")
144
+ end
145
+ def title
146
+ "Year #{year}: #{stats.total_stars}/50 *"
147
+ end
148
+ def underline
149
+ "-" * (cal.data[0].to_s.length + 2)
150
+ end
151
+ def make
152
+ <<~file
153
+ #{title}
154
+ #{underline}
155
+ #{cal.data.join("\n")}\n
156
+ #{stats.data.join("\n") if stats.total_stars > 0 &&
157
+ include_leaderboard?}
158
+ file
159
+ end
160
+ end
92
161
  end
93
162
  end