Time is an instant, Date is a calendar day¶
Level: 201 · anyone about to do arithmetic on a timestamp, or to parse one
One line: a Time is a point on the timeline with an offset attached — add seconds, subtract two for a Float — and a Date is a day on the calendar with no clock: add days with +, months with >> (which clamps January 31 to February 29), and subtract two for a Rational; Python's datetime covers both jobs with timedelta for arithmetic, has no month step, and splits its objects into aware and naive instead of UTC and local.
Time.utc(y, m, d, h) makes an instant flagged UTC; Time.new(y, m, d, h) makes the same wall-clock reading in the local zone, which is whatever TZ says — this program pins ENV["TZ"] = "UTC" before the first Time so its local time is UTC and the output cannot depend on the machine. Time.new(..., in: "+02:00") and getlocal("+02:00") give a fixed offset without a zone database. +/- with a number move by seconds (fractions kept), - between two Times is a Float of seconds, and == compares the instant, so a +02:00 reading of 14:00 equals the UTC 12:00. strftime is Ruby's own implementation — %-d and %e work everywhere — and since Ruby 3.4 iso8601/xmlschema are core; the class-method parsers Time.parse, Time.iso8601 and Time.strptime still come from require "time", though Time.new itself parses a String since 3.2. Date (from require "date") is the calendar: + n is days, >> n and << n are months with end-of-month clamping, Date - Date is a Rational, leap?, valid_date?, wday/cwday/yday/cweek, negative day numbers counting from the end, and Ranges of Dates. DateTime is a Date with a clock; the documentation steers you to Time instead. (Not machine-checked here.)
Verified output of time_and_date_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. Time.utc(2024, 2, 29, 12); class; utc?; zone 2024-02-29 12:00:00 UTC; Time; true; UTC
2. + 86400 adds seconds; later - t is a Float of seconds 2024-03-01 12:00:00 UTC; 86400.0 (Float)
3. + 0.5 keeps subseconds (inspect shows them); usec 2024-02-29 12:00:00.5 UTC; 500000
4. strftime("%Y-%m-%d %H:%M:%S %z"); ("%A %d %B %Y") 2024-02-29 12:00:00 +0000; Thursday 29 February 2024
5. strftime("%j %U %u %I %p %-d %e|") (%-d and %e are Ruby's own) 060 08 4 12 PM 29 29|
6. iso8601 and xmlschema are core methods in 4.0 (no require) 2024-02-29T12:00:00Z; 2024-02-29T12:00:00Z; 2024-02-29T12:00:00.000Z
7. to_i, to_f, to_r are seconds since the epoch 1709208000; 1709208000.0; 1709208000/1
8. Time.at(0) is local time (TZ=UTC here): utc? is false; .utc 1970-01-01 00:00:00 +0000; false; 1970-01-01 00:00:00 UTC
9. Time.new(2024, 2, 29, 12) is local: utc?; zone; == t 2024-02-29 12:00:00 +0000; false; UTC; true
10. Time.new(..., in: "+02:00"); getutc; utc_offset; == t 2024-02-29 12:00:00 +0200; 2024-02-29 10:00:00 UTC; 7200; false
11. t.getlocal("+02:00") is the same instant shown elsewhere 2024-02-29 14:00:00 +0200; hour=14; == t: true
12. t <=> t + 1; == and eql? with an equal Time; equal? -1; true; true; false
13. Time.utc(2024, 2, 30) rolls over; hour 25 does not 2024-03-01 00:00:00 UTC; ArgumentError: hour out of range
14. (t + 0.5).round; t.to_a 2024-02-29 12:00:01 UTC; [0, 0, 12, 29, 2, 2024, 4, 60, false, "UTC"]
15. Time.parse/iso8601/strptime need require "time": before, after false, true
16. Time.iso8601("2024-02-29T12:00:00+02:00").utc; Time.strptime 2024-02-29 10:00:00 UTC; 2024-02-29 00:00:00 +0000
17. Time.new also parses a String (3.2+) 2024-02-29 12:00:00 UTC
18. Date.new(2024, 1, 31); + 1 is a day; >> 1 is a month (clamped) 2024-01-31; 2024-02-01; 2024-02-29; 2025-02-28; 2023-12-31
19. Date - Date is a Rational number of days (30/1) (Rational); to_i=30
20. Date.parse, Date.iso8601, Date.strptime 2024-03-01; 2024-03-01; 2024-03-01
21. Date.leap?(2024, 2100, 2000); valid_date?(2024, 2, 30); Date.new [true, false, true]; false; Date::Error: invalid date
22. wday (Sunday=0), cwday (Monday=1), yday, cweek; DAYNAMES[wday] 3; 3; 31; 5; Wednesday
23. a negative day counts from the end: Date.new(2024, 2, -1) 2024-02-29; 2023-02-28
24. a Range of Dates; step ["2024-01-01", "2024-01-02", "2024-01-03"]; ["2024-01-01", "2024-01-04", "2024-01-07", "2024-01-10"]
25. Date#to_time is local midnight; Time#to_date 2024-01-31 00:00:00 +0000; 2024-02-29 (Date)
26. DateTime exists (DateTime < Date) but Time is the one to use 2024-02-29T12:00:00+00:00; superclass=Date
27. Time and Date deconstruct_keys for pattern matching (3.2+) {year: 2024, month: 2}; matched, hour=12
Reading the output¶
Rows 1–3: the instant prints as 2024-02-29 12:00:00 UTC, adding 86400 gives March 1st, the difference is the Float 86400.0, and + 0.5 keeps the half second. Rows 4–7 are the formatters: strftime, including Ruby's own %-d and %e; iso8601 printing Z for UTC, with iso8601(3) for milliseconds; and to_i/to_f/to_r for seconds since 1970. Row 8 is the local/UTC flag: Time.at(0) is local — under TZ=UTC it prints +0000 rather than UTC and utc? is false — and .utc re-flags it. Rows 9–11 are offsets: Time.new without a zone is local and equals t because the local zone here is UTC; with in: "+02:00" the same digits are two hours earlier as an instant (getutc shows 10:00, the offset is 7200 seconds), so == t is false; getlocal("+02:00") shows t as 14:00 and == t is true, because it is t. Row 12: <=>, == and eql? compare instants; equal? is identity. Row 13 is a surprise worth knowing: Time.utc(2024, 2, 30) rolls over to March 1st without complaint, while hour 25 raises ArgumentError. Rows 15–17 are parsing: Time.parse appears only after require "time", Time.iso8601 honours the offset, Time.strptime gives local time, and Time.new("...") parses a String. Rows 18–19 are the Date claim: + 1 is a day, >> 1 from January 31st is February 29th (clamped, and 2024 is a leap year), >> 13 is 2025-02-28, and the difference of two Dates is (30/1), a Rational. Rows 20–24: the three parsers, leap? for 2024/2100/2000 (true false true), valid_date? false for February 30th and Date::Error: invalid date when you build it anyway, the four day numbers with Date::DAYNAMES for the name, Date.new(2024, 2, -1) for the last day of a month, and Ranges and step. Row 25 converts both ways. Row 26 shows DateTime exists and is a Date. Row 27: Time and Date implement deconstruct_keys, so case t in {year: 2024, month: 2..3, hour:} matches and binds.
Compared with Python¶
The twin pins TZ the same way and uses time.tzset(), then builds datetime(2024, 2, 29, 12, tzinfo=timezone.utc). Arithmetic goes through timedelta in both directions: + timedelta(days=1) and later - t is 1 day, 0:00:00 (rows 2–3), with total_seconds() for the Float Ruby gives directly. strftime is the C library's, so %-d and %e are not portable (row 5); isoformat() writes +00:00 where Ruby writes Z (row 6); timestamp() is a float (row 7). The big structural difference is rows 8–11: Python does not flag UTC versus local but aware versus naive — fromtimestamp(0) and datetime(2024, 2, 29, 12) have tzinfo None, and a naive value is not equal to an aware one and cannot be ordered against it (TypeError, row 9) — where Ruby's Time.new result compared equal to t. A fixed offset is timezone(timedelta(hours=2)) (row 10), and astimezone is getlocal (row 11). Python raises for February 30th and for hour 25 (row 13) where Ruby rolled the day over. fromisoformat and strptime need no import (row 15), and Z is accepted since 3.11 (row 17). date has no month step: row 18's add_months uses calendar.monthrange for the clamp, and date - date is a timedelta (row 19). calendar.isleap is Date.leap?, date(2024, 2, 30) is a ValueError (row 21), weekday() counts from Monday=0 where wday counts from Sunday=0 (row 22), and there is no negative day — monthrange(y, m)[1] is the last day (row 23). One datetime class does both jobs (row 26), and pattern matching reads attributes through a class pattern rather than deconstruct_keys (row 27). Ruby's Rational for Date - Date connects to Rational and BigDecimal; the seconds Float to Floats and rounding.
Verified output of time_and_date_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. datetime(2024, 2, 29, 12, tzinfo=timezone.utc); type; aware?; tzname 2024-02-29 12:00:00+00:00; datetime; True; UTC
2. + timedelta(days=1); later - t is a timedelta 2024-03-01 12:00:00+00:00; 1 day, 0:00:00 (timedelta); total_seconds=86400.0
3. + timedelta(seconds=0.5) keeps microseconds; microsecond 2024-02-29 12:00:00.500000+00:00; 500000
4. strftime("%Y-%m-%d %H:%M:%S %z"); ("%A %d %B %Y") 2024-02-29 12:00:00 +0000; Thursday 29 February 2024
5. strftime("%j %U %u %I %p") (%-d and %e depend on the C library) 060 08 4 12 PM
6. isoformat() writes +00:00, never Z; timespec= 2024-02-29T12:00:00+00:00; 2024-02-29T12:00:00.000+00:00
7. timestamp() is a float; int() of it 1709208000.0; 1709208000
8. fromtimestamp(0) is naive local; tz=timezone.utc makes it aware 1970-01-01 00:00:00; None; 1970-01-01 00:00:00+00:00
9. datetime(2024, 2, 29, 12) is naive: tzinfo; == t; < t 2024-02-29 12:00:00; None; False; TypeError
10. tzinfo=timezone(timedelta(hours=2)); astimezone(utc); utcoffset; == t 2024-02-29 12:00:00+02:00; 2024-02-29 10:00:00+00:00; 2:00:00; False
11. t.astimezone(+02:00) is the same instant shown elsewhere 2024-02-29 14:00:00+02:00; hour=14; == t: True
12. t < t + 1s; == with an equal datetime; hash equal; is True; True; True; False
13. datetime(2024, 2, 30) and hour 25 both raise ValueError; ValueError
14. replace(microsecond=0) instead of round; timetuple()[:6] 2024-02-29 12:00:00+00:00; (2024, 2, 29, 12, 0, 0)
15. fromisoformat, strptime and strftime need no import True, True
16. fromisoformat("...+02:00").astimezone(utc); strptime is naive 2024-02-29 10:00:00+00:00; 2024-02-29 00:00:00
17. fromisoformat accepts a trailing Z (3.11+) 2024-02-29 12:00:00+00:00
18. date(2024, 1, 31); + timedelta(days=1); no month arithmetic: a helper 2024-01-31; 2024-02-01; 2024-02-29; 2025-02-28; 2023-12-31
19. date - date is a timedelta datetime.timedelta(days=30) (timedelta); days=30
20. strptime(...).date(), date.fromisoformat, strptime 2024-03-01; 2024-03-01; 2024-03-01
21. calendar.isleap(2024, 2100, 2000); no valid_date; date(2024, 2, 30) [True, False, True]; n/a; ValueError
22. weekday() (Monday=0), isoweekday() (Monday=1), tm_yday, week; day_name 2; 3; 31; 5; Wednesday
23. no negative day: calendar.monthrange(y, m)[1] is the last day 2024-02-29; 2023-02-28
24. a list of dates by timedelta; a stride ['2024-01-01', '2024-01-02', '2024-01-03']; ['2024-01-01', '2024-01-04', '2024-01-07', '2024-01-10']
25. datetime.combine(d, time()) is naive midnight; t.date() 2024-01-31 00:00:00; 2024-02-29 (date)
26. one datetime class does both jobs; no DateTime split n/a
27. class patterns read attributes: case datetime(year=2024, ...) n/a (no deconstruct_keys); matched, hour=12
Try it¶
- Write
next_leap_day(from)returning the first February 29th afterfrom, using onlyDate.leap?,Date.newand comparison, and print for a few fixed dates how many days away it is (Date - Date, thento_i) and which weekday it falls on (Date::DAYNAMES). - Given
Time.utc(2024, 3, 31, 1, 30), print the same instant withgetlocalat"+01:00","+05:30"and"-08:00", then explain — withoutTime.now— why a fixed offset is not a time zone, and whatin: "Europe/Paris"would need that this program does not have.
Solution to 1
time_and_date_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: for a few fixed dates, the next leap day and how many days away it is,
# with Date arithmetic only -- Date.leap?, Date.new and Date - Date.
require "date"
def next_leap_day(from)
year = from.year
year += 1 until Date.leap?(year) && Date.new(year, 2, 29) > from
Date.new(year, 2, 29)
end
[Date.new(2024, 1, 1), Date.new(2024, 2, 29), Date.new(2099, 6, 15), Date.new(2100, 1, 1)].each do |from|
leap = next_leap_day(from)
puts format("%s -> %s in %4d days (%s)", from, leap, (leap - from).to_i, Date::DAYNAMES[leap.wday])
end
Verified output of time_and_date_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
#{}callsto_s;formatis printf —strftimeis the third format language, beside interpolation andformatJSON.parsegives string keys;YAML.loadis safe — what happens to aTimein JSON (a String) and in Marshal (aTime)Pathname#/joins, andDir.globis sorted —File.mtimereturns aTime- Floats and rounding — the Float that
Time - Timereturns, and whyroundmatters on it - Rational and BigDecimal — the Rational that
Date - Datereturns case/inpattern matching — the hash pattern that row 27 matched aTimeagainst