reading 1.0.2 → 1.1.1

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: 731cc9d7bf42123b5627cc5bd498c317cf1f60faed19d75010bf01d360945cf3
4
- data.tar.gz: 6e74543ddfd36e7a3221b7c668797106d1082805a62be57aade5c32266a45b27
3
+ metadata.gz: 4bf09fbcc2e77924a371eade404c83ad4fd5c3d7576eac45d0c1236c3db479f6
4
+ data.tar.gz: f854509f8ae88553df964428d18098fceeb3c885093728c2f54bba530199c8cb
5
5
  SHA512:
6
- metadata.gz: a6ee081473b294836ac9ad2d787283460de4563b3725548cae3447001a1f6d29e3435bc13fe1185fb84a167942c7fa3c040c5610685d389f7a541b6c5a739e42
7
- data.tar.gz: 6b67433e096a8111bf15582fdc3b409beef982636446ec71f2ca0916b7366253087c38877bb8ffc7dbf25e2cdaf2acc3075388412072ca6c88c4b2a633558e62
6
+ metadata.gz: a285b194b566697bd586628d1266ed5582e1cfab48dc3023e243635e4b21b3f31f100cf4b41a1777358b70175c15a8b2d57d7589859697c917e1914f452e492e
7
+ data.tar.gz: 6aa82f83175a22cd652b545aa46935ba5ef72009d33b8bbcd35a6667e1c2081cbe708fce40703503027aaf5938c5020a475fea9575d6e5d45fe75bef49233b4e
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
@@ -2,7 +2,7 @@ module Reading
2
2
  class Item
3
3
  # The length of an item when it is a time, as opposed to pages. (Pages are
4
4
  # represented simply with an Integer or Float.)
5
- class Item::TimeLength
5
+ class TimeLength
6
6
  include Comparable
7
7
 
8
8
  attr_reader :value # in total minutes
@@ -12,17 +12,17 @@ module Reading
12
12
  @value = value
13
13
  end
14
14
 
15
- # Builds an Item::TimeLength from a string.
15
+ # Builds a TimeLength from a string.
16
16
  # @param string [String] a time duration in "h:mm" format.
17
17
  # @return [TimeLength, nil]
18
18
  def self.parse(string)
19
- return nil unless string.match? /\A\d+:\d\d\z/
19
+ return nil unless string.match?(/\A\d+:\d\d\z/)
20
20
 
21
21
  hours, minutes = string.split(":").map(&:to_i)
22
22
  new((hours * 60) + minutes)
23
23
  end
24
24
 
25
- # Builds an Item::TimeLength based on a page count.
25
+ # Builds a TimeLength based on a page count.
26
26
  # @param pages [Integer, Float]
27
27
  # @return [TimeLength]
28
28
  def self.from_pages(pages)
@@ -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,32 +87,36 @@ 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]
94
100
  # @return [TimeLength]
95
101
  def +(other)
96
- if other.is_a? Item::TimeLength
102
+ if other.is_a? TimeLength
97
103
  self.class.new(value + other.value)
98
104
  elsif other.is_a? Numeric
99
105
  self.class.new(value + self.class.pages_to_minutes(other))
100
106
  else
101
- raise TypeError, "#{other.class} can't be added to Item::TimeLength."
107
+ raise TypeError, "#{other.class} can't be added to TimeLength."
102
108
  end
103
109
  end
104
110
 
105
111
  # @param other [TimeLength, Numeric]
106
112
  # @return [TimeLength]
107
113
  def -(other)
108
- if other.is_a? Item::TimeLength
114
+ if other.is_a? TimeLength
109
115
  self.class.new(value - other.value)
110
116
  elsif other.is_a? Numeric
111
117
  self.class.new(value - self.class.pages_to_minutes(other))
112
118
  else
113
- raise TypeError, "#{other.class} can't be subtracted from Item::TimeLength."
119
+ raise TypeError, "#{other.class} can't be subtracted from TimeLength."
114
120
  end
115
121
  end
116
122
 
@@ -134,6 +140,17 @@ module Reading
134
140
  end
135
141
  end
136
142
 
143
+ # For relativizing progress to a percentage of amount.
144
+ # @param other [TimeLength, Numeric]
145
+ # @return [TimeLength]
146
+ def percentage_of(other)
147
+ if other.is_a? TimeLength
148
+ value.to_f / other.value
149
+ else
150
+ raise TypeError, "TimeLength can't be percent-divided by #{other.class}."
151
+ end
152
+ end
153
+
137
154
  # See https://web.archive.org/web/20221206095821/https://www.mutuallyhuman.com/blog/class-coercion-in-ruby/
138
155
  # @param other [Numeric]
139
156
  def coerce(other)
@@ -153,7 +170,7 @@ module Reading
153
170
  other = self.class.from_pages(other)
154
171
  end
155
172
 
156
- unless other.is_a? Item::TimeLength
173
+ unless other.is_a? TimeLength
157
174
  raise TypeError, "TimeLength can't be compared to #{other.class} #{other}."
158
175
  end
159
176
 
@@ -90,14 +90,28 @@ module Reading
90
90
  Attributes::Shared.length(parsed_row[:length], format:)
91
91
  no_end_date = !dates.end if dates &&
92
92
  Config.hash.fetch(:enabled_columns).include?(:end_dates)
93
+ progress = Attributes::Shared.progress(start_entry, no_end_date:) ||
94
+ Attributes::Shared.progress(parsed_row[:head][head_index]) ||
95
+ (1.0 if end_entry)
96
+ amount =
97
+ if dates && length
98
+ length
99
+ elsif !progress.is_a?(Float)
100
+ progress
101
+ end
102
+
103
+ # Change progress from absolute to relative (percentage) if amount is given.
104
+ if amount && progress && !progress.is_a?(Float)
105
+ amount_time = amount.is_a?(Item::TimeLength) ? amount : Item::TimeLength.from_pages(amount)
106
+ progress_time = progress.is_a?(Item::TimeLength) ? progress : Item::TimeLength.from_pages(progress)
107
+ progress = progress_time.percentage_of(amount_time)
108
+ end
93
109
 
94
110
  [
95
111
  {
96
- dates: dates,
97
- amount: (length if dates),
98
- progress: Attributes::Shared.progress(start_entry, no_end_date:) ||
99
- Attributes::Shared.progress(parsed_row[:head][head_index]) ||
100
- (1.0 if end_entry),
112
+ dates:,
113
+ amount:,
114
+ progress:,
101
115
  name: span_template.fetch(:name),
102
116
  favorite?: span_template.fetch(:favorite?),
103
117
  }.map { |k, v| [k, v || span_template.fetch(k)] }.to_h
@@ -212,15 +212,22 @@ module Reading
212
212
  # is when tracking fixed-length items such as books. See
213
213
  # https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#history-pages-and-stopping-points-books
214
214
  if !amount && progress
215
- if progress.is_a? Float
215
+ if progress.is_a?(Float)
216
216
  total_length = Attributes::Shared.length(parsed_row[:length], format:)
217
- amount = total_length * progress
217
+ amount = total_length * progress if total_length
218
218
  else
219
219
  amount = progress
220
220
  end
221
221
  amount_from_progress = true
222
222
  end
223
223
 
224
+ # Change progress from absolute to relative (percentage) if amount is given.
225
+ if amount && progress && !progress.is_a?(Float)
226
+ amount_time = amount.is_a?(Item::TimeLength) ? amount : Item::TimeLength.from_pages(amount)
227
+ progress_time = progress.is_a?(Item::TimeLength) ? progress : Item::TimeLength.from_pages(progress)
228
+ progress = progress_time.percentage_of(amount_time)
229
+ end
230
+
224
231
  repetitions = entry[:repetitions]&.to_i
225
232
  frequency = entry[:frequency]
226
233
 
@@ -16,7 +16,7 @@ module Reading
16
16
  hash[:progress_percent]&.to_f&./(100) ||
17
17
  hash[:progress_pages]&.to_i ||
18
18
  hash[:progress_time]&.then { Item::TimeLength.parse(_1) } ||
19
- (0 if hash[:progress_dnf]) ||
19
+ (0.0 if hash[:progress_dnf]) ||
20
20
  (1.0 if hash[:progress_done]) ||
21
21
  (0.0 if no_end_date) ||
22
22
  nil
@@ -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.2"
2
+ VERSION = "1.1.1"
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.2
4
+ version: 1.1.1
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.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: []