reading 0.8.0 → 0.9.1
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 +95 -10
- data/lib/reading/config.rb +27 -5
- data/lib/reading/errors.rb +4 -1
- data/lib/reading/item/time_length.rb +60 -23
- data/lib/reading/item/view.rb +14 -19
- data/lib/reading/item.rb +324 -54
- data/lib/reading/parsing/attributes/attribute.rb +0 -7
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +17 -13
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +172 -60
- data/lib/reading/parsing/attributes/experiences/spans_validator.rb +19 -20
- data/lib/reading/parsing/attributes/experiences.rb +5 -5
- data/lib/reading/parsing/attributes/shared.rb +17 -7
- data/lib/reading/parsing/attributes/variants.rb +9 -6
- data/lib/reading/parsing/csv.rb +38 -35
- data/lib/reading/parsing/parser.rb +23 -24
- 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 +16 -10
- data/lib/reading/parsing/rows/regular_columns/start_dates.rb +5 -1
- data/lib/reading/parsing/transformer.rb +13 -17
- data/lib/reading/stats/filter.rb +738 -0
- data/lib/reading/stats/grouping.rb +257 -0
- data/lib/reading/stats/operation.rb +345 -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_array_deep_fetch.rb +1 -23
- data/lib/reading/util/hash_to_data.rb +2 -2
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +36 -21
- metadata +28 -24
- data/bin/readingfile +0 -31
- data/lib/reading/util/string_remove.rb +0 -28
- data/lib/reading/util/string_truncate.rb +0 -22
data/lib/reading/item.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require 'forwardable'
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative "config"
|
5
|
-
require_relative "util/hash_to_data"
|
6
|
-
require_relative "util/hash_array_deep_fetch"
|
3
|
+
require_relative 'item/view'
|
7
4
|
|
8
5
|
module Reading
|
9
6
|
# A wrapper for an item parsed from a CSV reading log, providing convenience
|
@@ -15,28 +12,36 @@ module Reading
|
|
15
12
|
|
16
13
|
ATTRIBUTES = %i[rating author title genres variants experiences notes]
|
17
14
|
|
18
|
-
private attr_reader :
|
19
|
-
attr_reader :view
|
15
|
+
private attr_reader :data
|
16
|
+
attr_reader :view
|
20
17
|
|
21
|
-
def_delegators :
|
18
|
+
def_delegators :data, *ATTRIBUTES
|
22
19
|
|
23
|
-
# @param
|
24
|
-
# Config#default_config[:item][:template].
|
25
|
-
# @param config [Hash] an entire config.
|
20
|
+
# @param item_hash_or_data [Hash, Data] a parsed item Hash like the template
|
21
|
+
# in Config#default_config[:item][:template]; or a Data from another Item.
|
26
22
|
# @param view [Class, nil, Boolean] the class that will be used to build the
|
27
23
|
# view object, or nil/false if no view object should be built. If you use
|
28
24
|
# a custom view class, the only requirement is that its #initialize take
|
29
|
-
# an Item
|
30
|
-
def initialize(
|
31
|
-
|
25
|
+
# an Item.
|
26
|
+
def initialize(item_hash_or_data, view: Item::View)
|
27
|
+
if item_hash_or_data.is_a? Data
|
28
|
+
@data = item_hash_or_data
|
29
|
+
elsif item_hash_or_data.is_a? Hash
|
30
|
+
item_hash = item_hash_or_data.dup
|
32
31
|
|
33
|
-
|
32
|
+
add_missing_attributes_with_filler_values!(item_hash)
|
33
|
+
add_statuses_and_last_end_dates!(item_hash)
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
@data = item_hash.to_data
|
36
|
+
end
|
37
|
+
|
38
|
+
@view = view.new(self) if view
|
39
|
+
end
|
37
40
|
|
38
|
-
|
39
|
-
|
41
|
+
# This item's status.
|
42
|
+
# @return [Symbol] :planned, :in_progress, or :done
|
43
|
+
def status
|
44
|
+
data.experiences.last&.status || :planned
|
40
45
|
end
|
41
46
|
|
42
47
|
# Whether this item is done.
|
@@ -45,11 +50,263 @@ module Reading
|
|
45
50
|
status == :done
|
46
51
|
end
|
47
52
|
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
+
# This item's last end date.
|
54
|
+
# @return [Date, nil]
|
55
|
+
def last_end_date
|
56
|
+
data.experiences.last&.last_end_date
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns a new Item containing shallow copy of @data, with its experiences
|
60
|
+
# replaced with new_experiences.
|
61
|
+
# @param new_experiences [Array<Data>]
|
62
|
+
# @param view [Class, nil, Boolean]
|
63
|
+
# @return [Item]
|
64
|
+
def with_experiences(new_experiences, view: false)
|
65
|
+
new_variants = variants.filter.with_index { |variant, old_index|
|
66
|
+
new_experiences.any? { _1.variant_index == old_index }
|
67
|
+
}
|
68
|
+
|
69
|
+
with_variants(
|
70
|
+
new_variants,
|
71
|
+
new_experiences:,
|
72
|
+
view:,
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns a new Item containing shallow copy of @data, with its variants
|
77
|
+
# replaced with new_variants.
|
78
|
+
# @param new_variants [Array<Data>]
|
79
|
+
# @param new_experiences [Array<Data>]
|
80
|
+
# @param view [Class, nil, Boolean]
|
81
|
+
# @return [Item]
|
82
|
+
def with_variants(new_variants, new_experiences: nil, view: false)
|
83
|
+
updated_variant_indices = []
|
84
|
+
|
85
|
+
# Map old to new indices, omitting those of variants that are not in new_variants.
|
86
|
+
variants.each.with_index { |variant, old_index|
|
87
|
+
new_index = new_variants.index(variant)
|
88
|
+
updated_variant_indices[old_index] = new_index if new_index
|
89
|
+
}
|
90
|
+
|
91
|
+
# Remove experiences associated with the removed variants.
|
92
|
+
kept_experiences = (new_experiences || experiences).select { |experience|
|
93
|
+
# Conditional in case Item was created with fragmentary experience hashes,
|
94
|
+
# as in stats_test.rb
|
95
|
+
variant_index = experience.variant_index if experience.members.include?(:variant_index)
|
96
|
+
|
97
|
+
!!updated_variant_indices[variant_index || 0]
|
98
|
+
}
|
99
|
+
|
100
|
+
# Then update the kept experiences' variant indices.
|
101
|
+
updated_kept_experiences = kept_experiences.map { |experience|
|
102
|
+
updated_variant_index = updated_variant_indices[experience.variant_index]
|
103
|
+
experience.with(variant_index: updated_variant_index)
|
104
|
+
}
|
105
|
+
|
106
|
+
self.class.new(
|
107
|
+
data.with(
|
108
|
+
variants: new_variants,
|
109
|
+
experiences: updated_kept_experiences,
|
110
|
+
),
|
111
|
+
view:,
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Splits this Item into two Items: one < the given date, and the other >= it.
|
116
|
+
# @date [Date] must be the first day of a month.
|
117
|
+
# @return [Array(Item, Item)]
|
118
|
+
def split(date)
|
119
|
+
before_index = nil
|
120
|
+
middle_indices = experiences.map.with_index { |experience, i|
|
121
|
+
if experience.spans.first.dates &&
|
122
|
+
experience.spans.first.dates.begin < date
|
123
|
+
|
124
|
+
before_index = i
|
125
|
+
|
126
|
+
if (experience.last_end_date || Date.today) >= date
|
127
|
+
i
|
128
|
+
else
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
}
|
133
|
+
.compact
|
134
|
+
|
135
|
+
# There are no experiences with spans that overlap the date.
|
136
|
+
if middle_indices.none?
|
137
|
+
# The Item is planned.
|
138
|
+
return [] if experiences.none? { _1.spans.first.dates }
|
139
|
+
# date is after all spans.
|
140
|
+
return [self, nil] if experiences.all? { _1.last_end_date && date > _1.last_end_date }
|
141
|
+
# date is before all spans.
|
142
|
+
return [nil, self] if experiences.all? { _1.spans.first.dates.begin >= date }
|
143
|
+
|
144
|
+
# Date is in between experiences.
|
145
|
+
if before_index
|
146
|
+
item_before = with_experiences(experiences[..before_index])
|
147
|
+
item_after = with_experiences(experiences[(before_index + 1)..])
|
148
|
+
|
149
|
+
return [item_before, item_after]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if middle_indices.first == 0
|
154
|
+
experiences_before = []
|
155
|
+
else
|
156
|
+
experiences_before = experiences[..(middle_indices.first - 1)]
|
157
|
+
end
|
158
|
+
experiences_after = experiences[(middle_indices.first + middle_indices.count)..]
|
159
|
+
experiences_middle = experiences.values_at(*middle_indices)
|
160
|
+
|
161
|
+
# TODO remove this check?
|
162
|
+
unless middle_indices == (middle_indices.min..middle_indices.max).to_a
|
163
|
+
raise Reading::Error, "Non-consecutive experiences found during Item#split."
|
164
|
+
end
|
165
|
+
|
166
|
+
experiences_middle.each do |experience_middle|
|
167
|
+
before_index = nil
|
168
|
+
span_middle_index = experience_middle
|
169
|
+
.spans
|
170
|
+
.index.with_index { |span, i|
|
171
|
+
if span.dates && span.dates.begin < date
|
172
|
+
before_index = i
|
173
|
+
|
174
|
+
(span.dates.end || Date.today) >= date
|
175
|
+
end
|
176
|
+
}
|
177
|
+
|
178
|
+
if span_middle_index.nil? # date is between spans.
|
179
|
+
spans_before = experience_middle.spans[..before_index]
|
180
|
+
spans_after = experience_middle.spans[(before_index + 1)..]
|
181
|
+
else
|
182
|
+
span_middle = experience_middle.spans[span_middle_index]
|
183
|
+
|
184
|
+
unless span_middle.dates.end
|
185
|
+
end_today_instead_of_endless = { dates: span_middle.dates.begin..Date.today }
|
186
|
+
span_middle = span_middle.to_h.merge(end_today_instead_of_endless).to_data
|
187
|
+
end
|
188
|
+
|
189
|
+
dates_before = span_middle.dates.begin..date.prev_day
|
190
|
+
amount_before = (span_middle.amount || 0) * (dates_before.count / span_middle.dates.count.to_f)
|
191
|
+
span_middle_before = span_middle.with(
|
192
|
+
dates: dates_before,
|
193
|
+
amount: amount_before,
|
194
|
+
)
|
195
|
+
|
196
|
+
dates_after = date..span_middle.dates.end
|
197
|
+
amount_after = (span_middle.amount || 0) * (dates_after.count / span_middle.dates.count.to_f)
|
198
|
+
span_middle_after = span_middle.with(
|
199
|
+
dates: dates_after,
|
200
|
+
amount: amount_after,
|
201
|
+
)
|
202
|
+
|
203
|
+
if span_middle_index.zero?
|
204
|
+
spans_before = [span_middle_before]
|
205
|
+
else
|
206
|
+
spans_before = [
|
207
|
+
*experience_middle.spans[..(span_middle_index - 1)],
|
208
|
+
span_middle_before,
|
209
|
+
]
|
210
|
+
end
|
211
|
+
|
212
|
+
spans_after = [
|
213
|
+
span_middle_after,
|
214
|
+
*experience_middle.spans[(span_middle_index + 1)..],
|
215
|
+
]
|
216
|
+
end
|
217
|
+
|
218
|
+
experience_middle_before = experience_middle.with(
|
219
|
+
spans: spans_before,
|
220
|
+
last_end_date: spans_before.map { _1.dates&.end }.compact.last,
|
221
|
+
)
|
222
|
+
experience_middle_after = experience_middle.with(
|
223
|
+
spans: spans_after,
|
224
|
+
)
|
225
|
+
|
226
|
+
experiences_before << experience_middle_before
|
227
|
+
experiences_after.unshift(*experience_middle_after)
|
228
|
+
end
|
229
|
+
|
230
|
+
# RM (alternate implementation)
|
231
|
+
# experiences_before = experiences
|
232
|
+
# .select(&:last_end_date)
|
233
|
+
# .select { _1.last_end_date < date }
|
234
|
+
# experiences_after = experiences
|
235
|
+
# .select { _1.spans.first.dates.nil? || _1.spans.first.dates.begin >= date }
|
236
|
+
|
237
|
+
# experiences_middle = experiences.select {
|
238
|
+
# _1.spans.first.dates.begin < date && _1.last_end_date >= date
|
239
|
+
# }
|
240
|
+
# experiences_middle.each do |experience_middle|
|
241
|
+
# spans_before = experience_middle
|
242
|
+
# .spans
|
243
|
+
# .select { _1.dates&.end }
|
244
|
+
# .select { _1.dates.end < date }
|
245
|
+
# spans_after = experience_middle
|
246
|
+
# .spans
|
247
|
+
# .select(&:dates)
|
248
|
+
# .select { _1.dates.begin >= date }
|
249
|
+
|
250
|
+
# span_middle = experience_middle
|
251
|
+
# .spans
|
252
|
+
# .find { _1.dates && _1.dates.begin < date && _1.dates.end >= date }
|
253
|
+
|
254
|
+
# middle_index = experience_middle.spans.index(span_middle)
|
255
|
+
# planned_spans_before = experience_middle
|
256
|
+
# .spans
|
257
|
+
# .map.with_index { |span, i|
|
258
|
+
# [i, span] if span.dates.nil? && i < middle_index
|
259
|
+
# }
|
260
|
+
# .compact
|
261
|
+
# planned_spans_after = experience_middle
|
262
|
+
# .spans
|
263
|
+
# .map.with_index { |span, i|
|
264
|
+
# [i, span] if span.dates.nil? && i > middle_index
|
265
|
+
# }
|
266
|
+
# .compact
|
267
|
+
|
268
|
+
# if span_middle
|
269
|
+
# dates_before = span_middle.dates.begin..date.prev_day
|
270
|
+
# amount_before = span_middle.amount * (dates_before.count / span_middle.dates.count.to_f)
|
271
|
+
# span_middle_before = span_middle.with(
|
272
|
+
# dates: dates_before,
|
273
|
+
# amount: amount_before,
|
274
|
+
# )
|
275
|
+
|
276
|
+
# dates_after = date..span_middle.dates.end
|
277
|
+
# amount_after = span_middle.amount * (dates_after.count / span_middle.dates.count.to_f)
|
278
|
+
# span_middle_after = span_middle.with(
|
279
|
+
# dates: dates_after,
|
280
|
+
# amount: amount_after,
|
281
|
+
# )
|
282
|
+
|
283
|
+
# spans_before = [*spans_before, span_middle_before]
|
284
|
+
# spans_after = [span_middle_after, *spans_after]
|
285
|
+
|
286
|
+
# planned_spans_before.each do |i, planned_span|
|
287
|
+
# spans_before.insert(i, planned_span)
|
288
|
+
# end
|
289
|
+
# planned_spans_after.each do |i, planned_span|
|
290
|
+
# spans_after.insert(i - middle_index, planned_span)
|
291
|
+
# end
|
292
|
+
# end
|
293
|
+
|
294
|
+
# experience_middle_before = experience_middle.with(
|
295
|
+
# spans: spans_before,
|
296
|
+
# last_end_date: spans_before.last.dates.end,
|
297
|
+
# )
|
298
|
+
# experience_middle_after = experience_middle.with(
|
299
|
+
# spans: spans_after,
|
300
|
+
# )
|
301
|
+
|
302
|
+
# experiences_before << experience_middle_before
|
303
|
+
# experiences_after = [experience_middle_after, *experiences_after]
|
304
|
+
# end
|
305
|
+
|
306
|
+
item_before = with_experiences(experiences_before)
|
307
|
+
item_after = with_experiences(experiences_after)
|
308
|
+
|
309
|
+
[item_before, item_after]
|
53
310
|
end
|
54
311
|
|
55
312
|
# Equality to another Item.
|
@@ -60,17 +317,16 @@ module Reading
|
|
60
317
|
raise ArgumentError, "An Item can be compared only with another Item."
|
61
318
|
end
|
62
319
|
|
63
|
-
|
320
|
+
data == other.send(:data)
|
64
321
|
end
|
65
322
|
|
66
323
|
private
|
67
324
|
|
68
|
-
# For each missing item attribute (key in
|
325
|
+
# For each missing item attribute (key in Config.hash[:item][:template]) in
|
69
326
|
# item_hash, adds the key and a filler value.
|
70
327
|
# @param item_hash [Hash]
|
71
|
-
|
72
|
-
|
73
|
-
config.deep_fetch(:item, :template).each do |k, v|
|
328
|
+
def add_missing_attributes_with_filler_values!(item_hash)
|
329
|
+
Config.hash.deep_fetch(:item, :template).each do |k, v|
|
74
330
|
next if item_hash.has_key?(k)
|
75
331
|
|
76
332
|
filler = v.is_a?(Array) ? [] : nil
|
@@ -78,39 +334,53 @@ module Reading
|
|
78
334
|
end
|
79
335
|
end
|
80
336
|
|
81
|
-
# Determines the status and the last end date
|
82
|
-
#
|
83
|
-
#
|
84
|
-
# the
|
337
|
+
# Determines the status and the last end date of each experience, and adds
|
338
|
+
# it into the experience hash. Note: for an item of indefinite length (e.g.
|
339
|
+
# podcast) there is a grace period during which the status remains
|
340
|
+
# :in_progress after the last activity. If that grace period is over, the
|
341
|
+
# status is :done. It's :planned if there are no spans with dates.
|
342
|
+
# @param item_hash [Hash]
|
85
343
|
# @return [Array(Symbol, Date)]
|
86
|
-
def
|
87
|
-
|
344
|
+
def add_statuses_and_last_end_dates!(item_hash)
|
345
|
+
item_hash[:experiences] = item_hash[:experiences].dup
|
88
346
|
|
89
|
-
|
90
|
-
|
347
|
+
item_hash[:experiences].each do |experience|
|
348
|
+
experience[:status] = :planned
|
349
|
+
experience[:last_end_date] = nil
|
91
350
|
|
92
|
-
|
351
|
+
next unless experience[:spans]&.any? { |span|
|
352
|
+
span[:dates]
|
353
|
+
}
|
93
354
|
|
94
|
-
|
95
|
-
.last
|
96
|
-
.spans
|
97
|
-
.select { |span| span.dates }
|
98
|
-
.last
|
99
|
-
.dates
|
100
|
-
.end
|
355
|
+
experience[:status] = :in_progress
|
101
356
|
|
102
|
-
|
357
|
+
experience[:last_end_date] = experience[:spans]
|
358
|
+
.select { |span| span[:dates] }
|
359
|
+
.last[:dates]
|
360
|
+
.end
|
103
361
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
362
|
+
next unless experience[:last_end_date]
|
363
|
+
|
364
|
+
# Whether this item has a fixed length, such as a book or audiobook (as
|
365
|
+
# opposed to e.g. an ongoing podcast).
|
366
|
+
has_definite_length =
|
367
|
+
!!item_hash[:variants][experience[:variant_index] || 0]&.dig(:length)
|
368
|
+
|
369
|
+
if has_definite_length
|
370
|
+
experience[:status] = :done
|
371
|
+
else
|
372
|
+
grace_period = Config.hash.deep_fetch(
|
373
|
+
:item,
|
374
|
+
:indefinite_in_progress_grace_period_days,
|
375
|
+
)
|
110
376
|
|
111
|
-
|
377
|
+
indefinite_in_progress_grace_period_is_over =
|
378
|
+
(Date.today - grace_period) > experience[:last_end_date]
|
112
379
|
|
113
|
-
|
380
|
+
if indefinite_in_progress_grace_period_is_over
|
381
|
+
experience[:status] = :done
|
382
|
+
end
|
383
|
+
end
|
114
384
|
end
|
115
385
|
end
|
116
386
|
end
|
@@ -6,13 +6,6 @@ module Reading
|
|
6
6
|
# parsed row (an intermediate hash) into item attributes, as in
|
7
7
|
# Config#default_config[:item][:template].
|
8
8
|
class Attribute
|
9
|
-
private attr_reader :config
|
10
|
-
|
11
|
-
# @param config [Hash] an entire config.
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
end
|
15
|
-
|
16
9
|
# Extracts this attribute's value from a parsed row.
|
17
10
|
# @param parsed_row [Hash] a parsed row (the intermediate hash).
|
18
11
|
# @param head_index [Integer] current item's position in the Head column.
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative 'spans_validator'
|
2
2
|
|
3
3
|
module Reading
|
4
4
|
module Parsing
|
@@ -10,13 +10,11 @@ module Reading
|
|
10
10
|
class DatesAndHeadTransformer
|
11
11
|
using Util::HashArrayDeepFetch
|
12
12
|
|
13
|
-
private attr_reader :
|
13
|
+
private attr_reader :parsed_row, :head_index
|
14
14
|
|
15
15
|
# @param parsed_row [Hash] a parsed row (the intermediate hash).
|
16
16
|
# @param head_index [Integer] current item's position in the Head column.
|
17
|
-
|
18
|
-
def initialize(parsed_row, head_index, config)
|
19
|
-
@config = config
|
17
|
+
def initialize(parsed_row, head_index)
|
20
18
|
@parsed_row = parsed_row
|
21
19
|
@head_index = head_index
|
22
20
|
end
|
@@ -31,7 +29,10 @@ module Reading
|
|
31
29
|
start_dates = Array.new(size) { |i| parsed_row[:start_dates]&.dig(i) || {} }
|
32
30
|
end_dates = Array.new(size) { |i| parsed_row[:end_dates]&.dig(i) || nil }
|
33
31
|
|
34
|
-
start_end_dates = start_dates
|
32
|
+
start_end_dates = start_dates
|
33
|
+
.reject { _1[:planned] }
|
34
|
+
.zip(end_dates)
|
35
|
+
.presence || [[{}, nil]]
|
35
36
|
|
36
37
|
experiences_with_dates = start_end_dates.map { |start_entry, end_entry|
|
37
38
|
{
|
@@ -42,8 +43,7 @@ module Reading
|
|
42
43
|
}.presence
|
43
44
|
|
44
45
|
if experiences_with_dates
|
45
|
-
|
46
|
-
Experiences::SpansValidator.validate(experiences_with_dates, config)
|
46
|
+
Experiences::SpansValidator.validate(experiences_with_dates)
|
47
47
|
end
|
48
48
|
|
49
49
|
experiences_with_dates
|
@@ -54,13 +54,13 @@ module Reading
|
|
54
54
|
# A shortcut to the experience template.
|
55
55
|
# @return [Hash]
|
56
56
|
def template
|
57
|
-
|
57
|
+
Config.hash.deep_fetch(:item, :template, :experiences).first
|
58
58
|
end
|
59
59
|
|
60
60
|
# A shortcut to the span template.
|
61
61
|
# @return [Hash]
|
62
62
|
def span_template
|
63
|
-
|
63
|
+
Config.hash.deep_fetch(:item, :template, :experiences, 0, :spans).first
|
64
64
|
end
|
65
65
|
|
66
66
|
# The :spans sub-attribute for the given pair of date entries.
|
@@ -84,14 +84,18 @@ module Reading
|
|
84
84
|
end
|
85
85
|
|
86
86
|
variant_index = (start_entry[:variant] || 1).to_i - 1
|
87
|
-
|
88
|
-
|
87
|
+
format = parsed_row[:sources]&.dig(variant_index)&.dig(:format) ||
|
88
|
+
parsed_row[:head][head_index][:format]
|
89
|
+
length = Attributes::Shared.length(parsed_row[:sources]&.dig(variant_index), format:) ||
|
90
|
+
Attributes::Shared.length(parsed_row[:length], format:)
|
91
|
+
no_end_date = !dates.end if dates &&
|
92
|
+
Config.hash.fetch(:enabled_columns).include?(:end_dates)
|
89
93
|
|
90
94
|
[
|
91
95
|
{
|
92
96
|
dates: dates,
|
93
97
|
amount: (length if dates),
|
94
|
-
progress: Attributes::Shared.progress(start_entry) ||
|
98
|
+
progress: Attributes::Shared.progress(start_entry, no_end_date:) ||
|
95
99
|
Attributes::Shared.progress(parsed_row[:head][head_index]) ||
|
96
100
|
(1.0 if end_entry),
|
97
101
|
name: span_template.fetch(:name),
|