dimensional 0.0.2
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/CHANGELOG +0 -0
- data/LICENSE +20 -0
- data/README +11 -0
- data/Rakefile +38 -0
- data/lib/dimensional.rb +8 -0
- data/lib/dimensional/configurator.rb +118 -0
- data/lib/dimensional/dimension.rb +56 -0
- data/lib/dimensional/measure.rb +98 -0
- data/lib/dimensional/metric.rb +59 -0
- data/lib/dimensional/system.rb +51 -0
- data/lib/dimensional/unit.rb +90 -0
- data/test/configurator_test.rb +144 -0
- data/test/demo.rb +140 -0
- data/test/dimension_test.rb +55 -0
- data/test/dimensional_test.rb +27 -0
- data/test/measure_test.rb +185 -0
- data/test/metric_test.rb +90 -0
- data/test/system_test.rb +40 -0
- data/test/unit_test.rb +102 -0
- metadata +80 -0
data/CHANGELOG
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008-2009 Christopher Cyrus Hapgood
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or 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 HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# This library aims to provide effective parsing of user-supplied measures/metrics, intuitive scaling of those
|
2
|
+
# measures into a standard base scale unit and intuitive presentation of values. It does not attempt to mixin
|
3
|
+
# unit-oriented methods into Ruby standard classes -to perform operations, particularly across dimensions, you
|
4
|
+
# will need to use basic conversion methods provided by this library combined with standard Ruby numerical oper-
|
5
|
+
# ations. There is no method_missing magic or large mixin of spiffy unit-like methods here.
|
6
|
+
|
7
|
+
# The long-term objective for this library is to achieve compliance with the UCUM standard, with additional
|
8
|
+
# facilities for simple parsing and formatting of measures according to localized standards, and conversion
|
9
|
+
# of measures between units. References:
|
10
|
+
# UCUM Website: http://unitsofmeasure.org/
|
11
|
+
# UCUM Standard: http://aurora.regenstrief.org/~ucum/ucum.html
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
task :default => [:test]
|
8
|
+
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << 'test'
|
11
|
+
t.test_files = FileList['test/*_test.rb']
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
Rake::Task['test'].comment = "Run all tests in test/*_test.rb"
|
15
|
+
|
16
|
+
spec = Gem::Specification.new do |s|
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
s.name = %q{dimensional}
|
19
|
+
s.version = "0.0.2"
|
20
|
+
s.required_ruby_version = '>= 1.6.8'
|
21
|
+
s.date = %q{2009-10-09}
|
22
|
+
s.authors = ["Chris Hapgood"]
|
23
|
+
s.email = %q{cch1@hapgoods.com}
|
24
|
+
s.summary = %q{Dimensional provides handling for numbers with units.}
|
25
|
+
s.homepage = %q{http://cho.hapgoods.com/dimensional}
|
26
|
+
s.description = <<-EOF
|
27
|
+
Dimensional provides handling for dimensional values (numbers with units). Dimensional values
|
28
|
+
can be parsed, stored, converted and formatted for output.
|
29
|
+
EOF
|
30
|
+
s.files = Dir['lib/**/*.rb'] + Dir['test/**/*.rb']
|
31
|
+
s.files += ["README", "CHANGELOG", "LICENSE", "Rakefile"]
|
32
|
+
s.test_files = Dir['test/**/*.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
36
|
+
pkg.need_zip = true
|
37
|
+
pkg.need_tar = false
|
38
|
+
end
|
data/lib/dimensional.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'dimensional/dimension'
|
2
|
+
require 'dimensional/system'
|
3
|
+
require 'dimensional/unit'
|
4
|
+
require 'dimensional/metric'
|
5
|
+
|
6
|
+
module Dimensional
|
7
|
+
# A little DSL for defining units. Beware of Ruby 1.8 binding a block variable
|
8
|
+
# to a local variable if they have the same name -it can subtly goof things up.
|
9
|
+
class Configurator
|
10
|
+
# A simple container for holding a context for definition, parsing and formatting of dimensional data
|
11
|
+
Context = Struct.new(:system, :dimension, :unit) do
|
12
|
+
def valid?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :context
|
18
|
+
|
19
|
+
# Start the configurator with the given context hash and evaluate the supplied block
|
20
|
+
def self.start(c_hash = {}, &block)
|
21
|
+
new.change_context(c_hash, block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.dimension_default_metric_name(d)
|
25
|
+
d && d.symbol
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(c = Context.new)
|
29
|
+
@context = c
|
30
|
+
raise "Invalid context" unless context.valid?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Change the context, either for the scope of the supplied block or for this instance
|
34
|
+
# NB: The scope in which constants (and classes) are evaluated in instance_eval is surprising in some
|
35
|
+
# versions of Ruby. Reference: http://groups.google.com/group/ruby-talk-google/browse_thread/thread/186ac9e618a7312d/a8c5dafa7fcfa3dd?lnk=raot
|
36
|
+
def change_context(c_hash, block = nil)
|
37
|
+
new_context = context.dup
|
38
|
+
c_hash.each{|k, v| new_context[k] = v}
|
39
|
+
if block
|
40
|
+
self.class.new(new_context).instance_eval &block
|
41
|
+
else
|
42
|
+
@context = new_context
|
43
|
+
self # Allow chaining
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Change dimension of the context to the given dimension (or its symbol)
|
48
|
+
def dimension(d = nil, &block)
|
49
|
+
d = Dimension[d] unless d.kind_of?(Dimension)
|
50
|
+
change_context({:dimension => d}, block)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Change system of the context to the given system (or its abbreviation)
|
54
|
+
def system(s = nil, &block)
|
55
|
+
s = System[s] unless s.kind_of?(System)
|
56
|
+
change_context({:system => s}, block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Register a new base unit
|
60
|
+
def base(name, options = {}, &block)
|
61
|
+
u = register_unit(name, options)
|
62
|
+
change_context({:unit => u}, block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Register a new derived unit
|
66
|
+
def derive(name, factor, options = {}, &block)
|
67
|
+
options[:reference_factor] = factor
|
68
|
+
options[:reference_unit] = context.unit
|
69
|
+
u = register_unit(name, options)
|
70
|
+
change_context({:unit => u}, block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Register an alias for the unit in context
|
74
|
+
def alias(name, options = {}, &block)
|
75
|
+
derive(name, 1, options, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Register a new unit in the current context that references an arbitrary unit
|
79
|
+
def reference(name, reference, factor, options = {}, &block)
|
80
|
+
options.merge!(:reference_factor => factor, :reference_unit => reference)
|
81
|
+
u = register_unit(name, options)
|
82
|
+
change_context({:unit => u}, block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Register a new unit in the current context that is composed of multiple units
|
86
|
+
def combine(name, components, options = {}, &block)
|
87
|
+
options.merge!(:reference_factor => 1, :reference_unit => components)
|
88
|
+
u = register_unit(name, options)
|
89
|
+
change_context({:unit => u}, block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Register preferred options for the current unit in the named metric
|
93
|
+
def prefer(name, options = {})
|
94
|
+
m = find_or_register_metric(name, dimension_metric)
|
95
|
+
m.prefer(context.unit, options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
context.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def register_unit(name, options)
|
104
|
+
u = Unit.register(name, context.system, context.dimension, options)
|
105
|
+
dimension_metric.prefer(u, options)
|
106
|
+
u
|
107
|
+
end
|
108
|
+
|
109
|
+
def dimension_metric
|
110
|
+
find_or_register_metric(self.class.dimension_default_metric_name(context.dimension), nil)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Register the metric defined by the given key and the current context if it does not already exist.
|
114
|
+
def find_or_register_metric(name, parent = nil)
|
115
|
+
Metric[name] || Metric.register(name, context.dimension, parent)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Dimensional
|
2
|
+
# Represents a dimension that can be used to characterize a physical quantity. With the exception of certain
|
3
|
+
# fundamental dimensions, all dimensions are expressed as a set of exponents relative to the fundamentals.
|
4
|
+
# Reference: http://en.wikipedia.org/wiki/Dimensional_analysis
|
5
|
+
class Dimension
|
6
|
+
@registry = Hash.new
|
7
|
+
@symbol_registry = Hash.new # Not a Ruby Symbol but a (typically one-character) natural language symbol
|
8
|
+
|
9
|
+
def self.register(*args)
|
10
|
+
d = new(*args)
|
11
|
+
raise "Dimension #{d.name} already exists" if @registry[d.name]
|
12
|
+
raise "Dimension #{d.name}'s symbol already exists" if @symbol_registry[d.symbol]
|
13
|
+
@registry[d.name.to_sym] = d
|
14
|
+
@symbol_registry[d.symbol.to_sym] = d
|
15
|
+
const_set(d.symbol.to_s, d) rescue nil # Not all symbols strings are valid constant names
|
16
|
+
d
|
17
|
+
end
|
18
|
+
|
19
|
+
# Lookup the dimension by name or symbol
|
20
|
+
def self.[](sym)
|
21
|
+
return nil unless sym = sym && sym.to_sym
|
22
|
+
@registry[sym] || @symbol_registry[sym]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Purge all dimensions from storage.
|
26
|
+
def self.reset!
|
27
|
+
constants.each {|d| remove_const(d)}
|
28
|
+
@registry.clear
|
29
|
+
@symbol_registry.clear
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :exponents, :name, :symbol
|
33
|
+
|
34
|
+
def initialize(name, symbol = nil, exponents = {})
|
35
|
+
exponents.each_pair do |k,v|
|
36
|
+
raise "Invalid fundamental dimension #{k}" unless k.fundamental?
|
37
|
+
raise "Invalid exponent #{v}" unless v.kind_of?(Integer) # Can't this really be any Rational?
|
38
|
+
end
|
39
|
+
@exponents = Hash.new(0).merge(exponents)
|
40
|
+
@name = name.to_s
|
41
|
+
@symbol = symbol.nil? ? name.to_s.slice(0, 1).upcase : symbol.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def fundamental?
|
45
|
+
exponents.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
name rescue super
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_sym
|
53
|
+
symbol.to_sym
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'dimensional/unit'
|
2
|
+
require 'dimensional/metric'
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Dimensional
|
6
|
+
# A numeric-like class used to represent the measure of a physical quantity. An instance of this
|
7
|
+
# class represents both the unit and the numerical value of a measure. In turn, the (scale)
|
8
|
+
# unit implies a dimension of the measure. Instances of this class are immutable (value objects)
|
9
|
+
# Reference: http://en.wikipedia.org/wiki/Physical_quantity
|
10
|
+
class Measure < DelegateClass(Numeric)
|
11
|
+
# A Measure string is composed of a number followed by a unit separated by optional whitespace.
|
12
|
+
# A unit (optional) is composed of a non-digit character followed by zero or more word characters and terminated by some stuff.
|
13
|
+
# Scientific notation is not currently supported.
|
14
|
+
NUMERIC_REGEXP = /((?=\d|\.\d)\d*(?:\.\d*)?)\s*(\D\w*?)?(?=\b|\d|\W|$)/
|
15
|
+
|
16
|
+
# Parse a string into a Measure instance. The metric and system parameters may be keys for looking up the associated values.
|
17
|
+
# Unrecognized strings return nil.
|
18
|
+
def self.parse(str, metric, system = nil)
|
19
|
+
metric = Metric[metric] unless metric.kind_of?(Metric)
|
20
|
+
system = System[system] unless system.kind_of?(System)
|
21
|
+
raise "Metric not specified" unless metric
|
22
|
+
units = metric.units
|
23
|
+
elements = str.to_s.scan(NUMERIC_REGEXP).map do |(v, us)|
|
24
|
+
units = units.select{|u| system == u.system} if system
|
25
|
+
unit = us.nil? ? units.first : units.detect{|u| u.match(us.to_s)}
|
26
|
+
raise ArgumentError, "Unit cannot be determined (#{us})" unless unit
|
27
|
+
system = unit.system
|
28
|
+
value = unit.dimension.nil? ? v.to_i : v.to_f
|
29
|
+
new(value, unit, metric)
|
30
|
+
end
|
31
|
+
# Coalesce the elements into a single Measure instance in "expression base" units.
|
32
|
+
# The expression base is the first provided unit in an expression like "1 mile 200 feet"
|
33
|
+
elements.inject do |t, e|
|
34
|
+
converted_value = e.convert(t.unit)
|
35
|
+
new(t + converted_value, t.unit, metric)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :unit, :metric
|
40
|
+
|
41
|
+
def initialize(value, unit, metric = nil)
|
42
|
+
@unit = unit
|
43
|
+
metric = Metric[metric] if metric.kind_of?(Symbol)
|
44
|
+
@metric = metric || Metric[unit.dimension]
|
45
|
+
super(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convert this dimensional value to a different unit
|
49
|
+
def convert(new_unit)
|
50
|
+
new_value = self * unit.convert(new_unit)
|
51
|
+
self.class.new(new_value, new_unit, metric)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return a new dimensional value expressed in the base unit
|
55
|
+
# DEPRECATE: this method has dubious semantics for composed units as there may be no defined unit with
|
56
|
+
# a matching dimension vector.
|
57
|
+
def base
|
58
|
+
raise "Composed units cannot be converted to a base unit" if unit.reference_unit.kind_of?(Enumerable)
|
59
|
+
convert(unit.base)
|
60
|
+
end
|
61
|
+
|
62
|
+
def native
|
63
|
+
metric.dimension ? to_f : to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
strfmeasure(metric.preferences(unit)[:format]) rescue super
|
68
|
+
end
|
69
|
+
|
70
|
+
# Like Date, Time and DateTime, Measure represents both a value and a context. Like those built-in classes,
|
71
|
+
# Measure needs this output method to control the context. The format string is identical to that used by
|
72
|
+
# Kernel.sprintf with the addition of support for the U specifier:
|
73
|
+
# %U replace with unit. This specifier supports the '#' flag to use the unit's name instead of abbreviation
|
74
|
+
# In addition, this specifier supports the same width and precision modfiers as the '%s' specifier.
|
75
|
+
# For example: %#10.10U
|
76
|
+
# All other specifiers are applied to the numeric value of the measure.
|
77
|
+
# TODO: Support positional arguments (n$).
|
78
|
+
# TODO: Support modulo subordinate units with format hash -> {1 => "'", 12 => :inch} or {1 => "%d#", 16 => "%doz."}
|
79
|
+
def strfmeasure(format = nil, *args)
|
80
|
+
v = if precision = metric.preferences(unit)[:precision]
|
81
|
+
pfactor = 10**(-precision)
|
82
|
+
((self * pfactor).round / pfactor.to_f).to_s
|
83
|
+
else
|
84
|
+
native
|
85
|
+
end
|
86
|
+
format = format || unit.format
|
87
|
+
format.gsub!(/%(#)?([\d.\-\*]*)U/) do |s|
|
88
|
+
arg = ($1) ? unit.name : unit.abbreviation
|
89
|
+
Kernel.sprintf("%#{$2}s", arg)
|
90
|
+
end
|
91
|
+
Kernel.sprintf(format, v, *args)
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
"#{super} : #{unit.inspect}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'dimensional/dimension'
|
2
|
+
|
3
|
+
module Dimensional
|
4
|
+
# A specific physical entity that can be measured.
|
5
|
+
# TODO: Add a hierarchy that allows metrics to be grouped by domain, like shipping, carpentry or sports
|
6
|
+
class Metric
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
@registry = Hash.new
|
10
|
+
|
11
|
+
attr_reader :name, :dimension, :parent
|
12
|
+
|
13
|
+
def self.register(*args)
|
14
|
+
m = new(*args)
|
15
|
+
raise "Metric #{m} has already been registered." if @registry[m.name]
|
16
|
+
@registry[m.name && m.name.to_sym] = m
|
17
|
+
m
|
18
|
+
end
|
19
|
+
|
20
|
+
# Lookup the metric by key. The default metric for a dimension is keyed by the dimension's symbol
|
21
|
+
def self.[](sym)
|
22
|
+
@registry[sym && sym.to_sym]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.reset!
|
26
|
+
@registry.clear
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(name, dimension, parent = nil)
|
30
|
+
@name = name && name.to_s
|
31
|
+
@dimension = dimension
|
32
|
+
@units = Hash.new{|h,k| h[k] = {}}
|
33
|
+
@parent = parent
|
34
|
+
end
|
35
|
+
|
36
|
+
def prefer(unit, options = {})
|
37
|
+
raise "Unit #{unit} is not compatible with dimension #{dimension || '<nil>'}." unless unit.dimension == dimension
|
38
|
+
@units[unit] = options
|
39
|
+
end
|
40
|
+
|
41
|
+
def units
|
42
|
+
baseline = parent ? parent.units : @units.keys
|
43
|
+
baseline.sort{|a,b| (@units.has_key?(b) ? 1 : 0) <=> (@units.has_key?(a) ? 1 : 0)}
|
44
|
+
end
|
45
|
+
|
46
|
+
def preferences(u)
|
47
|
+
baseline = parent ? parent.preferences(u) : {}
|
48
|
+
baseline.merge(@units[u])
|
49
|
+
end
|
50
|
+
|
51
|
+
def each
|
52
|
+
units.each
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
name || super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Dimensional
|
2
|
+
# Represents a set of units for comprehensive measurement of physical quantities.
|
3
|
+
class System
|
4
|
+
@registry = Hash.new
|
5
|
+
@abbreviation_registry = Hash.new
|
6
|
+
|
7
|
+
def self.register(*args)
|
8
|
+
s = new(*args)
|
9
|
+
raise "System #{s.name} already exists" if @registry[s.name]
|
10
|
+
raise "System #{s.name}'s abbreviation already exists" if @abbreviation_registry[s.abbreviation]
|
11
|
+
@registry[s.name.to_sym] = s
|
12
|
+
@abbreviation_registry[s.abbreviation.to_sym] = s if s.abbreviation
|
13
|
+
const_set(s.abbreviation, s) rescue nil # Not all symbols strings are valid constant names
|
14
|
+
s
|
15
|
+
end
|
16
|
+
|
17
|
+
# Lookup the system by name or abbreviation
|
18
|
+
def self.[](sym)
|
19
|
+
return nil unless sym = sym && sym.to_sym
|
20
|
+
@abbreviation_registry[sym] || @registry[sym]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Systems are expected to be declared 'universally' and always be in context so we only dump the name
|
24
|
+
def self._load(str)
|
25
|
+
@registry[str.to_sym]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Purge all systems from storage.
|
29
|
+
def self.reset!
|
30
|
+
constants.each {|d| remove_const(d)}
|
31
|
+
@registry.clear
|
32
|
+
@abbreviation_registry.clear
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :name, :abbreviation
|
36
|
+
|
37
|
+
def initialize(name, abbreviation = nil)
|
38
|
+
@name = name.to_s.freeze
|
39
|
+
@abbreviation = abbreviation && abbreviation.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
# Systems are expected to be declared 'universally' and always be in context so we only dump the name
|
43
|
+
def _dump(depth)
|
44
|
+
name
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
name rescue super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|