marty 1.2.0 → 1.2.1
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +2 -2
- data/app/components/marty/base_rule_view.rb +279 -0
- data/app/components/marty/delorean_rule_view.rb +26 -0
- data/app/components/marty/extras/layout.rb +22 -7
- data/app/components/marty/log_view.rb +1 -1
- data/app/components/marty/mcfly_grid_panel.rb +53 -0
- data/app/components/marty/mcfly_grid_panel/client/dup_in_form.js +20 -0
- data/app/models/marty/base_rule.rb +126 -0
- data/app/models/marty/delorean_rule.rb +121 -0
- data/lib/marty/data_importer.rb +0 -1
- data/lib/marty/rule_script_set.rb +176 -0
- data/lib/marty/version.rb +1 -1
- data/lib/tasks/marty_tasks.rake +42 -0
- data/spec/dummy/app/components/gemini/cm_auth_app.rb +18 -0
- data/spec/dummy/app/components/gemini/my_rule_view.rb +63 -0
- data/spec/dummy/app/components/gemini/xyz_rule_view.rb +25 -0
- data/spec/dummy/app/models/gemini/guard_one.rb +5 -0
- data/spec/dummy/app/models/gemini/guard_two.rb +5 -0
- data/spec/dummy/app/models/gemini/my_rule.rb +46 -0
- data/spec/dummy/app/models/gemini/my_rule_type.rb +5 -0
- data/spec/dummy/app/models/gemini/xyz_enum.rb +5 -0
- data/spec/dummy/app/models/gemini/xyz_rule.rb +63 -0
- data/spec/dummy/app/models/gemini/xyz_rule_type.rb +5 -0
- data/spec/dummy/config/locales/en.yml +10 -0
- data/spec/dummy/db/migrate/20171220150101_add_rule_type_enums.rb +14 -0
- data/spec/dummy/db/migrate/20171221095312_create_gemini_my_rules.rb +22 -0
- data/spec/dummy/db/migrate/20171221095359_create_gemini_xyz_rules.rb +21 -0
- data/spec/dummy/db/migrate/20171222150100_add_rule_indices.rb +34 -0
- data/spec/dummy/db/seeds.rb +1 -1
- data/spec/dummy/delorean/base_code.dl +6 -0
- data/spec/dummy/lib/gemini/my_rule_script_set.rb +13 -0
- data/spec/dummy/lib/gemini/xyz_rule_script_set.rb +22 -0
- data/spec/features/rule_spec.rb +265 -0
- data/spec/fixtures/csv/rule/DataGrid.csv +6 -0
- data/spec/fixtures/csv/rule/MyRule.csv +14 -0
- data/spec/fixtures/csv/rule/XyzRule.csv +6 -0
- data/spec/models/rule_spec.rb +322 -0
- data/spec/support/integration_helpers.rb +1 -0
- metadata +29 -2
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
// copied from basepack grid's onEditInForm
|
3
|
+
netzkeOnDupInForm: function(){
|
4
|
+
var selModel = this.getSelectionModel();
|
5
|
+
var recordId = selModel.selected.first().getId();
|
6
|
+
this.netzkeLoadComponent("edit_window", {
|
7
|
+
title: "Duplicate in Form",
|
8
|
+
serverConfig: {record_id: recordId},
|
9
|
+
callback: function(w){
|
10
|
+
w.show();
|
11
|
+
var form = w.items.first();
|
12
|
+
form.baseParams = {dup: true};
|
13
|
+
w.on('close', function(){
|
14
|
+
if (w.closeRes === "ok") {
|
15
|
+
this.store.load();
|
16
|
+
}
|
17
|
+
}, this);
|
18
|
+
}, scope: this});
|
19
|
+
}
|
20
|
+
}
|
@@ -0,0 +1,126 @@
|
|
1
|
+
class Marty::BaseRule < Marty::Base
|
2
|
+
self.abstract_class = true
|
3
|
+
has_mcfly
|
4
|
+
|
5
|
+
def self.guard_info
|
6
|
+
{}
|
7
|
+
end
|
8
|
+
|
9
|
+
def chkrange(v)
|
10
|
+
v.match(/\A(\[|\()([0-9\.-]*),([0-9\.-]*)(\]|\))\z/)
|
11
|
+
end
|
12
|
+
def gettypes(v)
|
13
|
+
types = []
|
14
|
+
types << :string if v.is_a?(String)
|
15
|
+
types += [:int, :integer] if Integer(v) rescue nil
|
16
|
+
types << :float if Float(v) rescue nil
|
17
|
+
types << :date if Date.parse(v) rescue nil
|
18
|
+
types << :datetime if DateTime.parse(v) rescue nil
|
19
|
+
types << :range if chkrange(v) rescue nil
|
20
|
+
types << :boolean if [true, false].include?(v)
|
21
|
+
types
|
22
|
+
end
|
23
|
+
def check(name, h)
|
24
|
+
multi, type, enum, values, req = h.values_at(:multi, :type, :enum, :values,
|
25
|
+
:required)
|
26
|
+
ns = "'#{name}'"
|
27
|
+
expmulti = multi ? 'multi' : 'single'
|
28
|
+
errtype = :guards
|
29
|
+
v = simple_guards[name]
|
30
|
+
type ||= :string
|
31
|
+
return errors[errtype] << "- Required field #{ns} is missing" if
|
32
|
+
v.blank? && req
|
33
|
+
return if v.blank?
|
34
|
+
gotmulti = v.is_a?(Array) ? 'multi' : 'single'
|
35
|
+
return errors[errtype] << "- Wrong arity for #{ns} (expected #{expmulti} "\
|
36
|
+
"got #{gotmulti})" if expmulti != gotmulti
|
37
|
+
vs = [v].flatten.to_set
|
38
|
+
vs.each do |vv|
|
39
|
+
return errors[errtype] << "- Wrong type for #{ns}" unless
|
40
|
+
gettypes(vv).member?(type)
|
41
|
+
end
|
42
|
+
return unless enum || values
|
43
|
+
vals = enum && enum::VALUES || values.to_set
|
44
|
+
bad = (vs - vals)
|
45
|
+
p = bad.count > 1 ? 's' : ''
|
46
|
+
return errors[errtype] <<
|
47
|
+
%Q(- Bad value#{p} '#{bad.to_a.join("', '")}' for #{ns}) if bad.present?
|
48
|
+
end
|
49
|
+
def validate
|
50
|
+
self.class.guard_info.each { |name, h| check(name, h) }
|
51
|
+
grids.each do |vn, gn|
|
52
|
+
return errors[:grids] << "- Bad grid name '#{gn}' for '#{vn}'" unless
|
53
|
+
gn.blank? || Marty::DataGrid.lookup('infinity', gn)
|
54
|
+
end
|
55
|
+
cg_err = computed_guards.delete("~~ERROR~~")
|
56
|
+
errors[:computed] <<
|
57
|
+
"- Error in rule '#{name}' field 'computed_guards': #{cg_err.capitalize}" if cg_err
|
58
|
+
res_err = results.delete("~~ERROR~~")
|
59
|
+
errors[:computed] <<
|
60
|
+
"- Error in rule '#{name}' field 'results': #{res_err.capitalize}" if res_err
|
61
|
+
|
62
|
+
same_name_diff_guards = self.class.
|
63
|
+
where(obsoleted_dt: 'infinity', name: self.name).
|
64
|
+
# id is nil on new rules
|
65
|
+
where.not(id: self.id).
|
66
|
+
where("simple_guards != '#{self.simple_guards.to_json}'")
|
67
|
+
|
68
|
+
errors[:base] =
|
69
|
+
"Can't have rule with same name and different type/guards" +
|
70
|
+
" - #{self.name}" if same_name_diff_guards.exists?
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
validates_presence_of :name
|
75
|
+
validate :validate
|
76
|
+
|
77
|
+
before_validation(on: [:create, :update]) do
|
78
|
+
self.simple_guards ||= {}
|
79
|
+
self.computed_guards ||= {}
|
80
|
+
self.grids ||= {}
|
81
|
+
self.results ||= {}
|
82
|
+
end
|
83
|
+
|
84
|
+
before_create do
|
85
|
+
self.class.guard_info.each do |k,v|
|
86
|
+
next if v[:default].blank? || self.simple_guards.include?(k)
|
87
|
+
self.simple_guards[k] = v[:default]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.get_subq(field, subfield, multi, type, vraw)
|
92
|
+
arrow = multi || ![:range, :string, :date, :datetime].include?(type) ?
|
93
|
+
"->" : "->>"
|
94
|
+
op = multi || type == :range ? "@>" : "="
|
95
|
+
value0 = [:string, :date, :datetime].include?(type) ?
|
96
|
+
ActiveRecord::Base.connection.quote(vraw) :
|
97
|
+
type == :range ? vraw.to_f :
|
98
|
+
"'#{vraw}'::jsonb"
|
99
|
+
value = multi ? %Q('["%s"]') % value0[1..-2] : value0
|
100
|
+
fieldcast = type == :range ? "::numrange" : ''
|
101
|
+
"(#{field}#{arrow}'#{subfield}')#{fieldcast} #{op} #{value}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.get_matches_(pt, attrs, params)
|
105
|
+
|
106
|
+
q = select("DISTINCT ON (name) *").where(attrs)
|
107
|
+
|
108
|
+
params.each do |k, vraw|
|
109
|
+
h = guard_info
|
110
|
+
use_k = (h[k] && k) ||
|
111
|
+
(h[k+"_array"] && k+"_array") ||
|
112
|
+
(h[k+"_range"] && k+"_range")
|
113
|
+
next unless use_k
|
114
|
+
multi, type = h[use_k].values_at(:multi, :type)
|
115
|
+
filts = [vraw].flatten.map do |v|
|
116
|
+
qstr = get_subq('simple_guards', use_k, multi, type, v)
|
117
|
+
end.join(" OR ")
|
118
|
+
isn = "simple_guards->'#{use_k}' IS NULL OR"
|
119
|
+
|
120
|
+
q = q.where("(#{isn} #{filts})")
|
121
|
+
end
|
122
|
+
# print q.to_sql
|
123
|
+
q.order(:name)
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
class Marty::DeloreanRule < Marty::BaseRule
|
2
|
+
self.abstract_class = true
|
3
|
+
|
4
|
+
validates_presence_of :rule_type, :start_dt
|
5
|
+
|
6
|
+
def validate
|
7
|
+
super
|
8
|
+
if self.class.where(obsoleted_dt: 'infinity', name: name).
|
9
|
+
where.not(id: id).
|
10
|
+
where("(start_dt, coalesce(end_dt, 'infinity')) OVERLAPS (?, ?)",
|
11
|
+
start_dt, end_dt || 'infinity').exists?
|
12
|
+
return errors[:base] <<
|
13
|
+
"Can't have rule with same name and overlapping start/end dates"\
|
14
|
+
" - #{name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
return errors[:base] = "Start date must be before end date" if
|
18
|
+
start_dt && end_dt && start_dt >= end_dt
|
19
|
+
|
20
|
+
if computed_guards.present? || results.present?
|
21
|
+
begin
|
22
|
+
eclass = engine && engine.constantize || Marty::RuleScriptSet
|
23
|
+
eng = eclass.new('infinity').get_engine(self)
|
24
|
+
rescue => e
|
25
|
+
return errors[:computed] = "- " + e.message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_fixed(results)
|
31
|
+
results.each_with_object({}) do |(k, v), h|
|
32
|
+
v_wo_comment = /\A([^#]+)/.match(v)[1] if v.include?("#")
|
33
|
+
# if v contains a #, try cut it there and attempt parse that way first
|
34
|
+
jp = (v_wo_comment && JSON.parse("[#{v_wo_comment}]") rescue nil) ||
|
35
|
+
(JSON.parse("[#{v}]") rescue nil)
|
36
|
+
next h[k] = jp[0] if jp
|
37
|
+
# json doesn't like single quotes, so handle those manually
|
38
|
+
m = %r{\A'(.*)'\z}.match(v)
|
39
|
+
next unless m
|
40
|
+
h[k] = m[1]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
before_validation(on: [:create, :update]) do
|
45
|
+
# identify result values that are fixed, stash them (removing quotes)
|
46
|
+
self.fixed_results = self.class.find_fixed(self.results)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.results_cfg_var
|
50
|
+
"NOT DEFINED"
|
51
|
+
end
|
52
|
+
|
53
|
+
def compg_keys
|
54
|
+
computed_guards.keys
|
55
|
+
end
|
56
|
+
|
57
|
+
def compres_keys
|
58
|
+
defkeys = (Marty::Config[self.class.results_cfg_var] || {}).keys +
|
59
|
+
["adjustment", "breakeven"]
|
60
|
+
results.keys.select{|k| defkeys.include?(k)} + grid_keys
|
61
|
+
end
|
62
|
+
def grid_keys
|
63
|
+
grids.keys.map{|k|k.ends_with?("_grid") ? k : k + "_grid"}
|
64
|
+
end
|
65
|
+
def base_compute(params, dgparams=params)
|
66
|
+
eclass = engine && engine.constantize || Marty::RuleScriptSet
|
67
|
+
engine = eclass.new(params["pt"]).get_engine(self) if
|
68
|
+
computed_guards.present? || results.present?
|
69
|
+
|
70
|
+
if computed_guards.present?
|
71
|
+
begin
|
72
|
+
res = engine.evaluate(eclass.node_name,
|
73
|
+
compg_keys,
|
74
|
+
params.clone)
|
75
|
+
rescue => e
|
76
|
+
raise e, "Error (guard) in rule '#{id}:#{name}': #{e}", e.backtrace
|
77
|
+
end
|
78
|
+
return Hash[compg_keys.zip(res).select{|k,v| !v}] unless res.all?
|
79
|
+
end
|
80
|
+
grids_computed = false
|
81
|
+
grid_results = {}
|
82
|
+
if (results.keys - fixed_results.keys).present?
|
83
|
+
begin
|
84
|
+
eval_result = engine.evaluate(
|
85
|
+
eclass.node_name,
|
86
|
+
compres_keys,
|
87
|
+
params + {
|
88
|
+
"dgparams__" => dgparams,
|
89
|
+
})
|
90
|
+
grids_computed = true
|
91
|
+
rescue => e
|
92
|
+
raise e, "Error (results) in rule '#{id}:#{name}': #{e}", e.backtrace
|
93
|
+
end
|
94
|
+
result = Hash[compres_keys.zip(eval_result)]
|
95
|
+
elsif fixed_results.keys.sort == results.keys.sort
|
96
|
+
result = fixed_results
|
97
|
+
end
|
98
|
+
if grids.present? && !grids_computed
|
99
|
+
pt = params['pt']
|
100
|
+
gres = {}
|
101
|
+
grid_results = grids.each_with_object({}) do |(gvar, gname), h|
|
102
|
+
usename = gvar.ends_with?("_grid") ? gvar : gvar + "_grid"
|
103
|
+
next h[usename] = gres[gname] if gres[gname]
|
104
|
+
dg = Marty::DataGrid.lookup(pt,gname)
|
105
|
+
dgr = dg && dg.lookup_grid_distinct_entry(pt, dgparams)
|
106
|
+
h[usename] = gres[gname] = dgr["result"] if dgr
|
107
|
+
end
|
108
|
+
end
|
109
|
+
result + grid_results
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.get_matches_(pt, attrs, params)
|
113
|
+
q = super(pt, attrs.except("rule_dt"), params)
|
114
|
+
rule_dt = attrs["rule_dt"]
|
115
|
+
q=q.where("start_dt <= ?", rule_dt).
|
116
|
+
where("end_dt >= ? OR end_dt IS NULL", rule_dt) if rule_dt
|
117
|
+
#puts q.to_sql
|
118
|
+
q
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
data/lib/marty/data_importer.rb
CHANGED
@@ -0,0 +1,176 @@
|
|
1
|
+
class Marty::RuleScriptSet < Delorean::AbstractContainer
|
2
|
+
attr_reader :sset, :pt
|
3
|
+
|
4
|
+
def self.clear_cache
|
5
|
+
@@engines, @@dengines, @@dengines_dt = {}, {}, nil
|
6
|
+
end
|
7
|
+
|
8
|
+
clear_cache
|
9
|
+
|
10
|
+
def self.node_name
|
11
|
+
"Node"
|
12
|
+
end
|
13
|
+
def self.body_start
|
14
|
+
"#{node_name}:\n"
|
15
|
+
end
|
16
|
+
def self.body_lines
|
17
|
+
self.body_start.count("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(pt)
|
21
|
+
@pt = Mcfly.normalize_infinity(pt)
|
22
|
+
|
23
|
+
# if pt is Infinity, we get a DEV Tag
|
24
|
+
tag = Marty::Tag.cached_find_match(pt)
|
25
|
+
@sset = Marty::ScriptSet.new(tag)
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_check(sname, body)
|
30
|
+
sset.parse_check(sname, body)
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_attr(k, v)
|
34
|
+
k + (v == :parameter ? " =?" : " = #{v}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def paramify_h(h)
|
38
|
+
"{" + h.keys.reject{|k|k.ends_with?("__")}.
|
39
|
+
map {|k| %Q("#{k}": #{k}) }.join(",\n") + "}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def expand_grid_code(h, dgid, dgname, cache, extra_params)
|
43
|
+
final_name = dgid.ends_with?("_grid") ? dgid : dgid + "_grid"
|
44
|
+
if cache[dgname]
|
45
|
+
h[final_name] = "#{cache[dgname]}"
|
46
|
+
else
|
47
|
+
h["#{dgid}_dg__"] = "Marty::DataGrid.lookup(pt,'#{dgname}')"
|
48
|
+
h["#{dgid}_dgp__"] = "dgparams__ + \n" + self.class.indent(paramify_h(h))
|
49
|
+
lgde = "lookup_grid_distinct_entry"
|
50
|
+
h["#{dgid}_h__"] = "#{dgid}_dg__.#{lgde}(pt,#{dgid}_dgp__)"
|
51
|
+
h[final_name] = "#{dgid}_h__ && #{dgid}_h__.result"
|
52
|
+
cache[dgname] = final_name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def write_code(attrs)
|
57
|
+
return '' if attrs.blank?
|
58
|
+
newh = attrs.each_with_object({}) do |(k, v), h|
|
59
|
+
if k.ends_with?("_grid")
|
60
|
+
expand_grid_code(h, k, v, {}, h)
|
61
|
+
else
|
62
|
+
h[k] = v
|
63
|
+
end
|
64
|
+
end
|
65
|
+
newh.map { |k, v| write_attr(k, v) }.join("\n") + "\n"
|
66
|
+
end
|
67
|
+
|
68
|
+
def grid_code(rule)
|
69
|
+
dgcache = {}
|
70
|
+
h = {}
|
71
|
+
rule.grids.each do |k, v|
|
72
|
+
expand_grid_code(h, k, v, dgcache, {})
|
73
|
+
end
|
74
|
+
h.map { |k, v| write_attr(k, v) }.join("\n") + "\n"
|
75
|
+
end
|
76
|
+
|
77
|
+
def guard_code(rule)
|
78
|
+
write_code(rule.computed_guards)
|
79
|
+
end
|
80
|
+
|
81
|
+
def result_code(rule)
|
82
|
+
write_code(rule.results)
|
83
|
+
end
|
84
|
+
|
85
|
+
def grid_init(rule)
|
86
|
+
if rule.grids.present? || rule.results.keys.any?{|k|k.ends_with?("_grid")}
|
87
|
+
write_code({ "pt" => :parameter,
|
88
|
+
"dgparams__" => :parameter,
|
89
|
+
})
|
90
|
+
else
|
91
|
+
''
|
92
|
+
end
|
93
|
+
end
|
94
|
+
def get_code(rule)
|
95
|
+
grid_i = grid_init(rule)
|
96
|
+
grid_c = grid_code(rule)
|
97
|
+
result_c = result_code(rule)
|
98
|
+
guard_c = guard_code(rule)
|
99
|
+
|
100
|
+
code = self.class.body_start + self.class.indent(grid_i +
|
101
|
+
guard_c +
|
102
|
+
grid_c +
|
103
|
+
result_c)
|
104
|
+
#puts '='*40
|
105
|
+
#puts rule.name
|
106
|
+
#puts '-'
|
107
|
+
#puts code
|
108
|
+
#puts '-'*10
|
109
|
+
code
|
110
|
+
end
|
111
|
+
|
112
|
+
def code_section_counts(rule)
|
113
|
+
errs = {}
|
114
|
+
errs[:grid_params] = grid_init(rule).count("\n")
|
115
|
+
errs[:computed_guards] = guard_code(rule).count("\n")
|
116
|
+
errs[:grids] = grid_code(rule).count("\n")
|
117
|
+
errs[:results] = result_code(rule).count("\n")
|
118
|
+
errs
|
119
|
+
end
|
120
|
+
def get_parse_error_field(rule, exc)
|
121
|
+
line = exc.line ? exc.line - self.class.body_lines : 0
|
122
|
+
errs = code_section_counts(rule)
|
123
|
+
line_count = 0
|
124
|
+
errs.each do |k,v|
|
125
|
+
line_count += v
|
126
|
+
return k if line <= line_count
|
127
|
+
end
|
128
|
+
errs.keys.last
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_engine(rule)
|
132
|
+
begin
|
133
|
+
# if rule is a str => importing a regular Script (i.e. not rule)
|
134
|
+
return sset.get_engine(rule) if rule.is_a? String
|
135
|
+
|
136
|
+
# on create rule doesn't have an id => don't cache
|
137
|
+
return sset.parse_check("New RULE #{rule.name}", get_code(rule)) unless
|
138
|
+
rule.id
|
139
|
+
|
140
|
+
rule_pfx = rule.class.name.demodulize
|
141
|
+
|
142
|
+
# unique name for specific version of rule
|
143
|
+
sname = "#{rule_pfx}_#{rule.group_id}_#{rule.created_dt.to_f}"
|
144
|
+
|
145
|
+
# is it a dev posting?
|
146
|
+
if Mcfly.is_infinity(pt)
|
147
|
+
max_dt = rule.class.
|
148
|
+
order("created_dt DESC").limit(1).pluck(:created_dt).first
|
149
|
+
|
150
|
+
@@dengines_dt ||= max_dt
|
151
|
+
|
152
|
+
# reset dengine cache if an rule has changed
|
153
|
+
@@dengines = {} if max_dt > @@dengines_dt
|
154
|
+
|
155
|
+
engine = @@dengines[sname]
|
156
|
+
|
157
|
+
return engine if engine
|
158
|
+
|
159
|
+
@@dengines[sname] = sset.parse_check(sname, get_code(rule))
|
160
|
+
else
|
161
|
+
engine = @@engines[[pt, sname]]
|
162
|
+
|
163
|
+
return engine if engine
|
164
|
+
|
165
|
+
@@engines[[pt, sname]] = sset.parse_check(sname, get_code(rule))
|
166
|
+
end
|
167
|
+
rescue Delorean::ParseError => e
|
168
|
+
f = get_parse_error_field(rule, e)
|
169
|
+
raise "Error in rule '#{rule.name}' field '#{f}': #{e}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.indent(s)
|
174
|
+
s.gsub(/^/, ' '*4)
|
175
|
+
end
|
176
|
+
end
|