constants 0.1.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.
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2006-2008, Regents of the University of Colorado.
2
+ Developer:: Simon Chiang, Biomolecular Structure Program
3
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
9
+ to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or
12
+ substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
21
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,125 @@
1
+ = Constants
2
+
3
+ Libraries of physical and chemical constants for scientific calculations in Ruby.
4
+
5
+ == Description
6
+
7
+ Constants provides libraries of constant values such as the precise mass of carbon 13, or
8
+ the spin of a strange quark. When applicable, the constant values include uncertainty and
9
+ units (via {ruby-units}[http://rubyforge.org/projects/ruby-units]). Also provides
10
+ Constants::Library to index and generate common collections of constant values.
11
+
12
+ I have attempted to use reputable sources for the constants data (see below). Please notify
13
+ me of any errors and send me suggestions for other constants to include.
14
+
15
+ * Rubyforge[http://rubyforge.org/projects/bioactive]
16
+ * Lighthouse[http://bahuvrihi.lighthouseapp.com/projects/13504-constants/overview]
17
+ * Github[http://github.com/bahuvrihi/constants/tree/master]
18
+
19
+ == Usage
20
+
21
+ require 'constants'
22
+ include Constants::Libraries
23
+
24
+ # Element predefines all chemical elements
25
+ c = Element::C
26
+ c.name # => "Carbon"
27
+ c.symbol # => "C"
28
+ c.atomic_number # => 6
29
+ c.mass # => 12.0
30
+ c.mass(13) # => 13.0033548378
31
+
32
+ # A smorgasbord of lookups methods
33
+ Element['Carbon'] # => Element::C
34
+ Element['C'] # => Element::C
35
+ Element[6] # => Element::C
36
+
37
+ === Custom Libraries
38
+
39
+ Making a new constants library is straightforward using the Constants::Library module.
40
+ The following example is adapted from the molecule[http://github.com/bahuvrihi/molecules/tree/master]
41
+ library (a subproject of constants).
42
+
43
+ # A library of amino acid residues.
44
+ class Residue
45
+ attr_reader :letter, :abbr, :name
46
+
47
+ def initialize(letter, abbr, name)
48
+ @letter = letter
49
+ @abbr = abbr
50
+ @name = name
51
+ end
52
+
53
+ A = Residue.new('A', "Ala", "Alanine")
54
+ C = Residue.new('C', "Cys", "Cysteine")
55
+ D = Residue.new('D', "Asp", "Aspartic Acid")
56
+ # ... normally you'd add the rest here ...
57
+
58
+ include Constants::Library
59
+
60
+ # add an index by an attribute or method
61
+ library.index_by_attribute :letter
62
+
63
+ # add an index where keys are calculated by a block
64
+ library.index_by 'upcase abbr' do |residue|
65
+ residue.abbr.upcase
66
+ end
67
+
68
+ # add a collection (same basic idea, but using an array)
69
+ library.collect_attribute 'name'
70
+ end
71
+
72
+ # index access through []
73
+ Residue['D'] # => Residue::D
74
+ Residue['ALA'] # => Residue::A
75
+
76
+ # access an index hash or collection array
77
+ Residue.index('upcase abbr') # => {'ALA' => Residue::A, 'CYS' => Residue::C, 'ASP' => Residue::D}
78
+ Residue.collection('name') # => ["Alanine", "Cysteine", "Aspartic Acid"]
79
+
80
+ As you can see, Constants::Library allows the predefinition of common views generated
81
+ for a set of constants. Nothing you couldn't do yourself, but very handy.
82
+
83
+ == Known Issues
84
+
85
+ * Particle data is from an unreliable source
86
+ * Constants::Constant could use some development; constants should support mathematical
87
+ operations and comparisons based on uncertainty as well as value.
88
+ * Ruby doesn't track of the order of constant declaration until Ruby 1.9. Constants
89
+ are indexed/collected as they appear in [module].constants
90
+
91
+ == Installation
92
+
93
+ Constants is available as a gem through RubyForge[http://rubyforge.org/projects/bioactive]. Use:
94
+
95
+ % gem install constants
96
+
97
+ == Info
98
+
99
+ Copyright (c) 2006-2008, Regents of the University of Colorado.
100
+ Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
101
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
102
+ Licence:: MIT-Style
103
+
104
+ === Element Data
105
+
106
+ Element isotope, mass, and abundance information was obtained from the NIST {Atomic Weights and Isotopic Compositions}[http://www.physics.nist.gov/PhysRefData/Compositions/index.html] reference. All isotopes
107
+ with a non-nil isotopic composition (ie relative abundance) were compiled from this {view}[http://www.physics.nist.gov/cgi-bin/Compositions/stand_alone.pl?ele=&all=all&ascii=ascii2&isotype=all]
108
+ on 2008-01-22.
109
+
110
+ === Physical Constant Data
111
+
112
+ The physical constant data is assembled from the NIST {Fundamental Physical Constants}[http://www.physics.nist.gov/cuu/Constants/Table/allascii.txt]
113
+ on 2008-04-28.
114
+
115
+ Constants adds several units to ruby-units to support the physical constants
116
+ reported in the NIST data. These are (as reported in the NIST data):
117
+
118
+ electron-volt:: 1.602176487e-19 joules
119
+ kelvin:: 1.3806504e-23 joules
120
+ hartree:: 4.35974394e-18 joules
121
+
122
+ === Particle Data
123
+
124
+ Particle data was assembled from a non-ideal source, wikipedia[http://www.wikipedia.org/], and will remain
125
+ so until I have time to update it with something better.
@@ -0,0 +1,171 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'yaml'
6
+
7
+ # tasks
8
+ desc 'Default: Run tests.'
9
+ task :default => :test
10
+
11
+ desc 'Run tests.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib'
14
+ t.pattern = File.join('test', ENV['subset'] || '', ENV['pattern'] || '**/*_test.rb')
15
+ t.verbose = true
16
+ end
17
+
18
+ #
19
+ # admin tasks
20
+ #
21
+
22
+ def gemspec
23
+ data = File.read('constants.gemspec')
24
+ spec = nil
25
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
26
+ spec
27
+ end
28
+
29
+ Rake::GemPackageTask.new(gemspec) do |pkg|
30
+ pkg.need_tar = true
31
+ end
32
+
33
+ task :print_manifest do
34
+ # collect files from the gemspec, labeling
35
+ # with true or false corresponding to the
36
+ # file existing or not
37
+ files = gemspec.files.inject({}) do |files, file|
38
+ files[File.expand_path(file)] = [File.exists?(file), file]
39
+ files
40
+ end
41
+
42
+ # gather non-rdoc/pkg files for the project
43
+ # and add to the files list if they are not
44
+ # included already (marking by the absence
45
+ # of a label)
46
+ Dir.glob("**/*").each do |file|
47
+ next if file =~ /^(rdoc|pkg)/ || File.directory?(file)
48
+
49
+ path = File.expand_path(file)
50
+ files[path] = ["", file] unless files.has_key?(path)
51
+ end
52
+
53
+ # sort and output the results
54
+ files.values.sort_by {|exists, file| file }.each do |entry|
55
+ puts "%-5s : %s" % entry
56
+ end
57
+ end
58
+
59
+ desc 'Generate documentation.'
60
+ Rake::RDocTask.new(:rdoc) do |rdoc|
61
+ rdoc.rdoc_dir = 'rdoc'
62
+ rdoc.title = 'constants'
63
+ rdoc.options << '--line-numbers' << '--inline-source'
64
+ rdoc.rdoc_files.include(["README", 'MIT-LICENSE'])
65
+ rdoc.rdoc_files.include(gemspec.files.select {|file| file =~ /^lib/})
66
+ end
67
+
68
+ desc "Publish RDoc to RubyForge"
69
+ task :publish_rdoc => [:rdoc] do
70
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
71
+ host = "#{config["username"]}@rubyforge.org"
72
+
73
+ rsync_args = "-v -c -r"
74
+ remote_dir = "/var/www/gforge-projects/bioactive/constants"
75
+ local_dir = "rdoc"
76
+
77
+ sh %{rsync #{rsync_args} #{local_dir}/ #{host}:#{remote_dir}}
78
+ end
79
+
80
+ #
81
+ # constants tasks
82
+ #
83
+
84
+ desc "Regenerate physical constants and relationship data"
85
+ task :generate_constants do
86
+ require 'open-uri'
87
+
88
+ nist_url = "http://www.physics.nist.gov/cuu/Constants/Table/allascii.txt"
89
+ nist_data = open(nist_url)
90
+
91
+ split_regexp = /^(.+?\s\s)(\d[\de\-\s\.]*?\s\s\s*)(\d[\de\-\s\.]*?\s\s\s*)(.*)$/
92
+ split_regexp_str = nil
93
+
94
+ constants = []
95
+ declarations = []
96
+ units = []
97
+
98
+ nist_data.each_line do |line|
99
+ next if line =~ /^(\s|-)/
100
+
101
+ unless line =~ split_regexp
102
+ raise "could not match line:\n#{line}\nwith: #{split_regexp_str}"
103
+ end
104
+
105
+ if split_regexp_str == nil
106
+ split_regexp_str = "^(.{#{$1.length}})(.{#{$2.length}})(.{#{$3.length}})(.*)$"
107
+ split_regexp = Regexp.new(split_regexp_str)
108
+ redo
109
+ end
110
+
111
+ name = $1
112
+ value = $2
113
+ uncertainty = $3
114
+ unit = $4
115
+ constant = name.split(/[\s\-]/).collect do |word|
116
+ word = word.gsub(/\./, "")
117
+ word = word.gsub("/", "_")
118
+
119
+ word =~ /^[A-z\d]+$/ ? word.upcase : nil
120
+ end.compact.join("_")
121
+
122
+ if constants.include?(constant)
123
+ raise "constant name conflict: #{constant}"
124
+ end
125
+
126
+ constants << constant
127
+ type = (constant =~ /_RELATIONSHIP$/ ? units : declarations)
128
+ type << [constant, name.strip, value.gsub(/\s/, ""), uncertainty.gsub(/\s/, "").gsub("(exact)", "0"), unit.strip]
129
+ end
130
+
131
+ puts "# Constants from: #{nist_url}"
132
+ puts "# Date: #{Time.now.to_s}"
133
+
134
+ max = declarations.inject(0) {|max, c| c.first.length > max ? c.first.length : max}
135
+ declarations.each do |declaration|
136
+ puts %Q{%-#{max}s = Physical.new("%s", "%s", "%s", "%s")} % declaration
137
+ end
138
+
139
+ puts
140
+ puts "# Relationships from: #{nist_url}"
141
+ puts "# Date: #{Time.now.to_s}"
142
+
143
+ require 'ruby-units'
144
+ base_units = Unit.class_eval('@@BASE_UNITS').collect {|u| u[1..-2] }
145
+ unit_map = Unit.class_eval('@@UNIT_MAP')
146
+ units = units.collect do |constant, name, value, uncertainty, unit|
147
+
148
+ # parse the relationship units
149
+ unit_name, relation_name = name.strip.chomp('relationship').split('-', 2).collect do |str|
150
+ str.strip!
151
+ case str
152
+ when 'atomic mass unit' then 'amu'
153
+ else str.gsub(/\s/, "-")
154
+ end
155
+ end
156
+
157
+ # format constants to sort in the correct declaration order
158
+ constant = constant.chomp('_RELATIONSHIP')
159
+ [constant, unit_name, relation_name, value, unit, uncertainty]
160
+ end
161
+
162
+ max = units.inject(0) {|max, c| c.first.length > max ? c.first.length : max}
163
+ units.each do |declaration|
164
+ puts %Q{%-#{max}s = ['%s', '%s', '%s', '%s', '%s']} % declaration
165
+ end
166
+ end
167
+
168
+ # desc "Regenerate elements data"
169
+ # task :generate_elements do
170
+ #
171
+ # end
@@ -0,0 +1,5 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'constants/libraries/element'
4
+ require 'constants/libraries/physical'
5
+ require 'constants/libraries/particle'
@@ -0,0 +1,94 @@
1
+ require 'constants/uncertainty'
2
+ require 'constants/library'
3
+ require 'rubygems'
4
+ require 'ruby-units'
5
+
6
+ # Adds several units to {ruby-units}[http://rubyforge.org/projects/ruby-units] for use
7
+ # in reporting physical constant data. See the README.
8
+ class Unit < Numeric
9
+
10
+ # Relationships from: http://www.physics.nist.gov/cuu/Constants/Table/allascii.txt
11
+ # Date: Mon Apr 28 21:09:29 -0600 2008
12
+ # ELECTRON_VOLT_JOULE = ['electron-volt', 'joule', '1.602176487e-19', 'J', '0.000000040e-19']
13
+ # KELVIN_JOULE = ['kelvin', 'joule', '1.3806504e-23', 'J', '0.0000024e-23']
14
+ # HARTREE_JOULE = ['hartree', 'joule', '4.35974394e-18', 'J', '0.00000022e-18']
15
+
16
+ UNIT_DEFINITIONS['<electron-volt>'] = [%w{eV}, 1.602176487e-19, :energy, %w{<meter> <meter> <kilogram>}, %w{<second> <second>}]
17
+ UNIT_DEFINITIONS['<kelvin>'] = [%w{K}, 1.3806504e-23, :energy, %w{<meter> <meter> <kilogram>}, %w{<second> <second>}]
18
+ UNIT_DEFINITIONS['<hartree>'] = [%w{E_h}, 4.35974394e-18, :energy, %w{<meter> <meter> <kilogram>}, %w{<second> <second>}]
19
+ end
20
+ Unit.setup
21
+
22
+ module Constants
23
+
24
+ # Constant tracks the value, uncertainty, and unit of measurement for a
25
+ # measured quantity. Units are tracked via {ruby-units}[http://rubyforge.org/projects/ruby-units].
26
+ class Constant
27
+ include Comparable
28
+
29
+ class << self
30
+
31
+ # Parses the common notation '<value>(<uncertainty>)' into a value and
32
+ # an uncertainty. When no uncertainty is specified, the uncertainty is nil.
33
+ # Whitespace is allowed.
34
+ #
35
+ # Base.parse("1.0(2)").vu # => [1.0, 0.2]
36
+ # Base.parse("1.007 825 032 1(4)").vu # => [1.0078250321, 1/2500000000]
37
+ # Base.parse("6.626 068 96").vu # => [6.62606896, nil]
38
+ #
39
+ def parse(str)
40
+ str = str.to_s.gsub(/\s/, '')
41
+ raise "cannot parse: #{str}" unless str =~ /^(-?\d+)(\.\d+)?(\(\d+\))?(e-?\d+)?(.*)$/
42
+
43
+ value = "#{$1}#{$2}#{$4}".to_f
44
+ unit = $5
45
+ uncertainty = case
46
+ when $3 == nil then nil
47
+ else
48
+ factor = $2 == nil ? 0 : 1 - $2.length
49
+ factor += $4[1..-1].to_i unless $4 == nil
50
+ $3[1..-2].to_i * 10 ** factor
51
+ end
52
+
53
+ block_given? ? yield(value, unit, uncertainty) : new(value, unit, uncertainty)
54
+ end
55
+ end
56
+
57
+ # The measured value of the constant
58
+ attr_reader :value
59
+
60
+ # The uncertainty of the measured value. May be exact or unknown,
61
+ # represented by Uncertainty::EXACT (0) and Uncertainty::UNKNOWN
62
+ # (nil) respectively.
63
+ attr_reader :uncertainty
64
+
65
+ # The units of the measured value, may be nil for unitless constants
66
+ attr_reader :unit
67
+
68
+ def initialize(value, unit=nil, uncertainty=Uncertainty::UNKNOWN)
69
+ @value = value
70
+ @unit = unit.to_s.strip.empty? ? nil : Unit.new(unit)
71
+ @uncertainty = uncertainty
72
+ end
73
+
74
+ # For Numeric inputs, compares value to another. For all other inputs,
75
+ # peforms the default == comparison.
76
+ def ==(another)
77
+ case another
78
+ when Numeric then value == another
79
+ else super(another)
80
+ end
81
+ end
82
+
83
+ # Compares the value of another to the value of self.
84
+ def <=>(another)
85
+ value <=> another.value
86
+ end
87
+
88
+ # Returns the array: [value, uncertainty]
89
+ def to_a
90
+ [value, uncertainty]
91
+ end
92
+ end
93
+
94
+ end
@@ -0,0 +1,299 @@
1
+ require 'constants/stash'
2
+
3
+ module Constants
4
+
5
+ # ConstantLibrary facilitates indexing and collection of a set of values.
6
+ #
7
+ # lib = ConstantLibrary.new('one', 'two', :three)
8
+ # lib.index_by('upcase') {|value| value.to_s.upcase }
9
+ # lib.indicies['upcase'] # => {'ONE' => 'one', 'TWO' => 'two', 'THREE' => :three}
10
+ #
11
+ # lib.collect("string") {|value| value.to_s }
12
+ # lib.collections['string'] # => ['one', 'two', 'three']
13
+ #
14
+ # See Constants::Library for more details.
15
+ class ConstantLibrary
16
+
17
+ # A hash-based Stash to index library objects.
18
+ class Index < Hash
19
+ include Constants::Stash
20
+
21
+ # The block used to calculate keys during stash
22
+ attr_reader :block
23
+
24
+ # Indicates when values are skipped during stash
25
+ attr_reader :exclusion_value
26
+
27
+ # Initializes a new Index (a type of Hash).
28
+ #
29
+ # The block is used by stash to calculate the key for
30
+ # stashing a given value. If the key equals the exclusion
31
+ # value, then the value is skipped. The new index will
32
+ # return nil_value for unknown keys (ie it is the default
33
+ # value for self) and CANNOT be stashed (see Stash).
34
+ def initialize(exclusion_value=nil, nil_value=nil, &block)
35
+ super(nil_value, &nil)
36
+ @nil_value = nil_value
37
+ @exclusion_value = exclusion_value
38
+ @block = block
39
+ end
40
+
41
+ # Stashes the specified values using keys calculated
42
+ # by the block. Skips values when the block returns
43
+ # the exclusion value.
44
+ #
45
+ # See Constants::ConstantLibrary#index_by for more details.
46
+ def stash(values)
47
+ values.each_with_index do |value, index|
48
+ result = block.call(value)
49
+
50
+ case result
51
+ when exclusion_value then next
52
+ when Array then super(*result)
53
+ else super(result, value)
54
+ end
55
+ end
56
+
57
+ self
58
+ end
59
+ end
60
+
61
+ # An array-based Stash for collections of library objects.
62
+ #
63
+ #--
64
+ # Note: comparison of Index and Collection indicates that
65
+ # these are highly related classes. Why no exclusion value
66
+ # and why no modifiable nil_value for Collection? Simply
67
+ # because an array ALWAYS returns nil for uninitialized
68
+ # locations (esp out-of-bounds locations). This means that
69
+ # Stash, which uses the value at self[] to determine when
70
+ # to stash and when not to stash, must have nil as it's
71
+ # nil_value to behave correctly. Effectively treating
72
+ # nil as an exclusion value for collection works well in
73
+ # this case since nils cannot be stashed.
74
+ #
75
+ # Hashes (ie Index) do not share this behavior. Since you
76
+ # can define a default value for missing keys, self[] can
77
+ # return something other than nil... hence there is an
78
+ # opportunity to use non-nil nil_values and non-nil
79
+ # exclusion values.
80
+ class Collection < Array
81
+ include Constants::Stash
82
+
83
+ # The block used to calculate keys during stash
84
+ attr_reader :block
85
+
86
+ # Initializes a new Collection (a type of Array). The block is
87
+ # used by stash to calculate the values in a collection.
88
+ def initialize(&block)
89
+ super(&nil)
90
+ @nil_value = nil
91
+ @block = block
92
+ end
93
+
94
+ # Stashes the specified values in self using values calculated
95
+ # by the block. Values are skipped if the block returns nil.
96
+ #
97
+ # See Constants::ConstantLibrary#collect for more details.
98
+ def stash(values)
99
+ values.each do |value|
100
+ value, index = block.call(value)
101
+ next if value == nil
102
+
103
+ super(index == nil ? self.length : index, value)
104
+ end
105
+
106
+ self
107
+ end
108
+ end
109
+
110
+ # An array of values in the library
111
+ attr_reader :values
112
+
113
+ # A hash of (name, index) pairs tracking the indicies in self
114
+ attr_reader :indicies
115
+
116
+ # A hash of (name, collection) pairs tracking the collections in self
117
+ attr_reader :collections
118
+
119
+ def initialize(*values)
120
+ @values = values.uniq
121
+ @indicies = {}
122
+ @collections = {}
123
+ end
124
+
125
+ # Adds an index to self for all values currently in self. The block is
126
+ # used to specify keys for each value in self; it receives each value and
127
+ # should return one of the following:
128
+ # - a key
129
+ # - a [key, value] array when an alternate value should be stored
130
+ # in the place of value
131
+ # - the exclusion_value to exclude the value from the index
132
+ #
133
+ # When multiple values return the same key, they are stashed into an array.
134
+ #
135
+ # lib = ConstantLibrary.new('one', 'two', :one)
136
+ # lib.index_by("string") {|value| value.to_s }
137
+ # lib.indicies['string']
138
+ # # => {
139
+ # # 'one' => ['one', :one],
140
+ # # 'two' => 'two'}
141
+ #
142
+ # Existing indicies by the specified name are overwritten.
143
+ #
144
+ # ==== nil values
145
+ #
146
+ # The index stores it's data in an Index (ie a Hash) where nil_value
147
+ # acts as the default value returned for non-existant keys as well as
148
+ # the stash nil_value. Hence index_by will raise an error if you try
149
+ # to store the nil_value.
150
+ #
151
+ # This behavior can be seen when the exclusion value is set to something
152
+ # other than nil, so that the nil value isn't skipped outright:
153
+ #
154
+ # # the nil will cause trouble
155
+ # lib = ConstantLibrary.new(1,2,nil)
156
+ # lib.index_by("error", false, nil) {|value| value } # ! ArgumentError
157
+ #
158
+ # Specify an alternate nil_value (and exclusion value) to index nils;
159
+ # a plain old Object works well.
160
+ #
161
+ # obj = Object.new
162
+ # index = lib.index_by("ok", false, obj) {|value| value }
163
+ # index[1] # => 1
164
+ # index[nil] # => nil
165
+ #
166
+ # # remember the nil_value is the default value
167
+ # index['non-existant'] # => obj
168
+ #
169
+ def index_by(name, exclusion_value=nil, nil_value=nil, &block) # :yields: value
170
+ raise ArgumentError.new("no block given") unless block_given?
171
+
172
+ index = Index.new(exclusion_value, nil_value, &block)
173
+ indicies[name] = index
174
+ index.stash(values)
175
+ end
176
+
177
+ # Adds an index using the attribute or method. Equivalent to:
178
+ #
179
+ # lib.index_by(attribute) {|value| value.attribute }
180
+ #
181
+ def index_by_attribute(attribute, exclusion_value=nil, nil_value=nil)
182
+ method = attribute.to_sym
183
+ index_by(attribute, exclusion_value, nil_value) {|value| value.send(method) }
184
+ end
185
+
186
+ # Adds a collection to self for all values currently in self. The block
187
+ # is used to calculate the values in the collection. The block receives
188
+ # each value in self and should return one of the following:
189
+ # - a value to be pushed onto the collection
190
+ # - a [value, index] array when an alternate value should be stored
191
+ # in the place of value, or when the value should be at a special
192
+ # index in the collection. When multiple values are directed to the
193
+ # same index, they are stashed into an array.
194
+ # - nil to exclude the value from the collection
195
+ #
196
+ # For example:
197
+ #
198
+ # lib = ConstantLibrary.new('one', 'two', :three)
199
+ # lib.collect("string") {|value| value.to_s }
200
+ # lib.collections['string'] # => ['one', 'two', 'three']
201
+ #
202
+ # lib.collect("length") {|value| [value, value.to_s.length] }
203
+ # lib.collections['length'] # => [nil, nil, nil, ['one', 'two'], nil, :three]
204
+ #
205
+ # Works much like index_by, except that the underlying data store for a
206
+ # collection is a Collection (ie an array) rather than an Index (a hash).
207
+ def collect(name, &block) # :yields: value
208
+ raise ArgumentError.new("no block given") unless block_given?
209
+
210
+ collection = Collection.new(&block)
211
+ collections[name] = collection
212
+ collection.stash(values)
213
+ end
214
+
215
+ # Adds a collection using the attribute or method. Equivalent to:
216
+ #
217
+ # lib.collect(attribute) {|value| value.attribute }
218
+ #
219
+ def collect_attribute(attribute)
220
+ method = attribute.to_sym
221
+ collect(attribute) {|value| value.send(method) }
222
+ end
223
+
224
+ # Lookup values by a key. All indicies will be searched in order; the first
225
+ # matching value is returned. Returns nil if no matches are found.
226
+ def [](key)
227
+ indicies.values.each do |index|
228
+ return index[key] if index.has_key?(key)
229
+ end
230
+
231
+ nil
232
+ end
233
+
234
+ # Clears all values, from self, indicies, and collections. The indicies,
235
+ # and collections themselves are preserved unless complete==true.
236
+ def clear(complete=false)
237
+ values.clear
238
+
239
+ [indicies, collections].each do |stashes|
240
+ complete ? stashes.clear : stashes.values.each {|stash| stash.clear}
241
+ end
242
+ end
243
+
244
+ # Add the specified values to self. New values are incorporated into existing
245
+ # indicies and collections. Returns the values added (ie values minus any
246
+ # already-existing values).
247
+ def add(*values)
248
+ new_values = values - self.values
249
+ self.values.concat(new_values)
250
+
251
+ [indicies, collections].each do |stashes|
252
+ stashes.values.each do |stash|
253
+ stash.stash(new_values)
254
+ end
255
+ end
256
+
257
+ #new_values.each(&add_block) if add_block
258
+ new_values
259
+ end
260
+
261
+ # Adds the constants from the specified module. If mod is a Class, then
262
+ # only constants that are a kind of mod will be added. This behavior
263
+ # can be altered by providing a block which each constant value; values
264
+ # are only included if the block evaluates to true.
265
+ def add_constants_from(mod)
266
+ const_names = mod.constants.select do |const_name|
267
+ const = mod.const_get(const_name)
268
+ block_given? ? yield(const) : (mod.kind_of?(Class) ? const.kind_of?(mod) : true)
269
+ end
270
+
271
+ add(*const_names.collect {|const_name| mod.const_get(const_name) })
272
+ end
273
+
274
+ # # Specifies a block to execute when values are added to self.
275
+ # # Existing values are sent to the add_block immediately.
276
+ # def on_add(override=false, &block) # :yields: value
277
+ # raise "Add block already set!" unless add_block.nil? || override
278
+ # @add_block = block
279
+ # values.each(&block)
280
+ # block
281
+ # end
282
+
283
+ # def merge!(hash)
284
+ # hash.each_pair do |key, item|
285
+ # existing = self[key]
286
+ # if existing != nil
287
+ # items[items.index(existing)] = item
288
+ # else
289
+ # add(item)
290
+ # end
291
+ # end
292
+ #
293
+ # items = self.items
294
+ # self.clear(false)
295
+ # add *items
296
+ # end
297
+
298
+ end
299
+ end