bbc_weather 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 917180090c503d03f9d5b0431f6d445034f54766
4
+ data.tar.gz: 4477139c89d314359f4b453400bf23d7d11f9ac6
5
+ SHA512:
6
+ metadata.gz: 935321055d6017ecc7094e31a595e29b997895a0996f7c7451fc171716d78ca07885aac5c264b82b610b9e0ecc15c0262784673e9518e17ce2ca1dc35643579d
7
+ data.tar.gz: 45beeea7cb1aa564982bddc13f7d824974b206c12ef05141ebe5e61480e8f94fa781ce24aac1b53b2e88ef7f256872037536e1c55d9554c656e57e74ac499d15
@@ -0,0 +1,105 @@
1
+ require 'nokogiri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'weather_result'
5
+
6
+ class BBCWeather
7
+ $temp_unit = "c"
8
+ $speed_unit = "mph"
9
+
10
+ def self.city(city_id)
11
+ result = {}
12
+
13
+ if city_id.is_a?(Integer) || city_id =~ /^[0-9]+$/
14
+ begin
15
+ result = get_weather_from_bbc_url("http://www.bbc.co.uk/weather/en/#{city_id}")
16
+ rescue ArgumentError => e
17
+ if e.to_s[/404/]
18
+ raise ArgumentError, "City ID: #{city_id} not found"
19
+ end
20
+ raise
21
+ end
22
+ else
23
+ # Convert string location to integer city code
24
+ city_ids = BBCWeather.get_city_id(city_id)
25
+
26
+ if city_ids.length == 0
27
+ raise ArgumentError, "City ID: '#{city_id}' could not be located"
28
+ elsif city_ids.length > 1
29
+ raise ArgumentError, "City ID: '#{city_id}' returned more than one matching city (#{city_ids}). Please refine your search term"
30
+ else
31
+ # Recursive call using integer city code
32
+ return BBCWeather.city(city_ids[0]["id"])
33
+ end
34
+ end
35
+ return result
36
+ end
37
+
38
+ def self.get_city_id(city_name)
39
+ city_name_d = city_name.downcase
40
+ city_ids = JSON.parse(Net::HTTP.get(URI("http://www.bbc.co.uk/locator/default/en-GB/autocomplete.json?search=#{city_name_d}&filter=international")))
41
+ city_id = city_ids.select {|a| a["fullName"].downcase.eql?(city_name_d) || a["fullName"].downcase.eql?("#{city_name_d}, #{city_name_d}") || a["fullName"].downcase.eql?("#{city_name_d}, #{city_name_d} city") }
42
+
43
+ city_id.empty? ? (return city_ids) : (return city_id)
44
+ end
45
+
46
+ def self.get_weather_from_bbc_url(url)
47
+ html = {}
48
+ html[:main] = Nokogiri::HTML(Net::HTTP.get(URI(url)))
49
+
50
+ if html[:main].css("title")[0].children[0].text[/not found/i]
51
+ raise ArgumentError, "The given URL returned a 404 error. Please check the city ID and try again"
52
+ end
53
+
54
+ lock = Mutex.new
55
+ connections = []
56
+ html[:main].css("div.daily-window > ul > li > a").each do |day|
57
+ connections << Thread.new {
58
+ day_url = day.attributes["data-ajax-href"].value
59
+ day_html = Nokogiri::HTML(Net::HTTP.get(URI("http://www.bbc.co.uk#{day_url}")))
60
+ lock.synchronize {
61
+ html[day_url[/[0-9]+$/].to_i] = day_html
62
+ }
63
+ }
64
+ end
65
+ connections.each {|conn| conn.join}
66
+
67
+ return WeatherResult.new(html)
68
+ end
69
+
70
+ def self.set_units(*units)
71
+ curr_units = self.units
72
+ count = {:temp => 0, :speed => 0}
73
+
74
+ units.each do |u|
75
+ if u == "c" || u == "celcius"
76
+ $temp_unit = "c"
77
+ count[:temp] += 1
78
+ elsif u == "f" || u == "fahrenheit"
79
+ $temp_unit = "f"
80
+ count[:temp] += 1
81
+ elsif u == "kph" || u == "km/h"
82
+ $speed_unit = "kph"
83
+ count[:speed] += 1
84
+ elsif u == "mph"
85
+ $speed_unit = "mph"
86
+ count[:speed] += 1
87
+ else
88
+ $temp_unit = curr_units[0]
89
+ $speed_unit = curr_units[1]
90
+ raise ArgumentError, "'#{u}' is not a recognised unit of speed/temperature. Unit must be either 'c' or 'f' (celcius or fahrenheit), or 'kph' or 'mph' (kilometers per hour or miles per hour). Units have not been changed"
91
+ end
92
+ end
93
+ if count[:temp] > 1 || count[:speed] > 1
94
+ $temp_unit = curr_units[0]
95
+ $speed_unit = curr_units[1]
96
+ raise ArgumentError, "Cannot pass in two units of the same type (i.e. #set_units('kph', 'mph')). Units have not been changed"
97
+ end
98
+
99
+ return [$temp_unit, $speed_unit]
100
+ end
101
+
102
+ def self.units
103
+ return [$temp_unit, $speed_unit]
104
+ end
105
+ end
@@ -0,0 +1,118 @@
1
+ require 'timeslot'
2
+ require 'date'
3
+
4
+ class Day
5
+ attr_accessor :date, :sunrise, :sunset, :timeslots, :nextday_timeslots
6
+
7
+ def initialize(day_html, date)
8
+ @timeslots = Array.new
9
+ nextday_timeslot_index = nil
10
+
11
+ @date = Date.parse(date)
12
+ @sunrise = day_html.css("span.sunrise")[0].children[0].text[/\d{2}:\d{2}/]
13
+ @sunset = day_html.css("span.sunset")[0].children[0].text[/\d{2}:\d{2}/]
14
+
15
+ ###
16
+ ### START LOOPS
17
+ ###
18
+ # Get times for each TimeSlot
19
+ day_html.css("table.weather tr.time > th.value").each_with_index do |times_html, i|
20
+ nextday_timeslot_index = i if times_html.attributes["class"].value.include?("next-day") && !nextday_timeslot_index
21
+
22
+ # Sort out Timeslot time
23
+ ts = TimeSlot.new
24
+ time = "#{times_html.css("span[class='hour']").text}:#{times_html.css("span[class='mins']").text}"
25
+ ts.time = DateTime.parse("#{@date}T#{time}")
26
+ ts.time += 1 if nextday_timeslot_index
27
+
28
+ # Sort out prev and next Timeslots
29
+ unless @timeslots[-1].nil?
30
+ ts.prev = @timeslots[-1]
31
+ @timeslots[-1].next = ts
32
+ end
33
+
34
+ @timeslots.push(ts)
35
+ end
36
+
37
+ # Get weather conditions for each TimeSlot
38
+ day_html.css("table.weather tr.weather-type > td img").each_with_index do |conditions_html, i|
39
+ @timeslots[i].conditions = conditions_html.attributes["title"].value
40
+ @timeslots[i].icon_url = conditions_html.attributes["src"].value
41
+ end
42
+
43
+ # Get temperature for each TimeSlot
44
+ day_html.css("table.weather tr.temperature > td span[data-unit='c']").each_with_index do |temperature_html, i|
45
+ @timeslots[i].temperature = temperature_html.children[0].text.to_i
46
+ end
47
+
48
+ # Get wind details for each TimeSlot
49
+ day_html.css("table.weather tr.windspeed > td > span.wind").each_with_index do |wind_html, i|
50
+ wind_data = wind_html.attributes["data-tooltip-mph"].value
51
+ @timeslots[i].wind_speed = wind_data[/\d+/].to_i
52
+ @timeslots[i].wind_direction = wind_data.gsub(/[^A-Z]/, "")
53
+ end
54
+
55
+ # Get humidity for each TimeSlot
56
+ day_html.css("table.weather tr.humidity > td.value").each_with_index do |humidity_html, i|
57
+ @timeslots[i].humidity = humidity_html.children[0].text[/\d+/].to_i
58
+ end
59
+
60
+ # Get visibility for each TimeSlot
61
+ day_html.css("table.weather tr.visibility > td.value abbr").each_with_index do |visibility_html, i|
62
+ @timeslots[i].visibility = visibility_html.attributes["title"].value
63
+ end
64
+
65
+ # Get pressure for each TimeSlot
66
+ day_html.css("table.weather tr.pressure > td.value").each_with_index do |pressure_html, i|
67
+ @timeslots[i].pressure = pressure_html.children[0].text[/\d+/].to_i # In Millibars
68
+ end
69
+ ###
70
+ ### END LOOPS
71
+ ###
72
+ # Shift any timeslots after midnight into the nextday_timeslots
73
+ @nextday_timeslots = @timeslots.slice!(nextday_timeslot_index, @timeslots.length-nextday_timeslot_index)
74
+ end
75
+
76
+ def high
77
+ @timeslots.max_by {|ts| ts.temperature}.temperature
78
+ end
79
+
80
+ def low
81
+ @timeslots.min_by {|ts| ts.temperature}.temperature
82
+ end
83
+
84
+ def timeslot(i)
85
+ return nil unless i.is_a?(Integer)
86
+ @timeslots[i]
87
+ end
88
+
89
+ def at(time)
90
+ if time.is_a?(DateTime) || time.is_a?(Time) || (time.is_a?(String) && time =~ /\d{2}:\d{2}/ && time[0..1].to_i >= 0 && time[0..1].to_i <= 23 && time[2..3].to_i >= 0 && time[2..3].to_i <= 59)
91
+ curr_slot = @timeslots[0]
92
+ time = "#{@date}T#{time}" if time.is_a?(String)
93
+ search_time = DateTime.parse(time.to_s).strftime("%s").to_i
94
+ curr_slot_time = curr_slot.time.strftime("%s").to_i
95
+
96
+ curr_diff = (search_time - curr_slot_time).abs
97
+
98
+ loop do
99
+ if search_time > curr_slot_time
100
+ if !curr_slot.next.nil? && (curr_slot.next.time.strftime("%s").to_i - search_time).abs < curr_diff
101
+ curr_slot = curr_slot.next
102
+ curr_diff = (search_time - curr_slot.time.strftime("%s").to_i).abs
103
+ else
104
+ return curr_slot
105
+ end
106
+ elsif search_time <= curr_slot_time
107
+ if !curr_slot.prev.nil? && ((curr_slot.prev.time-1).strftime("%s").to_i - search_time).abs < curr_diff
108
+ return curr_slot.prev
109
+ else
110
+ return curr_slot
111
+ end
112
+ end
113
+ end
114
+ else
115
+ raise ArgumentError, "Time must be in the format 'HH:MM' (00-23) i.e. '23:45' or as a DateTime/Time object"
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,22 @@
1
+ class TimeSlot
2
+ attr_accessor :time, :temperature, :humidity, :visibility, :pressure, :wind_speed, :wind_direction, :conditions, :icon_url, :next, :prev
3
+
4
+ def initialize
5
+ end
6
+
7
+ def temperature
8
+ if $temp_unit.eql?("c")
9
+ return @temperature
10
+ else
11
+ return ((@temperature * 9 / 5) + 32).round
12
+ end
13
+ end
14
+
15
+ def wind_speed
16
+ if $speed_unit.eql?("mph")
17
+ return @wind_speed
18
+ else
19
+ return (@wind_speed * 1.609344).round
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ require 'nokogiri'
2
+ require 'date'
3
+ require 'day'
4
+ require 'timeslot'
5
+
6
+ class WeatherResult
7
+ attr_reader :location, :current_time, :current_temp, :current_humidity, :days
8
+ def initialize(html)
9
+ @location = html[:main].css("span.location-name")[0].children[0].text
10
+ @current_temp = html[:main].css("div.observationsRecord span.temperature-value")[0].children[0].text.to_i
11
+ @current_humidity = html[:main].css("div.observationsRecord p.humidity > span")[0].children[0].text[/\d+/].to_i # In %
12
+
13
+ timezone = html[:main].css("div.ack > p")[1].children[0].text[/GMT[+-]\d{4}/]
14
+ @current_time = DateTime.now.new_offset(timezone).to_s
15
+
16
+ @days = []
17
+ nextday_timeslots = []
18
+
19
+ 5.times do |i|
20
+ date = html[:main].css("div.daily-window > ul > li > a")[i].attributes["data-ajax-href"].value[/\d{4}-\d{2}-\d{2}/]
21
+ day = Day.new(html[i], date)
22
+
23
+ # Transfer 'yesterdays' next_day timeslots to today and link up next and prev links
24
+ unless nextday_timeslots.empty?
25
+ nextday_timeslots[-1].next = day.timeslots[0]
26
+ day.timeslots[0].prev = nextday_timeslots[-1]
27
+ day.timeslots = nextday_timeslots.concat(day.timeslots)
28
+ end
29
+ nextday_timeslots = day.nextday_timeslots.dup
30
+ day.nextday_timeslots.clear
31
+ @days.push day
32
+ end
33
+
34
+ @days.shift if @days[0].timeslots.empty?
35
+ end
36
+
37
+ # Returns today of the city location, _not_ today of the user
38
+ # i.e. a user in Canada wants the forecast for Auckland, New Zealand..
39
+ # .today then refers to Auckland's today, which may well be one day ahead of Canada
40
+ def today
41
+ return @days[0]
42
+ end
43
+
44
+ def tomorrow
45
+ return @days[1]
46
+ end
47
+
48
+ def days_forward(i)
49
+ return nil unless i.is_a?(Integer)
50
+ return @days[i]
51
+ end
52
+
53
+ def on(day)
54
+ day = Date.parse(day) if day =~ /\d{4}-\d{2}-\d{2}/
55
+ if day.is_a?(String)
56
+ weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
57
+ day_temp = weekdays.select {|d| d.downcase.include?(day.downcase)}.first
58
+
59
+ raise ArgumentError, "'#{day}' is not a valid day" if day_temp.nil?
60
+ day = day_temp
61
+
62
+ 7.times do |i|
63
+ if (Date.today + i).strftime("%A").eql?(day)
64
+ day = Date.today + i
65
+ break
66
+ end
67
+ end
68
+ elsif day.is_a?(DateTime)
69
+ day = Date.parse(day.to_s)
70
+ end
71
+
72
+ # day var is now a Date object
73
+
74
+ 7.times do |i|
75
+ if !@days[i].nil? && @days[i].date.eql?(day)
76
+ return @days[i]
77
+ end
78
+ end
79
+ raise ArgumentError, "'#{day.to_s}' is not in the forecast range (#{@days.first.date.to_s} - #{@days.last.date.to_s})"
80
+ end
81
+
82
+ def current_temp
83
+ if $temp_unit.eql?("c")
84
+ return @current_temp
85
+ else
86
+ return ((@current_temp * 9 / 5) + 32).round
87
+ end
88
+ end
89
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bbc_weather
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamie Guthrie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ description: A simple gem to grab the BBC weather forecast for any given city
56
+ email: jamie.guthrie@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/bbc_weather.rb
62
+ - lib/day.rb
63
+ - lib/timeslot.rb
64
+ - lib/weather_result.rb
65
+ homepage: http://www.github.com/jguthrie100/bbc_weather
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.6.8
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Get the weather!
89
+ test_files: []