Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@ permissions:
contents: read

jobs:
test:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop

test:
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
96 changes: 96 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
plugins:
- rubocop-minitest

AllCops:
TargetRubyVersion: 3.1
NewCops: enable
Exclude:
- 'vendor/**/*'
- 'coverage/**/*'
- '*.gemspec'
- 'bin/**/*'
- 'install.rb'

# Existing code lacks documentation, can add incrementally
Style/Documentation:
Enabled: false

# Test files often have long blocks
Metrics/BlockLength:
Exclude:
- 'test/**/*'
- 'lib/classifier/extensions/vector.rb'

# Allow longer methods in complex algorithms (SVD, etc.)
Metrics/MethodLength:
Max: 25
Exclude:
- 'test/**/*'
- 'lib/classifier/extensions/vector.rb'
- 'lib/classifier/lsi/content_node.rb'

# Allow higher complexity for mathematical algorithms
Metrics/AbcSize:
Max: 30
Exclude:
- 'test/**/*'
- 'lib/classifier/extensions/vector.rb'
- 'lib/classifier/lsi.rb'
- 'lib/classifier/lsi/content_node.rb'

Metrics/CyclomaticComplexity:
Max: 10
Exclude:
- 'lib/classifier/extensions/vector.rb'
- 'lib/classifier/lsi/content_node.rb'

Metrics/PerceivedComplexity:
Max: 10
Exclude:
- 'lib/classifier/extensions/vector.rb'
- 'lib/classifier/lsi/content_node.rb'

# Class length limits - algorithms and tests can be longer
Metrics/ClassLength:
Max: 250
Exclude:
- 'test/**/*'

# SV_decomp is a standard algorithm name
Naming/MethodName:
Exclude:
- 'lib/classifier/extensions/vector.rb'

# Short parameter names are acceptable for serialization
Naming/MethodParameterName:
Exclude:
- 'lib/classifier/extensions/vector_serialize.rb'

# Marshal.load is intentional for deserialization
Security/MarshalLoad:
Exclude:
- 'lib/classifier/extensions/vector_serialize.rb'
- 'test/**/*'

# CORPUS_SKIP_WORDS is a public constant used externally
Lint/UselessConstantScoping:
Enabled: false

# Frozen string literal is optional for this gem
Style/FrozenStringLiteralComment:
Enabled: false

# Allow compact class/module child definitions
Style/ClassAndModuleChildren:
Enabled: false

# Allow both styles of string literals
Style/StringLiterals:
EnforcedStyle: single_quotes

Style/StringLiteralsInInterpolation:
EnforcedStyle: single_quotes

# Minitest assertions
Minitest/MultipleAssertions:
Max: 10
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ gem 'mutex_m'
group :test do
gem 'simplecov', require: false
end

group :development do
gem 'rubocop', require: false
gem 'rubocop-minitest', require: false
end
36 changes: 36 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,57 @@ PATH
GEM
remote: https://rubygems.org/
specs:
ast (2.4.3)
docile (1.4.1)
fast-stemmer (1.0.2)
json (2.18.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
matrix (0.4.2)
minitest (5.18.1)
mutex_m (0.2.0)
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1)
racc
prism (1.7.0)
psych (5.1.2)
stringio
racc (1.8.1)
rainbow (3.1.1)
rake (13.0.6)
rdoc (6.5.1.1)
psych (>= 4.0.0)
regexp_parser (2.11.3)
rubocop (1.82.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.48.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-minitest (0.38.2)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (1.13.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
stringio (3.1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)

PLATFORMS
arm64-darwin-22
Expand All @@ -41,6 +75,8 @@ DEPENDENCIES
minitest
mutex_m
rdoc
rubocop
rubocop-minitest
simplecov

BUNDLED WITH
Expand Down
6 changes: 5 additions & 1 deletion lib/classifier/bayes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def classifications(text)
# b.classify "I hate bad words and you"
# => 'Uninteresting'
def classify(text)
(classifications(text).sort_by { |a| -a[1] })[0][0]
classifications(text).min_by { |a| -a[1] }[0]
end

#
Expand All @@ -109,6 +109,10 @@ def method_missing(name, *args)
args.each { |text| send(method, category, text) }
end

def respond_to_missing?(name, include_private = false)
name.to_s =~ /(un)?train_(\w+)/ || super
end

#
# Provides a list of category names
# For example:
Expand Down
10 changes: 5 additions & 5 deletions lib/classifier/extensions/vector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
require 'matrix'

class Array
def sum_with_identity(identity = 0.0, &block)
def sum_with_identity(identity = 0.0, &)
return identity unless size.to_i.positive?
return map(&block).sum_with_identity(identity) if block_given?
return map(&).sum_with_identity(identity) if block_given?

compact.reduce(:+).to_f || identity.to_f
compact.reduce(identity, :+).to_f
end
end

Expand Down Expand Up @@ -58,8 +58,8 @@ def SV_decomp(max_sweeps = 20)

loop do
iteration_count += 1
(0...q_rotation_matrix.row_size - 1).each do |row|
(1..q_rotation_matrix.row_size - 1).each do |col|
(0...(q_rotation_matrix.row_size - 1)).each do |row|
(1..(q_rotation_matrix.row_size - 1)).each do |col|
next if row == col

angle = Math.atan((2.to_r * q_rotation_matrix[row,
Expand Down
2 changes: 1 addition & 1 deletion lib/classifier/extensions/word_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class String
# "Hello (greeting's), with {braces} < >...?".without_punctuation
# => "Hello greetings with braces "
def without_punctuation
tr(',?.!;:"@#$%^&*()_=+[]{}\|<>/`~', ' ').tr("'\-", '')
tr(',?.!;:"@#$%^&*()_=+[]{}|<>/`~', ' ').tr("'-", '')
end

# Return a Hash of strings => ints. Each word in the string is stemmed,
Expand Down
30 changes: 15 additions & 15 deletions lib/classifier/lsi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def highest_relative_content(max_chunks = 10)
avg_density = {}
@items.each_key { |x| avg_density[x] = proximity_array_for_content(x).inject(0.0) { |i, j| i + j[1] } }

avg_density.keys.sort_by { |x| avg_density[x] }.reverse[0..max_chunks - 1].map
avg_density.keys.sort_by { |x| avg_density[x] }.reverse[0..(max_chunks - 1)].map
end

# This function is the primitive that find_related and classify
Expand All @@ -180,10 +180,10 @@ def highest_relative_content(max_chunks = 10)
# The parameter doc is the content to compare. If that content is not
# indexed, you can pass an optional block to define how to create the
# text data. See add_item for examples of how this works.
def proximity_array_for_content(doc, &block)
def proximity_array_for_content(doc, &)
return [] if needs_rebuild?

content_node = node_for_content(doc, &block)
content_node = node_for_content(doc, &)
result =
@items.keys.collect do |item|
val = if self.class.gsl_available
Expand All @@ -201,10 +201,10 @@ def proximity_array_for_content(doc, &block)
# calculated vectors instead of their full versions. This is useful when
# you're trying to perform operations on content that is much smaller than
# the text you're working with. search uses this primitive.
def proximity_norms_for_content(doc, &block)
def proximity_norms_for_content(doc, &)
return [] if needs_rebuild?

content_node = node_for_content(doc, &block)
content_node = node_for_content(doc, &)
result =
@items.keys.collect do |item|
val = if self.class.gsl_available
Expand All @@ -229,7 +229,7 @@ def search(string, max_nearest = 3)

carry = proximity_norms_for_content(string)
result = carry.collect { |x| x[0] }
result[0..max_nearest - 1]
result[0..(max_nearest - 1)]
end

# This function takes content and finds other documents
Expand All @@ -245,7 +245,7 @@ def find_related(doc, max_nearest = 3, &block)
carry =
proximity_array_for_content(doc, &block).reject { |pair| pair[0] == doc }
result = carry.collect { |x| x[0] }
result[0..max_nearest - 1]
result[0..(max_nearest - 1)]
end

# This function uses a voting system to categorize documents, based on
Expand All @@ -257,17 +257,17 @@ def find_related(doc, max_nearest = 3, &block)
# text. A cutoff of 1 means that every document in the index votes on
# what category the document is in. This may not always make sense.
#
def classify(doc, cutoff = 0.30, &block)
votes = vote(doc, cutoff, &block)
def classify(doc, cutoff = 0.30, &)
votes = vote(doc, cutoff, &)

ranking = votes.keys.sort_by { |x| votes[x] }
ranking[-1]
end

def vote(doc, cutoff = 0.30, &block)
def vote(doc, cutoff = 0.30, &)
icutoff = (@items.size * cutoff).round
carry = proximity_array_for_content(doc, &block)
carry = carry[0..icutoff - 1]
carry = proximity_array_for_content(doc, &)
carry = carry[0..(icutoff - 1)]
votes = {}
carry.each do |pair|
categories = @items[pair[0]].categories
Expand All @@ -291,8 +291,8 @@ def vote(doc, cutoff = 0.30, &block)
#
#
# See classify() for argument docs
def classify_with_confidence(doc, cutoff = 0.30, &block)
votes = vote(doc, cutoff, &block)
def classify_with_confidence(doc, cutoff = 0.30, &)
votes = vote(doc, cutoff, &)
votes_sum = votes.values.inject(0.0) { |sum, v| sum + v }
return [nil, nil] if votes_sum.zero?

Expand All @@ -309,7 +309,7 @@ def highest_ranked_stems(doc, count = 3)
raise 'Requested stem ranking on non-indexed content!' unless @items[doc]

arr = node_for_content(doc).lsi_vector.to_a
top_n = arr.sort.reverse[0..count - 1]
top_n = arr.sort.reverse[0..(count - 1)]
top_n.collect { |x| @word_list.word_for_index(arr.index(x)) }
end

Expand Down
Loading