Skip to content

Ruby Date & Time

Proper handling of dates and times is crucial in programming. Ruby provides powerful date and time processing capabilities, including built-in Date, Time, and DateTime classes, as well as modern Time class extensions. This chapter will provide a detailed introduction to creating, manipulating, and processing dates and times in Ruby.

🎯 Date and Time Basics

Required Libraries

ruby
# Date and DateTime classes need explicit require
require 'date'

# Time class is built-in, but can include extended functionality
require 'time'

Current Date and Time

ruby
# Get current time
current_time = Time.now
puts current_time  # 2023-12-25 14:30:45 +0800

# Get current date
current_date = Date.today
puts current_date  # 2023-12-25

# Get current datetime
current_datetime = DateTime.now
puts current_datetime  # 2023-12-25T14:30:45+08:00

# Use Time to get date portion
time_date = Time.now.to_date
puts time_date  # 2023-12-25

📅 Date Class

Creating Date Objects

ruby
require 'date'

# Create specific date
date1 = Date.new(2023, 12, 25)
puts date1  # 2023-12-25

# Parse string to create date
date2 = Date.parse("2023-12-25")
puts date2  # 2023-12-25

date3 = Date.parse("December 25, 2023")
puts date3  # 2023-12-25

# Create from epoch day (days since January 1, year 1)
date4 = Date.jd(2451545)  # 2000-01-01
puts date4  # 2000-01-01

# Create from commercial date (year, week, day of week)
date5 = Date.commercial(2023, 52, 1)  # Monday of week 52, 2023
puts date5  # 2023-12-25

# Create from astronomical ordinal day
date6 = Date.ordinal(2023, 359)  # Day 359 of 2023
puts date6  # 2023-12-25

Date Properties and Methods

ruby
date = Date.new(2023, 12, 25)

# Basic properties
puts date.year    # 2023
puts date.month   # 12
puts date.day     # 25
puts date.wday    # 1 (Monday, 0 = Sunday)
puts date.yday    # 359 (day of year)

# Weekday information
puts date.monday?    # true
puts date.sunday?    # false
puts date.strftime("%A")  # Monday

# Month and season
puts date.strftime("%B")  # December
puts date.mday           # 25 (day of month)

# Leap year check
puts date.leap?  # false (2023 is not a leap year)

# Date comparison
date1 = Date.new(2023, 12, 25)
date2 = Date.new(2023, 12, 26)

puts date1 < date2   # true
puts date1 == date2  # false
puts date1 > date2   # false

# Date arithmetic
future_date = date + 10
puts future_date  # 2024-01-04

past_date = date - 5
puts past_date  # 2023-12-20

# Calculate days between two dates
days_difference = (date2 - date1).to_i
puts days_difference  # 1

🕐 Time Class

Creating Time Objects

ruby
# Current time
current_time = Time.now
puts current_time  # 2023-12-25 14:30:45 +0800

# Create specific time
time1 = Time.new(2023, 12, 25, 14, 30, 45)
puts time1  # 2023-12-25 14:30:45 +0800

# Specify timezone
time2 = Time.new(2023, 12, 25, 14, 30, 45, "+09:00")
puts time2  # 2023-12-25 14:30:45 +0900

# Parse string to create time
time3 = Time.parse("2023-12-25 14:30:45")
puts time3  # 2023-12-25 14:30:45 +0800

time4 = Time.parse("December 25, 2023 2:30 PM")
puts time4  # 2023-12-25 14:30:00 +0800

# Create from epoch time
epoch_time = Time.at(1703485845)
puts epoch_time  # 2023-12-25 14:30:45 +0800

# Local time and UTC time
local_time = Time.local(2023, 12, 25, 14, 30, 45)
puts local_time  # 2023-12-25 14:30:45 +0800

utc_time = Time.utc(2023, 12, 25, 14, 30, 45)
puts utc_time  # 2023-12-25 14:30:45 UTC

Time Properties and Methods

ruby
time = Time.new(2023, 12, 25, 14, 30, 45, "+08:00")

# Basic properties
puts time.year    # 2023
puts time.month   # 12
puts time.day     # 25
puts time.hour    # 14
puts time.min     # 30
puts time.sec     # 45

# Weekday information
puts time.wday    # 1 (Monday)
puts time.yday    # 359 (day of year)

# Timestamp
puts time.to_i    # 1703485845 (second timestamp)
puts time.to_f    # 1703485845.0 (floating point timestamp)

# Timezone information
puts time.zone    # +08:00
puts time.utc?    # false
puts time.utc_offset  # 28800 (seconds, +08:00 = 8 * 3600)

# Time comparison
time1 = Time.new(2023, 12, 25, 14, 30, 45)
time2 = Time.new(2023, 12, 25, 15, 0, 0)

puts time1 < time2   # true
puts time1 == time2  # false

# Time arithmetic
future_time = time + 3600  # Add 1 hour
puts future_time  # 2023-12-25 15:30:45 +0800

past_time = time - 1800  # Subtract 30 minutes
puts past_time  # 2023-12-25 14:00:45 +0800

# Calculate time difference
seconds_difference = time2 - time1
puts seconds_difference  # 1755.0 (seconds)
puts seconds_difference / 60  # 29.25 (minutes)

📆 DateTime Class

Creating DateTime Objects

ruby
require 'date'

# Create specific datetime
datetime1 = DateTime.new(2023, 12, 25, 14, 30, 45)
puts datetime1  # 2023-12-25T14:30:45+00:00

# Specify timezone
datetime2 = DateTime.new(2023, 12, 25, 14, 30, 45, "+08:00")
puts datetime2  # 2023-12-25T14:30:45+08:00

# Parse string to create datetime
datetime3 = DateTime.parse("2023-12-25 14:30:45")
puts datetime3  # 2023-12-25T14:30:45+00:00

# Create from Julian day
datetime4 = DateTime.jd(2451545.5)
puts datetime4  # 2000-01-01T12:00:00+00:00

# ISO 8601 format
datetime5 = DateTime.iso8601("2023-12-25T14:30:45+08:00")
puts datetime5  # 2023-12-25T14:30:45+08:00

DateTime Properties and Methods

ruby
datetime = DateTime.new(2023, 12, 25, 14, 30, 45, "+08:00")

# Basic properties
puts datetime.year    # 2023
puts datetime.month   # 12
puts datetime.day     # 25
puts datetime.hour    # 14
puts datetime.min     # 30
puts datetime.sec     # 45

# Precise fractional seconds
puts datetime.sec_fraction  # (0/1) (fractional part of seconds)

# Timezone information
puts datetime.zone    # +08:00
puts datetime.offset  # (1/3) (timezone offset, +08:00 = 8/24 = 1/3)

# Weekday information
puts datetime.wday    # 1 (Monday)
puts datetime.yday    # 359 (day of year)

# Datetime comparison
dt1 = DateTime.new(2023, 12, 25, 14, 30, 45)
dt2 = DateTime.new(2023, 12, 25, 15, 0, 0)

puts dt1 < dt2   # true
puts dt1 == dt2  # false

# Datetime arithmetic
future_dt = datetime + Rational(1, 24)  # Add 1 hour (1/24 day)
puts future_dt  # 2023-12-25T15:30:45+08:00

past_dt = datetime - Rational(30, 1440)  # Subtract 30 minutes (30/1440 day)
puts past_dt  # 2023-12-25T14:00:45+08:00

🔄 DateTime Formatting

strftime Method

ruby
time = Time.new(2023, 12, 25, 14, 30, 45, "+08:00")

# Common formats
puts time.strftime("%Y-%m-%d")           # 2023-12-25
puts time.strftime("%H:%M:%S")           # 14:30:45
puts time.strftime("%Y-%m-%d %H:%M:%S")  # 2023-12-25 14:30:45

# Detailed formats
puts time.strftime("%A, %B %d, %Y")     # Monday, December 25, 2023
puts time.strftime("%a, %b %d, %Y")     # Mon, Dec 25, 2023

# Chinese format
puts time.strftime("%Y年%m月%d日 %H时%M分%S秒")  # 2023年12月25日 14时30分45秒

# 12-hour format
puts time.strftime("%I:%M:%S %p")       # 02:30:45 PM

# With timezone information
puts time.strftime("%Y-%m-%d %H:%M:%S %z")  # 2023-12-25 14:30:45 +0800
puts time.strftime("%Y-%m-%d %H:%M:%S %Z")  # 2023-12-25 14:30:45 +08:00

# Week and week information
puts time.strftime("%A is day %w of the week")  # Monday is day 1 of the week
puts time.strftime("Day %j of the year")        # Day 359 of the year

# ISO 8601 format
puts time.strftime("%Y-%m-%dT%H:%M:%S%:z")      # 2023-12-25T14:30:45+08:00

Parsing DateTime Strings

ruby
require 'time'

# Parse various datetime string formats
time_strings = [
  "2023-12-25 14:30:45",
  "December 25, 2023 2:30 PM",
  "2023/12/25 14:30:45",
  "Mon, 25 Dec 2023 14:30:45 +0800",
  "2023-12-25T14:30:45+08:00"
]

time_strings.each do |str|
  begin
    parsed_time = Time.parse(str)
    puts "#{str} => #{parsed_time}"
  rescue ArgumentError => e
    puts "Cannot parse: #{str} (#{e.message})"
  end
end

# ISO 8601 format parsing
iso_string = "2023-12-25T14:30:45+08:00"
iso_time = Time.iso8601(iso_string)
puts iso_time  # 2023-12-25 14:30:45 +0800

# RFC 2822 format parsing
rfc_string = "Mon, 25 Dec 2023 14:30:45 +0800"
rfc_time = Time.rfc2822(rfc_string)
puts rfc_time  # 2023-12-25 14:30:45 +0800

⏱️ Time Calculations and Operations

Time Arithmetic

ruby
# Time addition and subtraction
time = Time.now

# Add/subtract seconds
future_time = time + 60  # 1 minute later
past_time = time - 300   # 5 minutes ago

# Add/subtract days
next_day = time + 86400  # 1 day later (24 * 60 * 60)
last_week = time - 604800  # 1 week ago (7 * 24 * 60 * 60)

# Use Rational for precise calculation
exact_time = time + Rational(1, 2)  # 0.5 days = 12 hours later

# Calculate time difference
time1 = Time.new(2023, 12, 25, 14, 30, 0)
time2 = Time.new(2023, 12, 25, 16, 45, 30)

difference = time2 - time1
puts "Difference: #{difference} seconds"  # Difference: 8130.0 seconds

# Convert to more friendly format
minutes = difference / 60
hours = minutes / 60
puts "Difference: #{hours} hours"  # Difference: 2.2583333333333333 hours

Time Range and Iteration

ruby
# Create time range
start_time = Time.new(2023, 12, 25, 9, 0, 0)
end_time = Time.new(2023, 12, 25, 17, 0, 0)
time_range = start_time..end_time

# Check if time is in range
check_time = Time.new(2023, 12, 25, 12, 0, 0)
puts time_range.include?(check_time)  # true

# Iterate time range (by hour)
current = start_time
while current <= end_time
  puts current.strftime("%H:%M")
  current += 3600  # Add 1 hour
end

# Use step method
start_time.step(end_time, 3600) do |t|
  puts t.strftime("%H:%M")
end

Business Days Calculation

ruby
class BusinessDays
  def self.add_business_days(start_date, days)
    current_date = start_date
    days_added = 0
    
    while days_added < days
      current_date += 1
      next if current_date.saturday? || current_date.sunday?
      days_added += 1
    end
    
    current_date
  end
  
  def self.business_days_between(start_date, end_date)
    return 0 if start_date > end_date
    
    business_days = 0
    current_date = start_date
    
    while current_date <= end_date
      unless current_date.saturday? || current_date.sunday?
        business_days += 1
      end
      current_date += 1
    end
    
    business_days
  end
end

# Using business days calculation
start_date = Date.new(2023, 12, 25)  # Monday
end_date = Date.new(2023, 12, 31)    # Sunday

# Calculate date 5 business days later
future_date = BusinessDays.add_business_days(start_date, 5)
puts "5 business days later: #{future_date}"  # 2023-12-39 => 2024-01-02

# Calculate business days between two dates
business_days = BusinessDays.business_days_between(start_date, end_date)
puts "Business days: #{business_days}"  # Business days: 5

🌍 Timezone Handling

Timezone Conversion

ruby
# Create time in different timezones
utc_time = Time.utc(2023, 12, 25, 14, 30, 45)
puts "UTC time: #{utc_time}"  # UTC time: 2023-12-25 14:30:45 UTC

# Convert to local time
local_time = utc_time.getlocal
puts "Local time: #{local_time}"  # Local time: 2023-12-25 22:30:45 +0800

# Convert to specified timezone
# Note: Ruby standard library doesn't directly support timezone conversion, need to use gems like tzinfo
# Here's demonstrating the concept:

class TimeZoneConverter
  # Simple timezone offset conversion
  def self.convert_timezone(time, offset_hours)
    # offset_hours: positive for eastern timezones, negative for western timezones
    time + (offset_hours * 3600)
  end
end

# Convert from UTC to different timezones
beijing_time = TimeZoneConverter.convert_timezone(utc_time, 8)
puts "Beijing time: #{beijing_time}"  # Beijing time: 2023-12-25 22:30:45 +0800

tokyo_time = TimeZoneConverter.convert_timezone(utc_time, 9)
puts "Tokyo time: #{tokyo_time}"  # Tokyo time: 2023-12-25 23:30:45 +0900

new_york_time = TimeZoneConverter.convert_timezone(utc_time, -5)
puts "New York time: #{new_york_time}"  # New York time: 2023-12-25 09:30:45 +0800

Daylight Saving Time Handling

ruby
# Daylight saving time calculations require specialized libraries like tzinfo
# Here showing basic concepts:

class DSTHandler
  # Simplified DST check (US rules)
  def self.dst?(time)
    month = time.month
    return false if month < 3 || month > 11
    return true if month > 3 && month < 11
    
    # Second Sunday of March and first Sunday of November
    # Simplified handling here
    if month == 3
      time.day >= 8 && time.wday == 0  # After second Sunday of March
    elsif month == 11
      time.day < 8 && time.wday == 0   # Before first Sunday of November
    end
  end
  
  # Apply DST offset
  def self.apply_dst_offset(time, is_dst)
    if is_dst
      time + 3600  # Add 1 hour
    else
      time
    end
  end
end

# Using DST handling
time = Time.new(2023, 7, 15, 14, 30, 45, "-05:00")  # US Eastern Time
is_dst = DSTHandler.dst?(time)
adjusted_time = DSTHandler.apply_dst_offset(time, is_dst)
puts "After DST adjustment: #{adjusted_time}"

🎯 Practical Examples

Log Timestamp Generator

ruby
class TimestampGenerator
  # Generate standard log timestamp
  def self.log_timestamp
    Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
  end
  
  # Generate ISO 8601 timestamp
  def self.iso_timestamp
    Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
  end
  
  # Generate file-friendly timestamp
  def self.file_timestamp
    Time.now.strftime("%Y%m%d_%H%M%S")
  end
  
  # Generate human-readable timestamp
  def self.human_timestamp
    Time.now.strftime("%B %d, %Y at %I:%M %p")
  end
end

# Using timestamp generator
puts "Log timestamp: #{TimestampGenerator.log_timestamp}"      # Log timestamp: 2023-12-25 14:30:45.123
puts "ISO timestamp: #{TimestampGenerator.iso_timestamp}"       # ISO timestamp: 2023-12-25T06:30:45.123Z
puts "File timestamp: #{TimestampGenerator.file_timestamp}"     # File timestamp: 20231225_143045
puts "Human timestamp: #{TimestampGenerator.human_timestamp}"    # Human timestamp: December 25, 2023 at 02:30 PM

Time Calculator

ruby
class TimeCalculator
  # Calculate age
  def self.age(birth_date)
    today = Date.today
    age = today.year - birth_date.year
    age -= 1 if today < birth_date.next_year(age)
    age
  end
  
  # Calculate full years, months, days between two dates
  def self.date_difference(start_date, end_date)
    years = end_date.year - start_date.year
    months = end_date.month - start_date.month
    days = end_date.day - start_date.day
    
    if days < 0
      months -= 1
      days += Date.new(start_date.year, start_date.month + 1, 0).day
    end
    
    if months < 0
      years -= 1
      months += 12
    end
    
    { years: years, months: months, days: days }
  end
  
  # Calculate days until a future date
  def self.days_until(target_date)
    (target_date - Date.today).to_i
  end
  
  # Format duration
  def self.format_duration(seconds)
    days = (seconds / 86400).to_i
    hours = (seconds % 86400 / 3600).to_i
    minutes = (seconds % 3600 / 60).to_i
    seconds = (seconds % 60).to_i
    
    parts = []
    parts << "#{days} days" if days > 0
    parts << "#{hours} hours" if hours > 0
    parts << "#{minutes} minutes" if minutes > 0
    parts << "#{seconds} seconds" if seconds > 0
    
    parts.join(" ")
  end
end

# Using time calculator
birth_date = Date.new(1990, 5, 15)
age = TimeCalculator.age(birth_date)
puts "Age: #{age} years old"  # Age: 33 years old

start_date = Date.new(2020, 1, 1)
end_date = Date.new(2023, 8, 15)
diff = TimeCalculator.date_difference(start_date, end_date)
puts "Date difference: #{diff[:years]} years #{diff[:months]} months #{diff[:days]} days"  # Date difference: 3 years 7 months 14 days

future_date = Date.new(2024, 1, 1)
days_until = TimeCalculator.days_until(future_date)
puts "Days until #{future_date}: #{days_until}"

duration = 7890  # seconds
formatted_duration = TimeCalculator.format_duration(duration)
puts "Duration: #{formatted_duration}"  # Duration: 2 hours 11 minutes 30 seconds

Time Validator

ruby
class TimeValidator
  # Validate date format
  def self.valid_date?(date_string)
    Date.parse(date_string)
    true
  rescue ArgumentError
    false
  end
  
  # Validate time format
  def self.valid_time?(time_string)
    Time.parse(time_string)
    true
  rescue ArgumentError
    false
  end
  
  # Validate if date is within reasonable range
  def self.reasonable_date?(date, min_year = 1900, max_year = 2100)
    return false unless date.is_a?(Date)
    date.year >= min_year && date.year <= max_year
  end
  
  # Validate if time is within business hours
  def self.business_hours?(time, start_hour = 9, end_hour = 18)
    return false unless time.is_a?(Time)
    hour = time.hour
    hour >= start_hour && hour < end_hour
  end
  
  # Validate if date is a weekday
  def self.weekday?(date)
    return false unless date.is_a?(Date)
    !(date.saturday? || date.sunday?)
  end
end

# Using time validator
puts TimeValidator.valid_date?("2023-12-25")     # true
puts TimeValidator.valid_date?("invalid-date")   # false
puts TimeValidator.valid_time?("14:30:45")       # true
puts TimeValidator.valid_time?("25:00:00")       # false

date = Date.new(2023, 12, 25)
puts TimeValidator.reasonable_date?(date)        # true
puts TimeValidator.weekday?(date)                # true (Monday)

time = Time.new(2023, 12, 25, 14, 30, 45)
puts TimeValidator.business_hours?(time)         # true

📊 Performance Optimization

Efficient Time Processing

ruby
# Avoid creating time objects repeatedly
# Inefficient way
def inefficient_time_check
  1000.times do
    if Time.now.hour > 12
      # Processing logic
    end
  end
end

# Efficient way
def efficient_time_check
  current_time = Time.now  # Create only once
  1000.times do
    if current_time.hour > 12
      # Processing logic
    end
  end
end

# Cache time calculation results
class TimeCache
  def initialize
    @cache = {}
    @last_update = Time.now
  end
  
  def current_date
    now = Time.now
    if now - @last_update > 1  # Update after more than 1 second
      @cache[:current_date] = now.to_date
      @last_update = now
    end
    @cache[:current_date]
  end
end

# Using time cache
cache = TimeCache.new
puts cache.current_date  # 2023-12-25

Batch Time Processing

ruby
# Batch process time data
def process_time_data(time_array)
  # Pre-calculate commonly used values
  base_time = Time.now
  one_day = 86400
  
  results = []
  time_array.each do |time_str|
    begin
      time = Time.parse(time_str)
      # Perform calculation
      diff = (base_time - time).abs
      days = (diff / one_day).to_i
      results << { time: time, days_ago: days }
    rescue ArgumentError
      results << { time: time_str, error: "Invalid time format" }
    end
  end
  results
end

# Using batch processing
time_strings = [
  "2023-12-20 10:30:00",
  "2023-12-22 15:45:30",
  "invalid-time",
  "2023-12-24 09:15:20"
]

results = process_time_data(time_strings)
results.each { |result| puts result }

🎯 Best Practices

1. Choose the Right Time Class

ruby
# Use Date when only date is needed
def calculate_age(birth_date_string)
  birth_date = Date.parse(birth_date_string)
  today = Date.today
  today.year - birth_date.year - ((today.month > birth_date.month || 
    (today.month == birth_date.month && today.day >= birth_date.day)) ? 0 : 1)
end

# Use Time when time precision is needed
def log_user_activity(user_id, action)
  timestamp = Time.now
  puts "[#{timestamp.strftime('%Y-%m-%d %H:%M:%S')}] User #{user_id}: #{action}"
end

# Use DateTime when high precision and timezone support are needed
def schedule_event(event_name, datetime_string)
  event_time = DateTime.parse(datetime_string)
  puts "Event '#{event_name}' scheduled at #{event_time.strftime('%Y-%m-%d %H:%M:%S %z')}"
end

2. Timezone Handling Best Practices

ruby
class TimeZoneBestPractice
  # Always store time in UTC
  def self.store_as_utc(local_time)
    local_time.utc
  end
  
  # Convert to local timezone for display
  def self.display_in_timezone(utc_time, timezone_offset = 8)
    utc_time.getlocal(timezone_offset * 3600)
  end
  
  # Process user input time
  def self.parse_user_input(time_string, user_timezone = "+08:00")
    # Assume user input is their local time
    time = Time.parse(time_string)
    # Convert to UTC for storage
    time.utc
  end
end

# Using timezone best practices
user_input = "2023-12-25 14:30:00"
utc_time = TimeZoneBestPractice.parse_user_input(user_input)
puts "Stored UTC time: #{utc_time}"

display_time = TimeZoneBestPractice.display_in_timezone(utc_time)
puts "Displayed local time: #{display_time}"

3. Time Formatting Best Practices

ruby
class TimeFormattingBestPractice
  # Define standard format constants
  LOG_FORMAT = "%Y-%m-%d %H:%M:%S.%3N"
  ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ"
  HUMAN_FORMAT = "%B %d, %Y at %I:%M %p"
  
  # Standardize log timestamp
  def self.log_timestamp(time = Time.now)
    time.strftime(LOG_FORMAT)
  end
  
  # Standardize ISO timestamp
  def self.iso_timestamp(time = Time.now)
    time.utc.strftime(ISO_FORMAT)
  end
  
  # Standardize human-readable timestamp
  def self.human_timestamp(time = Time.now)
    time.strftime(HUMAN_FORMAT)
  end
  
  # Parse standard time format
  def self.parse_standard(time_string)
    Time.parse(time_string)
  rescue ArgumentError
    nil
  end
end

# Using formatting best practices
current_time = Time.now
puts "Log format: #{TimeFormattingBestPractice.log_timestamp(current_time)}"
puts "ISO format: #{TimeFormattingBestPractice.iso_timestamp(current_time)}"
puts "Human format: #{TimeFormattingBestPractice.human_timestamp(current_time)}"

📚 Next Steps

After mastering Ruby date and time processing, continue learning:

Continue your Ruby learning journey!

Content is for learning and research only.