In Ruby, how do you get the current time, turn a Unix timestamp into a Time, and format it with strftime or iso8601?
answer
- one class, no require
- epoch seconds in, epoch seconds out
- Time.at and to_i
- %m is month, %M is minute
- iso8601 moved into core in 3.4
basics
~20 sTime.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 linest = 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
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.
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.
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.
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.