amee-data-abstraction 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.rvmrc +1 -0
  2. data/CHANGELOG.txt +4 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +41 -0
  5. data/LICENSE.txt +27 -0
  6. data/README.txt +188 -0
  7. data/Rakefile +102 -0
  8. data/VERSION +1 -0
  9. data/amee-data-abstraction.gemspec +115 -0
  10. data/examples/_calculator_form.erb +27 -0
  11. data/examples/calculation_controller.rb +16 -0
  12. data/init.rb +4 -0
  13. data/lib/amee-data-abstraction.rb +30 -0
  14. data/lib/amee-data-abstraction/calculation.rb +236 -0
  15. data/lib/amee-data-abstraction/calculation_set.rb +101 -0
  16. data/lib/amee-data-abstraction/drill.rb +63 -0
  17. data/lib/amee-data-abstraction/exceptions.rb +47 -0
  18. data/lib/amee-data-abstraction/input.rb +197 -0
  19. data/lib/amee-data-abstraction/metadatum.rb +58 -0
  20. data/lib/amee-data-abstraction/ongoing_calculation.rb +545 -0
  21. data/lib/amee-data-abstraction/output.rb +16 -0
  22. data/lib/amee-data-abstraction/profile.rb +108 -0
  23. data/lib/amee-data-abstraction/prototype_calculation.rb +350 -0
  24. data/lib/amee-data-abstraction/term.rb +506 -0
  25. data/lib/amee-data-abstraction/terms_list.rb +150 -0
  26. data/lib/amee-data-abstraction/usage.rb +90 -0
  27. data/lib/config/amee_units.rb +129 -0
  28. data/lib/core-extensions/class.rb +27 -0
  29. data/lib/core-extensions/hash.rb +43 -0
  30. data/lib/core-extensions/ordered_hash.rb +21 -0
  31. data/lib/core-extensions/proc.rb +15 -0
  32. data/rails/init.rb +32 -0
  33. data/spec/amee-data-abstraction/calculation_set_spec.rb +54 -0
  34. data/spec/amee-data-abstraction/calculation_spec.rb +75 -0
  35. data/spec/amee-data-abstraction/drill_spec.rb +38 -0
  36. data/spec/amee-data-abstraction/input_spec.rb +77 -0
  37. data/spec/amee-data-abstraction/metadatum_spec.rb +17 -0
  38. data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +494 -0
  39. data/spec/amee-data-abstraction/profile_spec.rb +39 -0
  40. data/spec/amee-data-abstraction/prototype_calculation_spec.rb +256 -0
  41. data/spec/amee-data-abstraction/term_spec.rb +385 -0
  42. data/spec/amee-data-abstraction/terms_list_spec.rb +53 -0
  43. data/spec/config/amee_units_spec.rb +71 -0
  44. data/spec/core-extensions/class_spec.rb +25 -0
  45. data/spec/core-extensions/hash_spec.rb +44 -0
  46. data/spec/core-extensions/ordered_hash_spec.rb +12 -0
  47. data/spec/core-extensions/proc_spec.rb +12 -0
  48. data/spec/fixtures/electricity.rb +35 -0
  49. data/spec/fixtures/electricity_and_transport.rb +55 -0
  50. data/spec/fixtures/transport.rb +26 -0
  51. data/spec/spec.opts +2 -0
  52. data/spec/spec_helper.rb +244 -0
  53. metadata +262 -0
@@ -0,0 +1,506 @@
1
+ # Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
2
+ # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
3
+
4
+ # :title: Class: AMEE::DataAbstraction::Term
5
+ require 'bigdecimal'
6
+
7
+ module AMEE
8
+ module DataAbstraction
9
+
10
+ # Base class for representing quantities which are inputs to, outputs of, or
11
+ # metadata associated with, calculations. Typically several instances of the
12
+ # <i>Term</i> class (or subclasses) will be associated with instances of the
13
+ # <i>Calculation</i> class or its subclasses (<i>PrototypeCalculation</i>,
14
+ # <i>OngoingCalculation</i>).
15
+ #
16
+ # Instances of <i>Term</i> are represented by several primary attributes:
17
+ #
18
+ # label:: Symbol representing the unique, machine-readable name for the
19
+ # term (<b>required</b>)
20
+ #
21
+ # value:: In principle, any object which represent the desired value
22
+ # which the term represents
23
+ #
24
+ # name:: String representing a human-readable name for the term
25
+ #
26
+ # path:: String representing the AMEE platform path to the AMEE item
27
+ # value definition which is associated with <tt>self</tt>.
28
+ # This attribute is required only if the term represents an
29
+ # item value definition in the AMEE platform
30
+ #
31
+ # Other available attribute-like methods include <tt>type</tt>,
32
+ # <tt>interface</tt>, <tt>note</tt>, <tt>unit</tt>, <tt>per_unit</tt>,
33
+ # <tt>default_unit</tt>, <tt>default_per_unit</tt> and <tt>parent</tt>.
34
+ #
35
+ # Subclasses of the <i>Term</i> correspond to:
36
+ # * <i>Input</i>
37
+ # * * <i>Profile</i> -- corresponds to an AMEE profile item value
38
+ # * * <i>Drill</i> -- corresponds to an AMEE drill down choice
39
+ # * * <i>Usage</i> -- corresponds to a (runtime adjustable) AMEE usage choice
40
+ # * * <i>Metadatum</i> -- represents other arbitrary inputs
41
+ # * <i>Output</i> -- corresponds to an AMEE return value
42
+ #
43
+ class Term
44
+
45
+ public
46
+
47
+ # Symbol representing the unique (within the parent calculation), machine-
48
+ # readable name for <tt>self</tt>. Set a value by passing an argument.
49
+ # Retrieve a value by calling without an argument, e.g.,
50
+ #
51
+ # my_term.label :distance
52
+ #
53
+ # my_term.label #=> :distance
54
+ #
55
+ attr_property :label
56
+
57
+ # String representing a human-readable name for <tt>self</tt>. Set a value
58
+ # by passing an argument. Retrieve a value by calling without an argument,
59
+ # e.g.,
60
+ #
61
+ # my_term.name 'Distance driven'
62
+ #
63
+ # my_term.name #=> 'Distance driven'
64
+ #
65
+ attr_property :name
66
+
67
+ # Symbol representing the class the value should be parsed to. If
68
+ # omitted a string is assumed, e.g.:
69
+ #
70
+ # my_term.type :integer
71
+ # my_term.value "12"
72
+ # my_term.value # => 12
73
+ # my_term.value_before_cast # => "12"
74
+ #
75
+ attr_property :type
76
+
77
+ # String representing a the AMEE platform path for <tt>self</tt>. Set a
78
+ # value by passing an argument. Retrieve a value by calling without an
79
+ # argument, e.g.,
80
+ #
81
+ # my_term.path 'mass'
82
+ #
83
+ # my_term.path #=> 'mass'
84
+ #
85
+ attr_property :path
86
+
87
+ # String representing an annotation for <tt>self</tt>. Set a value by
88
+ # passing an argument. Retrieve a value by calling without an argument,
89
+ # e.g.,
90
+ #
91
+ # my_term.note 'Enter the mass of cement produced in the reporting period'
92
+ #
93
+ # my_term.note #=> 'Enter the mass of cement ...'
94
+ #
95
+ attr_property :note
96
+
97
+ # Symbol representing the owning parent calculation of <tt>self</tt>. Set
98
+ # the owning calculation object by passing as an argument. Retrieve it by
99
+ # calling without an argument, e.g.,
100
+ #
101
+ # my_calculation = <AMEE::DataAbstraction::OngoingCalculation ... >
102
+ #
103
+ # my_term.parent my_calculation
104
+ #
105
+ # my_term.parent #=> <AMEE::DataAbstraction::OngoingCalculation ... >
106
+ #
107
+ attr_accessor :parent
108
+
109
+ # Stores pre-cast value
110
+ attr_accessor :value_before_cast
111
+
112
+ # Initialize a new instance of <i>Term</i>.
113
+ #
114
+ # The term can be configured in place by passing a block (evaluated in the
115
+ # context of the new instance) which defines the term properties using the
116
+ # macro-style instance helper methods.
117
+ #
118
+ # my_term = Term.new {
119
+ #
120
+ # label :size
121
+ # path "vehicleSize"
122
+ # hide!
123
+ # ...
124
+ # }
125
+ #
126
+ # The parent calculation object associated with <tt>self</tt> can be assigned
127
+ # using the :parent hash key passed as an argument.
128
+ #
129
+ # Unless otherwise configured within the passed block, several attributes
130
+ # attempt to take default configurations if possible using rules of thumb:
131
+ #
132
+ # * value => <tt>nil</tt>
133
+ # * enabled => <tt>true</tt>
134
+ # * visible => <tt>true</tt>
135
+ # * label => underscored, symbolized version of <tt>path</tt>
136
+ # * path => stringified version of <tt>label</tt>
137
+ # * name => stringified and humanised version of <tt>label</tt>
138
+ # * unit => <tt>default_unit</tt>
139
+ # * per_unit => <tt>default_per_unit</tt>
140
+ #
141
+ def initialize(options={},&block)
142
+ @parent=options[:parent]
143
+ @value=nil
144
+ @type=nil
145
+ @enabled=true
146
+ @visible=true
147
+ instance_eval(&block) if block
148
+ label path.to_s.underscore.to_sym unless path.blank?||label
149
+ path label.to_s unless path
150
+ name label.to_s.humanize unless name
151
+ unit default_unit unless unit
152
+ per_unit default_per_unit unless per_unit
153
+ end
154
+
155
+ # Valid choices for suggested interfaces for a term.
156
+ # Dynamic boolean methods (such as <tt>text_box?</tt>) are generated for
157
+ # checking which value is set.
158
+ #
159
+ # my_term.drop_down? #=> true
160
+ #
161
+ Interfaces=[:text_box,:drop_down,:date]
162
+
163
+ Interfaces.each do |inf|
164
+ define_method("#{inf.to_s}?") {
165
+ interface==inf
166
+ }
167
+ end
168
+
169
+ # Symbolized attribute representing the expected interface type for
170
+ # <tt>self</tt>. Set a value by passing an argument. Retrieve a value by
171
+ # calling without an argument, e.g.,
172
+ #
173
+ # my_term.interface :drop_down
174
+ #
175
+ # my_term.interface #=> :drop_down
176
+ #
177
+ # Must represent one of the valid choices defined in the
178
+ # <i>Term::Interfaces</i> constant
179
+ #
180
+ # If the provided interface is not valid (as defined in <i>Term::Interfaces</i>)
181
+ # an <i>InvalidInterface</i> exception is raised
182
+ #
183
+ def interface(inf=nil)
184
+ if inf
185
+ raise Exceptions::InvalidInterface unless Interfaces.include? inf
186
+ @interface=inf
187
+ end
188
+ return @interface
189
+ end
190
+
191
+ # Object representing the value which <tt>self</tt> is considered to
192
+ # represent (e.g. the quantity or name of something). Set a value by
193
+ # passing an argument. Retrieve a value by calling without an argument,
194
+ # e.g.,
195
+ #
196
+ # my_term.value 12
197
+ # my_term.value #=> 12
198
+ #
199
+ #
200
+ # my_term.value 'Ford Escort'
201
+ # my_term.value #=> 'Ford Escort'
202
+ #
203
+ #
204
+ # my_term.value DateTime.civil(2010,12,31)
205
+ # my_term.value #=> <Date: 4911123/2,0,2299161>
206
+ #
207
+ def value(*args)
208
+ unless args.empty?
209
+ @value_before_cast = args.first
210
+ @value = @type ? self.class.convert_value_to_type(args.first, @type) : args.first
211
+ end
212
+ @value
213
+ end
214
+
215
+ # Symbols representing the attributes of <tt>self</tt> which are concerned
216
+ # with quantity units.
217
+ #
218
+ # Each symbol also represents <b>dynamically defined method<b> name for
219
+ # setting and retrieving the default and current units and per units. Units
220
+ # are initialized as instances of <i>Quantify::Unit::Base</tt> is required.
221
+ #
222
+ # Set a unit attribute by passing an argument. Retrieve a value by calling
223
+ # without an argument. Unit attributes can be defined by any form which is
224
+ # accepted by the <i>Quantify::Unit#for</i> method (either an instance of
225
+ # <i>Quantify::Unit::Base</i> (or subclass) or a symbolized or string
226
+ # representation of the a unit symbol, name or label). E.g.,
227
+ #
228
+ # my_term.unit :mi
229
+ # my_term.unit #=> <Quantify::Unit::NonSI:0xb71cac48 @label="mi" ... >
230
+ #
231
+ # my_term.default_unit 'feet'
232
+ # my_term.default_unit #=> <Quantify::Unit::NonSI:0xb71cac48 @label="ft" ... >
233
+ #
234
+ #
235
+ # my_time_unit = Unit.hour #=> <Quantify::Unit::NonSI:0xb71cac48 @label="h" ... >
236
+ # my_term.default_per_unit my_time_unit
237
+ # my_term.default_per_unit #=> <Quantify::Unit::NonSI:0xb71cac48 @label="h" ... >
238
+ #
239
+ #
240
+ # Dynamically defined methods are also available for setting and retrieving
241
+ # alternative units for the <tt>unit</tt> and <tt>per_unit</tt> attributes.
242
+ # If no alternative units are explicitly defined, they are instantiated by
243
+ # default to represent all dimensionally equivalent units available in the
244
+ # system of units defined by <i>Quantify</i>. E.g.
245
+ #
246
+ # my_term.unit :kg
247
+ # my_term.alternative_units #=> [ <Quantify::Unit::NonSI:0xb71cac48 @label="mi" ... >,
248
+ # <Quantify::Unit::SI:0xb71cac48 @label="km" ... >,
249
+ # <Quantify::Unit::NonSI:0xb71cac48 @label="ft" ... >,
250
+ # ... ]
251
+ #
252
+ # my_term.unit 'litre'
253
+ # my_term.alternative_units :bbl, :gal
254
+ # my_term.alternative_units #=> [ <Quantify::Unit::NonSI:0xb71cac48 @label="bbl" ... >,
255
+ # <Quantify::Unit::NonSI:0xb71cac48 @label="gal" ... > ]
256
+ #
257
+ UnitFields = [:unit,:per_unit,:default_unit,:default_per_unit]
258
+
259
+ UnitFields.each do |field|
260
+ define_method(field) do |*unit|
261
+ instance_variable_set("@#{field}",Unit.for(unit.first)) unless unit.empty?
262
+ instance_variable_get("@#{field}")
263
+ end
264
+ end
265
+
266
+ [:unit,:per_unit].each do |field|
267
+ define_method("alternative_#{field}s") do |*args|
268
+ ivar = "@alternative_#{field}s"
269
+ default = send("default_#{field}".to_sym)
270
+ unless args.empty?
271
+ args << default if default
272
+ units = args.map {|arg| Unit.for(arg) }
273
+ Term.validate_dimensional_equivalence?(*units)
274
+ instance_variable_set(ivar, units)
275
+ else
276
+ return instance_variable_get(ivar) if instance_variable_get(ivar)
277
+ return instance_variable_set(ivar, (default.alternatives << default)) if default
278
+ end
279
+ end
280
+ end
281
+
282
+ # Returns <tt>true</tt> if <tt>self</tt> has a populated value attribute.
283
+ # Otherwise, returns <tt>false</tt>.
284
+ #
285
+ def set?
286
+ !value_before_cast.nil?
287
+ end
288
+
289
+ # Returns <tt>true</tt> if <tt>self</tt> does not have a populated value
290
+ # attribute. Otherwise, returns <tt>false</tt>.
291
+ #
292
+ def unset?
293
+ value_before_cast.nil?
294
+ end
295
+
296
+ # Declare that the term's UI element should be disabled
297
+ def disable!
298
+ @disabled=true
299
+ end
300
+
301
+ # Declare that the term's UI element should be enabled
302
+ def enable!
303
+ @disabled=false
304
+ end
305
+
306
+ # Returns <tt>true</tt> if the UI element of <tt>self</tt> is disabled.
307
+ # Otherwise, returns <tt>false</tt>.
308
+ #
309
+ def disabled?
310
+ @disabled
311
+ end
312
+
313
+ # Returns <tt>true</tt> if the UI element of <tt>self</tt> is enabled.
314
+ # Otherwise, returns <tt>false</tt>.
315
+ #
316
+ def enabled?
317
+ !disabled?
318
+ end
319
+
320
+ # Returns <tt>true</tt> if <tt>self</tt> is configured as visible.
321
+ # Otherwise, returns <tt>false</tt>.
322
+ #
323
+ def visible?
324
+ @visible
325
+ end
326
+
327
+ # Returns <tt>true</tt> if <tt>self</tt> is configured as hidden.
328
+ # Otherwise, returns <tt>false</tt>.
329
+ #
330
+ def hidden?
331
+ !visible?
332
+ end
333
+
334
+ # Declare that the term's UI element should not be shown in generated UIs.
335
+ def hide!
336
+ @visible=false
337
+ end
338
+
339
+ # Declare that the term's UI element should be shown in generated UIs.
340
+ def show!
341
+ @visible=true
342
+ end
343
+
344
+ # Returns <tt>true</tt> if <tt>self</tt> has a numeric value. That is, can
345
+ # it have statistics applied? This method permits handling of term summing,
346
+ # averaging, etc. Otherwise, returns <tt>false</tt>.
347
+ #
348
+ def has_numeric_value?
349
+ set? and Float(value) rescue false
350
+ end
351
+
352
+ # Returns a pretty print string representation of <tt>self</tt>
353
+ def inspect
354
+ elements = {:label => label, :value => value, :unit => unit,
355
+ :per_unit => per_unit, :type => type,
356
+ :disabled => disabled?, :visible => visible?}
357
+ attr_list = elements.map {|k,v| "#{k}: #{v.inspect}" } * ', '
358
+ "<#{self.class.name} #{attr_list}>"
359
+ end
360
+
361
+ # Returns <tt>true</tt> if <tt>self</tt> occurs before the term with a label
362
+ # matching <tt>lab</tt> in the terms list of the parent calculation. Otherwise,
363
+ # returns <tt>false</tt>.
364
+ #
365
+ def before?(lab)
366
+ parent.terms.labels.index(lab)>parent.terms.labels.index(label)
367
+ end
368
+
369
+ # Returns <tt>true</tt> if <tt>self</tt> occurs after the term with a label
370
+ # matching <tt>lab</tt> in the terms list of the parent calculation. Otherwise,
371
+ # returns <tt>false</tt>.
372
+ #
373
+ def after?(lab)
374
+ parent.terms.labels.index(lab)<parent.terms.labels.index(label)
375
+ end
376
+
377
+ def initialize_copy(source)
378
+ super
379
+ UnitFields.each do |property|
380
+ prop = send(property)
381
+ self.send(property, prop.clone) unless prop.nil?
382
+ end
383
+ end
384
+
385
+ # Return a new instance of <i>Term</i>, based on <tt>self</tt> but with
386
+ # a change of units, according to the <tt>options</tt> hash provided, and
387
+ # the value attribute updated to reflect the new units.
388
+ #
389
+ # To specify a new unit, pass the required unit via the <tt>:unit</tt> key.
390
+ # To specify a new per_unit, pass the required per unit via the
391
+ # <tt>:per_unit</tt> key. E.g.,
392
+ #
393
+ # my_term.convert_unit(:unit => :kg)
394
+ #
395
+ # my_term.convert_unit(:unit => :kg, :per_unit => :h)
396
+ #
397
+ # my_term.convert_unit(:unit => 'kilogram')
398
+ #
399
+ # my_term.convert_unit(:per_unit => Quantify::Unit.h)
400
+ #
401
+ # my_term.convert_unit(:unit => <Quantify::Unit::SI ... >)
402
+ #
403
+ # If <tt>self</tt> does not hold a numeric value or either a unit or per
404
+ # unit attribute, <tt.self</tt> is returned.
405
+ #
406
+ def convert_unit(options={})
407
+ return self unless has_numeric_value? and (unit or per_unit)
408
+ new = clone
409
+ if options[:unit] and unit
410
+ new_unit = Unit.for(options[:unit])
411
+ Term.validate_dimensional_equivalence?(unit,new_unit)
412
+ new.value Quantity.new(new.value,new.unit).to(new_unit).value
413
+ new.unit options[:unit]
414
+ end
415
+ if options[:per_unit] and per_unit
416
+ new_per_unit = Unit.for(options[:per_unit])
417
+ Term.validate_dimensional_equivalence?(per_unit,new_per_unit)
418
+ new.value Quantity.new(new.value,(1/new.per_unit)).to(Unit.for(new_per_unit)).value
419
+ new.per_unit options[:per_unit]
420
+ end
421
+ return new
422
+ end
423
+
424
+ # Return an instance of Quantify::Quantity describing the quantity represented
425
+ # by <tt>self</tt>.
426
+ #
427
+ # If <tt>self</tt> does not contain a numeric value, <tt>nil</tt> is returned.
428
+ #
429
+ # If <tt>self</tt> contains a numeric value, but no unit or per unit, just
430
+ # the numeric value is returned
431
+ #
432
+ def to_quantity
433
+ return nil unless has_numeric_value?
434
+ if (unit.is_a? Quantify::Unit::Base) && (per_unit.is_a? Quantify::Unit::Base)
435
+ quantity_unit = unit/per_unit
436
+ elsif unit.is_a? Quantify::Unit::Base
437
+ quantity_unit = unit
438
+ elsif per_unit.is_a? Quantify::Unit::Base
439
+ quantity_unit = 1/per_unit
440
+ else
441
+ return value
442
+ end
443
+ Quantity.new(value,quantity_unit)
444
+ end
445
+ alias :to_q :to_quantity
446
+
447
+ # Returns a string representation of term based on the term value and any
448
+ # units which are defined. The format of the unit representation follows
449
+ # that defined by <tt>format</tt>, which should represent any of the formats
450
+ # supported by the <i>Quantify::Unit::Base</tt> class (i.e. :name,
451
+ # :pluralized_name, :symbol and :label). Default behaviour uses the unit
452
+ # symbol atribute, i.e. if no format explcitly specified:
453
+ #
454
+ # my_term.to_s #=> "12345 ton"
455
+ #
456
+ # my_term.to_s :symbol #=> "12345 ton"
457
+ #
458
+ # my_term.to_s :name #=> "12345 short ton"
459
+ #
460
+ # my_term.to_s :pluralized_name #=> "12345 tons"
461
+ #
462
+ # my_term.to_s :label #=> "12345 ton_us"
463
+ #
464
+ def to_s(format=:symbol)
465
+ string = "#{value}"
466
+ if unit and per_unit
467
+ string += " #{(unit/per_unit).send(format)}"
468
+ elsif unit
469
+ string += " #{unit.send(format)}"
470
+ elsif per_unit
471
+ string += " #{(1/per_unit).send(format)}"
472
+ end
473
+ return string
474
+ end
475
+
476
+ # Checks that the units included in <tt>units</tt> are dimensionally
477
+ # equivalent, that is, that they represent the same physucal quantity
478
+ #
479
+ def self.validate_dimensional_equivalence?(*units)
480
+ unless [units].flatten.all? {|unit| unit.dimensions == units[0].dimensions }
481
+ raise AMEE::DataAbstraction::Exceptions::InvalidUnits,
482
+ "The specified term units are not of equivalent dimensions: #{units.map(&:label).join(",")}"
483
+ end
484
+ end
485
+
486
+ def self.convert_value_to_type(value, type)
487
+ return nil if value.blank?
488
+ type = type.downcase.to_sym if type.is_a?(String)
489
+
490
+ case type
491
+ when :string then value.to_s
492
+ when :text then value.to_s
493
+ when :integer then value.to_i rescue 0
494
+ when :fixnum then value.to_i rescue 0
495
+ when :float then value.to_f rescue 0
496
+ when :decimal then value.to_s.to_d rescue 0
497
+ when :double then value.to_s.to_d rescue 0
498
+ when :datetime then DateTime.parse(value.to_s) rescue nil
499
+ when :time then Time.parse(value.to_s) rescue nil
500
+ when :date then Date.parse(value.to_s) rescue nil
501
+ else value
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end