Chapter 13 - Loops

Remember earlier when we implemented a function that took two numbers and returned the largest one? We called it largest and it looked like this:

function largest(num1, num2) {
  if (num1 > num2) {
    return num1;
  } else {
    return num2;
  }
}

For Loop

Well, I'm not going to lie, this now looks to me like a half-assed solution, so I want our function to take an arbitrary number of numbers and return the largest one. This implementation does just that, using what's called a "for-loop":

function largest(numberArray) {
  var largest = numberArray.pop();

  for (var i = 0; i < numberArray.length; i++) {
    if (numberArray[i] > largest) {
      largest = numberArray[i];
    }
  }

  return largest;
}

Hmmmm... Let's talk about our different scenarios for inputs and their expected outputs. For example, if our input were the following array:

[3, 2, 5, -7]

We should expect our output from our function now to be: 5, as it is the largest number in the array.

Now, let's think of an edge case. If the array supplied to the largest function were empty ([]), I would be okay with saying our output should be undefined which means no value at all. The solution above does just that.

But how did we do this? We declared a largest variable at the top of our function. Not giving that variable an initial value would cause issues. Why? Because not giving largest a value at all, means that it'd just be initialized as undefined and when we compare undefined with any number, JavaScript tries to coerce undefined to a number which yields NaN (Not a Number) and therefore make it seem like undefined is neither bigger nor smaller than any number we compare it to. That wouldn't get us very far.

So instead, we're initializing our largest variable with the first element of our array. We do that using the pop method which is available on all JS arrays. pop simply returns the last element in the array and removes it from the original array as well. Keep in mind that in our situation that's alright to assume the last element is the largest for now, if it isn't, it'll be replaced by the element that's bigger later. Additionally, it's cool that we remove it from the array since we don't need to iterate over it again. Think about it for a second.

Now to the interesting part. In the second line of our function, we use a simple JS for loop and iterate over our entire array. Notice how a for loop takes three expressions. The first expression (var i = 0;) is the initial expression the loop starts with. In this case we are declaring a variable by the name of i which we're going to use to keep track of the index of our array.

In the second expression we pass to our for loop (i < numberArray.length;) is a condition that as long as it evaluates to true, the loop will continue execution. In this case numberArray.length provides the count of the array members. So as long as i (our index) is less than the count of the elements, the loop will continue executing its code.

And then the last expression we give our for loop (i++) is an expression that gets executed after each one of the loop's iterations completes. In this case we are simply incrementing our index variable, i, by 1 (note that i++ is just a shorthand for i = i + 1.)

So basically this loop just iterates over all of our array elements as you'd expect. In each iteration we have an if statement to check if the element we're on right now (numberArray[i]) is greater than the value that we already have in our variable largest. If the value is greater, we simply replace it and store our new greatest value in largest. Once we're done iterating over our array and make sure we have the largest number in largest we return it as the result!

While Loop

Another way we could solve the challenge of finding the largest number, would be a while loop:

function largest(numberArray) {
  var num = numberArray.pop();
  var largest = num;

  while (typeof num !== 'undefined') {
    if (num > largest) {
      largest = num;
    }
    num = numberArray.pop();
  }

  return largest;
}

This implementation adds a few concepts we haven't covered before, and is somewhat less straightforward than our for loop implementation.

First thing to note, is that we declare a variable by the name of num which is just going to be used to hold the number we're currently iterating over. We initialize num with the last element from our input array. We also still declare a largest variable and populate it with the same value of num (the last element).

Then we have our while loop. While-loops simply continue execution as long as the expression given to them evaluates to true.

So this while-loop, will theoretically never stop executing:

while (true) {
  // some code
}

In reality, your browser will kill this script as soon as it runs out of memory.

So in our while-loop we have this:

while (typeof num !== 'undefined') {
  // some code

  num = numberArray.pop();
}

If num was undefined, typeof num would return the string "undefined" and so our while-loop wouldn't even enter its code block. On the contrary, if we initialized num with a perfectly legit number, say 4, then typeof would return "number" and our while-loop would execute its code block.

The last line of our while-loop's code block, is already familiar to you, and simply pops out the last element from our numberArray and assigns it to num (overwriting its previous value). This methodology is a common way to iterate over all member elements of an array. When numberArray is finally empty, calling pop on it, will result in undefined which will terminate our while-loop.

Now, some of you might be thinking to yourselves why not just write this loop as:

while (num) {
  // code
}

Since we already said JS coerces to booleans when it needs to. For example, when the expression would yield 7 JS will consider it a truthy value and enter the loop. Same will happen when it iterates over the number -3 -- it will enter the loop. When the value will finally be undefined then the iteration will stop (since undefined is falsy). But what will happen if we had the number 0 in our array? Ah-ha! Then our loop will exit prematurely since zero will be coerced to false!

If you are curious for a simpler solution, since you are as lazy as I am, all of this loop bizwax could be avoided by using JS's built-in max method like this:

function largest(numberArray) {
  return Math.max(numberArray);
}

But you still need to know how to use loops, so keep reading...

So how are we planning on using loops in our Weather App?

Our Weather App, up until now, could only show us weather for our current location. But what if we wanted it to show us weather from other locations, too? To keep this simple, let's stick to only ZIP (Zone Improvement Plan!) codes and nothing fancier.

This illustrates what our app should take as an input, and what it should output to the user.

Input - an array of ZIP codes:

[11211, 80634]

Desired Output (along the lines of the following text):

 Weather in "Current Location" : "96"
 Weather in "11211" : "87"
 Weather in "80634" : "65"

In order to get there, we will need to make a few adjustments to our app.

Let's start with what we have. We have a function by the name of getWeather It takes one argument (a user's coordinate) and a callback to be called when the weather data is fetched. So now all we need to do is make getWeather work when it's given a ZIP code instead of a coordinate as well.

Keep in mind that in our case changing getWeather to take ZIPs shouldn't be too hard since the Open Weather Map API can take a ZIP code as a query parameter as well. If we were less lucky, and their API could only take coordinates, we would most likely have to introduce another API to convert our ZIPs to coordinates and only then call our getWeather function.

In order to use a ZIP code instead of a coordinate, we can query the Open Weather Map API with the following URL:

http://api.openweathermap.org/data/2.5/weather
  ?appid=fa32f&units=imperial&zip=11211

Additionally, in order for us to be able to show the weather in multiple locations at a time, we will need some wrapper function that will iterate over all of the ZIP codes provided by the user and call getWeather for each one of them separately.

But first things first; as you can see in the URL above, the only difference in our getWeather function needs to be in the URL query parameters -- if our function gets a ZIP it should add the parameter zip and if it gets a coordinate object, it should add the parameters lat and lon. That's it.

We could be cumbersome and do this to solve our scenario:

function getWeather(zipOrCoordinate, callback) {

  var url = apiUrl + '?appid=' + apiKey + '&units=imperial';

  if (zipOrCoordinate.longitude) { // We assume it's a coordinate
    url += '&lon=' + zipOrCoordinate.longitude +
           '&lat=' + zipOrCoordinate.latitude;
  } else { // Assuming we have a zipcode
    url += '&zip=' + zipOrCoordinate;
  }

  makeRequest(url, callback);
}

In the scenario above we just change the first argument's name to zipOrCoordinate because we don't know what's in it anymore. Then we add a conditional to check whether we got ourselves an object with the key longitude or something else. If zipOrCoordinate.longitude returns any truthy value (not undefined), it is safe to assume zipOrCoordinate is a coordinate. Otherwise, zipOrCoordinate could be anything else, but for brevity, we're going to assume it is a string or a number representing a ZIP code and just add it as a query param to the end of the URL.

But it turns out the Open Weather Map API can take even more than just a ZIP or a specific coordinate. It turns out it works with full city names as well, and we would like to take advantage of that too. Besides, our solution just seems messy and a bit too brittle. The fact that we're relying on zipOrCoordinate to be a ZIP if it's not a coordinate, is not really self-explanatory and I bet future you will have a hard time understanding why we did that. My suggestion, therefore, is to change our code to something like this:

function getWeather(locationObject, callback) {

  var url = apiUrl +
    '?appid=' + apiKey +
    '&units=imperial' +
    '&' + parameterize(locationObject);

  makeRequest(url, callback);
}

What we did here is not that tricky. Instead of checking for the different types our getWeather function receives, we just made it a bit more agnostic. Now our function can take whatever kind of object you wanna give it. Surely it will fail to return the weather if you don't give it something it expects, but this is not its responsibility to decide that right now. Our getWeather function, we assume, is getting a valid input.

So what's going to be inside locationObject? An object with a location in it! That could mean properties such as a coordinate, a ZIP code, or a city name. Our parameterize function (which we haven't implemented yet) is now responsible to turn our function argument, locationObject, into the query params we then append to the rest of the URL.

Here is an example of a valid input and output for our (future) parameterize function:

Input:

{
  lat: 40.56,
  lon: -30.87
}

Output:

"lat=40.56&lon=-30.87"

For...In Loop

Here is one way to implement our parameterize function:

function parameterize(object) {
  var params = [];
  for (var key in object) {
    params.push(key + '=' + encodeURIComponent(object[key]));
  }
  return params.join('&');
}

As you can see, it takes one object, named object, initializes an empty array and stores it in a variable called params. Then it loops over the object's keys using a for...in loop. Inside the loop it creates a string representing the key and the value in a "parameterized" way like we've demonstrated above.

Note how inside the loop, we reference the value of the object like this: object[key]. This is because key is just another variable holding a string representation of the key, so when key has the string "lat" in it, object[key] means object['lat'] (which is the same as object.lat), and it yields the value in it which is 40.56.

The reason we wrap object[key] in JS's built-in function, encodeURIComponent, is that the value in object[key] could be non-URI compliant. For example, if the key was q (to indicate free-form query of any word, most likely country or city), the value could very well contain spaces which are not allowed in URLs. So in order to account for that issue, the encodeURIComponent function simply replaces non-valid URI characters with their UTF-8 representation (for space that would be the string "%20") so we can transfer the data accurately without causing any issues.

Once we're done with creating our string, we push it to our params array, and move on to the next key in our object. You can find out what the params array will look like in each iteration by adding:

console.log(params); // will first yield: ["lat=40.56"]

inside the loop.

Moving on to our second iteration, now our key variable holds the string "lon". Great. So we push the string "lon=-30.87" to our array, which makes it look like this now:

["lat=40.56", "lon=-30.87"]

Fantastic! Now we have all of our parameters in our params array. All that's left for us to do is concatenate them all into one big string with an ampersand (&) separating between (again, that's the convention for multiple query parameters). And this is how we do it:

params.join('&');

join is a built in method on any array in JS and it simply joins all of the array's elements with the string provided to it as an argument. In our case, this will finally yield the following string:

"lat=40.56&long=-30.87"

Ta Da! This is all our parameterize function does, and this is all we want it to do! Keep it simple, homie.

So at this point, if you've lost track, our getWeather function can take either a ZIP code, a coordinate, or a city/state name (via the q parameter) and return the current weather for it. However, our implementation is far from perfect as our function makes a pretty big assumption that it is given, as an input, a valid object with the keys required by the Open Weather Map API. In a real world application, we would probably want to include some validations to assert our input is valid before continuing with our execution. In the meantime if something is not right, we will most likely just get an ugly error displayed in our Console.

So back to our original premise -- making our application give us weather for more than one location. First off, we need it to get us weather for an array of ZIP codes and this is one way to do it:

function weatherForZipCodes(zips) {
  for (var i = 0; i < zips.length; i++) {
    getWeather({
      zip: zips[i]
    }, printWeather);
  }
}

And with our previous entrance point to our application, via the /linebreak weatherForMyLocation function, we can still get weather data for the current location. The only change weatherForMyLocation needs to undergo is pass a more verbose object to getWeather in order for it to have the correct keys since we're going to use them as our query params' keys and so we need them to fit the Open Weather Map API convention. This is our new implementation of weatherForMyLocation:

function weatherForMyLocation() {
  getUserCoordinates(function (coord) {
    getWeather({
      lon: coord.longitude,
      lat: coord.latitude
    }, printWeather);
  });
}

As a recap, this is what we now have for the whole application in order for it to show us weather data for our current location and a list of ZIP codes.

/*
  Filename: weather_app.html
  Find me on GitHub: 
  github.com/ughcode/app/blob/master/chapter-13/weather_app.html
*/

<script>

var apiUrl = 'http://api.openweathermap.org/data/2.5/weather';

var apiKey = 'fa32f';

function getWeather(locationObject, callback) {
  var url = apiUrl +
    '?appid=' + apiKey +
    '&units=imperial' +
    '&' + parameterize(locationObject);

  makeRequest(url, function (responseText) {
    response = JSON.parse(responseText);
    callback(response);
  });
}

function parameterize(object) {
  var params = [];
  for (var key in object) {
    params.push(key + '=' + encodeURIComponent(object[key]));
  }
  return params.join('&');
}

function makeRequest(url, callback) {
  var xhrObject = new XMLHttpRequest();
  xhrObject.onload = function () {
    callback(xhrObject.responseText);
  };
  xhrObject.open('GET', url, true);
  xhrObject.send();
}

function getUserCoordinates(callback) {
  navigator.geolocation.getCurrentPosition(function (position) {
    if (position.coords) {
      callback(position.coords);
    } else {
      console.error("Didn't get a valid coordinate, meow!");
    }
  });
}

function printWeather(weatherData) {
  var currentTemperature = weatherData.main.temp;
  var location = weatherData.name;
  console.log("The current temprature in " + location +
              " is: " + currentTemperature);
}

function weatherForMyLocation() {
  getUserCoordinates(function (coord) {
    getWeather({
      lon: coord.longitude,
      lat: coord.latitude
    }, printWeather);
  });
}

function weatherForZipCodes(zips) {
  for (var i = 0; i < zips.length; i++) {
    getWeather({
      zip: zips[i]
    }, printWeather);
  }
}

</script>

So now take all of the code above, type it into your HTML file and try to run it again. Keep in mind now we have two different entrance points to our application: When we want to get weather for ZIP codes, we'll invoke weatherForZipCodes with an array of ZIPs. And when we want to get weather for our current location, we'll invoke weatherForMyLocation. Capiche?

'Nough programming for today. You've made some real progress and we're almost done with our Weather App. Go to bed.

Table of Contents
Home