linkage 0.0.6 → 0.0.8
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.
- data/.gitignore +10 -0
- data/Gemfile +15 -13
- data/Gemfile.lock +67 -37
- data/Guardfile +0 -2
- data/Rakefile +122 -25
- data/lib/linkage/comparator.rb +172 -0
- data/lib/linkage/comparators/binary.rb +12 -0
- data/lib/linkage/comparators/compare.rb +46 -0
- data/lib/linkage/comparators/within.rb +32 -0
- data/lib/linkage/configuration.rb +285 -153
- data/lib/linkage/data.rb +32 -7
- data/lib/linkage/dataset.rb +107 -32
- data/lib/linkage/decollation.rb +93 -0
- data/lib/linkage/expectation.rb +21 -0
- data/lib/linkage/expectations/exhaustive.rb +63 -0
- data/lib/linkage/expectations/simple.rb +168 -0
- data/lib/linkage/field.rb +30 -4
- data/lib/linkage/field_set.rb +6 -3
- data/lib/linkage/function.rb +50 -3
- data/lib/linkage/functions/binary.rb +30 -0
- data/lib/linkage/functions/cast.rb +54 -0
- data/lib/linkage/functions/length.rb +29 -0
- data/lib/linkage/functions/strftime.rb +12 -11
- data/lib/linkage/functions/trim.rb +8 -0
- data/lib/linkage/group.rb +20 -0
- data/lib/linkage/import_buffer.rb +5 -16
- data/lib/linkage/meta_object.rb +139 -0
- data/lib/linkage/result_set.rb +74 -17
- data/lib/linkage/runner/single_threaded.rb +125 -10
- data/lib/linkage/version.rb +3 -0
- data/lib/linkage.rb +11 -0
- data/linkage.gemspec +16 -121
- data/test/config.yml +5 -0
- data/test/helper.rb +73 -8
- data/test/integration/test_collation.rb +45 -0
- data/test/integration/test_configuration.rb +268 -0
- data/test/integration/test_cross_linkage.rb +4 -17
- data/test/integration/test_dataset.rb +45 -2
- data/test/integration/test_dual_linkage.rb +40 -24
- data/test/integration/test_functions.rb +22 -0
- data/test/integration/test_result_set.rb +85 -0
- data/test/integration/test_scoring.rb +84 -0
- data/test/integration/test_self_linkage.rb +5 -0
- data/test/integration/test_within_comparator.rb +100 -0
- data/test/unit/comparators/test_compare.rb +105 -0
- data/test/unit/comparators/test_within.rb +57 -0
- data/test/unit/expectations/test_exhaustive.rb +111 -0
- data/test/unit/expectations/test_simple.rb +303 -0
- data/test/unit/functions/test_binary.rb +54 -0
- data/test/unit/functions/test_cast.rb +98 -0
- data/test/unit/functions/test_length.rb +52 -0
- data/test/unit/functions/test_strftime.rb +17 -13
- data/test/unit/functions/test_trim.rb +11 -4
- data/test/unit/test_comparator.rb +124 -0
- data/test/unit/test_configuration.rb +137 -175
- data/test/unit/test_data.rb +44 -0
- data/test/unit/test_dataset.rb +73 -21
- data/test/unit/test_decollation.rb +201 -0
- data/test/unit/test_field.rb +38 -14
- data/test/unit/test_field_set.rb +12 -8
- data/test/unit/test_function.rb +83 -16
- data/test/unit/test_group.rb +28 -0
- data/test/unit/test_import_buffer.rb +13 -27
- data/test/unit/test_meta_object.rb +208 -0
- data/test/unit/test_result_set.rb +221 -3
- metadata +82 -190
data/.gitignore
ADDED
data/Gemfile
CHANGED
@@ -1,19 +1,21 @@
|
|
1
|
-
source
|
1
|
+
source 'http://rubygems.org'
|
2
2
|
|
3
|
-
|
3
|
+
gemspec
|
4
4
|
|
5
5
|
group :development do
|
6
|
-
gem
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
gem
|
10
|
-
gem
|
11
|
-
gem
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
15
|
-
gem '
|
6
|
+
gem 'bundler'
|
7
|
+
gem 'test-unit'
|
8
|
+
gem 'mocha'
|
9
|
+
gem 'yard'
|
10
|
+
gem 'rake'
|
11
|
+
gem 'versionomy'
|
12
|
+
gem 'sqlite3', :platforms => :ruby
|
13
|
+
gem 'mysql2', :platforms => :ruby
|
14
|
+
gem 'jdbc-sqlite3', :platforms => :jruby
|
15
|
+
gem 'jdbc-mysql', :platforms => :jruby
|
16
16
|
gem 'rdiscount'
|
17
17
|
gem 'guard-test'
|
18
|
-
gem 'guard-yard'
|
18
|
+
gem 'guard-yard', :platforms => :ruby_19
|
19
|
+
gem 'rb-inotify', '~> 0.8.8'
|
20
|
+
gem 'debugger'
|
19
21
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,61 +1,91 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
linkage (0.0.8)
|
5
|
+
hashery
|
6
|
+
sequel
|
7
|
+
sequel-collation
|
8
|
+
|
1
9
|
GEM
|
2
10
|
remote: http://rubygems.org/
|
3
11
|
specs:
|
4
|
-
blockenspiel (0.4.
|
5
|
-
coderay (1.0.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
blockenspiel (0.4.5)
|
13
|
+
coderay (1.0.8)
|
14
|
+
columnize (0.3.6)
|
15
|
+
debugger (1.3.0)
|
16
|
+
columnize (>= 0.3.1)
|
17
|
+
debugger-linecache (~> 1.1.1)
|
18
|
+
debugger-ruby_core_source (~> 1.1.7)
|
19
|
+
debugger-linecache (1.1.2)
|
20
|
+
debugger-ruby_core_source (>= 1.1.1)
|
21
|
+
debugger-ruby_core_source (1.1.7)
|
22
|
+
ffi (1.3.1)
|
23
|
+
ffi (1.3.1-java)
|
24
|
+
guard (1.6.2)
|
25
|
+
listen (>= 0.6.0)
|
26
|
+
lumberjack (>= 1.0.2)
|
27
|
+
pry (>= 0.9.10)
|
28
|
+
terminal-table (>= 1.4.3)
|
29
|
+
thor (>= 0.14.6)
|
30
|
+
guard-test (0.7.0)
|
31
|
+
guard (>= 1.1)
|
13
32
|
test-unit (~> 2.2)
|
14
|
-
guard-yard (
|
15
|
-
guard (>=
|
33
|
+
guard-yard (2.0.1)
|
34
|
+
guard (>= 1.1.0)
|
16
35
|
yard (>= 0.7.0)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
json (1.6.6)
|
36
|
+
hashery (2.1.0)
|
37
|
+
jdbc-mysql (5.1.22.1)
|
38
|
+
jdbc-sqlite3 (3.7.2.1)
|
39
|
+
listen (0.7.2)
|
40
|
+
lumberjack (1.0.2)
|
23
41
|
metaclass (0.0.1)
|
24
|
-
method_source (0.
|
25
|
-
mocha (0.
|
42
|
+
method_source (0.8.1)
|
43
|
+
mocha (0.13.2)
|
26
44
|
metaclass (~> 0.0.1)
|
27
45
|
mysql2 (0.3.11)
|
28
|
-
pry (0.9.
|
46
|
+
pry (0.9.11.4)
|
29
47
|
coderay (~> 1.0.5)
|
30
|
-
method_source (~> 0.
|
31
|
-
slop (
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
48
|
+
method_source (~> 0.8)
|
49
|
+
slop (~> 3.4)
|
50
|
+
pry (0.9.11.4-java)
|
51
|
+
coderay (~> 1.0.5)
|
52
|
+
method_source (~> 0.8)
|
53
|
+
slop (~> 3.4)
|
54
|
+
spoon (~> 0.0)
|
55
|
+
rake (10.0.3)
|
56
|
+
rb-inotify (0.8.8)
|
57
|
+
ffi (>= 0.5.0)
|
58
|
+
rdiscount (2.0.7)
|
59
|
+
sequel (3.44.0)
|
60
|
+
sequel-collation (0.1.0)
|
61
|
+
sequel
|
62
|
+
slop (3.4.3)
|
63
|
+
spoon (0.0.1)
|
64
|
+
sqlite3 (1.3.7)
|
65
|
+
terminal-table (1.4.5)
|
66
|
+
test-unit (2.5.4)
|
67
|
+
thor (0.17.0)
|
68
|
+
versionomy (0.4.4)
|
69
|
+
blockenspiel (>= 0.4.5)
|
70
|
+
yard (0.8.4.1)
|
44
71
|
|
45
72
|
PLATFORMS
|
73
|
+
java
|
46
74
|
ruby
|
47
75
|
|
48
76
|
DEPENDENCIES
|
49
77
|
bundler
|
78
|
+
debugger
|
50
79
|
guard-test
|
51
80
|
guard-yard
|
52
|
-
|
81
|
+
jdbc-mysql
|
82
|
+
jdbc-sqlite3
|
83
|
+
linkage!
|
53
84
|
mocha
|
54
85
|
mysql2
|
55
|
-
pry
|
56
86
|
rake
|
87
|
+
rb-inotify (~> 0.8.8)
|
57
88
|
rdiscount
|
58
|
-
sequel
|
59
89
|
sqlite3
|
60
90
|
test-unit
|
61
91
|
versionomy
|
data/Guardfile
CHANGED
data/Rakefile
CHANGED
@@ -10,20 +10,7 @@ rescue Bundler::BundlerError => e
|
|
10
10
|
exit e.status_code
|
11
11
|
end
|
12
12
|
require 'rake'
|
13
|
-
|
14
|
-
require 'jeweler'
|
15
|
-
Jeweler::Tasks.new do |gem|
|
16
|
-
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
-
gem.name = "linkage"
|
18
|
-
gem.homepage = "http://github.com/coupler/linkage"
|
19
|
-
gem.license = "MIT"
|
20
|
-
gem.summary = %Q{Record linkage library}
|
21
|
-
gem.description = %Q{Performs record linkage between one or two datasets, using Sequel on the backend}
|
22
|
-
gem.email = "jeremy.f.stephens@vanderbilt.edu"
|
23
|
-
gem.authors = ["Jeremy Stephens"]
|
24
|
-
# dependencies defined in Gemfile
|
25
|
-
end
|
26
|
-
Jeweler::RubygemsDotOrgTasks.new
|
13
|
+
require "bundler/gem_tasks"
|
27
14
|
|
28
15
|
require 'rake/testtask'
|
29
16
|
Rake::TestTask.new(:test) do |test|
|
@@ -34,19 +21,129 @@ end
|
|
34
21
|
|
35
22
|
task :default => :test
|
36
23
|
|
37
|
-
require 'rdoc/task'
|
38
|
-
Rake::RDocTask.new do |rdoc|
|
39
|
-
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
-
|
41
|
-
rdoc.rdoc_dir = 'rdoc'
|
42
|
-
rdoc.title = "linkage #{version}"
|
43
|
-
rdoc.rdoc_files.include('README*')
|
44
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
-
end
|
46
|
-
|
47
24
|
require 'yard'
|
48
25
|
YARD::Rake::YardocTask.new do |t|
|
49
26
|
t.files = ['lib/**/*.rb']
|
50
27
|
end
|
51
28
|
|
52
|
-
|
29
|
+
# Yoinked from https://github.com/rails/rails/blob/master/railties/lib/rails/tasks/annotations.rake
|
30
|
+
namespace :notes do
|
31
|
+
["OPTIMIZE", "FIXME", "TODO"].each do |annotation|
|
32
|
+
desc "Enumerate all #{annotation} annotations"
|
33
|
+
task annotation.downcase.intern do
|
34
|
+
SourceAnnotationExtractor.enumerate annotation
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM"
|
39
|
+
task :custom do
|
40
|
+
SourceAnnotationExtractor.enumerate ENV['ANNOTATION']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Yoinked from https://github.com/rails/rails/blob/master/railties/lib/rails/source_annotation_extractor.rb
|
45
|
+
#
|
46
|
+
# Implements the logic behind the rake tasks for annotations like
|
47
|
+
#
|
48
|
+
# rake notes
|
49
|
+
# rake notes:optimize
|
50
|
+
#
|
51
|
+
# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>.
|
52
|
+
#
|
53
|
+
# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
|
54
|
+
# represent the line where the annotation lives, its tag, and its text. Note
|
55
|
+
# the filename is not stored.
|
56
|
+
#
|
57
|
+
# Annotations are looked for in comments and modulus whitespace they have to
|
58
|
+
# start with the tag optionally followed by a colon. Everything up to the end
|
59
|
+
# of the line (or closing ERB comment tag) is considered to be their text.
|
60
|
+
class SourceAnnotationExtractor
|
61
|
+
class Annotation < Struct.new(:line, :tag, :text)
|
62
|
+
|
63
|
+
# Returns a representation of the annotation that looks like this:
|
64
|
+
#
|
65
|
+
# [126] [TODO] This algorithm is simple and clearly correct, make it faster.
|
66
|
+
#
|
67
|
+
# If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
|
68
|
+
# Otherwise the string contains just line and text.
|
69
|
+
def to_s(options={})
|
70
|
+
s = "[%3d] " % line
|
71
|
+
s << "[#{tag}] " if options[:tag]
|
72
|
+
s << text
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Prints all annotations with tag +tag+ under the root directories +app+, +config+, +lib+,
|
77
|
+
# +script+, and +test+ (recursively). Only filenames with extension
|
78
|
+
# +.builder+, +.rb+, and +.erb+ are taken into account. The +options+
|
79
|
+
# hash is passed to each annotation's +to_s+.
|
80
|
+
#
|
81
|
+
# This class method is the single entry point for the rake tasks.
|
82
|
+
def self.enumerate(tag, options={})
|
83
|
+
extractor = new(tag)
|
84
|
+
extractor.display(extractor.find, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
attr_reader :tag
|
88
|
+
|
89
|
+
def initialize(tag)
|
90
|
+
@tag = tag
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a hash that maps filenames under +dirs+ (recursively) to arrays
|
94
|
+
# with their annotations.
|
95
|
+
def find(dirs=%w(app config lib script test))
|
96
|
+
dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a hash that maps filenames under +dir+ (recursively) to arrays
|
100
|
+
# with their annotations. Only files with annotations are included, and only
|
101
|
+
# those with extension +.builder+, +.rb+, +.erb+, +.haml+, +.slim+ and +.coffee+
|
102
|
+
# are taken into account.
|
103
|
+
def find_in(dir)
|
104
|
+
results = {}
|
105
|
+
|
106
|
+
Dir.glob("#{dir}/*") do |item|
|
107
|
+
next if File.basename(item)[0] == ?.
|
108
|
+
|
109
|
+
if File.directory?(item)
|
110
|
+
results.update(find_in(item))
|
111
|
+
elsif item =~ /\.(builder|rb|coffee)$/
|
112
|
+
results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/))
|
113
|
+
elsif item =~ /\.erb$/
|
114
|
+
results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/))
|
115
|
+
elsif item =~ /\.haml$/
|
116
|
+
results.update(extract_annotations_from(item, /-\s*#\s*(#{tag}):?\s*(.*)$/))
|
117
|
+
elsif item =~ /\.slim$/
|
118
|
+
results.update(extract_annotations_from(item, /\/\s*\s*(#{tag}):?\s*(.*)$/))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
results
|
123
|
+
end
|
124
|
+
|
125
|
+
# If +file+ is the filename of a file that contains annotations this method returns
|
126
|
+
# a hash with a single entry that maps +file+ to an array of its annotations.
|
127
|
+
# Otherwise it returns an empty hash.
|
128
|
+
def extract_annotations_from(file, pattern)
|
129
|
+
lineno = 0
|
130
|
+
result = File.readlines(file).inject([]) do |list, line|
|
131
|
+
lineno += 1
|
132
|
+
next list unless line =~ pattern
|
133
|
+
list << Annotation.new(lineno, $1, $2)
|
134
|
+
end
|
135
|
+
result.empty? ? {} : { file => result }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Prints the mapping from filenames to annotations in +results+ ordered by filename.
|
139
|
+
# The +options+ hash is passed to each annotation's +to_s+.
|
140
|
+
def display(results, options={})
|
141
|
+
results.keys.sort.each do |file|
|
142
|
+
puts "#{file}:"
|
143
|
+
results[file].each do |note|
|
144
|
+
puts " * #{note.to_s(options)}"
|
145
|
+
end
|
146
|
+
puts
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Linkage
|
2
|
+
# @abstract Abstract class to represent record comparators.
|
3
|
+
class Comparator
|
4
|
+
# Register a new comparator.
|
5
|
+
#
|
6
|
+
# @param [Class] klass Comparator subclass
|
7
|
+
def self.register(klass)
|
8
|
+
name = nil
|
9
|
+
begin
|
10
|
+
name = klass.comparator_name
|
11
|
+
rescue NotImplementedError
|
12
|
+
raise ArgumentError, "comparator_name class method must be defined"
|
13
|
+
end
|
14
|
+
|
15
|
+
if !klass.instance_methods(false).include?(:score)
|
16
|
+
raise ArgumentError, "class must define the score method"
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
if klass.parameters.length > 0
|
21
|
+
@comparators ||= {}
|
22
|
+
@comparators[name] = klass
|
23
|
+
else
|
24
|
+
raise ArgumentError, "class must have at least one parameter"
|
25
|
+
end
|
26
|
+
rescue NotImplementedError
|
27
|
+
raise ArgumentError, "parameters class method must be defined"
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
range = klass.score_range
|
32
|
+
if !range.is_a?(Range) || !range.first.is_a?(Numeric) ||
|
33
|
+
!range.last.is_a?(Numeric)
|
34
|
+
raise ArgumentError, "score_range must be a Range of two numbers"
|
35
|
+
end
|
36
|
+
rescue NotImplementedError
|
37
|
+
raise ArgumentError, "score_range class method must be defined"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.[](name)
|
42
|
+
@comparators ? @comparators[name] : nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# @abstract Override this to return the name of the comparator.
|
46
|
+
# @return [String]
|
47
|
+
def self.comparator_name
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
# @abstract Override this to require a specific number of arguments of a
|
52
|
+
# certain class. To require two parameters of either String or Integer,
|
53
|
+
# do something like this:
|
54
|
+
#
|
55
|
+
# @@parameters = [[String, Integer], [String, Integer]]
|
56
|
+
# def self.parameters
|
57
|
+
# @@parameters
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# At least one argument must be defined.
|
61
|
+
# @return [Array]
|
62
|
+
def self.parameters
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
# @abstract Override this to return a Range of the possible scores for the
|
67
|
+
# comparator.
|
68
|
+
# @return [Range]
|
69
|
+
def self.score_range
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_reader :args, :lhs_args, :rhs_args
|
74
|
+
|
75
|
+
# Create a new Comparator object.
|
76
|
+
# @param [Linkage::MetaObject, Hash] args Comparator arguments
|
77
|
+
def initialize(*args)
|
78
|
+
@args = args
|
79
|
+
@lhs_args = []
|
80
|
+
@rhs_args = []
|
81
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
82
|
+
process_args
|
83
|
+
end
|
84
|
+
|
85
|
+
# @abstract Override this to return the score of the linkage strength of
|
86
|
+
# two records.
|
87
|
+
# @return [Numeric]
|
88
|
+
def score(record_1, record_2)
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def process_args
|
95
|
+
parameters = self.class.parameters
|
96
|
+
if parameters.length != @args.length
|
97
|
+
raise ArgumentError, "wrong number of arguments (#{@args.length} for #{parameters.length})"
|
98
|
+
end
|
99
|
+
|
100
|
+
first_side = nil
|
101
|
+
second_side = nil
|
102
|
+
@args.each_with_index do |arg, i|
|
103
|
+
type = arg.ruby_type[:type]
|
104
|
+
|
105
|
+
parameter_types = parameters[i]
|
106
|
+
if parameter_types.last.is_a?(Hash)
|
107
|
+
parameter_options = parameter_types[-1]
|
108
|
+
parameter_types = parameter_types[0..-2]
|
109
|
+
else
|
110
|
+
parameter_options = {}
|
111
|
+
end
|
112
|
+
|
113
|
+
if parameter_types[0] != :any && !parameter_types.include?(type)
|
114
|
+
raise TypeError, "expected type #{parameters[i].join(" or ")}, got #{type}"
|
115
|
+
end
|
116
|
+
|
117
|
+
if parameter_options.has_key?(:values) && arg.raw? && !parameter_options[:values].include?(arg.object)
|
118
|
+
raise ArgumentError, "argument #{i + 1} (#{arg.object.inspect}) was not one of the expected values: #{parameter_options[:values].inspect}"
|
119
|
+
end
|
120
|
+
|
121
|
+
if parameter_options.has_key?(:same_type_as)
|
122
|
+
arg_index = parameter_options[:same_type_as]
|
123
|
+
other_type = @args[arg_index].ruby_type[:type]
|
124
|
+
if type != other_type
|
125
|
+
raise TypeError, "argument #{i + 1} (#{type}) was expected to have the same type as argument #{arg_index + 1} (#{other_type})"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if parameter_options.has_key?(:static) &&
|
130
|
+
parameter_options[:static] != arg.static?
|
131
|
+
raise TypeError, "argument #{i + 1} was expected to #{arg.static? ? "not be" : "be"} static"
|
132
|
+
end
|
133
|
+
|
134
|
+
if !arg.static?
|
135
|
+
if first_side.nil?
|
136
|
+
first_side = arg.side
|
137
|
+
elsif arg.side != first_side && second_side.nil?
|
138
|
+
second_side = arg.side
|
139
|
+
end
|
140
|
+
|
141
|
+
valid_side = true
|
142
|
+
case parameter_options[:side]
|
143
|
+
when :first
|
144
|
+
if arg.side != first_side
|
145
|
+
valid_side = false
|
146
|
+
end
|
147
|
+
when :second
|
148
|
+
if second_side.nil? || arg.side != second_side
|
149
|
+
valid_side = false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if !valid_side
|
154
|
+
raise TypeError, "argument #{i + 1} was expected to have a different side value"
|
155
|
+
end
|
156
|
+
|
157
|
+
case arg.side
|
158
|
+
when :lhs
|
159
|
+
@lhs_args << arg
|
160
|
+
when :rhs
|
161
|
+
@rhs_args << arg
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
path = File.expand_path(File.join(File.dirname(__FILE__), "comparators"))
|
170
|
+
require File.join(path, "binary")
|
171
|
+
require File.join(path, "compare")
|
172
|
+
require File.join(path, "within")
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Linkage
|
2
|
+
module Comparators
|
3
|
+
# @abstract Convenient abstract class for comparators that only return
|
4
|
+
# true/false values (0 or 1).
|
5
|
+
class Binary < Comparator
|
6
|
+
@@score_range = 0..1
|
7
|
+
def self.score_range
|
8
|
+
@@score_range
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Linkage
|
2
|
+
module Comparators
|
3
|
+
class Compare < Binary
|
4
|
+
@@parameters = [
|
5
|
+
[:any, :static => false, :side => :first],
|
6
|
+
[String, :values => %w{> >= <= < !=}],
|
7
|
+
[:any, :same_type_as => 0, :static => false, :side => :second]
|
8
|
+
]
|
9
|
+
def self.parameters
|
10
|
+
@@parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
@@comparator_name = 'compare'
|
14
|
+
def self.comparator_name
|
15
|
+
@@comparator_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
@name_1 = @args[0].name
|
21
|
+
@operator = @args[1].object
|
22
|
+
@name_2 = @args[2].name
|
23
|
+
end
|
24
|
+
|
25
|
+
def score(record_1, record_2)
|
26
|
+
result =
|
27
|
+
case @operator
|
28
|
+
when '!='
|
29
|
+
record_1[@name_1] != record_2[@name_2]
|
30
|
+
when '>'
|
31
|
+
record_1[@name_1] > record_2[@name_2]
|
32
|
+
when '>='
|
33
|
+
record_1[@name_1] >= record_2[@name_2]
|
34
|
+
when '<='
|
35
|
+
record_1[@name_1] <= record_2[@name_2]
|
36
|
+
when '<'
|
37
|
+
record_1[@name_1] < record_2[@name_2]
|
38
|
+
end
|
39
|
+
|
40
|
+
result ? 1 : 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Comparator.register(Compare)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Linkage
|
2
|
+
module Comparators
|
3
|
+
class Within < Binary
|
4
|
+
@@parameters = [
|
5
|
+
[:any, :static => false, :side => :first],
|
6
|
+
[Fixnum],
|
7
|
+
[:any, :same_type_as => 0, :static => false, :side => :second]
|
8
|
+
]
|
9
|
+
def self.parameters
|
10
|
+
@@parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
@@comparator_name = 'within'
|
14
|
+
def self.comparator_name
|
15
|
+
@@comparator_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
@name_1 = @args[0].name
|
21
|
+
@value = @args[1].object
|
22
|
+
@name_2 = @args[2].name
|
23
|
+
end
|
24
|
+
|
25
|
+
def score(record_1, record_2)
|
26
|
+
(record_1[@name_1] - record_2[@name_2]).abs <= @value ? 1 : 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Comparator.register(Within)
|
31
|
+
end
|
32
|
+
end
|