Morris Date and the Time
While working on our module 1 project at the Flatiron School, my partner Jake and came across Ruby’s DateTime class. Our command line application project involved scraping an API that contained data of all of the live shows performed by the American rock band, the Grateful Dead. There was all kinds of groovy data to grab with the most interesting being the date!
But how do we persist the date data to our database? We were using Active Record as our ORM to persist our model data to our database run on SQLite. Up to that point in our studies, we were primarily dealing with date types of string or integers. My brain had no problem with those data types. A string is a string and an integer is an integer. But a date? A date is a number, but it can also be thought of and used like a string.
Looking at our API data, the date was saved as a string in the form DD-MM-YY. My initial reaction was to save that data as a string to our database since that was its form.
Jake foresaw a crucial problem. If we save the data to our table through Active Record as a string, how will we be able to order our data by date later on the application? Enter Ruby’s Date object. We saved the data as a date data-type with Active Record to our database and took a look into our database using our handy DB browser.
Check out the last entry in the column date. The same date object that we scraped from the API in the format DD-MM-YYYY is now represented in our table in the format YYYY-MM-DD. It’s reversed! Okay. I’m freaking out a little bit. We didn’t tell Active Record or SQLite to manipulate our data. Have we broken our cardinal rule of adding bad data to our table? Will we be able to order the Date objects when we call them from the table? We must have screwed this whole date ordering by time thing up.
Let’s take a look at the info via pry.
Pry is displaying the date data with an abbreviated day of the week, date, abbreviated month, and a year. Another change! Jake and I held our breath and hoped that we could still order these shape shifting dates. To our delight, when we called dates from our data base, we could in fact, order them by date. We were relieved and excited, but still mystified. How could a data type pass through our ruby file, to ActiveRecord, then to our SQL table, back to our Ruby file, changing it’s appearance each time, and still be the SAME Date Object. I did some exploring.
Ruby has three basic Object types that deal with time
1. Date : deals only with what I call ‘everyday’ dates, but in different forms. No hours, minutes or seconds here. They are also known as calendar dates.
2. Time: deals with time in an abstract way compared to GMT. Think hours, minutes without being attached to a calendar.
3. DateTime: A combo of Date and Time, DateTime handles dates attached to a calendar AND deals with hours, minutes, seconds, milliseconds and beyond. Feeling the Milli Vibe
Date
A new date object can be defined in a few different ways. It can be instatiated as a Calendar Date (what I refer to as an ‘everyday’ date above), an ordinal date (the number of the day in the context of a year), a week date, a Julian date (based on the Julian calendar where a year has 365.25 days in a year instead of the 365.2425 that we use now in our Gregorian calendar), and a modified Julian date. For this blog, I’ll stick with a calendar date.
A new Date object is initialized with three integers delineated by commas. Be sure to require ‘date’.
require 'date'
jungle_time = Date.new(1986,10,18)=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>
You can also enter in a date as a string passed to the method #parse which will turn your string into a Date object. Not only that, but different strings that represent the same date will evaluate to true when using the == operator. Check it out:
aday = Date.parse("October 18, 1986")
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>bday = Date.parse("Oct 18, 1986")
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>cday = Date.parse("1986-10-18")
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>dday = Date.parse("18-10-1986")
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>eday = Date.parse("18 October, 1986")
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>[aday, bday, cday, dday, eday].uniq
=> [#<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>]
Pretty cool! Ruby is smart! Ruby knows how to parse a string with a different linguistic representations. And it knows that those objects are the same! However, if you enter a date in the American way (“10–18–1986”), Ruby will yell at you that this is not a valid date and this makes sense. With ordered numbers, Ruby must have the date ordered at the parsing either from least to greatest or greatest to least when no English is passed in at instantiation. In the example above, Ruby reads the string in the DD-MM-YYY format. There is no month that corresponds to the integer 18, so Ruby politely yells at you. ERROR! HAHAHAHAHA!
More cool stuff! You can add, subtract, and compare Date objects.
bday
=> #<Date: 1986-10-18 ((2446722j,0s,0n),+0s,2299161j)>cday = bday + 1
=> #<Date: 1986-10-19 ((2446723j,0s,0n),+0s,2299161j)>bday > cday
=> falsebday == cday
=> falsebday - cday
=> (-1/1)
The return value on the last example is a rational number. It will always be a whole number to account for errors in calculation and things like leap year over time. The return value is of the Rational class. Adding an integer to a Date object increases the date by days. Check it:
cday += 4524623642624562456345634573
=> #<Date: 12387998775127654794681232-12-23 ((4524623642624562456348081296j,0s,0n),+0s,2299161j)>bday - cday
=> (-4524623642624562456345634574/1)
More on the above later.
DateTime
A DateTime object is like a Date object with the added value of time. You can add hours, minutes, seconds, milliseconds….(refrains from milli vanilli gif….) You initialize the object in the same way, integers delineated by commas.
zday = DateTime.new(1996,10,18,9,53,17)=> #<DateTime: 1996-10-18T09:53:17+00:00 ((2450375j,35597s,0n),+0s,2299161j)>
The output separates the date and time with the “T” character. Pretty cool. But Seth, can you compare Date objects and DateTime objects? YOU CAN!!!!
bday - zday
=> (-315654797/86400)# Do the divison to get the difference in days-315654797/86400
=> -3654#Divide by 365 and call the result to a float to get the number of
# years! 10 and some change!-3654/365.to_f
=> -10.01095890410959
Time
The Time object stores time objects abstractly without a rigid connection to a calendar year. Time is stored internally and counts by the fraction of a second since 00:00 (midnight) January 1, 1970, Coordinated Universal Time (a more accurate way of saying Greenwich Mean Time). This is based on Unix Time. Time can be used as a time stamp for when a program executes a function with the call Time.now. Upon initialization, a new Time object again, takes in integer arguments in the same year, month, day, hour, minutes, seconds, milliseconds, etc fashion. It will default to ‘midnight’ if no hour is passed.
xday = Time.new(1986, 10, 18)
=> 1986-10-18 00:00:00 -0400
Does a Time object compare with a Date or DateTime object? NO!
xday = Time.new(1986, 10, 18)
=> 1986-10-18 00:00:00 -04002.3.3 :045 > bday - xday
TypeError: expected numericfrom (irb):45:in `-'from (irb):45from /Users/sethbarden/.rvm/rubies/ruby-2.3.3/bin/irb:11:in `<main>'
But it is pretty simple to convert a Time object to a DateTime object. Just call #parse on a new DateTime object and send in your Time object as a string. Peek:
new_day = DateTime.parse(xday.to_s)
=> #<DateTime: 1986-10-18T00:00:00-04:00 ((2446722j,14400s,0n),-14400s,2299161j)>new_day.class
=> DateTime
The best part about all of these date types is the flexibility that Ruby gives us with formatting to our interface. The #strftime method allows easy flexibility for formatting a Date, DateTime, or Time object to people readable dates as strings. Check out this chart below, then I’ll show you an example.
Date (Year, Month, Day):
%Y - Year with century (can be negative, 4 digits at least)
-0001, 0000, 1995, 2009, 14292, etc.
%C - year / 100 (round down. 20 in 2009)
%y - year % 100 (00..99)
%m - Month of the year, zero-padded (01..12)
%_m blank-padded ( 1..12)
%-m no-padded (1..12)
%B - The full month name (``January'')
%^B uppercased (``JANUARY'')
%b - The abbreviated month name (``Jan'')
%^b uppercased (``JAN'')
%h - Equivalent to %b
%d - Day of the month, zero-padded (01..31)
%-d no-padded (1..31)
%e - Day of the month, blank-padded ( 1..31)
%j - Day of the year (001..366)
Time (Hour, Minute, Second, Subsecond):
%H - Hour of the day, 24-hour clock, zero-padded (00..23)
%k - Hour of the day, 24-hour clock, blank-padded ( 0..23)
%I - Hour of the day, 12-hour clock, zero-padded (01..12)
%l - Hour of the day, 12-hour clock, blank-padded ( 1..12)
%P - Meridian indicator, lowercase (``am'' or ``pm'')
%p - Meridian indicator, uppercase (``AM'' or ``PM'')
%M - Minute of the hour (00..59)
%S - Second of the minute (00..59)
%L - Millisecond of the second (000..999)
%N - Fractional seconds digits, default is 9 digits (nanosecond)
%3N millisecond (3 digits) %15N femtosecond (15 digits)
%6N microsecond (6 digits) %18N attosecond (18 digits)
%9N nanosecond (9 digits) %21N zeptosecond (21 digits)
%12N picosecond (12 digits) %24N yoctosecond (24 digits)
Time zone:
%z - Time zone as hour and minute offset from UTC (e.g. +0900)
%:z - hour and minute offset from UTC with a colon (e.g. +09:00)
%::z - hour, minute and second offset from UTC (e.g. +09:00:00)
%:::z - hour, minute and second offset from UTC
(e.g. +09, +09:30, +09:30:30)
%Z - Time zone abbreviation name or something similar information.
Weekday:
%A - The full weekday name (``Sunday'')
%^A uppercased (``SUNDAY'')
%a - The abbreviated name (``Sun'')
%^a uppercased (``SUN'')
%u - Day of the week (Monday is 1, 1..7)
%w - Day of the week (Sunday is 0, 0..6)
ISO 8601 week-based year and week number:
The week 1 of YYYY starts with a Monday and includes YYYY-01-04.
The days in the year before the first week are in the last week of
the previous year.
%G - The week-based year
%g - The last 2 digits of the week-based year (00..99)
%V - Week number of the week-based year (01..53)
Week number:
The week 1 of YYYY starts with a Sunday or Monday (according to %U
or %W). The days in the year before the first week are in week 0.
%U - Week number of the year. The week starts with Sunday. (00..53)
%W - Week number of the year. The week starts with Monday. (00..53)
Seconds since the Unix Epoch:
%s - Number of seconds since 1970-01-01 00:00:00 UTC.
%Q - Number of milliseconds since 1970-01-01 00:00:00 UTC.
Literal string:
%n - Newline character (\n)
%t - Tab character (\t)
%% - Literal ``%'' character
Combination:
%c - date and time (%a %b %e %T %Y)
%D - Date (%m/%d/%y)
%F - The ISO 8601 date format (%Y-%m-%d)
%v - VMS date (%e-%b-%Y)
%x - Same as %D
%X - Same as %T
%r - 12-hour time (%I:%M:%S %p)
%R - 24-hour time (%H:%M)
%T - 24-hour time (%H:%M:%S)
%+ - date(1) (%a %b %e %H:%M:%S %Z %Y)#source : https://ruby-doc.org/stdlib-2.4.1/libdoc/date/rdoc/Date.html
When using the #strftime method, you pass in an argument of a string with the above symbols for the english date representation that you want Ruby to interpolate for you. For example
my_bday = DateTime.new(1986, 10, 18, 9, 53, 17)
=> #<DateTime: 1986-10-18T09:53:17+00:00 ((2446722j,35597s,0n),+0s,2299161j)>my_bday.strftime('I was born at %H:%M %P and %S seconds on %A %B the %dth, %y.')
=> "I was born at 09:53 am and 17 seconds on Saturday October the 18th, 86."
Pretty slick and it works on all three object types! Jake and I were able to use #strftime to write elegant date strings to our terminal output for our command line application. Happy day!
For other cool methods, check out the documentation here:
Date
DateTime
Time
Back to my original confusion. Why were Jake and I seeing the Date object formatted so differently on each step of our backend? I looked up the documentation in both Active Record and SQLite to no avail. But through good old experimentation and logic, I’ve reached some conclusions. Each framework’s developers made their own choices about how to represent the Date object visually. Just as we can tell a Date object to parse a string and create a date object and reformat it to display how we please, the developers did the same with how they chose to parse and display the Date object’s information in Active Record, SQL DB browser and when using the pry gem. Date, DateTime, and Time objects have the flexibility to handle time in the way that people talk and write about them without compromising the data the instances contain! Our database lives!