diff --git a/lib/rubyXL/objects/ooxml_object.rb b/lib/rubyXL/objects/ooxml_object.rb
index 9a9d3e9ee..2d47cb257 100644
--- a/lib/rubyXL/objects/ooxml_object.rb
+++ b/lib/rubyXL/objects/ooxml_object.rb
@@ -18,6 +18,59 @@ def obtain_class_variable(var_name, default = {})
self.class_variable_set(var_name, default)
end
+ # This is like attr_accessor method, creating the getter and setter
+ # However, in this case the setter update the hash of the object containing it.
+ # It also trigger the parent in case of change
+ def hash_attr_accessor symbol
+ class_eval <<-RUBY, __FILE__, __LINE__
+ def #{symbol}
+ @#{symbol}
+ end
+
+ def #{symbol}= value
+ str_hash = "#{symbol}".hash
+
+ before_hash = @hash
+ @hash ^= @#{symbol}.hash * str_hash # Outmerge the hash component
+ @#{symbol} = value
+ @hash ^= value.hash * str_hash # Merge again the new hash component
+
+ @parent.hash_dirty! if @parent
+
+ value
+ end
+ RUBY
+ end
+
+ # This is like attr_accessor method, creating the getter and setter
+ # However, in this case the setter update the hash of the object containing it.
+ # It also register the parent link on setup
+ def hash_attr_accessor_child_node symbol
+ class_eval <<-RUBY, __FILE__, __LINE__
+ def #{symbol}
+ @#{symbol}
+ end
+
+ def #{symbol}= new_value
+ str_hash = "#{symbol}".hash
+
+ old_value = @#{symbol}
+
+ old_value.parent = nil if old_value.is_a?(OOXMLObjectInstanceMethods)
+ new_value.parent = self if new_value.is_a?(OOXMLObjectInstanceMethods)
+
+ before_hash = @hash
+ @hash ^= old_value.hash * str_hash # Outmerge the hash component
+ @#{symbol} = new_value
+ @hash ^= new_value.hash * str_hash # Merge again the new hash component
+
+ @parent.hash_dirty! if @parent
+
+ new_value
+ end
+ RUBY
+ end
+
# Defines an attribute of OOXML object.
# === Parameters
# * +attribute_name+ - Name of the element attribute as seen in the source XML. Can be either "String" or :Symbol
@@ -50,7 +103,7 @@ def define_attribute(attr_name, attr_type, extra_params = {})
attr_hash = extra_params.merge({ :attr_type => attr_type })
attr_hash[:accessor] ||= accessorize(attr_name)
attrs[attr_name.to_s] = attr_hash
- self.send(:attr_accessor, attr_hash[:accessor]) unless attr_hash[:computed]
+ self.send(:hash_attr_accessor, attr_hash[:accessor]) unless attr_hash[:computed]
end
# Defines a child node of OOXML object.
@@ -86,7 +139,7 @@ def define_child_node(klass, extra_params = {})
define_count_attribute if extra_params[:collection] == :with_count
- self.send(:attr_accessor, accessor)
+ self.send(:hash_attr_accessor_child_node, accessor)
end
def define_count_attribute
@@ -208,6 +261,10 @@ def process_attribute(obj, raw_value, params)
module OOXMLObjectInstanceMethods
attr_accessor :local_namespaces
+ # Accessor used to get back to the parent from this node,
+ # and dirtyize the parent in case of hash update.
+ attr_accessor :parent
+
def self.included(klass)
klass.extend RubyXL::OOXMLObjectClassMethods
end
@@ -225,6 +282,7 @@ def initialize(params = {})
}
init_child_nodes(params)
+ recompute_hash
end
def init_child_nodes(params)
@@ -246,10 +304,27 @@ def preserve_whitespace
end
private :preserve_whitespace
+ # Manually update a hashing part.
+ def hash_dirty!
+ @need_hash_recompute = true
+ end
+
+ # Recompute the hash with segment given by the childrens
+ def recompute_hash
+ @hash = 0
+ @hash = obtain_class_variable(:@@ooxml_attributes).inject(@hash) { |h, (k, v)| h ^ ( v[:accessor].hash * self.send(v[:accessor]).hash ) }
+ @hash = obtain_class_variable(:@@ooxml_child_nodes).inject(@hash) { |h, (k, v)| h ^ ( v[:accessor].hash * self.send(v[:accessor]).hash ) }
+ @need_hash_recompute = false
+ end
+
+ def hash
+ recompute_hash if @need_hash_recompute
+ @hash
+ end
+
def ==(other)
other.is_a?(self.class) &&
- obtain_class_variable(:@@ooxml_attributes).all? { |k, v| self.send(v[:accessor]) == other.send(v[:accessor]) } &&
- obtain_class_variable(:@@ooxml_child_nodes).all? { |k, v| self.send(v[:accessor]) == other.send(v[:accessor]) }
+ self.hash == other.hash
end
# Recursively write the OOXML object and all its children out as Nokogiri::XML. Immediately before the actual