Skip to content

Commit

Permalink
Introduce concept of strict and not diffs. #10.
Browse files Browse the repository at this point in the history
The default (non-strict) diff attempts to only include meaningful
changes. It ignores:

* Changes to whitespace, punctuation, or case in code list labels
* Variable status changes which are not actionable
* The unused "global_value" and "master_cl" attributes on CodeListEntry

The strict diff includes all these things along with the meaningful
changes.
  • Loading branch information
rsutphin committed Mar 2, 2013
1 parent 9f5466b commit fa27a02
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 102 deletions.
27 changes: 18 additions & 9 deletions lib/ncs_navigator/mdes/code_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,29 @@ def initialize(value)

alias :to_s :value

# @private
DIFF_CRITERIA = {
:value => Differences::ValueCriterion.new,
:label => Differences::ValueCriterion.new,
:global_value => Differences::ValueCriterion.new,
:master_cl => Differences::ValueCriterion.new
}
def diff_criteria(diff_options)
if diff_options[:strict]
{
:value => Differences::ValueCriterion.new,
:label => Differences::ValueCriterion.new,
:global_value => Differences::ValueCriterion.new,
:master_cl => Differences::ValueCriterion.new
}
else
{
:value => Differences::ValueCriterion.new,
:label => Differences::ValueCriterion.new(:value_extractor => :word_chars_downcase)
}
end
end
protected :diff_criteria

##
# Computes the differences between this code list entry and the other.
#
# @return [Differences::Entry,nil]
def diff(other)
Differences::Entry.compute(self, other, DIFF_CRITERIA)
def diff(other, options={})
Differences::Entry.compute(self, other, diff_criteria(options), options)
end
end
end
21 changes: 18 additions & 3 deletions lib/ncs_navigator/mdes/differences/collection_criterion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ class CollectionCriterion
def initialize(alignment_attribute, options={})
@alignment_attribute = alignment_attribute
@attribute = options.delete(:collection)
@alignment_value_extractor =
select_value_extractor(options.delete(:value_extractor))
end

def apply(c1, c2)
def apply(c1, c2, diff_options)
c1_map = map_for_alignment(c1)
c2_map = map_for_alignment(c2)

Expand All @@ -22,7 +24,7 @@ def apply(c1, c2)

both = c1_map.keys & c2_map.keys
entry_differences = both.each_with_object({}) do |key, differences|
diff = c1_map[key].diff(c2_map[key])
diff = c1_map[key].diff(c2_map[key], diff_options)
differences[key] = diff if diff
end

Expand All @@ -36,9 +38,22 @@ def apply(c1, c2)
def map_for_alignment(c)
return {} unless c
c.each_with_object({}) do |element, map|
map[element.send(@alignment_attribute)] = element
value = @alignment_value_extractor.call(element.send(@alignment_attribute))
map[value] = element
end
end
private :map_for_alignment

def select_value_extractor(param)
case param
when nil
ValueCriterion::VALUE_EXTRACTORS[:identity]
when Symbol
ValueCriterion::VALUE_EXTRACTORS[param] or fail "Unknown extractor #{param.inspect}"
else
param
end
end
private :select_value_extractor
end
end
5 changes: 3 additions & 2 deletions lib/ncs_navigator/mdes/differences/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ class Entry
# @param [Object] o1 the left object
# @param [Object] o2 the right object
# @param [Hash<Symbol, #apply>] attribute_criteria a list of objects which produce difference objects
# @param [Hash] diff_options options to pass to nested calls to #diff
# @return [Entry, nil] the differences between o1 and o2 according to the
# criteria, or nil if there are no differences.
def self.compute(o1, o2, attribute_criteria)
def self.compute(o1, o2, attribute_criteria, diff_options)
differences = attribute_criteria.each_with_object({}) do |(diff_attribute, criterion), diffs|
o_attribute = (criterion.respond_to?(:attribute) && criterion.attribute) || diff_attribute
d = criterion.apply(o1.send(o_attribute), o2.send(o_attribute))
d = criterion.apply(o1.send(o_attribute), o2.send(o_attribute), diff_options)
diffs[diff_attribute] = d if d
end

Expand Down
22 changes: 19 additions & 3 deletions lib/ncs_navigator/mdes/differences/value_criterion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ class ValueCriterion
:predicate => lambda { |a, b| !(a ^ b) }
}

IDENTITY_VALUE_EXTRACTOR = lambda { |o| o }
VALUE_EXTRACTORS = {
:identity => lambda { |o| o },
:word_chars_downcase =>
lambda { |o| o ? o.downcase.gsub(/[^ \w]+/, ' ').gsub(/\s+/, ' ').strip : o }
}

attr_reader :comparator, :value_extractor

def initialize(options={})
@comparator = select_comparator(options.delete(:comparator))
@value_extractor = options.delete(:value_extractor) || IDENTITY_VALUE_EXTRACTOR
@value_extractor = select_value_extractor(options.delete(:value_extractor))
end

def apply(v1, v2)
def apply(v1, v2, diff_options)
cv1 = value_extractor.call(v1)
cv2 = value_extractor.call(v2)

Expand All @@ -38,5 +42,17 @@ def select_comparator(param)
end
end
private :select_comparator

def select_value_extractor(param)
case param
when nil
VALUE_EXTRACTORS[:identity]
when Symbol
VALUE_EXTRACTORS[param] or fail "Unknown extractor #{param.inspect}"
else
param
end
end
private :select_value_extractor
end
end
4 changes: 2 additions & 2 deletions lib/ncs_navigator/mdes/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ def inspect
:types => Differences::CollectionCriterion.new(:name)
}

def diff(other)
Differences::Entry.compute(self, other, DIFF_CRITERIA)
def diff(other, options={})
Differences::Entry.compute(self, other, DIFF_CRITERIA, options)
end
end
end
4 changes: 2 additions & 2 deletions lib/ncs_navigator/mdes/transmission_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ def operational_table?
# Computes the differences between this table and the other.
#
# @return [Differences::Entry,nil]
def diff(other_table)
Differences::Entry.compute(self, other_table, DIFF_CRITERIA)
def diff(other_table, options={})
Differences::Entry.compute(self, other_table, DIFF_CRITERIA, options)
end
end
end
58 changes: 36 additions & 22 deletions lib/ncs_navigator/mdes/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def resolve_foreign_key!(tables, override_name=nil, options={})

# @private
class EmbeddedVariableTypeCriterion
def apply(vt1, vt2)
def apply(vt1, vt2, diff_options)
cvt1 = vt1 || VariableType.new
cvt2 = vt2 || VariableType.new

Expand All @@ -214,36 +214,50 @@ def apply(vt1, vt2)
# be reported once under the specification's entry for the named type.
nil
else
cvt1.diff(cvt2)
cvt1.diff(cvt2, diff_options)
end
end
end

# @private
DIFF_CRITERIA = {
:name => Differences::ValueCriterion.new,
:pii => Differences::ValueCriterion.new,
:omittable? => Differences::ValueCriterion.new(:comparator => :predicate),
:nillable? => Differences::ValueCriterion.new(:comparator => :predicate),

:status => Differences::ValueCriterion.new(
:comparator => lambda { |left, right|
(left == :new && right == :active) || (left == right)
}
),
:table_reference => Differences::ValueCriterion.new(
:value_extractor => lambda { |o| o ? o.name : nil }
),

:type => EmbeddedVariableTypeCriterion.new
}
def diff_criteria(diff_options={})
base = {
:name => Differences::ValueCriterion.new,
:type => EmbeddedVariableTypeCriterion.new,
:pii => Differences::ValueCriterion.new,
:omittable? => Differences::ValueCriterion.new(:comparator => :predicate),
:nillable? => Differences::ValueCriterion.new(:comparator => :predicate),
:table_reference => Differences::ValueCriterion.new(
:value_extractor => lambda { |o| o ? o.name : nil }
)
}

if diff_options[:strict]
base[:status] = Differences::ValueCriterion.new
else
base[:status] = Differences::ValueCriterion.new(
:comparator => lambda { |left, right|
no_change_changes = [
[:new, :active],
[:new, :modified],
[:active, :modified],
[:modified, :active]
]

no_change_changes.include?([left, right]) || (left == right)
}
)
end

base
end
protected :diff_criteria

##
# Computes the differences between this variable and the other.
#
# @return [Differences::Entry,nil]
def diff(other_variable)
Differences::Entry.compute(self, other_variable, DIFF_CRITERIA)
def diff(other_variable, options={})
Differences::Entry.compute(self, other_variable, diff_criteria(options), options)
end
end
end
42 changes: 29 additions & 13 deletions lib/ncs_navigator/mdes/variable_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,35 @@ def inspect
"#<#{self.class} #{attrs.join(' ')}>"
end

# @private
DIFF_CRITERIA = {
:name => Differences::ValueCriterion.new,
:base_type => Differences::ValueCriterion.new,
:pattern => Differences::ValueCriterion.new,
:max_length => Differences::ValueCriterion.new,
:min_length => Differences::ValueCriterion.new,
:code_list_by_value => Differences::CollectionCriterion.new(:value, :collection => :code_list),
:code_list_by_label => Differences::CollectionCriterion.new(:label, :collection => :code_list)
}

def diff(other_type)
Differences::Entry.compute(self, other_type, DIFF_CRITERIA)
def diff_criteria(diff_options={})
base = {
:name => Differences::ValueCriterion.new,
:base_type => Differences::ValueCriterion.new,
:pattern => Differences::ValueCriterion.new,
:max_length => Differences::ValueCriterion.new,
:min_length => Differences::ValueCriterion.new,
}

if diff_options[:strict]
base.merge(
:code_list_by_value => Differences::CollectionCriterion.new(
:value, :collection => :code_list),
:code_list_by_label => Differences::CollectionCriterion.new(
:label, :collection => :code_list)
)
else
base.merge(
:code_list_by_value => Differences::CollectionCriterion.new(
:value, :collection => :code_list),
:code_list_by_label => Differences::CollectionCriterion.new(
:label, :collection => :code_list, :value_extractor => :word_chars_downcase)
)
end
end
protected :diff_criteria

def diff(other_type, options={})
Differences::Entry.compute(self, other_type, diff_criteria(options), options)
end
end
end
Loading

0 comments on commit fa27a02

Please sign in to comment.