skip to content

Time, Date & Zones

Creating and formatting instants with Time.now, Time.at and strftime, converting with utc and localtime, and Date from the date library. Interviewers probe zone bugs and measuring durations correctly.

on this pageshow

explore

questions

6

In Ruby, how do you get the current time, turn a Unix timestamp into a Time, and format it with strftime or iso8601?

level: juniorimportance: must knowfreq 62%

answer

  1. one class, no require
  2. epoch seconds in, epoch seconds out
  3. Time.at and to_i
  4. %m is month, %M is minute
  5. iso8601 moved into core in 3.4

basics

~20 s

Time.now returns the current instant in the local zone; Time.at(seconds) builds a Time from Unix epoch seconds and to_i converts back. strftime formats with directives such as %Y-%m-%d %H:%M, and iso8601 returns a machine-readable 2026-05-28T20:26:40Z string.

solid answer

~40 s

`Time` is a core class, so nothing needs requiring. `Time.now` reads the system clock and returns an instant shown in the process's local zone; `Time.at(1_780_000_000)` turns **Unix epoch seconds** into a `Time`, and `t.to_i` turns it back (`to_f` keeps the fraction). `Time.at` takes seconds, not milliseconds, and `in: "UTC"` picks the display zone. For humans, `strftime` builds text from directives: `%Y-%m-%d %H:%M:%S`, where `%m` is the month and `%M` the minute. For machines, `iso8601` (an alias of `xmlschema`) gives `2026-05-28T20:26:40Z` for a UTC time or `...+02:00` otherwise, and `iso8601(3)` adds milliseconds. Since Ruby 3.4 `Time#iso8601` is core; the parsing side, `Time.iso8601(string)` and `Time.parse`, still needs `require "time"`.

code

ruby · 11 lines
ruby
t = Time.at(1_780_000_000, in: "UTC")
t.to_i                           # => 1780000000
t.strftime("%Y-%m-%d %H:%M")     # => "2026-05-28 20:26"
t.strftime("%H:%m")              # => "20:05"  (%m is the month)
t.iso8601                        # => "2026-05-28T20:26:40Z"
t.iso8601(3)                     # => "2026-05-28T20:26:40.000Z"
t.getlocal("+02:00").iso8601     # => "2026-05-28T22:26:40+02:00"

ms = 1_780_000_000_250           # a millisecond timestamp
Time.at(ms / 1000, ms % 1000, :millisecond, in: "UTC").iso8601(3)
# => "2026-05-28T20:26:40.250Z"

go deeper

for a junior

Recall Time.now, Time.at with epoch seconds, to_i, and the core strftime directives. Say out loud that %m is the month and %M the minute.

for a middle

Explain that a Time is an instant plus a display zone, why the same to_i prints differently per server, and why iso8601 with an offset or Z is the interchange format.

for a senior

Show the production habits: log and serialise in ISO 8601 with an offset, normalise millisecond inputs at the boundary, and format for users only at the edge.

for a principal

Frame formatting as a contract: pick one serialisation for every service boundary, forbid zone-less strings in APIs, and make the parse side strict so bad timestamps fail loudly.

## What a Time object is A **`Time`** in Ruby represents one **instant** on the timeline. Internally it is a count of seconds, with sub-second precision down to nanoseconds, since the **Unix epoch** (1970-01-01 00:00:00 UTC), plus a **zone mode** that decides how that instant is displayed: local time, UTC, or a fixed offset such as `+09:00`. `Time` is a core class, so no `require` is needed to create, compare or format one. - `Time.now` reads the system clock and returns the current instant in the **local zone** of the process, which comes from the operating system or the `TZ` environment variable. - `Time.new` with no arguments is the same as `Time.now`; with components, `Time.new(2026, 9, 30, 14, 5)` builds that wall-clock time in the local zone. - `Time.utc(2026, 9, 30, 14, 5)` interprets the same components as UTC. - Since Ruby 3.1, `Time.now(in: "+09:00")`, `Time.at(..., in: "UTC")` and `Time.new(..., in: ...)` choose the zone at construction. ## From and to Unix epoch seconds Databases, tokens, log lines and HTTP APIs often carry a **Unix timestamp**: whole seconds since the epoch. 1. `Time.at(1_780_000_000)` turns epoch seconds into a `Time`, displayed in the local zone. 2. `Time.at(1_780_000_000, in: "UTC")` is the same instant, displayed in UTC. 3. `t.to_i` returns whole seconds as an `Integer`; `t.to_f` keeps the fraction as a `Float`; `t.to_r` keeps it exactly as a `Rational`. Two traps live here. First, `Time.at` takes **seconds**. A millisecond timestamp from a browser passed straight in lands tens of thousands of years in the future; divide first, or pass the remainder with the unit form `Time.at(ms / 1000, ms % 1000, :millisecond)`. Second, the wall clock you see depends on the machine's zone: two servers print different hours for the same `to_i`. That is only display: comparison and arithmetic work on the instant, so `Time.at(x) == Time.at(x, in: "UTC")` is `true`. ## Formatting for people with strftime `Time#strftime(format)` builds a string from **directives**, each starting with `%`. For `Time.new(2026, 3, 5, 14, 7, 9, "+01:00")`: | Directive | Meaning | Output | |---|---|---| | `%Y` | year with century | `2026` | | `%m` | month, zero-padded | `03` | | `%d` | day of month, zero-padded | `05` | | `%H` | hour 00-23 | `14` | | `%M` | minute | `07` | | `%S` | second | `09` | | `%z` | offset as `+HHMM` | `+0100` | | `%:z` | offset as `+HH:MM` | `+01:00` | | `%F` | same as `%Y-%m-%d` | `2026-03-05` | | `%T` | same as `%H:%M:%S` | `14:07:09` | **Flags** adjust a directive: `%-d` drops the padding (`5`), `%^B` upcases the month name, `%3N` prints three fractional digits (milliseconds) and `%L` does the same. The bug seen most often in review is `%m` (month) typed where `%M` (minute) was meant: `"%H:%m"` silently prints the month after the hour. `%Z` prints a zone name, but the name is platform-dependent and ambiguous (several regions use the same abbreviation), so prefer `%z` or `%:z` whenever someone might parse the string back. ## Formatting for machines with iso8601 When another program reads the value, as in JSON APIs, logs and queues, use **ISO 8601**: - `Time#iso8601`, an alias of `Time#xmlschema`, returns `"2026-03-05T14:07:09+01:00"`. - A time in UTC mode ends in `Z` instead of an offset: `"2026-05-28T20:26:40Z"`. - `iso8601(3)` appends three fractional digits: `"...T20:26:40.000Z"`. Since **Ruby 3.4** these instance methods live in core `Time`; earlier releases added them only after `require "time"`. The *parsing* direction, `Time.iso8601(string)`, `Time.parse` and `Time.strptime`, still comes from the `time` library and still needs that `require`. `Time#to_s` and `inspect` (`"2026-03-05 14:07:09 +0100"`) are for humans and debugging: a space instead of `T` and no colon in the offset. They are not an interchange format. ## Precision and equality `Time` keeps nanoseconds, so two values that print the same second can still differ, and `Time.now == Time.now` is almost always `false`. When a test or a cache key needs second precision, compare `to_i`, or call `round`, `floor` or `ceil`, each taking the number of fractional digits to keep, to get a new `Time` with the sub-second part trimmed. Serialising with plain `iso8601` also drops the fraction, so a value read back from JSON may not `==` the original. ## Checklist for interviews - `Time.now` is local, not UTC; call `utc` or `getutc`, or pass `in: "UTC"`, when you mean UTC. - `Time.at` wants seconds; `to_i` gives seconds back. - `%m` month, `%M` minute, `%S` second, `%L` or `%3N` milliseconds. - Send `iso8601` to machines; keep `strftime` for people. - Store and compare instants; format at the edge.

  • In Ruby, why can Time.at(1_780_000_000) print different hours on two servers?
    `Time.at` builds the same instant everywhere, but a `Time` without `in:` is displayed in the process's local zone, which comes from the system setting or `TZ`. The two objects are equal and have the same `to_i`; only the rendering differs. Pass `in: "UTC"`, or call `getutc`, when the output must be stable.
  • In Ruby, how do you include milliseconds when formatting a Time?
    For ISO 8601 pass a fraction-digit count: `t.iso8601(3)` gives `2026-05-28T20:26:40.250Z`. With `strftime`, `%L` prints milliseconds and `%N` prints fractional digits with a default width of nine, so `%3N` is milliseconds and `%6N` microseconds.

saying these in an interview costs you the question

  • Time.now returns UTC unless you configure otherwise.
  • Time.at expects milliseconds, like a JavaScript timestamp.
  • In strftime, %m means minutes.
  • Time#to_s output is ISO 8601 and fine for APIs.
  • On Ruby 4.0, Time#iso8601 needs require "time" first.
open as a page

A Ruby service sends dentist appointment reminders to patients in several time zones; how should it store and compute those times so daylight-saving changes never shift them?

level: seniorimportance: must knowfreq 42%

basics

~20 s

Store each appointment's local date, time and IANA zone name; resolve them to a UTC instant through a timezone object when scheduling, and build "day before at 18:00" with Date arithmetic in that zone. Fixed offsets and ENV["TZ"] swaps break.

open as a page

In Ruby, how does Time arithmetic in seconds work, and when should you use Date rather than Time or the deprecated DateTime?

level: middleimportance: should knowfreq 45%

basics

~20 s

Time + and - take seconds and return a new Time; Time - Time returns a Float of seconds. For calendar days and months use Date from require "date" (Date + 1, Date >> 1); DateTime is documented as deprecated.

open as a page

In Ruby, why should you time an operation with Process.clock_gettime(Process::CLOCK_MONOTONIC) rather than subtracting two Time.now values?

level: middleimportance: should knowfreq 48%

basics

~10 s

Time.now reads the wall clock, which NTP or an operator can step forwards or backwards mid-measurement. Process.clock_gettime(Process::CLOCK_MONOTONIC) never goes backwards, so the difference of two readings is a reliable elapsed time in seconds.

open as a page

In Ruby, how do Time.parse and Time.strptime from the time library differ, and which should read externally supplied timestamps?

level: middleimportance: should knowfreq 40%

basics

~20 s

Time.parse guesses the format heuristically and fills missing parts from the current date; Time.strptime(string, format) accepts only the declared format and raises ArgumentError otherwise. For external input, prefer strptime or a strict parser such as Time.iso8601.

open as a page

In Ruby, how do Time#utc and Time#localtime differ from getutc and getlocal, and what decides the local zone?

level: middleimportance: should knowfreq 38%

basics

~20 s

Time#utc and Time#localtime convert the receiver in place and return self; getutc and getlocal return a new Time and leave the original alone. The local zone comes from the operating system or the TZ environment variable, for the whole process.

open as a page