bahuvrihi-constants 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -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.
data/Rakefile ADDED
@@ -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
data/lib/constants.rb ADDED
@@ -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