boof-not-naughty 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +57 -0
- data/COPYING +18 -0
- data/README.rdoc +59 -0
- data/Rakefile +126 -0
- data/lib/core_extensions.rb +20 -0
- data/lib/not_naughty.rb +86 -0
- data/lib/not_naughty/class_methods.rb +78 -0
- data/lib/not_naughty/error_handler.rb +48 -0
- data/lib/not_naughty/instance_methods.rb +20 -0
- data/lib/not_naughty/validation.rb +132 -0
- data/lib/not_naughty/validations/acceptance_validation.rb +46 -0
- data/lib/not_naughty/validations/confirmation_validation.rb +49 -0
- data/lib/not_naughty/validations/format_validation.rb +71 -0
- data/lib/not_naughty/validations/length_validation.rb +91 -0
- data/lib/not_naughty/validations/numericality_validation.rb +43 -0
- data/lib/not_naughty/validations/presence_validation.rb +32 -0
- data/lib/not_naughty/validator.rb +125 -0
- data/lib/not_naughty/violation.rb +36 -0
- data/spec/error_handler_spec.rb +21 -0
- data/spec/not_naughty_spec.rb +82 -0
- data/spec/rcov.opts +4 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/validation_spec.rb +117 -0
- data/spec/validations_spec.rb +267 -0
- data/spec/validator_spec.rb +132 -0
- data/spec/violation_spec.rb +36 -0
- metadata +102 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
module Tree #:nodoc:
|
2
|
+
class TreeNode #:nodoc:
|
3
|
+
|
4
|
+
def closest(obj)
|
5
|
+
if @name == obj then self
|
6
|
+
elsif @name > obj
|
7
|
+
closest = self
|
8
|
+
@children.any? { |child| node = child.closest(obj) and closest = node }
|
9
|
+
|
10
|
+
closest
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module NotNaughty
|
18
|
+
class ErrorHandler
|
19
|
+
|
20
|
+
def initialize(handler = Kernel)
|
21
|
+
@handles = Tree::TreeNode.new Exception, proc { |e| handler.raise e }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calls closest handle with exception.
|
25
|
+
def raise(exception)
|
26
|
+
handle = @handles.closest exception.class
|
27
|
+
handle.content.call exception
|
28
|
+
end
|
29
|
+
|
30
|
+
# Inserts handle into the ordered tree.
|
31
|
+
def handle(exception_class, &block)
|
32
|
+
closest_handle = @handles.closest exception_class
|
33
|
+
|
34
|
+
if closest_handle == exception_class then closest_handle.content = block
|
35
|
+
else
|
36
|
+
new_handle = Tree::TreeNode.new exception_class, block
|
37
|
+
|
38
|
+
closest_handle.children do |child|
|
39
|
+
exception_class > child.name and
|
40
|
+
new_handle << closest_handle.remove!(child)
|
41
|
+
end
|
42
|
+
|
43
|
+
closest_handle << new_handle
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
# Returns instance of Errors.
|
5
|
+
def errors() @errors ||= ::NotNaughty::Violation.new end
|
6
|
+
|
7
|
+
# Returns true if all validations passed
|
8
|
+
def valid?
|
9
|
+
validate
|
10
|
+
errors.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Clears errors and validates.
|
14
|
+
def validate
|
15
|
+
errors.clear
|
16
|
+
self.class.validator.invoke self
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
|
3
|
+
# == The superclass for Validations.
|
4
|
+
#
|
5
|
+
# See new for more information.
|
6
|
+
class Validation
|
7
|
+
|
8
|
+
# Returns array of paths which are scanned for validations by load.
|
9
|
+
def self.load_paths
|
10
|
+
@load_paths
|
11
|
+
end
|
12
|
+
@load_paths = [File.join(%W[#{ File.dirname __FILE__ } validations])]
|
13
|
+
PATTERN = File.join %w[%s ** %s_validation.rb]
|
14
|
+
|
15
|
+
# Loads validations from load_paths.
|
16
|
+
def self.load(*validations)
|
17
|
+
validations.each do |validation|
|
18
|
+
@load_paths.each do |load_path|
|
19
|
+
pattern = PATTERN % [load_path, validation]
|
20
|
+
Dir[pattern].each { |validation_path| require validation_path }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
extend Observable
|
26
|
+
def self.inherited(descendant) #:nodoc:
|
27
|
+
changed and notify_observers(descendant)
|
28
|
+
|
29
|
+
descendant.
|
30
|
+
instance_variable_set :@observer_peers, @observer_peers.clone
|
31
|
+
end
|
32
|
+
|
33
|
+
# Builds validations.
|
34
|
+
#
|
35
|
+
# <b>Example:</b>
|
36
|
+
# NotNaughty::Validation.new :temp, :if => :water? do |obj, attr, val|
|
37
|
+
# obj.errors.add attr, 'too hot' unless val < 100
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# <b>Like:</b>
|
41
|
+
# class TempValidation < NotNaughty::Validation
|
42
|
+
# def initialize(opts, attributes)
|
43
|
+
# super opts, attributes method(:temp_validation)
|
44
|
+
# end
|
45
|
+
# def temp_validation(obj, attr, val)
|
46
|
+
# obj.errors.add attr, 'too hot' unless val < 100
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Validation.new TempValidation, :temp, :if => :water?
|
51
|
+
#
|
52
|
+
# The last one also notifies all Observers of Validation (see
|
53
|
+
# Builder#update). If Builder#update is called because <Name>Validation
|
54
|
+
# is inherited from Validation the ValidationBuilder gets the method
|
55
|
+
# validates_<name>_of and so does the classes that included the Builder.
|
56
|
+
def self.new(*params, &block)
|
57
|
+
attributes = if params.first.is_a? Class and params.first < self
|
58
|
+
klass = params.shift
|
59
|
+
klass.new(*params, &block)
|
60
|
+
else
|
61
|
+
options = params.extract_options!
|
62
|
+
instance = allocate
|
63
|
+
instance.send :initialize, options, params.map {|p|p.to_sym}, &block
|
64
|
+
instance
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :attributes
|
69
|
+
|
70
|
+
def initialize(opts, attributes, &block) #:nodoc:
|
71
|
+
build_conditions opts[:if], opts[:unless]
|
72
|
+
@attributes, @block, @opts = attributes, block, opts
|
73
|
+
end
|
74
|
+
|
75
|
+
def call_without_conditions(obj, attr, value) #:nodoc:
|
76
|
+
@block.call obj, attr, value
|
77
|
+
end
|
78
|
+
alias_method :call, :call_without_conditions
|
79
|
+
|
80
|
+
def call_with_conditions(obj, attr, value) #:nodoc:
|
81
|
+
if @conditions.all? { |c| c.evaluate obj }
|
82
|
+
call_without_conditions obj, attr, value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
def build_conditions(p, n) #:nodoc:
|
88
|
+
@conditions = []
|
89
|
+
[p].flatten.each {|c| @conditions << Condition.new(c) if c }
|
90
|
+
[n].flatten.each {|c| @conditions << Condition.new(c, false) if c }
|
91
|
+
|
92
|
+
(class << self; self; end).module_eval do
|
93
|
+
alias_method :call, :call_with_conditions
|
94
|
+
end unless @conditions.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
# Conditions for use in Validations are usually used with Validations.
|
98
|
+
class Condition
|
99
|
+
|
100
|
+
# An instance of Condition accepts Symbols, UnboundMethods or anything
|
101
|
+
# that responds to :call.
|
102
|
+
#
|
103
|
+
# The following examples are similiar to each other:
|
104
|
+
#
|
105
|
+
# NotNaughty::Validation::Condition.new proc {|o| o.nil?}
|
106
|
+
# NotNaughty::Validation::Condition.new :nil?
|
107
|
+
# NotNaughty::Validation::Condition.new Object.instance_method(:nil?)
|
108
|
+
def self.new(condition, positive = true)
|
109
|
+
instance = allocate
|
110
|
+
instance.instance_variable_set :@condition, condition
|
111
|
+
|
112
|
+
block = case condition
|
113
|
+
when Symbol then positive ?
|
114
|
+
proc { |o| o.send @condition } :
|
115
|
+
proc { |o| not o.send @condition }
|
116
|
+
when UnboundMethod then positive ?
|
117
|
+
proc { |o| @condition.bind(o).call } :
|
118
|
+
proc { |o| not @condition.bind(o).call }
|
119
|
+
else positive ?
|
120
|
+
proc { |o| @condition.call o } :
|
121
|
+
proc { |o| not @condition.call o }
|
122
|
+
end
|
123
|
+
|
124
|
+
(class << instance; self; end).
|
125
|
+
module_eval { define_method(:evaluate, &block) }
|
126
|
+
|
127
|
+
instance
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
|
3
|
+
# == Validates acceptance of obj's attribute against a fixed value via <tt>:eql?</tt> call.
|
4
|
+
#
|
5
|
+
# Unless the validation succeeds an error hash (:attribute => :message)
|
6
|
+
# is added to the obj's instance of Errors.
|
7
|
+
#
|
8
|
+
# <b>Options:</b>
|
9
|
+
# <tt>:accept</tt>:: object to check against (via :eql?)
|
10
|
+
# <tt>:message</tt>:: see NotNaughty::Errors for details
|
11
|
+
# <tt>:if</tt>:: see NotNaughty::Validation::Condition for details
|
12
|
+
# <tt>:unless</tt>:: see NotNaughty::Validation::Condition for details
|
13
|
+
#
|
14
|
+
# <b>Example:</b>
|
15
|
+
#
|
16
|
+
# invalid = 'abc'
|
17
|
+
# valid = '1'
|
18
|
+
# def invalid.errors() @errors ||= NotNauthy::Errors.new end
|
19
|
+
# def valid.errors() @errors ||= NotNauthy::Errors.new end
|
20
|
+
#
|
21
|
+
# AcceptanceValidation.new({}, :to_s).call invalid, :to_s, 'abc'
|
22
|
+
# invalid.errors.on(:to_s).any? # => true
|
23
|
+
#
|
24
|
+
# AcceptanceValidation.new({}, :to_s).call valid, :to_s, '1'
|
25
|
+
# valid.errors.on(:to_s).any? # => false
|
26
|
+
class AcceptanceValidation < Validation
|
27
|
+
|
28
|
+
def initialize(opts, attributes) #:nodoc:
|
29
|
+
__message, __accept =
|
30
|
+
opts[:message] || '#{"%s".humanize} not accepted.',
|
31
|
+
opts[:accept] || '1'
|
32
|
+
|
33
|
+
if opts[:allow_blank] or opts.fetch(:allow_nil, true)
|
34
|
+
__allow = if opts[:allow_blank] then :blank? else :nil? end
|
35
|
+
super opts, attributes do |o, a, v|
|
36
|
+
o.errors.add a, __message unless v.send __allow or __accept.eql? v
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super opts, attributes do |o, a, v|
|
40
|
+
o.errors.add a, __message unless __accept.eql? v
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
|
3
|
+
# == Validates confirmaton of obj's attribute via <tt>:eql?</tt> call against the _appropiate_ confirmation attribute.
|
4
|
+
#
|
5
|
+
# Unless the validation succeeds an error hash (:attribute => :message)
|
6
|
+
# is added to the obj's instance of Errors.
|
7
|
+
#
|
8
|
+
# <b>Options:</b>
|
9
|
+
# <tt>:message</tt>:: see NotNaughty::Errors for details
|
10
|
+
# <tt>:if</tt>:: see NotNaughty::Validation::Condition for details
|
11
|
+
# <tt>:unless</tt>:: see NotNaughty::Validation::Condition for details
|
12
|
+
#
|
13
|
+
# <b>Example:</b>
|
14
|
+
#
|
15
|
+
# obj = 'abc'
|
16
|
+
# def obj.errors() @errors ||= NotNauthy::Errors.new end
|
17
|
+
# def obj.to_s_confirmation() '123 end
|
18
|
+
#
|
19
|
+
# ConfirmationValidation.new({}, :to_s).call obj, :to_s, 'abc'
|
20
|
+
# obj.errors.on(:to_s).any? # => true
|
21
|
+
class ConfirmationValidation < Validation
|
22
|
+
|
23
|
+
def initialize(valid, attributes) #:nodoc:
|
24
|
+
valid[:message] ||= '%s could not be confirmed.'
|
25
|
+
|
26
|
+
if valid[:allow_blank] || valid[:allow_nil]
|
27
|
+
valid[:allow] = valid[:allow_blank] ? :blank? : :nil?
|
28
|
+
super valid, attributes, &confirmation_block_with_exception(valid)
|
29
|
+
else
|
30
|
+
super valid, attributes, &confirmation_block(valid)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
def confirmation_block_with_exception(valid)
|
36
|
+
proc do |obj, attr, value|
|
37
|
+
value.send valid[:allow] or
|
38
|
+
obj.send(:"#{ attr }_confirmation") == value or
|
39
|
+
obj.errors.add attr, valid[:message]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
def confirmation_block(valid)
|
43
|
+
proc do |obj, attr, value|
|
44
|
+
obj.send(:"#{ attr }_confirmation") == value or
|
45
|
+
obj.errors.add attr, valid[:message]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
|
3
|
+
# == Validates format of obj's attribute via the <tt>:match</tt> method.
|
4
|
+
#
|
5
|
+
# Unless the validation succeeds an error hash (:attribute => :message)
|
6
|
+
# is added to the obj's instance of Errors.
|
7
|
+
#
|
8
|
+
# <b>Options:</b>
|
9
|
+
# <tt>:with</tt>:: object that checks via a <tt>:match</tt> call
|
10
|
+
# <tt>:message</tt>:: see NotNaughty::Errors for details
|
11
|
+
# <tt>:if</tt>:: see NotNaughty::Validation::Condition for details
|
12
|
+
# <tt>:unless</tt>:: see NotNaughty::Validation::Condition for details
|
13
|
+
#
|
14
|
+
# <b>Example:</b>
|
15
|
+
#
|
16
|
+
# obj = 'abc'
|
17
|
+
# def obj.errors() @errors ||= NotNauthy::Errors.new end
|
18
|
+
#
|
19
|
+
# FormatValidation.new({:with => /[a-z]/}, :to_s).call obj, :to_s, obj
|
20
|
+
# obj.errors.on(:to_s).any? # => false
|
21
|
+
#
|
22
|
+
# FormatValidation.new({:with => /[A-Z]/}, :to_s).call obj, :to_s, obj
|
23
|
+
# obj.errors.on(:to_s) # => ["Format of to_s does not match."]
|
24
|
+
#
|
25
|
+
# <b>Use predefined format expressions:</b>
|
26
|
+
#
|
27
|
+
# obj = 'Valid Address <foo@bar.baz>'
|
28
|
+
# def obj.errors() @errors ||= NotNauthy::Errors.new end
|
29
|
+
#
|
30
|
+
# FormatValidation.new({:with => :email}, :to_s).call obj, :to_s, obj
|
31
|
+
# obj.errors.on(:to_s).any? # => false
|
32
|
+
class FormatValidation < Validation
|
33
|
+
|
34
|
+
# Predefined matchers.
|
35
|
+
PREDEFINED = {
|
36
|
+
:email => /[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/,
|
37
|
+
:ip => /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/
|
38
|
+
}
|
39
|
+
|
40
|
+
def initialize(valid, attributes) #:nodoc:
|
41
|
+
valid[:with] = PREDEFINED.fetch valid[:with] if valid[:with].is_a? Symbol
|
42
|
+
valid[:with].respond_to? :match or
|
43
|
+
raise ArgumentError, "#{ valid[:with].inspect } doesn't :match"
|
44
|
+
|
45
|
+
valid[:message] ||= '%s does not match format.'
|
46
|
+
|
47
|
+
if valid[:allow_blank] || valid[:allow_nil]
|
48
|
+
valid[:allow] = valid[:allow_blank] ? :blank? : :nil?
|
49
|
+
super valid, attributes, &matching_block_with_exception(valid)
|
50
|
+
else
|
51
|
+
super valid, attributes, &matching_block(valid)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
def matching_block_with_exception(valid)
|
57
|
+
proc do |obj, attr, value|
|
58
|
+
value.send valid[:allow] or
|
59
|
+
valid[:with].match value or
|
60
|
+
obj.errors.add attr, valid[:message]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
def matching_block(valid)
|
64
|
+
proc do |obj, attr, value|
|
65
|
+
valid[:with].match value or
|
66
|
+
obj.errors.add attr, valid[:message]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module NotNaughty
|
2
|
+
|
3
|
+
# == Validates length of obj's attribute via the <tt>:length</tt> method.
|
4
|
+
#
|
5
|
+
# Unless the validation succeeds an error hash (:attribute => :message)
|
6
|
+
# is added to the obj's instance of Errors.
|
7
|
+
#
|
8
|
+
# <b>Options:</b>
|
9
|
+
# <tt>:message</tt>:: see NotNaughty::Errors for details
|
10
|
+
# <tt>:if</tt>:: see NotNaughty::Validation::Condition for details
|
11
|
+
# <tt>:unless</tt>:: see NotNaughty::Validation::Condition for details
|
12
|
+
#
|
13
|
+
# <b>Boundaries (by precendence):</b>
|
14
|
+
# <tt>:is</tt>:: valid length
|
15
|
+
# <tt>:within</tt>:: valid range of length
|
16
|
+
# <tt>:minimum</tt>:: maximum length
|
17
|
+
# <tt>:maximum</tt>:: minimum length
|
18
|
+
#
|
19
|
+
# If both, <tt>:minimum</tt> and <tt>:maximum</tt> are provided they're
|
20
|
+
# converted to :within. Each boundary type has its own default message:
|
21
|
+
# precise:: "Length of %s is not equal to #{__length}."
|
22
|
+
# range:: "Length of %s is not within #{__range.first} and #{__range.last}."
|
23
|
+
# lower:: "Length of %s is smaller than #{__boundary}."
|
24
|
+
# upper:: "Length of %s is greater than #{__boundary}."
|
25
|
+
#
|
26
|
+
# <b>Example:</b>
|
27
|
+
#
|
28
|
+
# obj = %w[a sentence with five words] #
|
29
|
+
# def obj.errors() @errors ||= NotNauthy::Errors.new end
|
30
|
+
#
|
31
|
+
# LengthValidation.new({:minimum => 4}, :to_a).
|
32
|
+
# call obj, :to_a, %w[a sentence with five words]
|
33
|
+
# obj.errors.on(:to_s).any? # => false
|
34
|
+
#
|
35
|
+
# LengthValidation.new({:within => 1..4}, :to_a).
|
36
|
+
# call obj, :to_a, %w[a sentence with five words]
|
37
|
+
# obj.errors.on(:to_s).any? # => true
|
38
|
+
#
|
39
|
+
# I'm sorry for using eval here...
|
40
|
+
class LengthValidation < Validation
|
41
|
+
|
42
|
+
TEMPLATE = <<-BLOCK
|
43
|
+
proc do |obj, attr, value|
|
44
|
+
value.%s or %s value.length or
|
45
|
+
obj.errors.add attr, %s
|
46
|
+
end
|
47
|
+
BLOCK
|
48
|
+
|
49
|
+
def initialize(valid, attributes) #:nodoc:
|
50
|
+
super valid, attributes, &build_block(valid)
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def build_block(valid) #:nodoc:
|
55
|
+
if valid[:is] then is_block valid
|
56
|
+
elsif valid[:within] or valid[:minimum] && valid[:maximum] then within_block valid
|
57
|
+
elsif valid[:minimum] then minimum_block valid
|
58
|
+
elsif valid[:maximum] then maximum_block valid
|
59
|
+
else
|
60
|
+
raise ArgumentError, 'no boundary given'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_block(valid)
|
65
|
+
valid[:message] ||= "Length of %s is not equal to #{ valid[:is] }."
|
66
|
+
|
67
|
+
eval TEMPLATE % [ valid[:allow_blank] ? :blank? : :nil?,
|
68
|
+
"#{ valid[:is].inspect } ==", valid[:message].inspect ]
|
69
|
+
end
|
70
|
+
def within_block(valid)
|
71
|
+
valid[:within] ||= Range.new valid[:minimum], valid[:maximum]
|
72
|
+
valid[:message] ||= "Length of %s is not within #{ valid[:within].min } and #{ valid[:within].max }."
|
73
|
+
|
74
|
+
eval TEMPLATE % [ valid[:allow_blank] ? :blank? : :nil?,
|
75
|
+
"(#{ valid[:within].inspect }).include?", valid[:message].inspect ]
|
76
|
+
end
|
77
|
+
def minimum_block(valid)
|
78
|
+
valid[:message] ||= "Length of %s is less than #{ valid[:minimum] }."
|
79
|
+
|
80
|
+
eval TEMPLATE % [ valid[:allow_blank] ? :blank? : :nil?,
|
81
|
+
"#{ valid[:minimum].inspect } <=", valid[:message].inspect ]
|
82
|
+
end
|
83
|
+
def maximum_block(valid)
|
84
|
+
valid[:message] ||= "Length of %s is greater than #{ valid[:maximum] }."
|
85
|
+
|
86
|
+
eval TEMPLATE % [ valid[:allow_blank] ? :blank? : :nil?,
|
87
|
+
"#{ valid[:maximum].inspect } >=", valid[:message].inspect ]
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|