From 6ddd6d2f1be64b51e8caaa4d971eeb60546b5e21 Mon Sep 17 00:00:00 2001 From: moozzi Date: Sat, 20 Dec 2025 20:53:37 +0100 Subject: [PATCH] Added a `Ronin::Support::Text::Distance::Levenshtein` module (closes #568). --- .../support/text/distance/levenshtein.rb | 68 ++++++++++++++++ spec/text/distance/levenshtein_spec.rb | 77 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 lib/ronin/support/text/distance/levenshtein.rb create mode 100644 spec/text/distance/levenshtein_spec.rb diff --git a/lib/ronin/support/text/distance/levenshtein.rb b/lib/ronin/support/text/distance/levenshtein.rb new file mode 100644 index 00000000..c92ca0b8 --- /dev/null +++ b/lib/ronin/support/text/distance/levenshtein.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +# +# Copyright (c) 2006-2025 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-support is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-support is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-support. If not, see . +# + +module Ronin + module Support + module Text + module Distance + # + # Methods for calculating Levenshtein Distance + # + module Levenshtein + # + # Calculates Levenshtein Distance between two strings. + # + # @param [String] string1 + # First string to compare. + # + # @param [String] string2 + # Second string to compare. + # + # @return [Integer] + # The calculated distance. + # + def levenshtein_distance(string1,string2) + m = string1.size + n = string2.size + + previous_row = (0..n).to_a + + 1.upto(m) do |i| + current_value = i + + 1.upto(n) do |j| + diagonal = previous_row[j - 1] + previous_row[j - 1] = current_value + + current_value = if string1[i - 1] == string2[j - 1] + diagonal + else + 1 + [current_value, previous_row[j], diagonal].min + end + end + + previous_row[n] = current_value + end + + previous_row[n] + end + end + end + end + end +end diff --git a/spec/text/distance/levenshtein_spec.rb b/spec/text/distance/levenshtein_spec.rb new file mode 100644 index 00000000..480df366 --- /dev/null +++ b/spec/text/distance/levenshtein_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'ronin/support/text/distance/levenshtein' + +describe Ronin::Support::Text::Distance::Levenshtein do + class TestTextLevenshtein + include Ronin::Support::Text::Distance::Levenshtein + end + + let(:test_class) { TestTextLevenshtein } + subject { test_class.new } + + it "must provide a #levenshtein_distance method" do + expect(subject).to respond_to(:levenshtein_distance) + end + + describe "#levenshtein_distance" do + context "when given two somewhat similar strings" do + let(:string1) { "kitten" } + let(:string2) { "sitting" } + let(:expected_distance) { 3 } + + it "must calculate levenshtein distance between them" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + + context "when given two empty strings" do + let(:string1) { "" } + let(:string2) { "" } + let(:expected_distance) { 0 } + + it "must returns 0" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + + context "when given one empty string" do + let(:string1) { "test" } + let(:string2) { "" } + let(:expected_distance) { string1.size } + + it "must return length of given string" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + + context "when given two identical strings" do + let(:string1) { "test" } + let(:string2) { "test" } + let(:expected_distance) { 0 } + + it "must return 0" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + + context "when given strings differing by one character" do + let(:string1) { "cat" } + let(:string2) { "bat" } + let(:expected_distance) { 1 } + + it "must returns 1" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + + context "when given two completely different strings" do + let(:string1) { "long_foo" } + let(:string2) { "bar" } + let(:expected_distance) { string1.size } + + it "must return lenght of the longest one" do + expect(subject.levenshtein_distance(string1, string2)).to eq(expected_distance) + end + end + end +end