fixed_length_encoder 1.2.1 → 2.0.0

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/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+ gem 'rspec', '~> 2.14.1'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2013 Brett Pontarelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # fixed_length_encoder
2
+
3
+ A one-to-one mapping function between integers and fixed length strings, such that sequential
4
+ integers are mapped to non-sequential strings. In other words you can obfuscate user ids for use
5
+ in urls.
6
+
7
+ * https://rubygems.org/gems/fixed_length_encoder
8
+ * http://github.com/brettwp/fixed_length_encoder
9
+
10
+ ## How it works
11
+
12
+ ### Encoding
13
+
14
+ Converts an integer value to a string of fixed length (default is 8). As of `1.2` the maximum encodable value is the same as the alphabet maximum. For example the default 36 character alphabet and 8 character fixed length can encoded numbers between 0 and 2,821,109,907,455 = `36**8 - 1`.
15
+
16
+ ### Decoding
17
+
18
+ Given a valid string returns the decoded number. Note that the two operations are reversible and
19
+ adjacent values are unlikely to return adjacent strings (See Stats section below). For example, using the default configuration:
20
+
21
+ FixedLengthEncoder.decode(FixedLengthEncoder.encode(100)) == 100
22
+ FixedLengthEncoder.encode(100) == 'ycxk2ntw'
23
+ FixedLengthEncoder.encode(101) == 'd8gxk24x'
24
+
25
+ ## How to install
26
+
27
+ sudo gem install fixed_length_encoder
28
+
29
+ ## How to use
30
+
31
+ require 'fixed_length_encoder'
32
+
33
+ FixedLengthEncoder.encode(100)
34
+ FixedLengthEncoder.decode('ycxk2ntw')
35
+
36
+ FixedLengthEncoder.encode(42, 3)
37
+ FixedLengthEncoder.decode('5c4')
38
+
39
+ ## Changing the length
40
+
41
+ FixedLengthEncoder::MESSAGE_LENGTH = 10
42
+
43
+ ## Changing the alphabet and encoding
44
+
45
+ The `ALPHABET`, `ENCODE_MAP` and `DECODE_MAP` must all work together. The two maps must also be
46
+ reversible. For example, for an alphabet of 62 characters you will need to build two maps of
47
+ length `62**2 - 1` such that `DECODE_MAP[ENCODE_MAP[x]] == x`. One such way to do this would be:
48
+
49
+ max = 62*62 - 1
50
+ ENCODE_MAP = (0..max).to_a.shuffle
51
+ DECODE_MAP = []
52
+ (0..max).each { |i| DECODE_MAP[ENCODE_MAP[i]] = i }
53
+
54
+ Then, hard code these results into your application. You will have three lines much like the lines
55
+ that define the default `ALPHABET`, `ENCODE_MAP` and `DECODE_MAP` in the `FixedLengthEncoded`:
56
+
57
+ FixedLengthEncoder::ALPHABET = 'abcdefg'
58
+ FixedLengthEncoder::ENCODE_MAP = [19, 22, 25, 44, 17, 21, 33, 48, 39, 0, 16, 20, 29, 40, 43, 23, 3, 41, 12, 35, 7, 14, 10, 32, 46, 38, 9, 11, 27, 31, 26, 18, 34, 24, 4, 42, 47, 5, 1, 36, 13, 37, 30, 15, 45, 2, 8, 28, 6]
59
+ FixedLengthEncoder::DECODE_MAP = [9, 38, 45, 16, 34, 37, 48, 20, 46, 26, 22, 27, 18, 40, 21, 43, 10, 4, 31, 0, 11, 5, 1, 15, 33, 2, 30, 28, 47, 12, 42, 29, 23, 6, 32, 19, 39, 41, 25, 8, 13, 17, 35, 14, 3, 44, 24, 36, 7]
60
+
61
+ # Stats
62
+
63
+ Consider a random `value` using the `FixedLengthEncoder` default `LENGTH` of 8 and `ALPHABET` of 36
64
+ characters. If we encode `value` and `value + 1` and compute the difference between them in base 36
65
+ we get a `delta`. The table below compares the distribution of 10M pairs of two adjacent encoded values with 10M pairs of two random numbers.
66
+ Both sets are taken from the range `0` to `36**8 = 2,821,109,907,456`. As expected the number of
67
+ negative deltas is near `50%` with the encoded values `49.9860%` negative and random `49.9989%`.
68
+ It's interesting to note that there are no random occurances of two adjecent values, but in the
69
+ encoded values there are `674`.
70
+
71
+ | |: Encoded :|: Random :|
72
+ | ---------------:| -----------------:| -----------------:|
73
+ | Negative deltas| 4,998,610 | 4,999,894 |
74
+ | Delta equals one| 674 | 0 |
75
+ | Maximum Delta| 2,820,278,456,877 | 2,820,579,691,973 |
76
+ | Average Delta| 935,745,508,922 | 940,183,477,180 |
77
+ | Std Dev| 1,148,460,034,903 | 1,151,442,250,985 |
78
+
79
+ * Author :: Brett Pontarelli <brett@paperyfrog.com>
80
+ * Website :: http://brett.pontarelli.com
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rake'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
6
+
7
+ desc 'Run statistical tests'
8
+ task :stats do
9
+ load 'extra/stats.rb'
10
+ end
11
+
12
+ desc 'Build examples for README'
13
+ task :readme do
14
+ load 'extra/readme.rb'
15
+ end
data/extra/readme.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'fixed_length_encoder'
2
+
3
+ puts FixedLengthEncoder.encode(100)
4
+ puts FixedLengthEncoder.encode(101)
5
+ puts FixedLengthEncoder.encode(42, 3)
6
+
7
+ max = 7*7 - 1
8
+ ENCODE_MAP = (0..max).to_a.shuffle
9
+ DECODE_MAP = []
10
+ (0..max).each { |i| DECODE_MAP[ENCODE_MAP[i]] = i }
11
+ puts "[#{ENCODE_MAP.join(', ')}]"
12
+ puts "[#{DECODE_MAP.join(', ')}]"
data/extra/stats.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'fixed_length_encoder'
2
+
3
+ class Stats
4
+ def initialize
5
+ @delta_max = 0
6
+ @negative_count = 0
7
+ @one_count = 0
8
+ @iterations = 0
9
+ @average_cumm = 0
10
+ @stddev_cumm = 0
11
+ end
12
+
13
+ def add_delta(delta)
14
+ if (delta < 0)
15
+ delta = -delta
16
+ @negative_count += 1
17
+ end
18
+ @one_count += 1 if delta == 1
19
+ @iterations += 1
20
+ @average_cumm += delta
21
+ @stddev_cumm += delta*delta
22
+ @delta_max = delta if delta > @delta_max
23
+ end
24
+
25
+ def negative_count
26
+ @negative_count
27
+ end
28
+
29
+ def one_count
30
+ @one_count
31
+ end
32
+
33
+ def delta_max
34
+ @delta_max
35
+ end
36
+
37
+ def average
38
+ @average_cumm / @iterations
39
+ end
40
+
41
+ def stddev
42
+ Math.sqrt((@stddev_cumm - (average*average/@iterations))/@iterations)
43
+ end
44
+
45
+ def one_percent
46
+ 100.0 * @one_count / @iterations
47
+ end
48
+
49
+ def negative_percent
50
+ 100.0 * @negative_count / @iterations
51
+ end
52
+ end
53
+
54
+ def comma(number)
55
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
56
+ end
57
+
58
+ max = 36**8
59
+ iter = 10**7
60
+ encoder = FixedLengthEncoder::Encoder.new(FixedLengthEncoder::ALPHABET, FixedLengthEncoder::ENCODE_MAP, FixedLengthEncoder::DECODE_MAP)
61
+ random_stats = Stats.new()
62
+ encoder_stats = Stats.new()
63
+ puts "36**8 = #{comma(max)}"
64
+ (0..iter).each do |i|
65
+ value = Random.rand(max - 2)
66
+ e1 = encoder.string_to_integer(encoder.encode(value, 8))
67
+ e2 = encoder.string_to_integer(encoder.encode(value+1, 8))
68
+ encoder_stats.add_delta(e1 - e2)
69
+
70
+ v1 = Random.rand(max - 1)
71
+ v2 = Random.rand(max - 1)
72
+ random_stats.add_delta(v1 - v2)
73
+ end
74
+
75
+ def report(title, encoder)
76
+ puts "===#{title}==="
77
+ puts 'Neg: ' + comma(encoder.negative_count) + " (#{encoder.negative_percent.to_s}%)"
78
+ puts 'One: ' + comma(encoder.one_count) + " (#{encoder.one_percent.to_s}%)"
79
+ puts 'Max: ' + comma(encoder.delta_max)
80
+ puts 'Avg: ' + comma(encoder.average)
81
+ puts 'Std: ' + comma(encoder.stddev)
82
+ end
83
+
84
+ report('Random', random_stats)
85
+ report('Encoder', encoder_stats)
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'fixed_length_encoder'
8
+ spec.version = FixedLengthEncoder::VERSION
9
+ spec.authors = ['Brett Pontarelli']
10
+ spec.email = ['brett@paperyfrog.com']
11
+ spec.description = 'Integers are converted to strings using a complex mapping ' +
12
+ 'and a base 36 alphabet. Strings with valid digits (0-9 a-z)' +
13
+ 'are converted back to integers. The encoding is one-to-one but not sequential.' +
14
+ 'This is useful for obfuscating user ids in urls.'
15
+ spec.summary = 'Two way conversion from integer to a fixed (default=8) digit string.'
16
+ spec.homepage = 'http://rubygems.org/gems/fixed_length_encoder'
17
+ spec.license = 'MIT'
18
+
19
+ spec.files = `git ls-files`.split($/)
20
+ # spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.3'
25
+ spec.add_development_dependency 'rake'
26
+ end
@@ -63,30 +63,26 @@ module FixedLengthEncoder
63
63
 
64
64
  def offset(value, direction)
65
65
  offset = (@max_value/2).floor
66
- (value += offset) if direction > 0
67
- (value -= offset) if direction < 0
68
- (value += @max_value) if value < 0
69
- (value -= @max_value) if value >= @max_value
70
- value
66
+ (value + direction * offset) % @max_value
71
67
  end
72
68
 
73
69
  def scramble_value(value, direction)
74
- message = integer_to_array(value)
70
+ message = integer_to_array(value)
75
71
  message = map_array(message, @encode_map, direction) if direction > 0
76
72
  message = map_array(message, @decode_map, direction) if direction < 0
77
73
  array_to_integer(message)
78
74
  end
79
75
 
80
76
  def map_array(message, map, direction)
81
- indexes = (1..(@message_length - 1)).to_a
77
+ indexes = (0..@message_length).to_a.map { |i| i % @message_length }
82
78
  indexes = indexes.reverse if direction < 0
83
- indexes.each do |index|
84
- low = message[index - 1]
85
- high = message[index]
86
- map_index = high * @base + low
79
+ (0...@message_length).each do |index|
80
+ low_index = indexes[index]
81
+ high_index = indexes[index + 1]
82
+ map_index = message[high_index] * @base + message[low_index]
87
83
  map_value = map[map_index]
88
- message[index - 1] = map_value % @base
89
- message[index] = map_value / @base
84
+ message[low_index] = map_value / @base
85
+ message[high_index] = map_value % @base
90
86
  end
91
87
  message
92
88
  end
@@ -123,4 +119,4 @@ module FixedLengthEncoder
123
119
  value
124
120
  end
125
121
  end
126
- end
122
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module FixedLengthEncoder
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,125 @@
1
+ require 'fixed_length_encoder'
2
+
3
+ def suppress_warnings
4
+ original_verbosity = $VERBOSE
5
+ $VERBOSE = nil
6
+ result = yield
7
+ $VERBOSE = original_verbosity
8
+ return result
9
+ end
10
+
11
+ describe FixedLengthEncoder do
12
+ describe 'Invalid encodings' do
13
+ before (:each) do
14
+ suppress_warnings do
15
+ @origianl = FixedLengthEncoder::ALPHABET
16
+ FixedLengthEncoder::ALPHABET = '0123456789'
17
+ end
18
+ end
19
+
20
+ after (:each) do
21
+ suppress_warnings do
22
+ FixedLengthEncoder::ALPHABET = @origianl
23
+ end
24
+ end
25
+
26
+ it 'should error on non-integers' do
27
+ expect { FixedLengthEncoder.encode('ERROR') }.to raise_error(ArgumentError)
28
+ end
29
+
30
+ it 'shouldn\'t encode values too big for message length' do
31
+ expect { FixedLengthEncoder.encode(100, 2) }.to raise_error(ArgumentError)
32
+ end
33
+
34
+ it 'shouldn\'t encode negative values' do
35
+ expect { FixedLengthEncoder.encode(-1, 2) }.to raise_error(ArgumentError)
36
+ end
37
+
38
+ it 'should error for non-strings' do
39
+ expect { FixedLengthEncoder.decode(0) }.to raise_error(ArgumentError)
40
+ end
41
+
42
+ it 'should error for bad characters' do
43
+ expect { FixedLengthEncoder.decode('^') }.to raise_error(ArgumentError)
44
+ end
45
+ end
46
+
47
+ describe 'Valid encodings' do
48
+ before (:each) do
49
+ suppress_warnings do
50
+ @origianl = FixedLengthEncoder::ALPHABET
51
+ @encode_map = FixedLengthEncoder::ENCODE_MAP
52
+ @decode_map = FixedLengthEncoder::DECODE_MAP
53
+ FixedLengthEncoder::ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
54
+ max = 62*62 - 1
55
+ encode_map = (0..max).to_a.shuffle
56
+ decode_map = []
57
+ (0..max).each { |i| decode_map[encode_map[i]] = i }
58
+ FixedLengthEncoder::ENCODE_MAP = encode_map
59
+ FixedLengthEncoder::DECODE_MAP = decode_map
60
+ end
61
+ end
62
+
63
+ after (:each) do
64
+ suppress_warnings do
65
+ FixedLengthEncoder::ALPHABET = @origianl
66
+ FixedLengthEncoder::ENCODE_MAP = @encode_map
67
+ FixedLengthEncoder::DECODE_MAP = @decode_map
68
+ end
69
+ end
70
+
71
+ it 'should be a valid alphabet' do
72
+ FixedLengthEncoder.isValidAlphabet().should be_true
73
+ end
74
+
75
+ it 'should be a valid map' do
76
+ FixedLengthEncoder.isValidMap().should be_true
77
+ end
78
+
79
+ it 'should be reversible for the min value' do
80
+ value = 0
81
+ message = FixedLengthEncoder.encode(value)
82
+ FixedLengthEncoder.decode(message).should eq(value)
83
+ end
84
+
85
+ it 'should be reversible for the max value' do
86
+ value = (62**8)-1
87
+ message = FixedLengthEncoder.encode(value)
88
+ FixedLengthEncoder.decode(message).should eq(value)
89
+ end
90
+
91
+ it 'should be reversible for the middle value' do
92
+ value = ((62**8)/2).floor
93
+ message = FixedLengthEncoder.encode(value)
94
+ FixedLengthEncoder.decode(message).should eq(value)
95
+ end
96
+ end
97
+
98
+ describe 'Default encodings' do
99
+ it 'should be a valid alphabet' do
100
+ FixedLengthEncoder.isValidAlphabet().should be_true
101
+ end
102
+
103
+ it 'should be a valid map' do
104
+ FixedLengthEncoder.isValidMap().should be_true
105
+ end
106
+
107
+ it 'should be reversible for the min value' do
108
+ value = 0
109
+ message = FixedLengthEncoder.encode(value)
110
+ FixedLengthEncoder.decode(message).should eq(value)
111
+ end
112
+
113
+ it 'should be reversible for the max value' do
114
+ value = (36**8)-1
115
+ message = FixedLengthEncoder.encode(value)
116
+ FixedLengthEncoder.decode(message).should eq(value)
117
+ end
118
+
119
+ it 'should be reversible for the middle value' do
120
+ value = ((36**8)/2).floor
121
+ message = FixedLengthEncoder.encode(value)
122
+ FixedLengthEncoder.decode(message).should eq(value)
123
+ end
124
+ end
125
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixed_length_encoder
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,20 +9,65 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-28 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-09-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
14
46
  description: Integers are converted to strings using a complex mapping and a base
15
47
  36 alphabet. Strings with valid digits (0-9 a-z)are converted back to integers. The
16
48
  encoding is one-to-one but not sequential.This is useful for obfuscating user ids
17
49
  in urls.
18
- email: brett@paperyfrog.com
50
+ email:
51
+ - brett@paperyfrog.com
19
52
  executables: []
20
53
  extensions: []
21
54
  extra_rdoc_files: []
22
55
  files:
56
+ - .gitignore
57
+ - .rspec
58
+ - Gemfile
59
+ - LICENSE
60
+ - README.md
61
+ - Rakefile
62
+ - extra/readme.rb
63
+ - extra/stats.rb
64
+ - fixed_length_encoder.gemspec
23
65
  - lib/fixed_length_encoder.rb
66
+ - lib/version.rb
67
+ - spec/fixed_length_encoder_spec.rb
24
68
  homepage: http://rubygems.org/gems/fixed_length_encoder
25
- licenses: []
69
+ licenses:
70
+ - MIT
26
71
  post_install_message:
27
72
  rdoc_options: []
28
73
  require_paths:
@@ -33,16 +78,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
33
78
  - - ! '>='
34
79
  - !ruby/object:Gem::Version
35
80
  version: '0'
81
+ segments:
82
+ - 0
83
+ hash: 2142083918509383952
36
84
  required_rubygems_version: !ruby/object:Gem::Requirement
37
85
  none: false
38
86
  requirements:
39
87
  - - ! '>='
40
88
  - !ruby/object:Gem::Version
41
89
  version: '0'
90
+ segments:
91
+ - 0
92
+ hash: 2142083918509383952
42
93
  requirements: []
43
94
  rubyforge_project:
44
- rubygems_version: 1.8.24
95
+ rubygems_version: 1.8.25
45
96
  signing_key:
46
97
  specification_version: 3
47
98
  summary: Two way conversion from integer to a fixed (default=8) digit string.
48
- test_files: []
99
+ test_files:
100
+ - spec/fixed_length_encoder_spec.rb