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.
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