jebediah 1.0.1

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/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: []