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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/reading +5 -5
  3. data/bin/readingfile +31 -0
  4. data/lib/reading/config.rb +96 -108
  5. data/lib/reading/errors.rb +10 -66
  6. data/lib/reading/filter.rb +95 -0
  7. data/lib/reading/item/time_length.rb +140 -0
  8. data/lib/reading/item/view.rb +121 -0
  9. data/lib/reading/item.rb +117 -0
  10. data/lib/reading/parsing/attributes/attribute.rb +26 -0
  11. data/lib/reading/parsing/attributes/author.rb +15 -0
  12. data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +106 -0
  13. data/lib/reading/parsing/attributes/experiences/history_transformer.rb +452 -0
  14. data/lib/reading/parsing/attributes/experiences/spans_validator.rb +149 -0
  15. data/lib/reading/parsing/attributes/experiences.rb +27 -0
  16. data/lib/reading/parsing/attributes/genres.rb +16 -0
  17. data/lib/reading/parsing/attributes/notes.rb +22 -0
  18. data/lib/reading/parsing/attributes/rating.rb +17 -0
  19. data/lib/reading/parsing/attributes/shared.rb +62 -0
  20. data/lib/reading/parsing/attributes/title.rb +21 -0
  21. data/lib/reading/parsing/attributes/variants.rb +77 -0
  22. data/lib/reading/parsing/csv.rb +112 -0
  23. data/lib/reading/parsing/parser.rb +292 -0
  24. data/lib/reading/parsing/rows/column.rb +131 -0
  25. data/lib/reading/parsing/rows/comment.rb +26 -0
  26. data/lib/reading/parsing/rows/compact_planned.rb +30 -0
  27. data/lib/reading/parsing/rows/compact_planned_columns/head.rb +60 -0
  28. data/lib/reading/parsing/rows/regular.rb +33 -0
  29. data/lib/reading/parsing/rows/regular_columns/end_dates.rb +20 -0
  30. data/lib/reading/parsing/rows/regular_columns/genres.rb +20 -0
  31. data/lib/reading/parsing/rows/regular_columns/head.rb +45 -0
  32. data/lib/reading/parsing/rows/regular_columns/history.rb +143 -0
  33. data/lib/reading/parsing/rows/regular_columns/length.rb +35 -0
  34. data/lib/reading/parsing/rows/regular_columns/notes.rb +32 -0
  35. data/lib/reading/parsing/rows/regular_columns/rating.rb +15 -0
  36. data/lib/reading/parsing/rows/regular_columns/sources.rb +94 -0
  37. data/lib/reading/parsing/rows/regular_columns/start_dates.rb +35 -0
  38. data/lib/reading/parsing/transformer.rb +70 -0
  39. data/lib/reading/util/hash_compact_by_template.rb +1 -0
  40. data/lib/reading/util/hash_deep_merge.rb +1 -1
  41. data/lib/reading/util/hash_to_data.rb +30 -0
  42. data/lib/reading/util/numeric_to_i_if_whole.rb +12 -0
  43. data/lib/reading/util/string_truncate.rb +13 -4
  44. data/lib/reading/version.rb +1 -1
  45. data/lib/reading.rb +49 -0
  46. metadata +76 -42
  47. data/lib/reading/attribute/all_attributes.rb +0 -83
  48. data/lib/reading/attribute/attribute.rb +0 -25
  49. data/lib/reading/attribute/experiences/dates_validator.rb +0 -94
  50. data/lib/reading/attribute/experiences/experiences_attribute.rb +0 -74
  51. data/lib/reading/attribute/experiences/progress_subattribute.rb +0 -48
  52. data/lib/reading/attribute/experiences/spans_subattribute.rb +0 -82
  53. data/lib/reading/attribute/variants/extra_info_subattribute.rb +0 -44
  54. data/lib/reading/attribute/variants/length_subattribute.rb +0 -45
  55. data/lib/reading/attribute/variants/series_subattribute.rb +0 -57
  56. data/lib/reading/attribute/variants/sources_subattribute.rb +0 -78
  57. data/lib/reading/attribute/variants/variants_attribute.rb +0 -69
  58. data/lib/reading/csv.rb +0 -76
  59. data/lib/reading/line.rb +0 -23
  60. data/lib/reading/row/blank_row.rb +0 -23
  61. data/lib/reading/row/compact_planned_row.rb +0 -130
  62. data/lib/reading/row/regular_row.rb +0 -99
  63. data/lib/reading/row/row.rb +0 -88
  64. 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