amee-data-abstraction 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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