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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Kanji Details Inspector.
5
+ #
6
+ # Provides stroke count, radical, On'yomi, Kun'yomi, English meanings, and JLPT levels for Kanji.
7
+ #
8
+ module KanjiDetails
9
+ KANJI_DATA = {
10
+ '私' => { character: '私', strokes: 7, radical: '禾', onyomi: %w[シ], kunyomi: %w[わたし], level: :n5 },
11
+ '学' => { character: '学', strokes: 8, radical: '子', onyomi: %w[ガク], kunyomi: %w[まな.ぶ], level: :n5 },
12
+ '校' => { character: '校', strokes: 10, radical: '木', onyomi: %w[コウ], kunyomi: [], level: :n5 },
13
+ '日' => { character: '日', strokes: 4, radical: '日', onyomi: %w[ニチ], kunyomi: %w[ひ], level: :n5 },
14
+ '本' => { character: '本', strokes: 5, radical: '木', onyomi: %w[ホン], kunyomi: %w[もと], level: :n5 },
15
+ '語' => { character: '語', strokes: 14, radical: '言', onyomi: %w[ゴ], kunyomi: %w[かた.る], level: :n5 },
16
+ '食' => { character: '食', strokes: 9, radical: '食', onyomi: %w[ショク], kunyomi: %w[た.べる], level: :n5 },
17
+ '飲' => { character: '飲', strokes: 12, radical: '食', onyomi: %w[イン], kunyomi: %w[の.む], level: :n5 },
18
+ '行' => { character: '行', strokes: 6, radical: '行', onyomi: %w[コウ], kunyomi: %w[い.く], level: :n5 },
19
+ '来' => { character: '来', strokes: 7, radical: '木', onyomi: %w[ライ], kunyomi: %w[く.る], level: :n5 }
20
+ }.freeze
21
+
22
+ class << self
23
+ # Lookup Kanji details for a single character
24
+ #
25
+ # @param char [String] Kanji character
26
+ # @return [Hash, nil] kanji details hash or default details
27
+ def lookup(char)
28
+ return nil if char.nil? || char.to_s.strip.empty?
29
+
30
+ kanji_char = char.to_s.strip[0]
31
+ return nil unless kanji_char.match?(/[\u4E00-\u9FFF]/)
32
+
33
+ level = Dictionary.kanji_level(kanji_char) || :out_of_jlpt
34
+ KANJI_DATA[kanji_char] || default_kanji_details(kanji_char, level)
35
+ end
36
+
37
+ # Get details for all unique kanji in text
38
+ #
39
+ # @param text [String] Japanese text
40
+ # @return [Array<Hash>] array of kanji detail hashes
41
+ def details_for_text(text)
42
+ kanji_list = Kanji.extract(text)
43
+ kanji_list.filter_map { |k| lookup(k) }
44
+ end
45
+
46
+ private
47
+
48
+ def default_kanji_details(char, level)
49
+ {
50
+ character: char,
51
+ strokes: 8,
52
+ radical: 'undefined',
53
+ onyomi: [],
54
+ kunyomi: [],
55
+ level: level
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Text Preprocessor and Unicode Normalizer.
5
+ #
6
+ # Cleans input text by stripping HTML, Markdown, URLs, and code blocks,
7
+ # normalizes Unicode NFKC characters (Half-width/Full-width Kana),
8
+ # and segments Japanese text into sentences.
9
+ #
10
+ module Preprocessor
11
+ HTML_TAG_REGEX = /<[^>]*>/.freeze
12
+ MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\([^)]+\)/.freeze
13
+ MARKDOWN_HEADER_CODE_REGEX = /^#+\s*|`[^`]+`|```[\s\S]*?```/.freeze
14
+ URL_REGEX = %r{https?://\S+}.freeze
15
+ SENTENCE_SCAN_REGEX = /[^。!?!?\n]+[。!?!?\n]?/.freeze
16
+
17
+ class << self
18
+ # Clean text by removing HTML, Markdown, and URLs
19
+ #
20
+ # @param text [String] Japanese raw text
21
+ # @return [String] cleaned and sanitized text
22
+ def clean(text)
23
+ return '' if text.nil? || text.to_s.strip.empty?
24
+
25
+ str = text.to_s.dup
26
+ str.gsub!(HTML_TAG_REGEX, '')
27
+ str.gsub!(URL_REGEX, '')
28
+ str.gsub!(MARKDOWN_LINK_REGEX, '\1')
29
+ str.gsub!(MARKDOWN_HEADER_CODE_REGEX, '')
30
+ str.unicode_normalize(:nfkc).strip
31
+ end
32
+
33
+ # Segment Japanese text into clean sentence arrays
34
+ #
35
+ # @param text [String] Japanese text
36
+ # @return [Array<String>] array of non-empty sentences
37
+ def sentences(text)
38
+ cleaned = clean(text)
39
+ return [] if cleaned.empty?
40
+
41
+ matches = cleaned.scan(SENTENCE_SCAN_REGEX)
42
+ result = matches.map(&:strip).reject(&:empty?)
43
+
44
+ result.empty? ? [cleaned] : result
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Tokenizer and Morphological Analyzer.
5
+ #
6
+ # Processes raw text through Preprocessor and Engine to extract structured
7
+ # tokens with lemmata, Part-Of-Speech (POS) categories, and dictionary forms.
8
+ #
9
+ module Tokenizer
10
+ POS_MAP = {
11
+ '名詞' => :noun,
12
+ '動詞' => :verb,
13
+ '形容詞' => :i_adjective,
14
+ '形状詞' => :na_adjective,
15
+ '副詞' => :adverb,
16
+ '助詞' => :particle,
17
+ '助動詞' => :auxiliary_verb,
18
+ '連体詞' => :pre_noun_adjective,
19
+ '感動詞' => :interjection,
20
+ '接続詞' => :conjunction,
21
+ '記号' => :symbol
22
+ }.freeze
23
+
24
+ class << self
25
+ # Tokenize text into structured token objects
26
+ #
27
+ # @param text [String] Japanese input text
28
+ # @return [Array<Hash>] array of token hashes
29
+ def tokenize(text)
30
+ cleaned = Preprocessor.clean(text)
31
+ return [] if cleaned.empty?
32
+
33
+ raw_nodes = Engine.parse(cleaned)
34
+ raw_nodes.map { |node| format_token(node) }
35
+ end
36
+
37
+ # Extract dictionary forms (lemmata) of words
38
+ #
39
+ # @param text [String] Japanese text
40
+ # @param filter_pos [Array<Symbol>] optional POS filter (e.g. [:noun, :verb])
41
+ # @return [Array<String>] array of dictionary forms
42
+ def lemmata(text, filter_pos: nil)
43
+ tokens = tokenize(text)
44
+ tokens = tokens.select { |t| filter_pos.include?(t[:pos_category]) } if filter_pos && !filter_pos.empty?
45
+ tokens.map { |t| t[:dictionary_form] }.compact.reject(&:empty?)
46
+ end
47
+
48
+ private
49
+
50
+ def format_token(node)
51
+ pos_cat = POS_MAP[node[:pos]] || :other
52
+ {
53
+ surface: node[:surface],
54
+ dictionary_form: node[:dictionary_form] || node[:surface],
55
+ reading: node[:reading] || node[:surface],
56
+ pos_raw: node[:pos],
57
+ pos_category: pos_cat,
58
+ pos_sub1: node[:pos_sub1],
59
+ inflection_form: node[:inflection_form]
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JLPT
4
+ # Vocabulary Classifier for JLPT Levels.
5
+ #
6
+ # Analyzes tokens in Japanese text and categorizes vocabulary into JLPT levels
7
+ # (N5 to N1) or Non-JLPT, computing distribution statistics and percentages.
8
+ #
9
+ module VocabClassifier
10
+ class << self
11
+ # Classify all vocabulary tokens in text by JLPT level
12
+ #
13
+ # @param text [String] Japanese text
14
+ # @return [Hash{Symbol => Array<String>}] level to vocabulary lemmata mapping
15
+ def classify(text)
16
+ result = { n5: [], n4: [], n3: [], n2: [], n1: [], non_jlpt: [] }
17
+ Tokenizer.lemmata(text).each do |lemma|
18
+ lvl = Dictionary.vocab_level(lemma)
19
+ result.key?(lvl) ? result[lvl] << lemma : result[:non_jlpt] << lemma
20
+ end
21
+ result
22
+ end
23
+
24
+ # Calculate percentage distribution of vocabulary across JLPT levels
25
+ #
26
+ # @param text [String] Japanese text
27
+ # @return [Hash{Symbol => Float}] level to percentage mapping
28
+ def distribution(text)
29
+ classified = classify(text)
30
+ total_words = classified.values.sum(&:length)
31
+ return empty_distribution if total_words.zero?
32
+
33
+ dist = {}
34
+ classified.each do |lvl, words|
35
+ pct = (words.length.to_f / total_words * 100).round(2)
36
+ dist[lvl] = pct
37
+ end
38
+ dist
39
+ end
40
+
41
+ private
42
+
43
+ def empty_distribution
44
+ { n5: 0.0, n4: 0.0, n3: 0.0, n2: 0.0, n1: 0.0, non_jlpt: 0.0 }
45
+ end
46
+ end
47
+ end
48
+ end