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