jlpt 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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.ja.md +246 -0
  4. data/README.md +224 -0
  5. data/assets/logo.png +0 -0
  6. data/bin/jlpt +7 -0
  7. data/lib/jlpt/active_model/validator.rb +43 -0
  8. data/lib/jlpt/analyzers/comparer.rb +44 -0
  9. data/lib/jlpt/analyzers/conjugation.rb +60 -0
  10. data/lib/jlpt/analyzers/corpus_stats.rb +38 -0
  11. data/lib/jlpt/analyzers/dokkai.rb +53 -0
  12. data/lib/jlpt/analyzers/exam_readiness.rb +52 -0
  13. data/lib/jlpt/analyzers/grammar.rb +50 -0
  14. data/lib/jlpt/analyzers/jukugo.rb +46 -0
  15. data/lib/jlpt/analyzers/loanwords.rb +54 -0
  16. data/lib/jlpt/analyzers/onomatopeia.rb +29 -0
  17. data/lib/jlpt/analyzers/pitch_accent.rb +51 -0
  18. data/lib/jlpt/analyzers/pos_profiler.rb +47 -0
  19. data/lib/jlpt/analyzers/simplifier.rb +48 -0
  20. data/lib/jlpt/analyzers/style_profiler.rb +62 -0
  21. data/lib/jlpt/analyzers/syntax.rb +43 -0
  22. data/lib/jlpt/cli.rb +121 -0
  23. data/lib/jlpt/core/analysis_result.rb +45 -0
  24. data/lib/jlpt/core/analyzer.rb +58 -0
  25. data/lib/jlpt/core/cache_manager.rb +90 -0
  26. data/lib/jlpt/core/config.rb +45 -0
  27. data/lib/jlpt/core/dictionary.rb +113 -0
  28. data/lib/jlpt/core/engine.rb +142 -0
  29. data/lib/jlpt/core/kanji.rb +54 -0
  30. data/lib/jlpt/core/kanji_details.rb +60 -0
  31. data/lib/jlpt/core/preprocessor.rb +48 -0
  32. data/lib/jlpt/core/tokenizer.rb +64 -0
  33. data/lib/jlpt/core/vocab_classifier.rb +48 -0
  34. data/lib/jlpt/data/kanji.json +2213 -0
  35. data/lib/jlpt/data/vocab.json +50298 -0
  36. data/lib/jlpt/formatters/batch_processor.rb +41 -0
  37. data/lib/jlpt/formatters/exporter.rb +55 -0
  38. data/lib/jlpt/formatters/furigana.rb +69 -0
  39. data/lib/jlpt/formatters/reporter.rb +72 -0
  40. data/lib/jlpt/version.rb +5 -0
  41. data/lib/jlpt.rb +50 -0
  42. metadata +184 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Corpus Lexical Diversity and Statistical Analyzer.
5
+ #
6
+ # Computes Type-Token Ratio (TTR), total word count, unique word count,
7
+ # and hapax legomena count for text.
8
+ #
9
+ module CorpusStats
10
+ class << self
11
+ # Analyze text corpus stats and lexical diversity (TTR)
12
+ #
13
+ # @param text [String] Japanese text
14
+ # @return [Hash] corpus statistics hash
15
+ def analyze(text)
16
+ cleaned = Preprocessor.clean(text)
17
+ return empty_stats if cleaned.empty?
18
+
19
+ tokens = Tokenizer.lemmata(cleaned)
20
+ return empty_stats if tokens.empty?
21
+
22
+ total = tokens.length
23
+ counts = tokens.tally
24
+ unique = counts.keys.length
25
+ hapax = counts.values.count(1)
26
+ ttr = (unique.to_f / total).round(2)
27
+
28
+ { total_words: total, unique_words: unique, hapax_count: hapax, ttr: ttr }
29
+ end
30
+
31
+ private
32
+
33
+ def empty_stats
34
+ { total_words: 0, unique_words: 0, hapax_count: 0, ttr: 0.0 }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Dokkai (Reading Comprehension) Readability Evaluator.
5
+ #
6
+ # Evaluates Japanese text complexity for reading comprehension tasks,
7
+ # sentence length distribution, particle density, and recommended target reading time.
8
+ #
9
+ module Dokkai
10
+ PARTICLES_REGEX = /[はがをにで行とからよりへね]/.freeze
11
+
12
+ class << self
13
+ # Evaluate reading comprehension complexity and estimated reading time
14
+ #
15
+ # @param text [String] Japanese text
16
+ # @return [Hash] Dokkai metrics including reading time and readability score
17
+ def evaluate(text)
18
+ cleaned = Preprocessor.clean(text)
19
+ return empty_evaluation if cleaned.empty?
20
+
21
+ build_evaluation_metrics(cleaned)
22
+ end
23
+
24
+ private
25
+
26
+ def build_evaluation_metrics(cleaned)
27
+ sents = Preprocessor.sentences(cleaned)
28
+ avg_len = (cleaned.length.to_f / [sents.length, 1].max).round(1)
29
+ particles = cleaned.scan(PARTICLES_REGEX).length
30
+
31
+ { readability_score: calculate_readability(avg_len, particles, cleaned.length),
32
+ estimated_reading_time_min: (cleaned.length / 400.0).round(2),
33
+ sentence_count: sents.length, avg_sentence_length: avg_len, particle_count: particles }
34
+ end
35
+
36
+ def calculate_readability(avg_len, particles, char_len)
37
+ density = (particles.to_f / [char_len, 1].max) * 100.0
38
+ score = (avg_len * 0.5) + (density * 2.0)
39
+ [score.round(2), 100.0].min
40
+ end
41
+
42
+ def empty_evaluation
43
+ {
44
+ readability_score: 0.0,
45
+ estimated_reading_time_min: 0.0,
46
+ sentence_count: 0,
47
+ avg_sentence_length: 0.0,
48
+ particle_count: 0
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Exam Readiness and Level Coverage Evaluator.
5
+ #
6
+ # Evaluates how well input text aligns with a target JLPT exam level (N5-N1),
7
+ # calculating target level vocabulary density and readiness score.
8
+ #
9
+ module ExamReadiness
10
+ LEVEL_THRESHOLD_MAP = { n5: 1, n4: 2, n3: 3, n2: 4, n1: 5 }.freeze
11
+
12
+ class << self
13
+ # Evaluate readiness score for a target JLPT level
14
+ #
15
+ # @param text [String] Japanese text
16
+ # @param target_level [Symbol, String] target exam level (:n5, :n4, :n3, :n2, :n1)
17
+ # @return [Hash] exam readiness metrics
18
+ def evaluate(text, target_level: :n3)
19
+ target_sym = target_level.to_s.downcase.to_sym
20
+ target_val = LEVEL_THRESHOLD_MAP[target_sym] || 3
21
+
22
+ res = JLPT.analyze(text)
23
+ current_val = LEVEL_THRESHOLD_MAP[res.recommended_level] || 1
24
+
25
+ readiness = calculate_readiness_score(current_val, target_val, res.score)
26
+ status = determine_readiness_status(readiness)
27
+
28
+ { target_level: target_sym, current_level: res.recommended_level,
29
+ readiness_score: readiness, status: status }
30
+ end
31
+
32
+ private
33
+
34
+ def calculate_readiness_score(current_val, target_val, score)
35
+ diff = current_val - target_val
36
+ if diff >= 0
37
+ 100.0
38
+ else
39
+ [(100.0 + (diff * 25.0) + (score * 0.2)).round(2), 0.0].max
40
+ end
41
+ end
42
+
43
+ def determine_readiness_status(score)
44
+ case score
45
+ when 90.0..100.0 then :ready
46
+ when 60.0..89.99 then :moderate
47
+ else :needs_practice
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # JLPT Grammar Pattern Detection Engine.
5
+ #
6
+ # Recognizes Japanese grammar constructions across levels N5 to N1.
7
+ #
8
+ module Grammar
9
+ PATTERNS = [
10
+ { pattern: /てはいけない|てはならない/, level: :n5, name: '〜てはいけない' },
11
+ { pattern: /てください|てくださいませんか/, level: :n5, name: '〜てください' },
12
+ { pattern: /なければならない|なければいけない/, level: :n5, name: '〜なければならない' },
13
+ { pattern: /ことができる|ことができない/, level: :n4, name: '〜ことができる' },
14
+ { pattern: /ようになっている/, level: :n4, name: '〜ようになっている' },
15
+ { pattern: /わけがない|わけではない/, level: :n3, name: '〜わけがない' },
16
+ { pattern: /に違いない/, level: :n2, name: '〜に違いない' },
17
+ { pattern: /ざるを得ない/, level: :n1, name: '〜ざるを得ない' }
18
+ ].freeze
19
+
20
+ class << self
21
+ # Detect grammar patterns in text
22
+ #
23
+ # @param text [String] Japanese text
24
+ # @return [Array<Hash>] array of detected grammar hashes
25
+ def extract(text)
26
+ cleaned = Preprocessor.clean(text)
27
+ return [] if cleaned.empty?
28
+
29
+ PATTERNS.filter_map do |rule|
30
+ { name: rule[:name], level: rule[:level], pattern: rule[:pattern] } if cleaned.match?(rule[:pattern])
31
+ end
32
+ end
33
+
34
+ # Breakdown of detected grammar patterns by JLPT level
35
+ #
36
+ # @param text [String] Japanese text
37
+ # @return [Hash{Symbol => Array<String>}]
38
+ def breakdown(text)
39
+ extracted = extract(text)
40
+ res = { n5: [], n4: [], n3: [], n2: [], n1: [] }
41
+
42
+ extracted.each do |g|
43
+ lvl = g[:level]
44
+ res[lvl] << g[:name] if res.key?(lvl)
45
+ end
46
+ res
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Jukugo (Kanji Compound) Extractor and Classifier.
5
+ #
6
+ # Identifies multi-kanji compound words (2-kanji, 3-kanji, 4-kanji Yojijukugo)
7
+ # in Japanese text.
8
+ #
9
+ module Jukugo
10
+ JUKUGO_REGEX = /[\u4E00-\u9FFF]{2,}/.freeze
11
+
12
+ class << self
13
+ # Extract all kanji compounds from Japanese text
14
+ #
15
+ # @param text [String] Japanese text
16
+ # @return [Array<String>] unique array of Jukugo compounds
17
+ def extract(text)
18
+ cleaned = Preprocessor.clean(text)
19
+ return [] if cleaned.empty?
20
+
21
+ cleaned.scan(JUKUGO_REGEX).uniq
22
+ end
23
+
24
+ # Breakdown compounds by character length (2-kanji, 3-kanji, 4-kanji+)
25
+ #
26
+ # @param text [String] Japanese text
27
+ # @return [Hash] hash with keys :two_char, :three_char, :yojijukugo, :long
28
+ def breakdown(text)
29
+ result = { two_char: [], three_char: [], yojijukugo: [], long: [] }
30
+ extract(text).each { |c| categorize_compound(c, result) }
31
+ result
32
+ end
33
+
34
+ private
35
+
36
+ def categorize_compound(compound, result)
37
+ case compound.length
38
+ when 2 then result[:two_char] << compound
39
+ when 3 then result[:three_char] << compound
40
+ when 4 then result[:yojijukugo] << compound
41
+ else result[:long] << compound
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Japanese Word Origin and Loanword (Gairaigo 外来語) Detector.
5
+ #
6
+ # Categorizes vocabulary into Katakana loanwords (Gairaigo), Sino-Japanese
7
+ # Kanji compounds (Kango), and native Japanese words (Wago).
8
+ #
9
+ module Loanwords
10
+ KATAKANA_REGEX = /^[\u30A0-\u30FFー]+$/.freeze
11
+ KANJI_REGEX = /[\u4E00-\u9FFF]/.freeze
12
+
13
+ class << self
14
+ # Extract all Katakana loanwords (Gairaigo) from text
15
+ #
16
+ # @param text [String] Japanese text
17
+ # @return [Array<String>] unique array of loanword lemmata
18
+ def extract(text)
19
+ cleaned = Preprocessor.clean(text)
20
+ return [] if cleaned.empty?
21
+
22
+ lemmas = Tokenizer.lemmata(cleaned)
23
+ lemmas.select { |lemma| lemma.match?(KATAKANA_REGEX) && lemma.length >= 2 }.uniq
24
+ end
25
+
26
+ # Categorize text vocabulary by word origin (Wago, Kango, Gairaigo)
27
+ #
28
+ # @param text [String] Japanese text
29
+ # @return [Hash{Symbol => Array<String>}] origin breakdown hash
30
+ def breakdown(text)
31
+ cleaned = Preprocessor.clean(text)
32
+ res = { gairaigo: [], kango: [], wago: [] }
33
+ return res if cleaned.empty?
34
+
35
+ Tokenizer.lemmata(cleaned).uniq.each do |lemma|
36
+ categorize_lemma(lemma, res)
37
+ end
38
+ res
39
+ end
40
+
41
+ private
42
+
43
+ def categorize_lemma(lemma, res)
44
+ if lemma.match?(KATAKANA_REGEX) && lemma.length >= 2
45
+ res[:gairaigo] << lemma
46
+ elsif lemma.match?(KANJI_REGEX)
47
+ res[:kango] << lemma
48
+ else
49
+ res[:wago] << lemma
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Onomatopoeia (擬音語 Giongo & 擬態語 Gitaito) Extractor.
5
+ #
6
+ # Identifies Japanese sound-symbolic and mimetic words in Hiragana or Katakana.
7
+ #
8
+ module Onomatopeia
9
+ ONOMATOPOEIA_PATTERNS = %w[
10
+ どきどき わくわく ぺらぺら にこにこ ふわふわ ぎっしり
11
+ ギリギリ イライラ キラキラ ドキドキ ニコニコ ペラペラ
12
+ もちもち すらすら どんどん だんだん いろいろ
13
+ ].freeze
14
+
15
+ class << self
16
+ # Extract onomatopoeia words from Japanese text
17
+ #
18
+ # @param text [String] Japanese text
19
+ # @return [Array<String>] unique array of onomatopoeias
20
+ def extract(text)
21
+ cleaned = Preprocessor.clean(text)
22
+ return [] if cleaned.empty?
23
+
24
+ matches = ONOMATOPOEIA_PATTERNS.select { |word| cleaned.include?(word) }
25
+ matches.uniq
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Pitch Accent Profiler and Pattern Lookup.
5
+ #
6
+ # Categorizes pitch accent types (Heiban / 平板 [0], Atamadaka / 頭高 [1], Nakadaka / 中高 [2+], Odaka / 尾高)
7
+ # for Japanese vocabulary words.
8
+ #
9
+ module PitchAccent
10
+ # Built-in pitch accent database for common words
11
+ ACCENT_MAP = {
12
+ '学校' => { type: :odaka, pattern: 0 },
13
+ '私' => { type: :heiban, pattern: 0 },
14
+ '日本' => { type: :atamadaka, pattern: 1 },
15
+ '猫' => { type: :atamadaka, pattern: 1 },
16
+ '犬' => { type: :atamadaka, pattern: 2 },
17
+ '本' => { type: :atamadaka, pattern: 1 },
18
+ '先生' => { type: :nakadaka, pattern: 3 }
19
+ }.freeze
20
+
21
+ class << self
22
+ # Lookup pitch accent for a vocabulary word
23
+ #
24
+ # @param word [String] vocabulary word
25
+ # @return [Hash, nil] pitch accent hash with :type and :pattern
26
+ def accent_for(word)
27
+ return nil if word.nil? || word.to_s.strip.empty?
28
+
29
+ ACCENT_MAP[word.to_s.strip] || { type: :heiban, pattern: 0 }
30
+ end
31
+
32
+ # Profile pitch accent patterns in Japanese text
33
+ #
34
+ # @param text [String] Japanese text
35
+ # @return [Hash] breakdown of pitch accent types in text
36
+ def profile(text)
37
+ tokens = Tokenizer.tokenize(text)
38
+ result = { heiban: 0, atamadaka: 0, nakadaka: 0, odaka: 0 }
39
+
40
+ tokens.each do |t|
41
+ info = accent_for(t[:surface])
42
+ next unless info
43
+
44
+ type = info[:type]
45
+ result[type] += 1 if result.key?(type)
46
+ end
47
+ result
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Part-of-Speech (POS) Distribution Profiler.
5
+ #
6
+ # Analyzes Japanese tokens and calculates percentage breakdown per POS category
7
+ # (nouns, verbs, adjectives, particles, adverbs, symbols).
8
+ #
9
+ module PosProfiler
10
+ POS_LABELS = {
11
+ '名詞' => :noun,
12
+ '動詞' => :verb,
13
+ '形容詞' => :adjective,
14
+ '助詞' => :particle,
15
+ '副詞' => :adverb,
16
+ '記号' => :symbol
17
+ }.freeze
18
+
19
+ class << self
20
+ # Analyze text POS distribution
21
+ #
22
+ # @param text [String] Japanese text
23
+ # @return [Hash] POS breakdown hash with counts and distribution
24
+ def profile(text)
25
+ tokens = Tokenizer.tokenize(text)
26
+ return empty_profile if tokens.empty?
27
+
28
+ counts = Hash.new(0)
29
+ tokens.each do |token|
30
+ category = POS_LABELS[token[:pos]] || :other
31
+ counts[category] += 1
32
+ end
33
+
34
+ total = tokens.length
35
+ dist = counts.transform_values { |cnt| (cnt.to_f / total * 100).round(2) }
36
+
37
+ { counts: counts, distribution: dist, total_tokens: total }
38
+ end
39
+
40
+ private
41
+
42
+ def empty_profile
43
+ { counts: {}, distribution: {}, total_tokens: 0 }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Text Simplifier and Beginner Synonym Recommender.
5
+ #
6
+ # Scans Japanese text for complex N2/N1 vocabulary and recommends simpler
7
+ # N5/N4/N3 synonym alternatives for Japanese language learners.
8
+ #
9
+ module Simplifier
10
+ SIMPLIFICATIONS = {
11
+ '著しい' => { reading: 'いちじるしい', suggestions: %w[大きい 目立つ] },
12
+ '著しく' => { reading: 'いちじるしく', suggestions: %w[大きく 目立って] },
13
+ '未曽有' => { reading: 'みぞう', suggestions: %w[今までにない 初めての] },
14
+ '浸透' => { reading: 'しんとう', suggestions: %w[広がる 伝わる] },
15
+ '阻害' => { reading: 'そがい', suggestions: %w[邪魔する 妨げる] },
16
+ '急速' => { reading: 'きゅうそく', suggestions: %w[とても速い 急に] },
17
+ '懸念' => { reading: 'けねん', suggestions: %w[心配 不安] },
18
+ '普及' => { reading: 'ふきゅう', suggestions: %w[広まる みんなが使う] }
19
+ }.freeze
20
+
21
+ class << self
22
+ # Analyze text and return simplification suggestions for high-level words
23
+ #
24
+ # @param text [String] Japanese text
25
+ # @return [Array<Hash>] array of simplification suggestions
26
+ def simplify(text)
27
+ cleaned = Preprocessor.clean(text)
28
+ return [] if cleaned.empty?
29
+
30
+ SIMPLIFICATIONS.filter_map do |word, data|
31
+ build_suggestion(word, data) if cleaned.include?(word)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def build_suggestion(word, data)
38
+ level = Dictionary.vocab_level(word) || :n1
39
+ {
40
+ word: word,
41
+ level: level,
42
+ reading: data[:reading],
43
+ suggestions: data[:suggestions]
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Style and Speech Register Profiler.
5
+ #
6
+ # Analyzes polite vs plain form usage, Keigo honorifics (Sonkeigo/Kenjougo),
7
+ # dialect markers, and internet slang in Japanese text.
8
+ #
9
+ module StyleProfiler
10
+ POLITE_ENDINGS = /(?:です|ます|でした|ました|ではありません|ではありませんでした|でしょう)\b/.freeze
11
+ KEIGO_SONKEIGO_PATTERNS = /(?:お|ご)\p{L}+(?:になる|なさる|くださる)|いらっしゃる|おっしゃる|ご覧/.freeze
12
+ KEIGO_KENJOUGO_PATTERNS = /(?:お|ご)\p{L}+(?:する|いたします)|申す|申し上げ|参る|参り|いただく|いただき|存じる|存じ/.freeze
13
+ SLANG_PATTERNS = /(?:[wW]{2,}|笑|ww|やねん|やで|ほんま)/.freeze
14
+
15
+ class << self
16
+ # Analyze text style and return detailed profile hash
17
+ #
18
+ # @param text [String] Japanese text
19
+ # @return [Hash] style profile with formality, keigo, and register metrics
20
+ def profile(text)
21
+ cleaned = Preprocessor.clean(text)
22
+ return empty_profile if cleaned.empty?
23
+
24
+ build_style_profile(cleaned)
25
+ end
26
+
27
+ private
28
+
29
+ def build_style_profile(cleaned)
30
+ polite = cleaned.scan(POLITE_ENDINGS).length
31
+ sonkeigo = cleaned.scan(KEIGO_SONKEIGO_PATTERNS).length
32
+ kenjougo = cleaned.scan(KEIGO_KENJOUGO_PATTERNS).length
33
+ slang = cleaned.scan(SLANG_PATTERNS).length
34
+
35
+ formality = calculate_formality(cleaned, polite, sonkeigo, kenjougo)
36
+ register = formality > 50.0 ? :polite : :plain
37
+
38
+ { formality_score: formality, register: register, polite_count: polite,
39
+ sonkeigo_count: sonkeigo, kenjougo_count: kenjougo, slang_count: slang }
40
+ end
41
+
42
+ def calculate_formality(text, polite_count, sonkeigo_count, kenjougo_count)
43
+ sentences = Preprocessor.sentences(text)
44
+ return 0.0 if sentences.empty?
45
+
46
+ ratio = (polite_count + (sonkeigo_count * 1.5) + (kenjougo_count * 1.5)) / sentences.length
47
+ [(ratio * 60.0).round(2), 100.0].min
48
+ end
49
+
50
+ def empty_profile
51
+ {
52
+ formality_score: 0.0,
53
+ register: :plain,
54
+ polite_count: 0,
55
+ sonkeigo_count: 0,
56
+ kenjougo_count: 0,
57
+ slang_count: 0
58
+ }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Syntax and Sentence Clause Complexity Analyzer.
5
+ #
6
+ # Identifies conjunction particles and subordinate clause connectives
7
+ # (が, けど, から, ので, とき, たら, ば, なら, のに, けれども), calculating
8
+ # clause density and syntactic complexity indices.
9
+ #
10
+ module Syntax
11
+ CONJUNCTION_PATTERNS = /(?:けれども|けれど|けど|ので|から|とき|たら|なら|のに|が|し)\b/.freeze
12
+
13
+ class << self
14
+ # Analyze syntactic clause complexity of Japanese text
15
+ #
16
+ # @param text [String] Japanese text
17
+ # @return [Hash] syntax complexity metrics
18
+ def analyze(text)
19
+ cleaned = Preprocessor.clean(text)
20
+ return empty_analysis if cleaned.empty?
21
+
22
+ compute_syntax_metrics(cleaned)
23
+ end
24
+
25
+ private
26
+
27
+ def compute_syntax_metrics(cleaned)
28
+ sents = Preprocessor.sentences(cleaned)
29
+ matches = cleaned.scan(CONJUNCTION_PATTERNS)
30
+ cnt = matches.length
31
+ avg_clauses = ((cnt + sents.length).to_f / [sents.length, 1].max).round(2)
32
+
33
+ { conjunction_count: cnt, conjunctions: matches.uniq, sentence_count: sents.length,
34
+ avg_clauses_per_sentence: avg_clauses, complexity_index: [(avg_clauses * 25.0).round(2), 100.0].min }
35
+ end
36
+
37
+ def empty_analysis
38
+ { conjunction_count: 0, conjunctions: [], sentence_count: 0,
39
+ avg_clauses_per_sentence: 1.0, complexity_index: 0.0 }
40
+ end
41
+ end
42
+ end
43
+ end