“Booking” My First Coding Project

David Ryan Morphew
7 min readOct 26, 2020

What API do you choose to work with if you spend all your time reading old books and looking for “scholarly references” online? The GoogleBooks API, of course!

For my first CLI project, I decided to look for books in my new field of interest, “Object-Oriented Programming”(rather than an old one).

Do these books look up-to-date for Computer Programming? If they’re in English, I bet they use words like “thou,” “wouldst,” and “apothecary.”

With this program, I fetched the top 40 most relevant books (according to the Google search engine) on “Object-Oriented Computer Programming” from the Google Books API.

Once started, the program offers the user the opportunity to browse these books by their titles, and then select one for more details by entering the number of a book that appears next to its title.

That brings up the detailed view of the following book attributes (if they exist in the Google Books API or can be gleaned from it in some way):

  • Title & Subtitle
  • Author(s)
  • Date of Publication
  • Description
  • Some Common (Computer) Languages Covered
  • ISBN Identification
  • EPUB and PDF links (Clickable in Visual Studio Code)

The user can choose to browse the list again, pick another book, or exit the program altogether after any of the prompts.

If said book has an EPUB or PDF link, the user can copy it into the url of a browser (or click the link in Visual Studio Code) for quick access to at least some of the book in a digital format. No book fees or book return worries.

Conan the Librarian

So, how did I get all of those details and lists to print out? Well, I had to learn a lot about dealing with APIs, especially dealing with complicated hash search results and non-standardized data in large APIs like Google Books.

But more to the method: appropriately enough (given the book subject list), I used an object-oriented approach. I created a Bookclass, and I assigned the attributes listed above to each instance of that Bookclass (again, if they existed in the Google Books API in some way) as instance variables.

I also used mass assignment, creating an assignment_hashwith keys for each attribute:

assignment_hash = {}
assignment_hash[:authors] = "book's author"
assignment_hash[:title] = "book's title"
...
book = Book.new(assignment_hash)

Over in the Bookclass, each book initializes with all of the attributes that exist for that book, i.e. each new book instance has these attributes assigned to it when it is created, if those attributes exist for that book:

class Book   attr_accessor :authors, :title, :subtitle, :description,    
:publication_date, :categories, :links, :isbn_nums, :languages
@@all = [] def initialize(assignment_hash) assignment_hash.each {|key, value| self.send(("#{key}="),
value) if value && value != []}
@@all << self end def self.all @@all endend
  • Here, if there is no value given for a certain attribute’s key, it won’t get assigned to the book instance. If the value is an empty array, it also won’t get assigned.

Hurdles

Finding the attributes in the API, formatting the data, and assigning the values, was the tricky part for a few of the books.

—Authors

Did you know that you can put the name of a book’s author as “J K Bowling,” “JOHNNY HANCOCK,” or “J. R. R. McGriddles” and the Google Books API will be fine with all of those?

Do we “engage” with formatting, sir?

I decided that I needed to do a little formatting to fix some of that before adding the author(s) as an attribute to a newly minted book instance.

Take a hash, any book_hashyou want, in a newly created array_of_books = hash["items"] you create from the Google Books API (all the book hashes are stored in an array with the hash-key of “items”), and you’ll need to dig down a bit to get the value of the author-name(s), which are conveniently stored in an array you can access with the following hash-keys: book_hash["volumeInfo"]["authors"].

Now that you’ve got that, you’re going to want to map that array, taking each name in it, splitting it, and capitalizing each name_part, whether it’s the first name / first initial, middle name / middle initial, or last name—oh, and check to see if it is just a single character in need of a period for a name initial—before you join it all back together into a string of the author’s full name:

array_of_books.each do |book_hash|  assignment_hash = {}  assignment_hash[:authors] = book_hash["volumeInfo"] 
["authors"].map do |name|
name.split(" ").map do |name_part| if name_part.length == 1 (name_part += ".").capitalize else name_part.capitalize end end.join(" ") end ...end
  • Here, to account for single letter initials that lack a period, I check to see if the name_part is 1 character in length (“J” and “K” would fit this criterion in the author name of “J K Bowling”). If it is, you add a period to that.

Now you have the name(s) of the author(s) that you can assign to a book instance with mass assignment!

—Languages Covered

By far the most complicated attribute assignment involved common computer languages discussed by a book. The Google Books API does not give you a list of common programming languages that a book discusses, but I thought that was some useful information to have.

Seeing if a book discussed common programming languages involved searching the title, subtitle, and description of each book for mention of those languages. To do this, I first created a list of common programming languages and saved it as a class constant:

PROGRAMMING_LANGUAGES = ["C", "C#", "C++", "Java", "Python", "JavaScript", "Ruby", "Eiffel", "Smalltalk", "R"]

For most of these languages, a REGEX (regular expression) evaluation with the language name followed by a word boundary (\b) was sufficient to find out if the book mentioned that language in the title, subtitle, or description:

assignment_hash[:languages] = PROGRAMMING_LANGUAGES.select do |language|   if [:title, :subtitle, :description].any? do |attribute_key|      assignment_hash[attribute_key].match?(/#{language}\b/)    ...   endend
  • Here, I selected any language if it matched a word in any of the attributes of the title, subtitle, or description. The beauty of Ruby…the methods read like English!

The languages of “C”, “C#”, and “C++” were a bit more complicated to evaluate for with REGEX. “C”, for example, required reassigning the name for evaluation to “\sC[^+#a-z]” to make sure “C#”, “C++” and other words with “C” were not counted:

assignment_hash[:languages] = PROGRAMMING_LANGUAGES.select do |language|   if language == "C"      language = "\sC[^+#a-z]"      [:title, :subtitle, :description].any? do |attribute_key|         assignment_hash[attribute_key].match?(/#{language}/)      end   ...   endend
  • Here, the reassignment of language from "C"to "\sC[^+#a-z]"for the REGEX evaluation is local to the body of the .select method. It doesn’t affect the original array of PROGRAMMING_LANGUAGES or the return of the originallanguage value with the .select method.

Rubular was a life-saver for figuring what what expression worked best for these trickier “C” languages.

Exiting the Program

For this program, I wanted the user to be able to exit after any prompt.

  • If the user does not wish to see the book list the first time, or any other time that they are prompted to see the list by title, they can enter anything other than ‘yes’ or ‘y’ to exit the program.
  • If, after seeing the list of books by titles, the user does not wish to select a book for more details on that book, they can enter ‘exit’ to exit the program.

To make sure that the user could exit at any of these points, without going through a recursive loop of repeated questions and prompts, I created a flag of sorts with the instance variable @full_exit. Upon starting the program, the flag’s value is "no":

def start
@full_exit = "no"
...
end

I also wrote a simple method for exiting the program called exit_program:

def exit_program   puts "\nGoodbye! Feel free to come back!\n".light_blue   @full_exit = "yes"end

If, after seeing the book list by titles, one enters ‘exit’, theexit_program method is called. The goodbye message is output, and the flag,@full_exit, is reassigned the value of "yes".

This change in the @full_exit value keeps the user from seeing the list of book titles prompt again, since the method that prompts the user, menu, will not execute if the @full_exit has a value of "yes":

menu unless @full_exit == "yes"

I’m sure there are simpler, and perhaps better, ways to control the flow of the program, but this was a quick solution to break my program out of the recursive loop if the user wanted to exit the program at this point.

Future Functionality

As mentioned in the README, this CLI has room for expansion in at least two areas (i) book-listing by different attributes and (ii) formatting titles and subtitles.

(i) Book-Listing: currently, books are listed by title before a user can make a book selection. It would not be hard to modify the listing by a different attribute, such as by ‘author’, and one could start with something along the lines of the following CLI method to use with the existing code:

def display_books_by_attribute(attribute)
Book.all.each.with_index(1) do |book, index|
puts "(#{index}) " + book.send("#{attribute}")
end
end
  • The code for the user’s selection of a book inask_user_for_book_choice would need to be modified to use the results of the new display_books_by_attribute() method.

Formatting: formatting for author names has been standardized, but titles and subtitles could also be standardized. I left the titles as they were. In some circumstances this is fine. But for a bibliography, it would be better to have all of them formatted in a standard way.

To standardize titles and subtitles, one could write code that

  1. splits the title string into an array of word elements,
  2. checks to see if a word element is an initial or non-initial article (‘a’, ‘an’, ‘the’) or preposition (e.g. ‘with’)
  3. capitalizes each word element (except for non-initial articles and prepositions)
  4. and joins those word elements back into a string.

Categories: A third (possible) area could involve the use of the “categories” attribute, checking to see if the book’s genres includes “Computers” or anything instead of, or in addition to, “Computers.” I created this attribute, but did not find it useful for this CLI program, since almost every book just had “Computers” listed for its genre category.

Customizing Word-Wrap Output

For certain parts of the output, the word-wrap width is customizable to accommodate varying sizes of your Terminal and fonts.

In the CLI.rb file, you can adjust the width by passing in a larger or smaller integer as the width_argument to the .fit(width_argument) method (in l. 64 and l. 76), which are currently set at a width of ‘80’:

Go here to learn more on the word-wrap .fit method.

Comments Welcome

Please feel free to check out my project and my video demonstration on using it, or to give any feedback you think would be constructive!

--

--

David Ryan Morphew

I’m very excited to start a new career in Software Engineering. I love the languages, frameworks, and libraries I’ve already learned / worked with (Ruby, Rails,