Ruby Logo

Date/Time & Number Formatting

Implement locale-specific formatting for dates, times, and numbers. Learn to build culturally appropriate displays without Rails.

Home Ruby Date/Time & Number Formatting

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 strftime method 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.

Quick Navigation

Related Topics

Video Tutorial

Watch and learn date/time & number formatting

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