reading 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/bin/reading +5 -5
- data/bin/readingfile +31 -0
- data/lib/reading/config.rb +96 -108
- data/lib/reading/errors.rb +10 -66
- data/lib/reading/filter.rb +95 -0
- data/lib/reading/item/time_length.rb +140 -0
- data/lib/reading/item/view.rb +121 -0
- data/lib/reading/item.rb +117 -0
- data/lib/reading/parsing/attributes/attribute.rb +26 -0
- data/lib/reading/parsing/attributes/author.rb +15 -0
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +106 -0
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +452 -0
- data/lib/reading/parsing/attributes/experiences/spans_validator.rb +149 -0
- data/lib/reading/parsing/attributes/experiences.rb +27 -0
- data/lib/reading/parsing/attributes/genres.rb +16 -0
- data/lib/reading/parsing/attributes/notes.rb +22 -0
- data/lib/reading/parsing/attributes/rating.rb +17 -0
- data/lib/reading/parsing/attributes/shared.rb +62 -0
- data/lib/reading/parsing/attributes/title.rb +21 -0
- data/lib/reading/parsing/attributes/variants.rb +77 -0
- data/lib/reading/parsing/csv.rb +112 -0
- data/lib/reading/parsing/parser.rb +292 -0
- data/lib/reading/parsing/rows/column.rb +131 -0
- data/lib/reading/parsing/rows/comment.rb +26 -0
- data/lib/reading/parsing/rows/compact_planned.rb +30 -0
- data/lib/reading/parsing/rows/compact_planned_columns/head.rb +60 -0
- data/lib/reading/parsing/rows/regular.rb +33 -0
- data/lib/reading/parsing/rows/regular_columns/end_dates.rb +20 -0
- data/lib/reading/parsing/rows/regular_columns/genres.rb +20 -0
- data/lib/reading/parsing/rows/regular_columns/head.rb +45 -0
- data/lib/reading/parsing/rows/regular_columns/history.rb +143 -0
- data/lib/reading/parsing/rows/regular_columns/length.rb +35 -0
- data/lib/reading/parsing/rows/regular_columns/notes.rb +32 -0
- data/lib/reading/parsing/rows/regular_columns/rating.rb +15 -0
- data/lib/reading/parsing/rows/regular_columns/sources.rb +94 -0
- data/lib/reading/parsing/rows/regular_columns/start_dates.rb +35 -0
- data/lib/reading/parsing/transformer.rb +70 -0
- data/lib/reading/util/hash_compact_by_template.rb +1 -0
- data/lib/reading/util/hash_deep_merge.rb +1 -1
- data/lib/reading/util/hash_to_data.rb +30 -0
- data/lib/reading/util/numeric_to_i_if_whole.rb +12 -0
- data/lib/reading/util/string_truncate.rb +13 -4
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +49 -0
- metadata +76 -42
- data/lib/reading/attribute/all_attributes.rb +0 -83
- data/lib/reading/attribute/attribute.rb +0 -25
- data/lib/reading/attribute/experiences/dates_validator.rb +0 -94
- data/lib/reading/attribute/experiences/experiences_attribute.rb +0 -74
- data/lib/reading/attribute/experiences/progress_subattribute.rb +0 -48
- data/lib/reading/attribute/experiences/spans_subattribute.rb +0 -82
- data/lib/reading/attribute/variants/extra_info_subattribute.rb +0 -44
- data/lib/reading/attribute/variants/length_subattribute.rb +0 -45
- data/lib/reading/attribute/variants/series_subattribute.rb +0 -57
- data/lib/reading/attribute/variants/sources_subattribute.rb +0 -78
- data/lib/reading/attribute/variants/variants_attribute.rb +0 -69
- data/lib/reading/csv.rb +0 -76
- data/lib/reading/line.rb +0 -23
- data/lib/reading/row/blank_row.rb +0 -23
- data/lib/reading/row/compact_planned_row.rb +0 -130
- data/lib/reading/row/regular_row.rb +0 -99
- data/lib/reading/row/row.rb +0 -88
- data/lib/reading/util/hash_to_struct.rb +0 -29
@@ -1,130 +0,0 @@
|
|
1
|
-
require_relative "row"
|
2
|
-
require "debug"
|
3
|
-
|
4
|
-
module Reading
|
5
|
-
# Parses a row of compactly listed planned items into an array of hashes of
|
6
|
-
# item data.
|
7
|
-
class CompactPlannedRow < Row
|
8
|
-
using Util::StringRemove
|
9
|
-
using Util::HashDeepMerge
|
10
|
-
using Util::HashArrayDeepFetch
|
11
|
-
|
12
|
-
# Whether the given CSV line is a compact planned row.
|
13
|
-
# @param line [Reading::Line]
|
14
|
-
# @return [Boolean]
|
15
|
-
def self.match?(line)
|
16
|
-
comment_char = line.csv.config.deep_fetch(:csv, :comment_character)
|
17
|
-
|
18
|
-
line.string.strip.start_with?(comment_char) &&
|
19
|
-
line.string.match?(line.csv.config.deep_fetch(:csv, :regex, :compact_planned_row_start))
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def skip?
|
25
|
-
config.deep_fetch(:csv, :skip_compact_planned)
|
26
|
-
end
|
27
|
-
|
28
|
-
def before_parse
|
29
|
-
to_ignore = config.deep_fetch(:csv, :regex, :compact_planned_ignored_chars)
|
30
|
-
start_regex = config.deep_fetch(:csv, :regex, :compact_planned_row_start)
|
31
|
-
|
32
|
-
string_without_ignored_chars = string.remove_all(to_ignore)
|
33
|
-
start = string_without_ignored_chars.match(start_regex)
|
34
|
-
|
35
|
-
@genres = Array(start[:genres]&.downcase&.strip&.split(",")&.map(&:strip))
|
36
|
-
@sources = sources(start[:sources])
|
37
|
-
@row_without_genre = string_without_ignored_chars.remove(start.to_s)
|
38
|
-
end
|
39
|
-
|
40
|
-
def string_to_be_split_by_format_emojis
|
41
|
-
@row_without_genre
|
42
|
-
end
|
43
|
-
|
44
|
-
def item_hash(item_head)
|
45
|
-
item_match = item_head.match(config.deep_fetch(:csv, :regex, :compact_planned_item))
|
46
|
-
unless item_match
|
47
|
-
raise InvalidHeadError, "Title missing after #{item_head} in compact planned row"
|
48
|
-
end
|
49
|
-
|
50
|
-
author = AuthorAttribute.new(item_head: item_match[:author_title], config:).parse
|
51
|
-
|
52
|
-
begin
|
53
|
-
title = TitleAttribute.new(item_head: item_match[:author_title], config:).parse
|
54
|
-
rescue InvalidHeadError
|
55
|
-
raise InvalidHeadError, "Title missing after #{item_head} in compact planned row"
|
56
|
-
end
|
57
|
-
|
58
|
-
if item_match[:sources_column]
|
59
|
-
if item_match[:sources_column].include?(config.deep_fetch(:csv, :column_separator))
|
60
|
-
raise TooManyColumnsError, "Too many columns (only Sources allowed) " \
|
61
|
-
"after #{item_head} in compact planned row"
|
62
|
-
end
|
63
|
-
|
64
|
-
variants_attr = VariantsAttribute.new(
|
65
|
-
item_head: item_match[:format_emoji] + item_match[:author_title],
|
66
|
-
columns: { sources: item_match[:sources_column], length: nil },
|
67
|
-
config:,
|
68
|
-
)
|
69
|
-
variants = variants_attr.parse
|
70
|
-
else
|
71
|
-
variants = [parse_variant(item_match)]
|
72
|
-
end
|
73
|
-
|
74
|
-
template.deep_merge(
|
75
|
-
author: author || template.fetch(:author),
|
76
|
-
title: title,
|
77
|
-
genres: @genres.presence || template.fetch(:genres),
|
78
|
-
variants:,
|
79
|
-
)
|
80
|
-
end
|
81
|
-
|
82
|
-
def template
|
83
|
-
@template ||= config.deep_fetch(:item, :template)
|
84
|
-
end
|
85
|
-
|
86
|
-
def parse_variant(item_match)
|
87
|
-
item_head = item_match[:format_emoji] + item_match[:author_title]
|
88
|
-
series_attr = SeriesSubattribute.new(item_head:, config:)
|
89
|
-
extra_info_attr = ExtraInfoSubattribute.new(item_head:, config:)
|
90
|
-
sources = (@sources + sources(item_match[:sources])).uniq.presence
|
91
|
-
|
92
|
-
{
|
93
|
-
format: format(item_match[:format_emoji]),
|
94
|
-
series: series_attr.parse_head || template.deep_fetch(:variants, 0, :series),
|
95
|
-
sources: sources || template.deep_fetch(:variants, 0, :sources),
|
96
|
-
isbn: template.deep_fetch(:variants, 0, :isbn),
|
97
|
-
length: template.deep_fetch(:variants, 0, :length),
|
98
|
-
extra_info: extra_info_attr.parse_head || template.deep_fetch(:variants, 0, :extra_info),
|
99
|
-
}
|
100
|
-
end
|
101
|
-
|
102
|
-
def format(format_emoji)
|
103
|
-
config.deep_fetch(:item, :formats).key(format_emoji)
|
104
|
-
end
|
105
|
-
|
106
|
-
def sources(sources_str)
|
107
|
-
return [] if sources_str.nil?
|
108
|
-
|
109
|
-
sources_str
|
110
|
-
.split(config.deep_fetch(:csv, :compact_planned_source_prefix))
|
111
|
-
.map { |source| source.remove(/\s*,\s*/) }
|
112
|
-
.map(&:strip)
|
113
|
-
.reject(&:empty?)
|
114
|
-
.map { |source_name|
|
115
|
-
if valid_url?(source_name)
|
116
|
-
source_name = source_name.chop if source_name.chars.last == "/"
|
117
|
-
{ name: config.deep_fetch(:item, :sources, :default_name_for_url),
|
118
|
-
url: source_name }
|
119
|
-
else
|
120
|
-
{ name: source_name,
|
121
|
-
url: nil }
|
122
|
-
end
|
123
|
-
}
|
124
|
-
end
|
125
|
-
|
126
|
-
def valid_url?(str)
|
127
|
-
str&.match?(/http[^\s,]+/)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
@@ -1,99 +0,0 @@
|
|
1
|
-
require_relative "row"
|
2
|
-
require_relative "../attribute/all_attributes"
|
3
|
-
|
4
|
-
module Reading
|
5
|
-
# Parses a normal CSV row into an array of hashes of item data. Typically
|
6
|
-
# a normal row describes one item and so it's parsed into an array containing
|
7
|
-
# a single hash, but it's also possible for a row to describe multiple items.
|
8
|
-
class RegularRow < Row
|
9
|
-
using Util::HashArrayDeepFetch
|
10
|
-
|
11
|
-
private attr_reader :columns, :attribute_classes
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def after_initialize
|
16
|
-
set_attribute_classes
|
17
|
-
end
|
18
|
-
|
19
|
-
def before_parse
|
20
|
-
set_columns
|
21
|
-
ensure_head_column_present
|
22
|
-
end
|
23
|
-
|
24
|
-
def string_to_be_split_by_format_emojis
|
25
|
-
columns[:head]
|
26
|
-
end
|
27
|
-
|
28
|
-
def set_attribute_classes
|
29
|
-
@attribute_classes ||= config.deep_fetch(:item, :template).map { |attribute_name, _default|
|
30
|
-
attribute_name_camelcase = attribute_name.to_s.split("_").map(&:capitalize).join
|
31
|
-
attribute_class_name = "#{attribute_name_camelcase}Attribute"
|
32
|
-
attribute_class = self.class.const_get(attribute_class_name)
|
33
|
-
|
34
|
-
[attribute_name, attribute_class]
|
35
|
-
}.to_h
|
36
|
-
.merge(custom_attribute_classes)
|
37
|
-
end
|
38
|
-
|
39
|
-
def custom_attribute_classes
|
40
|
-
numeric = custom_attribute_classes_of_type(:numeric) do |value|
|
41
|
-
Float(value, exception: false)
|
42
|
-
end
|
43
|
-
|
44
|
-
text = custom_attribute_classes_of_type(:text) do |value|
|
45
|
-
value
|
46
|
-
end
|
47
|
-
|
48
|
-
(numeric + text).to_h
|
49
|
-
end
|
50
|
-
|
51
|
-
def custom_attribute_classes_of_type(type, &process_value)
|
52
|
-
config.deep_fetch(:csv, :"custom_#{type}_columns").map { |attribute, _default_value|
|
53
|
-
custom_class = Class.new(Attribute)
|
54
|
-
|
55
|
-
custom_class.define_method(:parse) do
|
56
|
-
value = columns[attribute.to_sym]&.strip&.presence
|
57
|
-
process_value.call(value)
|
58
|
-
end
|
59
|
-
|
60
|
-
[attribute.to_sym, custom_class]
|
61
|
-
}
|
62
|
-
end
|
63
|
-
|
64
|
-
def set_columns
|
65
|
-
column_names = config.deep_fetch(:csv, :enabled_columns) +
|
66
|
-
config.deep_fetch(:csv, :custom_numeric_columns).keys +
|
67
|
-
config.deep_fetch(:csv, :custom_text_columns).keys
|
68
|
-
|
69
|
-
columns_count = string.count(config.deep_fetch(:csv, :column_separator))
|
70
|
-
if columns_count >= column_names.count
|
71
|
-
raise TooManyColumnsError, "Too many columns"
|
72
|
-
end
|
73
|
-
|
74
|
-
column_contents = string.split(config.deep_fetch(:csv, :column_separator))
|
75
|
-
|
76
|
-
@columns = column_names.zip(column_contents).to_h
|
77
|
-
end
|
78
|
-
|
79
|
-
def ensure_head_column_present
|
80
|
-
if columns[:head].nil? || columns[:head].strip.empty?
|
81
|
-
raise InvalidHeadError, "The Head column must not be blank"
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def item_hash(item_head)
|
86
|
-
config
|
87
|
-
.deep_fetch(:item, :template)
|
88
|
-
.merge(config.deep_fetch(:csv, :custom_numeric_columns))
|
89
|
-
.merge(config.deep_fetch(:csv, :custom_text_columns))
|
90
|
-
.map { |attribute_name, default_value|
|
91
|
-
attribute_class = attribute_classes.fetch(attribute_name)
|
92
|
-
attribute_parser = attribute_class.new(item_head:, columns:, config:)
|
93
|
-
parsed = attribute_parser.parse
|
94
|
-
|
95
|
-
[attribute_name, parsed || default_value]
|
96
|
-
}.to_h
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
data/lib/reading/row/row.rb
DELETED
@@ -1,88 +0,0 @@
|
|
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
|
@@ -1,29 +0,0 @@
|
|
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
|