Practice: Weather - Solution

The Problem

Below is the problem again, which you can also find here.

Using the Ruby hash `weather_data` that includes weather forecast data for Chicago, write a weather summary out to the screen including the current conditions and upcoming forecast. Something like the output below.

Sample output:
In Chicago, IL it is currently 67 degrees and cloudy.
The rest of today will be a high of 65 and scattered shows.
The upcoming weather forecast is:
Wednesday: a high of 65 and scattered showers.
Thursday: a high of 67 and partly cloudy.
Friday: a high of 59 and rain.
Saturday: a high of 77 and cloudy.
...

STEPS
Look at the weather_data hash.
Find the current data.
Build a string with the text and dynamic data from the hash.
"In #{...} it is currently #{...} degrees and #{...}"
Find the array of forecast data.
Read only the first element of that array to display the conditions for the rest of today.
Use a loop to display the daily summary for the upcoming forecast.

CHALLENGE
Can you display the weather forecast summary for a user-entered city?
Use the following code at the very top of the file and then replace "chicago" in the api url with the user-entered city:
puts "What city are you in?"
city = gets.chomp
puts city
Note: what happens if the user-entered value is not a known city? You'll want to do some error handling.

Similar to the lab 4-hashes.rb, this exercise includes some starting code.  At the top of the file are several lines of code that will pull live weather forecast data from the WeatherDB API.

# DON'T CHANGE THIS CODE
# ----------------------
require "net/http"
require "json"
url = "https://weatherdbi.herokuapp.com/data/weather/chicago"
uri = URI(url)
response = Net::HTTP.get(uri)
weather_data = JSON.parse(response)
# ----------------------

Specifically, it gets weather data for Chicago which is part of the end of url, converts the response from that url into a ruby hash, and then assigns it to a variable weather_data.  So the first step is to look at that data:

puts weather_data

The hash has 3 keys at its top level: "region", "currentConditions", and "next_days".  The value of the "currentConditions" key appears to be the current weather data, which we'll use for the first line of the output.  Let's start by just displaying the hardcoded sample text:

puts "In Chicago, IL it is currently 67 degrees and cloudy."

We can now replace the data that should be dynamic from the weather_data hash.

puts "In Chicago, IL it is currently __ degrees and _____."

The value of the "currentConditions" key is another hash.  For the temperature, there's a "temp" key that is yet another hash.  The "f" key within it seems to be "fahrenheit".

current_temp = weather_data["currentConditions"]["temp"]["f"]
puts "In Chicago, IL it is currently #{current_temp} and ____."

Similarly, we want to replace "cloudy" with the current condition which appears to be in that same "currentConditions" hash in the "comment" key.

current_temp = weather_data["currentConditions"]["temp"]["f"]
current_condition = weather_data["currentConditions"]["comment"]
puts "In Chicago, IL it is currently #{current_temp} and #{current_condition}."

Next, we want to display the forecast for the rest of today.  The "currentConditions" doesn't include any forecast data, so we'll need to look elsewhere.  The "next_days" key is an array that looks promising - it would make sense for forecast data to be stored as an array since there are multiple days of forecast data in chronological order.  And the first element in that array has the same day of the week as today, so it looks like that's today's overall forecast (vs just the immediate current weather).  Let's just look more closely at it:

today_forecast = weather_data["next_days"][0]
puts today_forecast

It has a "max_temp" key with a similar hash from before including an "f" key for fahrenheit.  And there's a "comment" key.  So let's display the hardcoded text and then replace the dynamic pieces of data.

puts "The rest of today will be a high of 65 and scattered shows."

Identifying the dynamic data:

puts "The rest of today will be a high of __ and _____."

And replacing it:

puts "The rest of today will be a high of #{today_forecast["max_temp"]["f"]} and #{today_forecast["comment"]}."

The next line is just static text:

puts "The upcoming weather forecast is:"

And now for the entire upcoming forecast.  We could pull out each day manually using the index.  For example weather_data["next_days"][1], and then weather_data["next_days"][2], etc.  But we don't know how many days of forecast data to expect, plus it's just repetitive.  Let's use a loop!

for daily_forecast_data in weather_data["next_days"]
  puts "Wednesday: a high of 65 and scattered showers."
end

Here we're looping through the "next_days" array and assigning each element as daily_forecast_data within the loop.  The variable name is a little long, but it's clear what it is which is what really matters.  We're then displaying hardcoded text.  Just as before, we'll put in some placeholders for the dynamic data.

for daily_forecast_data in weather_data["next_days"]
  puts "____: a high of __ and ____."
end

It's similar to the "rest of today forecast" line, except that we also want to display the day of the forecast on each line.  Looking closely again at the weather_data hash, there's a "day" key inside each element in the next_days array.  That appears to be the day of the week.  Let's fill in the placeholders:

for daily_forecast_data in weather_data["next_days"]
  day_of_week = daily_forecast_data["day"]
  high_temp = daily_forecast_data["max_temp"]["f"]
  conditions = daily_forecast_data["comment"]
  puts "#{day_of_week}: a high of #{high_temp} and #{conditions}."
end

And the final code (before the challenge) is:

Final Code (pre-challenge)

current_temp = weather_data["currentConditions"]["temp"]["f"]
current_condition = weather_data["currentConditions"]["comment"]
puts "In Chicago, IL it is currently #{current_temp} and #{current_condition}."

today_forecast = weather_data["next_days"][0]
puts "The rest of today will be a high of #{today_forecast["max_temp"]["f"]} and #{today_forecast["comment"]}."

puts "The upcoming weather forecast is:"
for daily_forecast_data in weather_data["next_days"]
  day_of_week = daily_forecast_data["day"]
  high_temp = daily_forecast_data["max_temp"]["f"]
  conditions = daily_forecast_data["comment"]
  puts "#{day_of_week}: a high of #{high_temp} and #{conditions}."
end

Challenge!

Now for the challenge.  This little app only returns weather for Chicago.  But what if we want weather for another location.  We can modify the API url and replace "chicago" with another city.  We don't know for sure if that will work (without reading the API documentation), but it doesn't hurt to just try it and see.  Sure enough, if you replace "chicago" with "london", you'll get weather for London.

Now that we know that, it would be cool if the user can decide the location.

Above the API code, at the very top of the file, let's use the same gets.chomp code from lab 4-hashes.rb.  As a reminder, the code gets.chomp is sort of the opposite of puts.  puts displays text output.  gets accepts text input and will pause the application waiting until a user types in something and presses enter.  The .chomp part is needed because the text that gets captures includes the enter key that is pressed, but since we're not interested in that key, we drop it using .chomp.

puts "Check the weather in which city?"
city = gets.chomp

Now, even though it says "DON'T CHANGE THIS CODE", we're going to change it to incorporate this user-entered city.  The API url is:

url = "https://weatherdbi.herokuapp.com/data/weather/chicago"

Let's replace the hardcoded city "chicago" with the dynamic data that the user entered:

url = "https://weatherdbi.herokuapp.com/data/weather/#{city}"

Now run the application.  You should be prompted to enter a city and once you've done so and press enter, you'll see weather data for that city.  But you might notice something odd.  The first line of the output still says "Chicago, IL":

In Chicago, IL it is currently __ degrees and ____.

That was fine before when we only returned Chicago weather.  But now we should replace the location with the actual location of the weather forecast.  Fortunately, that's included in the API response under the top-level "region" key.  So let's replace that part of the first line:

puts "In #{weather_data["region"]} it is currently #{current_temp} and #{current_condition}."

Perfect!

One last step.  Try running the program and entering a fake city - it's actually kind of tricky to do so since there are a lot of cities.  However, once you come up with something (e.g. "fake_city"), you'll get some errors in your application.  Before we try to fix them, let's check what the API data looks like puts weather_data.  It should look considerably different:

{
  "status": "fail",
  "message": "invalid query",
  "query": "fake_city",
  "code": 0,
  "visit": "https://weatherdbi.herokuapp.com/documentation/v1"
}

So the API is actually providing some info that is helpful in handling errors in our code.  We can check if the hash key "status" is equal to "fail" and, if so, skip everything else.  Something like this:

if weather_data["status"] == "fail"
  puts "we don't know that city.  try again."
else
  # ... all the code
end

Now we're handling the "edge case" (a situation that does not occur often, but can occur and will cause unexpected issues) or "error handling" which makes our code more resilient and improves the user experience.

There is another error which is caused by city names with spaces (e.g. "San Francisco").  It makes sense if you think about it.  URLs don't know how to handle spaces.  A url like this won't work:

url = "https://weatherdbi.herokuapp.com/data/weather/San Diego"

Instead, according to the API documentation, we need to remove the spaces, so "San Diego" becomes SanDiego.  Sounds like we want to manipulate a string, so let's look back at the ruby String documentation.  There is a method .gsub which takes 2 parameters.  The first is a pattern to look for.  The second is what to replace it with if the pattern is found in the string.  So "apple".gsub("p", "9") returns "a99le".  In our situation, the edge case is a city name with a space, so we can just get rid of the space:

city = city.gsub(" ", "")

The code will look for an empty space in the city string.  If it finds 1 or more spaces, it will replace it/them with nothing, effectively just removing those spaces.

Final Code

puts "Check the weather in which city?"
city = gets.chomp
city = city.gsub(" ", "")

# even though it says don't change this, code - replace "chicago" in the url with #{city}:

# DON'T CHANGE THIS CODE
# ----------------------
require "net/http"
require "json"
url = "https://weatherdbi.herokuapp.com/data/weather/#{city}"
uri = URI(url)
response = Net::HTTP.get(uri)
weather_data = JSON.parse(response)
# ----------------------

# ... code comments

# 1. inspect the weather_data hash
# puts weather_data

# 2. check if user submits an unknown city and handle edge case
if weather_data["status"] == "fail"

  # 3. display error message to the user
  puts "we don't know that city.  try again."

# 4. otherwise, display weather summary for city
else

  # 5. get the current temp and conditions
  current_temp = weather_data["currentConditions"]["temp"]["f"]
  current_condition = weather_data["currentConditions"]["comment"]

  # 6. display string with region, current temp and conditions
  puts "In #{weather_data["region"]} it is currently #{current_temp} and #{current_condition}."

  # 7. get the summary forecast for today
  today_forecast = weather_data["next_days"][0]
  
  # 8. display string with summary forecast data
  puts "The rest of today will be a high of #{today_forecast["max_temp"]["f"]} and #{today_forecast["comment"]}."

  # 9. loop through array of daily forecast data
  puts "The upcoming weather forecast is:"
  for daily_forecast_data in weather_data["next_days"]
  
    # 10. get the day of week, high temp and conditions for each day's forecast data
    day_of_week = daily_forecast_data["day"]
    high_temp = daily_forecast_data["max_temp"]["f"]
    conditions = daily_forecast_data["comment"]
    
    # 11. display string with day of week and day's forecast summary
    puts "#{day_of_week}: a high of #{high_temp} and #{conditions}."

  # 12. end loop
  end

# 13. end conditional
end