reading 0.7.0 → 0.9.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/reading +80 -10
  3. data/lib/reading/config.rb +96 -52
  4. data/lib/reading/errors.rb +4 -1
  5. data/lib/reading/filter.rb +95 -0
  6. data/lib/reading/item/time_length.rb +69 -30
  7. data/lib/reading/item/view.rb +116 -0
  8. data/lib/reading/item.rb +384 -0
  9. data/lib/reading/parsing/attributes/attribute.rb +1 -8
  10. data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +11 -12
  11. data/lib/reading/parsing/attributes/experiences/history_transformer.rb +31 -22
  12. data/lib/reading/parsing/attributes/experiences/spans_validator.rb +19 -20
  13. data/lib/reading/parsing/attributes/experiences.rb +6 -6
  14. data/lib/reading/parsing/attributes/notes.rb +1 -1
  15. data/lib/reading/parsing/attributes/shared.rb +15 -8
  16. data/lib/reading/parsing/attributes/variants.rb +10 -7
  17. data/lib/reading/parsing/csv.rb +58 -44
  18. data/lib/reading/parsing/parser.rb +24 -25
  19. data/lib/reading/parsing/rows/blank.rb +23 -0
  20. data/lib/reading/parsing/rows/comment.rb +6 -7
  21. data/lib/reading/parsing/rows/compact_planned.rb +9 -9
  22. data/lib/reading/parsing/rows/compact_planned_columns/head.rb +2 -2
  23. data/lib/reading/parsing/rows/custom_config.rb +42 -0
  24. data/lib/reading/parsing/rows/regular.rb +15 -14
  25. data/lib/reading/parsing/rows/regular_columns/length.rb +8 -8
  26. data/lib/reading/parsing/rows/regular_columns/sources.rb +15 -9
  27. data/lib/reading/parsing/transformer.rb +15 -19
  28. data/lib/reading/stats/filter.rb +738 -0
  29. data/lib/reading/stats/grouping.rb +243 -0
  30. data/lib/reading/stats/operation.rb +313 -0
  31. data/lib/reading/stats/query.rb +37 -0
  32. data/lib/reading/stats/terminal_result_formatters.rb +91 -0
  33. data/lib/reading/util/exclude.rb +12 -0
  34. data/lib/reading/util/hash_to_data.rb +30 -0
  35. data/lib/reading/version.rb +1 -1
  36. data/lib/reading.rb +51 -5
  37. metadata +28 -7
  38. data/bin/readingfile +0 -31
  39. data/lib/reading/util/hash_to_struct.rb +0 -30
  40. data/lib/reading/util/string_remove.rb +0 -28
  41. data/lib/reading/util/string_truncate.rb +0 -22
@@ -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,7 +84,7 @@ 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
@@ -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
@@ -14,19 +14,15 @@ module Reading
14
14
  # Transforms an intermediate hash (parsed from a CSV row) into item data.
15
15
  # While the intermediate hash mirrors the structure of a row, the output of
16
16
  # Transformer is based around item attributes, which are listed in
17
- # Config#default_config[:item_template] and in the files in parsing/attributes.
17
+ # Config#default_config[:item][:template] and in the files in parsing/attributes.
18
18
  #
19
19
  class Transformer
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
 
@@ -34,13 +30,13 @@ module Reading
34
30
  # @param parsed_row [Hash{Symbol => Hash, Array}] output from
35
31
  # Parsing::Parser#parse_row_to_intermediate_hash.
36
32
  # @return [Array<Hash>] an array of Hashes like the template in
37
- # Config#default_config[:item_template].
33
+ # Config#default_config[:item][:template].
38
34
  def transform_intermediate_hash_to_item_hashes(parsed_row)
39
35
  if parsed_row[:head].blank?
40
36
  raise InvalidHeadError, "Blank or missing Head column"
41
37
  end
42
38
 
43
- template = config.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.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