# :encoding: utf-8 # Localized formatting for Human-iteraction module H class <0 p = txt.index(options[:separator]) if p.nil? txt << options[:separator] p = txt.size - 1 end p += 1 txt << "0"*(precision-txt.size+p) if txt.size-p < precision end digit_grouping txt, 3, options[:delimiter], txt.index(/\d/), txt.index(options[:separator]) || txt.size end def number_from(txt, options={}) options = number_format_options(options).except(:precision).merge(options) type = check_type(options[:type] || (options[:precision]==0 ? Integer : Float)) return nil if txt.to_s.strip.empty? || txt==options[:blank] if options[:delimiter] txt = txt.tr(' ','').tr(options[:delimiter]+options[:separator], ',.').tr(',','') else txt = txt.tr(' ','').tr(options[:separator], '.') end raise ArgumentError, "Invalid number #{txt}" unless /\A[+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?\Z/.match(txt) if type==Float txt.to_f elsif type==Integer txt.to_i else type.new txt end end def integer_to(value, options={}) options = number_format_options(options).merge(options) if value.nil? options[:blank] || '' else value = value.to_s digit_grouping value, 3, options[:delimiter], value.index(/\d/), value.size end end def integer_from(txt) if txt.to_s.strip.empty? || txt==options[:blank] nil else txt = txt.tr(' ','') txt = txt.tr(options[:delimiter],'') if options[:delimiter] txt = txt.tr(options[:separator],'.') end raise ArgumentError, "Invalid integer #{txt}" unless /\A[+-]?\d+(?:\.0*)?\Z/.match(txt) txt.to_i end def date_to(value, options={}) I18n.l(value, options) end def date_from(txt, options={}) options = number_format_options(options).merge(options) type = check_type(options[:type] || Date) return nil if txt.to_s.strip.empty? || txt==options[:blank] return txt if txt.respond_to?(:strftime) translate_month_and_day_names! txt, options[:locale] input_formats(type).each do |original_format| next unless txt =~ /^#{apply_regex(original_format)}$/ txt = DateTime.strptime(txt, original_format) return Date == type ? txt.to_date : Time.zone.local(txt.year, txt.mon, txt.mday, txt.hour, txt.min, txt.sec) end default_parse(txt, type) end def time_to(value, options={}) date_to value, options.reverse_merge(:type=>Time) end def time_from(txt, options={}) date_from value, options.reverse_merge(:type=>Time) end def datetime_to(value, options={}) date_to value, options.reverse_merge(:type=>DateTime) end def datetime_from(txt, options={}) date_from value, options.reverse_merge(:type=>DateTime) end def logical_to(value, options={}) options = logical_format_options(options).merge(options) value.nil? ? options[:blank] : (value ? options[:true] : options[:false]) end def logical_from(txt, options={}) options = logical_format_options(options).merge(options) txt = normalize_txt(txt) trues = options[:trues] trues ||= [normalize_txt(options[:true])] falses = options[:falses] falses ||= [normalize_txt(options[:falses])] trues.include?(txt) ? true : falses.include?(txt) ? false : nil end # TODO: currency, money, bank accounts, credit card numbers, ... private # include ActionView::Helpers::NumberHelper def number_format_options(options) opt = I18n.translate(:'number.format', :locale => options[:locale]) opt.kind_of?(Hash) ? opt : {:separator=>'.'} end def logical_format_options(options) opt = I18n.translate(:'logical.format', :locale => options[:locale]) opt.kind_of?(Hash) ? opt : {:separator=>'.'} end def round(v, ndec) return v if (v.respond_to?(:nan?) && v.nan?) || (v.respond_to?(:infinite?) && v.infinite?) if ndec case v when BigDecimal v = v.round(ndec) when Float k = 10**ndec v = (k*v).round.to_f/k end end v end # pos0 first digit, pos1 one past last integral digit def digit_grouping(txt,n,sep,pos0,pos1) txt[pos0...pos1] = txt[pos0...pos1].gsub(/(\d)(?=(#{'\\d'*n})+(?!\d))/, "\\1#{sep}") if sep txt end # Date-parsing has been taken from https://github.com/clemens/delocalize REGEXPS = { '%B' => "(#{Date::MONTHNAMES.compact.join('|')})", # long month name '%b' => "(#{Date::ABBR_MONTHNAMES.compact.join('|')})", # short month name '%m' => "(\\d{1,2})", # numeric month '%A' => "(#{Date::DAYNAMES.join('|')})", # full day name '%a' => "(#{Date::ABBR_DAYNAMES.join('|')})", # short day name '%Y' => "(\\d{4})", # long year '%y' => "(\\d{2})", # short year '%e' => "(\\s\\d|\\d{2})", # short day '%d' => "(\\d{1,2})", # full day '%H' => "(\\d{2})", # hour (24) '%M' => "(\\d{2})", # minute '%S' => "(\\d{2})" # second } def default_parse(datetime, type) return if datetime.blank? begin today = Date.current parsed = Date._parse(datetime) return if parsed.empty? # the datetime value is invalid # set default year, month and day if not found parsed.reverse_merge!(:year => today.year, :mon => today.mon, :mday => today.mday) datetime = Time.zone.local(*parsed.values_at(:year, :mon, :mday, :hour, :min, :sec)) Date == type ? datetime.to_date : datetime rescue datetime end end def translate_month_and_day_names!(datetime, locale=nil) translated = I18n.t([:month_names, :abbr_month_names, :day_names, :abbr_day_names], :scope => :date, :locale=>locale).flatten.compact original = (Date::MONTHNAMES + Date::ABBR_MONTHNAMES + Date::DAYNAMES + Date::ABBR_DAYNAMES).compact translated.each_with_index { |name, i| datetime.gsub!(name, original[i]) } end def input_formats(type, locale=nil) # Date uses date formats, all others use time formats type = type == Date ? :date : :time I18n.t(:"#{type}.formats", :locale=>locale).slice(*I18n.t(:"#{type}.input.formats", :locale=>locale)).values end def apply_regex(format) format.gsub(/(#{REGEXPS.keys.join('|')})/) { |s| REGEXPS[$1] } end def normalize_txt(txt) txt.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.strip.to_s end def check_type(type) orig_type = type type = type.to_s.camelcase.safe_constantize if type.kind_of?(Symbol) raise ArgumentError, "Invalid type #{orig_type}" unless type && type.class==Class type end end end