Class: Countless::Statistics

Inherits:
Object
  • Object
show all
Defined in:
lib/countless/statistics.rb

Overview

The source code statistics displaying handler.

Heavily stolen from: bit.ly/3qpvgfu

rubocop:disable Metrics/ClassLength – because of the calculation and

formatting logic

Defined Under Namespace

Classes: Calculator

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*dirs) ⇒ Countless::Statistics

Initialize a new source code statistics displaying handler. When no configurations are passed in directly, we fallback to the configured statistics directories of the gem.

rubocop:disable Metrics/AbcSize – because of the directory/config

resolving

rubocop:disable Metrics/PerceivedComplexity – ditto rubocop:disable Metrics/CyclomaticComplexity – ditto rubocop:disable Metrics/MethodLength – ditto

Parameters:

  • dirs (Array<Hash{Symbol => Mixed}>)

    the configurations



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/countless/statistics.rb', line 26

def initialize(*dirs)
  base_path = Countless.configuration.base_path

  # Resolve the given directory configurations to actual files
  dirs = dirs.presence || Countless.statistic_directories
  @dirs = dirs.each_with_object([]) do |cur, memo|
    copy = cur.deep_dup
    copy[:files] = Array(copy[:files])

    if copy[:pattern].is_a? Regexp
      copy[:files] += Dir[
        File.join(copy[:dir] || base_path, '**/*')
      ].select { |path| File.file?(path) && copy[:pattern].match?(path) }
    else
      copy[:files] += Dir[copy[:pattern]]
    end

    copy[:files].uniq!
    memo << copy if copy[:files].present?
  end

  @statistics = calculate_statistics
  @total = calculate_total if @dirs.length > 1
end

Instance Attribute Details

#dirsObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def dirs
  @dirs
end

#statisticsObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def statistics
  @statistics
end

#totalObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def total
  @total
end

Instance Method Details

#calculate_codeInteger

Calculate the total lines of code.

Returns:

  • (Integer)

    the total lines of code



103
104
105
106
# File 'lib/countless/statistics.rb', line 103

def calculate_code
  @statistics.values.reject { |conf| conf[:test] }
             .map { |conf| conf[:stats].code_lines }.sum
end

#calculate_file_statistics(name, files) ⇒ Countless::Statistics::Calculator

Setup a new Calculator for the given directory/pattern in order to extract the individual file statistics and calculate the sub-totals.

We match the pattern against the individual file name and the relative file path. This allows top-level only matches.

Parameters:

  • name (String)

    the name/description/label of the directory

  • files (Array<String, Pathname>)

    the files to extract statistics from

Returns:



92
93
94
95
96
97
98
# File 'lib/countless/statistics.rb', line 92

def calculate_file_statistics(name, files)
  Calculator.new(name: name).tap do |calc|
    Cloc.stats(*files).each do |path, stats|
      calc.add_by_file_path(path, **stats)
    end
  end
end

#calculate_statisticsHash{String => Hash{Symbol => Mixed}}

Calculate all statistics for the configured directories and pass back a named hash.

Returns:

  • (Hash{String => Hash{Symbol => Mixed}})

    the statistics per configuration



71
72
73
74
75
76
77
78
79
# File 'lib/countless/statistics.rb', line 71

def calculate_statistics
  @dirs.to_h do |conf|
    [
      conf[:name],
      conf.merge(stats: calculate_file_statistics(conf[:name],
                                                  conf[:files]))
    ]
  end
end

#calculate_testsInteger

Calculate the total lines of testing code.

Returns:

  • (Integer)

    the total lines of testing code



111
112
113
114
# File 'lib/countless/statistics.rb', line 111

def calculate_tests
  @statistics.values.select { |conf| conf[:test] }
             .map { |conf| conf[:stats].code_lines }.sum
end

#calculate_totalCountless::Statistics::Calculator

Calculate the total statistics of all sub-statistics for the configured directories.

Returns:



59
60
61
62
63
64
# File 'lib/countless/statistics.rb', line 59

def calculate_total
  calculator = Calculator.new(name: 'Total')
  @statistics.values.each_with_object(calculator) do |conf, total|
    total.add(conf[:stats])
  end
end

#code_test_stats_lineString

Return the final meta statistics line.

Returns:

  • (String)

    the meta statistics line



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/countless/statistics.rb', line 189

def code_test_stats_line
  code  = calculate_code
  tests = calculate_tests
  ratio = tests.fdiv(code)
  ratio = '0' if ratio.nan?

  res = [
    "Code LOC: #{code}",
    "Test LOC: #{tests}",
    "Code to Test Ratio: 1:#{format('%.1f', ratio)}"
  ].join(' ' * 5)
  "  #{res}"
end

#to_sString

Convert the code statistics to a formatted string buffer.

rubocop:disable Metrics/MethodLength – because of the complex formatting

logic with fully dynamic columns widths

rubocop:disable Metrics/PerceivedComplexity – ditto rubocop:disable Metrics/CyclomaticComplexity – ditto rubocop:disable Metrics/AbcSize – ditto

Returns:

  • (String)

    the formatted code statistics



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/countless/statistics.rb', line 125

def to_s
  col_sizes = {}
  rows = to_table.map do |row|
    next row unless row.is_a?(Array)

    row = row.map(&:to_s)
    cols = row.map(&:length).each_with_index.map { |len, idx| [idx, [len]] }
    col_sizes.deep_merge!(cols.to_h) { |_, left, right| left + right }
    row
  end

  # Calculate the correct column sizes
  col_sizes = col_sizes.values.each_with_object([]) do |widths, memo|
    memo << (widths.max + 2)
  end

  # Enforce the correct column sizes per row
  splitter = ([0] + col_sizes + [0]).map { |size| '-' * size }.join('+')
  rows.each_with_object([]) do |row, memo|
    next memo << splitter if row == :splitter
    next memo << row if row.is_a?(String)

    cols = row.each_with_index.map do |col, idx|
      meth = idx.zero? ? :ljust : :rjust
      col.send(meth, col_sizes[idx] - 2)
    end
    memo << "| #{cols.join(' | ')} |"
  end.join("\n")
end

#to_tableArray<Array<String, Integer>, Symbol>

Convert the code statistics to a processable table structure. Each element in the resulting array is a single line, while array elements reflect columns. The special :splitter row value will be converted later by #to_s.

rubocop:disable Metrics/MethodLength – because of the table construction

Returns:

  • (Array<Array<String, Integer>, Symbol>)

    the raw table



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/countless/statistics.rb', line 167

def to_table
  table = [
    :splitter,
    %w[Name Lines LOC Comments Classes Methods M/C LOC/M],
    :splitter
  ]
  @statistics.each_value { |conf| table << conf[:stats].to_h.values }
  table << :splitter

  if @total
    table << @total.to_h.values
    table << :splitter
  end

  table << code_test_stats_line
  table
end