text-metrics 1.0.0.beta2 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e71911a9dd9a27cc6dcaa9eb9525cf48d73d48c5fc49526d475cb2a1f6de98c6
4
- data.tar.gz: c686dfd9cf3cdb730b27c0329b1a0ba13748dea1de1f5730cd4283cea9885de2
3
+ metadata.gz: a7420551fa74c4621b2ce5d2d8d1e83a5bb46a1790c4bb340ed2556cf08b7bc1
4
+ data.tar.gz: 599106caafbdb730d6ea72d532afdc5f2e7b5bf69402d73f8c782523ed49c6c4
5
5
  SHA512:
6
- metadata.gz: 57de157f47ac0ab6cf79956509087371d9c9e12fbdbe6d80b7314110c1d8211e831561adb1c9476199e086bb3f6d33c55c1f3e7c39dcf144e04f0766c7e1773d
7
- data.tar.gz: f7b5f77a6fba329093f3f55c38844ac8696f28c9d5bbfaf08750d464732bdf5a5484ebea57444a928ebe8a65e6fdec76a8ade187987df17cd419edc3c4f8555b
6
+ metadata.gz: e35abe082c3a30179a7a1c171c80ae997690b609bd81ec21ea5f12d4b506e5cdad87802bd89d07867d193bd499f7a91e6f3fa1f05d9e11d9c8ab59c763413f1b
7
+ data.tar.gz: b9b9a018a2dc761540358386586923826ee5defd1fc5426a39d01acc2c5757de53c69629ec88daee674c0d6f14e9e2ba0c8b112e5a40348d52aea013f9815298
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Change log
2
2
 
3
3
  ## main
4
+ - ... TBD ...
5
+
6
+ ## 1.1.0
7
+ - Accept `:en` as an alias for `:en_us` in `TextMetrics.new`.
8
+ - Add five metrics to the analyzer and `#to_h`: `automated_readability_index`,
9
+ `type_token_ratio` (lexical diversity), `reading_time` (minutes, with a configurable
10
+ `wpm:`), and the previously-internal `polysyllabic_words_count` and `long_words_count`.
11
+
12
+ ## 1.0.0
13
+ - First public release.
14
+ - see `UPGRADING.md` for migration details from the pre-release `0.x` API.
4
15
 
5
16
  ## ## 1.0.0.beta2
6
17
  - Add `gunning_fog_index` to the analyzer metrics and `#to_h` output.
data/README.md CHANGED
@@ -8,6 +8,8 @@ Text Metrics is a Ruby library for analysing text. It was inspired by the Python
8
8
 
9
9
  It gives you the everyday counts you need — words, characters, sentences and syllables — plus readability scores such as Flesch Reading Ease, Flesch-Kincaid Grade Level, SMOG, Gunning Fog, Coleman-Liau Index and LIX. English and French are supported.
10
10
 
11
+ It is battle-tested in production on millions of student writings at [Plume](https://plume-app.co).
12
+
11
13
  ## Accurate, dictionary-based syllable counting
12
14
 
13
15
  Readability scores such as Flesch, Flesch-Kincaid and SMOG depend heavily on syllable counts. Many libraries estimate those counts with hyphenation rules, which are fast but often wrong. Text Metrics starts from real pronunciation data instead:
@@ -24,6 +26,8 @@ That makes syllable-sensitive scores more reliable, especially for longer or les
24
26
  - **Flesch-Kincaid Grade** maps to a US school grade. It uses the same formula for every language because there is no validated French adaptation.
25
27
  - **Gunning Fog** uses words with three or more syllables as complex words, matching the same syllable counts used by SMOG.
26
28
  - **Coleman-Liau** counts alphabetic letters only (not digits or punctuation), per its definition.
29
+ - **Automated Readability Index** counts characters (letters, digits and punctuation, spaces excluded), per its original "strokes" definition — so it uses `characters_count`, unlike Coleman-Liau which uses letters only.
30
+ - **Type-token ratio** is distinct-words over total-words and is sensitive to text length: longer texts trend lower because words repeat. Compare it only across texts of similar length.
27
31
 
28
32
  ## Features
29
33
 
@@ -32,10 +36,14 @@ _Basic metrics:_
32
36
  - [x] word count
33
37
  - [x] character count
34
38
  - [x] sentence count
39
+ - [x] polysyllabic word count (three or more syllables)
40
+ - [x] long word count (more than six characters)
35
41
  - [x] average syllables per word
36
42
  - [x] average letters per word
37
43
  - [x] average words per sentence
38
44
  - [x] average characters per sentence
45
+ - [x] type-token ratio (lexical diversity)
46
+ - [x] reading time (minutes, configurable words-per-minute)
39
47
 
40
48
  _Readability tests:_
41
49
 
@@ -45,6 +53,7 @@ _Readability tests:_
45
53
  - [x] Gunning Fog Index
46
54
  - [x] Coleman-Liau Index
47
55
  - [x] Lix Index
56
+ - [x] Automated Readability Index
48
57
 
49
58
  ## Installation
50
59
 
@@ -52,7 +61,7 @@ Text Metrics is not published to RubyGems yet. For now, install it from GitHub:
52
61
 
53
62
  ```ruby
54
63
  # Gemfile
55
- gem "text-metrics", github: "plume-app/text-metrics", branch: "main"
64
+ gem "text-metrics", "~> 1.1.0"
56
65
  ```
57
66
 
58
67
  ### Supported Ruby versions
@@ -68,11 +77,13 @@ metrics = TextMetrics.new("This gem analyses all kinds of text.")
68
77
  metrics.to_h
69
78
  # {
70
79
  # words_count: 7, characters_count: 30, sentences_count: 1, syllables_count: 10,
71
- # punctuation_count: 1, syllables_per_word_average: 1.4, letters_per_word_average: 4.14,
80
+ # punctuation_count: 1, polysyllabic_words_count: 1, long_words_count: 1,
81
+ # syllables_per_word_average: 1.4, letters_per_word_average: 4.14,
72
82
  # words_per_sentence_average: 7.0, characters_per_sentence_average: 30.0,
73
83
  # words_per_punctuation_average: 7.0, punctuation_per_sentence_average: 1.0,
74
- # flesch_reading_ease: 78.87, flesch_kincaid_grade: 4.0, lix: 21.29,
75
- # smog_index: 0.0, gunning_fog_index: 8.5, coleman_liau_index: 4.33
84
+ # type_token_ratio: 1.0, flesch_reading_ease: 78.87, flesch_kincaid_grade: 4.0,
85
+ # lix: 21.29, smog_index: 0.0, gunning_fog_index: 8.5, coleman_liau_index: 4.33,
86
+ # automated_readability_index: 2.3, reading_time: 0.04
76
87
  # }
77
88
 
78
89
  # Or ask for a single metric:
@@ -81,18 +92,48 @@ metrics.characters_count # => 30
81
92
  metrics.flesch_reading_ease # => 78.87
82
93
  metrics.flesch_kincaid_grade # => 4.0
83
94
  metrics.gunning_fog_index # => 8.5
95
+
96
+ # Reading time is in minutes; pass a custom reading pace if you like:
97
+ metrics.reading_time # => 0.04 (at the default 200 wpm)
98
+ metrics.reading_time(wpm: 130) # => 0.05
84
99
  ```
85
100
 
86
101
  ### Languages
87
102
 
88
- American English (`:en_us`) is the default. To analyse French text, pass `language: :fr`:
103
+ American English (`:en_us`) is the default. `:en` is accepted as an alias for `:en_us`. To analyse French text, pass `language: :fr`:
89
104
 
90
105
  ```ruby
91
106
  TextMetrics.new("Bonjour le monde.", language: :fr)
107
+ TextMetrics.new("Hello.", language: :en) # same as :en_us
92
108
  ```
93
109
 
94
110
  Unsupported languages raise `TextMetrics::Error`.
95
111
 
112
+ ### Configuration
113
+
114
+ Global defaults can be set via `TextMetrics.configure`. Call this once at boot time — in a Rails initializer, for example:
115
+
116
+ ```ruby
117
+ TextMetrics.configure do |config|
118
+ # Words per minute used by #reading_time (default: 200).
119
+ config.wpm = 250
120
+ end
121
+ ```
122
+
123
+ In a **Rails app**, generate the initializer with:
124
+
125
+ ```sh
126
+ rails generate text_metrics:install
127
+ ```
128
+
129
+ This creates `config/initializers/text_metrics.rb` with the options commented out at their defaults.
130
+
131
+ Per-call overrides are still supported and always take precedence over the global config:
132
+
133
+ ```ruby
134
+ metrics.reading_time(wpm: 130)
135
+ ```
136
+
96
137
  ### Comparing two texts
97
138
 
98
139
  Levenshtein distance compares two strings, so it is exposed on the `TextMetrics` module:
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module TextMetrics
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc "Creates a TextMetrics initializer in config/initializers"
9
+
10
+ def create_initializer
11
+ create_file "config/initializers/text_metrics.rb", <<~RUBY
12
+ # frozen_string_literal: true
13
+
14
+ TextMetrics.configure do |config|
15
+ # Words per minute used to compute reading_time (default: 200).
16
+ # config.wpm = 200
17
+ end
18
+ RUBY
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TextMetrics
4
+ class Configuration
5
+ attr_accessor :wpm
6
+
7
+ def initialize
8
+ @wpm = 200
9
+ end
10
+ end
11
+ end
@@ -7,26 +7,30 @@ module TextMetrics
7
7
  class Base
8
8
  GEM_PATH = File.dirname(__FILE__, 2).freeze
9
9
 
10
- # The public metric surface. #to_h and the individual readers are both derived
11
- # from this list, so they can never drift apart.
10
+ # Single source of truth: #to_h and reader methods are both derived from this list.
12
11
  METRICS = %i[
13
12
  words_count
14
13
  characters_count
15
14
  sentences_count
16
15
  syllables_count
17
16
  punctuation_count
17
+ polysyllabic_words_count
18
+ long_words_count
18
19
  syllables_per_word_average
19
20
  letters_per_word_average
20
21
  words_per_sentence_average
21
22
  characters_per_sentence_average
22
23
  words_per_punctuation_average
23
24
  punctuation_per_sentence_average
25
+ type_token_ratio
24
26
  flesch_reading_ease
25
27
  flesch_kincaid_grade
26
28
  lix
27
29
  smog_index
28
30
  gunning_fog_index
29
31
  coleman_liau_index
32
+ automated_readability_index
33
+ reading_time
30
34
  ].freeze
31
35
 
32
36
  attr_reader :text, :language
@@ -36,8 +40,6 @@ module TextMetrics
36
40
  @language = language
37
41
  end
38
42
 
39
- # Every metric in one hash. Single source of truth for the public surface.
40
- # Memoized — the analyzer is immutable once built.
41
43
  def to_h
42
44
  @to_h ||= METRICS.to_h { |metric| [metric, public_send(metric)] }
43
45
  end
@@ -65,8 +67,14 @@ module TextMetrics
65
67
  punctuation_marks.size
66
68
  end
67
69
 
68
- # averages — rounded for display only. The readability scores below are computed
69
- # from the full-precision ratios (#average_*), not from these rounded values.
70
+ def polysyllabic_words_count
71
+ words.count { |word| count_syllables_in_word(word) >= 3 }
72
+ end
73
+
74
+ def long_words_count
75
+ words.count { |word| word.length > 6 }
76
+ end
77
+
70
78
  def syllables_per_word_average
71
79
  average_syllables_per_word.round(1)
72
80
  end
@@ -97,16 +105,16 @@ module TextMetrics
97
105
  (punctuation_count.to_f / sentences_count).round(2)
98
106
  end
99
107
 
100
- # readability scores — computed from full-precision ratios, rounded only at the end,
101
- # and returned unclamped (a Flesch score can legitimately exceed 100 or go negative).
108
+ def type_token_ratio
109
+ return 0.0 if words_count.zero?
110
+
111
+ (words.uniq.size.to_f / words_count).round(2)
112
+ end
102
113
 
103
- # Language-specific; subclasses supply the constants.
104
114
  def flesch_reading_ease
105
115
  raise NotImplementedError
106
116
  end
107
117
 
108
- # Flesch-Kincaid Grade Level (US school grade). The same formula is used for every
109
- # language — there is no validated non-English adaptation.
110
118
  def flesch_kincaid_grade
111
119
  return 0.0 if words_count.zero?
112
120
 
@@ -116,7 +124,7 @@ module TextMetrics
116
124
  def smog_index
117
125
  return 0.0 if sentences_count < 3
118
126
 
119
- (1.043 * Math.sqrt(30.0 * count_polysyllabic_words / sentences_count) + 3.1291).round(1)
127
+ (1.043 * Math.sqrt(30.0 * polysyllabic_words_count / sentences_count) + 3.1291).round(1)
120
128
  rescue ZeroDivisionError
121
129
  0.0
122
130
  end
@@ -124,7 +132,7 @@ module TextMetrics
124
132
  def gunning_fog_index
125
133
  return 0.0 if words_count.zero?
126
134
 
127
- (0.4 * (average_words_per_sentence + 100.0 * count_polysyllabic_words / words_count)).round(1)
135
+ (0.4 * (average_words_per_sentence + 100.0 * polysyllabic_words_count / words_count)).round(1)
128
136
  end
129
137
 
130
138
  def coleman_liau_index
@@ -139,14 +147,24 @@ module TextMetrics
139
147
  def lix
140
148
  return 0.0 if words_count.zero?
141
149
 
142
- long_words = words.count { |word| word.length > 6 }
150
+ (average_words_per_sentence + 100.0 * long_words_count / words_count).round(2)
151
+ end
152
+
153
+ def automated_readability_index
154
+ return 0.0 if words_count.zero?
155
+
156
+ (4.71 * characters_count / words_count.to_f + 0.5 * average_words_per_sentence - 21.43).round(1)
157
+ end
158
+
159
+ def reading_time(wpm: nil)
160
+ wpm ||= TextMetrics.configuration.wpm
161
+ return 0.0 if words_count.zero? || wpm.zero?
143
162
 
144
- (average_words_per_sentence + 100.0 * long_words / words_count).round(2)
163
+ (words_count / wpm.to_f).round(2)
145
164
  end
146
165
 
147
166
  private
148
167
 
149
- # full-precision ratios feeding the readability formulas
150
168
  def average_syllables_per_word
151
169
  return 0.0 if words_count.zero?
152
170
 
@@ -165,34 +183,20 @@ module TextMetrics
165
183
  words_count.to_f / sentences_count
166
184
  end
167
185
 
168
- # Count of alphabetic characters only (letters), as required by Coleman-Liau and the
169
- # letters-per-word metric — distinct from #characters_count, which includes digits
170
- # and punctuation.
171
186
  def letters_count
172
187
  @letters_count ||= text.scan(/[[:alpha:]]/).size
173
188
  end
174
189
 
175
- # Subclasses provide the language-specific syllable counting.
176
190
  def count_syllables_in_word(word)
177
191
  raise NotImplementedError
178
192
  end
179
193
 
180
- def count_polysyllabic_words
181
- words.count { |word| count_syllables_in_word(word) >= 3 }
182
- end
183
-
184
- # tokenizers
185
194
  def punctuation_marks
186
195
  @punctuation_marks ||= text.scan(/[.,!?;:]/)
187
196
  end
188
197
 
189
198
  def words
190
- @words ||= begin
191
- normalized_text = text.downcase.strip
192
-
193
- # Split into words, including hyphenated words, and excluding numbers
194
- normalized_text.scan(/\b[A-Za-zÀ-ÖØ-öø-ÿ'-]+\b/)
195
- end
199
+ @words ||= text.downcase.strip.scan(/\b[A-Za-zÀ-ÖØ-öø-ÿ'-]+\b/)
196
200
  end
197
201
 
198
202
  def sentences
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TextMetrics
4
+ class Railtie < Rails::Railtie
5
+ generators do
6
+ require "generators/text_metrics/install_generator"
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TextMetrics # :nodoc:
4
- VERSION = "1.0.0.beta2"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/text_metrics.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "text_metrics/version"
4
+ require "text_metrics/configuration"
5
+ require "text_metrics/railtie" if defined?(Rails::Railtie)
4
6
  require "text_metrics/levenshtein"
5
7
  require "text_metrics/processors/american_english"
6
8
  require "text_metrics/processors/french"
@@ -10,6 +12,22 @@ module TextMetrics
10
12
 
11
13
  DEFAULT_LANGUAGE = :en_us
12
14
 
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield configuration
22
+ end
23
+
24
+ private
25
+
26
+ def reset_configuration!
27
+ @configuration = Configuration.new
28
+ end
29
+ end
30
+
13
31
  PROCESSORS = {
14
32
  en_us: Processors::AmericanEnglish,
15
33
  fr: Processors::French
@@ -34,8 +52,11 @@ module TextMetrics
34
52
 
35
53
  # Coerce to a known language symbol, or raise a helpful error.
36
54
  # Handles nil, strings and symbols without leaking a NoMethodError.
55
+ LANGUAGE_ALIASES = {en: :en_us}.freeze
56
+
37
57
  def self.resolve_language(language)
38
58
  resolved = language.to_s.to_sym
59
+ resolved = LANGUAGE_ALIASES.fetch(resolved, resolved)
39
60
  return resolved if PROCESSORS.key?(resolved)
40
61
 
41
62
  raise Error, "Unknown language #{language.inspect}. Available languages: #{PROCESSORS.keys.join(", ")}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: text-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrien POLY
@@ -120,14 +120,17 @@ files:
120
120
  - LICENSE.txt
121
121
  - README.md
122
122
  - UPGRADING.md
123
+ - lib/generators/text_metrics/install_generator.rb
123
124
  - lib/text-metrics.rb
124
125
  - lib/text_metrics.rb
126
+ - lib/text_metrics/configuration.rb
125
127
  - lib/text_metrics/dictionaries/english_word_syllable_database.txt
126
128
  - lib/text_metrics/dictionaries/french_word_syllable_exceptions.yml
127
129
  - lib/text_metrics/levenshtein.rb
128
130
  - lib/text_metrics/processors/american_english.rb
129
131
  - lib/text_metrics/processors/base.rb
130
132
  - lib/text_metrics/processors/french.rb
133
+ - lib/text_metrics/railtie.rb
131
134
  - lib/text_metrics/version.rb
132
135
  homepage: https://github.com/plume-app/text-metrics
133
136
  licenses: