constants 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/README +125 -0
- data/Rakefile +171 -0
- data/lib/constants.rb +5 -0
- data/lib/constants/constant.rb +94 -0
- data/lib/constants/constant_library.rb +299 -0
- data/lib/constants/libraries/element.rb +214 -0
- data/lib/constants/libraries/particle.rb +83 -0
- data/lib/constants/libraries/physical.rb +297 -0
- data/lib/constants/library.rb +133 -0
- data/lib/constants/stash.rb +94 -0
- data/lib/constants/uncertainty.rb +8 -0
- data/test/constants/constant_library_test.rb +304 -0
- data/test/constants/constant_test.rb +77 -0
- data/test/constants/libraries/element_test.rb +207 -0
- data/test/constants/libraries/particle_test.rb +43 -0
- data/test/constants/libraries/physical_test.rb +32 -0
- data/test/constants/library_test.rb +125 -0
- data/test/constants/stash_test.rb +99 -0
- data/test/constants_test_helper.rb +51 -0
- data/test/constants_test_suite.rb +3 -0
- data/test/readme_doc_test.rb +58 -0
- metadata +84 -0
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,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
|