In Ruby, how does Time arithmetic in seconds work, and when should you use Date rather than Time or the deprecated DateTime?
answer
- numbers mean seconds
- Time minus Time is a Float
- time + time? TypeError
- a day is not always 86,400 seconds
- Date >> 1 for months
basics
~20 sTime + 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.
solid answer
~40 sCore `Time` knows only seconds: `t + 3600` is an hour later, `t - 0.5` half a second earlier, and `t2 - t1` returns a `Float` of elapsed seconds. Adding two times raises `TypeError` ("time + time?"). Plain Ruby has no `1.day`; that comes from Rails. Because a day is not always 86,400 seconds, `t + 86_400` across a daylight-saving change lands an hour off in wall-clock terms. For calendar logic, `require "date"` and use `Date`: `Date.today`, `date + 1` for the next day, `date >> 1` for the next month (clamped to the month's last day), and `d2 - d1` returns a `Rational` number of days. `Date.new(2026, 2, 30)` raises `Date::Error`, an `ArgumentError` subclass. `DateTime` is documented as deprecated; use `Time`, keeping `DateTime` only for historical calendar-reform dates.
code
ruby · 12 linesrequire "date"
t1 = Time.utc(2026, 9, 30, 12, 0, 0)
t2 = t1 + 90 # => 2026-09-30 12:01:30 UTC
t2 - t1 # => 90.0 (Float seconds)
t1 + t2 # TypeError: time + time?
d = Date.new(2026, 1, 31)
d + 1 # => #<Date: 2026-02-01>
d >> 1 # => #<Date: 2026-02-28> (clamped)
Date.new(2026, 3, 29) - Date.new(2026, 3, 1) # => (28/1)
Date.new(2026, 2, 30) # Date::Error: invalid datego deeper
Recall that Time plus a number adds seconds, Time minus Time gives a Float, and Date needs require "date".
Explain why a day is not always 86,400 seconds, how Date >> clamps to month end, and why DateTime is deprecated in favour of Time.
Show where these bite in production: recurring schedules, billing periods and reminders built from seconds; demonstrate the Date-then-Time rebuild and chained-month drift.
Separate duration logic from calendar logic in the domain model, and set a rule for which type crosses each boundary so teams stop mixing seconds with calendar days.
## Time arithmetic is second arithmetic A Ruby **`Time`** is an instant counted in seconds since the Unix epoch, so its arithmetic operators work in **seconds**: | Expression | Result | |---|---| | `t + 3600` | a new `Time` one hour later, same zone mode | | `t - 0.5` | a new `Time` half a second earlier | | `t2 - t1` | a `Float`: seconds between the two instants | | `t1 + t2` | `TypeError` with the message `time + time?` | | `t1 <=> t2` | compares instants, whatever zone each displays | The result of `+` keeps the receiver's zone mode: a local time plus seconds is still a local time, recomputed for the new instant. Core Ruby has **no** duration units such as `1.day` or `2.hours`; those are Rails extensions, so plain Ruby code writes `24 * 60 * 60` or a named constant. ## Why a day is not 86,400 seconds In a zone with **daylight saving time**, the local day on which clocks move forward has 23 hours and the day they move back has 25. In Europe/Berlin, clocks go forward on 29 March 2026: ```ruby ENV["TZ"] = "Europe/Berlin" t = Time.new(2026, 3, 28, 9, 0, 0) # 09:00 +0100 t + 86_400 # 2026-03-29 10:00:00 +0200 ``` The arithmetic is correct for the instant (exactly 24 hours later), but a person expecting "same time tomorrow" wanted 09:00. That is a **calendar** operation, and seconds are the wrong unit for it. ## Date for calendar logic `Date` lives in the **date** library, a default gem, so it needs `require "date"`. A `Date` is a calendar day with no time of day and no zone. - `Date.today` is today's date in the process's local zone. - `Date.new(2026, 3, 28) + 1` is the next day; `next_day` and `prev_day` do the same. - `date >> 1` moves one month forward and `date << 1` one month back; `next_month` and `prev_month` are named forms. When the day does not exist, the month's last day is used: `Date.new(2026, 1, 31) >> 1` is 28 February. - `d2 - d1` returns a `Rational` number of days, such as `(28/1)`; call `to_i` for an integer. - An impossible date, `Date.new(2026, 2, 30)`, raises **`Date::Error`**, a subclass of `ArgumentError`. - `Time#to_date` and `Date#to_time` convert between the two once `date` is loaded; `Date#to_time` gives local midnight. A common pattern for "same wall-clock time N days later" is therefore: take the `Date`, add days, then build the `Time` again from the new date and the original hour and minute in the right zone. The month clamp has a trap of its own: `(Date.new(2026, 1, 31) >> 1) >> 1` is 28 March, not 31 March, because the second step starts from the 28th. Compute each target from the original date instead of chaining. ## DateTime is deprecated `DateTime` is a subclass of `Date` that adds a time of day and an offset. Its own documentation states that the class **is considered deprecated** and says to use `Time`. There is no runtime warning, so old code keeps working, but new code should not choose it: 1. `Time` is a core class with timezone-object support and the `in:` keyword. 2. `DateTime` has only fixed offsets, never a named zone with daylight saving. 3. Its one remaining niche is historical dates across the Julian-to-Gregorian **calendar reform**, which `Time` (a proleptic Gregorian calendar) cannot model. ## Traps interviewers probe 1. `Time.now - 86_400` is the instant 24 hours ago, not the calendar day "yesterday"; for that use `Date.today - 1`. 2. `Date.today` and `Time.now.to_date` both use the process's local zone, so a server running in UTC changes date hours before or after its users do. 3. "Every month on the 31st" needs a rule for short months: `>>` gives the clamp, but each occurrence must be computed from the anchor date. ## Choosing the right tool - Measuring or adding an exact duration: `Time` and seconds. - "Tomorrow", "next month", "every 1st of the month": `Date`. - A wall-clock appointment on a date: a `Date` plus hour and minute, turned into a `Time` in the right zone when needed. - Durations measured for performance: a monotonic clock, not `Time` subtraction.
- In Ruby, how do you get "the same local time tomorrow" safely across a daylight-saving change?Do calendar arithmetic, not second arithmetic: `d = t.to_date + 1` (after `require "date"`), then build `Time.new(d.year, d.month, d.day, t.hour, t.min, t.sec)`, which is local like `t` (pass `in:` for another zone). The new `Time` gets whatever offset applies on that date, so 09:00 stays 09:00.
- In Ruby, what does Date.new(2026, 3, 29) - Date.new(2026, 3, 1) return, and why that class?It returns the `Rational` `(28/1)`: `Date#-` with another date always returns the difference in days as a `Rational`, a type that can also express fractions of a day when `DateTime` values are involved. Call `to_i` when you need an `Integer`.
saying these in an interview costs you the question
- Adding 86_400 to a Time always gives the same clock time tomorrow.
- Subtracting two Time objects returns an Integer number of days.
- 1.day and 2.hours are part of core Ruby.
- DateTime is the right choice for new code that needs a time of day.
- Date.new(2026, 1, 31) >> 1 raises because February has no 31st.