reading 0.9.1 → 1.0.0
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.
- checksums.yaml +4 -4
- data/bin/reading +7 -7
- data/lib/reading/config.rb +1 -1
- data/lib/reading/item/time_length.rb +2 -2
- data/lib/reading/item/view.rb +2 -2
- data/lib/reading/item.rb +2 -2
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +1 -1
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +1 -1
- data/lib/reading/parsing/attributes/experiences.rb +3 -3
- data/lib/reading/parsing/csv.rb +4 -4
- data/lib/reading/parsing/parser.rb +6 -6
- data/lib/reading/parsing/rows/compact_planned.rb +4 -4
- data/lib/reading/parsing/rows/regular.rb +10 -10
- data/lib/reading/parsing/transformer.rb +9 -9
- data/lib/reading/stats/filter.rb +55 -51
- data/lib/reading/stats/grouping.rb +2 -2
- data/lib/reading/stats/operation.rb +89 -39
- data/lib/reading/stats/query.rb +7 -7
- data/lib/reading/stats/result_formatters.rb +140 -0
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +7 -7
- metadata +43 -19
- data/lib/reading/stats/terminal_result_formatters.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45b4a37abfdadf33aafc7cd1af59affdd341ebc006d07797c38fcde27d1f2198
|
4
|
+
data.tar.gz: 6214d4783c4b861e162d6df141c991a324072b1ad749bc1370d400c6b6d860f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 999323aac5ada251e74eec14107200e570fa52bf829ef35c52101647b81b1245888b0d50dca3c7eec0c1b1bcd3442f320085d46b28fcff3b1c3e0e8953fe797d
|
7
|
+
data.tar.gz: fcfd05d49225306a73ad3f3dbdae29aa311d9b2e29a05118e2b0cbd02ac7948a6b8781849d5680ceb4ea7a2e219bffc410cbd771d07e27d3b81621e0db6e67d9
|
data/bin/reading
CHANGED
@@ -13,12 +13,12 @@
|
|
13
13
|
# reading '3|📕Trying|Little Library 1970147288'
|
14
14
|
# reading '📕Trying|Little Library 1970147288' 'head, sources'
|
15
15
|
|
16
|
-
require_relative
|
17
|
-
require_relative
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
16
|
+
require_relative "../lib/reading"
|
17
|
+
require_relative "../lib/reading/stats/result_formatters"
|
18
|
+
require "debug"
|
19
|
+
require "amazing_print"
|
20
|
+
require "readline"
|
21
|
+
require "pastel"
|
22
22
|
|
23
23
|
EXIT_COMMANDS = %w[exit e quit q]
|
24
24
|
PASTEL = Pastel.new
|
@@ -72,7 +72,7 @@ if ARGV[1]
|
|
72
72
|
Reading::Config.build(enabled_columns:)
|
73
73
|
end
|
74
74
|
|
75
|
-
input_is_csv_path = input.end_with?(
|
75
|
+
input_is_csv_path = input.end_with?(".csv") || input.end_with?(".txt")
|
76
76
|
|
77
77
|
if input_is_csv_path
|
78
78
|
error_handler = ->(e) { puts "Skipped a row due to a parsing error: #{e}" }
|
data/lib/reading/config.rb
CHANGED
@@ -18,7 +18,7 @@ module Reading
|
|
18
18
|
def self.parse(string)
|
19
19
|
return nil unless string.match? /\A\d+:\d\d\z/
|
20
20
|
|
21
|
-
hours, minutes = string.split(
|
21
|
+
hours, minutes = string.split(":").map(&:to_i)
|
22
22
|
new((hours * 60) + minutes)
|
23
23
|
end
|
24
24
|
|
@@ -51,7 +51,7 @@ module Reading
|
|
51
51
|
# A string in "h:mm" format.
|
52
52
|
# @return [String]
|
53
53
|
def to_s
|
54
|
-
"#{hours}:#{minutes.round.to_s.rjust(2,
|
54
|
+
"#{hours}:#{minutes.round.to_s.rjust(2, "0")} or #{(value / 60.0 * Config.hash.fetch(:pages_per_hour)).round} pages"
|
55
55
|
end
|
56
56
|
|
57
57
|
# To pages.
|
data/lib/reading/item/view.rb
CHANGED
@@ -47,7 +47,7 @@ module Reading
|
|
47
47
|
item.variants.map { |variant|
|
48
48
|
isbn = variant.isbn
|
49
49
|
if isbn
|
50
|
-
url = Config.hash.deep_fetch(:item, :view, :url_from_isbn).sub(
|
50
|
+
url = Config.hash.deep_fetch(:item, :view, :url_from_isbn).sub("%{isbn}", isbn)
|
51
51
|
else
|
52
52
|
url = variant.sources.map { |source| source.url }.compact.first
|
53
53
|
end
|
@@ -108,7 +108,7 @@ module Reading
|
|
108
108
|
if item.done?
|
109
109
|
item.last_end_date&.strftime("%Y-%m-%d")
|
110
110
|
else
|
111
|
-
item.status.to_s.gsub(
|
111
|
+
item.status.to_s.gsub("_", " ")
|
112
112
|
end
|
113
113
|
end
|
114
114
|
end
|
data/lib/reading/item.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
3
|
-
require_relative
|
1
|
+
require "date"
|
2
|
+
require_relative "experiences/history_transformer"
|
3
|
+
require_relative "experiences/dates_and_head_transformer"
|
4
4
|
|
5
5
|
module Reading
|
6
6
|
module Parsing
|
data/lib/reading/parsing/csv.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
1
|
+
require_relative "rows/blank"
|
2
|
+
require_relative "rows/regular"
|
3
|
+
require_relative "rows/compact_planned"
|
4
|
+
require_relative "rows/custom_config"
|
5
|
+
require_relative "rows/comment"
|
6
6
|
|
7
7
|
module Reading
|
8
8
|
module Parsing
|
@@ -145,7 +145,7 @@ module Reading
|
|
145
145
|
# Parse each format-plus-string into an array of segments.
|
146
146
|
heads = format_strings.map { |string|
|
147
147
|
format_emoji = string[Config.hash.deep_fetch(:regex, :formats)]
|
148
|
-
string.sub!(format_emoji,
|
148
|
+
string.sub!(format_emoji, "")
|
149
149
|
format = Config.hash.fetch(:formats).key(format_emoji)
|
150
150
|
|
151
151
|
parse_segments(column_class, string)
|
@@ -1,7 +1,7 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
1
|
+
require_relative "column"
|
2
|
+
require_relative "compact_planned_columns/head"
|
3
|
+
require_relative "regular_columns/sources"
|
4
|
+
require_relative "regular_columns/length"
|
5
5
|
|
6
6
|
module Reading
|
7
7
|
module Parsing
|
@@ -1,13 +1,13 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
1
|
+
require_relative "column"
|
2
|
+
require_relative "regular_columns/rating"
|
3
|
+
require_relative "regular_columns/head"
|
4
|
+
require_relative "regular_columns/sources"
|
5
|
+
require_relative "regular_columns/start_dates"
|
6
|
+
require_relative "regular_columns/end_dates"
|
7
|
+
require_relative "regular_columns/genres"
|
8
|
+
require_relative "regular_columns/length"
|
9
|
+
require_relative "regular_columns/notes"
|
10
|
+
require_relative "regular_columns/history"
|
11
11
|
|
12
12
|
module Reading
|
13
13
|
module Parsing
|
@@ -1,12 +1,12 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
1
|
+
require_relative "attributes/shared"
|
2
|
+
require_relative "attributes/attribute"
|
3
|
+
require_relative "attributes/rating"
|
4
|
+
require_relative "attributes/author"
|
5
|
+
require_relative "attributes/title"
|
6
|
+
require_relative "attributes/genres"
|
7
|
+
require_relative "attributes/variants"
|
8
|
+
require_relative "attributes/experiences"
|
9
|
+
require_relative "attributes/notes"
|
10
10
|
|
11
11
|
module Reading
|
12
12
|
module Parsing
|
data/lib/reading/stats/filter.rb
CHANGED
@@ -82,7 +82,7 @@ module Reading
|
|
82
82
|
end
|
83
83
|
}
|
84
84
|
|
85
|
-
positive_operator = operator == :
|
85
|
+
positive_operator = operator == :"!=" ? :== : operator
|
86
86
|
|
87
87
|
matches = items.select { |item|
|
88
88
|
ratings.any? { |rating|
|
@@ -95,34 +95,39 @@ module Reading
|
|
95
95
|
# Instead of using item.rating.send(operator, format) above, invert
|
96
96
|
# the matches here to ensure multiple values after a negative operator
|
97
97
|
# have an "and" relation: "not(x and y)", rather than "not(x or y)".
|
98
|
-
if operator == :
|
98
|
+
if operator == :"!="
|
99
99
|
matches = items - matches
|
100
100
|
end
|
101
101
|
|
102
102
|
matches
|
103
103
|
},
|
104
|
-
|
105
|
-
|
106
|
-
|
104
|
+
progress: proc { |values, operator, items|
|
105
|
+
progresses = values.map { |value|
|
106
|
+
value = "0%" if value == "0"
|
107
|
+
match = value.match(/\A(\d+)?%/)
|
108
|
+
|
109
|
+
(match && match.captures.first.to_f.clamp(0.0, 100.0) / 100) ||
|
107
110
|
(raise InputError, "Progress must be a percentage")
|
108
111
|
}
|
109
112
|
|
110
113
|
filtered_items = items.map { |item|
|
111
114
|
# Ensure multiple values after a negative operator have an "and"
|
112
115
|
# relation: "not(x and y)", rather than "not(x or y)".
|
113
|
-
if operator == :
|
114
|
-
|
115
|
-
experience.spans.last.progress
|
116
|
+
if operator == :"!="
|
117
|
+
item_progresses = item.experiences.map { |experience|
|
118
|
+
experience.spans.last.progress || 0.0
|
116
119
|
}
|
117
120
|
|
118
|
-
next if (
|
121
|
+
next if (item_progresses - progresses).empty?
|
119
122
|
end
|
120
123
|
|
121
124
|
# Filter out non-matching experiences.
|
125
|
+
# TODO: Make this more accurate by calculating the average progress
|
126
|
+
# across spans? Currently it checks just the last span's progress.
|
122
127
|
filtered_experiences = item.experiences.select { |experience|
|
123
|
-
|
124
|
-
experience.
|
125
|
-
experience.spans.last.progress.send(operator,
|
128
|
+
progresses.any? { |progress|
|
129
|
+
experience.spans.any? &&
|
130
|
+
(experience.spans.last.progress || 0.0).send(operator, progress)
|
126
131
|
}
|
127
132
|
}
|
128
133
|
|
@@ -138,7 +143,7 @@ module Reading
|
|
138
143
|
filtered_items = items.map { |item|
|
139
144
|
# Treat empty variants as if they were a variant with a nil format.
|
140
145
|
if item.variants.empty?
|
141
|
-
if operator == :
|
146
|
+
if operator == :"!="
|
142
147
|
next item unless formats.include?(nil)
|
143
148
|
else
|
144
149
|
next item if formats.include?(nil)
|
@@ -147,7 +152,7 @@ module Reading
|
|
147
152
|
|
148
153
|
# Ensure multiple values after a negative operator have an "and"
|
149
154
|
# relation: "not(x and y)", rather than "not(x or y)".
|
150
|
-
if operator == :
|
155
|
+
if operator == :"!="
|
151
156
|
item_formats = item.variants.map(&:format)
|
152
157
|
|
153
158
|
next if (item_formats - formats).empty?
|
@@ -169,14 +174,14 @@ module Reading
|
|
169
174
|
author: proc { |values, operator, items|
|
170
175
|
authors = values
|
171
176
|
.map { _1.downcase if _1 }
|
172
|
-
.map { _1.gsub(/[^a-zA-Z ]/,
|
177
|
+
.map { _1.gsub(/[^a-zA-Z ]/, "").gsub(/\s/, "") if _1 }
|
173
178
|
|
174
179
|
matches = items.select { |item|
|
175
180
|
author = item
|
176
181
|
&.author
|
177
182
|
&.downcase
|
178
|
-
&.gsub(/[^a-zA-Z ]/,
|
179
|
-
&.gsub(/\s/,
|
183
|
+
&.gsub(/[^a-zA-Z ]/, "")
|
184
|
+
&.gsub(/\s/, "")
|
180
185
|
|
181
186
|
if %i[include? exclude?].include? operator
|
182
187
|
authors.any? {
|
@@ -200,7 +205,7 @@ module Reading
|
|
200
205
|
title: proc { |values, operator, items|
|
201
206
|
titles = values
|
202
207
|
.map(&:downcase)
|
203
|
-
.map { _1.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/,
|
208
|
+
.map { _1.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/, "").gsub(/\s/, "") }
|
204
209
|
|
205
210
|
matches = items.select { |item|
|
206
211
|
next unless item.title
|
@@ -208,8 +213,8 @@ module Reading
|
|
208
213
|
title = item
|
209
214
|
.title
|
210
215
|
.downcase
|
211
|
-
.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/,
|
212
|
-
.gsub(/\s/,
|
216
|
+
.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/, "")
|
217
|
+
.gsub(/\s/, "")
|
213
218
|
|
214
219
|
if %i[include? exclude?].include? operator
|
215
220
|
titles.any? { title.include? _1 }
|
@@ -228,8 +233,8 @@ module Reading
|
|
228
233
|
format_name = ->(str) {
|
229
234
|
str
|
230
235
|
&.downcase
|
231
|
-
&.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/,
|
232
|
-
&.gsub(/\s/,
|
236
|
+
&.gsub(/[^a-zA-Z0-9 ]|\ba\b|\bthe\b/, "")
|
237
|
+
&.gsub(/\s/, "")
|
233
238
|
}
|
234
239
|
|
235
240
|
series_names = values.map { format_name.call(_1) }
|
@@ -251,7 +256,7 @@ module Reading
|
|
251
256
|
# Ensure multiple values after a negative operator have an "and"
|
252
257
|
# relation: "not(x and y)", rather than "not(x or y)".
|
253
258
|
if %i[!= exclude?].include? operator
|
254
|
-
next if operator == :
|
259
|
+
next if operator == :"!=" && (item_series_names - series_names).empty?
|
255
260
|
next if operator == :exclude? &&
|
256
261
|
item_series_names.all? { |item_series_name|
|
257
262
|
series_names.any? { |series_name|
|
@@ -280,7 +285,7 @@ module Reading
|
|
280
285
|
|
281
286
|
series_names.any? {
|
282
287
|
if _1.nil?
|
283
|
-
nil_operator = { include?: :==, exclude?: :
|
288
|
+
nil_operator = { include?: :==, exclude?: :"!=" }[operator]
|
284
289
|
end
|
285
290
|
|
286
291
|
item_series_name.send(nil_operator || operator, _1)
|
@@ -322,7 +327,7 @@ module Reading
|
|
322
327
|
(url.nil? ? url_nil_match : sources.include?(url))
|
323
328
|
}
|
324
329
|
|
325
|
-
next if operator == :
|
330
|
+
next if operator == :"!=" && remainder_names_and_urls.empty?
|
326
331
|
next if operator == :exclude? &&
|
327
332
|
item_source_names_and_urls.all? { |item_source_name_and_url|
|
328
333
|
sources.any? { |source|
|
@@ -347,7 +352,7 @@ module Reading
|
|
347
352
|
variant.sources.any? { |source|
|
348
353
|
sources.any? {
|
349
354
|
if _1.nil?
|
350
|
-
nil_operator = { include?: :==, exclude?: :
|
355
|
+
nil_operator = { include?: :==, exclude?: :"!=" }[operator]
|
351
356
|
end
|
352
357
|
|
353
358
|
source.name&.downcase&.send(nil_operator || operator, _1) ||
|
@@ -362,7 +367,7 @@ module Reading
|
|
362
367
|
|
363
368
|
filtered_items
|
364
369
|
},
|
365
|
-
:
|
370
|
+
:"end-date" => proc { |values, operator, items|
|
366
371
|
end_date_ranges = values.map { |value|
|
367
372
|
match = value.match(DATES_REGEX) ||
|
368
373
|
(raise InputError,
|
@@ -396,7 +401,7 @@ module Reading
|
|
396
401
|
filtered_items = items.map { |item|
|
397
402
|
# Ensure multiple values after a negative operator have an "and"
|
398
403
|
# relation: "not(x and y)", rather than "not(x or y)".
|
399
|
-
if operator == :
|
404
|
+
if operator == :"!="
|
400
405
|
item_end_dates = item.experiences.map(&:last_end_date)
|
401
406
|
|
402
407
|
next if item_end_dates.all? { |item_end_date|
|
@@ -468,7 +473,7 @@ module Reading
|
|
468
473
|
without_before_or_after
|
469
474
|
}
|
470
475
|
.compact
|
471
|
-
when :
|
476
|
+
when :"!="
|
472
477
|
split_item = item
|
473
478
|
|
474
479
|
date_ranges.each do |date_range|
|
@@ -506,7 +511,7 @@ module Reading
|
|
506
511
|
(raise InputError, "Experience count must be an integer")
|
507
512
|
}
|
508
513
|
|
509
|
-
positive_operator = operator == :
|
514
|
+
positive_operator = operator == :"!=" ? :== : operator
|
510
515
|
|
511
516
|
matches = items.select { |item|
|
512
517
|
experience_counts.any? { |experience_count|
|
@@ -514,19 +519,19 @@ module Reading
|
|
514
519
|
}
|
515
520
|
}
|
516
521
|
|
517
|
-
if operator == :
|
522
|
+
if operator == :"!="
|
518
523
|
matches = items - matches
|
519
524
|
end
|
520
525
|
|
521
526
|
matches
|
522
527
|
},
|
523
528
|
status: proc { |values, operator, items|
|
524
|
-
statuses = values.map { _1.squeeze(
|
529
|
+
statuses = values.map { _1.squeeze(" ").gsub(" ", "_").to_sym }
|
525
530
|
|
526
531
|
filtered_items = items.map { |item|
|
527
532
|
# Ensure multiple values after a negative operator have an "and"
|
528
533
|
# relation: "not(x and y)", rather than "not(x or y)".
|
529
|
-
if operator == :
|
534
|
+
if operator == :"!="
|
530
535
|
item_statuses = item.experiences.map(&:status).presence || [:planned]
|
531
536
|
|
532
537
|
next unless (item_statuses - statuses).any?
|
@@ -550,7 +555,7 @@ module Reading
|
|
550
555
|
filtered_items
|
551
556
|
},
|
552
557
|
genre: proc { |values, operator, items|
|
553
|
-
genres = values.map { _1 ? _1.split(
|
558
|
+
genres = values.map { _1 ? _1.split("+").map(&:strip) : [_1] }
|
554
559
|
|
555
560
|
matches = items.select { |item|
|
556
561
|
genres.any? { |and_genres|
|
@@ -560,7 +565,7 @@ module Reading
|
|
560
565
|
}
|
561
566
|
}
|
562
567
|
|
563
|
-
if operator == :
|
568
|
+
if operator == :"!="
|
564
569
|
matches = items - matches
|
565
570
|
end
|
566
571
|
|
@@ -578,7 +583,7 @@ module Reading
|
|
578
583
|
filtered_items = items.map { |item|
|
579
584
|
# Treat empty variants as if they were a variant with a nil length.
|
580
585
|
if item.variants.empty?
|
581
|
-
if operator == :
|
586
|
+
if operator == :"!="
|
582
587
|
next item unless lengths.include?(nil)
|
583
588
|
else
|
584
589
|
next item if lengths.include?(nil)
|
@@ -587,7 +592,7 @@ module Reading
|
|
587
592
|
|
588
593
|
# Ensure multiple values after a negative operator have an "and"
|
589
594
|
# relation: "not(x and y)", rather than "not(x or y)".
|
590
|
-
if operator == :
|
595
|
+
if operator == :"!="
|
591
596
|
item_lengths = item.variants.map(&:length)
|
592
597
|
|
593
598
|
next if (item_lengths - lengths).empty?
|
@@ -609,13 +614,13 @@ module Reading
|
|
609
614
|
note: proc { |values, operator, items|
|
610
615
|
notes = values
|
611
616
|
.map { _1.downcase if _1 }
|
612
|
-
.map { _1.gsub(/[^a-zA-Z0-9 ]/,
|
617
|
+
.map { _1.gsub(/[^a-zA-Z0-9 ]/, "") if _1 }
|
613
618
|
|
614
619
|
matches = items.select { |item|
|
615
620
|
item.notes.any? { |original_note|
|
616
621
|
note = original_note
|
617
622
|
.downcase
|
618
|
-
.gsub(/[^a-zA-Z0-9 ]/,
|
623
|
+
.gsub(/[^a-zA-Z0-9 ]/, "")
|
619
624
|
|
620
625
|
if %i[include? exclude?].include? operator
|
621
626
|
notes.any? { _1.nil? ? note == _1 : note.include?(_1) }
|
@@ -635,11 +640,10 @@ module Reading
|
|
635
640
|
|
636
641
|
NUMERIC_OPERATORS = {
|
637
642
|
rating: true,
|
638
|
-
done: true,
|
639
643
|
progress: true,
|
640
644
|
experience: true,
|
641
645
|
date: true,
|
642
|
-
:
|
646
|
+
:"end-date" => true,
|
643
647
|
length: true,
|
644
648
|
}
|
645
649
|
|
@@ -650,18 +654,18 @@ module Reading
|
|
650
654
|
}
|
651
655
|
|
652
656
|
PROHIBIT_NONE_VALUE = {
|
653
|
-
|
657
|
+
progress: true,
|
654
658
|
title: true,
|
655
|
-
:
|
659
|
+
:"end-date" => true,
|
656
660
|
date: true,
|
657
661
|
experience: true,
|
658
662
|
status: true,
|
659
663
|
}
|
660
664
|
|
661
665
|
PROHIBIT_MULTIPLE_VALUES_AFTER_NOT = {
|
662
|
-
|
666
|
+
progress: true,
|
663
667
|
title: true,
|
664
|
-
:
|
668
|
+
:"end-date" => true,
|
665
669
|
date: true,
|
666
670
|
experience: true,
|
667
671
|
status: true,
|
@@ -699,20 +703,20 @@ module Reading
|
|
699
703
|
|
700
704
|
unless allowed_operators.include? operator_str
|
701
705
|
raise InputError, "Operator \"#{operator_str}\" not allowed in the " \
|
702
|
-
"\"#{key}\" filter, only #{allowed_operators.join(
|
706
|
+
"\"#{key}\" filter, only #{allowed_operators.join(", ")} allowed"
|
703
707
|
end
|
704
708
|
|
705
709
|
operator = operator_str.to_sym
|
706
|
-
operator = :== if operator == :
|
710
|
+
operator = :== if operator == :"="
|
707
711
|
operator = :include? if operator == :~
|
708
|
-
operator = :exclude? if operator == :
|
712
|
+
operator = :exclude? if operator == :"!~"
|
709
713
|
|
710
714
|
values = predicate
|
711
|
-
.split(
|
715
|
+
.split(",")
|
712
716
|
.map(&:strip)
|
713
|
-
.map { _1.downcase ==
|
717
|
+
.map { _1.downcase == "none" ? nil : _1 }
|
714
718
|
|
715
|
-
# if values.count > 1 && operator == :
|
719
|
+
# if values.count > 1 && operator == :"!=" && PROHIBIT_MULTIPLE_VALUES_AFTER_NOT[key]
|
716
720
|
# end
|
717
721
|
|
718
722
|
if values.count > 1 && %i[> < >= <=].include?(operator)
|
@@ -16,10 +16,10 @@ module Reading
|
|
16
16
|
|
17
17
|
if match
|
18
18
|
group_names = match[:groups]
|
19
|
-
.split(
|
19
|
+
.split(",")
|
20
20
|
.tap { _1.last.sub!(/(\w)\s+\w+/, '\1') }
|
21
21
|
.map(&:strip)
|
22
|
-
.map { _1.delete_suffix(
|
22
|
+
.map { _1.delete_suffix("s") }
|
23
23
|
.map(&:to_sym)
|
24
24
|
|
25
25
|
if group_names.uniq.count < group_names.count
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "bigdecimal/util"
|
2
|
+
|
1
3
|
module Reading
|
2
4
|
module Stats
|
3
5
|
# The beginning of a query which specifies what it does, e.g.
|
@@ -115,96 +117,145 @@ module Reading
|
|
115
117
|
items.sum { |item|
|
116
118
|
item.experiences.sum { |experience|
|
117
119
|
experience.spans.sum { |span|
|
118
|
-
(span.amount * span.progress).to_i_if_whole
|
120
|
+
(span.amount * (span.progress || 0.0)).to_i_if_whole
|
119
121
|
}
|
120
122
|
}
|
121
123
|
}
|
122
124
|
},
|
123
125
|
top_rating: proc { |items, number_arg|
|
124
126
|
items
|
125
|
-
.max_by(number_arg || DEFAULT_NUMBER_ARG) { _1.rating || 0}
|
126
127
|
.map { |item| [author_and_title(item), item.rating] }
|
128
|
+
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, rating|
|
129
|
+
rating || 0
|
130
|
+
}
|
127
131
|
},
|
128
132
|
top_length: proc { |items, number_arg|
|
129
133
|
items
|
130
134
|
.map { |item|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
]
|
135
|
+
# Longest length, or if undefined length then longest experience
|
136
|
+
# (code adapted from top_amount below).
|
137
|
+
length = item.variants.map(&:length).max ||
|
138
|
+
item.experiences.map { |experience|
|
139
|
+
experience.spans.sum { |span|
|
140
|
+
(span.amount * (span.progress || 0.0)).to_i_if_whole
|
141
|
+
}
|
142
|
+
}.max
|
143
|
+
|
144
|
+
[author_and_title(item), length]
|
142
145
|
}
|
143
146
|
.reject { |_title, length| length.nil? }
|
144
|
-
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
|
147
|
+
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
|
148
|
+
length
|
149
|
+
}
|
145
150
|
},
|
146
151
|
top_amount: proc { |items, number_arg|
|
147
152
|
items
|
148
153
|
.map { |item|
|
149
154
|
amount = item.experiences.sum { |experience|
|
150
155
|
experience.spans.sum { |span|
|
151
|
-
(span.amount * span.progress).to_i_if_whole
|
156
|
+
(span.amount * (span.progress || 0.0)).to_i_if_whole
|
152
157
|
}
|
153
158
|
}
|
154
159
|
|
155
160
|
[author_and_title(item), amount]
|
156
161
|
}
|
157
162
|
.reject { |_title, amount| amount.zero? }
|
158
|
-
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
|
163
|
+
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
|
164
|
+
amount
|
165
|
+
}
|
159
166
|
},
|
160
167
|
top_speed: proc { |items, number_arg|
|
161
168
|
items
|
162
|
-
.map { |item|
|
169
|
+
.map { |item|
|
170
|
+
speed = calculate_speed(item)
|
171
|
+
[author_and_title(item), speed] if speed
|
172
|
+
}
|
163
173
|
.compact
|
164
174
|
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, speed_hash|
|
165
175
|
speed_hash[:amount] / speed_hash[:days].to_f
|
166
176
|
}
|
167
177
|
},
|
178
|
+
top_experience: proc { |items, number_arg|
|
179
|
+
items
|
180
|
+
.map { |item|
|
181
|
+
experience_count = item
|
182
|
+
.experiences
|
183
|
+
.count { |experience|
|
184
|
+
experience.spans.all? { _1.progress.to_d == "1.0".to_d }
|
185
|
+
}
|
186
|
+
|
187
|
+
[author_and_title(item), [experience_count, item.rating || 0]]
|
188
|
+
}
|
189
|
+
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, experience_count_and_rating|
|
190
|
+
experience_count_and_rating
|
191
|
+
}
|
192
|
+
.map { |title, (experience_count, _rating)|
|
193
|
+
[title, experience_count]
|
194
|
+
}
|
195
|
+
},
|
196
|
+
top_note: proc { |items, number_arg|
|
197
|
+
items
|
198
|
+
.map { |item|
|
199
|
+
notes_word_count = item
|
200
|
+
.notes
|
201
|
+
.sum { |note|
|
202
|
+
note.content.scan(/[\w[:punct:]]+/).count
|
203
|
+
}
|
204
|
+
|
205
|
+
[author_and_title(item), notes_word_count]
|
206
|
+
}
|
207
|
+
.max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, notes_word_count|
|
208
|
+
notes_word_count
|
209
|
+
}
|
210
|
+
},
|
168
211
|
bottom_rating: proc { |items, number_arg|
|
169
212
|
items
|
170
|
-
.min_by(number_arg || DEFAULT_NUMBER_ARG) { _1.rating || 0}
|
171
213
|
.map { |item| [author_and_title(item), item.rating] }
|
214
|
+
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, rating|
|
215
|
+
rating || 0
|
216
|
+
}
|
172
217
|
},
|
173
218
|
bottom_length: proc { |items, number_arg|
|
174
219
|
items
|
175
220
|
.map { |item|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
]
|
221
|
+
# Longest length, or if undefined length then longest experience
|
222
|
+
# (code adapted from bottom_amount below).
|
223
|
+
length = item.variants.map(&:length).max ||
|
224
|
+
item.experiences.map { |experience|
|
225
|
+
experience.spans.sum { |span|
|
226
|
+
(span.amount * (span.progress || 0.0)).to_i_if_whole
|
227
|
+
}
|
228
|
+
}.max
|
229
|
+
|
230
|
+
[author_and_title(item), length]
|
187
231
|
}
|
188
232
|
.reject { |_title, length| length.nil? }
|
189
|
-
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
|
233
|
+
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
|
234
|
+
length
|
235
|
+
}
|
190
236
|
},
|
191
237
|
bottom_amount: proc { |items, number_arg|
|
192
238
|
items
|
193
239
|
.map { |item|
|
194
240
|
amount = item.experiences.sum { |experience|
|
195
241
|
experience.spans.sum { |span|
|
196
|
-
(span.amount * span.progress).to_i_if_whole
|
242
|
+
(span.amount * (span.progress || 0.0)).to_i_if_whole
|
197
243
|
}
|
198
244
|
}
|
199
245
|
|
200
246
|
[author_and_title(item), amount]
|
201
247
|
}
|
202
248
|
.reject { |_title, amount| amount.zero? }
|
203
|
-
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
|
249
|
+
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
|
250
|
+
amount
|
251
|
+
}
|
204
252
|
},
|
205
253
|
bottom_speed: proc { |items, number_arg|
|
206
254
|
items
|
207
|
-
.map { |item|
|
255
|
+
.map { |item|
|
256
|
+
speed = calculate_speed(item)
|
257
|
+
[author_and_title(item), speed] if speed
|
258
|
+
}
|
208
259
|
.compact
|
209
260
|
.min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, speed_hash|
|
210
261
|
speed_hash[:amount] / speed_hash[:days].to_f
|
@@ -227,6 +278,8 @@ module Reading
|
|
227
278
|
top_length: %w[tl],
|
228
279
|
top_amount: %w[ta],
|
229
280
|
top_speed: %w[ts],
|
281
|
+
top_experience: %w[te],
|
282
|
+
top_note: %w[tn],
|
230
283
|
bottom_rating: %w[br],
|
231
284
|
bottom_length: %w[bl],
|
232
285
|
bottom_amount: %w[ba],
|
@@ -235,7 +288,7 @@ module Reading
|
|
235
288
|
}
|
236
289
|
|
237
290
|
REGEXES = ACTIONS.map { |key, _action|
|
238
|
-
first_word, second_word = key.to_s.split(
|
291
|
+
first_word, second_word = key.to_s.split("_")
|
239
292
|
aliases = ALIASES.fetch(key)
|
240
293
|
|
241
294
|
regex =
|
@@ -260,7 +313,7 @@ module Reading
|
|
260
313
|
(
|
261
314
|
\A
|
262
315
|
\s*
|
263
|
-
(#{aliases.join(
|
316
|
+
(#{aliases.join("|")})
|
264
317
|
s?
|
265
318
|
\s*
|
266
319
|
(?<number_arg>
|
@@ -328,10 +381,7 @@ module Reading
|
|
328
381
|
|
329
382
|
return nil unless speeds.any?
|
330
383
|
|
331
|
-
|
332
|
-
.max_by { |hash| hash[:amount] / hash[:days].to_f }
|
333
|
-
|
334
|
-
[author_and_title(item), speed]
|
384
|
+
speeds.max_by { |hash| hash[:amount] / hash[:days].to_f }
|
335
385
|
end
|
336
386
|
|
337
387
|
# A shorter version of Item::View#name.
|
data/lib/reading/stats/query.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
1
|
+
require "pastel"
|
2
|
+
require_relative "operation"
|
3
|
+
require_relative "filter"
|
4
|
+
require_relative "grouping"
|
5
|
+
require_relative "result_formatters"
|
5
6
|
|
6
7
|
module Reading
|
7
8
|
module Stats
|
@@ -13,9 +14,8 @@ module Reading
|
|
13
14
|
# @param items [Array<Item>] the Items to be queried.
|
14
15
|
# @param result_formatters [Boolean, Hash{Symbol => Proc}] to alter the
|
15
16
|
# appearance of results; keys should be from among the keys of
|
16
|
-
# Operation::ACTIONS. Pre-made formatters
|
17
|
-
|
18
|
-
def initialize(input:, items:, result_formatters: {})
|
17
|
+
# Operation::ACTIONS. Pre-made formatters are in result_formatters.rb.
|
18
|
+
def initialize(input:, items:, result_formatters: Reading::Stats::ResultFormatters::TRUNCATED_TITLES)
|
19
19
|
@input = input
|
20
20
|
@items = items
|
21
21
|
@result_formatters = result_formatters
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "pastel"
|
2
|
+
|
3
|
+
module Reading
|
4
|
+
module Stats
|
5
|
+
module ResultFormatters
|
6
|
+
TRUNCATED_TITLES = {
|
7
|
+
top_length: ->(result) { with_truncated_title(result) },
|
8
|
+
top_amount: ->(result) { with_truncated_title(result) },
|
9
|
+
top_speed: ->(result) { with_truncated_title(result) },
|
10
|
+
top_experience: ->(result) { with_truncated_title(result) },
|
11
|
+
top_note: ->(result) { with_truncated_title(result) },
|
12
|
+
bottom_length: ->(result) { with_truncated_title(result) },
|
13
|
+
botom_amount: ->(result) { with_truncated_title(result) },
|
14
|
+
bottom_speed: ->(result) { with_truncated_title(result) },
|
15
|
+
}
|
16
|
+
|
17
|
+
TERMINAL = {
|
18
|
+
average_length: ->(result) { length_to_s(result) },
|
19
|
+
average_amount: ->(result) { length_to_s(result) },
|
20
|
+
:"average_daily-amount" => ->(result) { "#{length_to_s(result)} per day" },
|
21
|
+
total_item: ->(result) {
|
22
|
+
if result.zero?
|
23
|
+
PASTEL.bright_black("none")
|
24
|
+
else
|
25
|
+
color("#{result} #{result == 1 ? "item" : "items"}")
|
26
|
+
end
|
27
|
+
},
|
28
|
+
total_amount: ->(result) { length_to_s(result) },
|
29
|
+
top_rating: ->(result) { top_or_bottom_numbers_string(result, noun: "star") },
|
30
|
+
top_length: ->(result) { top_or_bottom_lengths_string(result) },
|
31
|
+
top_amount: ->(result) { top_or_bottom_lengths_string(result) },
|
32
|
+
top_speed: ->(result) { top_or_bottom_speeds_string(result) },
|
33
|
+
top_experience: ->(result) { top_or_bottom_numbers_string(result, noun: "experience") },
|
34
|
+
top_note: ->(result) { top_or_bottom_numbers_string(result, noun: "word") },
|
35
|
+
bottom_rating: ->(result) { top_or_bottom_numbers_string(result, noun: "star") },
|
36
|
+
bottom_length: ->(result) { top_or_bottom_lengths_string(result) },
|
37
|
+
botom_amount: ->(result) { top_or_bottom_lengths_string(result) },
|
38
|
+
bottom_speed: ->(result) { top_or_bottom_speeds_string(result) },
|
39
|
+
}
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
PASTEL = Pastel.new
|
44
|
+
|
45
|
+
# Applies a terminal color.
|
46
|
+
# @param string [String]
|
47
|
+
# @return [String]
|
48
|
+
private_class_method def self.color(string)
|
49
|
+
PASTEL.bright_blue(string)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Converts a length/amount (pages or time) into a string.
|
53
|
+
# @param length [Numeric, Reading::Item::TimeLength]
|
54
|
+
# @param color [Boolean] whether a terminal color should be applied.
|
55
|
+
# @return [String]
|
56
|
+
private_class_method def self.length_to_s(length, color: true)
|
57
|
+
if length.is_a?(Numeric)
|
58
|
+
length_string = "#{length.round} pages"
|
59
|
+
else
|
60
|
+
length_string = length.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
color ? color(length_string) : length_string
|
64
|
+
end
|
65
|
+
|
66
|
+
# Formats a list of top/bottom length results as a string.
|
67
|
+
# @param result [Array]
|
68
|
+
# @return [String]
|
69
|
+
private_class_method def self.top_or_bottom_lengths_string(result)
|
70
|
+
offset = result.count.digits.count
|
71
|
+
|
72
|
+
result
|
73
|
+
.map.with_index { |(title, length), index|
|
74
|
+
pad = " " * (offset - (index + 1).digits.count)
|
75
|
+
|
76
|
+
title_line = "#{index + 1}. #{pad}#{title}"
|
77
|
+
indent = " #{" " * offset}"
|
78
|
+
|
79
|
+
"#{title_line}\n#{indent}#{length_to_s(length)}"
|
80
|
+
}
|
81
|
+
.join("\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Formats a list of top/bottom speed results as a string.
|
85
|
+
# @param result [Array]
|
86
|
+
# @return [String]
|
87
|
+
private_class_method def self.top_or_bottom_speeds_string(result)
|
88
|
+
offset = result.count.digits.count
|
89
|
+
|
90
|
+
result
|
91
|
+
.map.with_index { |(title, hash), index|
|
92
|
+
amount = length_to_s(hash[:amount], color: false)
|
93
|
+
days = "#{hash[:days]} #{hash[:days] == 1 ? "day" : "days"}"
|
94
|
+
pad = " " * (offset - (index + 1).digits.count)
|
95
|
+
|
96
|
+
title_line = "#{index + 1}. #{pad}#{title}"
|
97
|
+
indent = " #{" " * offset}"
|
98
|
+
colored_speed = color("#{amount} in #{days}")
|
99
|
+
|
100
|
+
"#{title_line}\n#{indent}#{colored_speed}"
|
101
|
+
}
|
102
|
+
.join("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Formats a list of top/bottom number results as a string.
|
106
|
+
private_class_method def self.top_or_bottom_numbers_string(result, noun:)
|
107
|
+
offset = result.count.digits.count
|
108
|
+
|
109
|
+
result
|
110
|
+
.map.with_index { |(title, number), index|
|
111
|
+
pad = " " * (offset - (index + 1).digits.count)
|
112
|
+
|
113
|
+
title_line = "#{index + 1}. #{pad}#{title}"
|
114
|
+
indent = " #{" " * offset}"
|
115
|
+
number_string = color("#{number} #{number == 1 ? noun : "#{noun}s"}")
|
116
|
+
|
117
|
+
"#{title_line}\n#{indent}#{number_string}"
|
118
|
+
}
|
119
|
+
.join("\n")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Truncates the title of each result to a specified length.
|
123
|
+
# @param result [Array]
|
124
|
+
# @param length [Integer] the maximum length of the title.
|
125
|
+
# @return [Array]
|
126
|
+
private_class_method def self.with_truncated_title(result, length: 45)
|
127
|
+
result.map do |title, value|
|
128
|
+
truncated_title =
|
129
|
+
if title.length + 1 > length
|
130
|
+
"#{title[0...length]}…"
|
131
|
+
else
|
132
|
+
title
|
133
|
+
end
|
134
|
+
|
135
|
+
[truncated_title, value]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/reading/version.rb
CHANGED
data/lib/reading.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
Dir[File.join(__dir__,
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
1
|
+
Dir[File.join(__dir__, "reading", "util", "*.rb")].each { |file| require file }
|
2
|
+
require_relative "reading/errors"
|
3
|
+
require_relative "reading/config"
|
4
|
+
require_relative "reading/parsing/csv"
|
5
|
+
require_relative "reading/filter"
|
6
|
+
require_relative "reading/stats/query"
|
7
|
+
require_relative "reading/item/time_length.rb"
|
8
8
|
|
9
9
|
# The gem's public API. See https://github.com/fpsvogel/reading#usage
|
10
10
|
#
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reading
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felipe Vogel
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: pastel
|
@@ -30,56 +29,70 @@ dependencies:
|
|
30
29
|
requirements:
|
31
30
|
- - "~>"
|
32
31
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
32
|
+
version: '1.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: bigdecimal
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
34
47
|
type: :runtime
|
35
48
|
prerelease: false
|
36
49
|
version_requirements: !ruby/object:Gem::Requirement
|
37
50
|
requirements:
|
38
51
|
- - "~>"
|
39
52
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
53
|
+
version: '3.0'
|
41
54
|
- !ruby/object:Gem::Dependency
|
42
55
|
name: debug
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
44
57
|
requirements:
|
45
58
|
- - "~>"
|
46
59
|
- !ruby/object:Gem::Version
|
47
|
-
version: '1.
|
60
|
+
version: '1.0'
|
48
61
|
type: :development
|
49
62
|
prerelease: false
|
50
63
|
version_requirements: !ruby/object:Gem::Requirement
|
51
64
|
requirements:
|
52
65
|
- - "~>"
|
53
66
|
- !ruby/object:Gem::Version
|
54
|
-
version: '1.
|
67
|
+
version: '1.0'
|
55
68
|
- !ruby/object:Gem::Dependency
|
56
69
|
name: minitest
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
58
71
|
requirements:
|
59
72
|
- - "~>"
|
60
73
|
- !ruby/object:Gem::Version
|
61
|
-
version: '5.
|
74
|
+
version: '5.0'
|
62
75
|
type: :development
|
63
76
|
prerelease: false
|
64
77
|
version_requirements: !ruby/object:Gem::Requirement
|
65
78
|
requirements:
|
66
79
|
- - "~>"
|
67
80
|
- !ruby/object:Gem::Version
|
68
|
-
version: '5.
|
81
|
+
version: '5.0'
|
69
82
|
- !ruby/object:Gem::Dependency
|
70
83
|
name: minitest-reporters
|
71
84
|
requirement: !ruby/object:Gem::Requirement
|
72
85
|
requirements:
|
73
86
|
- - "~>"
|
74
87
|
- !ruby/object:Gem::Version
|
75
|
-
version: '1.
|
88
|
+
version: '1.0'
|
76
89
|
type: :development
|
77
90
|
prerelease: false
|
78
91
|
version_requirements: !ruby/object:Gem::Requirement
|
79
92
|
requirements:
|
80
93
|
- - "~>"
|
81
94
|
- !ruby/object:Gem::Version
|
82
|
-
version: '1.
|
95
|
+
version: '1.0'
|
83
96
|
- !ruby/object:Gem::Dependency
|
84
97
|
name: shoulda-context
|
85
98
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,21 +121,34 @@ dependencies:
|
|
108
121
|
- - "~>"
|
109
122
|
- !ruby/object:Gem::Version
|
110
123
|
version: '1.0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: rake
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '13.0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '13.0'
|
111
138
|
- !ruby/object:Gem::Dependency
|
112
139
|
name: rubycritic
|
113
140
|
requirement: !ruby/object:Gem::Requirement
|
114
141
|
requirements:
|
115
142
|
- - "~>"
|
116
143
|
- !ruby/object:Gem::Version
|
117
|
-
version: '4.
|
144
|
+
version: '4.0'
|
118
145
|
type: :development
|
119
146
|
prerelease: false
|
120
147
|
version_requirements: !ruby/object:Gem::Requirement
|
121
148
|
requirements:
|
122
149
|
- - "~>"
|
123
150
|
- !ruby/object:Gem::Version
|
124
|
-
version: '4.
|
125
|
-
description:
|
151
|
+
version: '4.0'
|
126
152
|
email:
|
127
153
|
- fps.vogel@gmail.com
|
128
154
|
executables:
|
@@ -173,7 +199,7 @@ files:
|
|
173
199
|
- lib/reading/stats/grouping.rb
|
174
200
|
- lib/reading/stats/operation.rb
|
175
201
|
- lib/reading/stats/query.rb
|
176
|
-
- lib/reading/stats/
|
202
|
+
- lib/reading/stats/result_formatters.rb
|
177
203
|
- lib/reading/util/blank.rb
|
178
204
|
- lib/reading/util/exclude.rb
|
179
205
|
- lib/reading/util/hash_array_deep_fetch.rb
|
@@ -190,7 +216,6 @@ metadata:
|
|
190
216
|
homepage_uri: https://github.com/fpsvogel/reading
|
191
217
|
source_code_uri: https://github.com/fpsvogel/reading
|
192
218
|
changelog_uri: https://github.com/fpsvogel/reading/blob/main/CHANGELOG.md
|
193
|
-
post_install_message:
|
194
219
|
rdoc_options: []
|
195
220
|
require_paths:
|
196
221
|
- lib
|
@@ -198,15 +223,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
198
223
|
requirements:
|
199
224
|
- - "~>"
|
200
225
|
- !ruby/object:Gem::Version
|
201
|
-
version: 3.
|
226
|
+
version: 3.4.4
|
202
227
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
228
|
requirements:
|
204
229
|
- - ">="
|
205
230
|
- !ruby/object:Gem::Version
|
206
231
|
version: '0'
|
207
232
|
requirements: []
|
208
|
-
rubygems_version: 3.
|
209
|
-
signing_key:
|
233
|
+
rubygems_version: 3.6.7
|
210
234
|
specification_version: 4
|
211
235
|
summary: Parses a CSV reading log.
|
212
236
|
test_files: []
|
@@ -1,91 +0,0 @@
|
|
1
|
-
require 'pastel'
|
2
|
-
|
3
|
-
module Reading
|
4
|
-
module Stats
|
5
|
-
module ResultFormatters
|
6
|
-
TERMINAL = {
|
7
|
-
average_length: ->(result) { length_to_s(result) },
|
8
|
-
average_amount: ->(result) { length_to_s(result) },
|
9
|
-
:'average_daily-amount' => ->(result) { "#{length_to_s(result)} per day" },
|
10
|
-
total_item: ->(result) {
|
11
|
-
if result.zero?
|
12
|
-
PASTEL.bright_black("none")
|
13
|
-
else
|
14
|
-
color("#{result} #{result == 1 ? "item" : "items"}")
|
15
|
-
end
|
16
|
-
},
|
17
|
-
total_amount: ->(result) { length_to_s(result) },
|
18
|
-
top_length: ->(result) { top_or_bottom_lengths(result) },
|
19
|
-
top_amount: ->(result) { top_or_bottom_lengths(result) },
|
20
|
-
top_speed: ->(result) { top_or_bottom_speeds(result) },
|
21
|
-
bottom_length: ->(result) { top_or_bottom_lengths(result) },
|
22
|
-
botom_amount: ->(result) { top_or_bottom_lengths(result) },
|
23
|
-
bottom_speed: ->(result) { top_or_bottom_speeds(result) },
|
24
|
-
}
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
PASTEL = Pastel.new
|
29
|
-
|
30
|
-
# Applies a terminal color.
|
31
|
-
# @param string [String]
|
32
|
-
# @return [String]
|
33
|
-
private_class_method def self.color(string)
|
34
|
-
PASTEL.bright_blue(string)
|
35
|
-
end
|
36
|
-
|
37
|
-
# Converts a length/amount (pages or time) into a string.
|
38
|
-
# @param length [Numeric, Reading::Item::TimeLength]
|
39
|
-
# @param color [Boolean] whether a terminal color should be applied.
|
40
|
-
# @return [String]
|
41
|
-
private_class_method def self.length_to_s(length, color: true)
|
42
|
-
if length.is_a?(Numeric)
|
43
|
-
length_string = "#{length.round} pages"
|
44
|
-
else
|
45
|
-
length_string = length.to_s
|
46
|
-
end
|
47
|
-
|
48
|
-
color ? color(length_string) : length_string
|
49
|
-
end
|
50
|
-
|
51
|
-
# Formats a list of top/bottom length results as a string.
|
52
|
-
# @param result [Array]
|
53
|
-
# @return [String]
|
54
|
-
private_class_method def self.top_or_bottom_lengths(result)
|
55
|
-
offset = result.count.digits.count
|
56
|
-
|
57
|
-
result
|
58
|
-
.map.with_index { |(title, length), index|
|
59
|
-
pad = ' ' * (offset - (index + 1).digits.count)
|
60
|
-
|
61
|
-
title_line = "#{index + 1}. #{pad}#{title}"
|
62
|
-
indent = " #{' ' * offset}"
|
63
|
-
|
64
|
-
"#{title_line}\n#{indent}#{length_to_s(length)}"
|
65
|
-
}
|
66
|
-
.join("\n")
|
67
|
-
end
|
68
|
-
|
69
|
-
# Formats a list of top/bottom speed results as a string.
|
70
|
-
# @param result [Array]
|
71
|
-
# @return [String]
|
72
|
-
private_class_method def self.top_or_bottom_speeds(result)
|
73
|
-
offset = result.count.digits.count
|
74
|
-
|
75
|
-
result
|
76
|
-
.map.with_index { |(title, hash), index|
|
77
|
-
amount = length_to_s(hash[:amount], color: false)
|
78
|
-
days = "#{hash[:days]} #{hash[:days] == 1 ? "day" : "days"}"
|
79
|
-
pad = ' ' * (offset - (index + 1).digits.count)
|
80
|
-
|
81
|
-
title_line = "#{index + 1}. #{pad}#{title}"
|
82
|
-
indent = " #{' ' * offset}"
|
83
|
-
colored_speed = color("#{amount} in #{days}")
|
84
|
-
|
85
|
-
"#{title_line}\n#{indent}#{colored_speed}"
|
86
|
-
}
|
87
|
-
.join("\n")
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|