linkage 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +10 -0
  2. data/Gemfile +15 -13
  3. data/Gemfile.lock +67 -37
  4. data/Guardfile +0 -2
  5. data/Rakefile +122 -25
  6. data/lib/linkage/comparator.rb +172 -0
  7. data/lib/linkage/comparators/binary.rb +12 -0
  8. data/lib/linkage/comparators/compare.rb +46 -0
  9. data/lib/linkage/comparators/within.rb +32 -0
  10. data/lib/linkage/configuration.rb +285 -153
  11. data/lib/linkage/data.rb +32 -7
  12. data/lib/linkage/dataset.rb +107 -32
  13. data/lib/linkage/decollation.rb +93 -0
  14. data/lib/linkage/expectation.rb +21 -0
  15. data/lib/linkage/expectations/exhaustive.rb +63 -0
  16. data/lib/linkage/expectations/simple.rb +168 -0
  17. data/lib/linkage/field.rb +30 -4
  18. data/lib/linkage/field_set.rb +6 -3
  19. data/lib/linkage/function.rb +50 -3
  20. data/lib/linkage/functions/binary.rb +30 -0
  21. data/lib/linkage/functions/cast.rb +54 -0
  22. data/lib/linkage/functions/length.rb +29 -0
  23. data/lib/linkage/functions/strftime.rb +12 -11
  24. data/lib/linkage/functions/trim.rb +8 -0
  25. data/lib/linkage/group.rb +20 -0
  26. data/lib/linkage/import_buffer.rb +5 -16
  27. data/lib/linkage/meta_object.rb +139 -0
  28. data/lib/linkage/result_set.rb +74 -17
  29. data/lib/linkage/runner/single_threaded.rb +125 -10
  30. data/lib/linkage/version.rb +3 -0
  31. data/lib/linkage.rb +11 -0
  32. data/linkage.gemspec +16 -121
  33. data/test/config.yml +5 -0
  34. data/test/helper.rb +73 -8
  35. data/test/integration/test_collation.rb +45 -0
  36. data/test/integration/test_configuration.rb +268 -0
  37. data/test/integration/test_cross_linkage.rb +4 -17
  38. data/test/integration/test_dataset.rb +45 -2
  39. data/test/integration/test_dual_linkage.rb +40 -24
  40. data/test/integration/test_functions.rb +22 -0
  41. data/test/integration/test_result_set.rb +85 -0
  42. data/test/integration/test_scoring.rb +84 -0
  43. data/test/integration/test_self_linkage.rb +5 -0
  44. data/test/integration/test_within_comparator.rb +100 -0
  45. data/test/unit/comparators/test_compare.rb +105 -0
  46. data/test/unit/comparators/test_within.rb +57 -0
  47. data/test/unit/expectations/test_exhaustive.rb +111 -0
  48. data/test/unit/expectations/test_simple.rb +303 -0
  49. data/test/unit/functions/test_binary.rb +54 -0
  50. data/test/unit/functions/test_cast.rb +98 -0
  51. data/test/unit/functions/test_length.rb +52 -0
  52. data/test/unit/functions/test_strftime.rb +17 -13
  53. data/test/unit/functions/test_trim.rb +11 -4
  54. data/test/unit/test_comparator.rb +124 -0
  55. data/test/unit/test_configuration.rb +137 -175
  56. data/test/unit/test_data.rb +44 -0
  57. data/test/unit/test_dataset.rb +73 -21
  58. data/test/unit/test_decollation.rb +201 -0
  59. data/test/unit/test_field.rb +38 -14
  60. data/test/unit/test_field_set.rb +12 -8
  61. data/test/unit/test_function.rb +83 -16
  62. data/test/unit/test_group.rb +28 -0
  63. data/test/unit/test_import_buffer.rb +13 -27
  64. data/test/unit/test_meta_object.rb +208 -0
  65. data/test/unit/test_result_set.rb +221 -3
  66. metadata +82 -190
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ coverage
2
+ rdoc
3
+ doc
4
+ .yardoc
5
+ .bundle
6
+ pkg
7
+ test.rb
8
+ results.db
9
+ .rbenv-version
10
+ bin
data/Gemfile CHANGED
@@ -1,19 +1,21 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
 
3
- gem "sequel"
3
+ gemspec
4
4
 
5
5
  group :development do
6
- gem "bundler"
7
- gem "jeweler"
8
- gem "test-unit"
9
- gem "mocha"
10
- gem "sqlite3"
11
- gem "yard"
12
- gem "rake"
13
- gem "versionomy"
14
- gem "mysql2"
15
- gem 'pry'
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.3)
5
- coderay (1.0.6)
6
- ffi (1.0.11)
7
- git (1.2.5)
8
- guard (1.0.1)
9
- ffi (>= 0.5.0)
10
- thor (~> 0.14.6)
11
- guard-test (0.4.3)
12
- guard (>= 0.4)
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 (1.0.2)
15
- guard (>= 0.2.2)
33
+ guard-yard (2.0.1)
34
+ guard (>= 1.1.0)
16
35
  yard (>= 0.7.0)
17
- jeweler (1.8.3)
18
- bundler (~> 1.0)
19
- git (>= 1.2.5)
20
- rake
21
- rdoc
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.7.1)
25
- mocha (0.10.5)
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.9)
46
+ pry (0.9.11.4)
29
47
  coderay (~> 1.0.5)
30
- method_source (~> 0.7.1)
31
- slop (>= 2.4.4, < 3)
32
- rake (0.9.2.2)
33
- rdiscount (1.6.8)
34
- rdoc (3.12)
35
- json (~> 1.4)
36
- sequel (3.34.1)
37
- slop (2.4.4)
38
- sqlite3 (1.3.6)
39
- test-unit (2.4.8)
40
- thor (0.14.6)
41
- versionomy (0.4.3)
42
- blockenspiel (>= 0.4.3)
43
- yard (0.7.5)
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
- jeweler
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
@@ -9,5 +9,3 @@ end
9
9
  guard 'yard' do
10
10
  watch(%r{lib/[^.].*\.rb$})
11
11
  end
12
-
13
- notification :notifysend, :u => :normal
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
- task :build => :gemspec
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