Skip to content

Instantly share code, notes, and snippets.

@DanCouper
Last active March 10, 2017 22:59
Show Gist options
  • Select an option

  • Save DanCouper/fcb52c941d4a0e66d7cbe717eea5533d to your computer and use it in GitHub Desktop.

Select an option

Save DanCouper/fcb52c941d4a0e66d7cbe717eea5533d to your computer and use it in GitHub Desktop.
A rough start on an explanation of the FCC 'Record Collection' challenge

Record Collection

Free Code Camp's Record Collection challenge seems to be the point in their curriculum at which JavaScript gets hard for new learners. It is the first relatively complex challenge, the first that isn't a simple exploration of syntax. It is also the first challenge that simulates common semi-complex functionality of the kind that would be found in real-world applications, so understanding it is important.

This article is long and ends with a possible solution, followed by refactoring: the focus is how and why. It goes over a lot of ground already covered on FCC in previous challenges.

Working with objects

Objects are the only complex data type found in JavaScript, and as such, understanding how to deal with them is central to understanding JavaScript.

The challenge covers:

  • Checking object properties exist.
  • Creating, updating and deleting properties.
  • Working with nested objects and arrays.

These are very basic features, but most time spent working with objects is likely to involve doing simple manipulations, so being able to so confidently makes every further step simpler.

Accessing objects

An object is a collection of properties; each property is a key/value pair. So it is like a dictionary: the key is the name of the property, and the value is the definition.

Keys in Javascript are represented as strings.

var myObj = { "key": "value" }

So myObj has a property called "key" which has a value of "value".

To access that value, you tell JS which key you want, and it will tell you the value:

> myObj["key"]
"value"

If the key is a valid identifier, you can:

  1. Leave off the quotation marks in the definition: myObj = { key: "value" }.

    var myObj = { key: "value" }
    
  2. Access using dot notation rather than using the square brackets, making it easier to read.

    > myObj.key
    "value"

In practice, what this generally means is:

  1. if you have a key that starts with a number, or has a space in, you need to use quotation marks and brackets:

    myObj = {
      "1234": "foo",
      "key with spaces": "bar",
    }
  2. To access a key, you have to use brackets:

    > myObj["1234"]
    "value"
    > myObj["key with spaces"]
    "bar"

If you have a variable, and you want to use it when checking an object, again, you have to use brackets:

var myKey = "imAKey";
var myObj = {
  imAKey: "Hello"
}

If you tried to use dot notation here - myObj.myKey - JS would try to find a key called "myKey" in myObj. Instead:

> myObj[myKey]
"Hello"

Note that arrays are a special kind of object, and in arrays, each key is a number.

These two are basically the same (NOTE that arrays are what's called exotic objects, which have some extra rules - ie the keys are automatically created starting from "0", and they have a length property that automatically updates when things are added or removed).

var myArrayOne = [1,2,3]
var myArrayTwo = {
  "0": 1,
  "1": 2,
  "2": 3,
}
> myArrayOne[0] // JS is converting this to a string automatically
1
> myArrayTwo[0]
1
> myArrayOne["0"]
1
> myArrayTwo["0"]
1

And if you try to use dot notation, it won't work - a number is not a valid identifier

> myArrayOne.0
SyntaxError: Unexpected number
> myArr.2
SyntaxError: Unexpected number

Checking whether properties exist

Let's assume an object has been defined. One very common operation is to check whether that object has a certain property.

var myObj = {
  foo: 1,
  bar: 2,
  baz: 3,
}

There are two generally-used ways to check whether a property exists. Firstly, in:

> "foo" in myObj
true
> "quux" in myObj
false

This is simple, and works in many use cases. However, it checks all the properties, including those defined natively, by JS. For example, all objects have a method "toString()" and a method "hasOwnProperty()".

> "toString" in myObj
true
> "hasOwnProperty" in myObj
true

Much of the time, this isn't an issue, but there is a slightly more robust way to check: the hasOwnProperty() method. This checks if the current object has a property:

> myObj.hasOwnProperty("foo")
true
> myObj.hasOwnProperty("hasOwnProperty")
false

Modifying, creating and deleting properties

The syntax in JS for creating/updating/deleting properties is identical regardless of whether they exist on the object or not.

No error will be thrown if attempting to delete a property that doesn't exist, setting the value of a property that doesn't exist will create that property, setting the value of a property that does exists will just change the value.

So with our object:

var myObj = {
  foo: 1,
  bar: 2,
  baz: 3,
}

Some operations can be performed on it:

> delete myObj.baz
true
> myObj
{ foo: 1, bar: 2 }
> delete myObj.quux
true
> myObj
{ foo: 1, bar: 2 }
> myObj.foo = 2
> myObj
{ foo: 2, bar: 2 }
> myObj.baz = 2
> myObj
{ foo: 2, bar: 2, baz: 2 }
> myObj["1234"] = "Some text"
> myObj
{ foo: 2, bar: 2, baz: 2, "1234": "Some text" }
> delete myObj["1234"]
> myObj
{ foo: 2, bar: 2, baz: 2 }
> var imAProp = "baz"
> delete myObj[imAProp]
> myObj
{ foo: 2, bar: 2 }

The challenge

You are given a JSON object representing a part of your musical album collection. Each album has several properties and a unique id number as its key. Not all albums have complete information.

So you have an object representing albums, and you can guarantee each album in the object has an ID that you can use to look it up. The actual album object itself has 0 or more properties.

var collection = {
    "2548": {
      "album": "Slippery When Wet",
      "artist": "Bon Jovi",
      "tracks": [
        "Let It Rock",
        "You Give Love a Bad Name"
      ]
    },
    "2468": {
      "album": "1999",
      "artist": "Prince",
      "tracks": [
        "1999",
        "Little Red Corvette"
      ]
    },
    "1245": {
      "artist": "Robert Palmer",
      "tracks": [ ]
    },
    "5439": {
      "album": "ABBA Gold"
    }
};

Write a function which takes an album's id (like 2548), a property prop (like "artist" or "tracks"), and a value (like "Addicted to Love") to modify the data in this collection. Your function must always return the entire collection object.

function updateRecords(id, prop, value) {
  return collection;
}

NOTE From hereon in, I am working with a modified version of the actual function signature used in the FCC challenge. As it stands, the function takes three arguments (id, prop, value), and collection, a global variable, is used inside the function. This I would consider extremely bad practice: the collection variable comes from outside the function, so the function cannot work on its own. It is dependent on something it should not be dependent on, hence I am adding collection as the first variable:

function updateRecords(collection, id, prop, value) {
  return collection;
}

It maybe could be assumed that if an id is passed, the album will be there in the collection, but it would be good to have a check in place to stop the collection being modified if the wrong thing is passed in.

So, instead of going straight into the logic like:

function updateRecords(collection, id, prop, value) {
  // logic to modify collection[id] here
  return collection;
}

Put in a check, so the collection cannot be modified when incorrect input is passed:

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    // logic to modify collection[id] here
  }
  return collection
}

Conditions

The logic to modify the collection can now be added. There are four rules:

  1. If prop isn't "tracks" and value isn't empty (""), update or set the value for that record album's property.
  2. If prop is "tracks" but the album doesn't have a "tracks" property, create an empty array before adding the new value to the album's corresponding property.
  3. If prop is "tracks" and value isn't empty (""), push the value onto the end of the album's existing tracks array.
  4. If value is empty (""), delete the given prop property from the album.

In pseudocode:

# The album being operated on is collection[id].
# Assuming the collection has a property of id (eg, it has the album):

updateRecords(collection id, prop, value) =
  if collection has id:
    # If prop isn't "tracks" and value isn't empty (""), update or set the value
    # for that record album's property.
    if prop is not "tracks" and value is not "":
      album[prop] = value

    # If prop is "tracks" but the album doesn't have a "tracks" property, create an
    # empty array before adding the new value to the album's corresponding property.
    if prop is "tracks" and album does not have prop "tracks" and value is not "":
      album["tracks"] = new array, push value

    # If prop is "tracks" and value isn't empty (""), push the value onto the end
    # of the album's existing tracks array.
    if prop is "tracks" and album has prop "tracks" and value is not "":
      album["tracks"], push value

    # If value is empty (""), delete the given prop property from the album.
    if value is "":
      delete album[prop]

  always return collection

So, converting the pseudocode to JS:

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    if (props !== "tracks" && value !== "") {
      collection[id][prop] = value;
    } else if (prop === "tracks" && collection[id].hasOwnProperty("tracks") === false && value !== "") {
      collection[id][prop] = [value];
    } else if (prop === "tracks" && collection[id].hasOwnProperty("tracks") && value !== "") {
      collection[id][prop].push(value);
    } else if (value === "") {
      delete collection[id][prop];
    }
  }
  return collection;
}

These can be done in any order, but to keep the code readable and the developer sane, simplifying those conditions as much as is possible is preferable.

Shifting them round helps. The conditions run in order - if one returns false, the next one is checked, and so on. The first three conditions check that the value is not "". If the value is an empty string, that prop is deleted regardless of what the prop is. So it would make sense to move the deletion to the top and take those conditions out:

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    if (value === "") {
      delete collection[id][prop];
    } else if (props !== "tracks") {
      collection[id][prop] = value;
    } else if (prop === "tracks" && collection[id].hasOwnProperty("tracks") === false) {
      collection[id][prop] = [value];
    } else if (prop === "tracks" && collection[id].hasOwnProperty("tracks")) {
      collection[id][prop].push(value);
    }
  }
  return collection;
}

The two conditions dealing with "tracks" are very similar - these can be merged, so that each condition now has only one check:

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    if (value === "") {
      delete collection[id][prop];
    } else if (props !== "tracks") {
      collection[id][prop] = value;
    } else if (prop === "tracks") {
      if(collection[id].hasOwnProperty("tracks")) {
        collection[id][prop].push(value);
      } else {
        collection[id][prop] = [value];
      }
    }
  }
  return collection;
}

Now, objects assigned to variables are referenced, rather than attached directly. If you have a variable like var str = "String", str points directly at "String". But with objects, this is not the case. Why is this relevant? Well, if I have an object like this:

var myObj = {
  foo: 1,
  bar: 2,
  baz: { quux: 3 },
}

If I assign a variable like this:

var baz = myObj.baz

This is pointing at the nested object inside myObj. If I modify my variable baz, I will modify the original object. Like so:

> baz.quux = 100
> myObj
{ foo: 1, bar: 2, baz: { quux: 100 }}

With this in mind, I can make the function a little more readable:

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    var album = collection[id];

    if (value === "") {
      delete album[prop];
    } else if (props !== "tracks") {
      album[prop] = value;
    } else if (prop === "tracks") {
      if(album.hasOwnProperty("tracks")) {
        album[prop].push(value);
      } else {
        album[prop] = [value];
      }
    }
  }
  return collection;
}

The "tracks" property functionality is very similar as well: it might be nice to flatten out the conditional statements a bit. We're three levels deep at that point, and it might be good to drop that to two by moving the functionality out:

function setArrayProperty(object, prop, value) {
  if (object.hasOwnProperty(prop) && Array.isArray(object[prop])) {
    object[prop].push(value)
  } else {
    object[prop] = [value]
  }
}

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    var album = collection[id];

    if (value === "") {
      delete album[prop];
    } else if (prop !== "tracks") {
      album[prop] = value;
    } else if (prop === "tracks") {
      setArrayProperty(album, prop, value);
    }
  }
  return collection;
}

NOTE: the tasts will fail with the object above. The solution should not have collection as the first argument, instead use:

function setArrayProperty(object, prop, value) {
  if (object.hasOwnProperty(prop) && Array.isArray(object[prop])) {
    object[prop].push(value)
  } else {
    object[prop] = [value]
  }
}

function updateRecords(collection, id, prop, value) {
  if (collection.hasOwnProperty(id)) {
    var album = collection[id];

    if (value === "") {
      delete album[prop];
    } else if (prop !== "tracks") {
      album[prop] = value;
    } else if (prop === "tracks") {
      setArrayProperty(album, prop, value);
    }
  }
  return collection;
}
@Vijayanandkrishnan
Copy link

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment