scatter_swap 0.0.1 → 0.0.2

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.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format doc
data/README.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # ScatterSwap
2
2
 
3
+ This is the hashing function behind ObfuscateId.
4
+ https://github.com/namick/obfuscate_id
5
+
6
+ Designing a hash function is a bit of a black art and
7
+ being that I don't have math background, I must resort
8
+ to this simplistic swaping and scattering of array elements.
9
+
10
+ After writing this and reading/learning some elemental hashing techniques,
11
+ I realize this library is what is known as a Minimal perfect hash function:
12
+ http://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function
13
+
14
+ I welcome all improvements :-)
15
+
16
+ If you have some comments or suggestions, please contact me on github
17
+ https://github.com/namick
18
+
19
+ - nathan amick
20
+
21
+
22
+ This library is built for integers that can be expressed with 10 digits:
23
+ It zero pads smaller numbers... so the number 1 is expressed with:
24
+ 0000000001
25
+
26
+ The biggest number it can deal with is:
27
+ 9999999999
28
+
29
+ Since we are working with a limited sequential set of input integers, 10 billion,
30
+ this algorithm will suffice for simple id obfuscation for many web apps.
31
+ The largest value that Ruby on Rails default id, Mysql INT type, is just over 2 billion (2147483647)
32
+ which is the same as 2 to the power of 31 minus 1, but considerably less than 10 billion.
33
+
34
+ ScatterSwap is an integer hash function designed to have:
35
+ - zero collisions ( http://en.wikipedia.org/wiki/Perfect_hash_function )
36
+ - achieve avalanche ( http://en.wikipedia.org/wiki/Avalanche_effect )
37
+ - reversable
38
+
39
+ We do that by combining two distinct strategies.
40
+
41
+ 1. Scattering - whereby we take the whole number, slice it up into 10 digits
42
+ and rearange their places, yet retaining the same 10 digits. This allows
43
+ us to preserve the sum of all the digits, regardless of order. This sum acts
44
+ as a key to reverse this scattering effect.
45
+
46
+ 2. Swapping - when dealing with many sequential numbers that we don't want
47
+ to look similar, scattering wont do us much good because so many of the
48
+ digits are the same; it deoesn't help to scatter 9 zeros around, so we need
49
+ to swap out each of those zeros for something else.. something different
50
+ for each place in the 10 digit array; for this, we need a map so that we
51
+ can reverse it.
52
+
3
53
  TODO: Write a gem description
4
54
 
5
55
  ## Installation
@@ -0,0 +1,81 @@
1
+ module ScatterSwap
2
+ class Hasher
3
+ attr_accessor :working_array
4
+
5
+ def initialize(original_integer, spin = 0)
6
+ @original_integer = original_integer
7
+ @spin = spin
8
+ zero_pad = original_integer.to_s.rjust(10, '0')
9
+ @working_array = zero_pad.split("").collect {|d| d.to_i}
10
+ end
11
+
12
+ # obfuscates an integer up to 10 digits in length
13
+ def hash
14
+ swap
15
+ scatter
16
+ completed_string
17
+ end
18
+
19
+ # de-obfuscates an integer
20
+ def reverse_hash
21
+ unscatter
22
+ unswap
23
+ completed_string
24
+ end
25
+
26
+ def completed_string
27
+ @working_array.join
28
+ end
29
+
30
+ # We want a unique map for each place in the original number
31
+ def swapper_map(index)
32
+ array = (0..9).to_a
33
+ 10.times.collect.with_index do |i|
34
+ array.rotate!(index + i ^ spin).pop
35
+ end
36
+ end
37
+
38
+ # Using a unique map for each of the ten places,
39
+ # we swap out one number for another
40
+ def swap
41
+ @working_array = @working_array.collect.with_index do |digit, index|
42
+ swapper_map(index)[digit]
43
+ end
44
+ end
45
+
46
+ # Reverse swap
47
+ def unswap
48
+ @working_array = @working_array.collect.with_index do |digit, index|
49
+ swapper_map(index).rindex(digit)
50
+ end
51
+ end
52
+
53
+ # Rearrange the order of each digit in a reversable way by using the
54
+ # sum of the digits (which doesn't change regardless of order)
55
+ # as a key to record how they were scattered
56
+ def scatter
57
+ sum_of_digits = @working_array.inject(:+).to_i
58
+ @working_array = 10.times.collect do
59
+ @working_array.rotate!(spin ^ sum_of_digits).pop
60
+ end
61
+ end
62
+
63
+ # Reverse the scatter
64
+ def unscatter
65
+ scattered_array = @working_array
66
+ sum_of_digits = scattered_array.inject(:+).to_i
67
+ @working_array = []
68
+ @working_array.tap do |unscatter|
69
+ 10.times do
70
+ unscatter.push scattered_array.pop
71
+ unscatter.rotate! (sum_of_digits ^ spin) * -1
72
+ end
73
+ end
74
+ end
75
+
76
+ # Add some spice so that different apps can have differently mapped hashes
77
+ def spin
78
+ @spin || 0
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,93 @@
1
+ require 'scatter_swap'
2
+
3
+ # This file is spike code and is not part of the library
4
+ #
5
+ # While developing this, I used this file to visualize what was going on with the numbers
6
+ #
7
+ # You can uncomment various methods at the bottom and then run it like this:
8
+ #
9
+ # watch -n1 ruby run_scatter_swap.rb
10
+ #
11
+ # tweak the code a bit and see instant visual changes in the generated numbers
12
+
13
+ def visualize_scatter_and_unscatter
14
+ # change this number to experiment with different values
15
+ rotations = 99
16
+
17
+ original = ScatterSwap.arrayify(123456789)
18
+ scattered = []
19
+ unscattered = []
20
+ puts original.join
21
+ puts "rotate!(#{rotations})"
22
+ 10.times do
23
+ puts original.rotate!(rotations).join + "->" + scattered.push(original.pop).join
24
+ end
25
+ puts "scattered"
26
+ puts scattered.join
27
+ 10.times do
28
+ puts unscattered.push(scattered.pop).join + "->" + unscattered.rotate!(rotations * -1).join
29
+ end
30
+
31
+ puts unscattered.join
32
+ end
33
+
34
+
35
+ def visualize_swapper_map
36
+ puts "swapper map"
37
+ 10.times do |index|
38
+ key = 1
39
+ puts ScatterSwap.swapper_map(index).join.to_s
40
+ end
41
+ end
42
+
43
+ def visualize_hash
44
+ puts "hash"
45
+ 40.times do |integer|
46
+ output = "|"
47
+ 3.times do |index|
48
+ output += " #{(integer + (123456789 * index)).to_s.rjust(5, ' ')}"
49
+ output += " => #{hashed = ScatterSwap.hash(integer + (123456789 * index) ) }"
50
+ output += " => #{ScatterSwap.reverse_hash(hashed) } |"
51
+ end
52
+ puts output
53
+ end
54
+ end
55
+
56
+
57
+ def visualize_scatter
58
+ puts "original scattered unscattered"
59
+ 20.times do |integer|
60
+ output = ""
61
+ 2.times do |index|
62
+ original = ScatterSwap.arrayify(integer + (123456789 * index))
63
+ scattered = ScatterSwap.scatter(original.clone)
64
+ unscattered = ScatterSwap.unscatter(scattered.clone)
65
+ output += "#{original.join}"
66
+ output += " => #{scattered.join}"
67
+ output += " => #{unscattered.join} | "
68
+ end
69
+ puts output
70
+ end
71
+ end
72
+
73
+
74
+ # find hash for lots of spins
75
+ def visualize_spin
76
+ 2000.times do |original|
77
+ hashed_values = []
78
+ 9000000000.times do |spin|
79
+ hashed = ScatterSwap.hash(original, spin)
80
+ if hashed_values.include? hashed
81
+ puts "collision: #{original} - #{spin} - #{hashed}"
82
+ break
83
+ end
84
+ hashed_values.push hashed
85
+ end
86
+ end
87
+ end
88
+
89
+ #visualize_spin
90
+ #visualize_hash
91
+ #visualize_scatter_and_unscatter
92
+ #visualize_scatter
93
+ #visualize_swapper_map
@@ -1,3 +1,3 @@
1
1
  module ScatterSwap
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/scatter_swap.rb CHANGED
@@ -1,140 +1,12 @@
1
1
  require "scatter_swap/version"
2
+ require "scatter_swap/hasher"
2
3
 
3
- class ScatterSwap
4
- # This is the hashing function behind ObfuscateId.
5
- # https://github.com/namick/obfuscate_id
6
- #
7
- # Designing a hash function is a bit of a black art and
8
- # being that I don't have math background, I must resort
9
- # to this simplistic swaping and scattering of array elements.
10
- #
11
- # After writing this and reading/learning some elemental hashing techniques,
12
- # I realize this library is what is known as a Minimal perfect hash function:
13
- # http://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function
14
- #
15
- # I welcome all improvements :-)
16
- #
17
- # If you have some comments or suggestions, please contact me on github
18
- # https://github.com/namick
19
- #
20
- # - nathan amick
21
- #
22
- #
23
- # This library is built for integers that can be expressed with 10 digits:
24
- # It zero pads smaller numbers... so the number one is expressed with:
25
- # 0000000001
26
- # The biggest number it can deal with is:
27
- # 9999999999
28
- #
29
- # Since we are working with a limited sequential set of input integers, 10 billion,
30
- # this algorithm will suffice for simple id obfuscation for many web apps.
31
- # The largest value that Ruby on Rails default id, Mysql INT type, is just over 2 billion (2147483647)
32
- # which is the same as 2 to the power of 31 minus 1, but considerably less than 10 billion.
33
- #
34
- # ScatterSwap is an integer hash function designed to have:
35
- # - zero collisions ( http://en.wikipedia.org/wiki/Perfect_hash_function )
36
- # - achieve avalanche ( http://en.wikipedia.org/wiki/Avalanche_effect )
37
- # - reversable
38
- #
39
- # We do that by combining two distinct strategies.
40
- #
41
- # 1. Scattering - whereby we take the whole number, slice it up into 10 digits
42
- # and rearange their places, yet retaining the same 10 digits. This allows
43
- # us to preserve the sum of all the digits, regardless of order. This sum acts
44
- # as a key to reverse this scattering effect.
45
- #
46
- # 2. Swapping - when dealing with many sequential numbers that we don't want
47
- # to look similar, scattering wont do us much good because so many of the
48
- # digits are the same; it deoesn't help to scatter 9 zeros around, so we need
49
- # to swap out each of those zeros for something else.. something different
50
- # for each place in the 10 digit array; for this, we need a map so that we
51
- # can reverse it.
52
-
53
- # Convience class method pointing to the instance method
4
+ module ScatterSwap
54
5
  def self.hash(plain_integer, spin = 0)
55
- new(plain_integer, spin).hash
6
+ Hasher.new(plain_integer, spin).hash
56
7
  end
57
8
 
58
- # Convience class method pointing to the instance method
59
9
  def self.reverse_hash(hashed_integer, spin = 0)
60
- new(hashed_integer, spin).reverse_hash
61
- end
62
-
63
- def initialize(original_integer, spin = 0)
64
- @original_integer = original_integer
65
- @spin = spin
66
- zero_pad = original_integer.to_s.rjust(10, '0')
67
- @working_array = zero_pad.split("").collect {|d| d.to_i}
68
- end
69
-
70
- attr_accessor :working_array
71
-
72
- # obfuscates an integer up to 10 digits in length
73
- def hash
74
- swap
75
- scatter
76
- completed_string
77
- end
78
-
79
- # de-obfuscates an integer
80
- def reverse_hash
81
- unscatter
82
- unswap
83
- completed_string
84
- end
85
-
86
- def completed_string
87
- @working_array.join
88
- end
89
-
90
- # We want a unique map for each place in the original number
91
- def swapper_map(index)
92
- array = (0..9).to_a
93
- 10.times.collect.with_index do |i|
94
- array.rotate!(index + i ^ spin).pop
95
- end
96
- end
97
-
98
- # Using a unique map for each of the ten places,
99
- # we swap out one number for another
100
- def swap
101
- @working_array = @working_array.collect.with_index do |digit, index|
102
- swapper_map(index)[digit]
103
- end
104
- end
105
-
106
- # Reverse swap
107
- def unswap
108
- @working_array = @working_array.collect.with_index do |digit, index|
109
- swapper_map(index).rindex(digit)
110
- end
111
- end
112
-
113
- # Rearrange the order of each digit in a reversable way by using the
114
- # sum of the digits (which doesn't change regardless of order)
115
- # as a key to record how they were scattered
116
- def scatter
117
- sum_of_digits = @working_array.inject(:+).to_i
118
- @working_array = 10.times.collect do
119
- @working_array.rotate!(spin ^ sum_of_digits).pop
120
- end
121
- end
122
-
123
- # Reverse the scatter
124
- def unscatter
125
- scattered_array = @working_array
126
- sum_of_digits = scattered_array.inject(:+).to_i
127
- @working_array = []
128
- @working_array.tap do |unscatter|
129
- 10.times do
130
- unscatter.push scattered_array.pop
131
- unscatter.rotate! (sum_of_digits ^ spin) * -1
132
- end
133
- end
134
- end
135
-
136
- # Add some spice so that different apps can have differently mapped hashes
137
- def spin
138
- @spin || 0
10
+ Hasher.new(hashed_integer, spin).reverse_hash
139
11
  end
140
12
  end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+ require 'scatter_swap'
3
+
4
+ describe "#hash" do
5
+ it "should be 10 digits" do
6
+ 100.times do |integer|
7
+ ScatterSwap.hash(integer).to_s.length.should == 10
8
+ end
9
+ end
10
+
11
+ it "should not be sequential" do
12
+ first = ScatterSwap.hash(1)
13
+ second = ScatterSwap.hash(2)
14
+ second.should_not eql(first.to_i + 1)
15
+ end
16
+
17
+ it "should be reversable" do
18
+ 100.times do |integer|
19
+ hashed = ScatterSwap.hash(integer)
20
+ ScatterSwap.reverse_hash(hashed).to_i.should == integer
21
+ end
22
+ end
23
+ end
24
+
25
+ describe "#swapper_map" do
26
+ before do
27
+ @map_set = []
28
+ s = ScatterSwap::Hasher.new(1)
29
+ 10.times do |digit|
30
+ @map_set.push s.swapper_map(digit)
31
+ end
32
+ end
33
+
34
+ it "should create a unique map array for each digit" do
35
+ @map_set.length.should == 10
36
+ @map_set.uniq.length.should == 10
37
+ end
38
+
39
+ it "should include all 10 digits in each map" do
40
+ @map_set.each do |map|
41
+ map.length.should == 10
42
+ map.uniq.length.should == 10
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "#scatter" do
48
+ it "should return a number different from original" do
49
+ 100.times do |integer|
50
+ s = ScatterSwap::Hasher.new(integer)
51
+ original_array = s.working_array
52
+ s.scatter
53
+ s.working_array.should_not == original_array
54
+ end
55
+ end
56
+
57
+ it "should be reversable" do
58
+ 100.times do |integer|
59
+ s = ScatterSwap::Hasher.new(integer)
60
+ original_array = s.working_array.clone
61
+ s.scatter
62
+ s.unscatter
63
+ s.working_array.should == original_array
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#swap" do
69
+ it "should be different from original" do
70
+ 100.times do |integer|
71
+ s = ScatterSwap::Hasher.new(integer)
72
+ original_array = s.working_array.clone
73
+ s.swap
74
+ s.working_array.should_not == original_array
75
+ end
76
+ end
77
+
78
+ it "should be reversable" do
79
+ 100.times do |integer|
80
+ s = ScatterSwap::Hasher.new(integer)
81
+ original_array = s.working_array.clone
82
+ s.swap
83
+ s.unswap
84
+ s.working_array.should == original_array
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scatter_swap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-27 00:00:00.000000000 Z
12
+ date: 2013-03-28 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ScatterSwap is an integer hash function designed to have zero collisions,
15
15
  achieve avalanche, and be reversible.
@@ -20,13 +20,18 @@ extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
22
  - .gitignore
23
+ - .rspec
23
24
  - Gemfile
24
25
  - LICENSE.txt
25
26
  - README.md
26
27
  - Rakefile
27
28
  - lib/scatter_swap.rb
29
+ - lib/scatter_swap/hasher.rb
30
+ - lib/scatter_swap/run.rb
28
31
  - lib/scatter_swap/version.rb
29
32
  - scatter_swap.gemspec
33
+ - spec/scatter_swap_spec.rb
34
+ - spec/spec_helper.rb
30
35
  homepage: ''
31
36
  licenses: []
32
37
  post_install_message:
@@ -47,8 +52,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
52
  version: '0'
48
53
  requirements: []
49
54
  rubyforge_project:
50
- rubygems_version: 1.8.24
55
+ rubygems_version: 1.8.23
51
56
  signing_key:
52
57
  specification_version: 3
53
58
  summary: Minimal perfect hash function for 10 digit integers
54
- test_files: []
59
+ test_files:
60
+ - spec/scatter_swap_spec.rb
61
+ - spec/spec_helper.rb