Module: H

Extended by:
Helpers
Defined in:
lib/h/h.rb,
lib/h/units.rb,
lib/h/fields.rb,
lib/h/helpers.rb,
lib/h/active_record_extensions.rb

Overview

Localized formatting for Human-iteraction

Defined Under Namespace

Modules: ActiveRecordExtensions, Fields, Helpers, Units

Class Method Summary collapse

Class Method Details

.date_from(txt, options = {}) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# 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 = {}) ⇒ Object



116
117
118
119
# 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 = {}) ⇒ Object



152
153
154
# 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 = {}) ⇒ Object



148
149
150
# 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 = {}) ⇒ Object

Raises:

  • (ArgumentError)


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# 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 = {}) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# 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 = {}) ⇒ Object

Produce data from human-localized text (from user interface)



10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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 = {}) ⇒ Object

Raises:

  • (ArgumentError)


103
104
105
106
107
108
109
110
111
112
113
114
# 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 = {}) ⇒ Object



93
94
95
96
97
98
99
100
101
# 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 = {}) ⇒ Object



249
250
251
# File 'lib/h/h.rb', line 249

def latitude_to(value, options={})
  dms_to value, options.merge(:latitude=>true)
end

.logical_from(txt, options = {}) ⇒ Object



161
162
163
164
165
166
167
168
169
# 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 = {}) ⇒ Object



156
157
158
159
# 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 = {}) ⇒ Object



245
246
247
# File 'lib/h/h.rb', line 245

def longitude_to(value, options={})
  dms_to value, options.merge(:longitude=>true)
end

.magnitude_from(txt, options = {}) ⇒ Object

Raises:

  • (ArgumentError)


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 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 = {}) ⇒ Object



56
57
58
59
60
61
62
# 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 = {}) ⇒ Object

Raises:

  • (ArgumentError)


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 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 = {}) ⇒ Object

Raises:

  • (ArgumentError)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 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 = {}) ⇒ Object



144
145
146
# 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 = {}) ⇒ Object



140
141
142
# File 'lib/h/h.rb', line 140

def time_to(value, options={})
  date_to value, options.reverse_merge(:type=>Time)
end

.to(value, options = {}) ⇒ Object

Generate human-localized text (for user interface) from data



26
27
28
29
30
31
32
33
34
35
36
37
# 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