reading 1.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38ec7e1054c383eb44cbd6389c9fdfdc6b17fef88f7ec22f49c4f1602ed5995e
4
- data.tar.gz: c683caba0f5b21b299e09d7b318cb957cef16394ba63fdf872bf2cc332197036
3
+ metadata.gz: 2895207e57d29e0056fac8eca5ddecf39853916bb3691df1c6adb2d657d9c709
4
+ data.tar.gz: 115aa06d704b41f03aeaf8b39fcfcc2bc4dc3f3ee49c232e644903a5b098bd8c
5
5
  SHA512:
6
- metadata.gz: f5d485ddba475e9c12cd2956770b3bafafc3221e6d8e888ca67da2802a11229d2056bd6deb942ce8645db628f8c5487c24531d5301856c165e4859bec0e43beb
7
- data.tar.gz: e08f9707ea8a903a68d3b17c6ef58f87acaee1b9431e841ddc7cc27199df6c3d0d1ae5f7cff5eb453a3ac9ab9d516e6e4730994377ad2544ace872caa3b996ac
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
- # Starts the reading statistics interactive CLI, if a CSV file path arg is given.
4
- # If a CSV string is given instead, then parsing output (item hashes) is displayed.
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 "<CSV file path or string>" "<optional comma-separated names of enabled columns>"
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
- # @param group_heading_formatters [Array<Proc>] a subset of GROUP_HEADING_FORMATTERS
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 " " * indent_level + PASTEL.bright_black("none") + "\n"
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
- puts " " * indent_level + group_heading_formatters.first.call(group_name)
51
- print_grouped_results(grouped, group_heading_formatters[1..])
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
- numbered_results = grouped_results.map.with_index { |v, i| "#{i + 1}. #{v}" }
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 " " * indent_level + grouped_results.to_s + "\n"
43
+ puts grouped_results.to_s + "\n\n"
59
44
  end
60
45
  end
61
46
 
62
- input = ARGV[0]
63
- unless input
64
- raise ArgumentError,
65
- "Argument required, either a CSV file path or a CSV string.\nExamples:\n" \
66
- "reading /home/felipe/reading.csv\n" \
67
- "reading '3|📕Trying|Little Library 1970147288'"
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
- input_is_csv_path = input.end_with?(".csv") || input.end_with?(".txt")
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
- if input_is_csv_path
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: input, item_view: false, error_handler:)
81
-
82
- loop do
83
- raw_input = Readline.readline(PASTEL.bright_cyan("> "), true)
84
-
85
- exit if EXIT_COMMANDS.include?(raw_input)
86
-
87
- input = raw_input.presence
88
- next if raw_input.blank?
89
-
90
- results = Reading.stats(
91
- input:,
92
- items:,
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
- else # CSV string arg
107
- input = input.gsub("\\|", "|") # because some pipes are escaped when pasting into the terminal
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: input, hash_output: true, item_view: false)
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.to_i == @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
- return self if @value.is_a?(Integer) || @value.to_i != @value
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
- self.class.new(@value.to_i)
95
+ self.class.new(@value.to_i)
96
+ end
91
97
  end
92
98
 
93
99
  # @param other [TimeLength, Numeric]
@@ -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]-[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.sort.to_h
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.sort.to_h
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.sort.to_h
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.sort.to_h
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
- PASTEL.bright_black("none")
23
+ "none"
24
24
  else
25
- color("#{result} #{result == 1 ? "item" : "items"}")
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, color: true)
57
- if length.is_a?(Numeric)
58
- length_string = "#{length.round} pages"
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
- length_string = length.to_s
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.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
- }
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.with_index { |(title, hash), index|
92
- amount = length_to_s(hash[:amount], color: false)
70
+ .map { |title, hash|
71
+ amount = length_to_s(hash[:amount])
93
72
  days = "#{hash[:days]} #{hash[:days] == 1 ? "day" : "days"}"
94
- pad = " " * (offset - (index + 1).digits.count)
73
+ speed = "#{amount} in #{days}"
95
74
 
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}"
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.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"}")
83
+ .map { |title, number|
84
+ number_string = "#{number} #{number == 1 ? noun : "#{noun}s"}"
116
85
 
117
- "#{title_line}\n#{indent}#{number_string}"
86
+ "#{title}\n #{number_string}"
118
87
  }
119
88
  .join("\n")
120
89
  end
@@ -4,7 +4,11 @@ module Reading
4
4
  module NumericToIIfWhole
5
5
  refine Numeric do
6
6
  def to_i_if_whole
7
- to_i == self ? to_i : self
7
+ if is_a?(Float) && nan?
8
+ 0
9
+ else
10
+ to_i == self ? to_i : self
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,3 +1,3 @@
1
1
  module Reading
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
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.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Vogel
@@ -221,16 +221,16 @@ require_paths:
221
221
  - lib
222
222
  required_ruby_version: !ruby/object:Gem::Requirement
223
223
  requirements:
224
- - - "~>"
224
+ - - ">="
225
225
  - !ruby/object:Gem::Version
226
- version: 3.4.4
226
+ version: '3.2'
227
227
  required_rubygems_version: !ruby/object:Gem::Requirement
228
228
  requirements:
229
229
  - - ">="
230
230
  - !ruby/object:Gem::Version
231
231
  version: '0'
232
232
  requirements: []
233
- rubygems_version: 3.6.7
233
+ rubygems_version: 3.7.1
234
234
  specification_version: 4
235
235
  summary: Parses a CSV reading log.
236
236
  test_files: []