linkage 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|