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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +18 -2
- data/Gemfile +0 -8
- data/README.md +138 -53
- data/aoc_cli.gemspec +1 -1
- data/bin/setup +1 -2
- data/lib/aoc_cli.rb +1 -1
- data/lib/aoc_cli/commands.rb +86 -69
- data/lib/aoc_cli/database.rb +75 -14
- data/lib/aoc_cli/day.rb +58 -87
- data/lib/aoc_cli/errors.rb +18 -3
- data/lib/aoc_cli/files.rb +125 -56
- data/lib/aoc_cli/help.rb +30 -9
- data/lib/aoc_cli/interface.rb +45 -28
- data/lib/aoc_cli/paths.rb +33 -30
- data/lib/aoc_cli/solve.rb +27 -35
- data/lib/aoc_cli/tables.rb +79 -79
- data/lib/aoc_cli/tools.rb +30 -6
- data/lib/aoc_cli/version.rb +1 -3
- data/lib/aoc_cli/year.rb +49 -32
- data/sample/aoc.rc +21 -0
- metadata +4 -4
- data/pkg/aoc_cli-0.1.2.gem +0 -0
data/lib/aoc_cli/database.rb
CHANGED
@@ -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:"*",
|
14
|
+
def select(t:, cols:"*", where:)
|
10
15
|
db.execute(
|
11
16
|
"SELECT #{cols} FROM #{t} "\
|
12
|
-
"WHERE #{
|
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
|
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(
|
47
|
-
db.insert(t:"attempts",
|
48
|
-
|
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(
|
52
|
-
[
|
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",
|
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
|
-
.
|
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 :
|
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
|
17
|
+
def meta
|
23
18
|
File.write(paths.local(f:"meta"),
|
24
|
-
|
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 :
|
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
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
48
|
-
|
44
|
+
def download(page:)
|
45
|
+
req = Requests.const_get(page)
|
49
46
|
.new(u:user, y:year, d:day)
|
50
|
-
.write(to:
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
57
|
-
class
|
58
|
-
attr_reader :
|
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:
|
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 <
|
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
|
96
|
+
def init_stats
|
86
97
|
Database::Stats::Init
|
87
98
|
.new(d:day, p:part)
|
88
|
-
.init if part < 3
|
89
|
-
|
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 <
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
data/lib/aoc_cli/errors.rb
CHANGED
@@ -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 #{
|
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
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
read
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|