gorilla 0.0.1.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -0
- data/Rakefile +13 -0
- data/lib/gorilla.rb +11 -0
- data/lib/gorilla/all.rb +5 -0
- data/lib/gorilla/core_ext.rb +72 -0
- data/lib/gorilla/scanner.rb +222 -0
- data/lib/gorilla/scanners.rb +9 -0
- data/lib/gorilla/scanners/omni_scanner.rb +21 -0
- data/lib/gorilla/scanners/temperature_scanner.rb +11 -0
- data/lib/gorilla/scanners/time_scanner.rb +34 -0
- data/lib/gorilla/scanners/volume_scanner.rb +15 -0
- data/lib/gorilla/scanners/weight_scanner.rb +10 -0
- data/lib/gorilla/scantron_ext.rb +83 -0
- data/lib/gorilla/temperature.rb +19 -0
- data/lib/gorilla/time.rb +51 -0
- data/lib/gorilla/unit.rb +453 -0
- data/lib/gorilla/version.rb +10 -0
- data/lib/gorilla/volume.rb +13 -0
- data/lib/gorilla/weight.rb +8 -0
- data/test/gorilla/scanners/temperature_scanner_test.rb +42 -0
- data/test/gorilla/scanners/time_scanner_test.rb +123 -0
- data/test/gorilla/scanners/volume_scanner_test.rb +148 -0
- data/test/gorilla/scanners/weight_scanner_test.rb +57 -0
- data/test/gorilla/time_test.rb +14 -0
- data/test/test_helper.rb +10 -0
- metadata +122 -0
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rdoctest/task'
|
3
|
+
|
4
|
+
Rdoctest::Task.new do |t|
|
5
|
+
t.ruby_opts << '-rgorilla/all -rgorilla/core_ext'
|
6
|
+
end
|
7
|
+
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.libs << 'test'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => [:doctest, :test]
|
data/lib/gorilla.rb
ADDED
data/lib/gorilla/all.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Gorilla::CoreExt
|
2
|
+
def unit
|
3
|
+
Gorilla::Unit.new self
|
4
|
+
end
|
5
|
+
alias units unit
|
6
|
+
|
7
|
+
Gorilla.units.each_pair do |klass_name, configs|
|
8
|
+
configs.each_key do |unit|
|
9
|
+
klass = Gorilla.const_get klass_name[/\w+$/]
|
10
|
+
|
11
|
+
define_method unit do
|
12
|
+
klass.new self, unit
|
13
|
+
end
|
14
|
+
alias_method "#{unit}s", unit if klass.pluralize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
if Gorilla.const_defined? :Temperature
|
19
|
+
alias Celsius celsius
|
20
|
+
alias C celsius
|
21
|
+
alias Fahrenheit fahrenheit
|
22
|
+
alias F fahrenheit
|
23
|
+
end
|
24
|
+
|
25
|
+
if Gorilla.const_defined? :Temperature
|
26
|
+
alias s second
|
27
|
+
alias sec second
|
28
|
+
alias ms millisecond
|
29
|
+
alias min minute
|
30
|
+
alias h hour
|
31
|
+
alias hr hour
|
32
|
+
alias d day
|
33
|
+
end
|
34
|
+
|
35
|
+
if Gorilla.const_defined? :Volume
|
36
|
+
alias litre liter
|
37
|
+
alias l liter
|
38
|
+
alias L liter
|
39
|
+
alias millilitre milliliter
|
40
|
+
alias ml milliliter
|
41
|
+
alias mL milliliter
|
42
|
+
alias centilitre centiliter
|
43
|
+
alias cl centiliter
|
44
|
+
alias cL centiliter
|
45
|
+
alias t teaspoon
|
46
|
+
alias tsp teaspoon
|
47
|
+
alias T tablespoon
|
48
|
+
alias tbs tablespoon
|
49
|
+
alias tbsp tablespoon
|
50
|
+
alias fl_oz fluid_ounce
|
51
|
+
alias oz_fl fluid_ounce
|
52
|
+
alias c cup
|
53
|
+
alias cu cup
|
54
|
+
alias pt pint
|
55
|
+
alias qt quart
|
56
|
+
alias gal gallon
|
57
|
+
end
|
58
|
+
|
59
|
+
if Gorilla.const_defined? :Weight
|
60
|
+
alias g gram
|
61
|
+
alias kg kilogram
|
62
|
+
alias mg milligram
|
63
|
+
alias lb pound
|
64
|
+
alias lbs pounds
|
65
|
+
alias oz ounce
|
66
|
+
alias ozs ounces
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Numeric
|
71
|
+
include Gorilla::CoreExt
|
72
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'gorilla/scantron_ext'
|
3
|
+
|
4
|
+
module Gorilla
|
5
|
+
# A {Scantron}[http://github.com/stephencelis/scantron] scanner class from
|
6
|
+
# which all Gorilla::Scanner classes inherit.
|
7
|
+
#
|
8
|
+
# Your own Gorilla::Scanners will inherit the data assigned their
|
9
|
+
# Gorilla::Unit definitions so that, for example, units defined as metric
|
10
|
+
# will have additional scanner rules created for each prefix.
|
11
|
+
#
|
12
|
+
# For more information, see Gorilla::Scanner.rule.
|
13
|
+
class Scanner < ::Scantron::Scanner
|
14
|
+
# Maps metric prefixes to regular expressions used for parsing.
|
15
|
+
METRIC_MAP = {
|
16
|
+
# :yotta => /Y/,
|
17
|
+
# :zetta => /Z/,
|
18
|
+
# :exa => /E/,
|
19
|
+
# :peta => /P/,
|
20
|
+
# :tera => /T/,
|
21
|
+
# :giga => /G/,
|
22
|
+
# :mega => /M/,
|
23
|
+
:kilo => /k(?:ilo)?/,
|
24
|
+
# :hecto => /H/,
|
25
|
+
# :deca => /da/,
|
26
|
+
# :deci => /d/,
|
27
|
+
:centi => /c(?:enti)?/,
|
28
|
+
:milli => /m(?:illi)?/
|
29
|
+
# :micro => /μ/,
|
30
|
+
# :nano => /n/,
|
31
|
+
# :pico => /p/,
|
32
|
+
# :femto => /f/,
|
33
|
+
# :atto => /a/,
|
34
|
+
# :zepto => /z/,
|
35
|
+
# :yocto => /y/
|
36
|
+
}
|
37
|
+
|
38
|
+
before_match do |r|
|
39
|
+
# Adjust for amounts before units.
|
40
|
+
pre_match = r.scanner.pre_match
|
41
|
+
if result = AmountScanner.new(pre_match).perform.last
|
42
|
+
between = r.scanner.string[
|
43
|
+
result.scanner.pos, pre_match.length - result.scanner.pos
|
44
|
+
]
|
45
|
+
|
46
|
+
if between =~ /\S/
|
47
|
+
result = nil
|
48
|
+
else
|
49
|
+
r.length = r.length + (r.offset - result.offset)
|
50
|
+
r.offset = result.offset
|
51
|
+
end
|
52
|
+
|
53
|
+
amount = result.value if result
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adjust for generic amounts.
|
57
|
+
unless amount
|
58
|
+
if match = pre_match =~ /\ban? *$/i
|
59
|
+
r.length = r.length + (r.offset - match)
|
60
|
+
r.offset = match
|
61
|
+
amount = 1
|
62
|
+
elsif match = pre_match =~ /\b(?:a )couple *$/i
|
63
|
+
r.length = r.length + (r.offset - match)
|
64
|
+
r.offset = match
|
65
|
+
amount = 2
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adjust for trailing amounts ("...and a half").
|
70
|
+
if match = r.scanner.post_match.match(/^ and(?: an?)? (.+)/)
|
71
|
+
if r.scantron.class.parse(match[1]).nil?
|
72
|
+
if result = NumberScanner.new(match[1]).perform.first
|
73
|
+
if result.offset == 0
|
74
|
+
r.length = r.length + result.length + match.offset(1).first
|
75
|
+
amount += result.value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# Adjust for trailing ranges ("...or two").
|
80
|
+
elsif match = r.scanner.post_match.match(/^ or (.+)/)
|
81
|
+
if r.scantron.class.parse(match[1]).nil?
|
82
|
+
if result = NumberScanner.new(match[1]).perform.first
|
83
|
+
if result.offset == 0
|
84
|
+
r.length = r.length + result.length + match.offset(1).first
|
85
|
+
amount = amount..result.value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
# Adjust for periods.
|
90
|
+
elsif r.scanner.post_match =~ /^\./
|
91
|
+
r.length += 1
|
92
|
+
end
|
93
|
+
|
94
|
+
r[:amount] = amount
|
95
|
+
r
|
96
|
+
end
|
97
|
+
|
98
|
+
after_match do |r|
|
99
|
+
case amount = r[:amount]
|
100
|
+
when Range
|
101
|
+
unit_class = constantize r.rule.data[:class_name]
|
102
|
+
unit_class.new(amount.min, r.name)..unit_class.new(amount.max, r.name)
|
103
|
+
else
|
104
|
+
constantize(r.rule.data[:class_name]).new amount, r.name
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class << self
|
109
|
+
# ==== Options
|
110
|
+
#
|
111
|
+
# [<tt>:class_name</tt>] The Gorilla::Unit return class for the rule
|
112
|
+
# given. By default, it is inferred from the name
|
113
|
+
# of the scanner, so that a "BogosityScanner"
|
114
|
+
# would try instantiate matches as "Bogosity"
|
115
|
+
# units.
|
116
|
+
#
|
117
|
+
# [<tt>:metric</tt>] Whether or not additional rules should be
|
118
|
+
# generated for metric prefixes. By default, it
|
119
|
+
# is inferred from the unit's original
|
120
|
+
# definition.
|
121
|
+
#
|
122
|
+
# ==== Example
|
123
|
+
#
|
124
|
+
# Here we define a metric unit and scanner rule:
|
125
|
+
#
|
126
|
+
# class Beauty < Gorilla::Unit
|
127
|
+
# base :Helen, :metric => true
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# And here we define the scanner rule:
|
131
|
+
#
|
132
|
+
# class BeautyScanner < Gorilla::Scanner
|
133
|
+
# rule :Helen, /[Hh](?:elen)?s?/
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# BeautyScanner.scan '1 milliHelen is required to launch the ship.'
|
137
|
+
# # => [(1 milliHelen)]
|
138
|
+
#
|
139
|
+
# BeautyScanner.scan '2 kiloHelens'
|
140
|
+
# # => [(2 kiloHelens)]
|
141
|
+
#
|
142
|
+
# The return class (Beauty) is inferred from the scanner's class name
|
143
|
+
# (less "Scanner"), and the metric setting is taken from the matching
|
144
|
+
# rule on that class, but both can be overridden or made explicit.
|
145
|
+
#
|
146
|
+
# class BeautyScanner < Gorilla::Scanner
|
147
|
+
# rule :Helen, /[Hh](?:elen)?s?/, :class_name => 'Beauty',
|
148
|
+
# :metric => true
|
149
|
+
# end
|
150
|
+
def rule unit, regexp, data = {}, &block
|
151
|
+
if class_name = data.delete(:class_name)
|
152
|
+
klass = constantize class_name
|
153
|
+
elsif class_name = name.sub!(/Scanner$/, '')
|
154
|
+
klass = constantize class_name rescue nil
|
155
|
+
end
|
156
|
+
|
157
|
+
config = { :class_name => class_name || 'Unit' }
|
158
|
+
config.update klass.rules[unit] if klass && klass.rules[unit]
|
159
|
+
config.update data
|
160
|
+
|
161
|
+
super unit, /(?<=^|[\d ])#{regexp}(?=[\d ]|\b|$)/, config, &block
|
162
|
+
|
163
|
+
if config[:metric]
|
164
|
+
METRIC_MAP.each_pair do |pre, sub|
|
165
|
+
super :"#{pre}#{unit}", /(?<=^|[\d ])#{sub}#{regexp}(?= |\b|$)/,
|
166
|
+
config, &block
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def constantize class_name
|
174
|
+
names = class_name.split '::'
|
175
|
+
names.shift if names.first && names.first.empty?
|
176
|
+
|
177
|
+
constant = Object
|
178
|
+
names.each do |name|
|
179
|
+
constant = if constant.const_defined?(name)
|
180
|
+
constant.const_get name
|
181
|
+
else
|
182
|
+
constant.const_missing name
|
183
|
+
end
|
184
|
+
end
|
185
|
+
constant
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def scan
|
190
|
+
results = perform
|
191
|
+
array = []
|
192
|
+
range = false
|
193
|
+
|
194
|
+
results.each.with_index do |result, index|
|
195
|
+
if range
|
196
|
+
range = false
|
197
|
+
next
|
198
|
+
end
|
199
|
+
|
200
|
+
if !result.value.is_a?(Range) and next_result = results[index + 1]
|
201
|
+
substring = string[
|
202
|
+
result.scanner.pos, next_result.offset - result.scanner.pos
|
203
|
+
]
|
204
|
+
|
205
|
+
case substring
|
206
|
+
when /^ *(and|or|to) *$/
|
207
|
+
if $1 == 'and' && result.pre_match !~ /between $/
|
208
|
+
array << (result.value + next_result.value)
|
209
|
+
else
|
210
|
+
array << (result.value..next_result.value)
|
211
|
+
end
|
212
|
+
range = true and next
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
array << result.value
|
217
|
+
end
|
218
|
+
|
219
|
+
array
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'scantron'
|
2
|
+
require 'number_scanner'
|
3
|
+
require 'range_scanner'
|
4
|
+
require 'gorilla/scanner'
|
5
|
+
require 'gorilla/scanners/temperature_scanner'
|
6
|
+
require 'gorilla/scanners/time_scanner'
|
7
|
+
require 'gorilla/scanners/volume_scanner'
|
8
|
+
require 'gorilla/scanners/weight_scanner'
|
9
|
+
require 'gorilla/scanners/omni_scanner'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'gorilla/scanner'
|
3
|
+
require 'gorilla/scanners/temperature_scanner'
|
4
|
+
require 'gorilla/scanners/time_scanner'
|
5
|
+
require 'gorilla/scanners/volume_scanner'
|
6
|
+
require 'gorilla/scanners/weight_scanner'
|
7
|
+
|
8
|
+
module Gorilla
|
9
|
+
# An all-purpose units scanner combining the rules of TemperatureScanner,
|
10
|
+
# TimeScanner, VolumeScanner, and WeightScanner.
|
11
|
+
#
|
12
|
+
# Gorilla::OmniScanner.
|
13
|
+
# scan "Add 1 cup flour (125g). Bake @ 350F for 25 min."
|
14
|
+
# # => [(1 cup), (125 grams), (350° Fahrenheit), (25 minutes)]
|
15
|
+
class OmniScanner < Scanner
|
16
|
+
rules.update TemperatureScanner.rules
|
17
|
+
rules.update TimeScanner.rules
|
18
|
+
rules.update VolumeScanner.rules
|
19
|
+
rules.update WeightScanner.rules
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'gorilla/scanner'
|
3
|
+
require 'gorilla/temperature'
|
4
|
+
|
5
|
+
module Gorilla
|
6
|
+
class TemperatureScanner < Scanner
|
7
|
+
degrees = /°| ?deg(?:ree)?s? /
|
8
|
+
rule :celsius, /#{degrees}?(?:C|[Cc]elsius)|℃/
|
9
|
+
rule :fahrenheit, /#{degrees}?(?:F|[Ff]ahrenheit)|℉/
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'gorilla/scanner'
|
2
|
+
require 'gorilla/time'
|
3
|
+
|
4
|
+
module Gorilla
|
5
|
+
class TimeScanner < Scanner
|
6
|
+
rule :second, /[Ss](?:ec(?:ond)?s?)?|S(?:EC(?:OND)?S?)/
|
7
|
+
rule :minute, /[Mm](?:in(?:ute)?s?)?|M(?:IN(?:UTE)?S?)/
|
8
|
+
rule :hour, /[Hh](?:(?:ou)?rs?)?|H(?:(?:OU)?RS?)?/
|
9
|
+
rule :day, /[Dd](?:ays?)?|D(?:AYS?)/
|
10
|
+
rule :week, /[Ww](?:ee)?ks?|W(?:EE)?KS?/
|
11
|
+
rule :month, /[Mm](?:o(?:n(?:th))?s?)|M(?:O(?:N(?:TH))?S?)/
|
12
|
+
rule :year, /[Yy](?:ea)?rs?|Y(?:EA)?RS?/
|
13
|
+
rule :decade, /[Dd]ecades?|DECADES?/
|
14
|
+
rule :century, /[Cc]entur(?:y|ies)|CENTUR(?:Y|IES)/
|
15
|
+
rule :millennium, /[Mm]illeni(?:um|a)|MILLENI(?:UM|A)/
|
16
|
+
|
17
|
+
rule :iso8601, /\bP(\d+Y)?(\d+W)?(\d+D)?T?(\d+H)?(\d+M)?([\d.]+S)?\b/ do |r|
|
18
|
+
y = Time.new r.scanner[1].to_i, :year
|
19
|
+
w = Time.new r.scanner[2].to_i, :week
|
20
|
+
d = Time.new r.scanner[3].to_i, :day
|
21
|
+
h = Time.new r.scanner[4].to_i, :hour
|
22
|
+
m = Time.new r.scanner[5].to_i, :minute
|
23
|
+
s = Time.new r.scanner[6].to_f, :second
|
24
|
+
y + w + d + h + m + s
|
25
|
+
end
|
26
|
+
|
27
|
+
rule :delimited, /\d{1,2}(?::\d{2}){1,2}/ do |r|
|
28
|
+
h, m, s = r.to_s.split ':'
|
29
|
+
time = Time.new(h, :hour) + Time.new(m, :minute)
|
30
|
+
time += Time.new(s, :second) if s
|
31
|
+
time
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'gorilla/scanner'
|
2
|
+
require 'gorilla/volume'
|
3
|
+
|
4
|
+
module Gorilla
|
5
|
+
class VolumeScanner < Scanner
|
6
|
+
rule :liter, /[Ll](?:i?t(?:er|re?)|t|r)?s?/
|
7
|
+
rule :teaspoon, /(?:t|(?:[Tt](?:ea)?s(?:p(?:oo)?n?)?))s?/
|
8
|
+
rule :tablespoon, /(?:T|(?:[Tt](?:bl?s?p?|a?b(?:le?)?(?:s(?:p(?:oo)?n?)))))s?/
|
9
|
+
rule :fluid_ounce, /(?:[Ff]l(?:uid|\.)? )?[Oo](?:unce|z)s?/
|
10
|
+
rule :cup, /[Cc]u?p?s?/
|
11
|
+
rule :pint, /[Pp](?:(?:i?n)?t)?s?/
|
12
|
+
rule :quart, /[Qq](?:(?:ua)?r)?ts?/
|
13
|
+
rule :gallon, /[Gg](?:a?l(?:(?:lo)?n)?)s?/
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'scantron'
|
2
|
+
require 'amount_scanner'
|
3
|
+
|
4
|
+
class NumberScanner
|
5
|
+
words = WORD_MAP.keys.map { |v| v.sub /y$/, 'y-?' } * '|'
|
6
|
+
rules[:human].regexp = \
|
7
|
+
%r{(?:\b(?:\d+ (?:an?d? )*)?(?:#{words}))(?: ?\b(?:#{words}|an?d?|\d+)\b ?)*}i
|
8
|
+
|
9
|
+
def self.human_to_number words
|
10
|
+
numbers = words.split(/\W+/).map { |w|
|
11
|
+
WORD_MAP[w.downcase] || parse(w) || w
|
12
|
+
}
|
13
|
+
|
14
|
+
case numbers.count { |n| n.is_a? Numeric }
|
15
|
+
when 0 then false
|
16
|
+
when 1 then numbers[0]
|
17
|
+
else
|
18
|
+
array = []
|
19
|
+
total = 0
|
20
|
+
limit = 1
|
21
|
+
words = []
|
22
|
+
reset = true
|
23
|
+
|
24
|
+
numbers.each.with_index do |n, i|
|
25
|
+
words << n and next if n.is_a? String
|
26
|
+
|
27
|
+
if n == 1 && limit == 1
|
28
|
+
reset = false
|
29
|
+
next
|
30
|
+
end
|
31
|
+
|
32
|
+
if n >= 1_000
|
33
|
+
total += n * limit
|
34
|
+
limit = 1
|
35
|
+
reset = true
|
36
|
+
else
|
37
|
+
if n < 1
|
38
|
+
if words.join(' ') =~ /\band\b/
|
39
|
+
if total > 0 && total % 1_000
|
40
|
+
if total % (factor = 10 ** (total.to_i.to_s.size - 1)) == 0
|
41
|
+
limit = n * factor
|
42
|
+
else
|
43
|
+
limit = n
|
44
|
+
end
|
45
|
+
else
|
46
|
+
limit += n
|
47
|
+
end
|
48
|
+
else
|
49
|
+
limit *= n
|
50
|
+
end
|
51
|
+
elsif words.join(' ') =~ /\band\b/ && numbers[i + 1].to_i < 1
|
52
|
+
total += limit
|
53
|
+
limit = n
|
54
|
+
elsif !reset && limit >= 1 &&
|
55
|
+
m1 = (n > (m2 = numbers[i + 1].to_i) ? n + m2 : n) and
|
56
|
+
m = [limit, m1].sort and
|
57
|
+
!m[1].to_s[-(m0 = m[0].to_i.to_s.size), m0].to_i.zero?
|
58
|
+
|
59
|
+
array << total + limit
|
60
|
+
total = 0
|
61
|
+
limit = n
|
62
|
+
elsif !reset && limit == 1 && n > numbers[i + 1].to_i &&
|
63
|
+
m = [limit, n + numbers[i + 1].to_i].sort and
|
64
|
+
!m[1].to_s[-(m[0].to_i.to_s.size), m[0].to_i.to_s.size].to_i.zero?
|
65
|
+
|
66
|
+
array << total + limit
|
67
|
+
total = 0
|
68
|
+
limit = n
|
69
|
+
else
|
70
|
+
n > limit ? limit *= n : limit += n
|
71
|
+
end
|
72
|
+
|
73
|
+
total += limit if numbers[i + 1].nil?
|
74
|
+
reset = false
|
75
|
+
end
|
76
|
+
|
77
|
+
words.clear
|
78
|
+
end
|
79
|
+
|
80
|
+
array.empty? ? total : array << total
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|