reading 0.7.0 → 0.9.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 +80 -10
- data/lib/reading/config.rb +96 -52
- data/lib/reading/errors.rb +4 -1
- data/lib/reading/filter.rb +95 -0
- data/lib/reading/item/time_length.rb +69 -30
- data/lib/reading/item/view.rb +116 -0
- data/lib/reading/item.rb +384 -0
- data/lib/reading/parsing/attributes/attribute.rb +1 -8
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +11 -12
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +31 -22
- data/lib/reading/parsing/attributes/experiences/spans_validator.rb +19 -20
- data/lib/reading/parsing/attributes/experiences.rb +6 -6
- data/lib/reading/parsing/attributes/notes.rb +1 -1
- data/lib/reading/parsing/attributes/shared.rb +15 -8
- data/lib/reading/parsing/attributes/variants.rb +10 -7
- data/lib/reading/parsing/csv.rb +58 -44
- data/lib/reading/parsing/parser.rb +24 -25
- data/lib/reading/parsing/rows/blank.rb +23 -0
- data/lib/reading/parsing/rows/comment.rb +6 -7
- data/lib/reading/parsing/rows/compact_planned.rb +9 -9
- data/lib/reading/parsing/rows/compact_planned_columns/head.rb +2 -2
- data/lib/reading/parsing/rows/custom_config.rb +42 -0
- data/lib/reading/parsing/rows/regular.rb +15 -14
- data/lib/reading/parsing/rows/regular_columns/length.rb +8 -8
- data/lib/reading/parsing/rows/regular_columns/sources.rb +15 -9
- data/lib/reading/parsing/transformer.rb +15 -19
- data/lib/reading/stats/filter.rb +738 -0
- data/lib/reading/stats/grouping.rb +243 -0
- data/lib/reading/stats/operation.rb +313 -0
- data/lib/reading/stats/query.rb +37 -0
- data/lib/reading/stats/terminal_result_formatters.rb +91 -0
- data/lib/reading/util/exclude.rb +12 -0
- data/lib/reading/util/hash_to_data.rb +30 -0
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +51 -5
- metadata +28 -7
- data/bin/readingfile +0 -31
- data/lib/reading/util/hash_to_struct.rb +0 -30
- data/lib/reading/util/string_remove.rb +0 -28
- data/lib/reading/util/string_truncate.rb +0 -22
@@ -0,0 +1,23 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
# A row that is a blank line.
|
5
|
+
module Blank
|
6
|
+
using Util::HashArrayDeepFetch
|
7
|
+
|
8
|
+
# No columns.
|
9
|
+
# @return [Array]
|
10
|
+
def self.column_classes
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Is a blank line.
|
15
|
+
# @param row_string [String]
|
16
|
+
# @return [Boolean]
|
17
|
+
def self.match?(row_string)
|
18
|
+
row_string == "\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -5,20 +5,19 @@ module Reading
|
|
5
5
|
module Comment
|
6
6
|
using Util::HashArrayDeepFetch
|
7
7
|
|
8
|
-
# No columns;
|
8
|
+
# No columns; parsed as if the row were blank.
|
9
9
|
# @return [Array]
|
10
10
|
def self.column_classes
|
11
11
|
[]
|
12
12
|
end
|
13
13
|
|
14
|
-
# Starts with a comment character
|
15
|
-
#
|
16
|
-
#
|
14
|
+
# Starts with a comment character. Note: this must be called *after*
|
15
|
+
# calling ::match? on Rows::CompactPlanned and Rows::CustomConfig,
|
16
|
+
# because those check for starting with a comment character too.
|
17
17
|
# @param row_string [String]
|
18
|
-
# @param config [Hash]
|
19
18
|
# @return [Boolean]
|
20
|
-
def self.match?(row_string
|
21
|
-
row_string.lstrip.start_with?(
|
19
|
+
def self.match?(row_string)
|
20
|
+
row_string.lstrip.start_with?(Config.hash.fetch(:comment_character))
|
22
21
|
end
|
23
22
|
end
|
24
23
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
1
|
+
require_relative 'column'
|
2
|
+
require_relative 'compact_planned_columns/head'
|
3
|
+
require_relative 'regular_columns/sources'
|
4
|
+
require_relative 'regular_columns/length'
|
4
5
|
|
5
6
|
module Reading
|
6
7
|
module Parsing
|
@@ -12,17 +13,16 @@ module Reading
|
|
12
13
|
# The columns that are possible in this type of row.
|
13
14
|
# @return [Array<Class>]
|
14
15
|
def self.column_classes
|
15
|
-
[CompactPlanned::Head, Regular::Sources]
|
16
|
+
[CompactPlanned::Head, Regular::Sources, Regular::Length]
|
16
17
|
end
|
17
18
|
|
18
19
|
# Starts with a comment character and includes one or more format emojis.
|
19
20
|
# @param row_string [String]
|
20
|
-
# @param config [Hash]
|
21
21
|
# @return [Boolean]
|
22
|
-
def self.match?(row_string
|
23
|
-
row_string.lstrip.start_with?(
|
24
|
-
row_string.match?(
|
25
|
-
row_string.count(
|
22
|
+
def self.match?(row_string)
|
23
|
+
row_string.lstrip.start_with?(Config.hash.fetch(:comment_character)) &&
|
24
|
+
row_string.match?(Config.hash.deep_fetch(:regex, :formats)) &&
|
25
|
+
row_string.count(Config.hash.fetch(:column_separator)) <= column_classes.count - 1
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -15,7 +15,7 @@ module Reading
|
|
15
15
|
\\ # comment character
|
16
16
|
\s*
|
17
17
|
(
|
18
|
-
(?<genres>[^a-z]+)?
|
18
|
+
(?<genres>[^a-z@]+)?
|
19
19
|
\s*
|
20
20
|
(?<sources>@.+)?
|
21
21
|
\s*:
|
@@ -49,7 +49,7 @@ module Reading
|
|
49
49
|
)?
|
50
50
|
(?<title>[^@]+)
|
51
51
|
(?<sources>@.+)?
|
52
|
-
\z}x if
|
52
|
+
\z}x if segment_index.zero?),
|
53
53
|
*Column::SHARED_REGEXES[:series_and_extra_info],
|
54
54
|
].compact
|
55
55
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Reading
|
2
|
+
module Parsing
|
3
|
+
module Rows
|
4
|
+
# A row that declares custom config.
|
5
|
+
module CustomConfig
|
6
|
+
using Util::HashArrayDeepFetch
|
7
|
+
|
8
|
+
# No columns; 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 opening curly brace, and ends with
|
15
|
+
# a closing curly brace.
|
16
|
+
# @param row_string [String]
|
17
|
+
# @return [Boolean]
|
18
|
+
def self.match?(row_string)
|
19
|
+
row_string.match?(
|
20
|
+
%r{\A
|
21
|
+
\s*
|
22
|
+
#{Regexp.escape(Config.hash.fetch(:comment_character))}
|
23
|
+
\s*
|
24
|
+
\{.+\}
|
25
|
+
\s*
|
26
|
+
\z}x
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds this row's custom config to the singleton config.
|
31
|
+
# @param row_string [String]
|
32
|
+
# @param config [Hash] an entire config.
|
33
|
+
def self.merge_custom_config!(row_string)
|
34
|
+
stripped_row = row_string.strip.delete_prefix(Config.hash.fetch(:comment_character))
|
35
|
+
custom_config = eval(stripped_row)
|
36
|
+
|
37
|
+
Config.hash.merge!(custom_config)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
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
11
|
|
12
12
|
module Reading
|
13
13
|
module Parsing
|
@@ -20,12 +20,13 @@ module Reading
|
|
20
20
|
[Rating, Head, Sources, StartDates, EndDates, Genres, Length, Notes, History]
|
21
21
|
end
|
22
22
|
|
23
|
-
# Does not start with a comment character.
|
23
|
+
# Does not start with a comment character. Note: this must be called
|
24
|
+
# *after* calling ::match? on Rows::Blank, because that one catches
|
25
|
+
# blank lines.
|
24
26
|
# @param row_string [String]
|
25
|
-
# @param config [Hash]
|
26
27
|
# @return [Boolean]
|
27
|
-
def self.match?(row_string
|
28
|
-
!row_string.lstrip.start_with?(
|
28
|
+
def self.match?(row_string)
|
29
|
+
!row_string.lstrip.start_with?(Config.hash.fetch(:comment_character))
|
29
30
|
end
|
30
31
|
end
|
31
32
|
end
|
@@ -15,16 +15,16 @@ module Reading
|
|
15
15
|
)
|
16
16
|
(\s+|\z)
|
17
17
|
)
|
18
|
-
# each
|
18
|
+
# each and repetitions are used in conjunction with the History column
|
19
|
+
# each
|
19
20
|
(
|
20
|
-
# each
|
21
21
|
(?<each>each)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
)
|
22
|
+
(\s+|\z)
|
23
|
+
)?
|
24
|
+
# repetitions
|
25
|
+
(
|
26
|
+
x
|
27
|
+
(?<repetitions>\d+)
|
28
28
|
)?
|
29
29
|
\z}x]
|
30
30
|
end
|
@@ -6,17 +6,12 @@ module Reading
|
|
6
6
|
# and https://github.com/fpsvogel/reading/blob/main/doc/csv-format.md#sources-column-variants
|
7
7
|
class Sources < Column
|
8
8
|
SOURCES_PARSING_ERRORS = {
|
9
|
-
"
|
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" =>
|
9
|
+
"The ISBN/ASIN must be placed last in the Sources column" =>
|
14
10
|
->(source) {
|
15
11
|
source.match?(/\A#{ISBN_REGEX}/o) || source.match(/\A#{ASIN_REGEX}/o)
|
16
12
|
},
|
17
13
|
}
|
18
14
|
|
19
|
-
|
20
15
|
def self.split_by_format?
|
21
16
|
true
|
22
17
|
end
|
@@ -32,7 +27,18 @@ module Reading
|
|
32
27
|
def self.tweaks
|
33
28
|
{
|
34
29
|
sources: -> {
|
35
|
-
|
30
|
+
comma = /\s*,\s*/
|
31
|
+
space_before_url = / (?=https?:\/\/)/
|
32
|
+
sources = _1.split(Regexp.union(comma, space_before_url))
|
33
|
+
|
34
|
+
# Split by space after URL.
|
35
|
+
sources = sources.flat_map { |src|
|
36
|
+
if src.match?(/\Ahttps?:\/\//)
|
37
|
+
src.split(" ", 2)
|
38
|
+
else
|
39
|
+
src
|
40
|
+
end
|
41
|
+
}
|
36
42
|
|
37
43
|
SOURCES_PARSING_ERRORS.each do |message, check|
|
38
44
|
if sources.any? { |source| check.call(source) }
|
@@ -58,7 +64,7 @@ module Reading
|
|
58
64
|
|
|
59
65
|
(?<length_time>\d+:\d\d)
|
60
66
|
)?
|
61
|
-
\z}x if
|
67
|
+
\z}x if segment_index.zero?),
|
62
68
|
# sources, ISBN/ASIN, length
|
63
69
|
(%r{\A
|
64
70
|
(
|
@@ -78,7 +84,7 @@ module Reading
|
|
78
84
|
|
|
79
85
|
(?<length_time>\d+:\d\d)
|
80
86
|
)?
|
81
|
-
\z}xo if
|
87
|
+
\z}xo if segment_index.zero?),
|
82
88
|
*Column::SHARED_REGEXES[:series_and_extra_info],
|
83
89
|
].compact
|
84
90
|
end
|
@@ -1,12 +1,12 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
1
|
+
require_relative 'attributes/shared'
|
2
|
+
require_relative 'attributes/attribute'
|
3
|
+
require_relative 'attributes/rating'
|
4
|
+
require_relative 'attributes/author'
|
5
|
+
require_relative 'attributes/title'
|
6
|
+
require_relative 'attributes/genres'
|
7
|
+
require_relative 'attributes/variants'
|
8
|
+
require_relative 'attributes/experiences'
|
9
|
+
require_relative 'attributes/notes'
|
10
10
|
|
11
11
|
module Reading
|
12
12
|
module Parsing
|
@@ -14,19 +14,15 @@ module Reading
|
|
14
14
|
# Transforms an intermediate hash (parsed from a CSV row) into item data.
|
15
15
|
# While the intermediate hash mirrors the structure of a row, the output of
|
16
16
|
# Transformer is based around item attributes, which are listed in
|
17
|
-
# Config#default_config[:
|
17
|
+
# Config#default_config[:item][:template] and in the files in parsing/attributes.
|
18
18
|
#
|
19
19
|
class Transformer
|
20
20
|
using Util::HashArrayDeepFetch
|
21
21
|
using Util::HashCompactByTemplate
|
22
22
|
|
23
|
-
attr_reader :config
|
24
23
|
private attr_reader :attributes
|
25
24
|
|
26
|
-
|
27
|
-
def initialize(config)
|
28
|
-
@config = config
|
29
|
-
|
25
|
+
def initialize
|
30
26
|
set_attributes
|
31
27
|
end
|
32
28
|
|
@@ -34,13 +30,13 @@ module Reading
|
|
34
30
|
# @param parsed_row [Hash{Symbol => Hash, Array}] output from
|
35
31
|
# Parsing::Parser#parse_row_to_intermediate_hash.
|
36
32
|
# @return [Array<Hash>] an array of Hashes like the template in
|
37
|
-
# Config#default_config[:
|
33
|
+
# Config#default_config[:item][:template].
|
38
34
|
def transform_intermediate_hash_to_item_hashes(parsed_row)
|
39
35
|
if parsed_row[:head].blank?
|
40
36
|
raise InvalidHeadError, "Blank or missing Head column"
|
41
37
|
end
|
42
38
|
|
43
|
-
template =
|
39
|
+
template = Config.hash.deep_fetch(:item, :template)
|
44
40
|
|
45
41
|
parsed_row[:head].map.with_index { |_head, head_index|
|
46
42
|
template.map { |attribute_name, default_value|
|
@@ -58,11 +54,11 @@ module Reading
|
|
58
54
|
# Sets the attributes classes which do all the transforming work.
|
59
55
|
# See parsing/attributes/*.
|
60
56
|
def set_attributes
|
61
|
-
@attributes ||=
|
57
|
+
@attributes ||= Config.hash.deep_fetch(:item, :template).map { |attribute_name, _default|
|
62
58
|
attribute_name_camelcase = attribute_name.to_s.split("_").map(&:capitalize).join
|
63
59
|
attribute_class = Attributes.const_get(attribute_name_camelcase)
|
64
60
|
|
65
|
-
[attribute_name, attribute_class.new
|
61
|
+
[attribute_name, attribute_class.new]
|
66
62
|
}.to_h
|
67
63
|
end
|
68
64
|
end
|