acts_as_span 0.0.5 → 1.2.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -3
  3. data/.rspec +2 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +12 -0
  6. data/Gemfile +0 -3
  7. data/README.rdoc +4 -16
  8. data/Rakefile +5 -1
  9. data/acts_as_span.gemspec +31 -14
  10. data/config/locales/en/acts_as_span.yml +15 -0
  11. data/lib/acts_as_span.rb +69 -98
  12. data/lib/acts_as_span/end_date_propagator.rb +198 -0
  13. data/lib/acts_as_span/no_overlap_validator.rb +86 -0
  14. data/lib/acts_as_span/span_instance.rb +24 -32
  15. data/lib/acts_as_span/span_instance/status.rb +12 -27
  16. data/lib/acts_as_span/span_instance/validations.rb +11 -53
  17. data/lib/acts_as_span/span_klass.rb +11 -19
  18. data/lib/acts_as_span/span_klass/status.rb +43 -12
  19. data/lib/acts_as_span/version.rb +6 -4
  20. data/lib/acts_as_span/within_parent_date_span_validator.rb +44 -0
  21. data/spec/lib/acts_as_span_spec.rb +38 -35
  22. data/spec/lib/delegation_spec.rb +45 -78
  23. data/spec/lib/end_date_propagator_spec.rb +319 -0
  24. data/spec/lib/no_overlap_validator_spec.rb +129 -0
  25. data/spec/lib/span_instance/named_scopes_on_spec.rb +193 -193
  26. data/spec/lib/span_instance/named_scopes_spec.rb +193 -191
  27. data/spec/lib/span_instance/overlap_spec.rb +193 -253
  28. data/spec/lib/span_instance/status_spec.rb +22 -35
  29. data/spec/lib/span_instance/validations_spec.rb +8 -44
  30. data/spec/lib/span_instance_spec.rb +17 -30
  31. data/spec/lib/span_klass/status_spec.rb +38 -0
  32. data/spec/lib/within_parent_date_span_validator_spec.rb +126 -0
  33. data/spec/spec_helper.rb +19 -6
  34. data/spec/spec_models.rb +226 -0
  35. metadata +167 -61
  36. data/Gemfile.lock +0 -47
  37. data/lib/acts_as_span/span_instance/overlap.rb +0 -17
  38. data/lib/acts_as_span/span_klass/overlap.rb +0 -21
  39. data/spec/lib/negative_spec.rb +0 -30
  40. data/spec/spec.opts +0 -1
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 59d6ed6e5f0b02d6d3683a99d62ac5f3b11ccdb5cf510f5bf1a3197e095102ff
4
+ data.tar.gz: 856975edeadab3d35def0a65e51bb6d65987fc2042cdec822d72b7c76741a9cb
5
+ SHA512:
6
+ metadata.gz: cb1ae3e3c092a528e83d7dcb2fd9ca7d601b985eaa974b6eac1c1eb2634dc4630473dc5bfcafd548d3748a602569185cd803c2f7f6551c486dddcee1905e8205
7
+ data.tar.gz: 605657a4fb0114f18018b4aab29d9c2f9c63bf87321fe7c04ceaccb7974ad064dc4112ce3b7e5b44c8b541b65680e768e60ef54e8d43e1502ec59f27f46c797d
data/.gitignore CHANGED
@@ -1,3 +1,11 @@
1
- coverage
2
- rdoc
3
- pkg
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+
6
+ .DS_Store
7
+ *.log
8
+ *.sqlite
9
+
10
+ .rvmrc
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --order rand
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.6.5
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.5
4
+ - 2.5.3
5
+ - 2.6.5
6
+
7
+ # match the bundler version in the gemspec
8
+ before_install:
9
+ - gem install -v 2.1.4 bundler --no-document
10
+ - bundle _2.1.4_ install
11
+ notifications:
12
+ email: false
data/Gemfile CHANGED
@@ -2,6 +2,3 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in acts_as_span.gemspec
4
4
  gemspec
5
-
6
- gem 'rspec'
7
- gem 'acts_as_fu'
data/README.rdoc CHANGED
@@ -1,6 +1,8 @@
1
1
  = acts_as_span
2
2
 
3
- ActiveRecord model w/ a start_date and an end_date
3
+ ActiveRecord model w/ a start_date and an end_date == ActsAsSpan
4
+
5
+ Treat those date spans like the objects they are!
4
6
 
5
7
  == Getting Started
6
8
 
@@ -14,20 +16,6 @@ In your model:
14
16
  acts_as_span
15
17
  end
16
18
 
17
- In your migrations:
18
-
19
- class AddSpanToSpanRecord < ActiveRecord::Migration
20
- def self.up
21
- add_column :span_records, :start_date, :date
22
- add_column :span_records, :end_date, :date
23
- end
24
-
25
- def self.down
26
- remove_column :span_records, :start_date
27
- remove_column :span_records, :end_date
28
- end
29
- end
30
-
31
19
  == Copyright
32
20
 
33
- Copyright (c) 2011 Annkissam. See LICENSE for details.
21
+ Copyright (c) 2011-2018 Annkissam. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
+
3
+ #RSPEC
2
4
  require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new('spec')
7
+
8
+ task :default => :spec
data/acts_as_span.gemspec CHANGED
@@ -1,23 +1,40 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "acts_as_span/version"
1
+ # frozen_string_Literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+ require 'acts_as_span/version'
4
5
 
5
6
  Gem::Specification.new do |s|
6
- s.name = "acts_as_span"
7
+ s.name = 'acts_as_span'
7
8
  s.version = ActsAsSpan::VERSION::STRING
8
- s.authors = ["Eric Sullivan"]
9
- s.email = ["eric.sullivan@annkissam.com"]
10
- s.homepage = "https://github.com/annkissam/acts_as_span"
9
+ s.authors = ['Eric Sullivan']
10
+ s.email = ['eric.sullivan@annkissam.com']
11
+ s.homepage = 'https://github.com/annkissam/acts_as_span'
11
12
  s.summary = ActsAsSpan::VERSION::SUMMARY
12
- s.description = %q{start_date and end_date as a span w/ ActiveRecord}
13
+ s.description = 'ActiveRecord model w/ a start_date and an end_date == ActsAsSpan'
14
+ s.license = 'MIT'
13
15
 
14
- s.rubyforge_project = "acts_as_span"
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if s.respond_to?(:metadata)
19
+ s.metadata['allowed_push_host'] = 'https://rubygems.org'
20
+ else
21
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
22
+ 'public gem pushes.'
23
+ end
15
24
 
16
25
  s.files = `git ls-files`.split("\n")
17
26
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
20
-
21
- s.add_dependency(%q<activerecord>, [">= 3"])
22
- s.add_dependency('activesupport')
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
28
+ s.require_paths = %w[lib]
29
+
30
+ s.add_development_dependency 'bundler', '~> 2.1.4'
31
+ s.add_development_dependency 'has_siblings', '~> 0.2.7'
32
+ s.add_development_dependency 'pry-byebug'
33
+ s.add_development_dependency 'rake', '>= 12.3.3'
34
+ s.add_development_dependency 'rspec', '~> 3.0'
35
+ s.add_development_dependency 'sqlite3', '~> 1.4'
36
+ s.add_development_dependency 'temping'
37
+
38
+ s.add_runtime_dependency('activerecord', '>= 5.0.0')
39
+ s.add_runtime_dependency('activesupport', '>= 5.0.0')
23
40
  end
@@ -0,0 +1,15 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ end_date_propagator:
6
+ propagation_failure: "%{parent} could not propagate
7
+ %{end_date_field_name} to %{child}:\n%{reason}"
8
+ # TODO: let pluralize handle pluralization
9
+ no_overlap:
10
+ one: "A %{model_name} already exists between
11
+ %{start_date} - %{end_date}: %{overlapping_records_s}"
12
+ other: "%{count} %{model_name_plural} already exist between
13
+ %{start_date} - %{end_date}: %{overlapping_records_s}"
14
+ not_within_parent_date_span: Must exist within the %{parent} date span
15
+ start_date_after_end_date: Must be on or after %{start_field}
data/lib/acts_as_span.rb CHANGED
@@ -1,161 +1,132 @@
1
- require 'active_support'
2
1
  require 'ostruct'
3
- require 'forwardable'
2
+ require 'acts_as_span/version'
3
+ require 'acts_as_span/span_klass'
4
+ require 'acts_as_span/span_instance'
5
+
6
+ require 'acts_as_span/no_overlap_validator'
7
+ require 'acts_as_span/within_parent_date_span_validator'
4
8
 
5
- ACTS_AS_SPAN_PATH = File.dirname(__FILE__) + "/acts_as_span/"
9
+ require 'acts_as_span/end_date_propagator'
6
10
 
7
- require ACTS_AS_SPAN_PATH + 'version'
8
- require ACTS_AS_SPAN_PATH + 'span_klass'
9
- require ACTS_AS_SPAN_PATH + 'span_instance'
11
+ require 'active_support'
12
+ require 'active_record'
13
+
14
+ I18n.load_path += Dir[File.join(File.dirname(__dir__), 'config', 'locales', '**', 'acts_as_span.yml')]
10
15
 
11
16
  module ActsAsSpan
12
17
  extend ActiveSupport::Concern
13
-
18
+
19
+ OPTIONS = %i[start_field end_field name].freeze
20
+
14
21
  class << self
15
22
  def options
16
23
  @options ||= {
17
- :start_date_field => :start_date,
18
- :end_date_field => :end_date,
19
- :start_date_field_required => false,
20
- :end_date_field_required => false,
21
- :span_overlap_scope => nil,
22
- :span_overlap_count => nil,
24
+ :start_field => :start_date,
25
+ :end_field => :end_date,
23
26
  :name => :default
24
27
  }
25
28
  end
26
-
29
+
27
30
  def configure
28
31
  yield(self) if block_given?
29
32
  end
30
33
  end
31
-
32
- #by default, all model classess & their instances will return false w/ acts_as_span?
33
- included do
34
- self.send(:extend, ActsAsSpan::NegativeMethods)
35
- self.send(:include, ActsAsSpan::NegativeMethods)
36
- end
37
-
34
+
38
35
  module ClassMethods
39
36
  def acts_as_span(*args)
40
- #this model & its instances will return true w/ acts_as_span?
41
- self.send(:extend, ActsAsSpan::PositiveMethods)
42
- self.send(:include, ActsAsSpan::PositiveMethods)
43
-
44
- self.send(:extend, Forwardable)
45
-
46
37
  self.send(:extend, ActsAsSpan::ExtendedClassMethods)
47
38
  self.send(:include, ActsAsSpan::IncludedInstanceMethods)
48
-
39
+
40
+ # TODO: There's some refactoring that could be done here using keyword args (or the more standard old hash arg pattern)
49
41
  options = OpenStruct.new(args.last.is_a?(Hash) ? ActsAsSpan.options.merge(args.pop) : ActsAsSpan.options)
50
-
51
- #if span_overlap_scope is specified the span_overlap_count defaults to 0
52
- options.span_overlap_count ||= 0 if options.span_overlap_scope
53
-
42
+
43
+ unsupported_options =
44
+ options.to_h.keys.reject { |opt| OPTIONS.include? opt }
45
+ unless unsupported_options.empty?
46
+ raise ArgumentError,
47
+ 'Unsupported option(s): ' <<
48
+ unsupported_options.map { |o| "'#{o}'" }.join(', ')
49
+ end
50
+
54
51
  acts_as_span_definitions[options.name] = options
55
-
56
- def_delegators :span, :close!,
57
- :close_on!,
58
- :span_status,
59
- :span_status_on,
60
- :span_status_to_s,
61
- :span_status_to_s_on,
62
- :current?,
63
- :current_on?,
64
- :future?,
65
- :future_on?,
66
- :expired?,
67
- :expired_on?
68
-
69
- def_delegators 'self.class', :acts_as_span_definitions
70
-
52
+
53
+ # TODO add tests that check delegation of all methos in span
54
+ delegate :span_status,
55
+ :span_status_on,
56
+ :current?,
57
+ :current_on?,
58
+ :future?,
59
+ :future_on?,
60
+ :expired?,
61
+ :expired_on?,
62
+ :past?,
63
+ :past_on?, to: :span
64
+
65
+ delegate :acts_as_span_definitions, to: :class
66
+
67
+ # TODO idem above
71
68
  class << self
72
- self.send(:extend, Forwardable)
73
-
74
- def_delegators :span, :current,
75
- :current_on,
76
- :future,
77
- :future_on,
78
- :expired,
79
- :expired_on
69
+ delegate :current,
70
+ :current_on,
71
+ :future,
72
+ :future_on,
73
+ :expired,
74
+ :expired_on,
75
+ :past_on,
76
+ :past,
77
+ :current_or_future_on,
78
+ :current_or_future, to: :span
80
79
  end
81
-
80
+
82
81
  validate :validate_spans
83
82
  end
84
-
83
+
85
84
  def acts_as_span_definitions
86
85
  @_acts_as_span_definitions ||= {}
87
86
  end
88
87
  end
89
-
88
+
90
89
  module ExtendedClassMethods
91
- def overlap(test_record)
92
- overlap_for(test_record, :default, :default)
93
- end
94
-
95
- def overlap_for(test_record, test_record_span_name = :default, this_span_name = :default)
96
- span_for(this_span_name).overlap(test_record.span_for(test_record_span_name))
97
- end
98
-
99
90
  def spans
100
91
  acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
101
92
  end
102
-
93
+
103
94
  def span
104
95
  span_for(:default)
105
96
  end
106
-
97
+
107
98
  def span_for(name = :default)
108
99
  acts_as_span_klasses[name] ||= SpanKlass.new(name, self, acts_as_span_definitions[name])
109
100
  end
110
-
101
+
111
102
  def acts_as_span_klasses
112
103
  @_acts_as_span_klasses ||= {}
113
104
  end
114
105
  end
115
-
106
+
116
107
  module IncludedInstanceMethods
117
108
  def spans
118
109
  acts_as_span_definitions.keys.map { |acts_as_span_definition_name| span_for(acts_as_span_definition_name) }
119
110
  end
120
-
111
+
121
112
  def span
122
113
  span_for(:default)
123
114
  end
124
-
115
+
125
116
  def span_for(name = :default)
126
117
  acts_as_span_instances[name] ||= SpanInstance.new(name, self, acts_as_span_definitions[name])
127
118
  end
128
-
119
+
129
120
  def acts_as_span_instances
130
121
  @_acts_as_span_instances ||= {}
131
122
  end
132
-
123
+
133
124
  def validate_spans
134
125
  spans.each(&:validate)
135
126
  end
136
-
137
- #This syntax assumes :default span
138
- def overlap?(other_record)
139
- overlap_for?(other_record, :default, :default)
140
- end
141
-
142
- #record.span_for(:this_span_name).overlap?(other_record.span_for(:other_record_span_name))
143
- def overlap_for?(other_record, this_span_name = :default, other_record_span_name = :default)
144
- span_for(this_span_name).overlap?(other_record.span_for(other_record_span_name))
145
- end
146
- end
147
-
148
- module PositiveMethods
149
- send(:define_method, "acts_as_span?") do
150
- true
151
- end
152
- end
153
-
154
- module NegativeMethods
155
- send(:define_method, "acts_as_span?") do
156
- false
157
- end
158
127
  end
159
128
  end
160
129
 
161
- ActiveRecord::Base.send(:include, ActsAsSpan)
130
+ if Object.const_defined?("ActiveRecord")
131
+ ActiveRecord::Base.send(:include, ActsAsSpan)
132
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_Literal: true
2
+
3
+ module ActsAsSpan
4
+ # # End Date Propagator
5
+ #
6
+ # When editing the `end_date` of a record, the record's children often also
7
+ # need to be updated. This propagator takes care of that.
8
+ # For each of the child records (defined below in the function `children`),
9
+ # the child record's `end_date` is updated to match that of the original
10
+ # object. The function `propagate` is recursive, propagating to
11
+ # children of children and so on.
12
+ # Records that should not have their end dates propagated in this manner
13
+ # (e.g. StatusRecords) are manually excluded in `skipped_classes`.
14
+ # If there is some error preventing propagation, the child record is NOT saved
15
+ # and that error message is added to the object's `errors`. These errors
16
+ # propagate upwards into a flattened array of error messages.
17
+ #
18
+ # This class uses its own definition of 'child' for an object. For a given
19
+ # object, the objects the propagator considers its children are:
20
+ # * Associated via `has_many` association
21
+ # * Association `:dependent` option is `:delete` or `:destroy`
22
+ # * acts_as_span (checked via `respond_to?(:span)`)
23
+ # * Not blacklisted via `skipped_classes` array
24
+ #
25
+ # The return value for `call` is the given object, updated to have children's
26
+ # errors added to its `:base` errors if any children had errors.
27
+ #
28
+ # ## Usage:
29
+ #
30
+ # Propagate end dates for an object that acts_as_span and has propagatable
31
+ # children to all propagatable children:
32
+ # ```
33
+ # ActsAsSpan::EndDatePropagator.call(object)
34
+ # ```
35
+ #
36
+ # To propagate to a subset of its propagatable children:
37
+ # ```
38
+ # ActsAsSpan::EndDatePropagator.call(
39
+ # object, skipped_classes: [ClassOne, ClassTwo]
40
+ # )
41
+ # ```
42
+ # ... where ClassOne and ClassTwo are the classes to be excluded.
43
+ #
44
+ # The EndDatePropagator does not use transactions. If the propagation should
45
+ # be run in a transaction, wrap the call in one like so:
46
+ # ```
47
+ # ActiveRecord::Base.transaction do
48
+ # ActsAsSpan::EndDatePropagator.call(
49
+ # obj, skipped_classes: [ClassOne, ClassTwo]
50
+ # )
51
+ # end
52
+ # ```
53
+ #
54
+ # One use case for the transaction wrapper would be to not follow through
55
+ # with propagation if the object has errors:
56
+ # ```
57
+ # ActiveRecord::Base.transaction do
58
+ # result = ActsAsSpan::EndDatePropagator.call(obj)
59
+ # if result.errors.present?
60
+ # fail OhNoMyObjetHasErrorsError, "Oh, no! My object has errors!"
61
+ # end
62
+ # end
63
+ # ```
64
+ #
65
+ # Currently only propagates "default" span. The approach to implementing such
66
+ # a feature is ambiguous - would all children have the same span propagated?
67
+ # Would each acts_as_span model need a method to tell which span to
68
+ # propagate to? Once there is a solid use case for using this object on
69
+ # models with multiple spans, that will inform the implementation strategy.
70
+ class EndDatePropagator
71
+ attr_reader :object,
72
+ :errors_cache,
73
+ :skipped_classes,
74
+ :include_errors
75
+
76
+ def initialize(object, errors_cache: [], skipped_classes: [], include_errors: true)
77
+ @object = object
78
+ @errors_cache = errors_cache
79
+ @skipped_classes = skipped_classes
80
+ @include_errors = include_errors
81
+ end
82
+
83
+ # class-level call: enable the usage of ActsAsSpan::EndDatePropagator.call
84
+ def self.call(object, **opts)
85
+ new(object, opts).call
86
+ end
87
+
88
+ def call
89
+ result = propagate
90
+ # only add new errors to the object
91
+ result.errors.each do |error, message|
92
+ unless object.errors[error].include? message
93
+ object.errors.add(error, message: message)
94
+ end
95
+ end
96
+ object
97
+ end
98
+
99
+ private
100
+
101
+ def propagate
102
+ # return if there is nothing to propagate
103
+ return object unless should_propagate_from? object
104
+
105
+ children(object).each do |child|
106
+ # End the record, its children too. And their children, forever, true.
107
+ propagated_child = assign_end_date(child, object.span.end_date)
108
+
109
+ # save child and add errors to cache
110
+ save_with_errors(object, child, propagated_child)
111
+ end
112
+
113
+ if errors_cache.present?
114
+ errors_cache.each do |message|
115
+ skip if object.errors.added?(:base, message)
116
+
117
+ object.errors.add(:base, message: message)
118
+ end
119
+ end
120
+
121
+ # return the object, with any newly-added errors
122
+ object
123
+ end
124
+
125
+ # returns the given child, but possibly with errors
126
+ def assign_end_date(child, new_end_date)
127
+ child.assign_attributes({ child.span.end_field => new_end_date })
128
+ ActsAsSpan::EndDatePropagator.call(
129
+ child,
130
+ errors_cache: errors_cache,
131
+ skipped_classes: skipped_classes,
132
+ )
133
+ end
134
+
135
+ # save the child record, add errors.
136
+ def save_with_errors(object, child, propagated_child)
137
+ if object_has_errors?(propagated_child) && include_errors
138
+ errors_cache << propagation_error_message(object, child)
139
+ end
140
+ child.save
141
+ end
142
+
143
+ def propagation_error_message(object, child)
144
+ I18n.t(
145
+ 'propagation_failure',
146
+ scope: %i[activerecord errors messages end_date_propagator],
147
+ end_date_field_name: child.class.human_attribute_name(
148
+ child.span.end_field,
149
+ ),
150
+ parent: object.model_name.human,
151
+ child: child.model_name.human,
152
+ reason: child.errors.full_messages.join('; '),
153
+ )
154
+ end
155
+
156
+ def object_has_errors?(object)
157
+ !object.valid? ||
158
+ (object.errors.present? && object.errors.messages.values.flatten.any?)
159
+ end
160
+
161
+ # check if the end_date analog is dirtied
162
+ def end_date_changed?(object)
163
+ end_date_field = object.span.end_field.to_s
164
+ object.changed.include? end_date_field
165
+ end
166
+
167
+ def should_propagate_from?(object)
168
+ object.respond_to?(:span) &&
169
+ end_date_changed?(object) &&
170
+ !object.span.end_date.nil?
171
+ end
172
+
173
+ # Use acts_as_span to determine whether a record has an end date
174
+ def should_propagate_to?(klass)
175
+ klass.respond_to?(:span) && @skipped_classes.exclude?(klass)
176
+ end
177
+
178
+ def child_associations(object)
179
+ object.class.reflect_on_all_associations(:has_many).select do |reflection|
180
+ %i[delete destroy].include?(reflection.options[:dependent]) &&
181
+ should_propagate_to?(reflection.klass)
182
+ end
183
+ end
184
+
185
+ def children(object)
186
+ child_objects = child_associations(object).flat_map do |reflection|
187
+ object.send(reflection.name)
188
+ end
189
+
190
+ # skip previously-ended children
191
+ child_objects.reject do |child|
192
+ child.span.end_date && child.span.end_date < object.span.end_date
193
+ end
194
+ end
195
+
196
+ attr_writer :object, :errors_cache
197
+ end
198
+ end