bit_magic 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coco.yml +4 -0
- data/.gitignore +10 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +290 -0
- data/Rakefile +39 -0
- data/TODO.md +25 -0
- data/bin/console +14 -0
- data/bin/setup +9 -0
- data/bit_magic.gemspec +30 -0
- data/lib/bit_magic.rb +9 -0
- data/lib/bit_magic/adapters/active_record_adapter.rb +250 -0
- data/lib/bit_magic/adapters/base.rb +61 -0
- data/lib/bit_magic/adapters/magician.rb +226 -0
- data/lib/bit_magic/adapters/mongoid_adapter.rb +233 -0
- data/lib/bit_magic/bit_field.rb +103 -0
- data/lib/bit_magic/bits.rb +285 -0
- data/lib/bit_magic/bits_generator.rb +399 -0
- data/lib/bit_magic/error.rb +7 -0
- data/lib/bit_magic/railtie.rb +28 -0
- data/lib/bit_magic/version.rb +7 -0
- metadata +129 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
module BitMagic
|
2
|
+
module Adapters
|
3
|
+
# A container for bit_magic settings and fields
|
4
|
+
# Meant to be used internally by the Base adapter (and by extension, all adapters).
|
5
|
+
#
|
6
|
+
# @attr_reader [Hash] field_options bit_magic options that define fields
|
7
|
+
# @attr_reader [Hash] action_options bit_magic options that define settings
|
8
|
+
# @attr_reader [Hash] field_list name => bits key pairs based off field_options
|
9
|
+
# @attr_reader [Integer] bits_length total number of bits defined in field_options
|
10
|
+
# @attr_reader [Integer] max_bit the largest bit defined in field_options
|
11
|
+
# @attr_reader [Symbol] bit_magic_name the name given to bit_magic
|
12
|
+
class Magician
|
13
|
+
|
14
|
+
DEFAULT_ACTION_OPTIONS = Bits::DEFAULT_OPTIONS.merge({
|
15
|
+
:query_by_value => true, # can be true, false, or a number of bits
|
16
|
+
:helpers => true, # TODO: enable deeper access control over injected helper methods
|
17
|
+
:bits_wrapper => Bits,
|
18
|
+
:bits_generator => BitsGenerator,
|
19
|
+
}).freeze
|
20
|
+
|
21
|
+
# Checks if the key is a a field key definition (Integer or Range, defining
|
22
|
+
# bit or bit ranges in question).
|
23
|
+
#
|
24
|
+
# @param [Integer, Range<Integer>] checkValue an integer bit or bit range
|
25
|
+
# to check if it is a valid bit index
|
26
|
+
#
|
27
|
+
# @return [Integer, Array<Integer>] will return an integer or an array of
|
28
|
+
# integers if the checkValue is a valid bit index or bit range
|
29
|
+
def self.field_key_value_check(check)
|
30
|
+
if check.is_a?(Integer)
|
31
|
+
check
|
32
|
+
elsif check.is_a?(Range)
|
33
|
+
list = check.to_a
|
34
|
+
valid = list.reduce(true) {|m, i| m && i.is_a?(Integer) }
|
35
|
+
list if valid and list.length > 0
|
36
|
+
elsif check.is_a?(Array)
|
37
|
+
valid = true
|
38
|
+
list = check.collect {|i|
|
39
|
+
key = self.field_key_value_check(i)
|
40
|
+
key.nil? ? (valid = false; break;) : key
|
41
|
+
}.flatten
|
42
|
+
list if valid and list.length > 0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Separate field and action options from the options passed to bit_magic
|
47
|
+
#
|
48
|
+
# Valid keys for field options are:
|
49
|
+
# Integer - for a specific bit position
|
50
|
+
# Range - for a range of bits
|
51
|
+
# Array<Integer> - an array of bit positions
|
52
|
+
# The value of the key-pair should be a Symbol.
|
53
|
+
#
|
54
|
+
# Action options are everything else that's not a field option
|
55
|
+
#
|
56
|
+
# @param [Hash] options the options passed to bit_magic
|
57
|
+
#
|
58
|
+
# @return [Hash, Hash] returns an array of two options [field, actions]
|
59
|
+
# field options and action options, in that order respectively
|
60
|
+
def self.options_splitter(options = {})
|
61
|
+
field_opts = {}
|
62
|
+
action_opts = DEFAULT_ACTION_OPTIONS.dup
|
63
|
+
|
64
|
+
options.each_pair do |check_key, value|
|
65
|
+
if key = self.field_key_value_check(check_key)
|
66
|
+
field_opts[key] = value
|
67
|
+
else
|
68
|
+
if (!options[:allow_failed_fields]) and (check_key.is_a?(Integer) or check_key.is_a?(Range) or check_key.is_a?(Array))
|
69
|
+
raise BitMagic::FieldError.new("key-pair expected to be a valid field option, but it is not: #{check_key.inspect} => #{value.inspect}. If this is an action option, you can disable this error by passing ':allow_failed_fields => true' as an option")
|
70
|
+
end
|
71
|
+
action_opts[check_key] = value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
[field_opts.freeze, action_opts.freeze]
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :field_options, :action_options
|
79
|
+
attr_reader :field_list
|
80
|
+
attr_reader :bits_length, :max_bit
|
81
|
+
attr_reader :bit_magic_name
|
82
|
+
|
83
|
+
# Initialize a new Magician, a container for bit_magic settings and fields
|
84
|
+
#
|
85
|
+
# @param [Symbol] name the name to be used as a namespace
|
86
|
+
# @param [Hash] options the options given to bit_magic. Keys that are
|
87
|
+
# Integer, Arrays or Range objects are treated as bit allocations, their
|
88
|
+
# value should be the name of the bit field. Keys that are anything else
|
89
|
+
# (usually Symbol) are treated as action options or settings.
|
90
|
+
#
|
91
|
+
# @example Initialize a Magician (usually you would not do this directly)
|
92
|
+
# magician = Magician.new(:example, 0 => :is_odd, [1, 2] => :eyes, 3..6 => :fingers, default: 7)
|
93
|
+
# # here, field names are :is_odd, :eyes, and :fingers
|
94
|
+
# # with bits indices 0, [1, 2], and [3, 4, 5, 6] respectively
|
95
|
+
# # default is an action option, set to 7
|
96
|
+
#
|
97
|
+
# @return a Magician
|
98
|
+
def initialize(name, options = {})
|
99
|
+
@bit_magic_name = name
|
100
|
+
@field_options, @action_options = self.class.options_splitter(options)
|
101
|
+
validate_field_options!
|
102
|
+
end
|
103
|
+
|
104
|
+
# Define helper methods on the instance, namespaced to the name given in
|
105
|
+
# the bit_magic invocation.
|
106
|
+
#
|
107
|
+
# This is an internal method, only meant to be used by the Base adapter to
|
108
|
+
# during its setup phase. Should not be used directly.
|
109
|
+
#
|
110
|
+
# Methods that are defined is namespaced to the name during initialization.
|
111
|
+
# Referred to here as NAMESPACE. These methods are available on instances.
|
112
|
+
#
|
113
|
+
# NAMESPACE - defined by Base adapter, returns a Bits wrapper
|
114
|
+
# NAMESPACE_enabled?(*field_names) - checks if all the given field names
|
115
|
+
# or bit indices are enabled (value > 0)
|
116
|
+
# NAMESPACE_disabled?(*field_names) - checks if all the given field names
|
117
|
+
# or bit indices are disabled (value == 0)
|
118
|
+
#
|
119
|
+
# The following are helpers, defined based on field names during initialization
|
120
|
+
# Refered to here as 'name'.
|
121
|
+
#
|
122
|
+
# name - returns the value of the field
|
123
|
+
# name=(new_value) - sets the value of the field to the new value
|
124
|
+
# name? - available only if the field is a single bit, returns true if
|
125
|
+
# the value of the bit is 1, or false if 0
|
126
|
+
#
|
127
|
+
#
|
128
|
+
# @param [Class] klass the class to inject methods into.
|
129
|
+
#
|
130
|
+
# @return nothing useful.
|
131
|
+
def define_bit_magic_methods(klass)
|
132
|
+
names = @field_list
|
133
|
+
bit_magic_name = @bit_magic_name
|
134
|
+
|
135
|
+
klass.instance_eval do
|
136
|
+
|
137
|
+
define_method(:"#{bit_magic_name}_enabled?") do |*fields|
|
138
|
+
self.send(bit_magic_name).enabled?(*fields)
|
139
|
+
end
|
140
|
+
|
141
|
+
define_method(:"#{bit_magic_name}_disabled?") do |*fields|
|
142
|
+
self.send(bit_magic_name).disabled?(*fields)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
if @action_options[:helpers]
|
147
|
+
|
148
|
+
klass.instance_eval do
|
149
|
+
names.each_pair do |name, bits|
|
150
|
+
|
151
|
+
define_method(:"#{name}") do
|
152
|
+
self.send(bit_magic_name)[name]
|
153
|
+
end
|
154
|
+
|
155
|
+
define_method(:"#{name}=") do |val|
|
156
|
+
self.send(bit_magic_name)[name] = val
|
157
|
+
end
|
158
|
+
|
159
|
+
if bits.is_a?(Integer) or bits.length == 1
|
160
|
+
define_method(:"#{name}?") do
|
161
|
+
self.send(bit_magic_name)[name] == 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
# List of bits defined in @field_options
|
173
|
+
#
|
174
|
+
# @return [Array<Integer>] an array of bits defined in @field_options
|
175
|
+
def bits
|
176
|
+
@field_list.values.flatten
|
177
|
+
end
|
178
|
+
|
179
|
+
# Used by the Base#bit_magic to create a Bits wrapper around instances
|
180
|
+
#
|
181
|
+
# @return [Class] a Bits class
|
182
|
+
def bits_wrapper
|
183
|
+
self.action_options[:bits_wrapper] || Bits
|
184
|
+
end
|
185
|
+
|
186
|
+
# Used by adapters to generate value lists for particular bit operations
|
187
|
+
#
|
188
|
+
# @return [BitsGenerator] a BitsGenerator object with this magician's field
|
189
|
+
# list
|
190
|
+
def bits_generator
|
191
|
+
@bits_generator ||= (self.action_options[:bits_generator] || BitsGenerator).new self
|
192
|
+
end
|
193
|
+
|
194
|
+
# Inspect this object. This is customized just to shorten the output to
|
195
|
+
# actually be readable.
|
196
|
+
def inspect
|
197
|
+
"#<#{self.class.to_s} name=#{@bit_magic_name} field_list=#{@field_list.inspect}>"
|
198
|
+
end
|
199
|
+
|
200
|
+
protected
|
201
|
+
# Internal use only. Sets @field_list @bits_length and @max_bit from @field_options
|
202
|
+
def validate_field_options!
|
203
|
+
@field_list = {}
|
204
|
+
|
205
|
+
@field_options.each_pair do |bits, name|
|
206
|
+
name = name.to_sym if name.is_a?(String)
|
207
|
+
|
208
|
+
if name.is_a?(Symbol)
|
209
|
+
raise FieldError.new("'#{name}' defined more than once") if @field_list.has_key?(name)
|
210
|
+
@field_list[name] = bits
|
211
|
+
else
|
212
|
+
raise FieldError.new("field name must be a symbol or string, #{name.inspect} is not")
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
bits = self.bits
|
218
|
+
@bits_length = bits.uniq.length
|
219
|
+
@max_bit = bits.max
|
220
|
+
@field_list.freeze
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require_relative './base'
|
2
|
+
|
3
|
+
module BitMagic
|
4
|
+
module Adapters
|
5
|
+
# This is the adapter for Mongoid. It's implemented as a concern to be
|
6
|
+
# included inside Mongoid::Document models.
|
7
|
+
#
|
8
|
+
# It's expected that you have an integer column (default name 'flags',
|
9
|
+
# override using the attribute_name option). It's suggested, though not
|
10
|
+
# required, that you set the default value same as the bit_magic default.
|
11
|
+
#
|
12
|
+
# If you have more than one model that you want to use BitMagic in, it's
|
13
|
+
# recommended that you just include this adapter globally:
|
14
|
+
# require 'bit_magic/adapters/mongoid_adapter'
|
15
|
+
# Mongoid::Document.include BitMagic::Adapters::MongoidAdapter
|
16
|
+
#
|
17
|
+
# Otherwise, you can include it on a per model basis before calling bit_magic
|
18
|
+
#
|
19
|
+
# class Example
|
20
|
+
# include Mongoid::Document
|
21
|
+
# # this line below can be excluded if you included the adapter globally
|
22
|
+
# include BitMagic::Adapters::MongoidAdapter
|
23
|
+
#
|
24
|
+
# bit_magic :settings, 0 => :is_odd, [1, 2, 3] => :amount, 4 => :is_cool
|
25
|
+
# field :flags, type: Integer, default: 0
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# After that, you can start using query helpers and instance helpers.
|
29
|
+
# Query helpers return a standard Mongoid::Criteria, so you can do everything
|
30
|
+
# you normally would in a query (like chaining additional conditions).
|
31
|
+
# Instance helpers are wrapped around by a Bits object, in this case, 'settings'
|
32
|
+
# but also have helper methods added based on the name of the fields.
|
33
|
+
#
|
34
|
+
module MongoidAdapter
|
35
|
+
VERSION = "0.1.0".freeze
|
36
|
+
extend ActiveSupport::Concern
|
37
|
+
|
38
|
+
included do
|
39
|
+
self.extend Base
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Cast the given value into a Boolean, follows Mongoid::Boolean rules
|
44
|
+
BIT_MAGIC_BOOLEAN_CASTER = lambda do |val|
|
45
|
+
!!Mongoid::Boolean.mongoize(val)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adapter options specific to this adapter
|
49
|
+
#
|
50
|
+
# :named_scopes Enables (true) or disables (false) individual scopes to
|
51
|
+
# query fields
|
52
|
+
#
|
53
|
+
# :query_by_value whether to use bitwise operations or IN (?) when querying
|
54
|
+
# by default will use IN (?) if the total bits defined by bit_magic is
|
55
|
+
# less than or equal to 8. true to always query by value, false to always
|
56
|
+
# query using bitwise operations
|
57
|
+
BIT_MAGIC_ADAPTER_DEFAULTS = {
|
58
|
+
:bool_caster => BIT_MAGIC_BOOLEAN_CASTER,
|
59
|
+
:named_scopes => true,
|
60
|
+
:query_by_value => 8
|
61
|
+
}.freeze
|
62
|
+
|
63
|
+
# Method used to set adapter defaults as options to Magician,
|
64
|
+
# Used by the bit_magic definition to add custom options to the magician
|
65
|
+
#
|
66
|
+
# @param [Hash] options some options list
|
67
|
+
#
|
68
|
+
# @return new options list including our custom defaults
|
69
|
+
def bit_magic_adapter_defaults(options)
|
70
|
+
BIT_MAGIC_ADAPTER_DEFAULTS.merge(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# This method is called by Base#bit_magic after setting up the magician
|
74
|
+
# Here, we inject query helpers, scopes, and other useful methods
|
75
|
+
#
|
76
|
+
# Query helpers: (NAMESPACE is the name given to bit_magic)
|
77
|
+
# All the methods that generate where queries take an optional options
|
78
|
+
# hash as the last value. Can be used to alter options given to bit_magic.
|
79
|
+
# eg: passing '{query_by_value: false}' as the last argument will force
|
80
|
+
# the query to generate bitwise operations instead of '$in => []' queries
|
81
|
+
#
|
82
|
+
# NAMESPACE_query_helper(field_names = nil)
|
83
|
+
# an internal method used by other query helpers
|
84
|
+
# NAMESPACE_where_in(array, column_name = nil)
|
85
|
+
# generates a 'column_name => {:$in => [...]}' query for the array numbers
|
86
|
+
# column_name defaults to attribute_name in the options
|
87
|
+
# NAMESPACE_with_all(*field_names, options = {})
|
88
|
+
# takes one or more field names, and queries for values where ALL of
|
89
|
+
# them are enabled. For fields with multiple bits, they must be max value
|
90
|
+
# This is the equivalent of: field[0] and field[1] and field[2] ...
|
91
|
+
# NAMESPACE_with_any(*field_names, options = {})
|
92
|
+
# takes one or more field names, and queries for values where any of
|
93
|
+
# them are enabled
|
94
|
+
# This is the equivalent of: field[0] or field[1] or field[2] ...
|
95
|
+
# NAMESPACE_without_any(*field_names, options = {})
|
96
|
+
# takes one or more field names, and queries for values where at least
|
97
|
+
# one of them is disabled. For fields with multiple bits, any value
|
98
|
+
# other than maximum number
|
99
|
+
# This is the equivalent of "!field[0] or !field[1] or !field[2] ..."
|
100
|
+
# NAMESPACE_without_all(*field_names, options = {})
|
101
|
+
# takes one or more field names and queries for values where none of
|
102
|
+
# them are enabled (all disabled). For fields with multiple bits,
|
103
|
+
# value must be zero.
|
104
|
+
# This is the equivalent of: !field[0] and !field[1] and !field[2] ...
|
105
|
+
# NAMESPACE_equals(field_value_list, options = {})
|
106
|
+
# * this will truncate values to match the number of bits available
|
107
|
+
# field_value_list is a Hash with field_name => value key-pairs.
|
108
|
+
# generates a query that matches the bits to the value, exactly
|
109
|
+
# This is the equivalent of: field[0] = val and field[1] = value ...
|
110
|
+
#
|
111
|
+
# Additional named scopes
|
112
|
+
# These can be disabled by passing 'named_scopes: false' as an option
|
113
|
+
# FIELD is the field name for the bit/bit range
|
114
|
+
#
|
115
|
+
# NAMESPACE_FIELD
|
116
|
+
# queries for values where FIELD has been enabled
|
117
|
+
# NAMESPACE_not_FIELD
|
118
|
+
# queries for values where FIELD has been disabled (not enabled)
|
119
|
+
# NAMESPACE_FIELD_equals(value)
|
120
|
+
# * only exists for fields with more than one bit
|
121
|
+
# queries for values where FIELD is exactly equal to value
|
122
|
+
#
|
123
|
+
# @param [Symbol] name the namespace (prefix) for our query helpers
|
124
|
+
#
|
125
|
+
# @return nothing important
|
126
|
+
def bit_magic_adapter(name)
|
127
|
+
query_prep = :"#{name}_query_helper"
|
128
|
+
query_in = :"#{name}_where_in"
|
129
|
+
|
130
|
+
self.class_eval do
|
131
|
+
define_singleton_method(query_prep) do |field_names = nil|
|
132
|
+
magician = @bit_magic_fields[name]
|
133
|
+
bit_gen = magician.bits_generator
|
134
|
+
|
135
|
+
options = (field_names.is_a?(Array) and field_names.last.is_a?(Hash)) ? field_names.pop : {}
|
136
|
+
|
137
|
+
by_value = options.key?(:query_by_value) ? options[:query_by_value] : magician.action_options[:query_by_value]
|
138
|
+
|
139
|
+
by_value = (magician.bits_length <= by_value) if by_value.is_a?(Integer)
|
140
|
+
column_name = options[:column_name] || magician.action_options[:column_name] || magician.action_options[:attribute_name]
|
141
|
+
|
142
|
+
[magician, bit_gen, by_value, column_name]
|
143
|
+
end
|
144
|
+
|
145
|
+
define_singleton_method(query_in) do |column_name, arr|
|
146
|
+
where(column_name => {:$in => arr})
|
147
|
+
end
|
148
|
+
|
149
|
+
define_singleton_method(:"#{name}_with_all") do |*field_names|
|
150
|
+
magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
|
151
|
+
|
152
|
+
if by_value === true
|
153
|
+
self.send(query_in, column_name, bit_gen.all_of(*field_names))
|
154
|
+
else
|
155
|
+
where(column_name => {:$bitsAllSet => bit_gen.all_of_number(*field_names)})
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
define_singleton_method(:"#{name}_without_any") do |*field_names|
|
160
|
+
magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
|
161
|
+
|
162
|
+
if by_value === true
|
163
|
+
self.send(query_in, column_name, bit_gen.instead_of(*field_names))
|
164
|
+
else
|
165
|
+
where(column_name => {:$bitsAnyClear => bit_gen.any_of_number(*field_names)})
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
define_singleton_method(:"#{name}_without_all") do |*field_names|
|
170
|
+
magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
|
171
|
+
|
172
|
+
if by_value === true
|
173
|
+
self.send(query_in, column_name, bit_gen.none_of(*field_names))
|
174
|
+
else
|
175
|
+
where(column_name => {:$bitsAllClear => bit_gen.any_of_number(*field_names)})
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Query for if any of these bits are set.
|
180
|
+
define_singleton_method(:"#{name}_with_any") do |*field_names|
|
181
|
+
magician, bit_gen, by_value, column_name = self.send(query_prep, field_names)
|
182
|
+
|
183
|
+
if by_value === true
|
184
|
+
self.send(query_in, column_name, bit_gen.any_of(*field_names))
|
185
|
+
else
|
186
|
+
where(column_name => {:$bitsAnySet => bit_gen.any_of_number(*field_names)})
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
define_singleton_method(:"#{name}_equals") do |field_value, options = {}|
|
191
|
+
magician, bit_gen, by_value, column_name = self.send(query_prep, [options])
|
192
|
+
|
193
|
+
if by_value === true
|
194
|
+
self.send(query_in, column_name, bit_gen.equal_to(field_value))
|
195
|
+
else
|
196
|
+
all_num, none_num = bit_gen.equal_to_numbers(field_value)
|
197
|
+
where(column_name => {:$bitsAllSet => all_num, :$bitsAllClear => none_num})
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
if @bit_magic_fields and @bit_magic_fields[name] and @bit_magic_fields[name].action_options[:named_scopes]
|
205
|
+
fields = @bit_magic_fields[name].field_list
|
206
|
+
|
207
|
+
self.class_eval do
|
208
|
+
fields.each_pair do |field, value|
|
209
|
+
define_singleton_method(:"#{name}_#{field}") do
|
210
|
+
self.send(:"#{name}_with_all", field)
|
211
|
+
end
|
212
|
+
|
213
|
+
define_singleton_method(:"#{name}_not_#{field}") do
|
214
|
+
self.send(:"#{name}_without_all", field)
|
215
|
+
end
|
216
|
+
|
217
|
+
if value.is_a?(Array) and value.length > 1
|
218
|
+
define_singleton_method(:"#{name}_#{field}_equals") do |val|
|
219
|
+
self.send(:"#{name}_equals", field => val)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|