module H
Localized formatting for Human-iteraction
Constants
- DMS_FACTORS
- DMS_UNITS
include ActionView::Helpers::NumberHelper
- REGEXPS
Date-parsing has been taken from github.com/clemens/delocalize
- SEC_EPSILON
- SEC_PRECISION
Public Class Methods
date_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 121 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
date_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 116 def date_to(value, options={}) return options[:blank] || '' if value.nil? I18n.l(value, options) end
datetime_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 152 def datetime_from(txt, options={}) date_from value, options.reverse_merge(:type=>DateTime) end
datetime_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 148 def datetime_to(value, options={}) date_to value, options.reverse_merge(:type=>DateTime) end
dms_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 171 def dms_from(txt, options={}) original_txt = txt options = dms_format_options(options).merge(options) return nil if txt.to_s.strip.empty? || txt==options[:blank] neg_signs = [options[:south], options[:west]] << '-' pos_signs = [options[:north], options[:east]] << '+' neg_signs, pos_signs = [neg_signs, pos_signs].map {|signs| (signs.map{|s| s.mb_chars.upcase.to_s} + signs.map{|s| s.mb_chars.downcase.to_s}).uniq } signs = neg_signs + pos_signs seps = Array(options[:deg_seps]) + Array(options[:min_seps]) + Array(options[:sec_seps]) neg = false txt = txt.to_s.strip neg_signs.each do |sign| if txt.start_with?(sign) txt = txt[sign.size..-1] neg = true break end if txt.end_with?(sign) txt = txt[0...-sign.size] neg = true break end end unless neg pos_signs.each do |sign| if txt.start_with?(sign) txt = txt[sign.size..-1] break end if txt.end_with?(sign) txt = txt[0...-sign.size] break end end end num_options = number_format_options(options).except(:precision).merge(options) txt = numbers_to_ruby(txt.strip, num_options) default_units = 0 v = 0 seps = (seps.map{|s| Regexp.escape(s)}<<"\\s+")*"|" scanned_txt = "" txt.scan(/((\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)(#{seps})?\s*)/) do |match| scanned_txt << match[0] number = match[1] sep = match[2] if Array(options[:deg_seps]).include?(sep) units = :deg elsif Array(options[:min_seps]).include?(sep) units = :min elsif Array(options[:sec_seps]).include?(sep) units = :sec else units = DMS_UNITS[default_units] end raise ArgumentError, "Invalid degrees-minutes-seconds value #{original_txt}" unless units default_units = DMS_UNITS.index(units) + 1 x = number.to_f x *= DMS_FACTORS[units] v += x end raise ArgumentError, "Invalid degrees-minutes-seconds value #{original_txt} [#{txt}] [#{scanned_txt}]" unless txt==scanned_txt v = -v if neg v end
dms_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 253 def dms_to(value, options={}) longitude = options[:longitude] latitude = options[:latitude] latitude = true if longitude==false && !options.has_key?(:latitude) longitude = true if latitude==false && !options.has_key?(:longitude) options = dms_format_options(options).except(:precision).merge(options) precision = options[:precision] return options[:blank] || '' if value.nil? if value.kind_of?(String) # TODO: recognize nan/infinite values value = value.to_f else if value.respond_to?(:nan?) && value.nan? return options[:nan] || "--" elsif value.respond_to?(:infinite?) && value.infinite? inf = options[:inf] || '∞' return value<0 ? "-#{inf}" : inf end end if value.to_s.start_with?('-') # value<0 # we use to_s to handle negative zero value = -value neg = true end deg = value.floor value -= deg value *= 60 min = value.floor value -= min value *= 60 sec = value.round(SEC_PRECISION) txt = [] txt << integer_to(deg, options.except(:precision)) + Array(options[:deg_seps]).first if min>0 || sec>0 txt << integer_to(min, options.except(:precision)) + Array(options[:min_seps]).first if sec>0 txt << number_to(sec, options) + Array(options[:sec_seps]).first end end txt = txt*" " if longitude || latitude if longitude letter = neg ? options[:west] : options[:east] else letter = neg ? options[:south] : options[:north] end txt = options[:prefix] ? "#{letter} #{txt}" : "#{txt} #{letter}" else txt = "-#{txt}" if neg end txt end
from(txt, options={})
click to toggle source
Produce data from human-localized text (from user interface)
# File lib/h/h.rb, line 10 def from(txt, options={}) type = options[:type] || Float type = Float if type==:number type = check_type(type) if type.ancestors.include?(Numeric) number_from(txt, options) elsif !(type.instance_methods & [:strftime, 'strftime']).empty? date_from(txt, options) elsif type==:logical || type==:boolean logical_from(txt, options) else nil end end
integer_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 103 def integer_from(txt, options={}) options = number_format_options(options).merge(options) 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
integer_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 93 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
latitude_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 249 def latitude_to(value, options={}) dms_to value, options.merge(:latitude=>true) end
logical_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 161 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
logical_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 156 def logical_to(value, options={}) options = logical_format_options(options).merge(options) value.nil? ? options[:blank] : (value ? options[:true] : options[:false]) end
longitude_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 245 def longitude_to(value, options={}) dms_to value, options.merge(:longitude=>true) end
magnitude_from(txt, options={})
click to toggle source
# File lib/h/units.rb, line 64 def magnitude_from(txt, options={}) return nil if txt.to_s.strip.empty? || txt==options[:blank] norm_units = options[:units] if txt.match(/^\s*([0-9\.,+-]+)\s*([a-zA-Z\"\'][a-zA-Z1-3\_\/\*\^\"\']*)\s*$/) txt = $1 from_units = $2 || norm_units else from_units = norm_units end from_units = H::Units.normalize_units(from_units) raise ArgumentError, "Invalid units for #{norm_units}: #{from_units}}" unless from_units v = number_from(txt, options) v *= ::Units.u(from_units) v.in(norm_units) end
magnitude_to(v, options={})
click to toggle source
# File lib/h/units.rb, line 56 def magnitude_to(v, options={}) return options[:blank] || '' if v.nil? norm_units = options[:units] txt = number_to(v, options) txt << " #{H::Units.denormalize_units(norm_units)}" txt end
number_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 76 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] txt = numbers_to_ruby(txt.tr(' ',''), options) 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
number_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 39 def number_to(value, options={}) options = number_format_options(options).except(:precision).merge(options) precision = options[:precision] return options[:blank] || '' if value.nil? unless value.kind_of?(String) value = round(value,precision) if value.respond_to?(:nan?) && value.nan? return options[:nan] || "--" elsif value.respond_to?(:infinite?) && value.infinite? inf = options[:inf] || '∞' return value<0 ? "-#{inf}" : inf else value = value.to_i if precision==0 value = value.to_s value = value[0...-2] if value.end_with?('.0') end # else: TODO recognize nan/infinite values end if options[:delimiter] txt = value.to_s.tr(' ','').tr('.,',options[:separator]+options[:delimiter]).tr(options[:delimiter],'') else txt = value.to_s.tr(' ,','').tr('.',options[:separator]) end raise ArgumentError, "Invalid number #{txt}" unless /\A[+-]?\d+(?:#{Regexp.escape(options[:separator])}\d*)?(?:[eE][+-]?\d+)?\Z/.match(txt) if precision && precision>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
time_from(txt, options={})
click to toggle source
# File lib/h/h.rb, line 144 def time_from(txt, options={}) date_from value, options.reverse_merge(:type=>Time) end
time_to(value, options={})
click to toggle source
# File lib/h/h.rb, line 140 def time_to(value, options={}) date_to value, options.reverse_merge(:type=>Time) end
to(value, options={})
click to toggle source
Generate human-localized text (for user interface) from data
# File lib/h/h.rb, line 26 def to(value, options={}) case value when Numeric number_to(value, options) when Time, Date, DateTime date_to(value, options) when TrueClass, FalseClass logical_to(value, options) else options[:blank] || '' end end
Private Class Methods
apply_regex(format)
click to toggle source
# File lib/h/h.rb, line 413 def apply_regex(format) format.gsub(/(#{REGEXPS.keys.join('|')})/) { |s| REGEXPS[$1] } end
check_type(type)
click to toggle source
# File lib/h/h.rb, line 421 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
default_parse(datetime, type)
click to toggle source
# File lib/h/h.rb, line 386 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
digit_grouping(txt,n,sep,pos0,pos1)
click to toggle source
pos0 first digit, pos1 one past last integral digit
# File lib/h/h.rb, line 364 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
dms_format_options(options)
click to toggle source
# File lib/h/h.rb, line 335 def dms_format_options(options) opt = I18n.translate(:'number.dms.format', :locale => options[:locale]) opt.kind_of?(Hash) ? opt : { :deg_seps => ['°', 'º'], :min_seps => "'", :sec_seps => '"', :north => 'N', :south => 'S', :east => 'E', :west => 'W', :prefix => false } end
input_formats(type, locale=nil)
click to toggle source
# File lib/h/h.rb, line 407 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
logical_format_options(options)
click to toggle source
# File lib/h/h.rb, line 330 def logical_format_options(options) opt = I18n.translate(:'logical.format', :locale => options[:locale]) opt.kind_of?(Hash) ? opt : {:separator=>'.'} end
normalize_txt(txt)
click to toggle source
# File lib/h/h.rb, line 417 def normalize_txt(txt) txt.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.strip.to_s end
number_format_options(options)
click to toggle source
# File lib/h/h.rb, line 325 def number_format_options(options) opt = I18n.translate(:'number.format', :locale => options[:locale]) opt.kind_of?(Hash) ? opt : {:separator=>'.'} end
numbers_to_ruby(txt, options)
click to toggle source
# File lib/h/h.rb, line 428 def numbers_to_ruby(txt, options) if options[:delimiter] txt = txt.tr(options[:delimiter]+options[:separator], ',.').tr(',','') else txt = txt.tr(options[:separator], '.') end txt end
round(v, ndec)
click to toggle source
# File lib/h/h.rb, line 349 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
translate_month_and_day_names!(datetime, locale=nil)
click to toggle source
# File lib/h/h.rb, line 401 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