Ruby Logo

Time & Date

Comprehensive guide to Time, Date, and DateTime classes with timezone handling and date arithmetic.

Home Ruby Time & Date

Time & Date Handling in Ruby

Why Master Time & Date Handling?

Time and date handling is critical for modern applications. Poor temporal logic leads to bugs, security issues, and user frustration:

  • Business Logic: Scheduling, billing cycles, deadlines, and time-based automation
  • User Experience: Display dates in user's timezone and preferred format
  • Data Management: Accurate timestamps, audit trails, and historical data
  • Global Applications: Handle multiple timezones, daylight saving time, and localization
  • Performance: Efficient date calculations and caching of time-based queries

Problems Time & Date Handling Solves

1. Timezone Confusion

Eliminate bugs caused by timezone mismatches. Store times in UTC, display in user's timezone, and handle daylight saving time transitions correctly.

2. Business Date Calculations

Calculate due dates, billing periods, working days, and recurring events accurately. Handle edge cases like leap years, month-end dates, and holidays.

3. Consistent Formatting

Display dates and times consistently across your application. Support internationalization and user preferences for date formats.

Learning Path

1 Understanding Time, Date, and DateTime classes
2 Creating and parsing temporal objects
3 Formatting and timezone handling
4 Date arithmetic and business calculations
5 Real-world applications and best practices

1. Understanding Ruby's Time Classes

Time vs Date vs DateTime: When to Use Each

Ruby provides three main classes for temporal data. Understanding their differences is crucial for choosing the right tool for your needs and avoiding subtle bugs.

# Time class - Most commonly used for current time and precise timestamps
current_time = Time.now
puts current_time                    # => 2024-03-15 14:30:45 -0500
puts current_time.class             # => Time
puts current_time.to_f              # => 1710533445.123456 (Unix timestamp)

# Time supports microsecond precision
puts Time.now.usec                  # => 123456 (microseconds)

# Date class - For calendar dates without time information
today = Date.today
puts today                          # => 2024-03-15
puts today.class                    # => Date

# DateTime class - Combines date and time (legacy, use Time instead)
now_dt = DateTime.now
puts now_dt                         # => 2024-03-15T14:30:45-05:00
puts now_dt.class                   # => DateTime

# Key differences and when to use each:
puts "=== COMPARISON ==="

# 1. Time: Use for current time, timestamps, and when you need timezone support
puts "Time.now: #{Time.now}"
puts "Time supports timezones: #{Time.now.zone}"
puts "Time precision: #{Time.now.nsec} nanoseconds"

# 2. Date: Use for calendar dates, birthdays, due dates
puts "Date.today: #{Date.today}"
puts "Date arithmetic: #{Date.today + 30}"  # 30 days from now
puts "Date comparison: #{Date.today > Date.new(2020, 1, 1)}"

# 3. DateTime: Mostly legacy, use Time instead for new code
puts "DateTime.now: #{DateTime.now}"
puts "DateTime timezone: #{DateTime.now.zone}"

# Converting between classes
time = Time.now
date = time.to_date                 # Time to Date
datetime = time.to_datetime         # Time to DateTime
back_to_time = datetime.to_time     # DateTime to Time

puts "Time -> Date: #{date}"
puts "Time -> DateTime: #{datetime}"
puts "DateTime -> Time: #{back_to_time}"
💡 Choosing the Right Class
  • Time: Current time, timestamps, logging, timezone-aware operations
  • Date: Calendar dates, birthdays, due dates, date-only comparisons
  • DateTime: Legacy support only; use Time for new applications

2. Creating and Parsing Temporal Objects

Creating Time and Date Objects

There are multiple ways to create temporal objects. Understanding these methods helps you handle user input, database data, and API responses correctly.

# Creating Time objects
current = Time.now                           # Current time
utc_time = Time.now.utc                     # Current time in UTC
specific = Time.new(2024, 3, 15, 14, 30, 45) # Specific time (local timezone)
utc_specific = Time.utc(2024, 3, 15, 14, 30, 45) # Specific time in UTC

# Creating Date objects
today = Date.today                          # Current date
specific_date = Date.new(2024, 3, 15)       # Specific date
christmas = Date.new(2024, 12, 25)          # Holiday date

# From components with validation
begin
  invalid_date = Date.new(2024, 2, 30)     # February 30th doesn't exist
rescue Date::Error => e
  puts "Invalid date: #{e.message}"
end

# Creating from strings (parsing)
time_from_string = Time.parse("2024-03-15 14:30:45")
date_from_string = Date.parse("March 15, 2024")
iso_time = Time.iso8601("2024-03-15T14:30:45Z")

puts "Parsed time: #{time_from_string}"
puts "Parsed date: #{date_from_string}"
puts "ISO 8601 time: #{iso_time}"

# Creating from Unix timestamps
unix_timestamp = 1710533445
time_from_unix = Time.at(unix_timestamp)
puts "From Unix timestamp: #{time_from_unix}"

# Creating with timezone information
require 'time'
with_timezone = Time.parse("2024-03-15 14:30:45 -0500")
puts "With timezone: #{with_timezone}"

# Robust parsing with multiple formats
def parse_flexible_time(time_string)
  formats = [
    "%Y-%m-%d %H:%M:%S",        # 2024-03-15 14:30:45
    "%m/%d/%Y %I:%M %p",        # 03/15/2024 2:30 PM
    "%B %d, %Y at %I:%M %p",    # March 15, 2024 at 2:30 PM
    "%Y-%m-%dT%H:%M:%S%z"       # ISO 8601 with timezone
  ]

  formats.each do |format|
    begin
      return Time.strptime(time_string, format)
    rescue ArgumentError
      next
    end
  end

  # Fallback to general parsing
  Time.parse(time_string)
rescue ArgumentError => e
  raise "Unable to parse time: #{time_string} (#{e.message})"
end

# Examples of flexible parsing
test_times = [
  "2024-03-15 14:30:45",
  "03/15/2024 2:30 PM",
  "March 15, 2024 at 2:30 PM",
  "2024-03-15T14:30:45-05:00"
]

test_times.each do |time_str|
  parsed = parse_flexible_time(time_str)
  puts "#{time_str} -> #{parsed}"
end

Validation and Error Handling

Always validate temporal data from external sources. Invalid dates and times can cause subtle bugs that are hard to track down.

# Validation helpers for temporal data
class TemporalValidator
  # Validate date ranges
  def self.valid_date_range?(start_date, end_date)
    return false unless start_date.is_a?(Date) && end_date.is_a?(Date)
    start_date <= end_date
  end

  # Validate business hours
  def self.business_hours?(time, start_hour = 9, end_hour = 17)
    time.hour >= start_hour && time.hour < end_hour
  end

  # Validate weekday (Monday = 1, Sunday = 7)
  def self.weekday?(date)
    date.wday.between?(1, 5)
  end

  # Validate future date
  def self.future_date?(date)
    date > Date.today
  end

  # Validate age calculation
  def self.valid_birth_date?(birth_date, max_age = 150)
    return false if birth_date > Date.today
    age = ((Date.today - birth_date) / 365.25).to_i
    age <= max_age
  end

  # Safe date parsing with validation
  def self.safe_parse_date(date_string, format = nil)
    return nil if date_string.nil? || date_string.strip.empty?

    begin
      parsed = format ? Date.strptime(date_string, format) : Date.parse(date_string)

      # Basic sanity checks
      return nil if parsed.year < 1900 || parsed.year > 2100

      parsed
    rescue Date::Error, ArgumentError
      nil
    end
  end
end

# Examples of validation
puts "=== VALIDATION EXAMPLES ==="

# Date range validation
start_date = Date.new(2024, 3, 15)
end_date = Date.new(2024, 3, 10)
puts "Valid range? #{TemporalValidator.valid_date_range?(start_date, end_date)}"

# Business hours validation
meeting_time = Time.new(2024, 3, 15, 14, 30)
puts "Business hours? #{TemporalValidator.business_hours?(meeting_time)}"

# Weekday validation
monday = Date.new(2024, 3, 18)  # Monday
saturday = Date.new(2024, 3, 16)  # Saturday
puts "Monday is weekday? #{TemporalValidator.weekday?(monday)}"
puts "Saturday is weekday? #{TemporalValidator.weekday?(saturday)}"

# Age validation
birth_date = Date.new(1990, 5, 15)
invalid_birth = Date.new(2025, 1, 1)
puts "Valid birth date? #{TemporalValidator.valid_birth_date?(birth_date)}"
puts "Invalid birth date? #{TemporalValidator.valid_birth_date?(invalid_birth)}"

# Safe parsing
safe_dates = ["2024-03-15", "invalid", "1850-01-01", "2150-01-01"]
safe_dates.each do |date_str|
  parsed = TemporalValidator.safe_parse_date(date_str)
  puts "#{date_str} -> #{parsed || 'INVALID'}"
end

3. Formatting and Timezone Handling

Time and Date Formatting

Professional applications need consistent, user-friendly date and time formatting. Ruby's strftime method provides extensive formatting options for different contexts and locales.

# Comprehensive formatting examples
now = Time.new(2024, 3, 15, 14, 30, 45)
today = Date.today

# Basic formatting patterns
puts "=== BASIC FORMATTING ==="
puts now.strftime("%Y-%m-%d")           # => 2024-03-15 (ISO date)
puts now.strftime("%m/%d/%Y")           # => 03/15/2024 (US format)
puts now.strftime("%d/%m/%Y")           # => 15/03/2024 (European format)
puts now.strftime("%B %d, %Y")          # => March 15, 2024 (long format)
puts now.strftime("%b %d, %Y")          # => Mar 15, 2024 (short format)

# Time formatting
puts "\n=== TIME FORMATTING ==="
puts now.strftime("%H:%M:%S")           # => 14:30:45 (24-hour)
puts now.strftime("%I:%M %p")           # => 02:30 PM (12-hour)
puts now.strftime("%I:%M:%S %p")        # => 02:30:45 PM (12-hour with seconds)

# Combined date and time
puts "\n=== COMBINED FORMATTING ==="
puts now.strftime("%Y-%m-%d %H:%M:%S")  # => 2024-03-15 14:30:45 (timestamp)
puts now.strftime("%B %d, %Y at %I:%M %p") # => March 15, 2024 at 02:30 PM
puts now.strftime("%A, %B %d, %Y")      # => Friday, March 15, 2024

# Specialized formatting for different contexts
class TimeFormatter
  # User-friendly relative times
  def self.relative_time(time)
    now = Time.now
    diff = now - time

    case diff
    when 0..59
      "just now"
    when 60..3599
      minutes = (diff / 60).round
      "#{minutes} minute#{'s' if minutes != 1} ago"
    when 3600..86399
      hours = (diff / 3600).round
      "#{hours} hour#{'s' if hours != 1} ago"
    when 86400..2591999
      days = (diff / 86400).round
      "#{days} day#{'s' if days != 1} ago"
    else
      time.strftime("%B %d, %Y")
    end
  end

  # Business-friendly formats
  def self.business_format(time)
    if time.to_date == Date.today
      "Today at #{time.strftime('%I:%M %p')}"
    elsif time.to_date == Date.today - 1
      "Yesterday at #{time.strftime('%I:%M %p')}"
    elsif time.to_date == Date.today + 1
      "Tomorrow at #{time.strftime('%I:%M %p')}"
    elsif time.year == Date.today.year
      time.strftime("%B %d at %I:%M %p")
    else
      time.strftime("%B %d, %Y at %I:%M %p")
    end
  end

  # Log-friendly ISO format
  def self.log_format(time)
    time.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
  end

  # Filename-safe format
  def self.filename_format(time)
    time.strftime("%Y%m%d_%H%M%S")
  end
end

# Examples of specialized formatting
test_times = [
  Time.now - 30,           # 30 seconds ago
  Time.now - 300,          # 5 minutes ago
  Time.now - 7200,         # 2 hours ago
  Time.now - 86400,        # 1 day ago
  Time.now - 604800        # 1 week ago
]

puts "\n=== RELATIVE TIME EXAMPLES ==="
test_times.each do |time|
  puts "#{time.strftime('%Y-%m-%d %H:%M:%S')} -> #{TimeFormatter.relative_time(time)}"
end

puts "\n=== BUSINESS FORMAT EXAMPLES ==="
business_times = [
  Time.now,                              # Now
  Time.now + 86400,                      # Tomorrow
  Time.new(2024, 6, 15, 10, 30),        # Future this year
  Time.new(2025, 1, 15, 14, 30)         # Next year
]

business_times.each do |time|
  puts TimeFormatter.business_format(time)
end

puts "\n=== TECHNICAL FORMATS ==="
puts "Log format: #{TimeFormatter.log_format(now)}"
puts "Filename format: #{TimeFormatter.filename_format(now)}"

Timezone Handling

Proper timezone handling is crucial for global applications. Always store times in UTC and convert to user's timezone for display.

# Timezone handling examples
require 'time'

# Current time in different representations
local_time = Time.now
utc_time = Time.now.utc
puts "Local time: #{local_time}"
puts "UTC time: #{utc_time}"
puts "Timezone: #{local_time.zone}"
puts "UTC offset: #{local_time.utc_offset} seconds"

# Converting between timezones
utc_meeting = Time.utc(2024, 3, 15, 19, 30)  # 7:30 PM UTC

# Convert to different timezones (requires manual offset calculation)
eastern_offset = -5 * 3600    # EST is UTC-5
pacific_offset = -8 * 3600    # PST is UTC-8

eastern_time = utc_meeting + eastern_offset
pacific_time = utc_meeting + pacific_offset

puts "\n=== TIMEZONE CONVERSION ==="
puts "UTC: #{utc_meeting.strftime('%Y-%m-%d %H:%M %Z')}"
puts "Eastern: #{(utc_meeting - 5*3600).strftime('%Y-%m-%d %H:%M')} EST"
puts "Pacific: #{(utc_meeting - 8*3600).strftime('%Y-%m-%d %H:%M')} PST"

# Timezone-aware application class
class TimezoneHelper
  TIMEZONE_OFFSETS = {
    'UTC' => 0,
    'EST' => -5,
    'CST' => -6,
    'MST' => -7,
    'PST' => -8,
    'GMT' => 0,
    'CET' => 1,    # Central European Time
    'JST' => 9,    # Japan Standard Time
    'AEST' => 10   # Australian Eastern Standard Time
  }.freeze

  def self.convert_to_timezone(utc_time, timezone)
    offset_hours = TIMEZONE_OFFSETS[timezone.upcase]
    return nil unless offset_hours

    local_time = utc_time + (offset_hours * 3600)
    {
      time: local_time,
      timezone: timezone,
      formatted: local_time.strftime("%Y-%m-%d %H:%M #{timezone}")
    }
  end

  def self.user_friendly_timezone(utc_time, user_timezone)
    converted = convert_to_timezone(utc_time, user_timezone)
    return utc_time.strftime("%Y-%m-%d %H:%M UTC") unless converted

    time = converted[:time]
    if time.to_date == Date.today
      "Today at #{time.strftime('%I:%M %p')} #{user_timezone}"
    elsif time.to_date == Date.today + 1
      "Tomorrow at #{time.strftime('%I:%M %p')} #{user_timezone}"
    else
      "#{time.strftime('%B %d at %I:%M %p')} #{user_timezone}"
    end
  end

  # Business hours check across timezones
  def self.business_hours?(utc_time, timezone, start_hour = 9, end_hour = 17)
    local_time = convert_to_timezone(utc_time, timezone)
    return false unless local_time

    hour = local_time[:time].hour
    hour >= start_hour && hour < end_hour
  end
end

# Examples of timezone conversion
meeting_utc = Time.utc(2024, 3, 15, 20, 30)  # 8:30 PM UTC

timezones = ['EST', 'PST', 'CET', 'JST']
puts "\n=== GLOBAL MEETING TIME ==="
puts "Meeting scheduled for: #{meeting_utc.strftime('%Y-%m-%d %H:%M UTC')}"

timezones.each do |tz|
  converted = TimezoneHelper.convert_to_timezone(meeting_utc, tz)
  friendly = TimezoneHelper.user_friendly_timezone(meeting_utc, tz)
  business_hours = TimezoneHelper.business_hours?(meeting_utc, tz)

  puts "#{tz}: #{friendly} #{'(outside business hours)' unless business_hours}"
end

# Database storage best practices
class EventScheduler
  # Always store in UTC
  def self.create_event(title, local_time, user_timezone)
    # Convert user's local time to UTC for storage
    offset_hours = TimezoneHelper::TIMEZONE_OFFSETS[user_timezone.upcase] || 0
    utc_time = local_time - (offset_hours * 3600)

    {
      title: title,
      utc_time: utc_time,
      user_timezone: user_timezone,
      display_time: TimezoneHelper.user_friendly_timezone(utc_time, user_timezone)
    }
  end

  # Display in user's timezone
  def self.display_event(event, user_timezone)
    display_time = TimezoneHelper.user_friendly_timezone(event[:utc_time], user_timezone)
    "#{event[:title]} - #{display_time}"
  end
end

# Example: Creating and displaying events
user_meeting = Time.new(2024, 3, 15, 14, 30)  # 2:30 PM local time
event = EventScheduler.create_event("Team Standup", user_meeting, "EST")

puts "\n=== EVENT SCHEDULING ==="
puts "Created event: #{event[:title]}"
puts "Stored as UTC: #{event[:utc_time]}"
puts "Display for EST user: #{EventScheduler.display_event(event, 'EST')}"
puts "Display for PST user: #{EventScheduler.display_event(event, 'PST')}"

4. Date Arithmetic and Business Calculations

Basic Date and Time Arithmetic

Date arithmetic is essential for calculating due dates, ages, durations, and scheduling. Understanding how Ruby handles edge cases like leap years and month boundaries prevents bugs.

# Basic date arithmetic
today = Date.today
future_date = today + 30          # 30 days from now
past_date = today - 7             # 7 days ago

puts "Today: #{today}"
puts "30 days from now: #{future_date}"
puts "7 days ago: #{past_date}"

# Time arithmetic (in seconds)
now = Time.now
one_hour_later = now + 3600       # 3600 seconds = 1 hour
one_day_later = now + 86400       # 86400 seconds = 1 day

puts "\nTime arithmetic:"
puts "Now: #{now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "One hour later: #{one_hour_later.strftime('%Y-%m-%d %H:%M:%S')}"
puts "One day later: #{one_day_later.strftime('%Y-%m-%d %H:%M:%S')}"

# Duration calculations
start_time = Time.new(2024, 3, 15, 9, 0)
end_time = Time.new(2024, 3, 15, 17, 30)
duration_seconds = end_time - start_time
duration_hours = duration_seconds / 3600

puts "\nDuration calculation:"
puts "Start: #{start_time.strftime('%I:%M %p')}"
puts "End: #{end_time.strftime('%I:%M %p')}"
puts "Duration: #{duration_hours} hours"

# Age calculation
birth_date = Date.new(1990, 5, 15)
age_in_days = Date.today - birth_date
age_in_years = (age_in_days / 365.25).to_i

puts "\nAge calculation:"
puts "Birth date: #{birth_date}"
puts "Age in days: #{age_in_days.to_i}"
puts "Age in years: #{age_in_years}"

# More precise age calculation
def calculate_age(birth_date)
  today = Date.today
  age = today.year - birth_date.year

  # Adjust if birthday hasn't occurred this year
  if today.month < birth_date.month ||
     (today.month == birth_date.month && today.day < birth_date.day)
    age -= 1
  end

  age
end

puts "Precise age: #{calculate_age(birth_date)} years"

# Working with months (careful with edge cases)
def add_months(date, months)
  new_year = date.year
  new_month = date.month + months

  # Handle year overflow/underflow
  while new_month > 12
    new_year += 1
    new_month -= 12
  end

  while new_month < 1
    new_year -= 1
    new_month += 12
  end

  # Handle day overflow (e.g., Jan 31 + 1 month)
  max_day = Date.new(new_year, new_month, -1).day
  new_day = [date.day, max_day].min

  Date.new(new_year, new_month, new_day)
end

puts "\nMonth arithmetic:"
jan_31 = Date.new(2024, 1, 31)
puts "Jan 31 + 1 month: #{add_months(jan_31, 1)}"  # Feb 29 (2024 is leap year)
puts "Jan 31 + 2 months: #{add_months(jan_31, 2)}" # Mar 31

# Leap year handling
def leap_year?(year)
  (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
end

puts "\nLeap year examples:"
[2020, 2021, 2024, 2100, 2000].each do |year|
  puts "#{year}: #{leap_year?(year) ? 'Leap year' : 'Not leap year'}"
end

Business Date Calculations

Business applications need sophisticated date calculations: working days, billing cycles, recurring events, and deadline management with holiday handling.

# Business date calculation utilities
class BusinessDateCalculator
  # US Federal Holidays (simplified)
  HOLIDAYS_2024 = [
    Date.new(2024, 1, 1),   # New Year's Day
    Date.new(2024, 7, 4),   # Independence Day
    Date.new(2024, 12, 25)  # Christmas Day
    # Add more holidays as needed
  ].freeze

  # Check if date is a weekday
  def self.weekday?(date)
    date.wday.between?(1, 5)  # Monday = 1, Friday = 5
  end

  # Check if date is a business day (weekday and not holiday)
  def self.business_day?(date)
    weekday?(date) && !HOLIDAYS_2024.include?(date)
  end

  # Find next business day
  def self.next_business_day(date)
    next_day = date + 1
    while !business_day?(next_day)
      next_day += 1
    end
    next_day
  end

  # Add business days (skipping weekends and holidays)
  def self.add_business_days(start_date, days)
    current_date = start_date
    days_added = 0

    while days_added < days
      current_date += 1
      if business_day?(current_date)
        days_added += 1
      end
    end

    current_date
  end

  # Calculate business days between two dates
  def self.business_days_between(start_date, end_date)
    return 0 if start_date >= end_date

    count = 0
    current = start_date

    while current < end_date
      count += 1 if business_day?(current)
      current += 1
    end

    count
  end

  # Billing cycle calculations
  def self.next_billing_date(last_bill_date, cycle_days = 30)
    candidate_date = last_bill_date + cycle_days

    # If it falls on weekend/holiday, move to next business day
    business_day?(candidate_date) ? candidate_date : next_business_day(candidate_date)
  end

  # Recurring event scheduler
  def self.generate_recurring_dates(start_date, frequency, count)
    dates = [start_date]

    (1...count).each do |i|
      case frequency
      when :daily
        dates << start_date + i
      when :weekly
        dates << start_date + (i * 7)
      when :monthly
        dates << add_months(start_date, i)
      when :yearly
        dates << Date.new(start_date.year + i, start_date.month, start_date.day)
      end
    end

    dates
  rescue ArgumentError
    # Handle invalid dates (e.g., Feb 29 in non-leap years)
    dates[0..-2] # Return what we have so far
  end

  private

  def self.add_months(date, months)
    new_year = date.year + (months / 12)
    new_month = date.month + (months % 12)

    if new_month > 12
      new_year += 1
      new_month -= 12
    end

    # Handle day overflow
    max_day = Date.new(new_year, new_month, -1).day
    new_day = [date.day, max_day].min

    Date.new(new_year, new_month, new_day)
  end
end

# Examples of business calculations
puts "=== BUSINESS DATE CALCULATIONS ==="

# Business day checks
test_dates = [
  Date.new(2024, 3, 15),  # Friday
  Date.new(2024, 3, 16),  # Saturday
  Date.new(2024, 3, 18),  # Monday
  Date.new(2024, 7, 4)    # Independence Day
]

test_dates.each do |date|
  weekday = BusinessDateCalculator.weekday?(date)
  business = BusinessDateCalculator.business_day?(date)
  puts "#{date.strftime('%A, %B %d')}: Weekday? #{weekday}, Business day? #{business}"
end

# Business day calculations
start_date = Date.new(2024, 3, 15)  # Friday
puts "\nBusiness day calculations from #{start_date.strftime('%A, %B %d')}:"
puts "Next business day: #{BusinessDateCalculator.next_business_day(start_date).strftime('%A, %B %d')}"
puts "5 business days later: #{BusinessDateCalculator.add_business_days(start_date, 5).strftime('%A, %B %d')}"

# Billing cycles
last_bill = Date.new(2024, 2, 15)
next_bill = BusinessDateCalculator.next_billing_date(last_bill, 30)
puts "\nBilling cycle:"
puts "Last bill: #{last_bill}"
puts "Next bill: #{next_bill}"

# Recurring events
meeting_start = Date.new(2024, 3, 15)
weekly_meetings = BusinessDateCalculator.generate_recurring_dates(meeting_start, :weekly, 5)
monthly_reports = BusinessDateCalculator.generate_recurring_dates(Date.new(2024, 1, 31), :monthly, 6)

puts "\nRecurring events:"
puts "Weekly meetings: #{weekly_meetings.map(&:to_s).join(', ')}"
puts "Monthly reports: #{monthly_reports.map(&:to_s).join(', ')}"

# Project deadline calculations
project_start = Date.new(2024, 3, 1)
project_deadline = BusinessDateCalculator.add_business_days(project_start, 20)
days_remaining = BusinessDateCalculator.business_days_between(Date.today, project_deadline)

puts "\nProject timeline:"
puts "Project start: #{project_start}"
puts "Project deadline: #{project_deadline}"
puts "Business days remaining: #{days_remaining}"

5. Real-World Applications

Event Scheduling System

# Complete event scheduling system
class EventScheduler
  def initialize
    @events = []
  end

  def schedule_event(title, start_time, duration_minutes, timezone = 'UTC')
    end_time = start_time + (duration_minutes * 60)

    event = {
      id: generate_id,
      title: title,
      start_time: start_time,
      end_time: end_time,
      timezone: timezone,
      duration: duration_minutes
    }

    @events << event
    event
  end

  def find_available_slots(date, duration_minutes, timezone = 'UTC')
    # Business hours: 9 AM to 5 PM
    start_of_day = Time.new(date.year, date.month, date.day, 9, 0)
    end_of_day = Time.new(date.year, date.month, date.day, 17, 0)

    slots = []
    current_time = start_of_day

    while current_time + (duration_minutes * 60) <= end_of_day
      slot_end = current_time + (duration_minutes * 60)

      # Check if slot conflicts with existing events
      conflict = @events.any? do |event|
        !(slot_end <= event[:start_time] || current_time >= event[:end_time])
      end

      unless conflict
        slots << {
          start: current_time,
          end: slot_end,
          formatted: TimeFormatter.business_format(current_time)
        }
      end

      current_time += 1800  # Move by 30 minutes
    end

    slots
  end

  def upcoming_events(days = 7)
    cutoff = Time.now + (days * 86400)
    @events.select { |event| event[:start_time] <= cutoff && event[:start_time] >= Time.now }
            .sort_by { |event| event[:start_time] }
  end

  private

  def generate_id
    Time.now.to_i.to_s(36) + rand(1000).to_s(36)
  end
end

# Example usage
scheduler = EventScheduler.new

# Schedule some events
scheduler.schedule_event("Team Standup", Time.new(2024, 3, 18, 9, 0), 30)
scheduler.schedule_event("Client Meeting", Time.new(2024, 3, 18, 14, 0), 90)

# Find available slots
available = scheduler.find_available_slots(Date.new(2024, 3, 18), 60)
puts "Available 1-hour slots on March 18:"
available.each do |slot|
  puts "  #{slot[:formatted]}"
end

Subscription Billing System

# Subscription billing with proper date handling
class SubscriptionBilling
  BILLING_CYCLES = {
    monthly: 1,
    quarterly: 3,
    yearly: 12
  }.freeze

  def self.calculate_next_billing(subscription_start, cycle)
    months = BILLING_CYCLES[cycle]
    return nil unless months

    current_date = Date.today
    next_bill = subscription_start

    # Find the next billing date after today
    while next_bill <= current_date
      next_bill = add_months(next_bill, months)
    end

    next_bill
  end

  def self.calculate_prorated_amount(amount, start_date, end_date, bill_date)
    # Calculate prorated amount for partial billing periods
    total_days = (end_date - start_date).to_i
    used_days = (bill_date - start_date).to_i

    return amount if used_days >= total_days

    (amount * used_days.to_f / total_days).round(2)
  end

  def self.billing_history(start_date, cycle, months_back = 12)
    bills = []
    current_bill = start_date

    while current_bill <= Date.today && bills.length < months_back
      bills << {
        date: current_bill,
        period_start: current_bill,
        period_end: add_months(current_bill, BILLING_CYCLES[cycle]) - 1,
        formatted: current_bill.strftime("%B %Y")
      }
      current_bill = add_months(current_bill, BILLING_CYCLES[cycle])
    end

    bills
  end

  private

  def self.add_months(date, months)
    new_year = date.year + (months / 12)
    new_month = date.month + (months % 12)

    if new_month > 12
      new_year += 1
      new_month -= 12
    end

    max_day = Date.new(new_year, new_month, -1).day
    new_day = [date.day, max_day].min

    Date.new(new_year, new_month, new_day)
  end
end

# Example billing calculations
subscription_start = Date.new(2023, 1, 31)
next_monthly = SubscriptionBilling.calculate_next_billing(subscription_start, :monthly)
next_yearly = SubscriptionBilling.calculate_next_billing(subscription_start, :yearly)

puts "Subscription started: #{subscription_start}"
puts "Next monthly bill: #{next_monthly}"
puts "Next yearly bill: #{next_yearly}"

# Prorated billing example
monthly_amount = 99.99
partial_start = Date.new(2024, 3, 15)
month_end = Date.new(2024, 3, 31)
prorated = SubscriptionBilling.calculate_prorated_amount(
  monthly_amount, Date.new(2024, 3, 1), month_end, partial_start
)

puts "\nProrated billing:"
puts "Started mid-month: #{partial_start}"
puts "Prorated amount: $#{'%.2f' % prorated} (vs $#{'%.2f' % monthly_amount} full month)"

Age Verification System

# Precise age verification for legal compliance
class AgeVerifier
  def self.calculate_exact_age(birth_date)
    today = Date.today

    years = today.year - birth_date.year
    months = today.month - birth_date.month
    days = today.day - birth_date.day

    # Adjust for partial months/years
    if days < 0
      months -= 1
      days += Date.new(today.year, today.month, 0).day
    end

    if months < 0
      years -= 1
      months += 12
    end

    { years: years, months: months, days: days }
  end

  def self.age_in_years(birth_date)
    calculate_exact_age(birth_date)[:years]
  end

  def self.legal_adult?(birth_date, jurisdiction = :us)
    age = age_in_years(birth_date)

    case jurisdiction
    when :us, :canada
      age >= 18
    when :uk
      age >= 18
    when :japan
      age >= 20
    else
      age >= 18  # Default
    end
  end

  def self.can_drink_alcohol?(birth_date, jurisdiction = :us)
    age = age_in_years(birth_date)

    case jurisdiction
    when :us
      age >= 21
    when :uk, :canada, :australia
      age >= 18
    when :germany
      age >= 16  # Beer and wine
    else
      age >= 18  # Default
    end
  end

  def self.retirement_eligible?(birth_date)
    age = age_in_years(birth_date)

    {
      early_retirement: age >= 62,
      full_retirement: age >= 67,  # For those born 1960+
      age: age
    }
  end
end

# Examples
test_birth_dates = [
  Date.new(2000, 3, 15),  # 24 years old
  Date.new(2010, 6, 20),  # 13-14 years old
  Date.new(1960, 1, 1)    # 64 years old
]

test_birth_dates.each do |birth_date|
  exact_age = AgeVerifier.calculate_exact_age(birth_date)
  age_years = AgeVerifier.age_in_years(birth_date)

  puts "Birth date: #{birth_date}"
  puts "Exact age: #{exact_age[:years]} years, #{exact_age[:months]} months, #{exact_age[:days]} days"
  puts "Legal adult (US): #{AgeVerifier.legal_adult?(birth_date)}"
  puts "Can drink (US): #{AgeVerifier.can_drink_alcohol?(birth_date)}"
  puts "Retirement: #{AgeVerifier.retirement_eligible?(birth_date)}"
  puts "---"
end

Time Tracking System

# Professional time tracking with reporting
class TimeTracker
  def initialize
    @sessions = []
  end

  def start_session(project, description = nil)
    @current_session = {
      id: generate_id,
      project: project,
      description: description,
      start_time: Time.now,
      end_time: nil,
      duration: nil
    }
  end

  def end_session
    return nil unless @current_session

    @current_session[:end_time] = Time.now
    @current_session[:duration] = @current_session[:end_time] - @current_session[:start_time]

    @sessions << @current_session
    completed = @current_session
    @current_session = nil

    completed
  end

  def daily_report(date = Date.today)
    day_sessions = @sessions.select do |session|
      session[:start_time].to_date == date
    end

    total_time = day_sessions.sum { |s| s[:duration] || 0 }

    {
      date: date,
      sessions: day_sessions,
      total_hours: (total_time / 3600.0).round(2),
      billable_hours: calculate_billable_hours(day_sessions),
      projects: day_sessions.map { |s| s[:project] }.uniq
    }
  end

  def weekly_summary(start_date = Date.today.beginning_of_week)
    week_sessions = @sessions.select do |session|
      session_date = session[:start_time].to_date
      session_date >= start_date && session_date < start_date + 7
    end

    project_totals = week_sessions.group_by { |s| s[:project] }
                                  .transform_values { |sessions|
                                    sessions.sum { |s| s[:duration] || 0 } / 3600.0
                                  }

    {
      week_start: start_date,
      total_hours: week_sessions.sum { |s| s[:duration] || 0 } / 3600.0,
      project_breakdown: project_totals,
      days_worked: week_sessions.map { |s| s[:start_time].to_date }.uniq.count
    }
  end

  private

  def generate_id
    Time.now.to_i.to_s(36)
  end

  def calculate_billable_hours(sessions)
    # Assume all time is billable for this example
    sessions.sum { |s| s[:duration] || 0 } / 3600.0
  end
end

# Example usage
tracker = TimeTracker.new

# Simulate a work day
tracker.start_session("Website Redesign", "Frontend development")
sleep(1)  # Simulate work time
tracker.end_session

tracker.start_session("Bug Fixes", "Payment processing issues")
sleep(1)
tracker.end_session

# Generate reports
daily = tracker.daily_report
puts "Daily Report for #{daily[:date]}:"
puts "Total hours: #{daily[:total_hours]}"
puts "Projects worked on: #{daily[:projects].join(', ')}"
daily[:sessions].each do |session|
  puts "  #{session[:project]}: #{'%.2f' % (session[:duration] / 3600.0)} hours"
end

Best Practices and Common Pitfalls

🎯 Best Practices

  • Always store times in UTC: Convert to user's timezone only for display
  • Use Time for timestamps: Date for calendar dates, avoid DateTime
  • Validate all temporal input: Handle edge cases like leap years and invalid dates
  • Consider timezone changes: Daylight saving time can cause issues
  • Test boundary conditions: Month-end dates, leap years, century changes

⚠️ Common Pitfalls

Timezone Confusion

Mixing local time and UTC leads to bugs. Always be explicit about what timezone your times represent.

Month Arithmetic Edge Cases

Adding months to Jan 31 gives different results in different years. Always handle day overflow carefully.

Daylight Saving Time

Times can "jump" during DST transitions. Be careful with scheduling and duration calculations.

🚀 Next Steps

With solid time and date handling skills, you can build robust applications that handle temporal data correctly. Continue your learning with:

  • Advanced Timezone Libraries: Explore gems like TZInfo for comprehensive timezone support
  • Internationalization: Learn to format dates and times for different locales
  • Performance Optimization: Cache calculations and use efficient date queries
  • Business Domain Logic: Apply these concepts to scheduling, billing, and workflow systems

Quick Navigation

Related Topics

Video Tutorial

Watch and learn time & date

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!