reading 0.9.0 → 1.0.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 +33 -18
- data/lib/reading/config.rb +2 -2
- data/lib/reading/item/time_length.rb +2 -2
- data/lib/reading/item/view.rb +2 -2
- data/lib/reading/item.rb +15 -12
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +8 -3
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +151 -48
- data/lib/reading/parsing/attributes/experiences/spans_validator.rb +2 -2
- data/lib/reading/parsing/attributes/experiences.rb +3 -3
- data/lib/reading/parsing/attributes/shared.rb +4 -1
- data/lib/reading/parsing/csv.rb +4 -4
- data/lib/reading/parsing/parser.rb +6 -6
- data/lib/reading/parsing/rows/compact_planned.rb +4 -4
- data/lib/reading/parsing/rows/regular.rb +10 -10
- data/lib/reading/parsing/rows/regular_columns/sources.rb +1 -1
- data/lib/reading/parsing/rows/regular_columns/start_dates.rb +5 -1
- data/lib/reading/parsing/transformer.rb +9 -9
- data/lib/reading/stats/filter.rb +55 -51
- data/lib/reading/stats/grouping.rb +18 -4
- data/lib/reading/stats/operation.rb +104 -22
- data/lib/reading/stats/query.rb +7 -7
- data/lib/reading/stats/result_formatters.rb +140 -0
- data/lib/reading/util/hash_array_deep_fetch.rb +1 -23
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +7 -7
- metadata +46 -22
- data/lib/reading/stats/terminal_result_formatters.rb +0 -91
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            require "bigdecimal/util"
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Reading
         | 
| 2 4 | 
             
              module Stats
         | 
| 3 5 | 
             
                # The beginning of a query which specifies what it does, e.g.
         | 
| @@ -50,14 +52,14 @@ module Reading | |
| 50 52 | 
             
                  # Items) of the given hash of grouped items.
         | 
| 51 53 | 
             
                  # @param grouped_items [Hash]
         | 
| 52 54 | 
             
                  # @yield [Array<Item>]
         | 
| 53 | 
            -
                  def self.apply_to_inner_items(grouped_items, &)
         | 
| 55 | 
            +
                  def self.apply_to_inner_items(grouped_items, &block)
         | 
| 54 56 | 
             
                    if grouped_items.values.first.is_a? Array
         | 
| 55 57 | 
             
                      grouped_items.transform_values! { |inner_items|
         | 
| 56 58 | 
             
                        yield inner_items
         | 
| 57 59 | 
             
                      }
         | 
| 58 60 | 
             
                    else # It's a Hash, so go one level deeper.
         | 
| 59 61 | 
             
                      grouped_items.each do |group_name, grouped|
         | 
| 60 | 
            -
                        apply_to_inner_items(grouped, &)
         | 
| 62 | 
            +
                        apply_to_inner_items(grouped, &block)
         | 
| 61 63 | 
             
                      end
         | 
| 62 64 | 
             
                    end
         | 
| 63 65 | 
             
                  end
         | 
| @@ -89,7 +91,7 @@ module Reading | |
| 89 91 | 
             
                        (lengths.sum / lengths.count.to_f).to_i_if_whole
         | 
| 90 92 | 
             
                      end
         | 
| 91 93 | 
             
                    },
         | 
| 92 | 
            -
                    : | 
| 94 | 
            +
                    average_amount: proc { |items|
         | 
| 93 95 | 
             
                      total_amount = items.sum { |item|
         | 
| 94 96 | 
             
                        item.experiences.sum { |experience|
         | 
| 95 97 | 
             
                          experience.spans.sum(&:amount)
         | 
| @@ -105,6 +107,9 @@ module Reading | |
| 105 107 | 
             
                        amounts_by_date.values.sum / amounts_by_date.count
         | 
| 106 108 | 
             
                      end
         | 
| 107 109 | 
             
                    },
         | 
| 110 | 
            +
                    list_item: proc { |items|
         | 
| 111 | 
            +
                      items.map { |item| author_and_title(item) }
         | 
| 112 | 
            +
                    },
         | 
| 108 113 | 
             
                    total_item: proc { |items|
         | 
| 109 114 | 
             
                      items.count
         | 
| 110 115 | 
             
                    },
         | 
| @@ -112,77 +117,153 @@ module Reading | |
| 112 117 | 
             
                      items.sum { |item|
         | 
| 113 118 | 
             
                        item.experiences.sum { |experience|
         | 
| 114 119 | 
             
                          experience.spans.sum { |span|
         | 
| 115 | 
            -
                            (span.amount * span.progress).to_i_if_whole
         | 
| 120 | 
            +
                            (span.amount * (span.progress || 0.0)).to_i_if_whole
         | 
| 116 121 | 
             
                          }
         | 
| 117 122 | 
             
                        }
         | 
| 118 123 | 
             
                      }
         | 
| 119 124 | 
             
                    },
         | 
| 120 125 | 
             
                    top_rating: proc { |items, number_arg|
         | 
| 121 126 | 
             
                      items
         | 
| 122 | 
            -
                        .max_by(number_arg || DEFAULT_NUMBER_ARG, &:rating)
         | 
| 123 127 | 
             
                        .map { |item| [author_and_title(item), item.rating] }
         | 
| 128 | 
            +
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, rating|
         | 
| 129 | 
            +
                          rating || 0
         | 
| 130 | 
            +
                        }
         | 
| 124 131 | 
             
                    },
         | 
| 125 132 | 
             
                    top_length: proc { |items, number_arg|
         | 
| 126 133 | 
             
                      items
         | 
| 127 | 
            -
                        .map { |item| | 
| 134 | 
            +
                        .map { |item|
         | 
| 135 | 
            +
                          # Longest length, or if undefined length then longest experience
         | 
| 136 | 
            +
                          # (code adapted from top_amount below).
         | 
| 137 | 
            +
                          length = item.variants.map(&:length).max ||
         | 
| 138 | 
            +
                            item.experiences.map { |experience|
         | 
| 139 | 
            +
                              experience.spans.sum { |span|
         | 
| 140 | 
            +
                                (span.amount * (span.progress || 0.0)).to_i_if_whole
         | 
| 141 | 
            +
                              }
         | 
| 142 | 
            +
                            }.max
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                          [author_and_title(item), length]
         | 
| 145 | 
            +
                        }
         | 
| 128 146 | 
             
                        .reject { |_title, length| length.nil? }
         | 
| 129 | 
            -
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length| | 
| 147 | 
            +
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
         | 
| 148 | 
            +
                          length
         | 
| 149 | 
            +
                        }
         | 
| 130 150 | 
             
                    },
         | 
| 131 151 | 
             
                    top_amount: proc { |items, number_arg|
         | 
| 132 152 | 
             
                      items
         | 
| 133 153 | 
             
                        .map { |item|
         | 
| 134 154 | 
             
                          amount = item.experiences.sum { |experience|
         | 
| 135 155 | 
             
                            experience.spans.sum { |span|
         | 
| 136 | 
            -
                              (span.amount * span.progress).to_i_if_whole
         | 
| 156 | 
            +
                              (span.amount * (span.progress || 0.0)).to_i_if_whole
         | 
| 137 157 | 
             
                            }
         | 
| 138 158 | 
             
                          }
         | 
| 139 159 |  | 
| 140 160 | 
             
                          [author_and_title(item), amount]
         | 
| 141 161 | 
             
                        }
         | 
| 142 162 | 
             
                        .reject { |_title, amount| amount.zero? }
         | 
| 143 | 
            -
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount| | 
| 163 | 
            +
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
         | 
| 164 | 
            +
                          amount
         | 
| 165 | 
            +
                        }
         | 
| 144 166 | 
             
                    },
         | 
| 145 167 | 
             
                    top_speed: proc { |items, number_arg|
         | 
| 146 168 | 
             
                      items
         | 
| 147 | 
            -
                        .map { |item| | 
| 169 | 
            +
                        .map { |item|
         | 
| 170 | 
            +
                          speed = calculate_speed(item)
         | 
| 171 | 
            +
                          [author_and_title(item), speed] if speed
         | 
| 172 | 
            +
                        }
         | 
| 148 173 | 
             
                        .compact
         | 
| 149 174 | 
             
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, speed_hash|
         | 
| 150 175 | 
             
                          speed_hash[:amount] / speed_hash[:days].to_f
         | 
| 151 176 | 
             
                        }
         | 
| 152 177 | 
             
                    },
         | 
| 178 | 
            +
                    top_experience: proc { |items, number_arg|
         | 
| 179 | 
            +
                      items
         | 
| 180 | 
            +
                        .map { |item|
         | 
| 181 | 
            +
                          experience_count = item
         | 
| 182 | 
            +
                            .experiences
         | 
| 183 | 
            +
                            .count { |experience|
         | 
| 184 | 
            +
                              experience.spans.all? { _1.progress.to_d == "1.0".to_d }
         | 
| 185 | 
            +
                            }
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                          [author_and_title(item), [experience_count, item.rating || 0]]
         | 
| 188 | 
            +
                        }
         | 
| 189 | 
            +
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, experience_count_and_rating|
         | 
| 190 | 
            +
                          experience_count_and_rating
         | 
| 191 | 
            +
                        }
         | 
| 192 | 
            +
                        .map { |title, (experience_count, _rating)|
         | 
| 193 | 
            +
                          [title, experience_count]
         | 
| 194 | 
            +
                        }
         | 
| 195 | 
            +
                    },
         | 
| 196 | 
            +
                    top_note: proc { |items, number_arg|
         | 
| 197 | 
            +
                      items
         | 
| 198 | 
            +
                        .map { |item|
         | 
| 199 | 
            +
                          notes_word_count = item
         | 
| 200 | 
            +
                            .notes
         | 
| 201 | 
            +
                            .sum { |note|
         | 
| 202 | 
            +
                              note.content.scan(/[\w[:punct:]]+/).count
         | 
| 203 | 
            +
                            }
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                          [author_and_title(item), notes_word_count]
         | 
| 206 | 
            +
                        }
         | 
| 207 | 
            +
                        .max_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, notes_word_count|
         | 
| 208 | 
            +
                          notes_word_count
         | 
| 209 | 
            +
                        }
         | 
| 210 | 
            +
                    },
         | 
| 153 211 | 
             
                    bottom_rating: proc { |items, number_arg|
         | 
| 154 212 | 
             
                      items
         | 
| 155 | 
            -
                        .min_by(number_arg || DEFAULT_NUMBER_ARG, &:rating)
         | 
| 156 213 | 
             
                        .map { |item| [author_and_title(item), item.rating] }
         | 
| 214 | 
            +
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, rating|
         | 
| 215 | 
            +
                          rating || 0
         | 
| 216 | 
            +
                        }
         | 
| 157 217 | 
             
                    },
         | 
| 158 218 | 
             
                    bottom_length: proc { |items, number_arg|
         | 
| 159 219 | 
             
                      items
         | 
| 160 | 
            -
                        .map { |item| | 
| 220 | 
            +
                        .map { |item|
         | 
| 221 | 
            +
                          # Longest length, or if undefined length then longest experience
         | 
| 222 | 
            +
                          # (code adapted from bottom_amount below).
         | 
| 223 | 
            +
                          length = item.variants.map(&:length).max ||
         | 
| 224 | 
            +
                            item.experiences.map { |experience|
         | 
| 225 | 
            +
                              experience.spans.sum { |span|
         | 
| 226 | 
            +
                                (span.amount * (span.progress || 0.0)).to_i_if_whole
         | 
| 227 | 
            +
                              }
         | 
| 228 | 
            +
                            }.max
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                          [author_and_title(item), length]
         | 
| 231 | 
            +
                        }
         | 
| 161 232 | 
             
                        .reject { |_title, length| length.nil? }
         | 
| 162 | 
            -
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length| | 
| 233 | 
            +
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, length|
         | 
| 234 | 
            +
                          length
         | 
| 235 | 
            +
                        }
         | 
| 163 236 | 
             
                    },
         | 
| 164 237 | 
             
                    bottom_amount: proc { |items, number_arg|
         | 
| 165 238 | 
             
                      items
         | 
| 166 239 | 
             
                        .map { |item|
         | 
| 167 240 | 
             
                          amount = item.experiences.sum { |experience|
         | 
| 168 241 | 
             
                            experience.spans.sum { |span|
         | 
| 169 | 
            -
                              (span.amount * span.progress).to_i_if_whole
         | 
| 242 | 
            +
                              (span.amount * (span.progress || 0.0)).to_i_if_whole
         | 
| 170 243 | 
             
                            }
         | 
| 171 244 | 
             
                          }
         | 
| 172 245 |  | 
| 173 246 | 
             
                          [author_and_title(item), amount]
         | 
| 174 247 | 
             
                        }
         | 
| 175 248 | 
             
                        .reject { |_title, amount| amount.zero? }
         | 
| 176 | 
            -
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount| | 
| 249 | 
            +
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, amount|
         | 
| 250 | 
            +
                          amount
         | 
| 251 | 
            +
                        }
         | 
| 177 252 | 
             
                    },
         | 
| 178 253 | 
             
                    bottom_speed: proc { |items, number_arg|
         | 
| 179 254 | 
             
                      items
         | 
| 180 | 
            -
                        .map { |item| | 
| 255 | 
            +
                        .map { |item|
         | 
| 256 | 
            +
                          speed = calculate_speed(item)
         | 
| 257 | 
            +
                          [author_and_title(item), speed] if speed
         | 
| 258 | 
            +
                        }
         | 
| 181 259 | 
             
                        .compact
         | 
| 182 260 | 
             
                        .min_by(number_arg || DEFAULT_NUMBER_ARG) { |_title, speed_hash|
         | 
| 183 261 | 
             
                          speed_hash[:amount] / speed_hash[:days].to_f
         | 
| 184 262 | 
             
                        }
         | 
| 185 263 | 
             
                    },
         | 
| 264 | 
            +
                    debug: proc { |items|
         | 
| 265 | 
            +
                      items
         | 
| 266 | 
            +
                    },
         | 
| 186 267 | 
             
                  }
         | 
| 187 268 |  | 
| 188 269 | 
             
                  ALIASES = {
         | 
| @@ -190,20 +271,24 @@ module Reading | |
| 190 271 | 
             
                    average_length: %w[al],
         | 
| 191 272 | 
             
                    average_amount: %w[aia ai],
         | 
| 192 273 | 
             
                    :"average_daily-amount" => %w[ada ad],
         | 
| 274 | 
            +
                    list_item: %w[li list],
         | 
| 193 275 | 
             
                    total_item: %w[item count],
         | 
| 194 276 | 
             
                    total_amount: %w[amount],
         | 
| 195 277 | 
             
                    top_rating: %w[tr],
         | 
| 196 278 | 
             
                    top_length: %w[tl],
         | 
| 197 279 | 
             
                    top_amount: %w[ta],
         | 
| 198 280 | 
             
                    top_speed: %w[ts],
         | 
| 281 | 
            +
                    top_experience: %w[te],
         | 
| 282 | 
            +
                    top_note: %w[tn],
         | 
| 199 283 | 
             
                    bottom_rating: %w[br],
         | 
| 200 284 | 
             
                    bottom_length: %w[bl],
         | 
| 201 285 | 
             
                    bottom_amount: %w[ba],
         | 
| 202 286 | 
             
                    bottom_speed: %w[bs],
         | 
| 287 | 
            +
                    debug: %w[d],
         | 
| 203 288 | 
             
                  }
         | 
| 204 289 |  | 
| 205 290 | 
             
                  REGEXES = ACTIONS.map { |key, _action|
         | 
| 206 | 
            -
                    first_word, second_word = key.to_s.split( | 
| 291 | 
            +
                    first_word, second_word = key.to_s.split("_")
         | 
| 207 292 | 
             
                    aliases = ALIASES.fetch(key)
         | 
| 208 293 |  | 
| 209 294 | 
             
                    regex =
         | 
| @@ -228,7 +313,7 @@ module Reading | |
| 228 313 | 
             
                        (
         | 
| 229 314 | 
             
                          \A
         | 
| 230 315 | 
             
                          \s*
         | 
| 231 | 
            -
                          (#{aliases.join( | 
| 316 | 
            +
                          (#{aliases.join("|")})
         | 
| 232 317 | 
             
                          s?
         | 
| 233 318 | 
             
                          \s*
         | 
| 234 319 | 
             
                          (?<number_arg>
         | 
| @@ -296,10 +381,7 @@ module Reading | |
| 296 381 |  | 
| 297 382 | 
             
                    return nil unless speeds.any?
         | 
| 298 383 |  | 
| 299 | 
            -
                     | 
| 300 | 
            -
                      .max_by { |hash| hash[:amount] / hash[:days].to_f }
         | 
| 301 | 
            -
             | 
| 302 | 
            -
                    [author_and_title(item), speed]
         | 
| 384 | 
            +
                    speeds.max_by { |hash| hash[:amount] / hash[:days].to_f }
         | 
| 303 385 | 
             
                  end
         | 
| 304 386 |  | 
| 305 387 | 
             
                  # A shorter version of Item::View#name.
         | 
    
        data/lib/reading/stats/query.rb
    CHANGED
    
    | @@ -1,7 +1,8 @@ | |
| 1 | 
            -
            require  | 
| 2 | 
            -
            require_relative  | 
| 3 | 
            -
            require_relative  | 
| 4 | 
            -
            require_relative  | 
| 1 | 
            +
            require "pastel"
         | 
| 2 | 
            +
            require_relative "operation"
         | 
| 3 | 
            +
            require_relative "filter"
         | 
| 4 | 
            +
            require_relative "grouping"
         | 
| 5 | 
            +
            require_relative "result_formatters"
         | 
| 5 6 |  | 
| 6 7 | 
             
            module Reading
         | 
| 7 8 | 
             
              module Stats
         | 
| @@ -13,9 +14,8 @@ module Reading | |
| 13 14 | 
             
                  # @param items [Array<Item>] the Items to be queried.
         | 
| 14 15 | 
             
                  # @param result_formatters [Boolean, Hash{Symbol => Proc}] to alter the
         | 
| 15 16 | 
             
                  #   appearance of results; keys should be from among the keys of
         | 
| 16 | 
            -
                  #   Operation::ACTIONS. Pre-made formatters  | 
| 17 | 
            -
                   | 
| 18 | 
            -
                  def initialize(input:, items:, result_formatters: {})
         | 
| 17 | 
            +
                  #   Operation::ACTIONS. Pre-made formatters are in result_formatters.rb.
         | 
| 18 | 
            +
                  def initialize(input:, items:, result_formatters: Reading::Stats::ResultFormatters::TRUNCATED_TITLES)
         | 
| 19 19 | 
             
                    @input = input
         | 
| 20 20 | 
             
                    @items = items
         | 
| 21 21 | 
             
                    @result_formatters = result_formatters
         | 
| @@ -0,0 +1,140 @@ | |
| 1 | 
            +
            require "pastel"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Reading
         | 
| 4 | 
            +
              module Stats
         | 
| 5 | 
            +
                module ResultFormatters
         | 
| 6 | 
            +
                  TRUNCATED_TITLES = {
         | 
| 7 | 
            +
                    top_length: ->(result) { with_truncated_title(result) },
         | 
| 8 | 
            +
                    top_amount: ->(result) { with_truncated_title(result) },
         | 
| 9 | 
            +
                    top_speed: ->(result) { with_truncated_title(result) },
         | 
| 10 | 
            +
                    top_experience: ->(result) { with_truncated_title(result) },
         | 
| 11 | 
            +
                    top_note: ->(result) { with_truncated_title(result) },
         | 
| 12 | 
            +
                    bottom_length: ->(result) { with_truncated_title(result) },
         | 
| 13 | 
            +
                    botom_amount: ->(result) { with_truncated_title(result) },
         | 
| 14 | 
            +
                    bottom_speed: ->(result) { with_truncated_title(result) },
         | 
| 15 | 
            +
                  }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  TERMINAL = {
         | 
| 18 | 
            +
                    average_length: ->(result) { length_to_s(result) },
         | 
| 19 | 
            +
                    average_amount: ->(result) { length_to_s(result) },
         | 
| 20 | 
            +
                    :"average_daily-amount" => ->(result) { "#{length_to_s(result)} per day" },
         | 
| 21 | 
            +
                    total_item: ->(result) {
         | 
| 22 | 
            +
                      if result.zero?
         | 
| 23 | 
            +
                        PASTEL.bright_black("none")
         | 
| 24 | 
            +
                      else
         | 
| 25 | 
            +
                        color("#{result} #{result == 1 ? "item" : "items"}")
         | 
| 26 | 
            +
                      end
         | 
| 27 | 
            +
                    },
         | 
| 28 | 
            +
                    total_amount: ->(result) { length_to_s(result) },
         | 
| 29 | 
            +
                    top_rating: ->(result) { top_or_bottom_numbers_string(result, noun: "star") },
         | 
| 30 | 
            +
                    top_length: ->(result) { top_or_bottom_lengths_string(result) },
         | 
| 31 | 
            +
                    top_amount: ->(result) { top_or_bottom_lengths_string(result) },
         | 
| 32 | 
            +
                    top_speed: ->(result) { top_or_bottom_speeds_string(result) },
         | 
| 33 | 
            +
                    top_experience: ->(result) { top_or_bottom_numbers_string(result, noun: "experience") },
         | 
| 34 | 
            +
                    top_note: ->(result) { top_or_bottom_numbers_string(result, noun: "word") },
         | 
| 35 | 
            +
                    bottom_rating: ->(result) { top_or_bottom_numbers_string(result, noun: "star") },
         | 
| 36 | 
            +
                    bottom_length: ->(result) { top_or_bottom_lengths_string(result) },
         | 
| 37 | 
            +
                    botom_amount: ->(result) { top_or_bottom_lengths_string(result) },
         | 
| 38 | 
            +
                    bottom_speed: ->(result) { top_or_bottom_speeds_string(result) },
         | 
| 39 | 
            +
                  }
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  PASTEL = Pastel.new
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  # Applies a terminal color.
         | 
| 46 | 
            +
                  # @param string [String]
         | 
| 47 | 
            +
                  # @return [String]
         | 
| 48 | 
            +
                  private_class_method def self.color(string)
         | 
| 49 | 
            +
                    PASTEL.bright_blue(string)
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  # Converts a length/amount (pages or time) into a string.
         | 
| 53 | 
            +
                  # @param length [Numeric, Reading::Item::TimeLength]
         | 
| 54 | 
            +
                  # @param color [Boolean] whether a terminal color should be applied.
         | 
| 55 | 
            +
                  # @return [String]
         | 
| 56 | 
            +
                  private_class_method def self.length_to_s(length, color: true)
         | 
| 57 | 
            +
                    if length.is_a?(Numeric)
         | 
| 58 | 
            +
                      length_string = "#{length.round} pages"
         | 
| 59 | 
            +
                    else
         | 
| 60 | 
            +
                      length_string = length.to_s
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    color ? color(length_string) : length_string
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  # Formats a list of top/bottom length results as a string.
         | 
| 67 | 
            +
                  # @param result [Array]
         | 
| 68 | 
            +
                  # @return [String]
         | 
| 69 | 
            +
                  private_class_method def self.top_or_bottom_lengths_string(result)
         | 
| 70 | 
            +
                    offset = result.count.digits.count
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    result
         | 
| 73 | 
            +
                      .map.with_index { |(title, length), index|
         | 
| 74 | 
            +
                        pad = " " * (offset - (index + 1).digits.count)
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                        title_line = "#{index + 1}. #{pad}#{title}"
         | 
| 77 | 
            +
                        indent = "    #{" " * offset}"
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                        "#{title_line}\n#{indent}#{length_to_s(length)}"
         | 
| 80 | 
            +
                      }
         | 
| 81 | 
            +
                      .join("\n")
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  # Formats a list of top/bottom speed results as a string.
         | 
| 85 | 
            +
                  # @param result [Array]
         | 
| 86 | 
            +
                  # @return [String]
         | 
| 87 | 
            +
                  private_class_method def self.top_or_bottom_speeds_string(result)
         | 
| 88 | 
            +
                    offset = result.count.digits.count
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    result
         | 
| 91 | 
            +
                      .map.with_index { |(title, hash), index|
         | 
| 92 | 
            +
                        amount = length_to_s(hash[:amount], color: false)
         | 
| 93 | 
            +
                        days = "#{hash[:days]} #{hash[:days] == 1 ? "day" : "days"}"
         | 
| 94 | 
            +
                        pad = " " * (offset - (index + 1).digits.count)
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                        title_line = "#{index + 1}. #{pad}#{title}"
         | 
| 97 | 
            +
                        indent = "    #{" " * offset}"
         | 
| 98 | 
            +
                        colored_speed = color("#{amount} in #{days}")
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                        "#{title_line}\n#{indent}#{colored_speed}"
         | 
| 101 | 
            +
                      }
         | 
| 102 | 
            +
                      .join("\n")
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  # Formats a list of top/bottom number results as a string.
         | 
| 106 | 
            +
                  private_class_method def self.top_or_bottom_numbers_string(result, noun:)
         | 
| 107 | 
            +
                    offset = result.count.digits.count
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    result
         | 
| 110 | 
            +
                      .map.with_index { |(title, number), index|
         | 
| 111 | 
            +
                        pad = " " * (offset - (index + 1).digits.count)
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                        title_line = "#{index + 1}. #{pad}#{title}"
         | 
| 114 | 
            +
                        indent = "    #{" " * offset}"
         | 
| 115 | 
            +
                        number_string = color("#{number} #{number == 1 ? noun : "#{noun}s"}")
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                        "#{title_line}\n#{indent}#{number_string}"
         | 
| 118 | 
            +
                      }
         | 
| 119 | 
            +
                      .join("\n")
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  # Truncates the title of each result to a specified length.
         | 
| 123 | 
            +
                  # @param result [Array]
         | 
| 124 | 
            +
                  # @param length [Integer] the maximum length of the title.
         | 
| 125 | 
            +
                  # @return [Array]
         | 
| 126 | 
            +
                  private_class_method def self.with_truncated_title(result, length: 45)
         | 
| 127 | 
            +
                    result.map do |title, value|
         | 
| 128 | 
            +
                      truncated_title =
         | 
| 129 | 
            +
                        if title.length + 1 > length
         | 
| 130 | 
            +
                          "#{title[0...length]}…"
         | 
| 131 | 
            +
                        else
         | 
| 132 | 
            +
                          title
         | 
| 133 | 
            +
                        end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                      [truncated_title, value]
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
            end
         | 
| @@ -1,31 +1,9 @@ | |
| 1 1 | 
             
            module Reading
         | 
| 2 2 | 
             
              module Util
         | 
| 3 | 
            -
                class FetchDepthExceededError < StandardError
         | 
| 4 | 
            -
                end
         | 
| 5 | 
            -
             | 
| 6 3 | 
             
                # Similar to Array#dig and Hash#dig but raises an error for not found elements.
         | 
| 7 | 
            -
                #
         | 
| 8 | 
            -
                # More flexible but slightly slower alternative:
         | 
| 9 | 
            -
                #   keys.reduce(self) { |a, e| a.fetch(e) }
         | 
| 10 | 
            -
                #
         | 
| 11 | 
            -
                # See performance comparisons:
         | 
| 12 | 
            -
                # https://fpsvogel.com/posts/2022/ruby-hash-dot-syntax-deep-fetch
         | 
| 13 4 | 
             
                module HashArrayDeepFetch
         | 
| 14 5 | 
             
                  def deep_fetch(*keys)
         | 
| 15 | 
            -
                     | 
| 16 | 
            -
                    when 1
         | 
| 17 | 
            -
                      fetch(keys[0])
         | 
| 18 | 
            -
                    when 2
         | 
| 19 | 
            -
                      fetch(keys[0]).fetch(keys[1])
         | 
| 20 | 
            -
                    when 3
         | 
| 21 | 
            -
                      fetch(keys[0]).fetch(keys[1]).fetch(keys[2])
         | 
| 22 | 
            -
                    when 4
         | 
| 23 | 
            -
                      fetch(keys[0]).fetch(keys[1]).fetch(keys[2]).fetch(keys[3])
         | 
| 24 | 
            -
                    when 5
         | 
| 25 | 
            -
                      fetch(keys[0]).fetch(keys[1]).fetch(keys[2]).fetch(keys[3]).fetch(keys[4])
         | 
| 26 | 
            -
                    else
         | 
| 27 | 
            -
                      raise FetchDepthExceededError, "#deep_fetch can't fetch that deep!"
         | 
| 28 | 
            -
                    end
         | 
| 6 | 
            +
                    keys.reduce(self) { |a, e| a.fetch(e) }
         | 
| 29 7 | 
             
                  end
         | 
| 30 8 |  | 
| 31 9 | 
             
                  refine Hash do
         | 
    
        data/lib/reading/version.rb
    CHANGED
    
    
    
        data/lib/reading.rb
    CHANGED
    
    | @@ -1,10 +1,10 @@ | |
| 1 | 
            -
            Dir[File.join(__dir__,  | 
| 2 | 
            -
            require_relative  | 
| 3 | 
            -
            require_relative  | 
| 4 | 
            -
            require_relative  | 
| 5 | 
            -
            require_relative  | 
| 6 | 
            -
            require_relative  | 
| 7 | 
            -
            require_relative  | 
| 1 | 
            +
            Dir[File.join(__dir__, "reading", "util", "*.rb")].each { |file| require file }
         | 
| 2 | 
            +
            require_relative "reading/errors"
         | 
| 3 | 
            +
            require_relative "reading/config"
         | 
| 4 | 
            +
            require_relative "reading/parsing/csv"
         | 
| 5 | 
            +
            require_relative "reading/filter"
         | 
| 6 | 
            +
            require_relative "reading/stats/query"
         | 
| 7 | 
            +
            require_relative "reading/item/time_length.rb"
         | 
| 8 8 |  | 
| 9 9 | 
             
            # The gem's public API. See https://github.com/fpsvogel/reading#usage
         | 
| 10 10 | 
             
            #
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: reading
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 1.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Felipe Vogel
         | 
| 8 | 
            -
            autorequire: 
         | 
| 9 8 | 
             
            bindir: bin
         | 
| 10 9 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 10 | 
            +
            date: 1980-01-02 00:00:00.000000000 Z
         | 
| 12 11 | 
             
            dependencies:
         | 
| 13 12 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 13 | 
             
              name: pastel
         | 
| @@ -24,48 +23,76 @@ dependencies: | |
| 24 23 | 
             
                - - "~>"
         | 
| 25 24 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 25 | 
             
                    version: '0.8'
         | 
| 26 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 27 | 
            +
              name: amazing_print
         | 
| 28 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 29 | 
            +
                requirements:
         | 
| 30 | 
            +
                - - "~>"
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: '1.0'
         | 
| 33 | 
            +
              type: :runtime
         | 
| 34 | 
            +
              prerelease: false
         | 
| 35 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 36 | 
            +
                requirements:
         | 
| 37 | 
            +
                - - "~>"
         | 
| 38 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 39 | 
            +
                    version: '1.0'
         | 
| 40 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 41 | 
            +
              name: bigdecimal
         | 
| 42 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 43 | 
            +
                requirements:
         | 
| 44 | 
            +
                - - "~>"
         | 
| 45 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 46 | 
            +
                    version: '3.0'
         | 
| 47 | 
            +
              type: :runtime
         | 
| 48 | 
            +
              prerelease: false
         | 
| 49 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 50 | 
            +
                requirements:
         | 
| 51 | 
            +
                - - "~>"
         | 
| 52 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 53 | 
            +
                    version: '3.0'
         | 
| 27 54 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 55 | 
             
              name: debug
         | 
| 29 56 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 57 | 
             
                requirements:
         | 
| 31 58 | 
             
                - - "~>"
         | 
| 32 59 | 
             
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            -
                    version: '1. | 
| 60 | 
            +
                    version: '1.0'
         | 
| 34 61 | 
             
              type: :development
         | 
| 35 62 | 
             
              prerelease: false
         | 
| 36 63 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 64 | 
             
                requirements:
         | 
| 38 65 | 
             
                - - "~>"
         | 
| 39 66 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            -
                    version: '1. | 
| 67 | 
            +
                    version: '1.0'
         | 
| 41 68 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 42 69 | 
             
              name: minitest
         | 
| 43 70 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 71 | 
             
                requirements:
         | 
| 45 72 | 
             
                - - "~>"
         | 
| 46 73 | 
             
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            -
                    version: '5. | 
| 74 | 
            +
                    version: '5.0'
         | 
| 48 75 | 
             
              type: :development
         | 
| 49 76 | 
             
              prerelease: false
         | 
| 50 77 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 78 | 
             
                requirements:
         | 
| 52 79 | 
             
                - - "~>"
         | 
| 53 80 | 
             
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            -
                    version: '5. | 
| 81 | 
            +
                    version: '5.0'
         | 
| 55 82 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 56 83 | 
             
              name: minitest-reporters
         | 
| 57 84 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 85 | 
             
                requirements:
         | 
| 59 86 | 
             
                - - "~>"
         | 
| 60 87 | 
             
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            -
                    version: '1. | 
| 88 | 
            +
                    version: '1.0'
         | 
| 62 89 | 
             
              type: :development
         | 
| 63 90 | 
             
              prerelease: false
         | 
| 64 91 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 92 | 
             
                requirements:
         | 
| 66 93 | 
             
                - - "~>"
         | 
| 67 94 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            -
                    version: '1. | 
| 95 | 
            +
                    version: '1.0'
         | 
| 69 96 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 97 | 
             
              name: shoulda-context
         | 
| 71 98 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -95,34 +122,33 @@ dependencies: | |
| 95 122 | 
             
                  - !ruby/object:Gem::Version
         | 
| 96 123 | 
             
                    version: '1.0'
         | 
| 97 124 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 98 | 
            -
              name:  | 
| 125 | 
            +
              name: rake
         | 
| 99 126 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 100 127 | 
             
                requirements:
         | 
| 101 128 | 
             
                - - "~>"
         | 
| 102 129 | 
             
                  - !ruby/object:Gem::Version
         | 
| 103 | 
            -
                    version: ' | 
| 130 | 
            +
                    version: '13.0'
         | 
| 104 131 | 
             
              type: :development
         | 
| 105 132 | 
             
              prerelease: false
         | 
| 106 133 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 107 134 | 
             
                requirements:
         | 
| 108 135 | 
             
                - - "~>"
         | 
| 109 136 | 
             
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            -
                    version: ' | 
| 137 | 
            +
                    version: '13.0'
         | 
| 111 138 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 112 139 | 
             
              name: rubycritic
         | 
| 113 140 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 114 141 | 
             
                requirements:
         | 
| 115 142 | 
             
                - - "~>"
         | 
| 116 143 | 
             
                  - !ruby/object:Gem::Version
         | 
| 117 | 
            -
                    version: '4. | 
| 144 | 
            +
                    version: '4.0'
         | 
| 118 145 | 
             
              type: :development
         | 
| 119 146 | 
             
              prerelease: false
         | 
| 120 147 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 121 148 | 
             
                requirements:
         | 
| 122 149 | 
             
                - - "~>"
         | 
| 123 150 | 
             
                  - !ruby/object:Gem::Version
         | 
| 124 | 
            -
                    version: '4. | 
| 125 | 
            -
            description: 
         | 
| 151 | 
            +
                    version: '4.0'
         | 
| 126 152 | 
             
            email:
         | 
| 127 153 | 
             
            - fps.vogel@gmail.com
         | 
| 128 154 | 
             
            executables:
         | 
| @@ -173,7 +199,7 @@ files: | |
| 173 199 | 
             
            - lib/reading/stats/grouping.rb
         | 
| 174 200 | 
             
            - lib/reading/stats/operation.rb
         | 
| 175 201 | 
             
            - lib/reading/stats/query.rb
         | 
| 176 | 
            -
            - lib/reading/stats/ | 
| 202 | 
            +
            - lib/reading/stats/result_formatters.rb
         | 
| 177 203 | 
             
            - lib/reading/util/blank.rb
         | 
| 178 204 | 
             
            - lib/reading/util/exclude.rb
         | 
| 179 205 | 
             
            - lib/reading/util/hash_array_deep_fetch.rb
         | 
| @@ -189,24 +215,22 @@ metadata: | |
| 189 215 | 
             
              allowed_push_host: https://rubygems.org
         | 
| 190 216 | 
             
              homepage_uri: https://github.com/fpsvogel/reading
         | 
| 191 217 | 
             
              source_code_uri: https://github.com/fpsvogel/reading
         | 
| 192 | 
            -
              changelog_uri: https://github.com/fpsvogel/reading/blob/ | 
| 193 | 
            -
            post_install_message: 
         | 
| 218 | 
            +
              changelog_uri: https://github.com/fpsvogel/reading/blob/main/CHANGELOG.md
         | 
| 194 219 | 
             
            rdoc_options: []
         | 
| 195 220 | 
             
            require_paths:
         | 
| 196 221 | 
             
            - lib
         | 
| 197 222 | 
             
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 198 223 | 
             
              requirements:
         | 
| 199 | 
            -
              - - " | 
| 224 | 
            +
              - - "~>"
         | 
| 200 225 | 
             
                - !ruby/object:Gem::Version
         | 
| 201 | 
            -
                  version: 3. | 
| 226 | 
            +
                  version: 3.4.4
         | 
| 202 227 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 203 228 | 
             
              requirements:
         | 
| 204 229 | 
             
              - - ">="
         | 
| 205 230 | 
             
                - !ruby/object:Gem::Version
         | 
| 206 231 | 
             
                  version: '0'
         | 
| 207 232 | 
             
            requirements: []
         | 
| 208 | 
            -
            rubygems_version: 3. | 
| 209 | 
            -
            signing_key: 
         | 
| 233 | 
            +
            rubygems_version: 3.6.7
         | 
| 210 234 | 
             
            specification_version: 4
         | 
| 211 235 | 
             
            summary: Parses a CSV reading log.
         | 
| 212 236 | 
             
            test_files: []
         |