playwhe 0.1.0 → 0.2.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 +1 -16
- data/Gemfile +1 -2
- data/LICENSE +17 -18
- data/README.md +98 -89
- data/Rakefile +9 -2
- data/bin/playwhe +99 -74
- data/lib/playwhe.rb +76 -133
- data/lib/playwhe/fetcher.rb +25 -0
- data/lib/playwhe/get.rb +214 -0
- data/lib/playwhe/http.rb +39 -0
- data/lib/playwhe/parser.rb +15 -0
- data/lib/playwhe/result.rb +119 -0
- data/lib/playwhe/settings.rb +15 -0
- data/lib/playwhe/util.rb +11 -0
- data/lib/playwhe/version.rb +1 -1
- data/playwhe.gemspec +32 -24
- data/test/playwhe/fetcher_test.rb +72 -0
- data/test/playwhe/get_test.rb +187 -0
- data/test/playwhe/http/adapter_test.rb +46 -0
- data/test/playwhe/http/response_test.rb +39 -0
- data/test/playwhe/parser_test.rb +34 -0
- data/test/playwhe/result_test.rb +158 -0
- data/test/playwhe/settings_test.rb +13 -0
- data/test/playwhe/util_test.rb +37 -0
- data/test/test_helper.rb +6 -0
- metadata +97 -45
- data/data/playwhe.db +0 -0
- data/lib/playwhe/storage.rb +0 -109
- data/lib/playwhe/storage/models.rb +0 -92
data/lib/playwhe.rb
CHANGED
@@ -1,148 +1,91 @@
|
|
1
|
-
require
|
2
|
-
require 'net/http'
|
3
|
-
|
4
|
-
require 'playwhe/version'
|
1
|
+
require "date"
|
5
2
|
|
6
3
|
module PlayWhe
|
7
|
-
#
|
8
|
-
BIRTHDAY = Date.
|
4
|
+
# Play Whe's birthday, July 4th, 1994
|
5
|
+
BIRTHDAY = Date.new(1994, 7, 4)
|
6
|
+
|
7
|
+
# The date that Play Whe changed to having 3 draws per day
|
8
|
+
THREE_DRAWS_DATE = Date.new(2011, 11, 21) # November 21st, 2011
|
9
9
|
|
10
|
-
# The
|
11
|
-
|
10
|
+
# The date that Play Whe changed to having 4 draws per day
|
11
|
+
FOUR_DRAWS_DATE = Date.new(2015, 7, 6) # July 6th, 2015
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
QUERY_URL = URI("http://#{HOST}/#{QUERY_PATH}")
|
13
|
+
# The lowest Play Whe mark
|
14
|
+
LOWEST_MARK = 1
|
16
15
|
|
17
|
-
#
|
18
|
-
LOWEST_MARK = 1
|
16
|
+
# The highest Play Whe mark
|
19
17
|
HIGHEST_MARK = 36
|
20
18
|
|
21
|
-
#
|
19
|
+
# The spirit name associated with each mark
|
22
20
|
SPIRITS = {
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
10 =>
|
33
|
-
11 =>
|
34
|
-
12 =>
|
35
|
-
13 =>
|
36
|
-
14 =>
|
37
|
-
15 =>
|
38
|
-
16 =>
|
39
|
-
17 =>
|
40
|
-
18 =>
|
41
|
-
19 =>
|
42
|
-
20 =>
|
43
|
-
21 =>
|
44
|
-
22 =>
|
45
|
-
23 =>
|
46
|
-
24 =>
|
47
|
-
25 =>
|
48
|
-
26 =>
|
49
|
-
27 =>
|
50
|
-
28 =>
|
51
|
-
29 =>
|
52
|
-
30 =>
|
53
|
-
31 =>
|
54
|
-
32 =>
|
55
|
-
33 =>
|
56
|
-
34 =>
|
57
|
-
35 =>
|
58
|
-
36 =>
|
21
|
+
1 => "centipede",
|
22
|
+
2 => "old lady",
|
23
|
+
3 => "carriage",
|
24
|
+
4 => "dead man",
|
25
|
+
5 => "parson man",
|
26
|
+
6 => "belly",
|
27
|
+
7 => "hog",
|
28
|
+
8 => "tiger",
|
29
|
+
9 => "cattle",
|
30
|
+
10 => "monkey",
|
31
|
+
11 => "corbeau",
|
32
|
+
12 => "king",
|
33
|
+
13 => "crapaud",
|
34
|
+
14 => "money",
|
35
|
+
15 => "sick woman",
|
36
|
+
16 => "jamette",
|
37
|
+
17 => "pigeon",
|
38
|
+
18 => "water boat",
|
39
|
+
19 => "horse",
|
40
|
+
20 => "dog",
|
41
|
+
21 => "mouth",
|
42
|
+
22 => "rat",
|
43
|
+
23 => "house",
|
44
|
+
24 => "queen",
|
45
|
+
25 => "morocoy",
|
46
|
+
26 => "fowl",
|
47
|
+
27 => "little snake",
|
48
|
+
28 => "red fish",
|
49
|
+
29 => "opium man",
|
50
|
+
30 => "house cat",
|
51
|
+
31 => "parson wife",
|
52
|
+
32 => "shrimps",
|
53
|
+
33 => "spider",
|
54
|
+
34 => "blind man",
|
55
|
+
35 => "big snake",
|
56
|
+
36 => "donkey"
|
59
57
|
}
|
60
58
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
when Net::HTTPOK
|
74
|
-
return parse_body(year, params['year'], month, params['month'], response.body)
|
75
|
-
else
|
76
|
-
raise ServiceUnavailable
|
77
|
-
end
|
78
|
-
else
|
79
|
-
# obviously we won't have any results
|
80
|
-
return []
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.results_for_year(year, callbacks = {})
|
85
|
-
all_results = []
|
86
|
-
|
87
|
-
12.times do |m|
|
88
|
-
month = m + 1
|
89
|
-
|
90
|
-
skip = callbacks[:before] ? callbacks[:before].call(month) : false
|
59
|
+
# The short-hand names for the time of day Play Whe results are drawn,
|
60
|
+
# ordered by earliest to latest
|
61
|
+
PERIODS = %w(EM AM AN PM)
|
62
|
+
|
63
|
+
# The times of day, represented as seconds past midnight, that Play Whe
|
64
|
+
# results are drawn
|
65
|
+
DRAW_TIMES = {
|
66
|
+
"EM" => 10 * 3600, # 10:00 AM
|
67
|
+
"AM" => 13 * 3600, # 1:00 PM
|
68
|
+
"AN" => 16 * 3600, # 4:00 PM
|
69
|
+
"PM" => 18 * 3600 + 1800 # 6:30 PM
|
70
|
+
}
|
91
71
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
rescue ServiceUnavailable
|
97
|
-
flag = :error
|
98
|
-
results = []
|
99
|
-
ensure
|
100
|
-
begin
|
101
|
-
continue = callbacks[:after] ? callbacks[:after].call(month, flag, results) : true
|
102
|
-
rescue
|
103
|
-
raise
|
104
|
-
ensure
|
105
|
-
all_results += results if results
|
106
|
-
end
|
107
|
-
end
|
72
|
+
class Error < StandardError; end
|
73
|
+
class NetworkError < Error; end
|
74
|
+
class BadResponseError < NetworkError
|
75
|
+
attr_reader :response
|
108
76
|
|
109
|
-
|
110
|
-
|
77
|
+
def initialize(response)
|
78
|
+
@response = response
|
79
|
+
super("status: #{response.status}, body: #{response.body}")
|
111
80
|
end
|
112
|
-
|
113
|
-
all_results
|
114
|
-
end
|
115
|
-
|
116
|
-
class ServiceUnavailable < Exception
|
117
|
-
end
|
118
|
-
|
119
|
-
private
|
120
|
-
|
121
|
-
def self.date_in_range?(year, month)
|
122
|
-
mday = year == BIRTHDAY.year && month == BIRTHDAY.month ? BIRTHDAY.day : 1
|
123
|
-
date = Date.new(year, month, mday)
|
124
|
-
|
125
|
-
date >= BIRTHDAY && date <= Date.today
|
126
|
-
end
|
127
|
-
|
128
|
-
def self.parse_body(year, yy, month, abbr_month, body)
|
129
|
-
# Here's the current form of the data on the page as of 27/05/2012:
|
130
|
-
#
|
131
|
-
# <date>: Draw # <draw> : <period>'s draw was <mark>
|
132
|
-
#
|
133
|
-
# where
|
134
|
-
#
|
135
|
-
# <date> : dd-Mmm-yy
|
136
|
-
# <draw> : a positive integer
|
137
|
-
# <period>: Morning | Midday | Evening
|
138
|
-
# <mark> : 1..36
|
139
|
-
results = body.scan(/(\d{2})-#{abbr_month}-#{yy}: Draw # (\d+) : (Morning|Midday|Evening)'s draw was (\d+)/)
|
140
|
-
|
141
|
-
results.collect! do |r|
|
142
|
-
{ draw: r[1].to_i,
|
143
|
-
date: Date.new(year, month, r[0].to_i),
|
144
|
-
period: {'Morning' => 1, 'Midday' => 2, 'Evening' => 3}[r[2]],
|
145
|
-
mark: r[3].to_i }
|
146
|
-
end.sort_by! { |r| r[:draw] }
|
147
81
|
end
|
148
82
|
end
|
83
|
+
|
84
|
+
require_relative "playwhe/version"
|
85
|
+
require_relative "playwhe/settings"
|
86
|
+
require_relative "playwhe/util"
|
87
|
+
require_relative "playwhe/http"
|
88
|
+
require_relative "playwhe/fetcher"
|
89
|
+
require_relative "playwhe/result"
|
90
|
+
require_relative "playwhe/parser"
|
91
|
+
require_relative "playwhe/get"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PlayWhe
|
2
|
+
class Fetcher
|
3
|
+
include Util
|
4
|
+
|
5
|
+
attr_reader :http_adapter, :url
|
6
|
+
|
7
|
+
def initialize(http_adapter, url)
|
8
|
+
@http_adapter = http_adapter
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(year:, month: nil)
|
13
|
+
y = normalize_year(year)
|
14
|
+
m = normalize_month(month)
|
15
|
+
|
16
|
+
response = http_adapter.post(url, year: y, month: m)
|
17
|
+
|
18
|
+
if response.ok?
|
19
|
+
response.body
|
20
|
+
else
|
21
|
+
raise BadResponseError, response
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/playwhe/get.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require "date"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
require "http"
|
5
|
+
|
6
|
+
module PlayWhe
|
7
|
+
class << self
|
8
|
+
def get
|
9
|
+
@_get ||= Get.new(fetcher: fetcher, parser: Parser)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def fetcher
|
15
|
+
settings = Settings.config
|
16
|
+
|
17
|
+
http = ::HTTP.timeout :global,
|
18
|
+
read: settings.http.read_timeout,
|
19
|
+
write: settings.http.write_timeout,
|
20
|
+
connect: settings.http.connect_timeout
|
21
|
+
|
22
|
+
Fetcher.new(HTTP::Adapter.new(http), settings.url)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Get
|
27
|
+
attr_reader :fetcher, :parser
|
28
|
+
|
29
|
+
def initialize(fetcher:, parser:)
|
30
|
+
@fetcher = fetcher
|
31
|
+
@parser = parser
|
32
|
+
end
|
33
|
+
|
34
|
+
VALID_ORDERS = %i(asc desc)
|
35
|
+
|
36
|
+
def results_for_year(date, order: :asc, skip_validation: false)
|
37
|
+
unless !!skip_validation
|
38
|
+
date = clean_and_validate_date(date)
|
39
|
+
order = clean_and_validate_order(order)
|
40
|
+
end
|
41
|
+
|
42
|
+
sort \
|
43
|
+
partition(
|
44
|
+
parse(fetcher.get(year: date.year)),
|
45
|
+
validation_context(date.year)
|
46
|
+
),
|
47
|
+
order
|
48
|
+
end
|
49
|
+
|
50
|
+
def results_for_month(date, order: :asc, skip_validation: false)
|
51
|
+
unless !!skip_validation
|
52
|
+
date = clean_and_validate_date(date)
|
53
|
+
order = clean_and_validate_order(order)
|
54
|
+
end
|
55
|
+
|
56
|
+
sort \
|
57
|
+
partition(
|
58
|
+
parse(fetcher.get(year: date.year, month: date.month)),
|
59
|
+
validation_context(date.year, date.month)
|
60
|
+
),
|
61
|
+
order
|
62
|
+
end
|
63
|
+
|
64
|
+
def results_for_day(date, order: :asc, skip_validation: false)
|
65
|
+
unless !!skip_validation
|
66
|
+
date = clean_and_validate_date(date)
|
67
|
+
order = clean_and_validate_order(order)
|
68
|
+
end
|
69
|
+
|
70
|
+
sort \
|
71
|
+
partition(
|
72
|
+
parse(fetcher.get(year: date.year, month: date.month)).select { |r|
|
73
|
+
r.date == date
|
74
|
+
}
|
75
|
+
),
|
76
|
+
order
|
77
|
+
end
|
78
|
+
|
79
|
+
def most_recent(limit: nil, order: :desc, skip_validation: false)
|
80
|
+
unless !!skip_validation
|
81
|
+
limit = clean_and_validate_limit(limit)
|
82
|
+
order = clean_and_validate_order(order)
|
83
|
+
end
|
84
|
+
|
85
|
+
start_year = Date.today.year
|
86
|
+
end_year = BIRTHDAY.year
|
87
|
+
delta = -1
|
88
|
+
|
89
|
+
results_for_range(start_year, end_year, delta, limit, order)
|
90
|
+
end
|
91
|
+
|
92
|
+
def least_recent(limit: nil, order: :asc, skip_validation: false)
|
93
|
+
unless !!skip_validation
|
94
|
+
limit = clean_and_validate_limit(limit)
|
95
|
+
order = clean_and_validate_order(order)
|
96
|
+
end
|
97
|
+
|
98
|
+
start_year = BIRTHDAY.year
|
99
|
+
end_year = Date.today.year
|
100
|
+
delta = 1
|
101
|
+
|
102
|
+
results_for_range(start_year, end_year, delta, limit, order)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def results_for_range(start_year, end_year, delta, limit, order)
|
108
|
+
if limit.zero?
|
109
|
+
sort \
|
110
|
+
partition(
|
111
|
+
parse(fetcher.get(year: start_year)),
|
112
|
+
validation_context(start_year)
|
113
|
+
),
|
114
|
+
order
|
115
|
+
else
|
116
|
+
year = start_year
|
117
|
+
all = []
|
118
|
+
|
119
|
+
while all.length < limit
|
120
|
+
all.concat(parse(fetcher.get(year: year)))
|
121
|
+
break if year == end_year
|
122
|
+
year += delta
|
123
|
+
end
|
124
|
+
|
125
|
+
all = sort_desc(all) if start_year > end_year
|
126
|
+
all = sort_asc(all) if start_year < end_year
|
127
|
+
|
128
|
+
sort \
|
129
|
+
partition(all[0, limit]),
|
130
|
+
order
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def sort(partitioned_results, order)
|
135
|
+
[
|
136
|
+
send(:"sort_#{order}", partitioned_results.first),
|
137
|
+
partitioned_results.last
|
138
|
+
]
|
139
|
+
end
|
140
|
+
|
141
|
+
def sort_asc(results)
|
142
|
+
default_date = Date.today
|
143
|
+
default_period_index = PERIODS.length
|
144
|
+
results.sort_by do |r|
|
145
|
+
[
|
146
|
+
r.date || default_date,
|
147
|
+
PERIODS.index(r.period) || default_period_index
|
148
|
+
]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def sort_desc(results)
|
153
|
+
sort_asc(results).reverse
|
154
|
+
end
|
155
|
+
|
156
|
+
def partition(results, context = nil)
|
157
|
+
results.partition { |r| r.is_valid?(context) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse(html_results)
|
161
|
+
parser.parse(html_results)
|
162
|
+
end
|
163
|
+
|
164
|
+
def validation_context(year, month = nil)
|
165
|
+
OpenStruct.new(year: year, month: month)
|
166
|
+
end
|
167
|
+
|
168
|
+
def clean_and_validate_date(date)
|
169
|
+
if date.nil?
|
170
|
+
raise ArgumentError, "date is required"
|
171
|
+
else
|
172
|
+
unless date.instance_of?(Date)
|
173
|
+
begin
|
174
|
+
date = Date.parse(date.to_s)
|
175
|
+
rescue ArgumentError
|
176
|
+
raise ArgumentError, "date must be a date"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
if date < BIRTHDAY || date > Date.today
|
181
|
+
raise ArgumentError, "date must be between " \
|
182
|
+
"#{format_date(BIRTHDAY)} and " \
|
183
|
+
"#{format_date(Date.today)} inclusive"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
date
|
188
|
+
end
|
189
|
+
|
190
|
+
def clean_and_validate_order(order)
|
191
|
+
order = order.to_s.to_sym
|
192
|
+
|
193
|
+
unless VALID_ORDERS.include?(order)
|
194
|
+
raise ArgumentError, "order must be one of #{VALID_ORDERS.join(', ')}"
|
195
|
+
end
|
196
|
+
|
197
|
+
order
|
198
|
+
end
|
199
|
+
|
200
|
+
def clean_and_validate_limit(limit)
|
201
|
+
limit = limit.to_i
|
202
|
+
|
203
|
+
if limit < 0
|
204
|
+
raise ArgumentError, "limit must be a non-negative integer"
|
205
|
+
end
|
206
|
+
|
207
|
+
limit
|
208
|
+
end
|
209
|
+
|
210
|
+
def format_date(date)
|
211
|
+
date.strftime("%Y-%m-%d")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|