jebediah 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/jebediah.rb ADDED
@@ -0,0 +1,200 @@
1
+ class Jebediah
2
+ def self.version
3
+ return "1.0.1"
4
+ end
5
+
6
+ def initialize(dictPaths=nil)
7
+ if dictPaths == nil then
8
+ # Default configuration is a 3-word phrase (adverb, verb, animal)
9
+ # e.g. "ridiculously elaborated parrot"
10
+ base = File.expand_path(File.dirname(__FILE__) + '/../dictionaries')
11
+ dictPaths = [
12
+ File.join(base, "adverbs.txt"),
13
+ File.join(base, "verbs.txt"),
14
+ File.join(base, "animals.txt"),
15
+ ]
16
+ end
17
+
18
+ loadDictionaries(dictPaths)
19
+ end
20
+
21
+ # Returns number of words expected in a phrase for this Jebediah instance
22
+ def phraseLength
23
+ return @dictionaries.length
24
+ end
25
+
26
+ # Load in dictionaries from paths
27
+ def loadDictionaries(dictPaths)
28
+ @dictionaries = []
29
+ dictPaths.each do |dictPath|
30
+ unless File.exists?(dictPath) then
31
+ puts "Dictionary does not exist: #{dictPath}"
32
+ return
33
+ end
34
+
35
+ unless File.file?(dictPath) then
36
+ puts "Dictionary is not a regular file: #{dictPath}"
37
+ end
38
+
39
+ unless File.readable?(dictPath) then
40
+ puts "Dictionary is not readable: #{dictPath}"
41
+ end
42
+
43
+ # Read dictionary lines into an array, no trailing newline
44
+ @dictionaries.push File.open(dictPath, 'r') { |file| file.readlines.collect{|line| line.chomp} }
45
+ end
46
+ end
47
+
48
+ # Test if a string is a valid hash
49
+ def isHash?(str)
50
+ str =~ /^[0-9a-fA-F]+$/
51
+ end
52
+
53
+ # Processes arbitrary input formatted as a string
54
+ def processString(str)
55
+ if isHash?(str) then
56
+ return { :type => 'phrase', :result => phraseForHash(str) }
57
+ else
58
+ terms = str.split(' ')
59
+ return { :type => 'hash', :result => hashForPhrase(terms) } if phraseLength == terms.length
60
+ return { :type => 'unreadable' }
61
+ end
62
+ end
63
+
64
+ # Processes arbitrary input formatted as an array
65
+ def processArray(arr)
66
+ return processString(arr[0]) if arr.length == 1
67
+ return { :type => 'hash', :result => hashForPhrase(arr) } if arr.length == phraseLength
68
+ return { :type => 'error' }
69
+ end
70
+
71
+ # Process an arbitrary string or array, and guess its meaning
72
+ # Returns a hash
73
+ # :result = hash of phrase (if :type == 'hash'), phrase for hash (if :type == 'phrase'), or undefined (otherwise)
74
+ # :type = 'hash', 'phrase', 'error'
75
+ def process(input)
76
+ r = processString(input) if input.is_a?(String)
77
+ r = processArray(input) if input.is_a?(Array)
78
+ r[:type] = 'error' if !r.has_key?(:result) || r[:result].nil?
79
+ r.delete(:result) if r[:type] == 'error'
80
+
81
+ return r
82
+ end
83
+
84
+ # Renders a result from process() as a string
85
+ def renderResult(result)
86
+ has_keys = result.has_key?(:type) and result.has_key?(:result)
87
+ return "Error processing input" if !has_keys or result[:type] == 'error' or result[:type].nil?
88
+ return result[:result] if result[:result].is_a?(String)
89
+ return result[:result].join(" ") if result[:result].is_a?(Array)
90
+
91
+ return "Error processing result"
92
+ end
93
+
94
+ # Convert a phrase into a hash, e.g. "disobligingly hypnotized grizzly" -> "0123abc"
95
+ # Returns nil if the phrase cannot be converted
96
+ # Phrase can be supplied as a string or array. If string, then words must be separated by whitespace.
97
+ def hashForPhrase(phrase)
98
+ # Suppose we have n+1 dictionaries in our nomenclature.
99
+ # Let L_i be the length of the nth dictionary for 0 <= i <= n.
100
+ # Let W_i be the ith word in the phrase
101
+ # Let K_i be the index of W_i in the ith dictionary, zero-based (i.e. 0 <= K_i < L_i)
102
+ # Our hash is:
103
+ # H = K0 + L0 K1 + L0 L1 K2 + ... + L0 L1 ... L_(n-1) K_n
104
+ #
105
+ # Represent this integer in hexadecimal to get a hash string.
106
+
107
+ weight = 1
108
+ hash = 0
109
+
110
+ phrase = phrase.gsub(/\s+/m, ' ').strip.split(' ') if phrase.is_a?(String)
111
+
112
+ # If the phrase doesn't have the same number of words as our nomenclature requires, we can't convert
113
+ if phrase.length != @dictionaries.length then
114
+ return nil
115
+ end
116
+
117
+ phrase.length.times do |i|
118
+ word = phrase[i]
119
+ dict = @dictionaries[i]
120
+ lineNumber = dict.index(word)
121
+ if lineNumber.nil? then
122
+ return nil
123
+ end
124
+
125
+ hash += lineNumber*weight
126
+ weight *= dict.length
127
+ end
128
+
129
+ # Render the hash as a 7-digit hex string (suitable for git)
130
+ "%07x" % hash
131
+ end
132
+
133
+ # Convert a hash into a phrase, e.g. "abc4321" -> "rightward succeeded seal"
134
+ # Hash can be supplied as an integer, or hexadecimal string.
135
+ #
136
+ # Returns nil if the hash cannot be converted
137
+ def phraseForHash(hash)
138
+ # As noted in hashForPhrase, our hash is just an integer index, of the form
139
+ # H = K0 + L0 K1 + L0 L1 K2 + ... + L0 L1 ... L_(n-1) K_n
140
+ #
141
+ # The key insight in reversing this is to realize that if we write,
142
+ # c_0 = 1, c_n = L0 L1 ... L_(n-1)
143
+ # s_n = c_n K_n
144
+ # S_n = s0 + s1 + ... + s_n,
145
+ # then we must have,
146
+ # c_(n+1) > S_n. (see proof below)
147
+ #
148
+ # So if we consider
149
+ # H = S_n = c_n K_n + S_(n-1)
150
+ # then
151
+ # S_n / c_n = K_n + S_(n-1) / c_n
152
+ # We know that 0 <= S_(n-1) / c_n < 1, so
153
+ # int(S_n/c_n) = K_n
154
+ #
155
+ # We can use this information to recurse:
156
+ # s_n = c_n K_n = c_n * int(S_n/c_n)
157
+ # S_n - s_n = S_(n-1),
158
+
159
+ begin
160
+ hash = "0x" + hash if hash.is_a?(String) and !hash.start_with?("0x")
161
+ weight = @dictionaries.inject(1) { |x, dict| dict.length * x } # L0 L1 L2 ... L_n
162
+ sum = Integer(hash) % weight
163
+ lines = [ 0 ] * @dictionaries.length # We fill from the end backwards, so allocate the total size up front
164
+
165
+ (@dictionaries.length-1).downto(0) do |n|
166
+ weight /= @dictionaries[n].length # c_n = L0 L1 .. L_(n-1)
167
+ lines[n] = (sum / weight).to_i # K_n = int(S_n / c_n)
168
+ sum -= weight * lines[n] # S_(n-1) = S_n - c_n K_n
169
+ end
170
+ rescue
171
+ return nil
172
+ end
173
+
174
+ # Proof of c_(n+1) > S_n
175
+ #
176
+ # The following is an inductive proof.
177
+ # Base case: (c1 > S0)
178
+ # c1 > S0 <=> L0 > K0, which we know by definition (0 <= K_i < L_i)
179
+ #
180
+ # Inductive step: (c_n > S_(n-1) => c_(n+1) > S_n)
181
+ # Recall that,
182
+ # c_n = L0 L1 ... L_(n-1)
183
+ # Notice that (L_n - K_n) >= 1, so
184
+ # c_n < L0 L1 ... L_(n-1) * (L_n - K_n) = c_(n+1) - s_n
185
+ # So,
186
+ # c_n > S_(n-1)
187
+ # => c_(n+1) - s_n > S_(n-1)
188
+ # => c_(n+1) > S_(n-1) + s_n = S_n
189
+ # Therefore, c_n > S_(n-1) => c_(n+1) > S_n.
190
+ # Since we have c1 > S0,
191
+ # c_(n+1) > S_n for all n > 0.
192
+
193
+ phrase = []
194
+ @dictionaries.length.times do |i|
195
+ phrase.push @dictionaries[i][lines[i]].strip
196
+ end
197
+
198
+ phrase
199
+ end
200
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jebediah
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jonas Acres
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A Gem to convert git hashes to memorable names, and vice versa
14
+ email: jonas@becuddle.com
15
+ executables:
16
+ - jeb
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - dictionaries/adverbs.txt
21
+ - dictionaries/animals.txt
22
+ - dictionaries/verbs.txt
23
+ - lib/jebediah.rb
24
+ - bin/jeb
25
+ homepage: http://github.com/jonasacres/jebediah
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.0.3
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Converts hashes to names, and names to hashes
49
+ test_files: []