jebediah 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/jeb +26 -0
- data/dictionaries/adverbs.txt +3719 -0
- data/dictionaries/animals.txt +194 -0
- data/dictionaries/verbs.txt +703 -0
- data/lib/jebediah.rb +200 -0
- metadata +49 -0
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: []
|