require 'hanami/utils/kernel' module Hanami module Helpers # Number formatter # # You can include this module inside your view and # the view will have access all methods. # # By including Hanami::Helpers::NumberFormattingHelper it will # inject private method: format_number. # # @since 0.2.0 module NumberFormattingHelper private # Format the given number, according to the options # # It accepts a number (Numeric) or a string representation. # # If an integer is given, no precision is applied. # For the rest of the numbers, it will format as a float representation. # This is the case of: Float, BigDecimal, # Complex, Rational. # # If the argument cannot be coerced into a number, it will raise a # TypeError. # # @param number [Numeric,String] the number to be formatted # # @return [String] formatted number # # @raise [TypeError] if number can't be formatted # # @since 0.2.0 # # @example # require 'hanami/helpers/number_formatting_helper' # # class Checkout # include Hanami::Helpers::NumberFormattingHelper # # def total # format_number 1999.99 # end # # def euros # format_number 1256.95, delimiter: '.', separator: ',' # end # # def visitors_count # format_number '1000' # end # end # # view = Checkout.new # # view.total # # => "1,999.99" # # view.euros # # => "1.256,95" # # view.visitors_count # # => "1,000" def format_number(number, options = {}) Formatter.new(number, options).format end # Formatter # # @since 0.2.0 # @api private class Formatter # Regex to delimitate integer part of a number # # @return [Regexp] the delimitation regex # # @since 0.2.0 # @api private # # @see Hanami::Helpers::NumberFormatter::Formatter#delimitate DELIMITATION_REGEX = /(\d)(?=(\d{3})+$)/ # Regex to guess if the number is a integer # # @return [Regexp] the guessing regex # # @since 0.2.0 # @api private # # @see Hanami::Helpers::NumberFormatter::Formatter#to_number INTEGER_REGEXP = /\A[\d]+\z/ # Default separator # # @return [String] default separator # # @since 0.2.0 # @api private DEFAULT_SEPARATOR = '.'.freeze # Default delimiter # # @return [String] default delimiter # # @since 0.2.0 # @api private DEFAULT_DELIMITER = ','.freeze # Default precision # # @return [Integer] default precision # # @since 0.2.0 # @api private DEFAULT_PRECISION = 2 # Initialize a new formatter # # @param number [Numeric,String] the number to format # @param options [Hash] options for number formatting # @option options [String] :delimiter hundred delimiter # @option options [String] :separator fractional part delimiter # @option options [Integer] :precision rounding precision # # @since 0.2.0 # @api private # # @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_DELIMITER # @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_SEPARATOR # @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_PRECISION def initialize(number, options) @number = number @delimiter = options.fetch(:delimiter, DEFAULT_DELIMITER) @separator = options.fetch(:separator, DEFAULT_SEPARATOR) @precision = options.fetch(:precision, DEFAULT_PRECISION) end # Format number according to the specified options # # @return [String] formatted number # # @raise [TypeError] if number can't be formatted # # @since 0.2.0 # @api private def format parts.join(@separator) end private # Return integer and fractional parts # # @return [Array] parts # # @since 0.2.0 # @api private def parts integer_part, fractional_part = to_str.split(DEFAULT_SEPARATOR) [delimitate(integer_part), fractional_part].compact end # Delimitate the given part # # @return [String] delimitated string # # @since 0.2.0 # @api private def delimitate(part) part.gsub(DELIMITATION_REGEX) { |digit| "#{digit}#{@delimiter}" } end # String coercion # # @return [String] coerced number # # @raise [TypeError] if number can't be formatted # # @since 0.2.0 # @api private def to_str to_number.to_s end # Numeric coercion # # @return [Numeric] coerced number # # @raise [TypeError] if number can't be formatted # # @since 0.2.0 # @api private def to_number case @number when NilClass raise TypeError when ->(n) { n.to_s.match(INTEGER_REGEXP) } Utils::Kernel.Integer(@number) else Utils::Kernel.Float(rounded_number) end end # Round number in case we need to return a Float representation. # If @number doesn't respond to #round return the number as it is. # # @return [Float,Complex,Rational,BigDecimal] rounded number, if applicable # # @since 0.2.0 # @api private def rounded_number if @number.respond_to?(:round) @number.round(@precision) else @number end end end end end end