lib/h/h.rb in modalh-1.0.5 vs lib/h/h.rb in modalh-1.1.1
- old
+ new
@@ -46,15 +46,16 @@
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
+ else
value = value.to_i if precision==0
value = value.to_s
value = value[0...-2] if value.end_with?('.0')
- end
+ 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])
@@ -76,15 +77,11 @@
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
+ 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
@@ -167,25 +164,188 @@
falses = options[:falses]
falses ||= [normalize_txt(options[:falses])]
trues.include?(txt) ? true : falses.include?(txt) ? false : nil
end
+ 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
+
+ def longitude_to(value, options={})
+ dms_to value, options.merge(:longitude=>true)
+ end
+
+ def latitude_to(value, options={})
+ dms_to value, options.merge(:latitude=>true)
+ end
+
+ 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
+
# TODO: currency, money, bank accounts, credit card numbers, ...
private
# include ActionView::Helpers::NumberHelper
+ DMS_UNITS = [:deg, :min, :sec]
+ DMS_FACTORS = {
+ :deg=>1, :min=>1.0/60.0, :sec=>1.0/3600.0
+ }
+ SEC_EPSILON = 1E-10
+ SEC_PRECISION = 10
+
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 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
+
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
@@ -259,9 +419,18 @@
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
+
+ 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
end