reading 0.8.0 → 0.9.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/bin/reading +95 -10
  3. data/lib/reading/config.rb +27 -5
  4. data/lib/reading/errors.rb +4 -1
  5. data/lib/reading/item/time_length.rb +60 -23
  6. data/lib/reading/item/view.rb +14 -19
  7. data/lib/reading/item.rb +324 -54
  8. data/lib/reading/parsing/attributes/attribute.rb +0 -7
  9. data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +17 -13
  10. data/lib/reading/parsing/attributes/experiences/history_transformer.rb +172 -60
  11. data/lib/reading/parsing/attributes/experiences/spans_validator.rb +19 -20
  12. data/lib/reading/parsing/attributes/experiences.rb +5 -5
  13. data/lib/reading/parsing/attributes/shared.rb +17 -7
  14. data/lib/reading/parsing/attributes/variants.rb +9 -6
  15. data/lib/reading/parsing/csv.rb +38 -35
  16. data/lib/reading/parsing/parser.rb +23 -24
  17. data/lib/reading/parsing/rows/blank.rb +23 -0
  18. data/lib/reading/parsing/rows/comment.rb +6 -7
  19. data/lib/reading/parsing/rows/compact_planned.rb +9 -9
  20. data/lib/reading/parsing/rows/compact_planned_columns/head.rb +2 -2
  21. data/lib/reading/parsing/rows/custom_config.rb +42 -0
  22. data/lib/reading/parsing/rows/regular.rb +15 -14
  23. data/lib/reading/parsing/rows/regular_columns/length.rb +8 -8
  24. data/lib/reading/parsing/rows/regular_columns/sources.rb +16 -10
  25. data/lib/reading/parsing/rows/regular_columns/start_dates.rb +5 -1
  26. data/lib/reading/parsing/transformer.rb +13 -17
  27. data/lib/reading/stats/filter.rb +738 -0
  28. data/lib/reading/stats/grouping.rb +257 -0
  29. data/lib/reading/stats/operation.rb +345 -0
  30. data/lib/reading/stats/query.rb +37 -0
  31. data/lib/reading/stats/terminal_result_formatters.rb +91 -0
  32. data/lib/reading/util/exclude.rb +12 -0
  33. data/lib/reading/util/hash_array_deep_fetch.rb +1 -23
  34. data/lib/reading/util/hash_to_data.rb +2 -2
  35. data/lib/reading/version.rb +1 -1
  36. data/lib/reading.rb +36 -21
  37. metadata +28 -24
  38. data/bin/readingfile +0 -31
  39. data/lib/reading/util/string_remove.rb +0 -28
  40. data/lib/reading/util/string_truncate.rb +0 -22
@@ -4,6 +4,7 @@ module Reading
4
4
  # Transformer for the :variant item attribute.
5
5
  class Variants < Attribute
6
6
  using Util::HashArrayDeepFetch
7
+ using Util::NumericToIIfWhole
7
8
 
8
9
  # @param parsed_row [Hash] a parsed row (the intermediate hash).
9
10
  # @param head_index [Integer] current item's position in the Head column.
@@ -14,13 +15,15 @@ module Reading
14
15
 
15
16
  # || [{}] in case there is no Sources column.
16
17
  (parsed_row[:sources].presence || [{}])&.map { |variant|
18
+ format = variant[:format] || head[:format]
19
+
17
20
  {
18
- format: variant[:format] || head[:format],
21
+ format:,
19
22
  series: (series(head) + series(variant)).presence,
20
23
  sources: sources(variant) || sources(head),
21
24
  isbn: variant[:isbn] || variant[:asin],
22
- length: Attributes::Shared.length(variant) ||
23
- Attributes::Shared.length(parsed_row[:length]),
25
+ length: Attributes::Shared.length(variant, format:) ||
26
+ Attributes::Shared.length(parsed_row[:length], format:),
24
27
  extra_info: Array(head[:extra_info]) + Array(variant[:extra_info]),
25
28
  }.map { |k, v| [k, v || template.fetch(k)] }.to_h
26
29
  }&.compact&.presence
@@ -29,7 +32,7 @@ module Reading
29
32
  # A shortcut to the variant template.
30
33
  # @return [Hash]
31
34
  def template
32
- config.deep_fetch(:item, :template, :variants).first
35
+ Config.hash.deep_fetch(:item, :template, :variants).first
33
36
  end
34
37
 
35
38
  # The :series sub-attribute for the given parsed hash.
@@ -57,11 +60,11 @@ module Reading
57
60
  end
58
61
 
59
62
  # The name for the given URL string, according to
60
- # config[:source_names_from_urls], or nil.
63
+ # Config.hash[:source_names_from_urls], or nil.
61
64
  # @param url [String] a URL.
62
65
  # @return [String, nil]
63
66
  def url_name(url)
64
- config
67
+ Config.hash
65
68
  .fetch(:source_names_from_urls)
66
69
  .each do |url_part, name|
67
70
  if url.include?(url_part)
@@ -1,23 +1,12 @@
1
- # Used throughout, in other files.
2
- require_relative "../util/blank"
3
- require_relative "../util/string_remove"
4
- require_relative "../util/string_truncate"
5
- require_relative "../util/numeric_to_i_if_whole"
6
- require_relative "../util/hash_deep_merge"
7
- require_relative "../util/hash_array_deep_fetch"
8
- require_relative "../util/hash_compact_by_template"
9
- require_relative "../errors"
10
-
11
- # Used just here.
12
- require_relative "../config"
13
- require_relative "../item"
14
- require_relative "parser"
15
- require_relative "transformer"
1
+ require 'pastel'
2
+ require_relative '../item'
3
+ require_relative 'parser'
4
+ require_relative 'transformer'
16
5
 
17
6
  module Reading
18
7
  module Parsing
19
8
  #
20
- # Validates a path or stream (string, file, etc.) of a CSV reading log, then
9
+ # Validates a path or lines (string, file, etc.) of a CSV reading log, then
21
10
  # parses it into an array of Items.
22
11
  #
23
12
  # Parsing happens in two steps:
@@ -31,31 +20,35 @@ module Reading
31
20
  # inspired by the Parslet gem: https://kschiess.github.io/parslet/transform.html
32
21
  #
33
22
  class CSV
34
- private attr_reader :parser, :transformer, :hash_output, :item_view
23
+ private attr_reader :parser, :transformer, :hash_output, :item_view, :error_handler, :pastel
35
24
 
36
- # Validates a path or stream (string, file, etc.) of a CSV reading log,
25
+ # Validates a path or lines (string, file, etc.) of a CSV reading log,
37
26
  # builds the config, and initializes the parser and transformer.
38
- # @param path [String] path to the CSV file; used if no stream is given.
39
- # @param stream [Object] an object responding to #each_linewith CSV row(s);
27
+ # @param path [String] path to the CSV file; used if no lines are given.
28
+ # @param lines [Object] an object responding to #each_line with CSV row(s);
40
29
  # if nil, path is used instead.
41
- # @param config [Hash] a custom config which overrides the defaults,
30
+ # @param config [Hash, Config] a custom config which overrides the defaults,
42
31
  # e.g. { errors: { styling: :html } }
43
32
  # @param hash_output [Boolean] whether an array of raw Hashes should be
44
33
  # returned, without Items being created from them.
45
- # @param view [Class, nil, Boolean] the class that will be used to build
34
+ # @param item_view [Class, nil, Boolean] the class that will be used to build
46
35
  # each Item's view object, or nil/false if no view object should be built.
47
36
  # If you use a custom view class, the only requirement is that its
48
37
  # #initialize take an Item and a full config as arguments.
49
- def initialize(path = nil, stream: nil, config: {}, hash_output: false, item_view: Item::View)
50
- validate_path_or_stream(path, stream)
51
- full_config = Config.new(config).hash
38
+ # @param error_handler [Proc] if not provided, errors are raised.
39
+ def initialize(path: nil, lines: nil, config: nil, hash_output: false, item_view: Item::View, error_handler: nil)
40
+ validate_path_or_lines(path, lines)
41
+
42
+ Config.build(config) if config
52
43
 
53
44
  @path = path
54
- @stream = stream
45
+ @lines = lines
55
46
  @hash_output = hash_output
56
47
  @item_view = item_view
57
- @parser = Parser.new(full_config)
58
- @transformer = Transformer.new(full_config)
48
+ @parser = Parser.new
49
+ @transformer = Transformer.new
50
+ @error_handler = error_handler
51
+ @pastel = Pastel.new
59
52
  end
60
53
 
61
54
  # Parses and transforms the reading log into item data.
@@ -64,16 +57,26 @@ module Reading
64
57
  # structure to that Hash (with every inner Hash replaced by a Data for
65
58
  # dot access).
66
59
  def parse
67
- input = @stream || File.open(@path)
60
+ input = @lines || File.open(@path)
68
61
  items = []
69
62
 
70
63
  input.each_line do |line|
71
64
  begin
72
65
  intermediate = parser.parse_row_to_intermediate_hash(line)
66
+
73
67
  next if intermediate.empty? # When the row is blank or a comment.
68
+
74
69
  row_items = transformer.transform_intermediate_hash_to_item_hashes(intermediate)
75
70
  rescue Reading::Error => e
76
- raise e.class, "#{e.message} in the row \"#{line}\""
71
+ colored_e =
72
+ e.class.new("#{pastel.bright_red(e.message)} in the row #{pastel.bright_yellow(line.chomp)}")
73
+
74
+ if error_handler
75
+ error_handler.call(colored_e)
76
+ next
77
+ else
78
+ raise colored_e
79
+ end
77
80
  end
78
81
 
79
82
  items += row_items
@@ -90,11 +93,11 @@ module Reading
90
93
 
91
94
  private
92
95
 
93
- # Checks on the given stream and path (arguments to #initialize).
96
+ # Checks on the given lines and path (arguments to #initialize).
94
97
  # @raise [FileError] if the given path is invalid.
95
- # @raise [ArgumentError] if both stream and path are nil.
96
- def validate_path_or_stream(path, stream)
97
- if stream && stream.respond_to?(:each_line)
98
+ # @raise [ArgumentError] if both lines and path are nil.
99
+ def validate_path_or_lines(path, lines)
100
+ if lines && lines.respond_to?(:each_line)
98
101
  return true
99
102
  elsif path
100
103
  if !File.exist?(path)
@@ -104,7 +107,7 @@ module Reading
104
107
  end
105
108
  else
106
109
  raise ArgumentError,
107
- "Either a file path or a stream (string, file, etc.) must be provided."
110
+ "Provide either a file path or object implementing #each_line (String, File, etc.)."
108
111
  end
109
112
  end
110
113
  end
@@ -1,6 +1,8 @@
1
- require_relative "rows/regular"
2
- require_relative "rows/compact_planned"
3
- 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'
4
6
 
5
7
  module Reading
6
8
  module Parsing
@@ -43,14 +45,6 @@ module Reading
43
45
  #
44
46
  class Parser
45
47
  using Util::HashArrayDeepFetch
46
- using Util::StringRemove
47
-
48
- attr_reader :config
49
-
50
- # @param config [Hash] an entire config.
51
- def initialize(config)
52
- @config = config
53
- end
54
48
 
55
49
  # Parses a row string into a hash that mirrors the structure of the row.
56
50
  # @param string [String] a string containing a row of a CSV reading log.
@@ -58,7 +52,7 @@ module Reading
58
52
  def parse_row_to_intermediate_hash(string)
59
53
  columns = extract_columns(string)
60
54
 
61
- if config.fetch(:skip_compact_planned) && columns.has_key?(Rows::CompactPlanned::Head)
55
+ if Config.hash.fetch(:skip_compact_planned) && columns.has_key?(Rows::CompactPlanned::Head)
62
56
  return {}
63
57
  end
64
58
 
@@ -76,14 +70,19 @@ module Reading
76
70
  # Parsing::Rows::Column.
77
71
  def extract_columns(string)
78
72
  string = string.dup.force_encoding(Encoding::UTF_8)
79
- column_strings = string.split(config.fetch(:column_separator))
73
+ column_strings = string.split(Config.hash.fetch(:column_separator))
80
74
 
81
- row_types = [Rows::Regular, Rows::CompactPlanned, Rows::Comment]
75
+ row_types = [Rows::Blank, Rows::Regular, Rows::CompactPlanned, Rows::CustomConfig, Rows::Comment]
82
76
  column_classes = row_types
83
- .find { |row_type| row_type.match?(string, config) }
77
+ .find { |row_type| row_type.match?(string) }
78
+ .tap { |row_type|
79
+ if row_type == Rows::CustomConfig
80
+ row_type.merge_custom_config!(string)
81
+ end
82
+ }
84
83
  .column_classes
85
- .filter { |column_class|
86
- config.fetch(:enabled_columns).include?(column_class.to_sym)
84
+ .select { |column_class|
85
+ Config.hash.fetch(:enabled_columns).include?(column_class.to_sym)
87
86
  }
88
87
 
89
88
  if !column_classes.count.zero? && column_strings.count > column_classes.count
@@ -123,7 +122,7 @@ module Reading
123
122
  # it doesn't contain any format emojis, return the same as above but
124
123
  # with an extra level of nesting (except when the parsed result is nil).
125
124
  if column_class.split_by_format? &&
126
- !column_string.match?(config.deep_fetch(:regex, :formats))
125
+ !column_string.match?(Config.hash.deep_fetch(:regex, :formats))
127
126
 
128
127
  parsed_column = parse_segments(column_class, column_string)
129
128
  # Wrap a non-empty value in an array so that e.g. a head without
@@ -136,18 +135,18 @@ module Reading
136
135
  # The rest is the complex case: if the column *can and is* split by format.
137
136
 
138
137
  # Each format plus the string after it.
139
- format_strings = column_string.split(config.deep_fetch(:regex, :formats_split))
138
+ format_strings = column_string.split(Config.hash.deep_fetch(:regex, :formats_split))
140
139
 
141
140
  # If there's a string before the first format, e.g. "DNF" in Head column.
142
- unless format_strings.first.match?(config.deep_fetch(:regex, :formats))
141
+ unless format_strings.first.match?(Config.hash.deep_fetch(:regex, :formats))
143
142
  before_formats = parse_segment(column_class, format_strings.shift, before_formats: true)
144
143
  end
145
144
 
146
145
  # Parse each format-plus-string into an array of segments.
147
146
  heads = format_strings.map { |string|
148
- format_emoji = string[config.deep_fetch(:regex, :formats)]
149
- string.remove!(format_emoji)
150
- format = config.fetch(:formats).key(format_emoji)
147
+ format_emoji = string[Config.hash.deep_fetch(:regex, :formats)]
148
+ string.sub!(format_emoji, '')
149
+ format = Config.hash.fetch(:formats).key(format_emoji)
151
150
 
152
151
  parse_segments(column_class, string)
153
152
  .merge(format: format)
@@ -252,7 +251,7 @@ module Reading
252
251
  # @return [Hash{Symbol => String}] e.g. { author: "Bram Stoker", title: "Dracula"}
253
252
  def parse_segment_with_regex(segment, regex)
254
253
  segment
255
- .tr(config.fetch(:ignored_characters), "")
254
+ .tr(Config.hash.fetch(:ignored_characters), "")
256
255
  .strip
257
256
  .match(regex)
258
257
  &.named_captures
@@ -0,0 +1,23 @@
1
+ module Reading
2
+ module Parsing
3
+ module Rows
4
+ # A row that is a blank line.
5
+ module Blank
6
+ using Util::HashArrayDeepFetch
7
+
8
+ # No columns.
9
+ # @return [Array]
10
+ def self.column_classes
11
+ []
12
+ end
13
+
14
+ # Is a blank line.
15
+ # @param row_string [String]
16
+ # @return [Boolean]
17
+ def self.match?(row_string)
18
+ row_string == "\n"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -5,20 +5,19 @@ module Reading
5
5
  module Comment
6
6
  using Util::HashArrayDeepFetch
7
7
 
8
- # No columns; comments are parsed as if the row were blank.
8
+ # No columns; parsed as if the row were blank.
9
9
  # @return [Array]
10
10
  def self.column_classes
11
11
  []
12
12
  end
13
13
 
14
- # Starts with a comment character and does not include any format emojis.
15
- # (Commented rows that DO include format emojis are matched as compact
16
- # planned rows.)
14
+ # Starts with a comment character. Note: this must be called *after*
15
+ # calling ::match? on Rows::CompactPlanned and Rows::CustomConfig,
16
+ # because those check for starting with a comment character too.
17
17
  # @param row_string [String]
18
- # @param config [Hash]
19
18
  # @return [Boolean]
20
- def self.match?(row_string, config)
21
- row_string.lstrip.start_with?(config.fetch(:comment_character))
19
+ def self.match?(row_string)
20
+ row_string.lstrip.start_with?(Config.hash.fetch(:comment_character))
22
21
  end
23
22
  end
24
23
  end
@@ -1,6 +1,7 @@
1
- require_relative "column"
2
- require_relative "compact_planned_columns/head"
3
- require_relative "regular_columns/sources"
1
+ require_relative 'column'
2
+ require_relative 'compact_planned_columns/head'
3
+ require_relative 'regular_columns/sources'
4
+ require_relative 'regular_columns/length'
4
5
 
5
6
  module Reading
6
7
  module Parsing
@@ -12,17 +13,16 @@ module Reading
12
13
  # The columns that are possible in this type of row.
13
14
  # @return [Array<Class>]
14
15
  def self.column_classes
15
- [CompactPlanned::Head, Regular::Sources]
16
+ [CompactPlanned::Head, Regular::Sources, Regular::Length]
16
17
  end
17
18
 
18
19
  # Starts with a comment character and includes one or more format emojis.
19
20
  # @param row_string [String]
20
- # @param config [Hash]
21
21
  # @return [Boolean]
22
- def self.match?(row_string, config)
23
- row_string.lstrip.start_with?(config.fetch(:comment_character)) &&
24
- row_string.match?(config.deep_fetch(:regex, :formats)) &&
25
- row_string.count(config.fetch(:column_separator)) <= column_classes.count - 1
22
+ def self.match?(row_string)
23
+ row_string.lstrip.start_with?(Config.hash.fetch(:comment_character)) &&
24
+ row_string.match?(Config.hash.deep_fetch(:regex, :formats)) &&
25
+ row_string.count(Config.hash.fetch(:column_separator)) <= column_classes.count - 1
26
26
  end
27
27
  end
28
28
  end
@@ -15,7 +15,7 @@ module Reading
15
15
  \\ # comment character
16
16
  \s*
17
17
  (
18
- (?<genres>[^a-z]+)?
18
+ (?<genres>[^a-z@]+)?
19
19
  \s*
20
20
  (?<sources>@.+)?
21
21
  \s*:
@@ -49,7 +49,7 @@ module Reading
49
49
  )?
50
50
  (?<title>[^@]+)
51
51
  (?<sources>@.+)?
52
- \z}x if segment_index.zero?),
52
+ \z}x if segment_index.zero?),
53
53
  *Column::SHARED_REGEXES[:series_and_extra_info],
54
54
  ].compact
55
55
  end
@@ -0,0 +1,42 @@
1
+ module Reading
2
+ module Parsing
3
+ module Rows
4
+ # A row that declares custom config.
5
+ module CustomConfig
6
+ using Util::HashArrayDeepFetch
7
+
8
+ # No columns; parsed as if the row were blank.
9
+ # @return [Array]
10
+ def self.column_classes
11
+ []
12
+ end
13
+
14
+ # Starts with a comment character and opening curly brace, and ends with
15
+ # a closing curly brace.
16
+ # @param row_string [String]
17
+ # @return [Boolean]
18
+ def self.match?(row_string)
19
+ row_string.match?(
20
+ %r{\A
21
+ \s*
22
+ #{Regexp.escape(Config.hash.fetch(:comment_character))}
23
+ \s*
24
+ \{.+\}
25
+ \s*
26
+ \z}x
27
+ )
28
+ end
29
+
30
+ # Adds this row's custom config to the singleton config.
31
+ # @param row_string [String]
32
+ # @param config [Hash] an entire config.
33
+ def self.merge_custom_config!(row_string)
34
+ stripped_row = row_string.strip.delete_prefix(Config.hash.fetch(:comment_character))
35
+ custom_config = eval(stripped_row)
36
+
37
+ Config.hash.merge!(custom_config)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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
@@ -20,12 +20,13 @@ module Reading
20
20
  [Rating, Head, Sources, StartDates, EndDates, Genres, Length, Notes, History]
21
21
  end
22
22
 
23
- # Does not start with a comment character.
23
+ # Does not start with a comment character. Note: this must be called
24
+ # *after* calling ::match? on Rows::Blank, because that one catches
25
+ # blank lines.
24
26
  # @param row_string [String]
25
- # @param config [Hash]
26
27
  # @return [Boolean]
27
- def self.match?(row_string, config)
28
- !row_string.lstrip.start_with?(config.fetch(:comment_character))
28
+ def self.match?(row_string)
29
+ !row_string.lstrip.start_with?(Config.hash.fetch(:comment_character))
29
30
  end
30
31
  end
31
32
  end
@@ -15,16 +15,16 @@ module Reading
15
15
  )
16
16
  (\s+|\z)
17
17
  )
18
- # each or repetitions, used in conjunction with the History column
18
+ # each and repetitions are used in conjunction with the History column
19
+ # each
19
20
  (
20
- # each
21
21
  (?<each>each)
22
- |
23
- # repetitions
24
- (
25
- x
26
- (?<repetitions>\d+)
27
- )
22
+ (\s+|\z)
23
+ )?
24
+ # repetitions
25
+ (
26
+ x
27
+ (?<repetitions>\d+)
28
28
  )?
29
29
  \z}x]
30
30
  end
@@ -6,17 +6,12 @@ module Reading
6
6
  # and https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#sources-column-variants
7
7
  class Sources < Column
8
8
  SOURCES_PARSING_ERRORS = {
9
- "Missing comma before URL(s) in the Sources column" =>
10
- ->(source) {
11
- source.match?(/\shttps?:\/\//) || source.scan(/https?:\/\//).count > 1
12
- },
13
- "The ISBN/ASIN must be placed after sources in the Sources column" =>
9
+ "The ISBN/ASIN must be placed last in the Sources column" =>
14
10
  ->(source) {
15
11
  source.match?(/\A#{ISBN_REGEX}/o) || source.match(/\A#{ASIN_REGEX}/o)
16
12
  },
17
13
  }
18
14
 
19
-
20
15
  def self.split_by_format?
21
16
  true
22
17
  end
@@ -32,7 +27,18 @@ module Reading
32
27
  def self.tweaks
33
28
  {
34
29
  sources: -> {
35
- sources = _1.split(/\s*,\s*/)
30
+ comma = /\s*,\s*/
31
+ space_before_url = / (?=https?:\/\/)/
32
+ sources = _1.split(Regexp.union(comma, space_before_url))
33
+
34
+ # Split by space after URL.
35
+ sources = sources.flat_map { |src|
36
+ if src.match?(/\Ahttps?:\/\//)
37
+ src.split(" ", 2)
38
+ else
39
+ src
40
+ end
41
+ }
36
42
 
37
43
  SOURCES_PARSING_ERRORS.each do |message, check|
38
44
  if sources.any? { |source| check.call(source) }
@@ -58,7 +64,7 @@ module Reading
58
64
  |
59
65
  (?<length_time>\d+:\d\d)
60
66
  )?
61
- \z}x if segment_index.zero?),
67
+ \z}x if segment_index.zero?),
62
68
  # sources, ISBN/ASIN, length
63
69
  (%r{\A
64
70
  (
@@ -78,14 +84,14 @@ module Reading
78
84
  |
79
85
  (?<length_time>\d+:\d\d)
80
86
  )?
81
- \z}xo if segment_index.zero?),
87
+ \z}xo if segment_index.zero?),
82
88
  *Column::SHARED_REGEXES[:series_and_extra_info],
83
89
  ].compact
84
90
  end
85
91
 
86
92
  private
87
93
 
88
- ISBN_REGEX = /(\d{3}[-\s]?)?\d{10}/
94
+ ISBN_REGEX = /(\d{3}[-\s]?)?(\d{10}|\d{9}X)/
89
95
  ASIN_REGEX = /B0[A-Z\d]{8}/
90
96
  end
91
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
@@ -20,13 +20,9 @@ module Reading
20
20
  using Util::HashArrayDeepFetch
21
21
  using Util::HashCompactByTemplate
22
22
 
23
- attr_reader :config
24
23
  private attr_reader :attributes
25
24
 
26
- # @param config [Hash] an entire config.
27
- def initialize(config)
28
- @config = config
29
-
25
+ def initialize
30
26
  set_attributes
31
27
  end
32
28
 
@@ -40,7 +36,7 @@ module Reading
40
36
  raise InvalidHeadError, "Blank or missing Head column"
41
37
  end
42
38
 
43
- template = config.deep_fetch(:item, :template)
39
+ template = Config.hash.deep_fetch(:item, :template)
44
40
 
45
41
  parsed_row[:head].map.with_index { |_head, head_index|
46
42
  template.map { |attribute_name, default_value|
@@ -58,11 +54,11 @@ module Reading
58
54
  # Sets the attributes classes which do all the transforming work.
59
55
  # See parsing/attributes/*.
60
56
  def set_attributes
61
- @attributes ||= config.deep_fetch(:item, :template).map { |attribute_name, _default|
57
+ @attributes ||= Config.hash.deep_fetch(:item, :template).map { |attribute_name, _default|
62
58
  attribute_name_camelcase = attribute_name.to_s.split("_").map(&:capitalize).join
63
59
  attribute_class = Attributes.const_get(attribute_name_camelcase)
64
60
 
65
- [attribute_name, attribute_class.new(config)]
61
+ [attribute_name, attribute_class.new]
66
62
  }.to_h
67
63
  end
68
64
  end