Oh, The Problems You’ll Solve: My First Ruby App
I’m making my first CLI application this week, and the entire process is a learning experience. It’s one thing to create some objects in Ruby and give them the power to interact behind the scenes. But tying that functionality to a user interface adds a whole new set of problems, strange problems that I didn’t anticipate.
My programming partner and I are creating a travel application that allows users to save trips they are planning to take and find other users who have already traveled to the same cities. Users can browse places of interests in the cities they are planning to visit and lookup reviews written by other users.
I ran into some very strange problems when I was building a feature of the application that allows user to edit the departure and return dates on their trips. Both dates present their own respective problems, but I’m going to focus on the departure date here because it is more complex.
When beginning to build the option to edit the user’s departure date, I faced the following problems:
- How can I ensure that the user always provides a date in a format that our program can use?
- How can I limit a user to only select a date after today?
- How can I prevent the user from setting the departure date for after their arrival date?
I solved these initial problems using TTY::Prompt.
TTY::Prompt makes it easy to control user input with a variety of different prompt options. I chose the slider because I knew there were going to be a lot of options to choose from when selecting a day of the month. Below is the code for the slider pictured above:
prompt = TTY::Prompt.newyear = prompt.slider("Year", min: 2020, max: 2030, step: 1, default: 2020)
That solved the first problem. No matter what, the user will always provide input that the program can interpret.
To solve the second problem, I used the
max: settings you can see in the code block above along with Ruby’s
Time class, which allows you to get the current date and time by calling the
today = Time.now
I then extracted the current year, month, and day using the following handy methods:
min_year = today.year
min_month = today.month
min_day = today.day
Now I could use these values as the minimum options on the slider. I also needed to have maximum values to prevent the user from setting the departure date after their return date. I got these using the same methods on a different source: the trip itself.
max_month = trip.return_date.month
max_day = trip.return_date.day
max_year = trip.return_date.year
From here, I have the ability to set the minimum and maximum values of my slider to align with both the current day and the return date.
But the fun was only beginning. The minimum and maximum boundaries I established above only apply to some cases. If the user selects next year and the return date is set for the year after that, they should be able to choose any month they want to.
Also, the maximum day value will change based on the month. For the month of February, the maximum day value depends on which year it is.
I solved this last problem with a method that takes in the value of the month and year and returns a number of days. My favorite line from this method comes courtesy of my pair programming partner, Jack:
if year % 4 != 0 || (year % 100 == 0 && year % 400 != 0)
The above line is for the month of February. The first part,
if year % 4 != 0, accounts for what most people understand about leap years. This says that if the year is not divisible by 4, then return 28.
The second part accounts for something I did not know about leap years before today; every 400 years, a year that is divisible by 4 is not a leap year.
Anyway, that took care of the months and days problem. Next, I had to create a very complicated tree of conditional statements to account for all possible cases that could arise when trying to set a departure date.
I won’t bore you with every detail of that tree, but we can follow one case.
Let’s say the current day is 2020–03–04 and the return date is set at 2020–05–24. I set up the program to automatically set the year when the current year is the same as the return date’s year.
After that we hit this
if min_month == max_month
If this statement applied to our case, meaning the current month was the same as the return month, the month would also be automatically set.
But this does not apply to our situation, May is not March, so we move on to the else statement, which gives us the following slider:
month = prompt.slider(“Month”, min: min_month, max: max_month, step: 1)
min_month is the current month (March) and the
max_month is the return date’s month (May)
Now we need to choose a month. Let’s choose April. April (actually, the number 4) gets sent into the above mentioned method that spits out how many days are in the month of April and then that value is assigned to the
max_day = max_day_for_month(month, year)
day = prompt.slider(“Day”, min: 1, max: max_day, step: 1)
Now the user has their choice of any day in April to depart on their trip.