obfs 0.1.2 → 0.3.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 +4 -4
- data/README.md +602 -0
- data/lib/obfs/levenshtein.rb +41 -0
- data/lib/obfs/store.rb +967 -114
- data/lib/obfs/string_similarity.rb +41 -0
- data/lib/obfs.rb +2 -4
- metadata +9 -7
- data/lib/text/levenshtein.rb +0 -166
- data/lib/text/white_similarity.rb +0 -66
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module OBFS
|
|
2
|
+
module StringSimilarity
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
# Dice overlap of character-bigram multisets. Repeated pairs count separately.
|
|
6
|
+
# Case, path separators and whitespace are normalized before comparison.
|
|
7
|
+
# Empty text has no evidence of similarity; equal nonempty text scores 1.0.
|
|
8
|
+
def similarity(str1, str2)
|
|
9
|
+
left = normalize(str1)
|
|
10
|
+
right = normalize(str2)
|
|
11
|
+
return 0.0 if left.empty? || right.empty?
|
|
12
|
+
return 1.0 if left == right
|
|
13
|
+
|
|
14
|
+
left_pairs = bigrams(left)
|
|
15
|
+
right_pairs = bigrams(right)
|
|
16
|
+
total = left_pairs.values.inject(0, :+) + right_pairs.values.inject(0, :+)
|
|
17
|
+
return 0.0 if total == 0
|
|
18
|
+
|
|
19
|
+
overlap = left_pairs.inject(0) do |sum, entry|
|
|
20
|
+
pair, count = entry
|
|
21
|
+
sum + [count, right_pairs[pair]].min
|
|
22
|
+
end
|
|
23
|
+
2.0 * overlap / total
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize(value)
|
|
29
|
+
value.to_s.encode('UTF-8').downcase
|
|
30
|
+
.gsub(/[\\\/:_\-]+/, ' ')
|
|
31
|
+
.gsub(/\s+/, ' ').strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def bigrams(value)
|
|
35
|
+
counts = Hash.new(0)
|
|
36
|
+
characters = value.each_codepoint.to_a
|
|
37
|
+
characters.each_cons(2) { |pair| counts[pair] += 1 }
|
|
38
|
+
counts
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/obfs.rb
CHANGED
metadata
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: obfs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jensel Gatchalian
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: File-based, object-oriented data store for Ruby
|
|
12
|
+
description: File-based, object-oriented data store for Ruby with recursive access,
|
|
13
|
+
fuzzy discovery, and no external runtime dependencies.
|
|
13
14
|
email: jensel.gatchalian@gmail.com
|
|
14
15
|
executables: []
|
|
15
16
|
extensions: []
|
|
16
17
|
extra_rdoc_files: []
|
|
17
18
|
files:
|
|
19
|
+
- README.md
|
|
18
20
|
- lib/obfs.rb
|
|
21
|
+
- lib/obfs/levenshtein.rb
|
|
19
22
|
- lib/obfs/store.rb
|
|
20
|
-
- lib/
|
|
21
|
-
- lib/text/white_similarity.rb
|
|
23
|
+
- lib/obfs/string_similarity.rb
|
|
22
24
|
homepage: https://github.com/jenselg/obfs-ruby
|
|
23
25
|
licenses:
|
|
24
26
|
- MIT
|
|
@@ -37,7 +39,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
37
39
|
- !ruby/object:Gem::Version
|
|
38
40
|
version: '0'
|
|
39
41
|
requirements: []
|
|
40
|
-
rubygems_version: 4.0.
|
|
42
|
+
rubygems_version: 4.0.20
|
|
41
43
|
specification_version: 4
|
|
42
|
-
summary:
|
|
44
|
+
summary: Filesystem-backed object store with fuzzy discovery
|
|
43
45
|
test_files: []
|
data/lib/text/levenshtein.rb
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Credits to threedaymonk
|
|
3
|
-
# https://github.com/threedaymonk/text/blob/master/lib/text/levenshtein.rb
|
|
4
|
-
#
|
|
5
|
-
# Levenshtein distance algorithm implementation for Ruby, with UTF-8 support.
|
|
6
|
-
#
|
|
7
|
-
# The Levenshtein distance is a measure of how similar two strings s and t are,
|
|
8
|
-
# calculated as the number of deletions/insertions/substitutions needed to
|
|
9
|
-
# transform s into t. The greater the distance, the more the strings differ.
|
|
10
|
-
#
|
|
11
|
-
# The Levenshtein distance is also sometimes referred to as the
|
|
12
|
-
# easier-to-pronounce-and-spell 'edit distance'.
|
|
13
|
-
#
|
|
14
|
-
# Author: Paul Battley (pbattley@gmail.com)
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
module OBFS # :nodoc:
|
|
18
|
-
|
|
19
|
-
module Levenshtein
|
|
20
|
-
|
|
21
|
-
# Calculate the Levenshtein distance between two strings +str1+ and +str2+.
|
|
22
|
-
#
|
|
23
|
-
# The optional argument max_distance can reduce the number of iterations by
|
|
24
|
-
# stopping if the Levenshtein distance exceeds this value. This increases
|
|
25
|
-
# performance where it is only necessary to compare the distance with a
|
|
26
|
-
# reference value instead of calculating the exact distance.
|
|
27
|
-
#
|
|
28
|
-
# The distance is calculated in terms of Unicode codepoints. Be aware that
|
|
29
|
-
# this algorithm does not perform normalisation: if there is a possibility
|
|
30
|
-
# of different normalised forms being used, normalisation should be performed
|
|
31
|
-
# beforehand.
|
|
32
|
-
#
|
|
33
|
-
def distance(str1, str2, max_distance = nil)
|
|
34
|
-
if max_distance
|
|
35
|
-
distance_with_maximum(str1, str2, max_distance)
|
|
36
|
-
else
|
|
37
|
-
distance_without_maximum(str1, str2)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def distance_with_maximum(str1, str2, max_distance) # :nodoc:
|
|
44
|
-
s = str1.encode(Encoding::UTF_8).unpack("U*")
|
|
45
|
-
t = str2.encode(Encoding::UTF_8).unpack("U*")
|
|
46
|
-
|
|
47
|
-
n = s.length
|
|
48
|
-
m = t.length
|
|
49
|
-
big_int = n * m
|
|
50
|
-
|
|
51
|
-
# Swap if necessary so that s is always the shorter of the two strings
|
|
52
|
-
s, t, n, m = t, s, m, n if m < n
|
|
53
|
-
|
|
54
|
-
# If the length difference is already greater than the max_distance, then
|
|
55
|
-
# there is nothing else to check
|
|
56
|
-
if (n - m).abs >= max_distance
|
|
57
|
-
return max_distance
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return 0 if s == t
|
|
61
|
-
return m if n.zero?
|
|
62
|
-
return n if m.zero?
|
|
63
|
-
|
|
64
|
-
# The values necessary for our threshold are written; the ones after must
|
|
65
|
-
# be filled with large integers since the tailing member of the threshold
|
|
66
|
-
# window in the bottom array will run min across them
|
|
67
|
-
d = (m + 1).times.map { |i|
|
|
68
|
-
if i < m || i < max_distance + 1
|
|
69
|
-
i
|
|
70
|
-
else
|
|
71
|
-
big_int
|
|
72
|
-
end
|
|
73
|
-
}
|
|
74
|
-
x = nil
|
|
75
|
-
e = nil
|
|
76
|
-
|
|
77
|
-
n.times do |i|
|
|
78
|
-
# Since we're reusing arrays, we need to be sure to wipe the value left
|
|
79
|
-
# of the starting index; we don't have to worry about the value above the
|
|
80
|
-
# ending index as the arrays were initially filled with large integers
|
|
81
|
-
# and we progress to the right
|
|
82
|
-
if e.nil?
|
|
83
|
-
e = i + 1
|
|
84
|
-
else
|
|
85
|
-
e = big_int
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
diag_index = t.length - s.length + i
|
|
89
|
-
|
|
90
|
-
# If max_distance was specified, we can reduce second loop. So we set
|
|
91
|
-
# up our threshold window.
|
|
92
|
-
# See:
|
|
93
|
-
# Gusfield, Dan (1997). Algorithms on strings, trees, and sequences:
|
|
94
|
-
# computer science and computational biology.
|
|
95
|
-
# Cambridge, UK: Cambridge University Press. ISBN 0-521-58519-8.
|
|
96
|
-
# pp. 263–264.
|
|
97
|
-
min = i - max_distance - 1
|
|
98
|
-
min = 0 if min < 0
|
|
99
|
-
max = i + max_distance
|
|
100
|
-
max = m - 1 if max > m - 1
|
|
101
|
-
|
|
102
|
-
min.upto(max) do |j|
|
|
103
|
-
# If the diagonal value is already greater than the max_distance
|
|
104
|
-
# then we can safety return: the diagonal will never go lower again.
|
|
105
|
-
# See: http://www.levenshtein.net/
|
|
106
|
-
if j == diag_index && d[j] >= max_distance
|
|
107
|
-
return max_distance
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
cost = s[i] == t[j] ? 0 : 1
|
|
111
|
-
insertion = d[j + 1] + 1
|
|
112
|
-
deletion = e + 1
|
|
113
|
-
substitution = d[j] + cost
|
|
114
|
-
x = insertion < deletion ? insertion : deletion
|
|
115
|
-
x = substitution if substitution < x
|
|
116
|
-
|
|
117
|
-
d[j] = e
|
|
118
|
-
e = x
|
|
119
|
-
end
|
|
120
|
-
d[m] = x
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
if x > max_distance
|
|
124
|
-
return max_distance
|
|
125
|
-
else
|
|
126
|
-
return x
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def distance_without_maximum(str1, str2) # :nodoc:
|
|
131
|
-
s = str1.encode(Encoding::UTF_8).unpack("U*")
|
|
132
|
-
t = str2.encode(Encoding::UTF_8).unpack("U*")
|
|
133
|
-
|
|
134
|
-
n = s.length
|
|
135
|
-
m = t.length
|
|
136
|
-
|
|
137
|
-
return m if n.zero?
|
|
138
|
-
return n if m.zero?
|
|
139
|
-
|
|
140
|
-
d = (0..m).to_a
|
|
141
|
-
x = nil
|
|
142
|
-
|
|
143
|
-
n.times do |i|
|
|
144
|
-
e = i + 1
|
|
145
|
-
m.times do |j|
|
|
146
|
-
cost = s[i] == t[j] ? 0 : 1
|
|
147
|
-
insertion = d[j + 1] + 1
|
|
148
|
-
deletion = e + 1
|
|
149
|
-
substitution = d[j] + cost
|
|
150
|
-
x = insertion < deletion ? insertion : deletion
|
|
151
|
-
x = substitution if substitution < x
|
|
152
|
-
|
|
153
|
-
d[j] = e
|
|
154
|
-
e = x
|
|
155
|
-
end
|
|
156
|
-
d[m] = x
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
return x
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
extend self
|
|
163
|
-
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Credits to threedaymonk
|
|
3
|
-
# https://github.com/threedaymonk/text/blob/master/lib/text/white_similarity.rb
|
|
4
|
-
#
|
|
5
|
-
# encoding: utf-8
|
|
6
|
-
# Original author: Wilker Lúcio <wilkerlucio@gmail.com>
|
|
7
|
-
|
|
8
|
-
module OBFS
|
|
9
|
-
|
|
10
|
-
# Ruby implementation of the string similarity described by Simon White
|
|
11
|
-
# at: http://www.catalysoft.com/articles/StrikeAMatch.html
|
|
12
|
-
#
|
|
13
|
-
# 2 * |pairs(s1) INTERSECT pairs(s2)|
|
|
14
|
-
# similarity(s1, s2) = -----------------------------------
|
|
15
|
-
# |pairs(s1)| + |pairs(s2)|
|
|
16
|
-
#
|
|
17
|
-
# e.g.
|
|
18
|
-
# 2 * |{FR, NC}|
|
|
19
|
-
# similarity(FRANCE, FRENCH) = ---------------------------------------
|
|
20
|
-
# |{FR,RA,AN,NC,CE}| + |{FR,RE,EN,NC,CH}|
|
|
21
|
-
#
|
|
22
|
-
# = (2 * 2) / (5 + 5)
|
|
23
|
-
#
|
|
24
|
-
# = 0.4
|
|
25
|
-
#
|
|
26
|
-
# WhiteSimilarity.new.similarity("FRANCE", "FRENCH")
|
|
27
|
-
#
|
|
28
|
-
class WhiteSimilarity
|
|
29
|
-
|
|
30
|
-
def self.similarity(str1, str2)
|
|
31
|
-
new.similarity(str1, str2)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def initialize
|
|
35
|
-
@word_letter_pairs = {}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def similarity(str1, str2)
|
|
39
|
-
pairs1 = word_letter_pairs(str1)
|
|
40
|
-
pairs2 = word_letter_pairs(str2).dup
|
|
41
|
-
|
|
42
|
-
union = pairs1.length + pairs2.length
|
|
43
|
-
|
|
44
|
-
intersection = 0
|
|
45
|
-
pairs1.each do |pair1|
|
|
46
|
-
if index = pairs2.index(pair1)
|
|
47
|
-
intersection += 1
|
|
48
|
-
pairs2.delete_at(index)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
(2.0 * intersection) / union
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def word_letter_pairs(str)
|
|
58
|
-
@word_letter_pairs[str] ||=
|
|
59
|
-
str.upcase.split(/\s+/).map{ |word|
|
|
60
|
-
(0 ... (word.length - 1)).map { |i| word[i, 2] }
|
|
61
|
-
}.flatten.freeze
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
end
|