# = dev-utils/debug/diff.rb
#
# _Planned_ functionality to support comparing complex objects and discovering object
# topologies. Nothing here for now.
#
=begin
# Implement Debug.diff here. Do not load this file directly.
module DevUtils::Debug
#
# Helps identify the difference between two objects when it's not immediately obvious to
# the eye.
#
# +o1+ and +o2+ are simply objects to be compared. If one or the other is a built-in type,
# like String, then it's a simple yes/no answer and this method doesn't buy you much. If
# they are both complex objects (aggregating other data, like a Struct or data object),
# then the comparison takes place with each of their composed objects, recursively.
#
# The typical usage: diff(x, y) returns an array representing the first difference
# found, like ['age: 31', 'age: 84']. This is designed to be output with +puts+
# in +irb+, so they appear one string per line.
#
# The +flags+ modify the result. Only one flag is currently observed. If it's a number,
# say 3, then the third difference, in alphabetical order, is returned. If it's
# :all, then all differences are returned in an array of arrays. If it's
# :n, then the number of differences is returned.
#
# == Examples
#
# ...
#
def diff(o1, o2, *flags)
case (flag = flags.first || 1)
when :n
op = :count
n = -1
when :all
op = :find_all
n = -1
when Numeric
op = :find_one
n = flag.to_i
n = 1 if n < 1
else
raise ArgumentError, flag.inspect
end
diff = catch(:found) do
diffs = _diff(o1, o2, '', n)
case op
when :find_one
return nil # If we get this far, no difference was found.
when :find_all
return diffs
when :count
return diffs.size
end
end
return diff # This was thrown from _diff; it's a single result.
end
private
#
# +o1+ and +o2+ are the objects being compared. +base+ is the base name for the fields so
# far (so we can report the full path of the field name, like 'person.name.first'). +n+ is
# the number of differences we should skip before immediately returning the one we're
# after.
#
# Each time we find a difference, we add it to +diffs+ and decrement n. If n is zero, we
# have found the difference we were looking for so we throw :found and the difference (a
# 2-tuple).
#
# We return an array of all the differences we have found.
#
def _diff(o1, o2, base, n)
if _immediate?(o1) or _immediate?(o2)
return "Immediate value(s)"
#return [o1,o2].map { |o| "#{field_path}: #{o.inspect}" }
else
# Both objects are complex, and we're happy.
h1 = _hash_representation(o1)
h2 = _hash_representation(o2)
fields = (h1.keys + h2.keys).uniq.sort
diffs = []
fields.each do |f|
v1 = h1[f]
v2 = h2[f]
goirb binding if $X
if v1 == v2
next
else
# We've found two unequal values. If they're immediate values,
# we count it as a difference. If they're complex, then we
# recurse into them.
field_path = [base, f].reject { |s| s.empty? }.join('.')
if _immediate?(v1) or _immediate?(v2)
diff = [v1,v2].map { |v| "#{field_path}: #{v.inspect}" }
throw :found, diff if (n -= 1).zero?
diffs << diff
else
diffs.concat _diff(v1, v2, field_path, n)
end
end
end
return diffs
end
end
#
# Returns true iff the given object is an "immediate value". That means a fundamenal Ruby
# data type. It's an arbitrary definition, and the choices are hardcoded. Hopefully I can
# think of a better heuristic.
#
def _immediate?(object)
[Numeric, String, Range, Array, Hash, NilClass, TrueClass,
FalseClass].any? { |klass| klass === object }
end
#
# Returns a hash representation of an object, mapping field names to values.
#
def _hash_representation(object)
_methods = object.public_methods(false)
if Struct === object
# In a Struct, if X is an attribute, then there are methods X and X=.
fields = _methods.grep(/=/).map { |meth| meth.to_s.tr('=', '') }
fields = fields.select { |meth| _methods.include? meth }
else
# In a general complex oject, if X in an attribute, then there is a method X and an
# instance variable @X.
_variables = object.instance_variables.map { |v| v.tr('@', '') }
fields = _variables.select { |v| _methods.include? v }
end
# Now we've got the fields, which in all cases are public methods. So we call them to
# get the values and construct our hash.
result = {}
fields.each do |field|
result[field] = object.send(field)
end
result
end
#
# Implementation notes for diff.
#
# Approach:
# * we don't dig into arrays or hashes, etc.; those are "immediate" values along
# with Numeric, String, Range, etc.
# * if object under inspection is immediate, we compare with 'equal?' and that's it
# * OK, so we've got two complex objects
# * get hash representations, one level deep: field => value
# * there is special logic for Structs
# * if field sets are different, do we care?
# * we can just report that field F is X in o1 but is nil in o2
# * examine fields in alphabetical order
# * compare values (with #equal?)
# * if equal, go to next field
# * if not equal
# * if value(s) are immediate, compare and report (or skip, or count,
# according to flags)
# * otherwise, dig into those values (recurse)
#
# Alternative notes for diff.
#
# Approach:
# * have a separate method 'structure(obj)', which returns the structure of the
# given object; e.g. structure(person) -> ['name', 'age', 'address.number',
# 'address.road', 'address.city']
# * that 'structure' method would be useful for some other things
# * you can yield each bit as it's done
# * use 'structure' and 'eval' to get the value of things
# * be smart about nil values; e.g. person.address could be nil, which
# would represent a different, but compatible, structure
#
# This seems simpler than mixing it all up as in the implementation above.
end # module Kernel
class Object
def topology
end
end
=end