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
@@ -0,0 +1,131 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
# The base class for all the columns in parsing/rows/compact_planned_columns
|
5
|
+
# and parsing/rows/regular_columns.
|
6
|
+
class Column
|
7
|
+
# The class name changed into a string, e.g. StartDates => "Start Dates"
|
8
|
+
# @return [String]
|
9
|
+
def self.column_name
|
10
|
+
class_name = name.split("::").last
|
11
|
+
class_name.gsub(/(.)([A-Z])/,'\1 \2')
|
12
|
+
end
|
13
|
+
|
14
|
+
# The class name changed into a symbol, e.g. StartDates => :start_dates
|
15
|
+
# @return [Symbol]
|
16
|
+
def self.to_sym
|
17
|
+
class_name = name.split("::").last
|
18
|
+
class_name
|
19
|
+
.gsub(/(.)([A-Z])/,'\1_\2')
|
20
|
+
.downcase
|
21
|
+
.to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
# Whether the column can contain "chunks" each set off by a format emoji.
|
25
|
+
# For example, the Head column of a compact planned row typically
|
26
|
+
# contains a list of multiple items. (The two others are the Sources
|
27
|
+
# column, for multiple variants of an item; and the regular Head column,
|
28
|
+
# for multiple items.)
|
29
|
+
# @return [Boolean]
|
30
|
+
def self.split_by_format?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
# Whether the column can contain multiple segments, e.g. "Cosmos -- 2013 paperback"
|
35
|
+
# @return [Boolean]
|
36
|
+
def self.split_by_segment?
|
37
|
+
!!segment_separator
|
38
|
+
end
|
39
|
+
|
40
|
+
# The regular expression used to split segments (e.g. /\s*--\s*/),
|
41
|
+
# or nil if the column should not be split by segment.
|
42
|
+
# @return [Regexp, nil]
|
43
|
+
def self.segment_separator
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Whether the column can contain multiple segment groups, e.g.
|
48
|
+
# "2021/1/28..2/1 x4 -- ..2/3 x5 ---- 11/1 -- 11/2"
|
49
|
+
# @return [Boolean]
|
50
|
+
def self.split_by_segment_group?
|
51
|
+
!!segment_group_separator
|
52
|
+
end
|
53
|
+
|
54
|
+
# The regular expression used to split segment groups (e.g. /\s*----\s*/),
|
55
|
+
# or nil if the column should not be split by segment group.
|
56
|
+
# @return [Regexp, nil]
|
57
|
+
def self.segment_group_separator
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adjustments that are made to captured values at the end of parsing
|
62
|
+
# the column. For example, if ::regexes includes a capture group named
|
63
|
+
# "sources" and it needs to be split by commas:
|
64
|
+
# { sources: -> { _1.split(/\s*,\s*/) } }
|
65
|
+
# @return [Hash{Symbol => Proc}]
|
66
|
+
def self.tweaks
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Keys in the parsed output hash that should be converted to an array, even
|
71
|
+
# if only one value was in the input, as in { ... extra_info: ["ed. Jane Doe"] }
|
72
|
+
# @return [Array<Symbol>]
|
73
|
+
def self.flatten_into_arrays
|
74
|
+
[]
|
75
|
+
end
|
76
|
+
|
77
|
+
# The regular expressions used to parse the column (except the part of
|
78
|
+
# the column before the first format emoji, which is in
|
79
|
+
# ::regexes_before_formats below). An array because sometimes it's
|
80
|
+
# simpler to try several smaller regular expressions in series, and
|
81
|
+
# because a regular expression might be applicable only for segments in
|
82
|
+
# a certain position. See parsing/rows/regular_columns/head.rb for an example.
|
83
|
+
# @param segment_index [Integer] the position of the current segment.
|
84
|
+
# @return [Array<Regexp>]
|
85
|
+
def self.regexes(segment_index)
|
86
|
+
[]
|
87
|
+
end
|
88
|
+
|
89
|
+
# The regular expressions used to parse the part of the column before
|
90
|
+
# the first format emoji.
|
91
|
+
# @return [Array<Regexp>]
|
92
|
+
def self.regexes_before_formats
|
93
|
+
[]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Regular expressions that are shared across more than one column,
|
97
|
+
# placed here just to be DRY.
|
98
|
+
SHARED_REGEXES = {
|
99
|
+
progress: %r{
|
100
|
+
(DNF\s+)?(?<progress_percent>\d\d?)%
|
101
|
+
|
|
102
|
+
(DNF\s+)?p?(?<progress_pages>\d+)p?
|
103
|
+
|
|
104
|
+
(DNF\s+)?(?<progress_time>\d+:\d\d)
|
105
|
+
|
|
106
|
+
# just DNF
|
107
|
+
(?<progress_dnf>DNF)
|
108
|
+
}x,
|
109
|
+
series_and_extra_info: [
|
110
|
+
# just series
|
111
|
+
%r{\A
|
112
|
+
in\s(?<series_names>.+)
|
113
|
+
# empty volume so that names and volumes have equal sizes when turned into arrays
|
114
|
+
(?<series_volumes>)
|
115
|
+
\z}x,
|
116
|
+
# series and volume
|
117
|
+
%r{\A
|
118
|
+
(?<series_names>.+?)
|
119
|
+
,?\s*
|
120
|
+
\#(?<series_volumes>\d+)
|
121
|
+
\z}x,
|
122
|
+
# extra info
|
123
|
+
%r{\A
|
124
|
+
(?<extra_info>.+)
|
125
|
+
\z}x,
|
126
|
+
],
|
127
|
+
}.freeze
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
# A row that is a comment.
|
5
|
+
module Comment
|
6
|
+
using Util::HashArrayDeepFetch
|
7
|
+
|
8
|
+
# No columns; comments are 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 does not include any format emojis.
|
15
|
+
# (Commented rows that DO include format emojis are matched as compact
|
16
|
+
# planned rows.)
|
17
|
+
# @param row_string [String]
|
18
|
+
# @param config [Hash]
|
19
|
+
# @return [Boolean]
|
20
|
+
def self.match?(row_string, config)
|
21
|
+
row_string.lstrip.start_with?(config.fetch(:comment_character))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative "column"
|
2
|
+
require_relative "compact_planned_columns/head"
|
3
|
+
require_relative "regular_columns/sources"
|
4
|
+
|
5
|
+
module Reading
|
6
|
+
module Parsing
|
7
|
+
module Rows
|
8
|
+
# A row that contains compact planned items.
|
9
|
+
module CompactPlanned
|
10
|
+
using Util::HashArrayDeepFetch
|
11
|
+
|
12
|
+
# The columns that are possible in this type of row.
|
13
|
+
# @return [Array<Class>]
|
14
|
+
def self.column_classes
|
15
|
+
[CompactPlanned::Head, Regular::Sources]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Starts with a comment character and includes one or more format emojis.
|
19
|
+
# @param row_string [String]
|
20
|
+
# @param config [Hash]
|
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
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module CompactPlanned
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#compact-planned-items
|
6
|
+
# and the sections following.
|
7
|
+
class Head < Column
|
8
|
+
def self.split_by_format?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.regexes_before_formats
|
13
|
+
[
|
14
|
+
%r{\A
|
15
|
+
\\ # comment character
|
16
|
+
\s*
|
17
|
+
(
|
18
|
+
(?<genres>[^a-z]+)?
|
19
|
+
\s*
|
20
|
+
(?<sources>@.+)?
|
21
|
+
\s*:
|
22
|
+
)?
|
23
|
+
\z}x,
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.segment_separator
|
28
|
+
/\s*--\s*/
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.flatten_into_arrays
|
32
|
+
%i[extra_info series_names series_volumes]
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.tweaks
|
36
|
+
{
|
37
|
+
genres: -> { _1.downcase.split(/\s*,\s*/) },
|
38
|
+
sources: -> { _1.split(/\s*@/).map(&:presence).compact }
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.regexes(segment_index)
|
43
|
+
[
|
44
|
+
# author, title, sources
|
45
|
+
(%r{\A
|
46
|
+
(
|
47
|
+
(?<author>[^@]+?)
|
48
|
+
\s+-\s+
|
49
|
+
)?
|
50
|
+
(?<title>[^@]+)
|
51
|
+
(?<sources>@.+)?
|
52
|
+
\z}x if segment_index.zero?),
|
53
|
+
*Column::SHARED_REGEXES[:series_and_extra_info],
|
54
|
+
].compact
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
|
12
|
+
module Reading
|
13
|
+
module Parsing
|
14
|
+
module Rows
|
15
|
+
# A normal row of (usually) one item.
|
16
|
+
module Regular
|
17
|
+
# The columns that are possible in this type of row.
|
18
|
+
# @return [Array<Class>]
|
19
|
+
def self.column_classes
|
20
|
+
[Rating, Head, Sources, StartDates, EndDates, Genres, Length, Notes, History]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Does not start with a comment character.
|
24
|
+
# @param row_string [String]
|
25
|
+
# @param config [Hash]
|
26
|
+
# @return [Boolean]
|
27
|
+
def self.match?(row_string, config)
|
28
|
+
!row_string.lstrip.start_with?(config.fetch(:comment_character))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#start-dates-and-end-dates-columns
|
6
|
+
class EndDates < Column
|
7
|
+
def self.segment_separator
|
8
|
+
/,\s*/
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.regexes(segment_index)
|
12
|
+
[%r{\A
|
13
|
+
(?<date>\d{4}/\d\d?/\d\d?)
|
14
|
+
\z}x]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#genres-column
|
6
|
+
class Genres < Column
|
7
|
+
def self.segment_separator
|
8
|
+
/,\s*/
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.regexes(segment_index)
|
12
|
+
[%r{\A
|
13
|
+
(?<genre>.+)
|
14
|
+
\z}x]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#head-column-title
|
6
|
+
# and https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#head-column-dnf
|
7
|
+
# and the sections following.
|
8
|
+
class Head < Column
|
9
|
+
def self.split_by_format?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.regexes_before_formats
|
14
|
+
[
|
15
|
+
/\A#{Column::SHARED_REGEXES[:progress]}\z/,
|
16
|
+
/.+/,
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.segment_separator
|
21
|
+
/\s*--\s*/
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.flatten_into_arrays
|
25
|
+
%i[extra_info series_names series_volumes]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.regexes(segment_index)
|
29
|
+
[
|
30
|
+
# author and title
|
31
|
+
(%r{\A
|
32
|
+
(
|
33
|
+
(?<author>.+?)
|
34
|
+
\s+-\s+
|
35
|
+
)?
|
36
|
+
(?<title>.+)
|
37
|
+
\z}x if segment_index.zero?),
|
38
|
+
*Column::SHARED_REGEXES[:series_and_extra_info],
|
39
|
+
].compact
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#history-column
|
6
|
+
class History < Column
|
7
|
+
def self.segment_separator
|
8
|
+
/\s*--\s*/
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.segment_group_separator
|
12
|
+
/\s*----\s*/
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.tweaks
|
16
|
+
{
|
17
|
+
except_dates: ->(dates_list) {
|
18
|
+
dates_list
|
19
|
+
.split(/\s*,\s*/)
|
20
|
+
.map { |date|
|
21
|
+
date.match(
|
22
|
+
%r{\A
|
23
|
+
#{START_END_DATES_REGEX}
|
24
|
+
\z}xo
|
25
|
+
)
|
26
|
+
&.named_captures
|
27
|
+
&.compact
|
28
|
+
&.transform_keys(&:to_sym)
|
29
|
+
&.presence
|
30
|
+
}
|
31
|
+
.compact
|
32
|
+
},
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.regexes(segment_index)
|
37
|
+
[
|
38
|
+
# entry of exception dates ("but not on these dates")
|
39
|
+
%r{\A
|
40
|
+
not
|
41
|
+
\s+
|
42
|
+
(?<except_dates>.+)
|
43
|
+
\z}x,
|
44
|
+
# normal entry
|
45
|
+
%r{\A
|
46
|
+
\(?\s*
|
47
|
+
# variant, group before first start date
|
48
|
+
(
|
49
|
+
(
|
50
|
+
v(?<variant>\d)
|
51
|
+
(\s+|\z)
|
52
|
+
)?
|
53
|
+
(
|
54
|
+
🤝🏼(?<group>.+?)
|
55
|
+
)?
|
56
|
+
(?=(\d{4}/)?\d\d?/\d\d?)
|
57
|
+
)?
|
58
|
+
# planned or dates
|
59
|
+
(
|
60
|
+
(
|
61
|
+
(?<planned>\?\?)
|
62
|
+
|
|
63
|
+
(#{START_END_DATES_REGEX})
|
64
|
+
)
|
65
|
+
(\s*\)?\s*\z|\s+)
|
66
|
+
)?
|
67
|
+
# progress
|
68
|
+
(
|
69
|
+
# requires the at symbol, unlike the shared progress regex in Column
|
70
|
+
# and also adds the done option
|
71
|
+
(
|
72
|
+
(DNF\s+)?@?(?<progress_percent>\d\d?)%
|
73
|
+
|
|
74
|
+
(DNF\s+)?@p?(?<progress_pages>\d+)p?
|
75
|
+
|
|
76
|
+
(DNF\s+)?@(?<progress_time>\d+:\d\d)
|
77
|
+
|
|
78
|
+
# just DNF
|
79
|
+
(?<progress_dnf>DNF)
|
80
|
+
|
|
81
|
+
# done
|
82
|
+
(?<progress_done>done)
|
83
|
+
)
|
84
|
+
(\s*\)?\s*\z|\s+)
|
85
|
+
)?
|
86
|
+
# amount, repetitions, frequency
|
87
|
+
(
|
88
|
+
(
|
89
|
+
p?(?<amount_pages>\d+)p?
|
90
|
+
|
|
91
|
+
(?<amount_time>\d+:\d\d)
|
92
|
+
)?
|
93
|
+
(
|
94
|
+
\s*
|
95
|
+
x(?<repetitions>\d+)
|
96
|
+
)?
|
97
|
+
(
|
98
|
+
/(?<frequency>day|week|month)
|
99
|
+
)?
|
100
|
+
(\s*\)?\s*\z|\s+)
|
101
|
+
)?
|
102
|
+
# favorite, name
|
103
|
+
(
|
104
|
+
(?<favorite>⭐)?
|
105
|
+
\s*
|
106
|
+
(?<name>[^\d].*)
|
107
|
+
)?
|
108
|
+
\z}xo,
|
109
|
+
]
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
START_END_DATES_REGEX =
|
115
|
+
%r{
|
116
|
+
(
|
117
|
+
(?<start_year>\d{4})
|
118
|
+
/
|
119
|
+
)?
|
120
|
+
(
|
121
|
+
(?<start_month>\d\d?)
|
122
|
+
/
|
123
|
+
)?
|
124
|
+
(?<start_day>\d\d?)?
|
125
|
+
(?<range>\.\.)?
|
126
|
+
(
|
127
|
+
(?<=\.\.)
|
128
|
+
(
|
129
|
+
(?<end_year>\d{4})
|
130
|
+
/
|
131
|
+
)?
|
132
|
+
(
|
133
|
+
(?<end_month>\d\d?)
|
134
|
+
/
|
135
|
+
)?
|
136
|
+
(?<end_day>\d\d?)?
|
137
|
+
)?
|
138
|
+
}x
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#length-column
|
6
|
+
class Length < Column
|
7
|
+
def self.regexes(segment_index)
|
8
|
+
[%r{\A
|
9
|
+
# length
|
10
|
+
(
|
11
|
+
(
|
12
|
+
(?<length_pages>\d+)p?
|
13
|
+
|
|
14
|
+
(?<length_time>\d+:\d\d)
|
15
|
+
)
|
16
|
+
(\s+|\z)
|
17
|
+
)
|
18
|
+
# each or repetitions, used in conjunction with the History column
|
19
|
+
(
|
20
|
+
# each
|
21
|
+
(?<each>each)
|
22
|
+
|
|
23
|
+
# repetitions
|
24
|
+
(
|
25
|
+
x
|
26
|
+
(?<repetitions>\d+)
|
27
|
+
)
|
28
|
+
)?
|
29
|
+
\z}x]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#notes-column
|
6
|
+
# and https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#notes-column-special-notes
|
7
|
+
class Notes < Column
|
8
|
+
def self.segment_separator
|
9
|
+
/\s*--\s*/
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.regexes(segment_index)
|
13
|
+
[
|
14
|
+
# blurb note
|
15
|
+
%r{\A
|
16
|
+
💬\s*(?<note_blurb>.+)
|
17
|
+
\z}x,
|
18
|
+
# private note
|
19
|
+
%r{\A
|
20
|
+
🔒\s*(?<note_private>.+)
|
21
|
+
\z}x,
|
22
|
+
# regular note
|
23
|
+
%r{\A
|
24
|
+
(?<note_regular>.+)
|
25
|
+
\z}x,
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#rating-column
|
6
|
+
class Rating < Column
|
7
|
+
def self.regexes(segment_index)
|
8
|
+
# integer or float
|
9
|
+
[/\A(?<number>\d+\.?\d*)?\z/]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
module Regular
|
5
|
+
# See https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#sources-column
|
6
|
+
# and https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#sources-column-variants
|
7
|
+
class Sources < Column
|
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" =>
|
14
|
+
->(source) {
|
15
|
+
source.match?(/\A#{ISBN_REGEX}/o) || source.match(/\A#{ASIN_REGEX}/o)
|
16
|
+
},
|
17
|
+
}
|
18
|
+
|
19
|
+
|
20
|
+
def self.split_by_format?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.segment_separator
|
25
|
+
/\s*--\s*/
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.flatten_into_arrays
|
29
|
+
%i[extra_info series_names series_volumes]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.tweaks
|
33
|
+
{
|
34
|
+
sources: -> {
|
35
|
+
sources = _1.split(/\s*,\s*/)
|
36
|
+
|
37
|
+
SOURCES_PARSING_ERRORS.each do |message, check|
|
38
|
+
if sources.any? { |source| check.call(source) }
|
39
|
+
raise ParsingError, message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
sources
|
44
|
+
},
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.regexes(segment_index)
|
49
|
+
[
|
50
|
+
# ISBN/ASIN and length (without sources)
|
51
|
+
(%r{\A
|
52
|
+
(
|
53
|
+
(?<isbn>(\d{3}[-\s]?)?[A-Z\d]{10})
|
54
|
+
,?(\s+|\z)
|
55
|
+
)?
|
56
|
+
(
|
57
|
+
(?<length_pages>\d+)p?
|
58
|
+
|
|
59
|
+
(?<length_time>\d+:\d\d)
|
60
|
+
)?
|
61
|
+
\z}x if segment_index.zero?),
|
62
|
+
# sources, ISBN/ASIN, length
|
63
|
+
(%r{\A
|
64
|
+
(
|
65
|
+
(?<sources>.+?)
|
66
|
+
,?(\s+|\z)
|
67
|
+
)?
|
68
|
+
(
|
69
|
+
(
|
70
|
+
(?<isbn>#{ISBN_REGEX})
|
71
|
+
|
|
72
|
+
(?<asin>#{ASIN_REGEX})
|
73
|
+
)
|
74
|
+
,?(\s+|\z)
|
75
|
+
)?
|
76
|
+
(
|
77
|
+
(?<length_pages>\d+)p?
|
78
|
+
|
|
79
|
+
(?<length_time>\d+:\d\d)
|
80
|
+
)?
|
81
|
+
\z}xo if segment_index.zero?),
|
82
|
+
*Column::SHARED_REGEXES[:series_and_extra_info],
|
83
|
+
].compact
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
ISBN_REGEX = /(\d{3}[-\s]?)?\d{10}/
|
89
|
+
ASIN_REGEX = /B0[A-Z\d]{8}/
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|