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 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