reading 0.6.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.
@@ -0,0 +1,88 @@
1
+ module Reading
2
+ # A base class that contains behaviors common to ___Row classes.
3
+ class Row
4
+ using Util::StringRemove
5
+ using Util::HashArrayDeepFetch
6
+ using Util::HashCompactByTemplate
7
+
8
+ private attr_reader :line
9
+
10
+ # @param line [Reading::Line] the Line that this Row represents.
11
+ def initialize(line)
12
+ @line = line
13
+
14
+ after_initialize
15
+ end
16
+
17
+ # Parses a CSV row into an array of hashes of item data. How this is done
18
+ # depends on how the template methods (further below) are implemented in
19
+ # subclasses of Row.
20
+ # @return [Array<Hash>] an array of hashes like the template in config.rb
21
+ def parse
22
+ return [] if skip?
23
+
24
+ before_parse
25
+
26
+ items = item_heads.map { |item_head|
27
+ item_hash(item_head)
28
+ .compact_by(template: config.deep_fetch(:item, :template))
29
+ }.compact
30
+
31
+ items
32
+
33
+ rescue Reading::Error => e
34
+ e.handle(line:)
35
+ []
36
+ end
37
+
38
+ private
39
+
40
+ def string
41
+ @line.string
42
+ end
43
+
44
+ def config
45
+ @line.csv.config
46
+ end
47
+
48
+ # A "head" is a string in the Head column containing a chunk of item
49
+ # information, starting with a format emoji. A typical row describes one
50
+ # item and so contains one head, but a row describing multiple items (with
51
+ # multiple heads in the Head column) is possible. Also, a row of compact
52
+ # planned items is essentially a list of heads, though with different
53
+ # elements than a normal row's head.
54
+ # @return [Array<String>]
55
+ def item_heads
56
+ string_to_be_split_by_format_emojis
57
+ .split(config.deep_fetch(:csv, :regex, :formats_split))
58
+ .tap { |item_heads|
59
+ item_heads.first.remove!(config.deep_fetch(:csv, :regex, :dnf))
60
+ item_heads.first.remove!(config.deep_fetch(:csv, :regex, :progress))
61
+ }
62
+ .map { |item_head| item_head.strip }
63
+ .partition { |item_head| item_head.match?(/\A#{config.deep_fetch(:csv, :regex, :formats)}/) }
64
+ .reject(&:empty?)
65
+ .first
66
+ end
67
+
68
+ # Below: template methods that can (or must) be overridden.
69
+
70
+ def after_initialize
71
+ end
72
+
73
+ def before_parse
74
+ end
75
+
76
+ def skip?
77
+ false
78
+ end
79
+
80
+ def string_to_be_split_by_format_emojis
81
+ raise NotImplementedError, "#{self.class} should have implemented #{__method__}"
82
+ end
83
+
84
+ def item_hash(item_head)
85
+ raise NotImplementedError, "#{self.class} should have implemented #{__method__}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,146 @@
1
+ # Copied from active_support/core_ext/object/blank
2
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/object/blank.rb
3
+ # Except detection of ENCODED_BLANKS is omitted here.
4
+ class Object
5
+ # An object is blank if it's false, empty, or a whitespace string.
6
+ # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
7
+ #
8
+ # This simplifies
9
+ #
10
+ # !address || address.empty?
11
+ #
12
+ # to
13
+ #
14
+ # address.blank?
15
+ #
16
+ # @return [true, false]
17
+ def blank?
18
+ respond_to?(:empty?) ? !!empty? : !self
19
+ end
20
+
21
+ # An object is present if it's not blank.
22
+ #
23
+ # @return [true, false]
24
+ def present?
25
+ !blank?
26
+ end
27
+
28
+ # Returns the receiver if it's present otherwise returns +nil+.
29
+ # <tt>object.presence</tt> is equivalent to
30
+ #
31
+ # object.present? ? object : nil
32
+ #
33
+ # For example, something like
34
+ #
35
+ # state = params[:state] if params[:state].present?
36
+ # country = params[:country] if params[:country].present?
37
+ # region = state || country || 'US'
38
+ #
39
+ # becomes
40
+ #
41
+ # region = params[:state].presence || params[:country].presence || 'US'
42
+ #
43
+ # @return [Object]
44
+ def presence
45
+ self if present?
46
+ end
47
+ end
48
+
49
+ class NilClass
50
+ # +nil+ is blank:
51
+ #
52
+ # nil.blank? # => true
53
+ #
54
+ # @return [true]
55
+ def blank?
56
+ true
57
+ end
58
+ end
59
+
60
+ class FalseClass
61
+ # +false+ is blank:
62
+ #
63
+ # false.blank? # => true
64
+ #
65
+ # @return [true]
66
+ def blank?
67
+ true
68
+ end
69
+ end
70
+
71
+ class TrueClass
72
+ # +true+ is not blank:
73
+ #
74
+ # true.blank? # => false
75
+ #
76
+ # @return [false]
77
+ def blank?
78
+ false
79
+ end
80
+ end
81
+
82
+ class Array
83
+ # An array is blank if it's empty:
84
+ #
85
+ # [].blank? # => true
86
+ # [1,2,3].blank? # => false
87
+ #
88
+ # @return [true, false]
89
+ alias_method :blank?, :empty?
90
+ end
91
+
92
+ class Hash
93
+ # A hash is blank if it's empty:
94
+ #
95
+ # {}.blank? # => true
96
+ # { key: 'value' }.blank? # => false
97
+ #
98
+ # @return [true, false]
99
+ alias_method :blank?, :empty?
100
+ end
101
+
102
+ class String
103
+ BLANK_RE = /\A[[:space:]]*\z/
104
+
105
+ # A string is blank if it's empty or contains whitespaces only:
106
+ #
107
+ # ''.blank? # => true
108
+ # ' '.blank? # => true
109
+ # "\t\n\r".blank? # => true
110
+ # ' blah '.blank? # => false
111
+ #
112
+ # Unicode whitespace is supported:
113
+ #
114
+ # "\u00a0".blank? # => true
115
+ #
116
+ # @return [true, false]
117
+ def blank?
118
+ # The regexp that matches blank strings is expensive. For the case of empty
119
+ # strings we can speed up this method (~3.5x) with an empty? call. The
120
+ # penalty for the rest of strings is marginal.
121
+ empty? || BLANK_RE.match?(self)
122
+ end
123
+ end
124
+
125
+ class Numeric # :nodoc:
126
+ # No number is blank:
127
+ #
128
+ # 1.blank? # => false
129
+ # 0.blank? # => false
130
+ #
131
+ # @return [false]
132
+ def blank?
133
+ false
134
+ end
135
+ end
136
+
137
+ class Time # :nodoc:
138
+ # No Time is blank:
139
+ #
140
+ # Time.now.blank? # => false
141
+ #
142
+ # @return [false]
143
+ def blank?
144
+ false
145
+ end
146
+ end
@@ -0,0 +1,40 @@
1
+ module Reading
2
+ module Util
3
+ class FetchDepthExceededError < StandardError
4
+ end
5
+
6
+ # Similar to Array#dig and Hash#dig but raises an error for not found elements.
7
+ #
8
+ # More flexible but slightly slower alternative:
9
+ # keys.reduce(self) { |a, e| a.fetch(e) }
10
+ #
11
+ # See performance comparisons:
12
+ # https://fpsvogel.com/posts/2022/ruby-hash-dot-syntax-deep-fetch
13
+ module HashArrayDeepFetch
14
+ def deep_fetch(*keys)
15
+ case keys.length
16
+ when 1
17
+ fetch(keys[0])
18
+ when 2
19
+ fetch(keys[0]).fetch(keys[1])
20
+ when 3
21
+ fetch(keys[0]).fetch(keys[1]).fetch(keys[2])
22
+ when 4
23
+ fetch(keys[0]).fetch(keys[1]).fetch(keys[2]).fetch(keys[3])
24
+ when 5
25
+ fetch(keys[0]).fetch(keys[1]).fetch(keys[2]).fetch(keys[3]).fetch(keys[4])
26
+ else
27
+ raise FetchDepthExceededError, "#deep_fetch can't fetch that deep!"
28
+ end
29
+ end
30
+
31
+ refine Hash do
32
+ import_methods HashArrayDeepFetch
33
+ end
34
+
35
+ refine Array do
36
+ import_methods HashArrayDeepFetch
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module Reading
2
+ module Util
3
+ # Utility method for a hash containing parsed item data, structured as the
4
+ # template in config.rb.
5
+ module HashCompactByTemplate
6
+ refine Hash do
7
+ # Removes blank arrays of hashes from the given item hash, e.g. series,
8
+ # variants, variants[:sources], and experiences in the template in config.rb.
9
+ # If no parsed data has been added to the template values for these, they
10
+ # are considered blank, and are replaced with an empty array so that their
11
+ # emptiness is more apparent, e.g. item[:experiences].empty? will return true.
12
+ def compact_by(template:)
13
+ map { |key, val|
14
+ if is_array_of_hashes?(val)
15
+ if is_blank_like_template?(val, template.fetch(key))
16
+ [key, []]
17
+ else
18
+ [key, val.map { |el| el.compact_by(template: template.fetch(key).first) }]
19
+ end
20
+ else
21
+ [key, val]
22
+ end
23
+ }.to_h
24
+ end
25
+
26
+ private
27
+
28
+ def is_array_of_hashes?(val)
29
+ val.is_a?(Array) && val.first.is_a?(Hash)
30
+ end
31
+
32
+ def is_blank_like_template?(val, template_val)
33
+ val.length == 1 && val == template_val
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ module Reading
2
+ module Util
3
+ # Modified from active_support/core_ext/hash/deep_merge
4
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
5
+ #
6
+ # This deep_merge also iterates through arrays of hashes and merges them.
7
+ module HashDeepMerge
8
+ refine Hash do
9
+ def deep_merge(other_hash, &block)
10
+ dup.deep_merge!(other_hash, &block)
11
+ end
12
+
13
+ def deep_merge!(other_hash, &block)
14
+ merge!(other_hash) do |key, this_val, other_val|
15
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
16
+ this_val.deep_merge(other_val, &block)
17
+ # I added this part for merging values that are arrays of hashes.
18
+ elsif this_val.is_a?(Array) && other_val.is_a?(Array) &&
19
+ this_val.all? { |el| el.is_a?(Hash) } &&
20
+ other_val.all? { |el| el.is_a?(Hash) }
21
+ zip =
22
+ if other_val.length >= this_val.length
23
+ other_val.zip(this_val)
24
+ else
25
+ this_val.zip(other_val).map(&:reverse)
26
+ end
27
+ zip.map { |other_el, this_el|
28
+ if this_el.nil?
29
+ other_el
30
+ else
31
+ this_el.deep_merge(other_el || {}, &block)
32
+ end
33
+ }
34
+ elsif block_given?
35
+ block.call(key, this_val, other_val)
36
+ else
37
+ other_val
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ module Reading
2
+ module Util
3
+ # Converts a Hash to a Struct. Converts inner hashes (and inner arrays of hashes) as well.
4
+ module HashToStruct
5
+ refine Hash do
6
+ def to_struct
7
+ MEMOIZED_STRUCTS[keys] ||= Struct.new(*keys)
8
+ struct_class = MEMOIZED_STRUCTS[keys]
9
+
10
+ struct_values = transform_values { |v|
11
+ if v.is_a?(Hash)
12
+ v.to_struct
13
+ elsif v.is_a?(Array) && v.all? { |el| el.is_a?(Hash) }
14
+ v.map(&:to_struct)
15
+ else
16
+ v
17
+ end
18
+ }.values
19
+
20
+ struct_class.new(*struct_values)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ MEMOIZED_STRUCTS = {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ module Reading
2
+ module Util
3
+ # Shortcuts for String#sub and String#gsub when replacing with an empty string.
4
+ module StringRemove
5
+ refine String do
6
+ def remove(pattern)
7
+ sub(pattern, EMPTY_STRING)
8
+ end
9
+
10
+ def remove!(pattern)
11
+ sub!(pattern, EMPTY_STRING)
12
+ end
13
+
14
+ def remove_all(pattern)
15
+ gsub(pattern, EMPTY_STRING)
16
+ end
17
+
18
+ def remove_all!(pattern)
19
+ gsub!(pattern, EMPTY_STRING)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ EMPTY_STRING = "".freeze
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Reading
2
+ module Util
3
+ module StringTruncate
4
+ refine String do
5
+ def truncate(max, padding: 0, min: 30)
6
+ end_index = max - padding
7
+ end_index = min if end_index < min
8
+ self.length + padding > max ? "#{self[0...end_index]}..." : self
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Reading
2
+ VERSION = "0.6.0"
3
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reading
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Felipe Vogel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pastel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: debug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pretty-diffs
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: amazing_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubycritic
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - fps.vogel@gmail.com
114
+ executables:
115
+ - reading
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - bin/reading
120
+ - lib/reading/attribute/all_attributes.rb
121
+ - lib/reading/attribute/attribute.rb
122
+ - lib/reading/attribute/experiences/dates_validator.rb
123
+ - lib/reading/attribute/experiences/experiences_attribute.rb
124
+ - lib/reading/attribute/experiences/progress_subattribute.rb
125
+ - lib/reading/attribute/experiences/spans_subattribute.rb
126
+ - lib/reading/attribute/variants/extra_info_subattribute.rb
127
+ - lib/reading/attribute/variants/length_subattribute.rb
128
+ - lib/reading/attribute/variants/series_subattribute.rb
129
+ - lib/reading/attribute/variants/sources_subattribute.rb
130
+ - lib/reading/attribute/variants/variants_attribute.rb
131
+ - lib/reading/config.rb
132
+ - lib/reading/csv.rb
133
+ - lib/reading/errors.rb
134
+ - lib/reading/line.rb
135
+ - lib/reading/row/blank_row.rb
136
+ - lib/reading/row/compact_planned_row.rb
137
+ - lib/reading/row/regular_row.rb
138
+ - lib/reading/row/row.rb
139
+ - lib/reading/util/blank.rb
140
+ - lib/reading/util/hash_array_deep_fetch.rb
141
+ - lib/reading/util/hash_compact_by_template.rb
142
+ - lib/reading/util/hash_deep_merge.rb
143
+ - lib/reading/util/hash_to_struct.rb
144
+ - lib/reading/util/string_remove.rb
145
+ - lib/reading/util/string_truncate.rb
146
+ - lib/reading/version.rb
147
+ homepage: https://github.com/fpsvogel/reading
148
+ licenses:
149
+ - MIT
150
+ metadata:
151
+ allowed_push_host: https://rubygems.org
152
+ homepage_uri: https://github.com/fpsvogel/reading
153
+ source_code_uri: https://github.com/fpsvogel/reading
154
+ changelog_uri: https://github.com/fpsvogel/reading/blob/master/CHANGELOG.md
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 3.0.0
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.4.1
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: reading parses a CSV reading log.
174
+ test_files: []