← All posts

May 28, 2026

The Date and Time cheat sheet I keep open in Rails

Time.current vs Time.now, Date.current vs Date.today, time zone parsing, and the rule I follow for every timestamp I touch in a Rails app.

rails ruby time-zones cheatsheet

I’ve debugged enough off-by-one-day bugs to last a career, and almost every one came from the same root cause: someone wrote Time.now or Date.today in a Rails app. This is the page I keep open when I’m working with timestamps.

The two methods that fix 90% of bugs

# illustrative
Time.current   # zone-aware, respects Time.zone
Time.now       # system local time, ignores Time.zone

Date.current   # zone-aware
Date.today     # system local date, ignores Time.zone

If your server is in UTC and Time.zone = "Europe/Paris", Time.now returns a UTC timestamp and Date.today returns the UTC date — which means at 23:30 Paris time you get yesterday’s date in any logic that uses Date.today. That’s the off-by-one.

The rule is small and absolute: in a Rails app, Time.current and Date.current. Always. There are valid reasons to reach for Time.now (you genuinely want the system clock for a benchmark or a logfile name), but they’re rare enough that you should justify each one in a comment.

Parsing user input safely

Never use Time.parse on a string from a form. It uses the system zone. Use Time.zone.parse.

# illustrative
Time.zone.parse("2026-05-04 14:00")
# => Mon, 04 May 2026 14:00:00 CEST +02:00

Same input, system-zone version:

# illustrative
Time.parse("2026-05-04 14:00")
# => 2026-05-04 14:00:00 +0000   (on a UTC server)

The first stores what the user meant. The second silently shifts everything by the timezone offset and you find out in a bug report from a customer in Sydney.

For dates, Date.parse is fine because dates don’t carry zones, but I still prefer pairing it with explicit zone conversion when I need a moment-in-time:

# illustrative
Time.zone.local(2026, 5, 4, 14, 0)

The day-boundary methods

These are the ones I forget the names of and re-google every other week:

# illustrative
Time.current.beginning_of_day  # 2026-05-04 00:00:00
Time.current.end_of_day        # 2026-05-04 23:59:59.999999
Time.current.beginning_of_week # default Monday
Time.current.beginning_of_month
Time.current.beginning_of_year

All have end_of_* partners. All respect Time.zone. For a “today’s records” scope:

# illustrative
scope :created_today, -> { where(created_at: Time.current.all_day) }

all_day returns a range from beginning to end of day. There are also all_week, all_month, all_quarter, all_year. They all take an optional argument for the week-start day.

Zone conversion

A timestamp in the database is UTC (or should be). To display it in the user’s zone:

# illustrative
post.created_at.in_time_zone("America/New_York")
post.created_at.in_time_zone(current_user.time_zone)

To do the reverse — interpret a UTC timestamp as if it were in another zone (rare, usually a sign someone’s data model is wrong):

# illustrative
Time.find_zone!("America/New_York").local_to_utc(naive_time)

Skip the second one unless you’ve thought hard about why.

Formatting

ISO-8601 for anything that crosses a wire (APIs, JSON, queue payloads):

# illustrative
Time.current.iso8601
# => "2026-05-04T14:00:00+02:00"

For human display, strftime. The directives I use weekly:

# illustrative
Time.current.strftime("%Y-%m-%d")        # 2026-05-04
Time.current.strftime("%B %-d, %Y")      # May 4, 2026
Time.current.strftime("%H:%M")           # 14:00
Time.current.strftime("%I:%M %p")        # 02:00 PM
Time.current.strftime("%a, %b %-d")      # Mon, May 4

The - in %-d strips the leading zero. There’s no %-m on every platform, so for cross-platform code I just use %-d and %B together.

Rails also ships preset formats via to_fs(:short), to_fs(:long), to_fs(:db). You can register your own in an initializer:

# illustrative
Time::DATE_FORMATS[:human] = "%b %-d, %Y at %-l:%M %p"
Time.current.to_fs(:human)
# => "May 4, 2026 at 2:00 PM"

Time arithmetic

Rails extends Numeric with the readable form:

# illustrative
3.days.ago
2.weeks.from_now
1.hour.from_now
30.minutes.ago
Time.current + 1.day
Time.current - 6.months

These are zone-aware and DST-aware. 1.day is a Duration, not 86400 seconds, so adding it across a daylight-saving boundary does the right thing. If you write Time.current + 86400 you’ll get a literal 24-hour offset which is wrong twice a year.

Time vs DateTime

In modern Rails: always Time (which is actually ActiveSupport::TimeWithZone once it goes through Time.current or .in_time_zone). DateTime is a relic from when Time couldn’t represent pre-1970 dates on 32-bit systems. That’s not a problem any of us have. Don’t require 'date' for DateTime and don’t store columns as :datetime because of the name — :datetime columns deserialize to Time instances anyway.

The one place DateTime still shows up is to_datetime calls in legacy code or some older gems. Convert back to Time with .to_time and don’t think about it.

Comparisons that bite

# illustrative
Time.current == Time.current   # almost certainly false (microseconds differ)
Date.current == Date.today     # depends on zone vs server local

For “are these the same day,” cast both to Date first:

# illustrative
a.to_date == b.to_date

For “is this within the last hour,” use a range:

# illustrative
(1.hour.ago..Time.current).cover?(post.created_at)

Database storage

Rails stores :datetime columns in UTC regardless of Time.zone. Good. Don’t fight it. The application zone is for display and parsing, not for storage. A timestamp in your database should always be UTC and you should never have to ask “what zone is this column in.”

If you find a column with a zone-naive timestamp (no offset stored), either it’s a bug or it’s a date-of-event field that intentionally carries no zone — like “the meeting is at 2pm wherever you are.” Those are rare; flag them in the schema with a comment.

The rule

Always Time.current and Date.current unless you have a specific reason not to.

That’s it. That’s the whole post. Tape it to your monitor.