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.
Binary file
@@ -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
@@ -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
+
@@ -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