My First Ruby Gem!

Posted by allan-glasier on May 1, 2018

You know that scene where Dr. Frankenstein flips the switch and starts screaming “Its ALIVE!!!” well today this is basically the same feeling I had when my gem ran without a hitch! I created my first monster little bits at a time as I rummaged through Google and previous labs. It is such a great feeling to be able to take an idea and turn it into a reality.

I am going to share the process I went through to create this little bundle of joy. I am also going to share the snags I ran into and how I went about overcoming them.

Planning Phase

It took a while for some ideas to pop into my head. I wanted to create something that would actually be useful for me and eventually turn into something bigger once I learn more. My first idea was to scrape game ratings from Metacritic.com but this seemed a little too much for this project. I would have to create multiple filters for different platforms or change data based on user score and Metacritic score. I could always just keep it simple and focus on one platform and score metric but I’m a little OCD and if I’m going to do it, I’m going to do it right damnit! My wife and I were talking about movies and then it came to me, a simple app to check our local theatre listings and provide information. We go to movies all the time so I’d definitely use that and eventually I could turn it into a little mobile app for quick access to showtimes.

Now that I have the idea, it is time to come up with how I’d like to interact with it. If I’ve learned nothing else from Avi’s videos, it’s that writing down how you want to interact with the program and what sort of output you expect from it first, paves the way for a guide when you are building the app. Here was my rough outline to go by:

  • Display numbered list of movies playing
  • Provide a menu that lets you choose a movie to learn more, display the list again or exit
  • When a movie is chosen then display: movie title, runtime, showtimes, rating, rating description, synopsis
  • When exiting the program, give a farewell message.

Given this info I know I’m going to need the following:

  • CLI class to handle the interface
  • Showtimes class to gather data from the website and assign it to an array for the CLI class
  • Showtimes class will need the following attr_accessors: movie, runtime, time, rating, rating_description, and synopsis

Now we have all that out of the way, time to start development!

Be forewarned, this is where things get technical!

Development Phase

Time to run:

bundle gem cineplex_showtimes

Now that the skeleton of my gem has been generated, it is time to flesh it out. I’ll start with the CLI portion.

CineplexShowtimes::CLI

#call

This method runs the gem once it is loaded. This will initialize getting the data needed to display the list of movies and providing a menu for the user to interact with.

#getshowtimelist

This method goes over into CineplexShowtimes::Showtimes and gathers the data we need for this gem to work

#showtimelist

This method iterates through the data we get from #getshowtimelist and displays it in a way the user can read

#menu

This method covers all of the user interaction and displays all the information to the user. I’ve set this up to display the movies currently playing in a numbered list like the following:

1. Avengers: Infinity War
2. Black Panther
3. Rampage
4. Ready Player One
5. A Quiet Place
6. Blockers
7. Blumhouse's Truth Or Dare
8. I Feel Pretty
9. Indian Horse
10. Super Troopers 2

You pick which movie you want to learn more about by its number and it will display something that looks like this:

Avengers: Infinity War | 2h 30m

Rated: "PG" for Frightening Scenes, Violence, Not Recommended for Young Children

Showtimes: 10:35PM, 10:35PM, 11:00PM

Synopsis:
An unprecedented cinematic journey ten year in the making and spanning the entire Marvel Cinematic Universe, Marvel Studios' AVENGERS: INFINITY WAR brings to the screen the ultimate, deadliest showdown of all time. The Avengers and their Super Hero allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.

The menu also allows you to display the list again by typing “list” or exiting the gem by typing “exit” which will give a farewell message.

CineplexShowtimes::Showtimes

Now we have the easy class out of the way which was pretty straightforward. Time to build the class that gave me the most issues!

.showtimes

This method simply calls the .scrapeshowtimes method to get an array to give to the CineplexShowtimes::CLI class.

.scrapeshowtimes

This method is what does all the work needed to provide the user with information. It uses Nokogiri to collect the html from my local Cineplex movie theatre website. The easiest way to explain this method is to simply show you my code with the comments:

def self.scrape_showtimes
    # sets date to pass into html method below
    date = DateTime.now.strftime("%m/%d/%Y")    
    # sets domain to be used when looking up synopsis
    domain = "https://www.cineplex.com"
    # sets html to the list of showtimes for the current day
    html = Nokogiri::HTML(open("https://www.cineplex.com/Showtimes/any-movie/cineplex-odeon-devonshire-mall-cinemas?Date=#{date}"))    
    #iterate over each showtime grabbing the details of each and putting it into array
    html.css(".showtime-card").each do |movie_object|
      # grabs move detail page link for later use    
      synopsis_link = movie_object.css(".movie-details-link-click").attribute("href").value
      # regex to isolate time as scrape returns a bunch of blank spaces
      regex = /\d{1,2}:\d{2} [AP]M/
      # uses the regex to isolate times and add them to an array
      time_array = movie_object.css(".showtime--list a").text.scan(regex)
        showtime = self.new
        # grabs movie title     
        showtime.movie = movie_object.css(".h3 a").text.strip,
        # grabs movie runtime
        showtime.runtime = movie_object.css(".h3 span").text.gsub("| ", ""),
        # converts time to date objects, sorts array, converts back to strings
        showtime.time = time_array.collect{|t| Time.parse(t)}.sort.collect{|t| t.strftime("%I:%M%p")},        
        # grabs movie rating
        showtime.rating = movie_object.css(".movie-header-details p meta").attribute("content").value,
        # grabs description of rating
        showtime.rating_description = movie_object.css(".movie-header-details p:first-child").text.strip,
        # goes into movie's page and grabs the synopsis
        showtime.synopsis = Nokogiri::HTML(open("#{domain}#{synopsis_link}")).css(".md-movie-info div:first-child p").text
        # throw the collected showtime into array of all showtimes
        @@showtimes << showtime      
    end
    # returns array
    @@showtimes       
  end

Now the things I struggled with here was first getting the date to display correctly. I needed this for the gem to lookup the showtimes for the current day. DateTime.now displays something a little hard to read: #<DateTime: 2018-04-30T22:52:25-04:00 ((2458240j,10345s,429479000n),-14400s,2299161j)>

To display something a little more readable I used DateTime.now.strftime("%m/%d/%Y") which provides this:

"04/30/2018"

What had me stuck for the longest time was I originally was using a lowercase “y” which returns:

"04/30/18"

After about 1-2hrs of searching I found I kept overlooking the capital “Y” which outputs the 4 digit year. (facepalm)

The argument that gave me the most trouble was the getting the showtimes correct as well as sorted. When scraping the site the showtimes returned looking like this:

"\r\n                                            7:00 PM\r\n\r\n\r\n                                        \r\n                                            \r\n                                        \r\n
                                 10:35 PM\r\n\r\n\r\n                                        \r\n                                            \r\n                                        \r\n
               5:45 PM\r\n\r\n\r\n                                        \r\n                                            6:15 PM\r\n\r\n\r\n                                        \r\n
           9:15 PM\r\n\r\n\r\n                                        \r\n                                            9:45 PM\r\n\r\n\r\n                                        \r\n
       11:00 PM\r\n\r\n\r\n                                        "

First I tried to remove basically everything but integers and letters but then all my dates were squished together like this:

7:00PM10:35PM5:45PM6:15PM9:15PM9:45PM11:00PM

I couldn’t for the life of me figure out how to split the times into an array without losing anything. I could use .split("M") and my times are separated into an array but I lost all of my “M”s.

I moved on to using this crazy thing here:

.gsub(" ", "").gsub("\n", "").split("\r").reject(&:empty?)

This removed all of the blank space, removes all of the \n and splits the array at \r which removed \r. This gave me the times the way I wanted but also a whole bunch of empty objects. This is where .reject came in and got rid of any objects in the array that were empty. This left me with an array of times; now all I needed to do was sort the times from earliest to latest and all is well right? Well, since the times are strings, they don’t sort the way you would expect. After another hour looking into this I learned I just needed to convert the times into Time objects then I’d be able to sort them. In order for them to display correctly, I converted them back to a string after they were sorted. This is where my second crazy addition to this line of code comes in:

.collect{|t| Time.parse(t)}.sort.collect{|t| t.strftime("%I:%M%p")}

This iterated through all of my times converting them one by one, sorts them, then iterates again converting them back to strings. To explain the last bit %I gives the hour in 12hr time, %M gives the minutes, p gives you AM/PM

Now after having spent several hours getting the time to finally work how it should, it was time to get into the next phase!

Testing Phase

Things are working as I was writing out my code; time to run the full gem to see how things go!

ruby\bin\cineplex_showtimes

Here we go! Its about to run perfectly!

1. Avengers: Infinity War
2. Black Panther
3. Rampage
4. Ready Player One
5. A Quiet Place
6. Blockers
7. Blumhouse's Truth Or Dare
8. I Feel Pretty
9. Indian Horse
10. Super Troopers 2

Enter the number of the movie you would like to know more about, type 'list' to display the list of showtimes again or type 'exit'

There it is! My beautiful menu, lets see when The Avenegers is playing

1

Avengers: Infinity War | 2h 30m

Rated: "PG" for Frightening Scenes, Violence, Not Recommended for Young Children

Showtimes:

Synopsis:
An unprecedented cinematic journey ten year in the making and spanning the entire Marvel Cinematic Universe, Marvel Studios' AVENGERS: INFINITY WAR brings to the screen the ultimate, deadliest showdown of all time. The Avengers and their Super Hero allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.

Enter the number of the movie you would like to know more about, type 'list' to display the list of showtimes again or type 'exit'

Awesome! it works! I finally made my first…wait…WHAT! What happened to the showtimes!! The one thing that gave me the most greif; the one thing I spent so long getting to work, is now broken! As I’m about to throw my monitor I look to the time and see that its almost midnight. No movies playing at this time of night means no showtimes being displayed. I hardcoded the date to tomorrow’s date; phew, times are now showing. For now I’m leaving my app as is for a today only movie lookup, but will be revisiting it later to allow multiple day lookups as well as multiple theatres. I just have to work out how I want to do the interface, possibly might wait until the JavaScript portion and make it web based.

Now that everything is working, time to take a look at my code and see what I can improve on

Cleanup Phase

One thing that has been bugging me was that goofy mess:

.gsub(" ", "").gsub("\n", "").split("\r").reject(&:empty?).collect{|t| Time.parse(t)}.sort.collect{|t| t.strftime("%I:%M%p")}

There has to be a better way to do this. After a couple hours of looking into this and many talks on slack I found my frist refactor: tring.gsub(/[\s]/, "").split("M").map{|x| x + "M"} I clear out any whitespace character which brings me back to where I originally started. The split with “M” gives me the times i need in an array but removes all of the “M”s. I realise I can just add it back with .map. This is still pretty ugly but an improvement.

After a while I get a response on slack from Charles Lee; a software engineer at Shopify! He shows me the ways of regex, you can use a regex pattern to pick exactly what data you want to grab from the string and then use it with .scan to create the array i’m looking for.

regex = /\d{1,2}:\d{2} [AP]M/
goofy_time_string.scan(regex)
=> ["7:00 PM", "10:35 PM", "5:45 PM", "6:15 PM", "9:15 PM", "9:45 PM", "11:00 PM"]

That’s so much better, now instead of putting it all on one line I make things a lot clearer doing the following:

regex = /\d{1,2}:\d{2} [AP]M/
time_array = movie_object.css(".showtime--list a").text.scan(regex)

#then in the loop
showtime.time = time_array.collect{|t| Time.parse(t)}.sort.collect{|t| t.strftime("%I:%M%p")},

I feel there’s a better way to clean this code up but I’ve failed to find a better solution so far. I have not given up however!

Finally Get to Sleep Phase

I’ve finally made it! I’ve made my first working Ruby Gem and the code even looks half decent; at least to me. It feels like I’ve come so far since I’ve started at Flatiron; even just last week, I couldn’t imagine I’d be doing what I’ve just done already!

If you have made it this far, I want to thank you for taking the time to read my post. It sure is a long winded one and admittedly dry especially with the technical bits. Unfortunately, I don’t know how to do that in a more fun way. If you would like to see my little creation at work please check out my walkthrough below.

https://www.youtube.com/watch?v=MUKd3aZNidc&feature=youtu.be

Cheers!