summaryrefslogtreecommitdiff
diff options
-rw-r--r--.gitignore15
-rw-r--r--.rspec1
-rw-r--r--.travis.yml7
-rw-r--r--.yardopts1
-rw-r--r--Gemfile6
-rw-r--r--LICENSE19
-rw-r--r--README.md207
-rw-r--r--Rakefile13
-rw-r--r--changelog.md44
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control25
-rw-r--r--debian/copyright31
-rw-r--r--debian/patches/0001-Port-tests-to-rspec-3.patch89
-rw-r--r--debian/patches/series1
-rw-r--r--debian/ruby-hashdiff.docs1
-rw-r--r--debian/ruby-tests.rake5
-rwxr-xr-xdebian/rules6
-rw-r--r--debian/source/format1
-rw-r--r--debian/watch2
-rw-r--r--hashdiff.gemspec25
-rw-r--r--lib/hashdiff.rb5
-rw-r--r--lib/hashdiff/diff.rb220
-rw-r--r--lib/hashdiff/lcs.rb69
-rw-r--r--lib/hashdiff/patch.rb84
-rw-r--r--lib/hashdiff/util.rb127
-rw-r--r--lib/hashdiff/version.rb3
-rw-r--r--metadata.yml110
-rw-r--r--spec/hashdiff/best_diff_spec.rb65
-rw-r--r--spec/hashdiff/diff_array_spec.rb60
-rw-r--r--spec/hashdiff/diff_spec.rb232
-rw-r--r--spec/hashdiff/lcs_spec.rb75
-rw-r--r--spec/hashdiff/patch_spec.rb136
-rw-r--r--spec/hashdiff/util_spec.rb73
-rw-r--r--spec/spec_helper.rb13
35 files changed, 1777 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..134a870
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile ~/.gitignore_global
+
+# Ignore bundler config
+/.bundle
+/doc
+/.yardoc
+/Gemfile.lock
+
+*.swp
+*.bak
+*.gem
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..4e1e0d2
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c931dfd
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: ruby
+rvm:
+ - 1.8.7
+ - 1.9.3
+ - 2.0.0
+ - 2.1.1
+script: "bundle exec rake spec"
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000..22ce944
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1 @@
+--no-private
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..3506a0e
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,6 @@
+source "http://rubygems.org"
+gemspec
+
+group :test do
+ gem 'rake'
+end
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9adfb9d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Liu Fengyun
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9b94662
--- /dev/null
+++ b/README.md
@@ -0,0 +1,207 @@
+# HashDiff [![Build Status](https://secure.travis-ci.org/liufengyun/hashdiff.png)](http://travis-ci.org/liufengyun/hashdiff) [![Gem Version](https://badge.fury.io/rb/hashdiff.png)](http://badge.fury.io/rb/hashdiff)
+
+HashDiff is a ruby library to compute the smallest difference between two hashes.
+
+**Docs**: [Documentation](http://rubydoc.info/gems/hashdiff)
+
+## Why HashDiff?
+
+Given two Hashes A and B, sometimes you face the question: what's the smallest modification that can be made to change A into B?
+
+An algorithm that responds to this question has to do following:
+
+* Generate a list of additions, deletions and changes, so that `A + ChangeSet = B` and `B - ChangeSet = A`.
+* Compute recursively -- Arrays and Hashes may be nested arbitrarily in A or B.
+* Compute the smallest change -- it should recognize similar child Hashes or child Arrays between A and B.
+
+HashDiff answers the question above using an opinionated approach:
+
+* Hash can be represented as a list of (dot-syntax-path, value) pairs. For example, `{a:[{c:2}]}` can be represented as `["a[0].c", 2]`.
+* The change set can be represented using the dot-syntax representation. For example, `[['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]`.
+* It compares Arrays using the [LCS(longest common subsequence)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithm.
+* It recognizes similar Hashes in an Array using a similarity value (0 < similarity <= 1).
+
+## Usage
+
+To use the gem, add the following to your Gemfile:
+
+```ruby
+gem 'hashdiff'
+```
+
+## Quick Start
+
+### Diff
+
+Two simple hashes:
+
+```ruby
+a = {a:3, b:2}
+b = {}
+
+diff = HashDiff.diff(a, b)
+diff.should == [['-', 'a', 3], ['-', 'b', 2]]
+```
+
+More complex hashes:
+
+```ruby
+a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
+b = {a:{y:3}, b:{y:3, z:30}}
+
+diff = HashDiff.diff(a, b)
+diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
+```
+
+Arrays in hashes:
+
+```ruby
+a = {a:[{x:2, y:3, z:4}, {x:11, y:22, z:33}], b:{x:3, z:45}}
+b = {a:[{y:3}, {x:11, z:33}], b:{y:22}}
+
+diff = HashDiff.best_diff(a, b)
+diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
+```
+
+### Patch
+
+patch example:
+
+```ruby
+a = {a: 3}
+b = {a: {a1: 1, a2: 2}}
+
+diff = HashDiff.diff(a, b)
+HashDiff.patch!(a, diff).should == b
+```
+
+unpatch example:
+
+```ruby
+a = [{a: 1, b: 2, c: 3, d: 4, e: 5}, {x: 5, y: 6, z: 3}, 1]
+b = [1, {a: 1, b: 2, c: 3, e: 5}]
+
+diff = HashDiff.diff(a, b) # diff two array is OK
+HashDiff.unpatch!(b, diff).should == a
+```
+
+### Options
+
+There are five options available: `:delimiter`, `:similarity`, `:strict`, `:numeric_tolerance` and `:strip`.
+
+#### `:delimiter`
+
+You can specify `:delimiter` to be something other than the default dot. For example:
+
+```ruby
+a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
+b = {a:{y:3}, b:{y:3, z:30}}
+
+diff = HashDiff.diff(a, b, :delimiter => '\t')
+diff.should == [['-', 'a\tx', 2], ['-', 'a\tz', 4], ['-', 'b\tx', 3], ['~', 'b\tz', 45, 30], ['+', 'b\ty', 3]]
+```
+
+#### `:similarity`
+
+In cases where you have similar hash objects in arrays, you can pass a custom value for `:similarity` instead of the default `0.8`. This is interpreted as a ratio of similarity (default is 80% similar, whereas `:similarity => 0.5` would look for at least a 50% similarity).
+
+#### `:strict`
+
+The `:strict` option, which defaults to `true`, specifies whether numeric types are compared on type as well as value. By default, a Fixnum will never be equal to a Float (e.g. 4 != 4.0). Setting `:strict` to false makes the comparison looser (e.g. 4 == 4.0).
+
+#### `:numeric_tolerance`
+
+The :numeric_tolerance option allows for a small numeric tolerance.
+
+```ruby
+a = {x:5, y:3.75, z:7}
+b = {x:6, y:3.76, z:7}
+
+diff = HashDiff.diff(a, b, :numeric_tolerance => 0.1)
+diff.should == [["~", "x", 5, 6]]
+```
+
+#### `:strip`
+
+The :strip option strips all strings before comparing.
+
+```ruby
+a = {x:5, s:'foo '}
+b = {x:6, s:'foo'}
+
+diff = HashDiff.diff(a, b, :comparison => { :numeric_tolerance => 0.1, :strip => true })
+diff.should == [["~", "x", 5, 6]]
+```
+
+#### Specifying a custom comparison method
+
+It's possible to specify how the values of a key should be compared.
+
+```ruby
+a = {a:'car', b:'boat', c:'plane'}
+b = {a:'bus', b:'truck', c:' plan'}
+
+diff = HashDiff.diff(a, b) do |path, obj1, obj2|
+ case path
+ when /a|b|c/
+ obj1.length == obj2.length
+ end
+end
+
+diff.should == [['~', 'b', 'boat', 'truck']]
+```
+
+The yielded params of the comparison block is `|path, obj1, obj2|`, in which path is the key (or delimited compound key) to the value being compared. When comparing elements in array, the path is with the format `array[*]`. For example:
+
+```ruby
+a = {a:'car', b:['boat', 'plane'] }
+b = {a:'bus', b:['truck', ' plan'] }
+
+diff = HashDiff.diff(a, b) do |path, obj1, obj2|
+ case path
+ when 'b[*]'
+ obj1.length == obj2.length
+ end
+end
+
+diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-", "b[0]", "boat"], ["+", "b[0]", "truck"]]
+```
+
+When a comparison block is given, it'll be given priority over other specified options. If the block returns value other than `true` or `false`, then the two values will be compared with other specified options.
+
+#### Sorting arrays before comparison
+
+An order difference alone between two arrays can create too many diffs to be useful. Consider sorting them prior to diffing.
+
+```ruby
+a = {a:'car', b:['boat', 'plane'] }
+b = {a:'car', b:['plane', 'boat'] }
+
+HashDiff.diff(a, b) => [["+", "b[0]", "plane"], ["-", "b[2]", "plane"]]
+
+b[:b].sort!
+
+HashDiff.diff(a, b) => []
+```
+
+### Special use cases
+
+#### Using HashDiff on JSON API results
+
+```ruby
+require 'uri'
+require 'net/http'
+require 'json'
+
+uri = URI('http://time.jsontest.com/')
+json_resp = ->(uri) { JSON.parse(Net::HTTP.get_response(uri).body) }
+a = json_resp.call(uri)
+b = json_resp.call(uri)
+
+HashDiff.diff(a,b) => [["~", "milliseconds_since_epoch", 1410542545874, 1410542545985]]
+```
+
+## License
+
+HashDiff is distributed under the MIT-LICENSE.
+
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..66fe6ac
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,13 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core/rake_task'
+
+task :default => :spec
+
+RSpec::Core::RakeTask.new(:spec) do |spec|
+ spec.pattern = "./spec/**/*_spec.rb"
+end
+
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..a370e35
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,44 @@
+# Change Log
+
+## v0.2.3 2015-11-5
+
+* improve performance of LCS algorithm #12
+
+## v0.2.2 2014-10-6
+
+* make library 1.8.7 compatible
+
+## v0.2.1 2014-7-13
+
+* yield added/deleted keys for custom comparison
+
+## v0.2.0 2014-3-29
+
+* support custom comparison blocks
+* support `:strip`, `:numeric_tolerance` and `:strict` options
+
+## v0.1.0 2013-8-25
+
+* use options for parameters `:delimiter` and `:similarity` in interfaces
+
+## v0.0.6 2013-3-2
+
+* Add parameter for custom property-path delimiter.
+
+## v0.0.5 2012-7-1
+
+* fix a bug in judging whehter two objects are similiar.
+* add more spec test for HashDiff.best_diff
+
+## v0.0.4 2012-6-24
+
+Main changes in this version is to output the whole object in addition & deletion, instead of recursely add/deletes the object.
+
+For example, `diff({a:2, c:[4, 5]}, {a:2}) will generate following output:
+
+ [['-', 'c', [4, 5]]]
+
+instead of following:
+
+ [['-', 'c[0]', 4], ['-', 'c[1]', 5], ['-', 'c', []]]
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..04954fa
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+ruby-hashdiff (0.2.3-1) unstable; urgency=medium
+
+ * Initial release
+
+ -- Antonio Terceiro <terceiro@debian.org> Fri, 22 Jan 2016 12:12:33 -0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..0a19e4f
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,25 @@
+Source: ruby-hashdiff
+Section: ruby
+Priority: optional
+Maintainer: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers@lists.alioth.debian.org>
+Uploaders: Antonio Terceiro <terceiro@debian.org>
+Build-Depends: debhelper (>= 9~),
+ gem2deb,
+ rake,
+ ruby-rspec
+Standards-Version: 3.9.6
+Vcs-Git: https://anonscm.debian.org/git/pkg-ruby-extras/ruby-hashdiff.git
+Vcs-Browser: https://anonscm.debian.org/cgit/pkg-ruby-extras/ruby-hashdiff.git
+Homepage: https://github.com/liufengyun/hashdiff
+Testsuite: autopkgtest-pkg-ruby
+XS-Ruby-Versions: all
+
+Package: ruby-hashdiff
+Architecture: all
+XB-Ruby-Versions: ${ruby:Versions}
+Depends: ruby | ruby-interpreter,
+ ${misc:Depends},
+ ${shlibs:Depends}
+Description: library for computing the smallest difference between two hashes
+ Given two Hashes A and B, HashDiff will calculate the smallest modification
+ that can be made to change A into B.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..08eef59
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,31 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: hashdiff
+Source: https://github.com/liufengyun/hashdiff
+
+Files: *
+Copyright: (c) 2012 Liu Fengyun
+License: Expat
+
+Files: debian/*
+Copyright: 2016 Antonio Terceiro <terceiro@debian.org>
+License: Expat
+Comment: the Debian packaging is licensed under the same terms as the original package.
+
+License: Expat
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
diff --git a/debian/patches/0001-Port-tests-to-rspec-3.patch b/debian/patches/0001-Port-tests-to-rspec-3.patch
new file mode 100644
index 0000000..fb18633
--- /dev/null
+++ b/debian/patches/0001-Port-tests-to-rspec-3.patch
@@ -0,0 +1,89 @@
+From: Antonio Terceiro <terceiro@debian.org>
+Date: Fri, 22 Jan 2016 12:14:40 -0200
+Subject: Port tests to rspec 3
+
+---
+ spec/hashdiff/util_spec.rb | 34 +++++++++++++++++-----------------
+ 1 file changed, 17 insertions(+), 17 deletions(-)
+
+diff --git a/spec/hashdiff/util_spec.rb b/spec/hashdiff/util_spec.rb
+index bce861c..9130686 100644
+--- a/spec/hashdiff/util_spec.rb
++++ b/spec/hashdiff/util_spec.rb
+@@ -14,59 +14,59 @@ describe HashDiff do
+ it "should be able to tell similiar hash" do
+ a = {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}
+ b = {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}
+- HashDiff.similar?(a, b).should be_true
+- HashDiff.similar?(a, b, :similarity => 1).should be_false
++ HashDiff.similar?(a, b).should be true
++ HashDiff.similar?(a, b, :similarity => 1).should be false
+ end
+
+ it "should be able to tell similiar hash with values within tolerance" do
+ a = {'a' => 1.5, 'b' => 2.25, 'c' => 3, 'd' => 4, 'e' => 5}
+ b = {'a' => 1.503, 'b' => 2.22, 'c' => 3, 'e' => 5}
+- HashDiff.similar?(a, b, :numeric_tolerance => 0.05).should be_true
+- HashDiff.similar?(a, b).should be_false
++ HashDiff.similar?(a, b, :numeric_tolerance => 0.05).should be true
++ HashDiff.similar?(a, b).should be false
+ end
+
+ it "should be able to tell numbers and strings" do
+- HashDiff.similar?(1, 2).should be_false
+- HashDiff.similar?("a", "b").should be_false
+- HashDiff.similar?("a", [1, 2, 3]).should be_false
+- HashDiff.similar?(1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}).should be_false
++ HashDiff.similar?(1, 2).should be false
++ HashDiff.similar?("a", "b").should be false
++ HashDiff.similar?("a", [1, 2, 3]).should be false
++ HashDiff.similar?(1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}).should be false
+ end
+
+ it "should be able to tell true when similarity == 0.5" do
+ a = {"value" => "New1", "onclick" => "CreateNewDoc()"}
+ b = {"value" => "New", "onclick" => "CreateNewDoc()"}
+
+- HashDiff.similar?(a, b, :similarity => 0.5).should be_true
++ HashDiff.similar?(a, b, :similarity => 0.5).should be true
+ end
+
+ it "should be able to tell false when similarity == 0.5" do
+ a = {"value" => "New1", "onclick" => "open()"}
+ b = {"value" => "New", "onclick" => "CreateNewDoc()"}
+
+- HashDiff.similar?(a, b, :similarity => 0.5).should be_false
++ HashDiff.similar?(a, b, :similarity => 0.5).should be false
+ end
+
+ describe '.compare_values' do
+ it "should compare numeric values exactly when no tolerance" do
+- expect(HashDiff.compare_values(10.004, 10.003)).to be_false
++ expect(HashDiff.compare_values(10.004, 10.003)).to be false
+ end
+
+ it "should allow tolerance with numeric values" do
+- expect(HashDiff.compare_values(10.004, 10.003, :numeric_tolerance => 0.01)).to be_true
++ expect(HashDiff.compare_values(10.004, 10.003, :numeric_tolerance => 0.01)).to be true
+ end
+
+ it "should compare other objects with or without tolerance" do
+- expect(HashDiff.compare_values('hats', 'ninjas')).to be_false
+- expect(HashDiff.compare_values('hats', 'ninjas', :numeric_tolerance => 0.01)).to be_false
+- expect(HashDiff.compare_values('horse', 'horse')).to be_true
++ expect(HashDiff.compare_values('hats', 'ninjas')).to be false
++ expect(HashDiff.compare_values('hats', 'ninjas', :numeric_tolerance => 0.01)).to be false
++ expect(HashDiff.compare_values('horse', 'horse')).to be true
+ end
+
+ it 'should compare strings exactly by default' do
+- expect(HashDiff.compare_values(' horse', 'horse')).to be_false
++ expect(HashDiff.compare_values(' horse', 'horse')).to be false
+ end
+
+ it 'should strip strings before comparing when requested' do
+- expect(HashDiff.compare_values(' horse', 'horse', :strip => true)).to be_true
++ expect(HashDiff.compare_values(' horse', 'horse', :strip => true)).to be true
+ end
+ end
+ end
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..13e8bcc
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1 @@
+0001-Port-tests-to-rspec-3.patch
diff --git a/debian/ruby-hashdiff.docs b/debian/ruby-hashdiff.docs
new file mode 100644
index 0000000..b43bf86
--- /dev/null
+++ b/debian/ruby-hashdiff.docs
@@ -0,0 +1 @@
+README.md
diff --git a/debian/ruby-tests.rake b/debian/ruby-tests.rake
new file mode 100644
index 0000000..cf1591e
--- /dev/null
+++ b/debian/ruby-tests.rake
@@ -0,0 +1,5 @@
+require 'gem2deb/rake/spectask'
+
+Gem2Deb::Rake::RSpecTask.new do |spec|
+ spec.pattern = './spec/**/*_spec.rb'
+end
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..3454d59
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,6 @@
+#!/usr/bin/make -f
+
+export GEM2DEB_TEST_RUNNER = --check-dependencies
+
+%:
+ dh $@ --buildsystem=ruby --with ruby
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..5d70f52
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,2 @@
+version=3
+http://pkg-ruby-extras.alioth.debian.org/cgi-bin/gemwatch/hashdiff .*/hashdiff-(.*).tar.gz
diff --git a/hashdiff.gemspec b/hashdiff.gemspec
new file mode 100644
index 0000000..251b504
--- /dev/null
+++ b/hashdiff.gemspec
@@ -0,0 +1,25 @@
+$LOAD_PATH << File.expand_path("../lib", __FILE__)
+require 'hashdiff/version'
+
+Gem::Specification.new do |s|
+ s.name = %q{hashdiff}
+ s.version = HashDiff::VERSION
+ s.license = 'MIT'
+ s.summary = %q{ HashDiff is a diff lib to compute the smallest difference between two hashes. }
+ s.description = %q{ HashDiff is a diff lib to compute the smallest difference between two hashes. }
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- Appraisals {spec}/*`.split("\n")
+
+ s.require_paths = ['lib']
+ s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
+
+ s.authors = ["Liu Fengyun"]
+ s.email = ["liufengyunchina@gmail.com"]
+
+ s.homepage = "https://github.com/liufengyun/hashdiff"
+
+ s.add_development_dependency("rspec", "~> 2.0")
+ s.add_development_dependency("yard")
+ s.add_development_dependency("bluecloth")
+end
diff --git a/lib/hashdiff.rb b/lib/hashdiff.rb
new file mode 100644
index 0000000..08e4936
--- /dev/null
+++ b/lib/hashdiff.rb
@@ -0,0 +1,5 @@
+require 'hashdiff/util'
+require 'hashdiff/lcs'
+require 'hashdiff/diff'
+require 'hashdiff/patch'
+require 'hashdiff/version'
diff --git a/lib/hashdiff/diff.rb b/lib/hashdiff/diff.rb
new file mode 100644
index 0000000..9f2a0c9
--- /dev/null
+++ b/lib/hashdiff/diff.rb
@@ -0,0 +1,220 @@
+module HashDiff
+
+ # Best diff two objects, which tries to generate the smallest change set using different similarity values.
+ #
+ # HashDiff.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
+ #
+ # @param [Array, Hash] obj1
+ # @param [Array, Hash] obj2
+ # @param [Hash] options the options to use when comparing
+ # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Fixnum, Float, BigDecimal to each other
+ # * :delimiter (String) ['.'] the delimiter used when returning nested key references
+ # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value.
+ # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
+ #
+ # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison.
+ #
+ # @return [Array] an array of changes.
+ # e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
+ #
+ # @example
+ # a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
+ # b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
+ # diff = HashDiff.best_diff(a, b)
+ # diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
+ #
+ # @since 0.0.1
+ def self.best_diff(obj1, obj2, options = {}, &block)
+ options[:comparison] = block if block_given?
+
+ opts = { :similarity => 0.3 }.merge!(options)
+ diffs_1 = diff(obj1, obj2, opts)
+ count_1 = count_diff diffs_1
+
+ opts = { :similarity => 0.5 }.merge!(options)
+ diffs_2 = diff(obj1, obj2, opts)
+ count_2 = count_diff diffs_2
+
+ opts = { :similarity => 0.8 }.merge!(options)
+ diffs_3 = diff(obj1, obj2, opts)
+ count_3 = count_diff diffs_3
+
+ count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2]
+ diffs = count < count_3 ? diffs : diffs_3
+ end
+
+ # Compute the diff of two hashes or arrays
+ #
+ # @param [Array, Hash] obj1
+ # @param [Array, Hash] obj2
+ # @param [Hash] options the options to use when comparing
+ # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Fixnum, Float, BigDecimal to each other
+ # * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}.
+ # * :delimiter (String) ['.'] the delimiter used when returning nested key references
+ # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value.
+ # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
+ #
+ # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison.
+ #
+ # @return [Array] an array of changes.
+ # e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
+ #
+ # @example
+ # a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ # b = {"a" => 1, "b" => {}}
+ #
+ # diff = HashDiff.diff(a, b)
+ # diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
+ #
+ # @since 0.0.1
+ def self.diff(obj1, obj2, options = {}, &block)
+ opts = {
+ :prefix => '',
+ :similarity => 0.8,
+ :delimiter => '.',
+ :strict => true,
+ :strip => false,
+ :numeric_tolerance => 0
+ }.merge!(options)
+
+ opts[:comparison] = block if block_given?
+
+ # prefer to compare with provided block
+ result = custom_compare(opts[:comparison], opts[:prefix], obj1, obj2)
+ return result if result
+
+ if obj1.nil? and obj2.nil?
+ return []
+ end
+
+ if obj1.nil?
+ return [['~', opts[:prefix], nil, obj2]]
+ end
+
+ if obj2.nil?
+ return [['~', opts[:prefix], obj1, nil]]
+ end
+
+ unless comparable?(obj1, obj2, opts[:strict])
+ return [['~', opts[:prefix], obj1, obj2]]
+ end
+
+ result = []
+ if obj1.is_a?(Array)
+ changeset = diff_array(obj1, obj2, opts) do |lcs|
+ # use a's index for similarity
+ lcs.each do |pair|
+ result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => "#{opts[:prefix]}[#{pair[0]}]")))
+ end
+ end
+
+ changeset.each do |change|
+ if change[0] == '-'
+ result << ['-', "#{opts[:prefix]}[#{change[1]}]", change[2]]
+ elsif change[0] == '+'
+ result << ['+', "#{opts[:prefix]}[#{change[1]}]", change[2]]
+ end
+ end
+ elsif obj1.is_a?(Hash)
+ if opts[:prefix].empty?
+ prefix = ""
+ else
+ prefix = "#{opts[:prefix]}#{opts[:delimiter]}"
+ end
+
+ deleted_keys = obj1.keys - obj2.keys
+ common_keys = obj1.keys & obj2.keys
+ added_keys = obj2.keys - obj1.keys
+
+ # add deleted properties
+ deleted_keys.sort.each do |k|
+ custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", obj1[k], nil)
+
+ if custom_result
+ result.concat(custom_result)
+ else
+ result << ['-', "#{prefix}#{k}", obj1[k]]
+ end
+ end
+
+ # recursive comparison for common keys
+ common_keys.sort.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{k}"))) }
+
+ # added properties
+ added_keys.sort.each do |k|
+ unless obj1.key?(k)
+ custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil, obj2[k])
+
+ if custom_result
+ result.concat(custom_result)
+ else
+ result << ['+', "#{prefix}#{k}", obj2[k]]
+ end
+ end
+ end
+ else
+ return [] if compare_values(obj1, obj2, opts)
+ return [['~', opts[:prefix], obj1, obj2]]
+ end
+
+ result
+ end
+
+ # @private
+ #
+ # diff array using LCS algorithm
+ def self.diff_array(a, b, options = {})
+ opts = {
+ :prefix => '',
+ :similarity => 0.8,
+ :delimiter => '.'
+ }.merge!(options)
+
+ change_set = []
+ if a.size == 0 and b.size == 0
+ return []
+ elsif a.size == 0
+ b.each_index do |index|
+ change_set << ['+', index, b[index]]
+ end
+ return change_set
+ elsif b.size == 0
+ a.each_index do |index|
+ i = a.size - index - 1
+ change_set << ['-', i, a[i]]
+ end
+ return change_set
+ end
+
+ links = lcs(a, b, opts)
+
+ # yield common
+ yield links if block_given?
+
+ # padding the end
+ links << [a.size, b.size]
+
+ last_x = -1
+ last_y = -1
+ links.each do |pair|
+ x, y = pair
+
+ # remove from a, beginning from the end
+ (x > last_x + 1) and (x - last_x - 2).downto(0).each do |i|
+ change_set << ['-', last_y + i + 1, a[i + last_x + 1]]
+ end
+
+ # add from b, beginning from the head
+ (y > last_y + 1) and 0.upto(y - last_y - 2).each do |i|
+ change_set << ['+', last_y + i + 1, b[i + last_y + 1]]
+ end
+
+ # update flags
+ last_x = x
+ last_y = y
+ end
+
+ change_set
+ end
+
+end
diff --git a/lib/hashdiff/lcs.rb b/lib/hashdiff/lcs.rb
new file mode 100644
index 0000000..1cc27d5
--- /dev/null
+++ b/lib/hashdiff/lcs.rb
@@ -0,0 +1,69 @@
+module HashDiff
+ # @private
+ #
+ # caculate array difference using LCS algorithm
+ # http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
+ def self.lcs(a, b, options = {})
+ opts = { :similarity => 0.8 }.merge!(options)
+
+ opts[:prefix] = "#{opts[:prefix]}[*]"
+
+ return [] if a.size == 0 or b.size == 0
+
+ a_start = b_start = 0
+ a_finish = a.size - 1
+ b_finish = b.size - 1
+ vector = []
+
+ lcs = []
+ (b_start..b_finish).each do |bi|
+ lcs[bi] = []
+ (a_start..a_finish).each do |ai|
+ if similar?(a[ai], b[bi], opts)
+ topleft = (ai > 0 and bi > 0)? lcs[bi-1][ai-1][1] : 0
+ lcs[bi][ai] = [:topleft, topleft + 1]
+ elsif
+ top = (bi > 0)? lcs[bi-1][ai][1] : 0
+ left = (ai > 0)? lcs[bi][ai-1][1] : 0
+ count = (top > left) ? top : left
+
+ direction = :both
+ if top > left
+ direction = :top
+ elsif top < left
+ direction = :left
+ else
+ if bi == 0
+ direction = :top
+ elsif ai == 0
+ direction = :left
+ else
+ direction = :both
+ end
+ end
+
+ lcs[bi][ai] = [direction, count]
+ end
+ end
+ end
+
+ x = a_finish
+ y = b_finish
+ while x >= 0 and y >= 0 and lcs[y][x][1] > 0
+ if lcs[y][x][0] == :both
+ x -= 1
+ elsif lcs[y][x][0] == :topleft
+ vector.insert(0, [x, y])
+ x -= 1
+ y -= 1
+ elsif lcs[y][x][0] == :top
+ y -= 1
+ elsif lcs[y][x][0] == :left
+ x -= 1
+ end
+ end
+
+ vector
+ end
+
+end
diff --git a/lib/hashdiff/patch.rb b/lib/hashdiff/patch.rb
new file mode 100644
index 0000000..3c184c4
--- /dev/null
+++ b/lib/hashdiff/patch.rb
@@ -0,0 +1,84 @@
+#
+# This module provides methods to diff two hash, patch and unpatch hash
+#
+module HashDiff
+
+ # Apply patch to object
+ #
+ # @param [Hash, Array] obj the object to be patched, can be an Array or a Hash
+ # @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
+ # @param [Hash] options supports following keys:
+ # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
+ #
+ # @return the object after patch
+ #
+ # @since 0.0.1
+ def self.patch!(obj, changes, options = {})
+ delimiter = options[:delimiter] || '.'
+
+ changes.each do |change|
+ parts = decode_property_path(change[1], delimiter)
+ last_part = parts.last
+
+ parent_node = node(obj, parts[0, parts.size-1])
+
+ if change[0] == '+'
+ if last_part.is_a?(Fixnum)
+ parent_node.insert(last_part, change[2])
+ else
+ parent_node[last_part] = change[2]
+ end
+ elsif change[0] == '-'
+ if last_part.is_a?(Fixnum)
+ parent_node.delete_at(last_part)
+ else
+ parent_node.delete(last_part)
+ end
+ elsif change[0] == '~'
+ parent_node[last_part] = change[3]
+ end
+ end
+
+ obj
+ end
+
+ # Unpatch an object
+ #
+ # @param [Hash, Array] obj the object to be unpatched, can be an Array or a Hash
+ # @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
+ # @param [Hash] options supports following keys:
+ # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
+ #
+ # @return the object after unpatch
+ #
+ # @since 0.0.1
+ def self.unpatch!(obj, changes, options = {})
+ delimiter = options[:delimiter] || '.'
+
+ changes.reverse_each do |change|
+ parts = decode_property_path(change[1], delimiter)
+ last_part = parts.last
+
+ parent_node = node(obj, parts[0, parts.size-1])
+
+ if change[0] == '+'
+ if last_part.is_a?(Fixnum)
+ parent_node.delete_at(last_part)
+ else
+ parent_node.delete(last_part)
+ end
+ elsif change[0] == '-'
+ if last_part.is_a?(Fixnum)
+ parent_node.insert(last_part, change[2])
+ else
+ parent_node[last_part] = change[2]
+ end
+ elsif change[0] == '~'
+ parent_node[last_part] = change[2]
+ end
+ end
+
+ obj
+ end
+
+end
diff --git a/lib/hashdiff/util.rb b/lib/hashdiff/util.rb
new file mode 100644
index 0000000..862d39a
--- /dev/null
+++ b/lib/hashdiff/util.rb
@@ -0,0 +1,127 @@
+module HashDiff
+
+ # @private
+ #
+ # judge whether two objects are similar
+ def self.similar?(a, b, options = {})
+ opts = { :similarity => 0.8 }.merge(options)
+
+ count_a = count_nodes(a)
+ count_b = count_nodes(b)
+ diffs = count_diff diff(a, b, opts)
+
+ if count_a + count_b == 0
+ return true
+ else
+ (1 - diffs.to_f/(count_a + count_b).to_f) >= opts[:similarity]
+ end
+ end
+
+ # @private
+ #
+ # count node differences
+ def self.count_diff(diffs)
+ diffs.inject(0) do |sum, item|
+ old_change_count = count_nodes(item[2])
+ new_change_count = count_nodes(item[3])
+ sum += (old_change_count + new_change_count)
+ end
+ end
+
+ # @private
+ #
+ # count total nodes for an object
+ def self.count_nodes(obj)
+ return 0 unless obj
+
+ count = 0
+ if obj.is_a?(Array)
+ obj.each {|e| count += count_nodes(e) }
+ elsif obj.is_a?(Hash)
+ obj.each {|k, v| count += count_nodes(v) }
+ else
+ return 1
+ end
+
+ count
+ end
+
+ # @private
+ #
+ # decode property path into an array
+ # @param [String] path Property-string
+ # @param [String] delimiter Property-string delimiter
+ #
+ # e.g. "a.b[3].c" => ['a', 'b', 3, 'c']
+ def self.decode_property_path(path, delimiter='.')
+ parts = path.split(delimiter).collect do |part|
+ if part =~ /^(\w*)\[(\d+)\]$/
+ if $1.size > 0
+ [$1, $2.to_i]
+ else
+ $2.to_i
+ end
+ else
+ part
+ end
+ end
+
+ parts.flatten
+ end
+
+ # @private
+ #
+ # get the node of hash by given path parts
+ def self.node(hash, parts)
+ temp = hash
+ parts.each do |part|
+ temp = temp[part]
+ end
+ temp
+ end
+
+ # @private
+ #
+ # check for equality or "closeness" within given tolerance
+ def self.compare_values(obj1, obj2, options = {})
+ if (options[:numeric_tolerance].is_a? Numeric) &&
+ [obj1, obj2].all? { |v| v.is_a? Numeric }
+ return (obj1 - obj2).abs <= options[:numeric_tolerance]
+ end
+
+ if options[:strip] == true
+ first = obj1.strip if obj1.respond_to?(:strip)
+ second = obj2.strip if obj2.respond_to?(:strip)
+ return first == second
+ end
+
+ obj1 == obj2
+ end
+
+ # @private
+ #
+ # check if objects are comparable
+ def self.comparable?(obj1, obj2, strict = true)
+ [Array, Hash].each do |type|
+ return true if obj1.is_a?(type) && obj2.is_a?(type)
+ end
+ return true if !strict && obj1.is_a?(Numeric) && obj2.is_a?(Numeric)
+ obj1.is_a?(obj2.class) && obj2.is_a?(obj1.class)
+ end
+
+ # @private
+ #
+ # try custom comparison
+ def self.custom_compare(method, key, obj1, obj2)
+ if method
+ res = method.call(key, obj1, obj2)
+
+ # nil != false here
+ if res == false
+ return [['~', key, obj1, obj2]]
+ elsif res == true
+ return []
+ end
+ end
+ end
+end
diff --git a/lib/hashdiff/version.rb b/lib/hashdiff/version.rb
new file mode 100644
index 0000000..452c3f3
--- /dev/null
+++ b/lib/hashdiff/version.rb
@@ -0,0 +1,3 @@
+module HashDiff
+ VERSION = '0.2.3'
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..9f51629
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,110 @@
+--- !ruby/object:Gem::Specification
+name: hashdiff
+version: !ruby/object:Gem::Version
+ version: 0.2.3
+platform: ruby
+authors:
+- Liu Fengyun
+autorequire:
+bindir: bin
+cert_chain: []
+date: 2015-11-05 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: rspec
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - ~>
+ - !ruby/object:Gem::Version
+ version: '2.0'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ~>
+ - !ruby/object:Gem::Version
+ version: '2.0'
+- !ruby/object:Gem::Dependency
+ name: yard
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+- !ruby/object:Gem::Dependency
+ name: bluecloth
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+description: ' HashDiff is a diff lib to compute the smallest difference between two
+ hashes. '
+email:
+- liufengyunchina@gmail.com
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- .gitignore
+- .rspec
+- .travis.yml
+- .yardopts
+- Gemfile
+- LICENSE
+- README.md
+- Rakefile
+- changelog.md
+- hashdiff.gemspec
+- lib/hashdiff.rb
+- lib/hashdiff/diff.rb
+- lib/hashdiff/lcs.rb
+- lib/hashdiff/patch.rb
+- lib/hashdiff/util.rb
+- lib/hashdiff/version.rb
+- spec/hashdiff/best_diff_spec.rb
+- spec/hashdiff/diff_array_spec.rb
+- spec/hashdiff/diff_spec.rb
+- spec/hashdiff/lcs_spec.rb
+- spec/hashdiff/patch_spec.rb
+- spec/hashdiff/util_spec.rb
+- spec/spec_helper.rb
+homepage: https://github.com/liufengyun/hashdiff
+licenses:
+- MIT
+metadata: {}
+post_install_message:
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: 1.8.7
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+requirements: []
+rubyforge_project:
+rubygems_version: 2.0.14
+signing_key:
+specification_version: 4
+summary: HashDiff is a diff lib to compute the smallest difference between two hashes.
+test_files: []
diff --git a/spec/hashdiff/best_diff_spec.rb b/spec/hashdiff/best_diff_spec.rb
new file mode 100644
index 0000000..c444e88
--- /dev/null
+++ b/spec/hashdiff/best_diff_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "should be able to best diff" do
+ a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
+ b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
+
+ diff = HashDiff.best_diff(a, b)
+ diff.should == [["-", "x[0].c", 3], ["+", "x[0].b", 2], ["-", "x[1]", {"y"=>3}]]
+ end
+
+ it "should use custom delimiter when provided" do
+ a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
+ b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
+
+ diff = HashDiff.best_diff(a, b, :delimiter => "\t")
+ diff.should == [["-", "x[0]\tc", 3], ["+", "x[0]\tb", 2], ["-", "x[1]", {"y"=>3}]]
+ end
+
+ it "should use custom comparison when provided" do
+ a = {'x' => [{'a' => 'foo', 'c' => 'goat', 'e' => 'snake'}, {'y' => 'baz'}]}
+ b = {'x' => [{'a' => 'bar', 'b' => 'cow', 'e' => 'puppy'}] }
+
+ diff = HashDiff.best_diff(a, b) do |path, obj1, obj2|
+ case path
+ when /^x\[.\]\..$/
+ obj1.length == obj2.length if obj1 and obj2
+ end
+ end
+
+ diff.should == [["-", "x[0].c", 'goat'], ["+", "x[0].b", 'cow'], ["-", "x[1]", {"y"=>'baz'}]]
+ end
+
+ it "should be able to best diff array in hash" do
+ a = {"menu" => {
+ "id" => "file",
+ "value" => "File",
+ "popup" => {
+ "menuitem" => [
+ {"value" => "New", "onclick" => "CreateNewDoc()"},
+ {"value" => "Close", "onclick" => "CloseDoc()"}
+ ]
+ }
+ }}
+
+ b = {"menu" => {
+ "id" => "file 2",
+ "value" => "File",
+ "popup" => {
+ "menuitem" => [
+ {"value" => "New1", "onclick" => "CreateNewDoc()"},
+ {"value" => "Open", "onclick" => "OpenDoc()"},
+ {"value" => "Close", "onclick" => "CloseDoc()"}
+ ]
+ }
+ }}
+
+ diff = HashDiff.best_diff(a, b)
+ diff.should == [
+ ['~', 'menu.id', 'file', 'file 2'],
+ ['~', 'menu.popup.menuitem[0].value', 'New', 'New1'],
+ ['+', 'menu.popup.menuitem[1]', {"value" => "Open", "onclick" => "OpenDoc()"}]
+ ]
+ end
+end
diff --git a/spec/hashdiff/diff_array_spec.rb b/spec/hashdiff/diff_array_spec.rb
new file mode 100644
index 0000000..c22af75
--- /dev/null
+++ b/spec/hashdiff/diff_array_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "should be able to diff two equal array" do
+ a = [1, 2, 3]
+ b = [1, 2, 3]
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == []
+ end
+
+ it "should be able to diff two arrays with one element in common" do
+ a = [1, 2, 3]
+ b = [1, 8, 7]
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['-', 2, 3], ['-', 1, 2], ['+', 1, 8], ['+', 2, 7]]
+ end
+
+ it "should be able to diff two arrays with nothing in common" do
+ a = [1, 2]
+ b = []
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['-', 1, 2], ['-', 0, 1]]
+ end
+
+ it "should be able to diff an empty array with an non-empty array" do
+ a = []
+ b = [1, 2]
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['+', 0, 1], ['+', 1, 2]]
+ end
+
+ it "should be able to diff two arrays with two elements in common" do
+ a = [1, 3, 5, 7]
+ b = [2, 3, 7, 5]
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['-', 0, 1], ['+', 0, 2], ['+', 2, 7], ['-', 4, 7]]
+ end
+
+ it "should be able to test two arrays with two common elements in different order" do
+ a = [1, 3, 4, 7]
+ b = [2, 3, 7, 5]
+
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['-', 0, 1], ['+', 0, 2], ['-', 2, 4], ['+', 3, 5]]
+ end
+
+ it "should be able to diff two arrays with similar elements" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+ diff = HashDiff.diff_array(a, b)
+ diff.should == [['+', 0, 1], ['-', 2, 3]]
+ end
+
+end
+
diff --git a/spec/hashdiff/diff_spec.rb b/spec/hashdiff/diff_spec.rb
new file mode 100644
index 0000000..903e35f
--- /dev/null
+++ b/spec/hashdiff/diff_spec.rb
@@ -0,0 +1,232 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "should be able to diff two empty hashes" do
+ diff = HashDiff.diff({}, {})
+ diff.should == []
+ end
+
+ it "should be able to diff an hash with an empty hash" do
+ a = { 'a' => 3, 'b' => 2 }
+ b = {}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['-', 'a', 3], ['-', 'b', 2]]
+
+ diff = HashDiff.diff(b, a)
+ diff.should == [['+', 'a', 3], ['+', 'b', 2]]
+ end
+
+ it "should be able to diff two equal hashes" do
+ diff = HashDiff.diff({ 'a' => 2, 'b' => 2}, { 'a' => 2, 'b' => 2 })
+ diff.should == []
+ end
+
+ it "should be able to diff two hashes with equivalent numerics, when strict is false" do
+ diff = HashDiff.diff({ 'a' => 2.0, 'b' => 2 }, { 'a' => 2, 'b' => 2.0 }, :strict => false)
+ diff.should == []
+ end
+
+ it "should be able to diff changes in hash value" do
+ diff = HashDiff.diff({ 'a' => 2, 'b' => 3, 'c' => " hello" }, { 'a' => 2, 'b' => 4, 'c' => "hello" })
+ diff.should == [['~', 'b', 3, 4], ['~', 'c', " hello", "hello"]]
+ end
+
+ it "should be able to diff changes in hash value which is array" do
+ diff = HashDiff.diff({ 'a' => 2, 'b' => [1, 2, 3] }, { 'a' => 2, 'b' => [1, 3, 4]})
+ diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]]
+ end
+
+ it "should be able to diff changes in hash value which is hash" do
+ diff = HashDiff.diff({ 'a' => { 'x' => 2, 'y' => 3, 'z' => 4 }, 'b' => { 'x' => 3, 'z' => 45 } },
+ { 'a' => { 'y' => 3 }, 'b' => { 'y' => 3, 'z' => 30 } })
+ diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
+ end
+
+ it "should be able to diff similar objects in array" do
+ diff = HashDiff.best_diff({ 'a' => [{ 'x' => 2, 'y' => 3, 'z' => 4 }, { 'x' => 11, 'y' => 22, 'z' => 33 }], 'b' => { 'x' => 3, 'z' => 45 } },
+ { 'a' => [{ 'y' => 3 }, { 'x' => 11, 'z' => 33 }], 'b' => { 'y' => 22 } })
+ diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
+ end
+
+ it 'should be able to diff addition of key value pair' do
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['+', 'g', 300]]
+
+ diff = HashDiff.diff(b, a)
+ diff.should == [['-', 'g', 300]]
+ end
+
+ it 'should be able to diff value type changes' do
+ a = {"a" => 3}
+ b = {"a" => {"a1" => 1, "a2" => 2}}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['~', 'a', 3, {"a1" => 1, "a2" => 2}]]
+
+ diff = HashDiff.diff(b, a)
+ diff.should == [['~', 'a', {"a1" => 1, "a2" => 2}, 3]]
+ end
+
+ it "should be able to diff value changes: array <=> []" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => []}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1]]
+ end
+
+ it "should be able to diff value changes: array <=> nil" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => nil}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [["~", "b", [1, 2], nil]]
+ end
+
+ it "should be able to diff value chagnes: remove array completely" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [["-", "b", [1, 2]]]
+ end
+
+ it "should be able to diff value changes: remove whole hash" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [["-", "b", {"b1"=>1, "b2"=>2}]]
+ end
+
+ it "should be able to diff value changes: hash <=> {}" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => {}}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
+ end
+
+ it "should be able to diff value changes: hash <=> nil" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => nil}
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [["~", "b", {"b1"=>1, "b2"=>2}, nil]]
+ end
+
+ it "should be able to diff similar objects in array" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [['-', '[0].d', 4], ['+', '[0]', 1], ['-', '[2]', 3]]
+ end
+
+ it "should be able to diff similar & equal objects in array" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 3]
+ b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3]
+
+ diff = HashDiff.diff(a, b)
+ diff.should == [["-", "[0].d", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]]
+ end
+
+ it "should use custom delimiter when provided" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 3]
+ b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3]
+
+ diff = HashDiff.diff(a, b, :similarity => 0.8, :delimiter => "\t")
+ diff.should == [["-", "[0]\td", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]]
+ end
+
+ context 'when :numeric_tolerance requested' do
+ it "should be able to diff changes in hash value" do
+ a = {'a' => 0.558, 'b' => 0.0, 'c' => 0.65, 'd' => 'fin'}
+ b = {'a' => 0.557, 'b' => 'hats', 'c' => 0.67, 'd' => 'fin'}
+
+ diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01)
+ diff.should == [["~", "b", 0.0, 'hats'], ["~", "c", 0.65, 0.67]]
+
+ diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01)
+ diff.should == [["~", "b", 'hats', 0.0], ["~", "c", 0.67, 0.65]]
+ end
+
+ it "should be able to diff changes in nested values" do
+ a = {'a' => {'x' => 0.4, 'y' => 0.338}, 'b' => [13, 68.03]}
+ b = {'a' => {'x' => 0.6, 'y' => 0.341}, 'b' => [14, 68.025]}
+
+ diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01)
+ diff.should == [["~", "a.x", 0.4, 0.6], ["-", "b[0]", 13], ["+", "b[0]", 14]]
+
+ diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01)
+ diff.should == [["~", "a.x", 0.6, 0.4], ["-", "b[0]", 14], ["+", "b[0]", 13]]
+ end
+ end
+
+ context 'when :strip requested' do
+ it "should strip strings before comparing" do
+ a = { 'a' => " foo", 'b' => "fizz buzz"}
+ b = { 'a' => "foo", 'b' => "fizzbuzz"}
+ diff = HashDiff.diff(a, b, :strip => true)
+ diff.should == [['~', 'b', "fizz buzz", "fizzbuzz"]]
+ end
+
+ it "should strip nested strings before comparing" do
+ a = { 'a' => { 'x' => " foo" }, 'b' => ["fizz buzz", "nerf"] }
+ b = { 'a' => { 'x' => "foo" }, 'b' => ["fizzbuzz", "nerf"] }
+ diff = HashDiff.diff(a, b, :strip => true)
+ diff.should == [['-', 'b[0]', "fizz buzz"], ['+', 'b[0]', "fizzbuzz"]]
+ end
+ end
+
+ context 'when both :strip and :numeric_tolerance requested' do
+ it 'should apply filters to proper object types' do
+ a = { 'a' => " foo", 'b' => 35, 'c' => 'bar', 'd' => 'baz' }
+ b = { 'a' => "foo", 'b' => 35.005, 'c' => 'bar', 'd' => 18.5}
+ diff = HashDiff.diff(a, b, :strict => false, :numeric_tolerance => 0.01, :strip => true)
+ diff.should == [['~', 'd', "baz", 18.5]]
+ end
+ end
+
+ context 'with custom comparison' do
+ let(:a) { { 'a' => 'car', 'b' => 'boat', 'c' => 'plane'} }
+ let(:b) { { 'a' => 'bus', 'b' => 'truck', 'c' => ' plan'} }
+
+ it 'should compare using proc specified in block' do
+ diff = HashDiff.diff(a, b) do |prefix, obj1, obj2|
+ case prefix
+ when /a|b|c/
+ obj1.length == obj2.length
+ end
+ end
+ diff.should == [['~', 'b', 'boat', 'truck']]
+ end
+
+ it 'should yield added keys' do
+ x = { 'a' => 'car', 'b' => 'boat'}
+ y = { 'a' => 'car' }
+
+ diff = HashDiff.diff(x, y) do |prefix, obj1, obj2|
+ case prefix
+ when /b/
+ true
+ end
+ end
+ diff.should == []
+ end
+
+ it 'should compare with both proc and :strip when both provided' do
+ diff = HashDiff.diff(a, b, :strip => true) do |prefix, obj1, obj2|
+ case prefix
+ when 'a'
+ obj1.length == obj2.length
+ end
+ end
+ diff.should == [['~', 'b', 'boat', 'truck'], ['~', 'c', 'plane', ' plan']]
+ end
+ end
+end
diff --git a/spec/hashdiff/lcs_spec.rb b/spec/hashdiff/lcs_spec.rb
new file mode 100644
index 0000000..7b8cece
--- /dev/null
+++ b/spec/hashdiff/lcs_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "should be able to find LCS between two equal array" do
+ a = [1, 2, 3]
+ b = [1, 2, 3]
+
+ lcs = HashDiff.lcs(a, b)
+ lcs.should == [[0, 0], [1, 1], [2, 2]]
+ end
+
+ it "should be able to find LCS between two close arrays" do
+ a = [1.05, 2, 3.25]
+ b = [1.06, 2, 3.24]
+
+ lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1)
+ lcs.should == [[0, 0], [1, 1], [2, 2]]
+ end
+
+ it "should strip strings when finding LCS if requested" do
+ a = ['foo', 'bar', 'baz']
+ b = [' foo', 'bar', 'zab']
+
+ lcs = HashDiff.lcs(a, b, :strip => true)
+ lcs.should == [[0, 0], [1, 1]]
+ end
+
+ it "should be able to find LCS with one common elements" do
+ a = [1, 2, 3]
+ b = [1, 8, 7]
+
+ lcs = HashDiff.lcs(a, b)
+ lcs.should == [[0, 0]]
+ end
+
+ it "should be able to find LCS with two common elements" do
+ a = [1, 3, 5, 7]
+ b = [2, 3, 7, 5]
+
+ lcs = HashDiff.lcs(a, b)
+ lcs.should == [[1, 1], [2, 3]]
+ end
+
+ it "should be able to find LCS with two close elements" do
+ a = [1, 3.05, 5, 7]
+ b = [2, 3.06, 7, 5]
+
+ lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1)
+ lcs.should == [[1, 1], [2, 3]]
+ end
+
+ it "should be able to find LCS with two common elements in different ordering" do
+ a = [1, 3, 4, 7]
+ b = [2, 3, 7, 5]
+
+ lcs = HashDiff.lcs(a, b)
+ lcs.should == [[1, 1], [3, 2]]
+ end
+
+ it "should be able to find LCS with a similarity value" do
+ a = [
+ {"value" => "New", "onclick" => "CreateNewDoc()"},
+ {"value" => "Close", "onclick" => "CloseDoc()"}
+ ]
+ b = [
+ {"value" => "New1", "onclick" => "CreateNewDoc()"},
+ {"value" => "Open", "onclick" => "OpenDoc()"},
+ {"value" => "Close", "onclick" => "CloseDoc()"}
+ ]
+
+ lcs = HashDiff.lcs(a, b, :similarity => 0.5)
+ lcs.should == [[0, 0], [1, 2]]
+ end
+end
+
diff --git a/spec/hashdiff/patch_spec.rb b/spec/hashdiff/patch_spec.rb
new file mode 100644
index 0000000..30d5a2e
--- /dev/null
+++ b/spec/hashdiff/patch_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "it should be able to patch key addition" do
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value type changes" do
+ a = {"a" => 3}
+ b = {"a" => {"a1" => 1, "a2" => 2}}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 3}
+ b = {"a" => {"a1" => 1, "a2" => 2}}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value array <=> []" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => []}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => []}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value array <=> nil" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => nil}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1, "b" => nil}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch array value removal" do
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => [1, 2]}
+ b = {"a" => 1}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch hash value removal" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value hash <=> {}" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => {}}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => {}}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value hash <=> nil" do
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => nil}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
+ b = {"a" => 1, "b" => nil}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch value nil removal" do
+ a = {"a" => 1, "b" => nil}
+ b = {"a" => 1}
+ diff = HashDiff.diff(a, b)
+
+ HashDiff.patch!(a, diff).should == b
+
+ a = {"a" => 1, "b" => nil}
+ b = {"a" => 1}
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch similar objects between arrays" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+
+ diff = HashDiff.diff(a, b)
+ HashDiff.patch!(a, diff).should == b
+
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+ it "should be able to patch similar & equal objects between arrays" do
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+
+ diff = HashDiff.diff(a, b)
+ HashDiff.patch!(a, diff).should == b
+
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
+ HashDiff.unpatch!(b, diff).should == a
+ end
+
+end
diff --git a/spec/hashdiff/util_spec.rb b/spec/hashdiff/util_spec.rb
new file mode 100644
index 0000000..bce861c
--- /dev/null
+++ b/spec/hashdiff/util_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe HashDiff do
+ it "should be able to decode property path" do
+ decoded = HashDiff.send(:decode_property_path, "a.b[0].c.city[5]")
+ decoded.should == ['a', 'b', 0, 'c', 'city', 5]
+ end
+
+ it "should be able to decode property path with custom delimiter" do
+ decoded = HashDiff.send(:decode_property_path, "a\tb[0]\tc\tcity[5]", "\t")
+ decoded.should == ['a', 'b', 0, 'c', 'city', 5]
+ end
+
+ it "should be able to tell similiar hash" do
+ a = {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}
+ b = {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}
+ HashDiff.similar?(a, b).should be_true
+ HashDiff.similar?(a, b, :similarity => 1).should be_false
+ end
+
+ it "should be able to tell similiar hash with values within tolerance" do
+ a = {'a' => 1.5, 'b' => 2.25, 'c' => 3, 'd' => 4, 'e' => 5}
+ b = {'a' => 1.503, 'b' => 2.22, 'c' => 3, 'e' => 5}
+ HashDiff.similar?(a, b, :numeric_tolerance => 0.05).should be_true
+ HashDiff.similar?(a, b).should be_false
+ end
+
+ it "should be able to tell numbers and strings" do
+ HashDiff.similar?(1, 2).should be_false
+ HashDiff.similar?("a", "b").should be_false
+ HashDiff.similar?("a", [1, 2, 3]).should be_false
+ HashDiff.similar?(1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}).should be_false
+ end
+
+ it "should be able to tell true when similarity == 0.5" do
+ a = {"value" => "New1", "onclick" => "CreateNewDoc()"}
+ b = {"value" => "New", "onclick" => "CreateNewDoc()"}
+
+ HashDiff.similar?(a, b, :similarity => 0.5).should be_true
+ end
+
+ it "should be able to tell false when similarity == 0.5" do
+ a = {"value" => "New1", "onclick" => "open()"}
+ b = {"value" => "New", "onclick" => "CreateNewDoc()"}
+
+ HashDiff.similar?(a, b, :similarity => 0.5).should be_false
+ end
+
+ describe '.compare_values' do
+ it "should compare numeric values exactly when no tolerance" do
+ expect(HashDiff.compare_values(10.004, 10.003)).to be_false
+ end
+
+ it "should allow tolerance with numeric values" do
+ expect(HashDiff.compare_values(10.004, 10.003, :numeric_tolerance => 0.01)).to be_true
+ end
+
+ it "should compare other objects with or without tolerance" do
+ expect(HashDiff.compare_values('hats', 'ninjas')).to be_false
+ expect(HashDiff.compare_values('hats', 'ninjas', :numeric_tolerance => 0.01)).to be_false
+ expect(HashDiff.compare_values('horse', 'horse')).to be_true
+ end
+
+ it 'should compare strings exactly by default' do
+ expect(HashDiff.compare_values(' horse', 'horse')).to be_false
+ end
+
+ it 'should strip strings before comparing when requested' do
+ expect(HashDiff.compare_values(' horse', 'horse', :strip => true)).to be_true
+ end
+ end
+end
+
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..c52a6fc
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,13 @@
+$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
+
+require 'rubygems'
+require 'rspec'
+require 'rspec/autorun'
+
+require 'hashdiff'
+
+RSpec.configure do |config|
+ config.mock_framework = :rspec
+
+ config.include RSpec::Matchers
+end