reading 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|