Class: VivlioStarter::CLI::Metrics::Analyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio_starter/cli/metrics/analyzer.rb

Overview

Markdown コンテンツを解析してメトリクスを算出する

Constant Summary collapse

SENTENCE_DELIMITER =
/[。!?!?]+/
CLAUSE_DELIMITER =
''
KANJI_PATTERN =
/\p{Han}/
DEFAULT_MATTR_WINDOW =

MATTR(移動平均 TTR)の既定の窓幅。Covington & McFall (2010) 由来で 50〜100 が標準、定番実装 quanteda の既定も 100。設定で上書き可能。

100

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content, config = {}) ⇒ Analyzer

Returns a new instance of Analyzer.



108
109
110
111
112
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 108

def initialize(content, config = {})
  @content = content
  @config = config
  @mecab_available = check_mecab_available
end

Class Method Details

.prose_length(content) ⇒ Integer

地の文の文字数(空白をすべて除いた長さ)。 vocabulary_stats.total_char_count と同じ数え方——分母を揃えるため、 語彙分析(MeCab を伴う)を走らせずに分量だけ欲しいときはこちらを使う。

Parameters:

  • content (String)

    Markdown 原文

Returns:

  • (Integer)


106
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 106

def self.prose_length(content) = prose_of(content).gsub(/\s/, '').length

.prose_of(content) ⇒ String

地の文(コード片と Markdown 記法を除いた本文)だけを取り出す。 索引語数の目安を本文の分量から導く経路(IndexSizeEstimator)が使う。 同じ除去処理を索引側へ書き写すと vs metrics の表示と黙ってずれるため、 ここを唯一の実装として公開する(index-term-selection-spec.md §3.3)。

Parameters:

  • content (String)

    Markdown 原文

Returns:

  • (String)

    地の文



99
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 99

def self.prose_of(content) = new(content).prose

Instance Method Details

#basic_statsObject

基本統計を算出する。 行数と chars / chars_no_newline は生の本文(コード込み)で数えるが、 文・節・読点は文章としての構造なので prose(コード除去後)で数える。

著者に見せる主役は本文で、コードと記法はその外数として添える (chapter-volume-calibration-data.md §7.1)。本文そのものは語彙分析の分母と 同じ値なので VocabularyStats#total_char_count を唯一の持ち主にし、ここでは 外数だけを持つ。全空白を除いた基準に揃えてあるため、本文と足すと原文に一致する。



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 122

def basic_stats
  sentences = sentence_segments(prose)
  clauses = clause_segments(prose)

  all_chars = content.gsub(/\s/, '').length
  without_code = Masking.strip_code(content).gsub(/\s/, '').length
  prose_chars = prose.gsub(/\s/, '').length

  BasicStats.new(
    chars: content.length,
    chars_no_newline: content.delete("\r\n").length,
    code_chars: all_chars - without_code,
    notation_chars: without_code - prose_chars,
    lines: content.empty? ? 0 : content.each_line.count,
    sentences: sentences.size,
    avg_sentence_len: safe_average(sentences.sum(&:length), sentences.size),
    clauses: clauses.size,
    avg_clause_len: safe_average(clauses.sum(&:length), clauses.size),
    commas: prose.count(CLAUSE_DELIMITER)
  )
end

#content_wordsObject

内容語(名詞・固有名詞・形容動詞・動詞・形容詞)を品詞ラベルつきで返す。 MeCab が使えない環境では空配列(D セクションは表示しない)。



174
175
176
177
178
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 174

def content_words
  return [] unless mecab_available

  extract_content_words(prose)
end

#proseObject

コード片を除いた解析対象本文(文構造・語彙・読解で共通利用)。 索引の分量算出(self.prose_length)からも使うため公開している。



191
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 191

def prose = @prose ||= extract_prose(content)

#readabilityObject

読解難度スコア(建石式 RS)を算出する



181
182
183
184
185
186
187
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 181

def readability
  features = Readability.extract_features(prose)
  score = Readability.score(features)
  label = Readability.label(score, readability_thresholds)

  ReadabilityScore.new(score:, label:, features:)
end

#vocabulary_statsObject

語彙分析を実行する(コード片は分析対象に含めない)



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/vivlio_starter/cli/metrics/analyzer.rb', line 145

def vocabulary_stats
  text = prose
  tokens = tokenize(text)
  unique = tokens.uniq
  tokens_map = build_token_frequencies(tokens)
  stripped = text.gsub(/\s/, '')
  total_chars = stripped.length
  kanji_count = stripped.scan(KANJI_PATTERN).size
  total_word_length = tokens.sum(&:length)

  VocabularyStats.new(
    kanji_ratio: calculate_kanji_ratio(kanji_count, total_chars),
    avg_word_length: safe_average(total_word_length, tokens.size),
    ttr: safe_average(unique.size.to_f, tokens.size),
    mattr: moving_average_ttr(tokens),
    total_tokens: tokens.size,
    unique_tokens: unique.size,
    kanji_char_count: kanji_count,
    hira_char_count: stripped.scan(/\p{Hiragana}/).size,
    kata_char_count: stripped.scan(/[\p{Katakana}ー]/).size,
    alpha_char_count: stripped.scan(/[A-Za-z]/).size,
    total_char_count: total_chars,
    total_word_length:,
    tokens_map: tokens_map
  )
end