gold_mine 1.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.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +27 -0
- data/LICENSE +4 -0
- data/README.md +87 -0
- data/fortunes/fortunes +9711 -0
- data/fortunes/fortunes.dat +0 -0
- data/gold_mine.gemspec +23 -0
- data/lib/gold_mine.rb +18 -0
- data/lib/gold_mine/db.rb +75 -0
- data/lib/gold_mine/fortune.rb +48 -0
- data/lib/gold_mine/idb.rb +19 -0
- data/lib/gold_mine/index_reader.rb +80 -0
- data/lib/gold_mine/index_writer.rb +196 -0
- data/lib/gold_mine/version.rb +3 -0
- data/spec/gold_mine/db_spec.rb +72 -0
- data/spec/gold_mine/fortune_spec.rb +68 -0
- data/spec/gold_mine/index_reader.rb +63 -0
- data/spec/gold_mine/index_writer_spec.rb +202 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/temp_file.rb +25 -0
- metadata +115 -0
Binary file
|
data/gold_mine.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "gold_mine/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gold_mine"
|
8
|
+
spec.version = GoldMine::VERSION
|
9
|
+
spec.authors = ['Marcin "Archetylator" Syngajewski']
|
10
|
+
spec.email = ["archetelecynacja@gmail.com"]
|
11
|
+
spec.summary = %q{A simple, fortune cookie library for Ruby}
|
12
|
+
spec.homepage = "http://github.com/Archetylator/goldmine"
|
13
|
+
spec.license = "Beerware"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
17
|
+
spec.require_paths = ["lib", "fortunes"]
|
18
|
+
|
19
|
+
spec.add_dependency "bitswitch", "~> 1.1.4"
|
20
|
+
|
21
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
end
|
data/lib/gold_mine.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Loads gems
|
2
|
+
%w{bitswitch pathname benchmark}.each { |x| require x }
|
3
|
+
|
4
|
+
# Loads library files
|
5
|
+
%w{db idb fortune index_reader index_writer version}.each do |x|
|
6
|
+
require_relative "gold_mine/#{x}"
|
7
|
+
end
|
8
|
+
|
9
|
+
module GoldMine
|
10
|
+
def self.base_dir
|
11
|
+
Pathname.new(File.expand_path("../../", __FILE__))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.default_db_path
|
15
|
+
base_dir.join("fortunes/fortunes").to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
data/lib/gold_mine/db.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module GoldMine
|
2
|
+
|
3
|
+
# Database of fortunes.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# db = DB.new(path: "/home/user/fortunes")
|
7
|
+
# db.random # => random fortune
|
8
|
+
#
|
9
|
+
# Options:
|
10
|
+
# [:+path+]
|
11
|
+
# The path of database.
|
12
|
+
#
|
13
|
+
# [:+comments+]
|
14
|
+
# Pass true if allow comments is needed.
|
15
|
+
#
|
16
|
+
# [:+delim+]
|
17
|
+
# The character which is used as delimiter in a database.
|
18
|
+
#
|
19
|
+
class DB
|
20
|
+
def self.default_options
|
21
|
+
@default_options ||= {
|
22
|
+
delim: "%"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :path, :options
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
@path = options.fetch(:path, GoldMine.default_db_path)
|
30
|
+
@options = self.class.default_options.merge(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def random
|
34
|
+
fortunes.sample
|
35
|
+
end
|
36
|
+
|
37
|
+
def fortunes
|
38
|
+
@fortunes ||= read_fortunes
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def find_fortune(index)
|
44
|
+
read_fortunes(offset: index, size: 1).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_fortunes(options = {})
|
48
|
+
offset = options.fetch(:offset, 0)
|
49
|
+
max_size = options.fetch(:size, Float::INFINITY)
|
50
|
+
fortunes, text = [], ""
|
51
|
+
|
52
|
+
File.open(@path, "r") do |file|
|
53
|
+
file.seek(offset)
|
54
|
+
file.each_line do |line|
|
55
|
+
break if fortunes.size == max_size
|
56
|
+
|
57
|
+
if @options[:comments]
|
58
|
+
next if line[/^#{@options[:delim]*2}/]
|
59
|
+
end
|
60
|
+
|
61
|
+
if line[/^#{@options[:delim]}/] || file.eof?
|
62
|
+
text << line if file.eof?
|
63
|
+
fortunes << Fortune.new(text) unless text.chomp.empty?
|
64
|
+
text.clear
|
65
|
+
next
|
66
|
+
end
|
67
|
+
|
68
|
+
text << line
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
fortunes
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module GoldMine
|
2
|
+
|
3
|
+
# Represents a single fortune.
|
4
|
+
#
|
5
|
+
# First argument is an entire text of fortune.
|
6
|
+
# From that text a content and an attribution are extracted.
|
7
|
+
#
|
8
|
+
# Fortune.new <<-EOF
|
9
|
+
# "Calvin Coolidge looks as if he had been weaned on a pickle."
|
10
|
+
# ― Alice Roosevelt Longworth
|
11
|
+
# EOF
|
12
|
+
#
|
13
|
+
# Returns a fortune where content attribute is equal to "Calvin Coolidge
|
14
|
+
# looks as if he had been weaned on a pickle." and attribution attribute
|
15
|
+
# is equal to "Alice Roosevelt Longworth".
|
16
|
+
#
|
17
|
+
class Fortune
|
18
|
+
ATTRB_RGXP = /^(\s*(―|--)|(―|--))\s*(?<attrb>.*)/
|
19
|
+
|
20
|
+
def initialize(content = "")
|
21
|
+
matches = content.match(ATTRB_RGXP)
|
22
|
+
|
23
|
+
@content = content.chomp.sub(ATTRB_RGXP, "")
|
24
|
+
@attribution = matches && matches[:attrb]
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :content, :attribution
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
if @content && !@content.empty? && @attribution
|
31
|
+
<<-FORMAT.gsub(/^ {10}/, "")
|
32
|
+
|
33
|
+
#{@content}
|
34
|
+
― #{@attribution}
|
35
|
+
|
36
|
+
FORMAT
|
37
|
+
elsif @content && !@content.empty? && !@attribution
|
38
|
+
<<-FORMAT.gsub(/^ {10}/, "")
|
39
|
+
|
40
|
+
#{@content}
|
41
|
+
|
42
|
+
FORMAT
|
43
|
+
else
|
44
|
+
""
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module GoldMine
|
2
|
+
|
3
|
+
# Database with a related index file.
|
4
|
+
#
|
5
|
+
class IDB < DB
|
6
|
+
attr_reader :index_reader
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
super
|
10
|
+
|
11
|
+
@index_reader = IndexReader.new("#{@path}.dat")
|
12
|
+
@options = @index_reader.options
|
13
|
+
end
|
14
|
+
|
15
|
+
def random
|
16
|
+
find_fortune(@index_reader.random_pointer)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module GoldMine
|
2
|
+
|
3
|
+
# Reads an index for fortunes database. The index is a binary
|
4
|
+
# file which contains a header and pointers.
|
5
|
+
#
|
6
|
+
# The header stores statistical information and instruction
|
7
|
+
# specifying how to read the file. Pointer indicates an initial
|
8
|
+
# position for the related fortune.
|
9
|
+
#
|
10
|
+
class IndexReader
|
11
|
+
HEADER_SIZE = 24
|
12
|
+
POINTER_SIZE = 4
|
13
|
+
|
14
|
+
def initialize(path)
|
15
|
+
@path = path
|
16
|
+
|
17
|
+
header = header_fields
|
18
|
+
@version = header[0]
|
19
|
+
@numstr = header[1]
|
20
|
+
@longlen = header[2]
|
21
|
+
@shortlen = header[3]
|
22
|
+
@flags = header[4].to_switch
|
23
|
+
@delim = header[5].chr
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :path, :numstr, :longlen, :shortlen, :version, :flags, :delim
|
27
|
+
|
28
|
+
# Returns a hash with selected header fields.
|
29
|
+
#
|
30
|
+
def options
|
31
|
+
{
|
32
|
+
version: @version,
|
33
|
+
delim: @delim,
|
34
|
+
randomized: @flags[0],
|
35
|
+
ordered: @flags[1],
|
36
|
+
rotated: @flags[2],
|
37
|
+
comments: @flags[3]
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a header.
|
42
|
+
#
|
43
|
+
# The header consists of six 32-bit unsigned integers.
|
44
|
+
# Integers are stored in big-endian byte order.
|
45
|
+
#
|
46
|
+
# The order and meaning of the fields are as follows:
|
47
|
+
#
|
48
|
+
# [+version+] version number
|
49
|
+
# [+numstr+] number of pointers
|
50
|
+
# [+longlen+] size of longest fortune
|
51
|
+
# [+shortlen+] size of shortest fortune
|
52
|
+
# [+flags+] stores multiple booleans (bit-field)
|
53
|
+
# [1] randomize order
|
54
|
+
# [2] sorting in alphabetical order
|
55
|
+
# [4] Caesar encryption
|
56
|
+
# [8] allow comments
|
57
|
+
# [+delim+] 8-bit unsigned integer packed to 32-bit
|
58
|
+
# which represents a delimeter character
|
59
|
+
#
|
60
|
+
def header_fields
|
61
|
+
IO.binread(@path, HEADER_SIZE, 0).unpack("N5C1")
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns all pointers.
|
65
|
+
#
|
66
|
+
def get_pointers
|
67
|
+
IO.binread(@path, @numstr * POINTER_SIZE, HEADER_SIZE).unpack("N*")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a pointer from a certain position.
|
71
|
+
#
|
72
|
+
def get_pointer_at(index)
|
73
|
+
IO.binread(@path, POINTER_SIZE, HEADER_SIZE + POINTER_SIZE * index).unpack("N").first
|
74
|
+
end
|
75
|
+
|
76
|
+
def random_pointer
|
77
|
+
get_pointer_at rand(@numstr)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module GoldMine
|
2
|
+
|
3
|
+
# Creates a file which consists of a table describing
|
4
|
+
# the file, a table of pointers and an end of file position
|
5
|
+
# of related database. This file is called an index.
|
6
|
+
#
|
7
|
+
# The index allows fast access to fortunes.
|
8
|
+
# Provides additional info about a database e.g
|
9
|
+
# gives information if fortunes are ordered or encrypted.
|
10
|
+
#
|
11
|
+
# First argument is a path of fortune database.
|
12
|
+
# The second is an optional hash of options.
|
13
|
+
#
|
14
|
+
# Options:
|
15
|
+
# [:+index_path+]
|
16
|
+
# The path under which the file will be saved.
|
17
|
+
#
|
18
|
+
# [:+version+]
|
19
|
+
# The version of writer.
|
20
|
+
#
|
21
|
+
# [:+delim+]
|
22
|
+
# The character which is used as delimiter in a database.
|
23
|
+
#
|
24
|
+
# [:+randomized+]
|
25
|
+
# Passing true will shuffle pointers.
|
26
|
+
#
|
27
|
+
# [:+ordered+]
|
28
|
+
# When enabled sorts pointers in an alphabetical order.
|
29
|
+
#
|
30
|
+
# [:+rotated+]
|
31
|
+
# Pass true if pointers should be encrypted (Caesar cipher).
|
32
|
+
#
|
33
|
+
# [:+comments+]
|
34
|
+
# If true allows comments in database.
|
35
|
+
#
|
36
|
+
class IndexWriter
|
37
|
+
def self.default_options
|
38
|
+
@default_options ||= {
|
39
|
+
version: 2,
|
40
|
+
delim: "%",
|
41
|
+
randomized: false,
|
42
|
+
ordered: false,
|
43
|
+
rotated: false,
|
44
|
+
comments: false
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(path, options = {})
|
49
|
+
@path = path
|
50
|
+
|
51
|
+
options = self.class.default_options.merge(options)
|
52
|
+
|
53
|
+
@options = options
|
54
|
+
@longlen = 0
|
55
|
+
@numstr = 0
|
56
|
+
@eof = 0
|
57
|
+
|
58
|
+
# It's very important to keep it high.
|
59
|
+
# Otherwise in some cases shortest
|
60
|
+
# string will not be assigned.
|
61
|
+
#
|
62
|
+
@shortlen = 99999
|
63
|
+
|
64
|
+
@index_path = options[:index_path]
|
65
|
+
@version = options[:version]
|
66
|
+
@delim = options[:delim]
|
67
|
+
|
68
|
+
# An arrangement excludes a randomness.
|
69
|
+
#
|
70
|
+
options[:randomized] = false if options[:ordered]
|
71
|
+
|
72
|
+
@flags = BitSwitch.new({
|
73
|
+
randomized: options[:randomized],
|
74
|
+
ordered: options[:ordered],
|
75
|
+
rotated: options[:rotated],
|
76
|
+
comments: options[:comments]
|
77
|
+
})
|
78
|
+
|
79
|
+
@pointers = load_pointers
|
80
|
+
@numstr = @pointers.size
|
81
|
+
|
82
|
+
order_pointers! if @flags[:ordered]
|
83
|
+
shuffle_pointers! if @flags[:randomized]
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :version, :delim, :flags, :pointers, :longlen, :shortlen,
|
87
|
+
:numstr
|
88
|
+
|
89
|
+
# When a path for index is not defined
|
90
|
+
# returns identical path to the database, but
|
91
|
+
# adds .dat extension at the end.
|
92
|
+
#
|
93
|
+
def index_path
|
94
|
+
@index_path || "#{@path}.dat"
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_pointers
|
98
|
+
dregex = /^#{@delim}/
|
99
|
+
cregex = /^#{@delim*2}/
|
100
|
+
fregex = /[a-zA-Z0-9]/
|
101
|
+
pointers = []
|
102
|
+
length = 0
|
103
|
+
offset = 0
|
104
|
+
fchar = ""
|
105
|
+
new_string = true
|
106
|
+
|
107
|
+
File.open(@path) do |file|
|
108
|
+
file.each_with_index do |line, index|
|
109
|
+
if @flags[:comments]
|
110
|
+
next if line[cregex]
|
111
|
+
end
|
112
|
+
|
113
|
+
if new_string && !line[dregex]
|
114
|
+
fchar = line[fregex][0] if line[fregex]
|
115
|
+
offset = (file.pos - line.size)
|
116
|
+
new_string = false
|
117
|
+
end
|
118
|
+
|
119
|
+
if line[dregex] || file.eof?
|
120
|
+
if file.eof?
|
121
|
+
@eof = file.pos
|
122
|
+
length += line.size
|
123
|
+
end
|
124
|
+
|
125
|
+
unless length.zero?
|
126
|
+
pointers << [offset, fchar]
|
127
|
+
|
128
|
+
@longlen = length if length > @longlen
|
129
|
+
@shortlen = length if length < @shortlen
|
130
|
+
|
131
|
+
length = 0
|
132
|
+
new_string = true
|
133
|
+
end
|
134
|
+
|
135
|
+
next
|
136
|
+
end
|
137
|
+
|
138
|
+
length += line.size
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
pointers
|
143
|
+
end
|
144
|
+
|
145
|
+
def order_pointers!
|
146
|
+
@pointers.sort! { |x,y| [x[1], x[0]] <=> [y[1], y[0]] }
|
147
|
+
end
|
148
|
+
|
149
|
+
def shuffle_pointers!
|
150
|
+
@pointers = Hash[@pointers.to_a.shuffle]
|
151
|
+
end
|
152
|
+
|
153
|
+
def write
|
154
|
+
File.write(index_path, packed_header << packed_pointers << packed_eof)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns an array of pointers.
|
158
|
+
#
|
159
|
+
# @pointers is an array where each element
|
160
|
+
# is an array. The array contains an offset at first
|
161
|
+
# and a first character of fortune as second.
|
162
|
+
# Described method extracts offsets.
|
163
|
+
#
|
164
|
+
def offsets
|
165
|
+
@pointers.map { |p| p.first }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Packs the pointers as 32-bit unsigned
|
169
|
+
# integers in big-endian byte order.
|
170
|
+
#
|
171
|
+
def packed_pointers
|
172
|
+
offsets.pack("N*")
|
173
|
+
end
|
174
|
+
|
175
|
+
# Packs a database end of file position as 32-bit
|
176
|
+
# unsigned integer in big-endian byte order.
|
177
|
+
#
|
178
|
+
def packed_eof
|
179
|
+
[@eof].pack("N")
|
180
|
+
end
|
181
|
+
|
182
|
+
# Packs the header fields as 32-bit unsigned
|
183
|
+
# unsigned integers in big-endian byte order.
|
184
|
+
#
|
185
|
+
def packed_header
|
186
|
+
[
|
187
|
+
@version,
|
188
|
+
@numstr,
|
189
|
+
@longlen,
|
190
|
+
@shortlen,
|
191
|
+
@flags.to_i,
|
192
|
+
@delim.ord << 24 # bitpacked to 32-bit integer
|
193
|
+
].pack("N6")
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|