yweather 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/CHANGELOG +3 -0
- data/Gemfile +2 -0
- data/Guardfile +9 -0
- data/LICENSE +504 -0
- data/README.md +109 -0
- data/Rakefile +14 -0
- data/bin/yweather +30 -0
- data/lib/yweather.rb +22 -0
- data/lib/yweather/astronomy.rb +12 -0
- data/lib/yweather/atmosphere.rb +34 -0
- data/lib/yweather/client.rb +80 -0
- data/lib/yweather/condition.rb +71 -0
- data/lib/yweather/forecast.rb +22 -0
- data/lib/yweather/image.rb +19 -0
- data/lib/yweather/location.rb +15 -0
- data/lib/yweather/response.rb +87 -0
- data/lib/yweather/units.rb +20 -0
- data/lib/yweather/utils.rb +7 -0
- data/lib/yweather/version.rb +3 -0
- data/lib/yweather/wind.rb +15 -0
- data/spec/bin/yweather_spec.rb +7 -0
- data/spec/cassettes/invalid_weather_transaction.yml +44 -0
- data/spec/cassettes/invalid_woeid_conversion_transaction.yml +38 -0
- data/spec/cassettes/valid_weather_transaction.yml +66 -0
- data/spec/cassettes/valid_weather_transaction_with_zipcode_conversion.yml +101 -0
- data/spec/cassettes/valid_woeid_conversion_transaction.yml +38 -0
- data/spec/lib/yweather/astronomy_spec.rb +27 -0
- data/spec/lib/yweather/atmosphere_spec.rb +25 -0
- data/spec/lib/yweather/client_spec.rb +58 -0
- data/spec/lib/yweather/condition_spec.rb +25 -0
- data/spec/lib/yweather/forecast_spec.rb +25 -0
- data/spec/lib/yweather/image_spec.rb +25 -0
- data/spec/lib/yweather/location_spec.rb +25 -0
- data/spec/lib/yweather/response_spec.rb +49 -0
- data/spec/lib/yweather/units_spec.rb +25 -0
- data/spec/lib/yweather/wind_spec.rb +25 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/helpers/spec_parse_helpers.rb +25 -0
- data/spec/support/helpers/spec_yweather_helpers.rb +16 -0
- data/yweather.gemspec +39 -0
- metadata +283 -0
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Yweather
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
This is a rewrite of Walter Korman's <shaper@fatgoose.com>
|
6
|
+
[yahoo-weather](http://rubyforge.org/projects/yahoo-weather) gem.
|
7
|
+
|
8
|
+
The aim of this port/rewrite is to add proper specs, better test-coverage and active
|
9
|
+
maintenance. I have ported much of Walter's original intent and LICENSE, and thank him for
|
10
|
+
his contribution. My initial motivation is to send Prowl notifications to my iPad, iPhone,
|
11
|
+
iPod whenever hazardous weather is coming.
|
12
|
+
|
13
|
+
Yweather provides an object-oriented Ruby interface to the Yahoo! Weather XML RSS feed
|
14
|
+
detailed at [http://developer.yahoo.com/weather](http://developer.yahoo.com/weather).
|
15
|
+
|
16
|
+
People care a lot about the weather. This may seem ironic given they can just glance out
|
17
|
+
the window. However, we can all understand a fascination with details and forecasting.
|
18
|
+
|
19
|
+
Log the weather information to your database! Graph it to your heart's content! Write a
|
20
|
+
widget that emails the weather to your cell phone every five minutes with a link to your
|
21
|
+
friend's PayPal account to deposit money if the weather's sunny and you both bet that it
|
22
|
+
would be rainy. And the fun doesn't have to stop there.
|
23
|
+
|
24
|
+
Source code is at [http://github.com/midwire/yweather](http://github.com/midwire/yweather).
|
25
|
+
|
26
|
+
NOTE: This library was updated as of December 2009 to use a new WOEID-based lookup
|
27
|
+
interface. Yahoo has deprecated the older non-WOEID-based lookup API. The archived page
|
28
|
+
with the deprecated API details is at:
|
29
|
+
|
30
|
+
[http://developer.yahoo.com/weather/archive.html](http://developer.yahoo.com/weather/archive.html)
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
gem install yweather
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
A simple example program:
|
39
|
+
|
40
|
+
#!/usr/bin/env ruby
|
41
|
+
require "yweather"
|
42
|
+
require "colorize"
|
43
|
+
|
44
|
+
zipcode = ARGV.shift
|
45
|
+
if zipcode.nil?
|
46
|
+
puts ">>> Usage: yweather zipcode"
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
client = Yweather::Client.new
|
51
|
+
response = client.get_response_for_zipcode(zipcode)
|
52
|
+
|
53
|
+
# TODO: Use a ~/.yweather YAML file to determine format of output
|
54
|
+
|
55
|
+
print <<edoc
|
56
|
+
#{response.title.yellow}
|
57
|
+
#{response.condition.temp} degrees
|
58
|
+
#{response.condition.text}
|
59
|
+
edoc
|
60
|
+
|
61
|
+
puts "Wind:".yellow
|
62
|
+
response.wind_conditions.each_pair do |key, value|
|
63
|
+
puts " #{key}: #{value}"
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "Atmosphere:".yellow
|
67
|
+
response.atmospheric_conditions.each_pair do |key, value|
|
68
|
+
puts " #{key}: #{value}"
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
Produces output as:
|
73
|
+
|
74
|
+
Conditions for Beverly Hills, CA at 10:51 am PST
|
75
|
+
61 degrees
|
76
|
+
Fair
|
77
|
+
Wind:
|
78
|
+
chill: 61°
|
79
|
+
direction: 0°
|
80
|
+
speed: 0 mph
|
81
|
+
Atmosphere:
|
82
|
+
humidity: 23%
|
83
|
+
visibility: 10 mi
|
84
|
+
pressure: steady at 30.18in
|
85
|
+
|
86
|
+
There is a variety of detailed weather information in other attributes of the Yweather::Response object.
|
87
|
+
|
88
|
+
## License
|
89
|
+
|
90
|
+
This library is provided via the GNU LGPL license at http://www.gnu.org/licenses/lgpl.html.
|
91
|
+
|
92
|
+
## Author
|
93
|
+
|
94
|
+
Copyright 2006 - 2009, Walter Korman <shaper@fatgoose.com>,
|
95
|
+
http://lemurware.blogspot.com.
|
96
|
+
|
97
|
+
Copyright 2012, Chris Blackburn <chris you-know-what-goes-here m and-here blackburn@gmail and-here com>,
|
98
|
+
http://midwire.github.com
|
99
|
+
|
100
|
+
Thanks to Walter Korman for the initial hack.
|
101
|
+
|
102
|
+
## Notes
|
103
|
+
|
104
|
+
Pull requests are appreciated. Please use feature/branches or hotfix/branches and add specs for your changes.
|
105
|
+
|
106
|
+
## ToDo
|
107
|
+
|
108
|
+
* Use a ~/.yweather YAML file, or some other templating scheme, to determine format of output when running the binary.
|
109
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
spec_tasks = Dir['spec/*/'].map { |d| File.basename(d) }
|
5
|
+
|
6
|
+
spec_tasks.each do |folder|
|
7
|
+
RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
|
8
|
+
t.pattern = "./spec/#{folder}/**/*_spec.rb"
|
9
|
+
t.rspec_opts = %w(-fs --color)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run complete application spec suite"
|
14
|
+
task 'spec' => spec_tasks.map { |f| "spec:#{f}" }
|
data/bin/yweather
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "yweather"
|
3
|
+
require "colorize"
|
4
|
+
|
5
|
+
zipcode = ARGV.shift
|
6
|
+
if zipcode.nil?
|
7
|
+
puts ">>> Usage: yweather zipcode"
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
client = Yweather::Client.new
|
12
|
+
response = client.get_response_for_zipcode(zipcode)
|
13
|
+
|
14
|
+
# TODO: Use a ~/.yweather YAML file to determine format of output
|
15
|
+
|
16
|
+
print <<edoc
|
17
|
+
#{response.title.yellow}
|
18
|
+
#{response.condition.temp} degrees
|
19
|
+
#{response.condition.text}
|
20
|
+
edoc
|
21
|
+
|
22
|
+
puts "Wind:".yellow
|
23
|
+
response.wind_conditions.each_pair do |key, value|
|
24
|
+
puts " #{key}: #{value}"
|
25
|
+
end
|
26
|
+
|
27
|
+
puts "Atmosphere:".yellow
|
28
|
+
response.atmospheric_conditions.each_pair do |key, value|
|
29
|
+
puts " #{key}: #{value}"
|
30
|
+
end
|
data/lib/yweather.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cgi'
|
3
|
+
require 'rest-client'
|
4
|
+
require 'yql'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
require 'json'
|
7
|
+
require "yweather/utils"
|
8
|
+
require "yweather/version"
|
9
|
+
require "yweather/client"
|
10
|
+
require "yweather/response"
|
11
|
+
require "yweather/astronomy"
|
12
|
+
require "yweather/atmosphere"
|
13
|
+
require "yweather/location"
|
14
|
+
require "yweather/wind"
|
15
|
+
require "yweather/units"
|
16
|
+
require "yweather/image"
|
17
|
+
require "yweather/condition"
|
18
|
+
require "yweather/forecast"
|
19
|
+
|
20
|
+
module Yweather
|
21
|
+
class YweatherException < RuntimeError; end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Yweather
|
2
|
+
class Atmosphere
|
3
|
+
|
4
|
+
class Barometer
|
5
|
+
STEADY = 'steady'
|
6
|
+
RISING = 'rising'
|
7
|
+
FALLING = 'falling'
|
8
|
+
|
9
|
+
# lists all possible barometer constants
|
10
|
+
ALL = [ STEADY, RISING, FALLING ]
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :humidity
|
14
|
+
attr_reader :visibility
|
15
|
+
attr_reader :pressure
|
16
|
+
attr_reader :barometer
|
17
|
+
|
18
|
+
def initialize(data)
|
19
|
+
@humidity = data[:humidity].to_i
|
20
|
+
@visibility = data[:visibility].to_i
|
21
|
+
@pressure = data[:pressure].to_f
|
22
|
+
@barometer = nil
|
23
|
+
case data[:rising].to_i
|
24
|
+
when 0
|
25
|
+
@barometer = Barometer::STEADY
|
26
|
+
when 1
|
27
|
+
@barometer = Barometer::RISING
|
28
|
+
when 2
|
29
|
+
@barometer = Barometer::FALLING
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Yweather
|
2
|
+
class Client
|
3
|
+
@@BASE_URL = "http://weather.yahooapis.com/forecastrss"
|
4
|
+
attr_reader :response
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@response = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_feed_for_zipcode(zipcode, options = {})
|
11
|
+
woeid = woeid_from_zipcode(zipcode)
|
12
|
+
get_feed_for_woeid(woeid, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_feed_for_woeid(woeid, options = {})
|
16
|
+
units = options[:units] || 'f'
|
17
|
+
format = options[:format] || :json
|
18
|
+
url = build_url(woeid, units)
|
19
|
+
get(url, format)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_response_for_woeid(woeid, options = {})
|
23
|
+
units = options[:units] || 'f'
|
24
|
+
format = options[:format] || :hash
|
25
|
+
url = build_url(woeid, units)
|
26
|
+
hash = get(url, format)
|
27
|
+
@response = Response.new(woeid, nil, url, hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_response_for_zipcode(zipcode, options = {})
|
31
|
+
units = options[:units] || 'f'
|
32
|
+
format = options[:format] || :hash
|
33
|
+
woeid = woeid_from_zipcode(zipcode)
|
34
|
+
url = build_url(woeid, units)
|
35
|
+
hash = get(url, format)
|
36
|
+
@response = Response.new(woeid, zipcode, url, hash)
|
37
|
+
end
|
38
|
+
|
39
|
+
##################################################
|
40
|
+
private
|
41
|
+
|
42
|
+
def build_url(woeid, units)
|
43
|
+
"#{@@BASE_URL}?w=#{CGI.escape(woeid.to_s)}&u=#{CGI.escape(units)}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def woeid_from_zipcode(zipcode)
|
47
|
+
yql = Yql::Client.new
|
48
|
+
query = Yql::QueryBuilder.new 'geo.places'
|
49
|
+
query.select = 'woeid'
|
50
|
+
query.conditions = "text = '#{zipcode}, usa'"
|
51
|
+
yql.query = query
|
52
|
+
yql.format = 'json'
|
53
|
+
response = yql.get
|
54
|
+
h = ::HashWithIndifferentAccess.new(::JSON::parse(response.body))
|
55
|
+
if h[:query][:results][:place].is_a?(Array)
|
56
|
+
raise YweatherException, "Cannot determine WOEID from the ZIPCODE [#{zipcode}]"
|
57
|
+
return
|
58
|
+
end
|
59
|
+
h[:query][:results][:place][:woeid]
|
60
|
+
end
|
61
|
+
|
62
|
+
def get(url, format)
|
63
|
+
response = RestClient.get(url)
|
64
|
+
if response.body =~ /Error/
|
65
|
+
hash = ::HashWithIndifferentAccess.new(Hash.from_xml(response.body))
|
66
|
+
item = hash[:rss][:channel][:item]
|
67
|
+
raise YweatherException, "#{hash[:rss][:channel][:title]} :: #{item[:title]} :: #{item[:description]}"
|
68
|
+
return
|
69
|
+
end
|
70
|
+
if format == :xml
|
71
|
+
response.body
|
72
|
+
elsif format == :json
|
73
|
+
Hash.from_xml(response.body).to_json
|
74
|
+
elsif format == :hash
|
75
|
+
::HashWithIndifferentAccess.new(Hash.from_xml(response.body))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Yweather
|
2
|
+
class Condition
|
3
|
+
include Yweather::Utils
|
4
|
+
|
5
|
+
CODES = {
|
6
|
+
0 => "tornado",
|
7
|
+
1 => "tropical storm",
|
8
|
+
2 => "hurricane",
|
9
|
+
3 => "severe thunderstorms",
|
10
|
+
4 => "thunderstorms",
|
11
|
+
5 => "mixed rain and snow",
|
12
|
+
6 => "mixed rain and sleet",
|
13
|
+
7 => "mixed snow and sleet",
|
14
|
+
8 => "freezing drizzle",
|
15
|
+
9 => "drizzle",
|
16
|
+
10 => "freezing rain",
|
17
|
+
11 => "showers",
|
18
|
+
12 => "showers",
|
19
|
+
13 => "snow flurries",
|
20
|
+
14 => "light snow showers",
|
21
|
+
15 => "blowing snow",
|
22
|
+
16 => "snow",
|
23
|
+
17 => "hail",
|
24
|
+
18 => "sleet",
|
25
|
+
19 => "dust",
|
26
|
+
20 => "foggy",
|
27
|
+
21 => "haze",
|
28
|
+
22 => "smoky",
|
29
|
+
23 => "blustery",
|
30
|
+
24 => "windy",
|
31
|
+
25 => "cold",
|
32
|
+
26 => "cloudy",
|
33
|
+
27 => "mostly cloudy (night)",
|
34
|
+
28 => "mostly cloudy (day)",
|
35
|
+
29 => "partly cloudy (night)",
|
36
|
+
30 => "partly cloudy (day)",
|
37
|
+
31 => "clear (night)",
|
38
|
+
32 => "sunny",
|
39
|
+
33 => "fair (night)",
|
40
|
+
34 => "fair (day)",
|
41
|
+
35 => "mixed rain and hail",
|
42
|
+
36 => "hot",
|
43
|
+
37 => "isolated thunderstorms",
|
44
|
+
38 => "scattered thunderstorms",
|
45
|
+
39 => "scattered thunderstorms",
|
46
|
+
40 => "scattered showers",
|
47
|
+
41 => "heavy snow",
|
48
|
+
42 => "scattered snow showers",
|
49
|
+
43 => "heavy snow",
|
50
|
+
44 => "partly cloudy",
|
51
|
+
45 => "thundershowers",
|
52
|
+
46 => "snow showers",
|
53
|
+
47 => "isolated thundershowers",
|
54
|
+
3200 => "not available"
|
55
|
+
}
|
56
|
+
|
57
|
+
# the Yahoo! Weather condition code, as detailed at http://developer.yahoo.com/weather.
|
58
|
+
attr_reader :code
|
59
|
+
attr_reader :date
|
60
|
+
attr_reader :temp
|
61
|
+
attr_reader :text
|
62
|
+
|
63
|
+
def initialize(data)
|
64
|
+
@code = data[:code].to_i
|
65
|
+
@date = parse_time(data[:date])
|
66
|
+
@temp = data[:temp].to_i
|
67
|
+
@text = data[:text]
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Yweather
|
2
|
+
class Forecast
|
3
|
+
include Yweather::Utils
|
4
|
+
|
5
|
+
attr_reader :day
|
6
|
+
attr_reader :date
|
7
|
+
attr_reader :low
|
8
|
+
attr_reader :high
|
9
|
+
attr_reader :text
|
10
|
+
attr_reader :code
|
11
|
+
|
12
|
+
def initialize (data)
|
13
|
+
@day = data[:day]
|
14
|
+
@date = parse_time(data[:date])
|
15
|
+
@low = data[:low].to_i
|
16
|
+
@high = data[:high].to_i
|
17
|
+
@text = data[:text]
|
18
|
+
@code = data[:code].to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yweather
|
2
|
+
class Image
|
3
|
+
|
4
|
+
attr_reader :height
|
5
|
+
attr_reader :link
|
6
|
+
attr_reader :title
|
7
|
+
attr_reader :url
|
8
|
+
attr_reader :width
|
9
|
+
|
10
|
+
def initialize (data)
|
11
|
+
@title = data[:title]
|
12
|
+
@link = data[:link]
|
13
|
+
@url = data[:url]
|
14
|
+
@height = data[:height]
|
15
|
+
@width = data[:width]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
module Yweather
|
3
|
+
class Response
|
4
|
+
attr_accessor :astronomy
|
5
|
+
attr_accessor :atmosphere
|
6
|
+
attr_accessor :condition
|
7
|
+
attr_accessor :description
|
8
|
+
attr_accessor :forecasts
|
9
|
+
attr_accessor :image
|
10
|
+
attr_accessor :latitude
|
11
|
+
attr_accessor :location
|
12
|
+
attr_accessor :longitude
|
13
|
+
attr_accessor :page_url
|
14
|
+
attr_accessor :request_location
|
15
|
+
attr_accessor :request_url
|
16
|
+
attr_accessor :title
|
17
|
+
attr_accessor :units
|
18
|
+
attr_accessor :wind
|
19
|
+
|
20
|
+
def initialize(woeid, zipcode, url, hash)
|
21
|
+
if hash[:rss][:channel][:title] =~ /Error/
|
22
|
+
item = hash[:rss][:channel][:item]
|
23
|
+
raise YweatherException, "#{hash[:rss][:channel][:title]} :: #{item[:title]} :: #{item[:description]}"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
@astronomy = Astronomy.new(hash[:rss][:channel][:astronomy])
|
28
|
+
@atmosphere = Atmosphere.new(hash[:rss][:channel][:atmosphere])
|
29
|
+
@location = Location.new(hash[:rss][:channel][:location])
|
30
|
+
@wind = Wind.new(hash[:rss][:channel][:wind])
|
31
|
+
@units = Units.new(hash[:rss][:channel][:units])
|
32
|
+
@image = Image.new(hash[:rss][:channel][:image])
|
33
|
+
|
34
|
+
item = hash[:rss][:channel][:item]
|
35
|
+
@condition = Condition.new(item[:condition])
|
36
|
+
@forecasts = []
|
37
|
+
item[:forecast].each do |forecast|
|
38
|
+
@forecasts << Forecast.new(forecast)
|
39
|
+
end
|
40
|
+
@latitude = item[:lat]
|
41
|
+
@longitude = item[:long]
|
42
|
+
@page_url = item[:link]
|
43
|
+
@title = item[:title]
|
44
|
+
@description = item[:description]
|
45
|
+
end
|
46
|
+
|
47
|
+
def atmospheric_conditions(options = {})
|
48
|
+
pressure = (@atmosphere.barometer == "steady") ?
|
49
|
+
"#{@atmosphere.barometer} at #{@atmosphere.pressure}#{@units.pressure}" :
|
50
|
+
"#{@atmosphere.pressure}#{@units.pressure} and #{@atmosphere.barometer}"
|
51
|
+
|
52
|
+
h = ::HashWithIndifferentAccess.new({
|
53
|
+
:humidity => "#{@atmosphere.humidity}%",
|
54
|
+
:visibility => "#{@atmosphere.visibility} #{@units.distance}",
|
55
|
+
:pressure => "#{pressure}"
|
56
|
+
})
|
57
|
+
|
58
|
+
return_content(h, options[:format])
|
59
|
+
end
|
60
|
+
|
61
|
+
def wind_conditions(options = {})
|
62
|
+
# chill: wind chill in degrees (integer)
|
63
|
+
# direction: wind direction, in degrees (integer)
|
64
|
+
# speed: wind speed, in the units specified in the speed attribute of the yweather:units element (mph or kph). (integer)
|
65
|
+
h = ::HashWithIndifferentAccess.new({
|
66
|
+
:chill => "#{@wind.chill}°",
|
67
|
+
:direction => "#{@wind.direction}°",
|
68
|
+
:speed => "#{@wind.speed} #{@units.speed}"
|
69
|
+
})
|
70
|
+
return_content(h, options[:format])
|
71
|
+
end
|
72
|
+
|
73
|
+
##################################################
|
74
|
+
private
|
75
|
+
|
76
|
+
def return_content(hash, format)
|
77
|
+
if format == :json
|
78
|
+
hash.to_json
|
79
|
+
elsif format == :xml
|
80
|
+
hash.to_xml
|
81
|
+
else
|
82
|
+
hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|