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.
Ruby's Time and Date classes provide flexible formatting options that can be adapted for different locales and cultural preferences.
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
Create formatters that adapt to different cultural conventions for displaying dates and times.
# 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
Format numbers according to different cultural conventions for decimal separators, thousands separators, and currency display.
# 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')}"
Implement advanced formatting features like relative dates, ordinal numbers, and smart pluralization.
# 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
Handle ordinal numbers, special number formatting, and cultural-specific number representations.
# 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
Combine all formatting capabilities into a comprehensive localization system.
# 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)}"
strftime method with custom patterns for complex formattingTest your localization knowledge with our comprehensive quiz covering character encoding, Unicode handling, and formatting techniques.
Watch and learn date/time & number formatting