reading 1.0.2 → 1.1.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 +77 -68
- data/lib/reading/item/time_length.rb +9 -3
- data/lib/reading/stats/filter.rb +1 -1
- data/lib/reading/stats/grouping.rb +12 -7
- data/lib/reading/stats/result_formatters.rb +16 -47
- data/lib/reading/util/numeric_to_i_if_whole.rb +5 -1
- data/lib/reading/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2895207e57d29e0056fac8eca5ddecf39853916bb3691df1c6adb2d657d9c709
|
4
|
+
data.tar.gz: 115aa06d704b41f03aeaf8b39fcfcc2bc4dc3f3ee49c232e644903a5b098bd8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 865dd1537a65a89e253f8b98ad82e6e15e3f21bd76467db5c06297575004ecc9f5a63cf766c339e61b143c478c1a2e7a6873bd370a57ef609469b070e243214f
|
7
|
+
data.tar.gz: 4ca3fe68262114c9791973c9b843fe2c33c6f96d99f0e2566f58ac5ca97c3196a772f84eaebc2db5665010ef11aa10d500f430a4496ab7b204cc8554f8826312
|
data/bin/reading
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
#
|
4
|
-
# If a
|
3
|
+
# Runs a reading statistics query, if a file path arg (CSV or TXT)is given.
|
4
|
+
# If a non-path string is given, then displays parsed output (item Hashes).
|
5
5
|
#
|
6
6
|
# Usage:
|
7
7
|
# Run on the command line:
|
8
|
-
# reading "<
|
8
|
+
# reading "<file path or entry string>" "<query, if file path>" "<optional comma-separated names of enabled columns>"
|
9
9
|
#
|
10
10
|
# Examples:
|
11
|
-
# reading /home/felipe/reading.csv
|
12
|
-
# reading /home/felipe/reading.csv 'head, sources'
|
11
|
+
# reading /home/felipe/reading.csv 'amount by year'
|
12
|
+
# reading /home/felipe/reading.csv 'amount by year' 'head, sources'
|
13
13
|
# reading '3|📕Trying|Little Library 1970147288'
|
14
14
|
# reading '📕Trying|Little Library 1970147288' 'head, sources'
|
15
15
|
|
@@ -17,29 +17,15 @@ require_relative "../lib/reading"
|
|
17
17
|
require_relative "../lib/reading/stats/result_formatters"
|
18
18
|
require "debug"
|
19
19
|
require "amazing_print"
|
20
|
-
require "readline"
|
21
20
|
require "pastel"
|
22
21
|
|
23
|
-
EXIT_COMMANDS = %w[exit e quit q]
|
24
22
|
PASTEL = Pastel.new
|
25
|
-
GROUP_HEADING_FORMATTERS = [
|
26
|
-
-> { PASTEL.magenta.bold.underline(_1) },
|
27
|
-
-> { PASTEL.green.bold.underline(_1) },
|
28
|
-
-> { PASTEL.yellow.bold.underline(_1) },
|
29
|
-
-> { PASTEL.cyan.bold.underline(_1) },
|
30
|
-
-> { PASTEL.magenta.on_white(_1) },
|
31
|
-
-> { PASTEL.green.on_white(_1) },
|
32
|
-
-> { PASTEL.yellow.on_white(_1) },
|
33
|
-
]
|
34
23
|
|
35
24
|
# Recursively prints a hash of results (possibly grouped).
|
36
25
|
# @param grouped_results [Hash, Array]
|
37
|
-
|
38
|
-
def print_grouped_results(grouped_results, group_heading_formatters)
|
39
|
-
indent_level = GROUP_HEADING_FORMATTERS.count - group_heading_formatters.count
|
40
|
-
|
26
|
+
def print_grouped_results(grouped_results)
|
41
27
|
if grouped_results.nil? || (grouped_results.respond_to?(:empty?) && grouped_results.empty?)
|
42
|
-
puts "
|
28
|
+
puts "none\n"
|
43
29
|
return
|
44
30
|
end
|
45
31
|
|
@@ -47,70 +33,93 @@ def print_grouped_results(grouped_results, group_heading_formatters)
|
|
47
33
|
(grouped_results.is_a?(Array) && grouped_results.first.length == 2)
|
48
34
|
|
49
35
|
grouped_results.each do |group_name, grouped|
|
50
|
-
|
51
|
-
|
36
|
+
group_name = group_name.nil? ? "(none)" : group_name
|
37
|
+
puts "# #{group_name}"
|
38
|
+
print_grouped_results(grouped)
|
52
39
|
end
|
53
40
|
elsif grouped_results.is_a?(Array)
|
54
|
-
|
55
|
-
|
56
|
-
puts " " * indent_level + numbered_results.join("\n" + " " * indent_level) + "\n"
|
41
|
+
puts grouped_results.join("\n") + "\n\n"
|
57
42
|
else
|
58
|
-
puts
|
43
|
+
puts grouped_results.to_s + "\n\n"
|
59
44
|
end
|
60
45
|
end
|
61
46
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
if ARGV[1]
|
71
|
-
enabled_columns = ARGV[1].split(",").map(&:strip).map(&:to_sym)
|
72
|
-
Reading::Config.build(enabled_columns:)
|
47
|
+
# Builds a Reading::Config from a comma-separated string of column names.
|
48
|
+
# @param enabled_columns_str [String, nil]
|
49
|
+
def build_config(enabled_columns_str:)
|
50
|
+
if enabled_columns_str
|
51
|
+
enabled_columns = enabled_columns_str.split(",").map(&:strip).map(&:to_sym)
|
52
|
+
Reading::Config.build(enabled_columns:)
|
53
|
+
end
|
73
54
|
end
|
74
55
|
|
75
|
-
|
56
|
+
# Runs a stats query on a file at the given path, and prints the results.
|
57
|
+
# @param file_path [String] path to a CSV or TXT file.
|
58
|
+
# @param query [String] the stats query to run.
|
59
|
+
# @param enabled_columns_str [String, nil] comma-separated string of enabled column names.
|
60
|
+
def run_query(file_path:, query:, enabled_columns_str:)
|
61
|
+
return if query.blank?
|
76
62
|
|
77
|
-
|
63
|
+
build_config(enabled_columns_str: ARGV[2])
|
78
64
|
error_handler = ->(e) { puts "Skipped a row due to a parsing error: #{e}" }
|
79
65
|
|
80
|
-
items = Reading.parse(path:
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
result_formatters: Reading::Stats::ResultFormatters::TERMINAL,
|
94
|
-
)
|
95
|
-
|
96
|
-
if results.is_a?(Array) && results.first.is_a?(Reading::Item) # `debug` operation
|
97
|
-
r = results
|
98
|
-
puts PASTEL.red.bold("Enter 'c' to leave the debugger.")
|
99
|
-
debugger
|
100
|
-
else
|
101
|
-
print_grouped_results(results, GROUP_HEADING_FORMATTERS)
|
102
|
-
end
|
103
|
-
rescue Reading::Error => e
|
104
|
-
puts e
|
66
|
+
items = Reading.parse(path: file_path, item_view: false, error_handler:)
|
67
|
+
results = Reading.stats(
|
68
|
+
input: query,
|
69
|
+
items:,
|
70
|
+
result_formatters: Reading::Stats::ResultFormatters::TERMINAL,
|
71
|
+
)
|
72
|
+
|
73
|
+
if results.is_a?(Array) && results.first.is_a?(Reading::Item) # `debug` operation
|
74
|
+
r = results
|
75
|
+
puts PASTEL.red.bold("Enter 'c' to leave the debugger.")
|
76
|
+
debugger
|
77
|
+
else
|
78
|
+
print_grouped_results(results)
|
105
79
|
end
|
106
|
-
|
107
|
-
|
80
|
+
rescue Reading::Error => e
|
81
|
+
puts e
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parses a string of one or more reading log entries, and prints the resulting item Hashes.
|
85
|
+
# @param entry [String] one or more reading log entries, separated by newlines.
|
86
|
+
# @param enabled_columns_str [String, nil] comma-separated string of enabled column names.
|
87
|
+
def print_parsed(entry:, enabled_columns_str:)
|
88
|
+
build_config(enabled_columns_str:)
|
89
|
+
entry = entry.gsub("\\|", "|") # because some pipes are escaped when pasting into the terminal
|
108
90
|
|
109
91
|
begin
|
110
|
-
item_hashes = Reading.parse(lines:
|
92
|
+
item_hashes = Reading.parse(lines: entry, hash_output: true, item_view: false)
|
111
93
|
rescue Reading::Error => e
|
112
94
|
puts "Skipped a row due to a parsing error: #{e}"
|
113
95
|
end
|
114
96
|
|
115
97
|
ap item_hashes
|
116
98
|
end
|
99
|
+
|
100
|
+
#################################################
|
101
|
+
# Script execution starts here.
|
102
|
+
#################################################
|
103
|
+
|
104
|
+
input = ARGV[0]
|
105
|
+
unless input
|
106
|
+
raise ArgumentError,
|
107
|
+
"First argument required, either a CSV/TXT file path or a reading log entry string.\nExamples:\n" \
|
108
|
+
" reading /home/felipe/reading.csv\n" \
|
109
|
+
" reading '3|📕Trying|Little Library 1970147288'"
|
110
|
+
end
|
111
|
+
|
112
|
+
input_is_file_path = input.end_with?(".csv") || input.end_with?(".txt")
|
113
|
+
|
114
|
+
if input_is_file_path
|
115
|
+
query = ARGV[1]
|
116
|
+
unless query
|
117
|
+
raise ArgumentError,
|
118
|
+
"Second argument required, the stats query to run.\nExample:\n" \
|
119
|
+
" reading /home/felipe/reading.csv 'amount by year'" \
|
120
|
+
end
|
121
|
+
|
122
|
+
run_query(file_path: input, query:, enabled_columns_str: ARGV[2])
|
123
|
+
else
|
124
|
+
print_parsed(entry: input, enabled_columns_str: ARGV[1])
|
125
|
+
end
|
@@ -74,7 +74,9 @@ module Reading
|
|
74
74
|
# Converts @value to an Integer if it's a whole number, and returns self.
|
75
75
|
# @return [TimeLength]
|
76
76
|
def to_i_if_whole!
|
77
|
-
if @value.
|
77
|
+
if @value.is_a?(Float) && @value.nan?
|
78
|
+
@value = 0
|
79
|
+
elsif @value.to_i == @value
|
78
80
|
@value = @value.to_i
|
79
81
|
end
|
80
82
|
|
@@ -85,9 +87,13 @@ module Reading
|
|
85
87
|
# refinement Numeric#to_i_if_whole.
|
86
88
|
# @return [TimeLength]
|
87
89
|
def to_i_if_whole
|
88
|
-
|
90
|
+
if @value.is_a?(Float) && @value.nan?
|
91
|
+
return self.class.new(0)
|
92
|
+
else
|
93
|
+
return self if @value.is_a?(Integer) || @value.to_i != @value
|
89
94
|
|
90
|
-
|
95
|
+
self.class.new(@value.to_i)
|
96
|
+
end
|
91
97
|
end
|
92
98
|
|
93
99
|
# @param other [TimeLength, Numeric]
|
data/lib/reading/stats/filter.rb
CHANGED
@@ -372,7 +372,7 @@ module Reading
|
|
372
372
|
match = value.match(DATES_REGEX) ||
|
373
373
|
(raise InputError,
|
374
374
|
"End date must be in yyyy/[mm] format, or a date range " \
|
375
|
-
"(yyyy/[mm]-
|
375
|
+
"(yyyy/[mm]-yyyy/[mm])")
|
376
376
|
|
377
377
|
start_date = Date.new(
|
378
378
|
match[:start_year].to_i,
|
@@ -10,8 +10,6 @@ module Reading
|
|
10
10
|
# @param items [Array<Item>] the Items on which to run the operation.
|
11
11
|
# @return [Hash] the return value of the group action(s).
|
12
12
|
def self.group(input, items)
|
13
|
-
grouped_items = {}
|
14
|
-
|
15
13
|
match = input.match(REGEX)
|
16
14
|
|
17
15
|
if match
|
@@ -80,7 +78,7 @@ module Reading
|
|
80
78
|
end
|
81
79
|
end
|
82
80
|
|
83
|
-
groups.
|
81
|
+
groups.sort_by { |key, _items| key }.to_h
|
84
82
|
},
|
85
83
|
source: proc { |items|
|
86
84
|
groups = Hash.new { |h, k| h[k] = [] }
|
@@ -103,7 +101,7 @@ module Reading
|
|
103
101
|
end
|
104
102
|
end
|
105
103
|
|
106
|
-
groups.
|
104
|
+
groups.sort_by { |key, _items| key }.to_h
|
107
105
|
},
|
108
106
|
year: proc { |items|
|
109
107
|
begin_date = items
|
@@ -187,7 +185,14 @@ module Reading
|
|
187
185
|
end
|
188
186
|
|
189
187
|
groups.transform_keys! { |month_range|
|
190
|
-
[month_range.begin.year, month_range.begin.month]
|
188
|
+
array = [month_range.begin.year, month_range.begin.month]
|
189
|
+
|
190
|
+
array.define_singleton_method(:to_s) do
|
191
|
+
map { it.to_s.rjust(2, "0") }
|
192
|
+
.join("/")
|
193
|
+
end
|
194
|
+
|
195
|
+
array
|
191
196
|
}
|
192
197
|
|
193
198
|
groups
|
@@ -200,7 +205,7 @@ module Reading
|
|
200
205
|
item.genres.each { |genre| groups[genre] << item }
|
201
206
|
end
|
202
207
|
|
203
|
-
groups.
|
208
|
+
groups.sort_by { |key, _items| key }.to_h
|
204
209
|
},
|
205
210
|
genre: proc { |items|
|
206
211
|
groups = Hash.new { |h, k| h[k] = [] }
|
@@ -212,7 +217,7 @@ module Reading
|
|
212
217
|
end
|
213
218
|
end
|
214
219
|
|
215
|
-
groups.
|
220
|
+
groups.sort_by { |key, _items| key }.to_h
|
216
221
|
},
|
217
222
|
length: proc { |items|
|
218
223
|
boundaries = Config.hash.fetch(:length_group_boundaries)
|
@@ -20,9 +20,9 @@ module Reading
|
|
20
20
|
:"average_daily-amount" => ->(result) { "#{length_to_s(result)} per day" },
|
21
21
|
total_item: ->(result) {
|
22
22
|
if result.zero?
|
23
|
-
|
23
|
+
"none"
|
24
24
|
else
|
25
|
-
|
25
|
+
"#{result} #{result == 1 ? "item" : "items"}"
|
26
26
|
end
|
27
27
|
},
|
28
28
|
total_amount: ->(result) { length_to_s(result) },
|
@@ -40,44 +40,25 @@ module Reading
|
|
40
40
|
|
41
41
|
private
|
42
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
43
|
# Converts a length/amount (pages or time) into a string.
|
53
44
|
# @param length [Numeric, Reading::Item::TimeLength]
|
54
|
-
# @param color [Boolean] whether a terminal color should be applied.
|
55
45
|
# @return [String]
|
56
|
-
private_class_method def self.length_to_s(length
|
57
|
-
if length.
|
58
|
-
|
46
|
+
private_class_method def self.length_to_s(length)
|
47
|
+
if length.nil? || length.zero?
|
48
|
+
"none"
|
49
|
+
elsif length.is_a?(Numeric)
|
50
|
+
"#{length.round} pages"
|
59
51
|
else
|
60
|
-
|
52
|
+
length.to_s
|
61
53
|
end
|
62
|
-
|
63
|
-
color ? color(length_string) : length_string
|
64
54
|
end
|
65
55
|
|
66
56
|
# Formats a list of top/bottom length results as a string.
|
67
57
|
# @param result [Array]
|
68
58
|
# @return [String]
|
69
59
|
private_class_method def self.top_or_bottom_lengths_string(result)
|
70
|
-
offset = result.count.digits.count
|
71
|
-
|
72
60
|
result
|
73
|
-
.map
|
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
|
-
}
|
61
|
+
.map { |title, length| "#{title}\n #{length_to_s(length)}" }
|
81
62
|
.join("\n")
|
82
63
|
end
|
83
64
|
|
@@ -85,36 +66,24 @@ module Reading
|
|
85
66
|
# @param result [Array]
|
86
67
|
# @return [String]
|
87
68
|
private_class_method def self.top_or_bottom_speeds_string(result)
|
88
|
-
offset = result.count.digits.count
|
89
|
-
|
90
69
|
result
|
91
|
-
.map
|
92
|
-
amount = length_to_s(hash[:amount]
|
70
|
+
.map { |title, hash|
|
71
|
+
amount = length_to_s(hash[:amount])
|
93
72
|
days = "#{hash[:days]} #{hash[:days] == 1 ? "day" : "days"}"
|
94
|
-
|
73
|
+
speed = "#{amount} in #{days}"
|
95
74
|
|
96
|
-
|
97
|
-
indent = " #{" " * offset}"
|
98
|
-
colored_speed = color("#{amount} in #{days}")
|
99
|
-
|
100
|
-
"#{title_line}\n#{indent}#{colored_speed}"
|
75
|
+
"#{title}\n #{speed}"
|
101
76
|
}
|
102
77
|
.join("\n")
|
103
78
|
end
|
104
79
|
|
105
80
|
# Formats a list of top/bottom number results as a string.
|
106
81
|
private_class_method def self.top_or_bottom_numbers_string(result, noun:)
|
107
|
-
offset = result.count.digits.count
|
108
|
-
|
109
82
|
result
|
110
|
-
.map
|
111
|
-
|
112
|
-
|
113
|
-
title_line = "#{index + 1}. #{pad}#{title}"
|
114
|
-
indent = " #{" " * offset}"
|
115
|
-
number_string = color("#{number} #{number == 1 ? noun : "#{noun}s"}")
|
83
|
+
.map { |title, number|
|
84
|
+
number_string = "#{number} #{number == 1 ? noun : "#{noun}s"}"
|
116
85
|
|
117
|
-
"#{
|
86
|
+
"#{title}\n #{number_string}"
|
118
87
|
}
|
119
88
|
.join("\n")
|
120
89
|
end
|
data/lib/reading/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reading
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felipe Vogel
|
@@ -230,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
230
|
- !ruby/object:Gem::Version
|
231
231
|
version: '0'
|
232
232
|
requirements: []
|
233
|
-
rubygems_version: 3.
|
233
|
+
rubygems_version: 3.7.1
|
234
234
|
specification_version: 4
|
235
235
|
summary: Parses a CSV reading log.
|
236
236
|
test_files: []
|