giant robots smashing into other giant robots

Written by thoughtbot

highwaybobbery

Date and time formats for pirates

Need to display a date in your Rails app? First try:

Time.now.strftime "arrrround %H'ish"

To remove duplication, you imagine a time format initializer. Second try:

Time.now.to_s :pirate

Then you remember the :default localized key. Third time’s a charm:

l Time.now

It formats based on the config/locales/en.yml file:

en:
  time:
    formats:
      default: "arrrround %H'ish"
  date:
    formats:
      default: "arrrround %H'ish"

Dates work, too:

l Date.today

The same system lets you alter the display of “days ago” and “minute from now.”

dancroak

Time.now is on my side

I needed an open? method. First try:

def open?
  opens_at < Time.now < closes_at
end

However, Ruby doesn’t support that kind of expression. Second try:

def open?
  (opens_at < Time.now) && (Time.now < closes_at)
end

It’s noisy and lacks expression. Third time’s a charm:

def open?
  Time.now.between? opens_at, closes_at
end

Ship it.

Written by .

gabebw

Custom formats for DateTime

Date::DATE_FORMATS is quite helpful. It lets you do this:

Date.today.to_s(:custom)

with only this code in config/initializers/date_format.rb:

Date::DATE_FORMATS[:custom] = "%Y-%m-%d"

To do the same thing for DateTime instances, like created_at columns, use Time::DATE_FORMATS.

Time::DATE_FORMATS

To set the default format for either one, set DATE_FORMATS[:default]. This will cause <%= item.created_at %> to output “2011-08-16”, with no extra work from you.

dancroak

Custom date and time formats in Rails

We wanted to shorten the “about 1 month ago” text in an information-rich table that was squeezing created_at data.

wrapping onto multiple lines

We can override distance_in_words with the i18n API (and, by extension, time_ago_in_words).

Speaks English real good

A quick change to config/locales/en.yml and we’re done:

en:
  datetime:
    distance_in_words:
      less_than_x_seconds:
        other: '1 minute'
      half_a_minute: '1 minute'
      less_than_x_minutes:
        one: '1 minute'
      x_minutes:
        one: '1 minute'
        other: '{{count}} minutes'
      about_x_hours:
        one: '1 hour'
        other: '{{count}} hours'
      about_x_months:
        one: '1 month'
        other: '{{count}} months'
      about_x_years:
        one: '1 year'
        other: '{{count}} years'
      over_x_years:
        one: 'over 1 year'
        other: 'over {{count}} years'

The result:

fits on one line

You can see other options in svenfuchs/i18n.

Written by .

dancroak

Test-driven Rails helper

This is how I unit tested a view helper to format a date range in a view.

I wrote the interface for the view helper I wanted, but which did not exist yet:

= format_date_range(course.date_range)

The outer integration tests (not shown) failed with NoMethoError: format_date_range.

I generated a new helper file:

script/generate helper date_time

It produced:

# app/helpers/date_time_helper.rb
module DateTimeHelper
end

# test/unit/helpers/date_time_helper_test.rb
require 'test_helper'

class DateTimeHelperTest < ActionView::TestCase
end

I wrote the unit test:

should 'format date range on same day' do
  eight_oclock = DateTime.new(2009, 10, 12, 8)
  nine_oclock = DateTime.new(2009, 10, 12, 9)
  date_range = eight_oclock..nine_oclock
  expected = 'October 12, 2009'

  assert_equal expected, format_date_range(date_range)
end

This style is flat test structure, intention-revealing temporary variables, respect an 80-character line limit.

I made it pass:

def format_date_range(date_range)
  first = date_range.first
  last  = date_range.last

  if same_day?(first, last)
    "#{first.to_s(:month_day)}, #{first.year}"
  end
end

private

def same_day?(date_one, date_two)
  date_one.day == date_two.day
end

Like the test, I used intention-revealing temporary variables and a private method for a more expressive question in the conditional.

I wrote another test case:

should 'format date range on different days of same month' do
  monday = DateTime.new(2009, 10, 12)
  tuesday = DateTime.new(2009, 10, 13)
  date_range = monday..tuesday
  expected = 'October 12-13, 2009'

  assert_equal expected, format_date_range(date_range)
end

I made it pass:

def format_date_range(date_range)
  # ...
  elsif same_month?(first, last)
    "#{first.to_s(:month_day)}-#{last.day}, #{last.year}"
  end
end

private

def same_month?(date_one, date_two)
  date_one.month == date_two.month
end

Rails date and time formatting is done in locales:

en:
  date:
    formats:
      long: ! '%B %d, %Y'
      month_day: ! '%B %d'
      short: ! '%b %d'

I wrote another test case:

should 'format date range on days of different months' do
  october = DateTime.new(2009, 10, 31)
  november = DateTime.new(2009, 11, 1)
  date_range = october..november
  expected = 'October 31-November 01, 2009'

  assert_equal expected, format_date_range(date_range)
end

I made it pass:

else
  "#{first.to_s(:month_day)}-#{last.to_s(:month_day)}, #{last.year}"
end

I wanted to remove the leading zero sucks. We could use `%e instead of %d, which replaces the leading zero with a space:

expected = "October 31-November  1, 2009"

That makes the test look a little ugly but since the output will be HTML, the extra space is fine.

Make it pass:

month_day: ! '%B %e'

Written by .