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.
- checksums.yaml +7 -0
- data/bin/reading +31 -0
- data/lib/reading/attribute/all_attributes.rb +83 -0
- data/lib/reading/attribute/attribute.rb +25 -0
- data/lib/reading/attribute/experiences/dates_validator.rb +94 -0
- data/lib/reading/attribute/experiences/experiences_attribute.rb +74 -0
- data/lib/reading/attribute/experiences/progress_subattribute.rb +48 -0
- data/lib/reading/attribute/experiences/spans_subattribute.rb +82 -0
- data/lib/reading/attribute/variants/extra_info_subattribute.rb +44 -0
- data/lib/reading/attribute/variants/length_subattribute.rb +45 -0
- data/lib/reading/attribute/variants/series_subattribute.rb +57 -0
- data/lib/reading/attribute/variants/sources_subattribute.rb +78 -0
- data/lib/reading/attribute/variants/variants_attribute.rb +69 -0
- data/lib/reading/config.rb +202 -0
- data/lib/reading/csv.rb +67 -0
- data/lib/reading/errors.rb +77 -0
- data/lib/reading/line.rb +23 -0
- data/lib/reading/row/blank_row.rb +23 -0
- data/lib/reading/row/compact_planned_row.rb +130 -0
- data/lib/reading/row/regular_row.rb +94 -0
- data/lib/reading/row/row.rb +88 -0
- data/lib/reading/util/blank.rb +146 -0
- data/lib/reading/util/hash_array_deep_fetch.rb +40 -0
- data/lib/reading/util/hash_compact_by_template.rb +38 -0
- data/lib/reading/util/hash_deep_merge.rb +44 -0
- data/lib/reading/util/hash_to_struct.rb +29 -0
- data/lib/reading/util/string_remove.rb +28 -0
- data/lib/reading/util/string_truncate.rb +13 -0
- data/lib/reading/version.rb +3 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 70825fd8a882da595b06db4f8a0766962dd71b8149248ada3ed55280d68b2975
|
4
|
+
data.tar.gz: 124c660e9888c712a8bedc05156700b3e233300a8fe9806dd36ec168e910bfea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 174a7a35761e5a26569651bee4e7ddefd0bbe8592b3828df34cb9f465d0a734876a57bb0fecb04683c470567a0df151289c375213cdf7ad67d632d8d06d02d10
|
7
|
+
data.tar.gz: 0cec333e90f2a701521d06c4924083e137268112c1a2749effb17b952b3028a4f4deae9ababd9ce1592eb0a21ba60afac75c0e18f81762af936306d86f2adb82
|
data/bin/reading
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# A script that provides a quick way to see the output of a CSV string.
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
# Run on the command line:
|
7
|
+
# reading "<CSV string>" "<optional comma-separated names of enabled columns>`
|
8
|
+
#
|
9
|
+
# Examples:
|
10
|
+
# reading '3|📕Trying|Lexpub 1970147288'
|
11
|
+
# reading '📕Trying|Lexpub 1970147288' 'head, sources'
|
12
|
+
|
13
|
+
|
14
|
+
require_relative "../lib/reading/csv"
|
15
|
+
require "amazing_print"
|
16
|
+
|
17
|
+
input = ARGV[0]
|
18
|
+
unless input
|
19
|
+
raise ArgumentError, "CSV string argument required, such as '3|📕Trying|Lexpub 1970147288'"
|
20
|
+
end
|
21
|
+
|
22
|
+
config = {}
|
23
|
+
if ARGV[1]
|
24
|
+
enabled_columns = ARGV[1].split(",").map(&:strip).map(&:to_sym)
|
25
|
+
config[:csv] = { enabled_columns: }
|
26
|
+
end
|
27
|
+
|
28
|
+
csv = Reading::CSV.new(input, config:)
|
29
|
+
items = csv.parse
|
30
|
+
|
31
|
+
ap items
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_relative "attribute"
|
2
|
+
require_relative "variants/variants_attribute"
|
3
|
+
require_relative "experiences/experiences_attribute"
|
4
|
+
|
5
|
+
module Reading
|
6
|
+
class Row
|
7
|
+
using Util::StringRemove
|
8
|
+
using Util::HashArrayDeepFetch
|
9
|
+
|
10
|
+
# The simpler attributes are collected below. The more complex attributes
|
11
|
+
# are separated into their own files.
|
12
|
+
|
13
|
+
class RatingAttribute < Attribute
|
14
|
+
def parse
|
15
|
+
return nil unless columns[:rating]
|
16
|
+
|
17
|
+
rating = columns[:rating].strip
|
18
|
+
return nil if rating.empty?
|
19
|
+
|
20
|
+
Integer(rating, exception: false) ||
|
21
|
+
Float(rating, exception: false) ||
|
22
|
+
(raise InvalidRatingError, "Invalid rating")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class AuthorAttribute < Attribute
|
27
|
+
def parse
|
28
|
+
item_head
|
29
|
+
.remove(/\A#{config.deep_fetch(:csv, :regex, :formats)}/)
|
30
|
+
.match(/.+(?=#{config.deep_fetch(:csv, :short_separator)})/)
|
31
|
+
&.to_s
|
32
|
+
&.strip
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class TitleAttribute < Attribute
|
37
|
+
def parse
|
38
|
+
if item_head.end_with?(config.deep_fetch(:csv, :short_separator).rstrip)
|
39
|
+
raise InvalidHeadError, "Missing title? Head column ends in a separator"
|
40
|
+
end
|
41
|
+
|
42
|
+
item_head
|
43
|
+
.remove(/\A#{config.deep_fetch(:csv, :regex, :formats)}/)
|
44
|
+
.remove(/.+#{config.deep_fetch(:csv, :short_separator)}/)
|
45
|
+
.remove(/#{config.deep_fetch(:csv, :long_separator)}.+\z/)
|
46
|
+
.strip
|
47
|
+
.presence || (raise InvalidHeadError, "Missing title")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class GenresAttribute < Attribute
|
52
|
+
def parse
|
53
|
+
return nil unless columns[:genres]
|
54
|
+
|
55
|
+
columns[:genres]
|
56
|
+
.split(config.deep_fetch(:csv, :separator))
|
57
|
+
.map(&:strip)
|
58
|
+
.map(&:downcase)
|
59
|
+
.map(&:presence)
|
60
|
+
.compact.presence
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class NotesAttribute < Attribute
|
65
|
+
def parse
|
66
|
+
return nil unless columns[:public_notes]
|
67
|
+
|
68
|
+
columns[:public_notes]
|
69
|
+
.presence
|
70
|
+
&.chomp
|
71
|
+
&.remove(/#{config.deep_fetch(:csv, :long_separator).rstrip}\s*\z/)
|
72
|
+
&.split(config.deep_fetch(:csv, :long_separator))
|
73
|
+
&.map { |string|
|
74
|
+
{
|
75
|
+
blurb?: !!string.delete!(config.deep_fetch(:csv, :blurb_emoji)),
|
76
|
+
private?: !!string.delete!(config.deep_fetch(:csv, :private_emoji)),
|
77
|
+
content: string.strip,
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
# A base class that contains behaviors common to ___Attribute classes.
|
4
|
+
class Attribute
|
5
|
+
private attr_reader :item_head, :columns, :config
|
6
|
+
|
7
|
+
# @param item_head [String] see Row#item_heads for a definition.
|
8
|
+
# @param columns [Array<String>] the CSV row split into columns.
|
9
|
+
# @param config [Hash]
|
10
|
+
def initialize(item_head: nil, columns: nil, config:)
|
11
|
+
unless item_head || columns
|
12
|
+
raise ArgumentError, "Either item_head or columns must be given to an Attribute."
|
13
|
+
end
|
14
|
+
|
15
|
+
@item_head = item_head
|
16
|
+
@columns = columns
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse
|
21
|
+
raise NotImplementedError, "#{self.class} should have implemented #{__method__}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Reading
|
2
|
+
# Methods to validate dates. This does not cover all the ways dates can be
|
3
|
+
# invalid, just the ones not covered by ExperiencesAttribute during parsing.
|
4
|
+
module DatesValidator
|
5
|
+
using Util::HashArrayDeepFetch
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# Checks the dates in the given experiences hash, and raises an error at
|
9
|
+
# the first invalid date found.
|
10
|
+
# @param experiences [Array<Hash>]
|
11
|
+
# @param config [Hash]
|
12
|
+
def validate(experiences, config)
|
13
|
+
validate_dates_started_are_in_order(experiences) if dates_started_column?(config)
|
14
|
+
validate_dates_finished_are_in_order(experiences) if dates_finished_column?(config)
|
15
|
+
validate_experiences_of_same_variant_do_not_overlap(experiences) if both_date_columns?(config)
|
16
|
+
validate_spans_are_in_order_and_not_overlapping(experiences)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def dates_started_column?(config)
|
22
|
+
config.deep_fetch(:csv, :enabled_columns).include?(:dates_started)
|
23
|
+
end
|
24
|
+
|
25
|
+
def dates_finished_column?(config)
|
26
|
+
config.deep_fetch(:csv, :enabled_columns).include?(:dates_finished)
|
27
|
+
end
|
28
|
+
|
29
|
+
def both_date_columns?(config)
|
30
|
+
dates_started_column?(config) && dates_finished_column?(config)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_dates_started_are_in_order(experiences)
|
34
|
+
experiences
|
35
|
+
.filter { |exp| exp[:spans].any? }
|
36
|
+
.map { |exp| exp[:spans].first[:dates].begin }
|
37
|
+
.each_cons(2) do |a, b|
|
38
|
+
if (a.nil? && b.nil?) || (a && b && a > b )
|
39
|
+
raise InvalidDateError, "Dates started are not in order"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_dates_finished_are_in_order(experiences)
|
45
|
+
experiences
|
46
|
+
.filter { |exp| exp[:spans].any? }
|
47
|
+
.map { |exp| exp[:spans].last[:dates].end }
|
48
|
+
.each_cons(2) do |a, b|
|
49
|
+
if (a.nil? && b.nil?) || (a && b && a > b )
|
50
|
+
raise InvalidDateError, "Dates finished are not in order"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_experiences_of_same_variant_do_not_overlap(experiences)
|
56
|
+
experiences
|
57
|
+
.group_by { |exp| exp[:variant_index] }
|
58
|
+
.each do |_variant_index, exps|
|
59
|
+
exps.filter { |exp| exp[:spans].any? }.each_cons(2) do |a, b|
|
60
|
+
a_metaspan = a[:spans].first[:dates].begin..a[:spans].last[:dates].end
|
61
|
+
b_metaspan = b[:spans].first[:dates].begin..b[:spans].last[:dates].end
|
62
|
+
if a_metaspan.cover?(b_metaspan.begin || a_metaspan.begin || a_metaspan.end) ||
|
63
|
+
b_metaspan.cover?(a_metaspan.begin || b_metaspan.begin || b_metaspan.end)
|
64
|
+
raise InvalidDateError, "Experiences are overlapping"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_spans_are_in_order_and_not_overlapping(experiences)
|
71
|
+
experiences
|
72
|
+
.filter { |exp| exp[:spans].any? }
|
73
|
+
.each do |exp|
|
74
|
+
exp[:spans]
|
75
|
+
.map { |span| span[:dates] }
|
76
|
+
.each do |dates|
|
77
|
+
if dates.begin && dates.end && dates.begin > dates.end
|
78
|
+
raise InvalidDateError, "A date range is backward"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
.each_cons(2) do |a, b|
|
82
|
+
if a.begin > b.begin || a.end > b.end
|
83
|
+
raise InvalidDateError, "Dates are not in order"
|
84
|
+
end
|
85
|
+
if a.cover?(b.begin || a.begin || a.end) ||
|
86
|
+
b.cover?(a.begin || b.begin || b.end)
|
87
|
+
raise InvalidDateError, "Dates are overlapping"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require_relative "spans_subattribute"
|
2
|
+
require_relative "progress_subattribute"
|
3
|
+
require_relative "dates_validator"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module Reading
|
7
|
+
class Row
|
8
|
+
class ExperiencesAttribute < Attribute
|
9
|
+
using Util::HashArrayDeepFetch
|
10
|
+
using Util::HashDeepMerge
|
11
|
+
|
12
|
+
def parse
|
13
|
+
started, finished = dates_split(columns)
|
14
|
+
|
15
|
+
experiences_with_dates = started.map.with_index { |entry, i|
|
16
|
+
variant_index = variant_index(entry)
|
17
|
+
spans_attr = SpansSubattribute.new(date_entry: entry, dates_finished: finished, date_index: i, variant_index:, columns:, config:)
|
18
|
+
|
19
|
+
{
|
20
|
+
spans: spans_attr.parse || template.fetch(:spans),
|
21
|
+
group: group(entry) || template.fetch(:group),
|
22
|
+
variant_index: variant_index || template.fetch(:variant_index)
|
23
|
+
}
|
24
|
+
}.presence
|
25
|
+
|
26
|
+
if experiences_with_dates
|
27
|
+
# Raises an error if any sequence of dates does not make sense.
|
28
|
+
DatesValidator.validate(experiences_with_dates, config)
|
29
|
+
|
30
|
+
return experiences_with_dates
|
31
|
+
else
|
32
|
+
if prog = ProgressSubattribute.new(columns:, config:).parse_head
|
33
|
+
return [template.deep_merge(spans: [{ progress: prog }] )]
|
34
|
+
else
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def template
|
43
|
+
@template ||= config.deep_fetch(:item, :template, :experiences).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def dates_split(columns)
|
47
|
+
dates_finished = columns[:dates_finished]&.presence
|
48
|
+
&.split(config.deep_fetch(:csv, :separator))&.map(&:strip) || []
|
49
|
+
# Don't use #has_key? because simply checking for nil covers the
|
50
|
+
# case where dates_started is the last column and omitted.
|
51
|
+
started_column_exists = columns[:dates_started]&.presence
|
52
|
+
|
53
|
+
dates_started =
|
54
|
+
if started_column_exists
|
55
|
+
columns[:dates_started]&.presence&.split(config.deep_fetch(:csv, :separator))&.map(&:strip)
|
56
|
+
else
|
57
|
+
[""] * dates_finished.count
|
58
|
+
end
|
59
|
+
|
60
|
+
[dates_started, dates_finished]
|
61
|
+
end
|
62
|
+
|
63
|
+
def group(entry)
|
64
|
+
entry.match(config.deep_fetch(:csv, :regex, :group_experience))&.captures&.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def variant_index(date_entry)
|
68
|
+
match = date_entry.match(config.deep_fetch(:csv, :regex, :variant_index))
|
69
|
+
|
70
|
+
(match&.captures&.first&.to_i || 1) - 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class ProgressSubattribute
|
4
|
+
using Util::HashArrayDeepFetch
|
5
|
+
|
6
|
+
private attr_reader :date_entry, :variant_index, :columns, :config
|
7
|
+
|
8
|
+
# @param date_entry [String] the entry in Dates Started.
|
9
|
+
# @param variant_index [Integer] the variant index, for getting length for default amount.
|
10
|
+
# @param columns [Array<String>]
|
11
|
+
# @param config [Hash]
|
12
|
+
def initialize(date_entry: nil, variant_index: nil, columns:, config:)
|
13
|
+
@date_entry = date_entry
|
14
|
+
@variant_index = variant_index
|
15
|
+
@columns = columns
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
progress(date_entry) || progress(columns[:head])
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_head
|
24
|
+
progress(columns[:head])
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def progress(str)
|
30
|
+
prog = str.match(config.deep_fetch(:csv, :regex, :progress))
|
31
|
+
|
32
|
+
if prog
|
33
|
+
if prog_percent = prog[:percent]&.to_i
|
34
|
+
return prog_percent / 100.0
|
35
|
+
elsif prog_time = prog[:time]
|
36
|
+
return prog_time
|
37
|
+
elsif prog_pages = prog[:pages]&.to_i
|
38
|
+
return prog_pages
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
dnf = str.match(config.deep_fetch(:csv, :regex, :dnf))&.captures&.first
|
43
|
+
return 0 if dnf
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class SpansSubattribute
|
4
|
+
using Util::HashArrayDeepFetch
|
5
|
+
|
6
|
+
private attr_reader :date_entry, :dates_finished, :date_index, :variant_index, :columns, :config
|
7
|
+
|
8
|
+
# @param date_entry [String] the entry in Dates Started.
|
9
|
+
# @param dates_finished [Array<String>] the entries in Dates Finished.
|
10
|
+
# @param date_index [Integer] the index of the entry.
|
11
|
+
# @param variant_index [Integer] the variant index, for getting length for default amount.
|
12
|
+
# @param columns [Array<String>]
|
13
|
+
# @param config [Hash]
|
14
|
+
def initialize(date_entry:, dates_finished:, date_index:, variant_index:, columns:, config:)
|
15
|
+
@date_entry = date_entry
|
16
|
+
@dates_finished = dates_finished
|
17
|
+
@date_index = date_index
|
18
|
+
@variant_index = variant_index
|
19
|
+
@columns = columns
|
20
|
+
@config = config
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse
|
24
|
+
started = date_started(date_entry)
|
25
|
+
finished = date_finished(dates_finished, date_index)
|
26
|
+
return [] if started.nil? && finished.nil?
|
27
|
+
|
28
|
+
progress_attr = ProgressSubattribute.new(date_entry:, variant_index:, columns:, config:)
|
29
|
+
progress = progress_attr.parse
|
30
|
+
|
31
|
+
[{
|
32
|
+
dates: started..finished || template.fetch(:dates),
|
33
|
+
amount: length || template.fetch(:amount),
|
34
|
+
progress: progress || (1.0 if finished) || template.fetch(:progress),
|
35
|
+
name: template.fetch(:name),
|
36
|
+
favorite?: template.fetch(:favorite?),
|
37
|
+
}]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def template
|
43
|
+
@template ||= config.deep_fetch(:item, :template, :experiences, 0, :spans).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def date_started(date_entry)
|
47
|
+
dates = date_entry.scan(config.deep_fetch(:csv, :regex, :date))
|
48
|
+
raise InvalidDateError, "Conjoined dates" if dates.count > 1
|
49
|
+
raise InvalidDateError, "Missing or incomplete date" if date_entry.present? && dates.empty?
|
50
|
+
|
51
|
+
date_str = dates.first
|
52
|
+
Date.parse(date_str) if date_str
|
53
|
+
rescue Date::Error
|
54
|
+
raise InvalidDateError, "Unparsable date"
|
55
|
+
end
|
56
|
+
|
57
|
+
def date_finished(dates_finished, date_index)
|
58
|
+
return nil if dates_finished.nil?
|
59
|
+
|
60
|
+
date_str = dates_finished[date_index]&.presence
|
61
|
+
Date.parse(date_str) if date_str
|
62
|
+
rescue Date::Error
|
63
|
+
if date_str.match?(config.deep_fetch(:csv, :regex, :date))
|
64
|
+
raise InvalidDateError, "Unparsable date"
|
65
|
+
else
|
66
|
+
raise InvalidDateError, "Missing or incomplete date"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def length
|
71
|
+
sources_str = columns[:sources]&.presence || " "
|
72
|
+
bare_variant = sources_str
|
73
|
+
.split(config.deep_fetch(:csv, :regex, :formats_split))
|
74
|
+
.dig(variant_index)
|
75
|
+
&.split(config.deep_fetch(:csv, :long_separator))
|
76
|
+
&.first
|
77
|
+
length_attr = LengthSubattribute.new(bare_variant:, columns:, config:)
|
78
|
+
length_attr.parse
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class ExtraInfoSubattribute
|
4
|
+
using Util::HashArrayDeepFetch
|
5
|
+
|
6
|
+
private attr_reader :item_head, :variant_with_extras, :config
|
7
|
+
|
8
|
+
# @param item_head [String] see Row#item_heads for a definition.
|
9
|
+
# @param variant_with_extras [String] the full variant string.
|
10
|
+
# @param config [Hash]
|
11
|
+
def initialize(item_head:, variant_with_extras: nil, config:)
|
12
|
+
@item_head = item_head
|
13
|
+
@variant_with_extras = variant_with_extras
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
(
|
19
|
+
Array(extra_info(item_head)) +
|
20
|
+
Array(extra_info(variant_with_extras))
|
21
|
+
).presence
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_head
|
25
|
+
extra_info(item_head)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def template
|
31
|
+
config.deep_fetch(:item, :template, :variants, 0, :series).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def extra_info(str)
|
35
|
+
separated = str.split(config.deep_fetch(:csv, :long_separator))
|
36
|
+
separated.delete_at(0) # everything before the extra info
|
37
|
+
separated.reject { |str|
|
38
|
+
str.start_with?("#{config.deep_fetch(:csv, :series_prefix)} ") ||
|
39
|
+
str.match(config.deep_fetch(:csv, :regex, :series_volume))
|
40
|
+
}.presence
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class LengthSubattribute
|
4
|
+
using Util::HashArrayDeepFetch
|
5
|
+
|
6
|
+
private attr_reader :item_head, :bare_variant, :columns, :config
|
7
|
+
|
8
|
+
# @param bare_variant [String] the variant string before series / extra info.
|
9
|
+
# @param columns [Array<String>]
|
10
|
+
# @param config [Hash]
|
11
|
+
def initialize(bare_variant:, columns:, config:)
|
12
|
+
@bare_variant = bare_variant
|
13
|
+
@columns = columns
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
in_variant = length_in(
|
19
|
+
bare_variant,
|
20
|
+
time_regex: config.deep_fetch(:csv, :regex, :time_length_in_variant),
|
21
|
+
pages_regex: config.deep_fetch(:csv, :regex, :pages_length_in_variant),
|
22
|
+
)
|
23
|
+
in_length = length_in(
|
24
|
+
columns[:length],
|
25
|
+
time_regex: config.deep_fetch(:csv, :regex, :time_length),
|
26
|
+
pages_regex: config.deep_fetch(:csv, :regex, :pages_length),
|
27
|
+
)
|
28
|
+
|
29
|
+
in_variant || in_length ||
|
30
|
+
(raise InvalidLengthError, "Missing length" unless columns[:length].blank?)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def length_in(str, time_regex:, pages_regex:)
|
36
|
+
return nil if str.blank?
|
37
|
+
|
38
|
+
time_length = str.strip.match(time_regex)&.captures&.first
|
39
|
+
return time_length unless time_length.nil?
|
40
|
+
|
41
|
+
str.strip.match(pages_regex)&.captures&.first&.to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class SeriesSubattribute
|
4
|
+
using Util::HashArrayDeepFetch
|
5
|
+
|
6
|
+
private attr_reader :item_head, :variant_with_extras, :config
|
7
|
+
|
8
|
+
# @param item_head [String] see Row#item_heads for a definition.
|
9
|
+
# @param variant_with_extras [String] the full variant string.
|
10
|
+
# @param config [Hash]
|
11
|
+
def initialize(item_head:, variant_with_extras: nil, config:)
|
12
|
+
@item_head = item_head
|
13
|
+
@variant_with_extras = variant_with_extras
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
(
|
19
|
+
Array(series(item_head)) +
|
20
|
+
Array(series(variant_with_extras))
|
21
|
+
).presence
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_head
|
25
|
+
series(item_head)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def template
|
31
|
+
config.deep_fetch(:item, :template, :variants, 0, :series).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def series(str)
|
35
|
+
separated = str
|
36
|
+
.split(config.deep_fetch(:csv, :long_separator))
|
37
|
+
.map(&:strip)
|
38
|
+
.map(&:presence)
|
39
|
+
.compact
|
40
|
+
|
41
|
+
separated.delete_at(0) # everything before the series/extra info
|
42
|
+
|
43
|
+
separated.map { |str|
|
44
|
+
volume = str.match(config.deep_fetch(:csv, :regex, :series_volume))
|
45
|
+
prefix = "#{config.deep_fetch(:csv, :series_prefix)} "
|
46
|
+
|
47
|
+
if volume || str.start_with?(prefix)
|
48
|
+
{
|
49
|
+
name: str.delete_suffix(volume.to_s).delete_prefix(prefix) || template[:name],
|
50
|
+
volume: volume&.captures&.first&.to_i || template[:volume],
|
51
|
+
}
|
52
|
+
end
|
53
|
+
}.compact.presence
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Reading
|
2
|
+
class Row
|
3
|
+
class SourcesSubattribute
|
4
|
+
using Util::StringRemove
|
5
|
+
using Util::HashArrayDeepFetch
|
6
|
+
|
7
|
+
private attr_reader :item_head, :bare_variant, :config
|
8
|
+
|
9
|
+
# @param bare_variant [String] the variant string before series / extra info.
|
10
|
+
# @param config [Hash]
|
11
|
+
def initialize(bare_variant:, config:)
|
12
|
+
@bare_variant = bare_variant
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
urls = sources_urls(bare_variant).map { |url|
|
18
|
+
{
|
19
|
+
name: url_name(url) || template.deep_fetch(:sources, 0, :name),
|
20
|
+
url: url,
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
names = sources_names(bare_variant).map { |name|
|
25
|
+
{
|
26
|
+
name: name,
|
27
|
+
url: template.deep_fetch(:sources, 0, :url),
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
(urls + names).presence
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def template
|
37
|
+
@template ||= config.deep_fetch(:item, :template, :variants).first
|
38
|
+
end
|
39
|
+
|
40
|
+
def sources_urls(str)
|
41
|
+
str.scan(config.deep_fetch(:csv, :regex, :url))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Turns everything that is not a source name (ISBN, source URL, length) into
|
45
|
+
# a separator, then splits by that separator and removes empty elements
|
46
|
+
# and format emojis. What's left is source names.
|
47
|
+
def sources_names(str)
|
48
|
+
not_names = [:isbn, :url, :time_length_in_variant, :pages_length_in_variant]
|
49
|
+
names_and_separators = str
|
50
|
+
|
51
|
+
not_names.each do |regex_type|
|
52
|
+
names_and_separators = names_and_separators.gsub(
|
53
|
+
config.deep_fetch(:csv, :regex, regex_type),
|
54
|
+
config.deep_fetch(:csv, :separator),
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
names_and_separators
|
59
|
+
.split(config.deep_fetch(:csv, :separator))
|
60
|
+
.map { |name| name.remove(/\A\s*#{config.deep_fetch(:csv, :regex, :formats)}\s*/) }
|
61
|
+
.map(&:strip)
|
62
|
+
.reject(&:empty?)
|
63
|
+
end
|
64
|
+
|
65
|
+
def url_name(url)
|
66
|
+
config
|
67
|
+
.deep_fetch(:item, :sources, :names_from_urls)
|
68
|
+
.each do |url_part, name|
|
69
|
+
if url.include?(url_part)
|
70
|
+
return name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
config.deep_fetch(:item, :sources, :default_name_for_url)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|