Date/Time & Number Formatting in Ruby
Proper locale-specific formatting of dates, times, and numbers is crucial for international applications. Learn to format these data types according to different cultural conventions without relying on Rails.
Date and Time Formatting
Ruby's Time and Date classes provide flexible formatting options that can be adapted for different locales and cultural preferences.
Basic Date/Time Formatting
require 'date'
require 'time'
# Current date and time
now = Time.now
today = Date.today
# Standard formatting
puts now.strftime("%Y-%m-%d %H:%M:%S") # => 2024-03-15 14:30:45
puts today.strftime("%B %d, %Y") # => March 15, 2024
# Common format patterns
formats = {
iso8601: "%Y-%m-%dT%H:%M:%S%z", # 2024-03-15T14:30:45+0900
rfc2822: "%a, %d %b %Y %H:%M:%S %z", # Fri, 15 Mar 2024 14:30:45 +0900
us_date: "%m/%d/%Y", # 03/15/2024
eu_date: "%d/%m/%Y", # 15/03/2024
long_date: "%A, %B %d, %Y", # Friday, March 15, 2024
short_time: "%H:%M", # 14:30
twelve_hour: "%I:%M %p" # 02:30 PM
}
formats.each do |name, pattern|
puts "#{name}: #{now.strftime(pattern)}"
end
# Parse dates in different formats
date_strings = [
"2024-03-15",
"03/15/2024",
"15/03/2024",
"March 15, 2024",
"2024-03-15T14:30:45Z"
]
date_strings.each do |date_str|
begin
parsed = Date.parse(date_str)
puts "#{date_str} => #{parsed}"
rescue Date::Error => e
puts "Failed to parse: #{date_str}"
end
end
Locale-Specific Date/Time Formatting
Create formatters that adapt to different cultural conventions for displaying dates and times.
Custom Locale Formatters
# Locale-aware date/time formatter
class LocaleDateTimeFormatter
MONTH_NAMES = {
en: %w[January February March April May June July August September October November December],
es: %w[enero febrero marzo abril mayo junio julio agosto septiembre octubre noviembre diciembre],
fr: %w[janvier février mars avril mai juin juillet août septembre octobre novembre décembre],
de: %w[Januar Februar März April Mai Juni Juli August September Oktober November Dezember],
ja: %w[1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月]
}.freeze
DAY_NAMES = {
en: %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday],
es: %w[domingo lunes martes miércoles jueves viernes sábado],
fr: %w[dimanche lundi mardi mercredi jeudi vendredi samedi],
de: %w[Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag],
ja: %w[日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日]
}.freeze
DATE_FORMATS = {
en: { short: "%m/%d/%Y", long: "%A, %B %d, %Y" },
de: { short: "%d.%m.%Y", long: "%A, %d. %B %Y" },
fr: { short: "%d/%m/%Y", long: "%A %d %B %Y" },
ja: { short: "%Y/%m/%d", long: "%Y年%m月%d日(%a)" }
}.freeze
def initialize(locale = :en)
@locale = locale
end
def format_date(date, style = :short)
format = DATE_FORMATS[@locale][style] || DATE_FORMATS[:en][style]
formatted = date.strftime(format)
# Replace English month/day names with localized versions
replace_month_names(replace_day_names(formatted))
end
def format_time(time, include_seconds: false)
case @locale
when :en
pattern = include_seconds ? "%I:%M:%S %p" : "%I:%M %p"
when :de, :fr
pattern = include_seconds ? "%H:%M:%S" : "%H:%M"
when :ja
pattern = include_seconds ? "%H時%M分%S秒" : "%H時%M分"
else
pattern = include_seconds ? "%H:%M:%S" : "%H:%M"
end
time.strftime(pattern)
end
def format_datetime(datetime, date_style: :short, include_seconds: false)
date_part = format_date(datetime, date_style)
time_part = format_time(datetime, include_seconds: include_seconds)
case @locale
when :ja
"#{date_part} #{time_part}"
else
"#{date_part} #{time_part}"
end
end
private
def replace_month_names(text)
return text unless MONTH_NAMES[@locale]
MONTH_NAMES[:en].each_with_index do |en_month, index|
localized_month = MONTH_NAMES[@locale][index]
text = text.gsub(en_month, localized_month)
end
text
end
def replace_day_names(text)
return text unless DAY_NAMES[@locale]
DAY_NAMES[:en].each_with_index do |en_day, index|
localized_day = DAY_NAMES[@locale][index]
text = text.gsub(en_day, localized_day)
end
text
end
end
# Usage examples
date = Date.new(2024, 3, 15)
time = Time.new(2024, 3, 15, 14, 30, 45)
[:en, :de, :fr, :ja].each do |locale|
formatter = LocaleDateTimeFormatter.new(locale)
puts "#{locale.upcase}:"
puts " Short date: #{formatter.format_date(date, :short)}"
puts " Long date: #{formatter.format_date(date, :long)}"
puts " Time: #{formatter.format_time(time)}"
puts " DateTime: #{formatter.format_datetime(time, date_style: :long)}"
puts
end
Number Formatting
Format numbers according to different cultural conventions for decimal separators, thousands separators, and currency display.
Locale-Aware Number Formatting
# Number formatter with locale support
class LocaleNumberFormatter
LOCALE_SETTINGS = {
en: {
decimal_separator: '.',
thousands_separator: ',',
currency_symbol: '$',
currency_format: '%s%n', # symbol + number
negative_format: '-%n'
},
de: {
decimal_separator: ',',
thousands_separator: '.',
currency_symbol: '€',
currency_format: '%n %s', # number + symbol
negative_format: '-%n'
},
fr: {
decimal_separator: ',',
thousands_separator: ' ',
currency_symbol: '€',
currency_format: '%n %s',
negative_format: '-%n'
},
ja: {
decimal_separator: '.',
thousands_separator: ',',
currency_symbol: '¥',
currency_format: '%s%n',
negative_format: '-%n'
}
}.freeze
def initialize(locale = :en)
@locale = locale
@settings = LOCALE_SETTINGS[@locale] || LOCALE_SETTINGS[:en]
end
def format_number(number, precision: 2)
# Handle negative numbers
negative = number < 0
abs_number = number.abs
# Split into integer and decimal parts
if precision > 0
formatted = sprintf("%.#{precision}f", abs_number)
integer_part, decimal_part = formatted.split('.')
else
integer_part = abs_number.to_i.to_s
decimal_part = nil
end
# Add thousands separators
integer_part = add_thousands_separators(integer_part)
# Combine parts
result = decimal_part ?
"#{integer_part}#{@settings[:decimal_separator]}#{decimal_part}" :
integer_part
# Handle negative formatting
if negative
@settings[:negative_format].gsub('%n', result)
else
result
end
end
def format_currency(amount, precision: 2)
number_part = format_number(amount.abs, precision: precision)
currency_format = @settings[:currency_format]
result = currency_format.gsub('%n', number_part)
.gsub('%s', @settings[:currency_symbol])
if amount < 0
@settings[:negative_format].gsub('%n', result)
else
result
end
end
def format_percentage(number, precision: 1)
percentage = number * 100
"#{format_number(percentage, precision: precision)}%"
end
def parse_number(text)
# Remove thousands separators and convert decimal separator
clean_text = text.gsub(@settings[:thousands_separator], '')
.gsub(@settings[:decimal_separator], '.')
# Extract numeric part
numeric_match = clean_text.match(/[+-]?\d+\.?\d*/)
return 0.0 unless numeric_match
Float(numeric_match[0])
end
private
def add_thousands_separators(integer_string)
# Add thousands separators from right to left
integer_string.reverse.gsub(/(\d{3})(?=\d)/, "\\1#{@settings[:thousands_separator]}").reverse
end
end
# Usage examples
amounts = [1234.56, -5678.90, 0.123, 1000000]
[:en, :de, :fr, :ja].each do |locale|
formatter = LocaleNumberFormatter.new(locale)
puts "#{locale.upcase} formatting:"
amounts.each do |amount|
puts " Number: #{formatter.format_number(amount)}"
puts " Currency: #{formatter.format_currency(amount)}"
puts " Percentage: #{formatter.format_percentage(amount / 100)}"
end
puts
end
# Parsing examples
en_formatter = LocaleNumberFormatter.new(:en)
de_formatter = LocaleNumberFormatter.new(:de)
puts "Parsing numbers:"
puts "EN '1,234.56' => #{en_formatter.parse_number('1,234.56')}"
puts "DE '1.234,56' => #{de_formatter.parse_number('1.234,56')}"
Advanced Formatting Features
Implement advanced formatting features like relative dates, ordinal numbers, and smart pluralization.
Relative Date Formatting
# Relative date formatter
class RelativeDateFormatter
RELATIVE_FORMATS = {
en: {
now: "now",
seconds: { one: "%d second ago", other: "%d seconds ago", future_one: "in %d second", future_other: "in %d seconds" },
minutes: { one: "%d minute ago", other: "%d minutes ago", future_one: "in %d minute", future_other: "in %d minutes" },
hours: { one: "%d hour ago", other: "%d hours ago", future_one: "in %d hour", future_other: "in %d hours" },
days: { one: "yesterday", other: "%d days ago", future_one: "tomorrow", future_other: "in %d days" },
weeks: { one: "last week", other: "%d weeks ago", future_one: "next week", future_other: "in %d weeks" },
months: { one: "last month", other: "%d months ago", future_one: "next month", future_other: "in %d months" },
years: { one: "last year", other: "%d years ago", future_one: "next year", future_other: "in %d years" }
},
ja: {
now: "今",
seconds: { one: "%d秒前", other: "%d秒前", future_one: "%d秒後", future_other: "%d秒後" },
minutes: { one: "%d分前", other: "%d分前", future_one: "%d分後", future_other: "%d分後" },
hours: { one: "%d時間前", other: "%d時間前", future_one: "%d時間後", future_other: "%d時間後" },
days: { one: "昨日", other: "%d日前", future_one: "明日", future_other: "%d日後" },
weeks: { one: "先週", other: "%d週間前", future_one: "来週", future_other: "%d週間後" },
months: { one: "先月", other: "%dヶ月前", future_one: "来月", future_other: "%dヶ月後" },
years: { one: "昨年", other: "%d年前", future_one: "来年", future_other: "%d年後" }
}
}.freeze
def initialize(locale = :en)
@locale = locale
@formats = RELATIVE_FORMATS[@locale] || RELATIVE_FORMATS[:en]
end
def format_relative(time_or_date, reference_time = Time.now)
time = time_or_date.is_a?(Date) ? time_or_date.to_time : time_or_date
diff = time - reference_time
return @formats[:now] if diff.abs < 10
is_future = diff > 0
abs_diff = diff.abs
case abs_diff
when 0...60
format_unit(:seconds, abs_diff.to_i, is_future)
when 60...3600
format_unit(:minutes, (abs_diff / 60).to_i, is_future)
when 3600...86400
format_unit(:hours, (abs_diff / 3600).to_i, is_future)
when 86400...604800
format_unit(:days, (abs_diff / 86400).to_i, is_future)
when 604800...2629746
format_unit(:weeks, (abs_diff / 604800).to_i, is_future)
when 2629746...31556952
format_unit(:months, (abs_diff / 2629746).to_i, is_future)
else
format_unit(:years, (abs_diff / 31556952).to_i, is_future)
end
end
private
def format_unit(unit, count, is_future)
key = if count == 1
is_future ? :future_one : :one
else
is_future ? :future_other : :other
end
format_string = @formats[unit][key]
format_string % count
end
end
# Usage examples
relative_formatter_en = RelativeDateFormatter.new(:en)
relative_formatter_ja = RelativeDateFormatter.new(:ja)
test_times = [
Time.now - 30, # 30 seconds ago
Time.now - 300, # 5 minutes ago
Time.now - 3600, # 1 hour ago
Time.now - 86400, # 1 day ago
Time.now + 86400, # 1 day from now
Time.now - 604800, # 1 week ago
Time.now - 2629746 # 1 month ago
]
puts "Relative time formatting:"
test_times.each do |time|
puts "EN: #{relative_formatter_en.format_relative(time)}"
puts "JA: #{relative_formatter_ja.format_relative(time)}"
puts
end
Ordinal Numbers and Special Formatting
Handle ordinal numbers, special number formatting, and cultural-specific number representations.
Ordinal Number Formatting
# Ordinal number formatter
class OrdinalFormatter
ORDINAL_RULES = {
en: ->(n) {
case n % 100
when 11, 12, 13
"#{n}th"
else
case n % 10
when 1 then "#{n}st"
when 2 then "#{n}nd"
when 3 then "#{n}rd"
else "#{n}th"
end
end
},
es: ->(n) { "#{n}º" }, # Spanish uses º for ordinals
fr: ->(n) { n == 1 ? "1er" : "#{n}e" }, # French 1er, 2e, 3e...
ja: ->(n) { "#{n}番目" } # Japanese uses 番目
}.freeze
def self.format(number, locale = :en)
rule = ORDINAL_RULES[locale] || ORDINAL_RULES[:en]
rule.call(number)
end
end
# Roman numeral formatting
class RomanNumerals
ROMAN_MAP = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
].freeze
def self.to_roman(number)
return "" if number <= 0 || number >= 4000
result = ""
ROMAN_MAP.each do |value, numeral|
count = number / value
result += numeral * count
number -= value * count
end
result
end
def self.from_roman(roman)
roman = roman.upcase
result = 0
i = 0
ROMAN_MAP.each do |value, numeral|
while i < roman.length && roman[i, numeral.length] == numeral
result += value
i += numeral.length
end
end
result
end
end
# Scientific notation formatter
class ScientificFormatter
def self.format(number, precision: 2, locale: :en)
return "0" if number == 0
exponent = Math.log10(number.abs).floor
mantissa = number / (10 ** exponent)
case locale
when :en
sprintf("%.#{precision}f × 10^%d", mantissa, exponent)
when :de
sprintf("%.#{precision}f × 10^%d", mantissa, exponent).gsub('.', ',')
else
sprintf("%.#{precision}fE%d", mantissa, exponent)
end
end
end
# Usage examples
puts "Ordinal numbers:"
(1..5).each do |n|
[:en, :es, :fr, :ja].each do |locale|
puts "#{locale}: #{OrdinalFormatter.format(n, locale)}"
end
puts
end
puts "Roman numerals:"
[1, 5, 10, 50, 100, 500, 1000, 1994, 2024].each do |n|
puts "#{n} => #{RomanNumerals.to_roman(n)}"
end
puts "\nScientific notation:"
[0.0001, 1234.5, 1000000].each do |n|
puts "EN: #{ScientificFormatter.format(n, locale: :en)}"
puts "DE: #{ScientificFormatter.format(n, locale: :de)}"
end
Complete Localization System
Combine all formatting capabilities into a comprehensive localization system.
Unified Localization Manager
# Comprehensive localization system
class LocalizationManager
def initialize(locale = :en)
@locale = locale
@date_formatter = LocaleDateTimeFormatter.new(locale)
@number_formatter = LocaleNumberFormatter.new(locale)
@relative_formatter = RelativeDateFormatter.new(locale)
end
def format_date(date, style: :short)
@date_formatter.format_date(date, style)
end
def format_time(time, include_seconds: false)
@date_formatter.format_time(time, include_seconds: include_seconds)
end
def format_datetime(datetime, date_style: :short, include_seconds: false)
@date_formatter.format_datetime(datetime,
date_style: date_style,
include_seconds: include_seconds)
end
def format_relative_time(time)
@relative_formatter.format_relative(time)
end
def format_number(number, precision: 2)
@number_formatter.format_number(number, precision: precision)
end
def format_currency(amount, precision: 2)
@number_formatter.format_currency(amount, precision: precision)
end
def format_percentage(number, precision: 1)
@number_formatter.format_percentage(number, precision: precision)
end
def format_ordinal(number)
OrdinalFormatter.format(number, @locale)
end
def format_file_size(bytes)
units = case @locale
when :en then %w[B KB MB GB TB]
when :de then %w[B KB MB GB TB]
when :fr then %w[o Ko Mo Go To]
when :ja then %w[B KB MB GB TB]
else %w[B KB MB GB TB]
end
return "0 #{units[0]}" if bytes == 0
exp = (Math.log(bytes) / Math.log(1024)).floor
exp = [exp, units.length - 1].min
size = bytes / (1024.0 ** exp)
"#{format_number(size, precision: exp == 0 ? 0 : 1)} #{units[exp]}"
end
# Format durations
def format_duration(seconds)
case @locale
when :en
format_duration_en(seconds)
when :ja
format_duration_ja(seconds)
else
format_duration_en(seconds)
end
end
private
def format_duration_en(seconds)
return "0 seconds" if seconds == 0
parts = []
if seconds >= 86400
days = seconds / 86400
parts << "#{days.to_i} day#{'s' if days != 1}"
seconds %= 86400
end
if seconds >= 3600
hours = seconds / 3600
parts << "#{hours.to_i} hour#{'s' if hours != 1}"
seconds %= 3600
end
if seconds >= 60
minutes = seconds / 60
parts << "#{minutes.to_i} minute#{'s' if minutes != 1}"
seconds %= 60
end
if seconds > 0 || parts.empty?
parts << "#{seconds.to_i} second#{'s' if seconds != 1}"
end
parts.join(", ")
end
def format_duration_ja(seconds)
return "0秒" if seconds == 0
parts = []
if seconds >= 86400
days = (seconds / 86400).to_i
parts << "#{days}日"
seconds %= 86400
end
if seconds >= 3600
hours = (seconds / 3600).to_i
parts << "#{hours}時間"
seconds %= 3600
end
if seconds >= 60
minutes = (seconds / 60).to_i
parts << "#{minutes}分"
seconds %= 60
end
if seconds > 0 || parts.empty?
parts << "#{seconds.to_i}秒"
end
parts.join("")
end
end
# Usage example
l10n_en = LocalizationManager.new(:en)
l10n_ja = LocalizationManager.new(:ja)
puts "Comprehensive formatting examples:"
puts "Date: #{l10n_en.format_date(Date.today, style: :long)}"
puts "Time: #{l10n_en.format_time(Time.now)}"
puts "Number: #{l10n_en.format_number(1234567.89)}"
puts "Currency: #{l10n_en.format_currency(1234.56)}"
puts "File size: #{l10n_en.format_file_size(1048576)}"
puts "Duration: #{l10n_en.format_duration(3661)}" # 1 hour, 1 minute, 1 second
puts
puts "Japanese formatting:"
puts "Date: #{l10n_ja.format_date(Date.today, style: :long)}"
puts "Duration: #{l10n_ja.format_duration(3661)}"
Best Practices
Date/Time & Number Formatting Best Practices
- Always consider the user's locale when formatting dates and numbers
- Provide consistent formatting throughout your application
- Test with edge cases like leap years, daylight saving time, and large numbers
- Store data in universal formats (ISO 8601 for dates, standard numeric formats)
- Separate formatting from storage - format only for display
- Handle timezone conversions properly when formatting times
- Use relative dates for recent timestamps to improve user experience
- Validate parsed input and provide clear error messages
💡 Pro Tips
- Cache formatted strings for frequently displayed data to improve performance
- Consider using the
strftimemethod with custom patterns for complex formatting - Test your formatting with different locales early in development
- Be aware of cultural differences in number formatting (thousands separators, decimal points)
- Use appropriate precision for different contexts (currency vs. scientific data)
🚀 Next Steps
Test your localization knowledge with our comprehensive quiz covering character encoding, Unicode handling, and formatting techniques.