reading 0.9.0 → 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.
@@ -1,6 +1,6 @@
1
- require 'date'
2
- require_relative 'experiences/history_transformer'
3
- require_relative 'experiences/dates_and_head_transformer'
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
@@ -9,13 +9,16 @@ module Reading
9
9
  # Extracts the :progress sub-attribute (percent, pages, or time) from
10
10
  # the given hash.
11
11
  # @param hash [Hash] any parsed hash that contains progress.
12
+ # @param no_end_date [Boolean] for start and end dates (as opposed to
13
+ # the History column), whether an end date is present.
12
14
  # @return [Float, Integer, Item::TimeLength]
13
- def self.progress(hash)
15
+ def self.progress(hash, no_end_date: nil)
14
16
  hash[:progress_percent]&.to_f&./(100) ||
15
17
  hash[:progress_pages]&.to_i ||
16
18
  hash[:progress_time]&.then { Item::TimeLength.parse(_1) } ||
17
19
  (0 if hash[:progress_dnf]) ||
18
20
  (1.0 if hash[:progress_done]) ||
21
+ (0.0 if no_end_date) ||
19
22
  nil
20
23
  end
21
24
 
@@ -1,7 +1,7 @@
1
- require 'pastel'
2
- require_relative '../item'
3
- require_relative 'parser'
4
- require_relative 'transformer'
1
+ require "pastel"
2
+ require_relative "../item"
3
+ require_relative "parser"
4
+ require_relative "transformer"
5
5
 
6
6
  module Reading
7
7
  module Parsing
@@ -1,8 +1,8 @@
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'
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 'column'
2
- require_relative 'compact_planned_columns/head'
3
- require_relative 'regular_columns/sources'
4
- require_relative 'regular_columns/length'
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 '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'
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
@@ -91,7 +91,7 @@ module Reading
91
91
 
92
92
  private
93
93
 
94
- ISBN_REGEX = /(\d{3}[-\s]?)?\d{10}/
94
+ ISBN_REGEX = /(\d{3}[-\s]?)?(\d{10}|\d{9}X)/
95
95
  ASIN_REGEX = /B0[A-Z\d]{8}/
96
96
  end
97
97
  end
@@ -16,7 +16,11 @@ module Reading
16
16
  (\s+|\z)
17
17
  )?
18
18
  (
19
- (?<date>\d{4}/\d\d?/\d\d?)
19
+ (
20
+ (?<date>\d{4}/\d\d?/\d\d?)
21
+ |
22
+ (?<planned>\?\?)
23
+ )
20
24
  (\s+|\z)
21
25
  )?
22
26
  (
@@ -1,12 +1,12 @@
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'
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
@@ -82,7 +82,7 @@ module Reading
82
82
  end
83
83
  }
84
84
 
85
- positive_operator = 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
- done: proc { |values, operator, items|
105
- done_progresses = values.map { |value|
106
- (value.match(/\A(\d+)?%/).captures.first.to_f.clamp(0.0, 100.0) / 100) ||
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
- item_done_progresses = item.experiences.map { |experience|
115
- experience.spans.last.progress if experience.status == :done
116
+ if operator == :"!="
117
+ item_progresses = item.experiences.map { |experience|
118
+ experience.spans.last.progress || 0.0
116
119
  }
117
120
 
118
- next if (item_done_progresses - done_progresses).empty?
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
- done_progresses.any? { |done_progress|
124
- experience.status == :done &&
125
- experience.spans.last.progress.send(operator, done_progress)
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 ]/, '').gsub(/\s/, '') if _1 }
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/, '').gsub(/\s/, '') }
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 == :'!=' && (item_series_names - series_names).empty?
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?: :'!=' }[operator]
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 == :'!=' && remainder_names_and_urls.empty?
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?: :'!=' }[operator]
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
- :'end-date' => proc { |values, operator, items|
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 == :'!=' ? :== : 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(' ').gsub(' ', '_').to_sym }
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('+').map(&:strip) : [_1] }
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 ]/, '') if _1 }
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
- :'end-date' => true,
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
- done: true,
657
+ progress: true,
654
658
  title: true,
655
- :'end-date' => true,
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
- done: true,
666
+ progress: true,
663
667
  title: true,
664
- :'end-date' => true,
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(', ')} allowed"
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 == 'none' ? nil : _1 }
717
+ .map { _1.downcase == "none" ? nil : _1 }
714
718
 
715
- # if values.count > 1 && operator == :'!=' && PROHIBIT_MULTIPLE_VALUES_AFTER_NOT[key]
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('s') }
22
+ .map { _1.delete_suffix("s") }
23
23
  .map(&:to_sym)
24
24
 
25
25
  if group_names.uniq.count < group_names.count
@@ -119,7 +119,9 @@ module Reading
119
119
  .compact
120
120
  .max
121
121
 
122
- year_ranges = (begin_date.year..end_date.year).flat_map { |year|
122
+ end_year = [Date.today.year, end_date.year].min
123
+
124
+ year_ranges = (begin_date.year..end_year).flat_map { |year|
123
125
  beginning_of_year = Date.new(year, 1, 1)
124
126
  end_of_year = Date.new(year + 1, 1, 1).prev_day
125
127
 
@@ -191,7 +193,7 @@ module Reading
191
193
  groups
192
194
  end
193
195
  },
194
- genre: proc { |items|
196
+ eachgenre: proc { |items|
195
197
  groups = Hash.new { |h, k| h[k] = [] }
196
198
 
197
199
  items.each do |item|
@@ -200,6 +202,18 @@ module Reading
200
202
 
201
203
  groups.sort.to_h
202
204
  },
205
+ genre: proc { |items|
206
+ groups = Hash.new { |h, k| h[k] = [] }
207
+
208
+ items.each do |item|
209
+ if item.genres.any?
210
+ genre_combination = item.genres.sort.join(", ")
211
+ groups[genre_combination] << item
212
+ end
213
+ end
214
+
215
+ groups.sort.to_h
216
+ },
203
217
  length: proc { |items|
204
218
  boundaries = Config.hash.fetch(:length_group_boundaries)
205
219