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.
@@ -1,148 +1,91 @@
1
- require 'date'
2
- require 'net/http'
3
-
4
- require 'playwhe/version'
1
+ require "date"
5
2
 
6
3
  module PlayWhe
7
- # The day that Play Whe had its first draw; its start date
8
- BIRTHDAY = Date.parse('4th July 1994')
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 day that Play Whe changed to having 3 draws per day
11
- THREE_DRAWS_DAY = Date.parse('21st November 2011')
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
- HOST = 'nlcb.co.tt'
14
- QUERY_PATH = 'search/pwq/countdateCash.php'
15
- QUERY_URL = URI("http://#{HOST}/#{QUERY_PATH}")
13
+ # The lowest Play Whe mark
14
+ LOWEST_MARK = 1
16
15
 
17
- # the lowest and highest Play Whe marks
18
- LOWEST_MARK = 1
16
+ # The highest Play Whe mark
19
17
  HIGHEST_MARK = 36
20
18
 
21
- # marks associated with their standard spirit
19
+ # The spirit name associated with each mark
22
20
  SPIRITS = {
23
- 1 => 'centipede',
24
- 2 => 'old lady',
25
- 3 => 'carriage',
26
- 4 => 'dead man',
27
- 5 => 'parson man',
28
- 6 => 'belly',
29
- 7 => 'hog',
30
- 8 => 'tiger',
31
- 9 => 'cattle',
32
- 10 => 'monkey',
33
- 11 => 'corbeau',
34
- 12 => 'king',
35
- 13 => 'crapaud',
36
- 14 => 'money',
37
- 15 => 'sick woman',
38
- 16 => 'jamette',
39
- 17 => 'pigeon',
40
- 18 => 'water boat',
41
- 19 => 'horse',
42
- 20 => 'dog',
43
- 21 => 'mouth',
44
- 22 => 'rat',
45
- 23 => 'house',
46
- 24 => 'queen',
47
- 25 => 'morocoy',
48
- 26 => 'fowl',
49
- 27 => 'little snake',
50
- 28 => 'red fish',
51
- 29 => 'opium man',
52
- 30 => 'house cat',
53
- 31 => 'parson wife',
54
- 32 => 'shrimps',
55
- 33 => 'spider',
56
- 34 => 'blind man',
57
- 35 => 'big snake',
58
- 36 => 'donkey'
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
- def self.results_for_month(year, month)
62
- if date_in_range?(year, month)
63
- params = { 'year' => (year%100).to_s.rjust(2, '0'),
64
- 'month' => Date::ABBR_MONTHNAMES[month] }
65
-
66
- begin
67
- response = Net::HTTP.post_form(QUERY_URL, params)
68
- rescue
69
- raise ServiceUnavailable
70
- end
71
-
72
- case response
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
- unless skip
93
- begin
94
- flag = :ok
95
- results = results_for_month(year, month)
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
- break unless continue
110
- end
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
@@ -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