Module: VivlioStarter::CLI::Metrics::KanjiLevels

Defined in:
lib/vivlio_starter/cli/metrics/kanji_levels.rb

Constant Summary collapse

DATA_PATH =
File.expand_path('data/kyoiku_joyo_kanji.tsv', __dir__)
HAN =
/\p{Han}/
CHUGAKU =

同梱データで L1(中学で習う常用漢字)を表す値。これ以外の値は配当学年(1〜6)。

'中学'
GRADES =

教育漢字(L0)の配当学年。

(1..6).freeze
LABELS =

表示ラベルと内訳の並び順。

{ kyoiku: '教育', chugaku: '中学', ippan: '一般(L2)', senmon: '専門(L3)', gaiji: 'JIS外' }.freeze
ORDER =
%i[kyoiku chugaku ippan senmon gaiji].freeze
LIST_LABELS =

一覧・出現箇所の表示上限。

{ chugaku: '中学漢字', ippan: '一般漢字(L2)', senmon: '専門漢字(L3)' }.freeze
LOCATION_CHAR_LIMIT =
15
LOCATION_PER_CHAR =
5

Class Method Summary collapse

Class Method Details

.build_report(sentences) ⇒ Object

位置つきの文の列から、漢字レベルの集計レポートを作る。漢字が無ければ nil。



72
73
74
75
76
77
78
79
80
81
82
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 72

def build_report(sentences)
  totals = Hash.new(0)
  chars = {}

  sentences.each do |sentence|
    scan_kanji(sentence, totals, chars)
  end
  return nil if chars.empty?

  KanjiLevelReport.new(ratios: ratios(totals), lists: candidate_lists(chars), locations: locations(chars))
end

.candidate_lists(chars) ⇒ Object



129
130
131
132
133
134
135
136
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 129

def candidate_lists(chars)
  LIST_LABELS.keys.to_h do |level|
    entries = chars.select { |_char, rec| rec[:level] == level }
                   .sort_by { |char, rec| [-rec[:count], char] }
                   .map { |char, rec| [char, rec[:count]] }
    [level, entries]
  end
end

.grade_of(char) ⇒ Object

教育漢字(L0)の配当学年 1〜6 を返す。教育漢字でなければ nil。

レベル(L0〜L4)だけでは「小4向けの本なら小5以上にルビ」という学年基準の 判定ができないため、同梱データが持っている学年をそのまま取り出せるようにする (furigana-level-spec.md §3.2)。level_of の戻り値は変えないので、 vs metrics の集計・表示には影響しない。



64
65
66
67
68
69
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 64

def grade_of(char)
  grade = table[char]
  return nil if grade.nil? || grade == CHUGAKU

  grade.to_i
end

.jis_level(char) ⇒ Object

JIS X 0208 の区(EUC-JP の第 1 バイト)で第一/第二水準を分ける。



98
99
100
101
102
103
104
105
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 98

def jis_level(char)
  bytes = char.encode('EUC-JP').bytes
  return :gaiji unless bytes.size == 2

  (bytes[0] - 0xA0) <= 47 ? :ippan : :senmon
rescue Encoding::UndefinedConversionError
  :gaiji
end

.level_of(char) ⇒ Object

漢字 1 文字のレベルを返す。教育・中学は同梱データ、それ以外は符号位置で判定。 :kyoiku を返すのは grade_of が学年を返す文字と過不足なく一致する (判定を grade_of に委ねているため、両者がずれることが構造的に起こらない)。



51
52
53
54
55
56
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 51

def level_of(char)
  return :chugaku if table[char] == CHUGAKU
  return :kyoiku if grade_of(char)

  jis_level(char)
end

.load_tableObject



88
89
90
91
92
93
94
95
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 88

def load_table
  File.foreach(DATA_PATH, encoding: 'UTF-8').each_with_object({}) do |line, map|
    next if line.start_with?('#') || line.strip.empty?

    char, grade = line.chomp.split("\t")
    map[char] = grade
  end
end

.locations(chars) ⇒ Object

一般・専門漢字(稀でルビを振りたい)を位置つきで返す。 挙げる漢字は出現の少ない順に選ぶ(稀なものほどルビが要る)が、 並べる順は初出の章・行にする——著者は原稿を頭から開いてルビを書き足すため、 出現順に並んでいないとファイルを行き来することになる。



142
143
144
145
146
147
148
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 142

def locations(chars)
  chars.select { |_char, rec| %i[ippan senmon].include?(rec[:level]) }
       .sort_by { |char, rec| [rec[:count], char] }
       .first(LOCATION_CHAR_LIMIT)
       .map { |char, rec| [char, rec[:locations].uniq.first(LOCATION_PER_CHAR)] }
       .sort_by { |_char, places| places.first }
end

.ratios(totals) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 120

def ratios(totals)
  grand = totals.values.sum.to_f
  ORDER.filter_map do |level|
    next if totals[level].zero?

    [LABELS[level], (totals[level] / grand * 100).round]
  end
end

.scan_kanji(sentence, totals, chars) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 107

def scan_kanji(sentence, totals, chars)
  sentence.text.each_char do |char|
    next unless char.match?(HAN)

    level = level_of(char)
    totals[level] += 1
    record = (chars[char] ||= { level:, count: 0, locations: [] })
    record[:count] += 1
    # 出現箇所は [章番号, 行] のまま持ち、表示側で章ごとにまとめる。
    record[:locations] << [sentence.chapter_num, sentence.line]
  end
end

.tableObject

--- 内部ヘルパー ---



86
# File 'lib/vivlio_starter/cli/metrics/kanji_levels.rb', line 86

def table = @table ||= load_table