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.
- 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
|