robsharp-extlib 0.9.15

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 (73) hide show
  1. data/.autotest +21 -0
  2. data/.document +5 -0
  3. data/.gitignore +22 -0
  4. data/LICENSE +47 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +28 -0
  7. data/VERSION +1 -0
  8. data/extlib.gemspec +147 -0
  9. data/lib/extlib.rb +50 -0
  10. data/lib/extlib/array.rb +38 -0
  11. data/lib/extlib/assertions.rb +8 -0
  12. data/lib/extlib/blank.rb +89 -0
  13. data/lib/extlib/boolean.rb +11 -0
  14. data/lib/extlib/byte_array.rb +6 -0
  15. data/lib/extlib/class.rb +179 -0
  16. data/lib/extlib/datetime.rb +29 -0
  17. data/lib/extlib/dictionary.rb +433 -0
  18. data/lib/extlib/hash.rb +450 -0
  19. data/lib/extlib/hook.rb +407 -0
  20. data/lib/extlib/inflection.rb +442 -0
  21. data/lib/extlib/lazy_array.rb +453 -0
  22. data/lib/extlib/lazy_module.rb +18 -0
  23. data/lib/extlib/logger.rb +198 -0
  24. data/lib/extlib/mash.rb +157 -0
  25. data/lib/extlib/module.rb +51 -0
  26. data/lib/extlib/nil.rb +5 -0
  27. data/lib/extlib/numeric.rb +5 -0
  28. data/lib/extlib/object.rb +178 -0
  29. data/lib/extlib/object_space.rb +13 -0
  30. data/lib/extlib/pathname.rb +20 -0
  31. data/lib/extlib/pooling.rb +235 -0
  32. data/lib/extlib/rubygems.rb +38 -0
  33. data/lib/extlib/simple_set.rb +66 -0
  34. data/lib/extlib/string.rb +176 -0
  35. data/lib/extlib/struct.rb +17 -0
  36. data/lib/extlib/symbol.rb +21 -0
  37. data/lib/extlib/time.rb +44 -0
  38. data/lib/extlib/try_dup.rb +44 -0
  39. data/lib/extlib/virtual_file.rb +10 -0
  40. data/spec/array_spec.rb +40 -0
  41. data/spec/blank_spec.rb +86 -0
  42. data/spec/byte_array_spec.rb +8 -0
  43. data/spec/class_spec.rb +158 -0
  44. data/spec/datetime_spec.rb +22 -0
  45. data/spec/hash_spec.rb +536 -0
  46. data/spec/hook_spec.rb +1235 -0
  47. data/spec/inflection/plural_spec.rb +565 -0
  48. data/spec/inflection/singular_spec.rb +498 -0
  49. data/spec/inflection_extras_spec.rb +111 -0
  50. data/spec/lazy_array_spec.rb +1961 -0
  51. data/spec/lazy_module_spec.rb +38 -0
  52. data/spec/mash_spec.rb +312 -0
  53. data/spec/module_spec.rb +71 -0
  54. data/spec/object_space_spec.rb +10 -0
  55. data/spec/object_spec.rb +114 -0
  56. data/spec/pooling_spec.rb +511 -0
  57. data/spec/rcov.opts +6 -0
  58. data/spec/simple_set_spec.rb +58 -0
  59. data/spec/spec.opts +4 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/string_spec.rb +222 -0
  62. data/spec/struct_spec.rb +13 -0
  63. data/spec/symbol_spec.rb +9 -0
  64. data/spec/time_spec.rb +31 -0
  65. data/spec/try_call_spec.rb +74 -0
  66. data/spec/try_dup_spec.rb +46 -0
  67. data/spec/virtual_file_spec.rb +22 -0
  68. data/tasks/ci.rake +1 -0
  69. data/tasks/metrics.rake +36 -0
  70. data/tasks/spec.rake +25 -0
  71. data/tasks/yard.rake +9 -0
  72. data/tasks/yardstick.rake +19 -0
  73. metadata +198 -0
@@ -0,0 +1,18 @@
1
+ class LazyModule < Module
2
+ def self.new(&blk)
3
+ # passing no-op block overrides &blk
4
+ m = super{ }
5
+ class << m
6
+ include ClassMethods
7
+ end
8
+ m.lazy_evaluated_body = blk
9
+ m
10
+ end
11
+
12
+ module ClassMethods
13
+ attr_accessor :lazy_evaluated_body
14
+ def included(host)
15
+ host.class_eval(&@lazy_evaluated_body)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,198 @@
1
+ require "time" # httpdate
2
+ # ==== Public Extlib Logger API
3
+ #
4
+ # To replace an existing logger with a new one:
5
+ # Extlib::Logger.set_log(log{String, IO},level{Symbol, String})
6
+ #
7
+ # Available logging levels are
8
+ # Extlib::Logger::{ Fatal, Error, Warn, Info, Debug }
9
+ #
10
+ # Logging via:
11
+ # Extlib.logger.fatal(message<String>,&block)
12
+ # Extlib.logger.error(message<String>,&block)
13
+ # Extlib.logger.warn(message<String>,&block)
14
+ # Extlib.logger.info(message<String>,&block)
15
+ # Extlib.logger.debug(message<String>,&block)
16
+ #
17
+ # Logging with autoflush:
18
+ # Extlib.logger.fatal!(message<String>,&block)
19
+ # Extlib.logger.error!(message<String>,&block)
20
+ # Extlib.logger.warn!(message<String>,&block)
21
+ # Extlib.logger.info!(message<String>,&block)
22
+ # Extlib.logger.debug!(message<String>,&block)
23
+ #
24
+ # Flush the buffer to
25
+ # Extlib.logger.flush
26
+ #
27
+ # Remove the current log object
28
+ # Extlib.logger.close
29
+ #
30
+ # ==== Private Extlib Logger API
31
+ #
32
+ # To initialize the logger you create a new object, proxies to set_log.
33
+ # Extlib::Logger.new(log{String, IO},level{Symbol, String})
34
+ module Extlib
35
+
36
+ class << self
37
+ attr_accessor :logger
38
+ end
39
+
40
+ class Logger
41
+
42
+ attr_accessor :level
43
+ attr_accessor :delimiter
44
+ attr_accessor :auto_flush
45
+ attr_reader :buffer
46
+ attr_reader :log
47
+ attr_reader :init_args
48
+
49
+ # ==== Notes
50
+ # Ruby (standard) logger levels:
51
+ # :fatal:: An unhandleable error that results in a program crash
52
+ # :error:: A handleable error condition
53
+ # :warn:: A warning
54
+ # :info:: generic (useful) information about system operation
55
+ # :debug:: low-level information for developers
56
+ Levels =
57
+ {
58
+ :fatal => 7,
59
+ :error => 6,
60
+ :warn => 4,
61
+ :info => 3,
62
+ :debug => 0
63
+ }
64
+
65
+ private
66
+
67
+ # Readies a log for writing.
68
+ #
69
+ # ==== Parameters
70
+ # log<IO, String>:: Either an IO object or a name of a logfile.
71
+ def initialize_log(log)
72
+ close if @log # be sure that we don't leave open files laying around.
73
+
74
+ if log.respond_to?(:write)
75
+ @log = log
76
+ elsif File.exist?(log)
77
+ @log = open(log, (File::WRONLY | File::APPEND))
78
+ @log.sync = true
79
+ else
80
+ FileUtils.mkdir_p(File.dirname(log)) unless File.directory?(File.dirname(log))
81
+ @log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
82
+ @log.sync = true
83
+ @log.write("#{Time.now.httpdate} #{delimiter} info #{delimiter} Logfile created\n")
84
+ end
85
+ end
86
+
87
+ public
88
+
89
+ # To initialize the logger you create a new object, proxies to set_log.
90
+ #
91
+ # ==== Parameters
92
+ # *args:: Arguments to create the log from. See set_logs for specifics.
93
+ def initialize(*args)
94
+ @init_args = args
95
+ set_log(*args)
96
+ end
97
+
98
+ # Replaces an existing logger with a new one.
99
+ #
100
+ # ==== Parameters
101
+ # log<IO, String>:: Either an IO object or a name of a logfile.
102
+ # log_level<~to_sym>::
103
+ # The log level from, e.g. :fatal or :info. Defaults to :error in the
104
+ # production environment and :debug otherwise.
105
+ # delimiter<String>::
106
+ # Delimiter to use between message sections. Defaults to " ~ ".
107
+ # auto_flush<Boolean>::
108
+ # Whether the log should automatically flush after new messages are
109
+ # added. Defaults to false.
110
+ def set_log(log, log_level = nil, delimiter = " ~ ", auto_flush = false)
111
+ if log_level && Levels[log_level.to_sym]
112
+ @level = Levels[log_level.to_sym]
113
+ else
114
+ @level = Levels[:debug]
115
+ end
116
+ @buffer = []
117
+ @delimiter = delimiter
118
+ @auto_flush = auto_flush
119
+
120
+ initialize_log(log)
121
+ end
122
+
123
+ # Flush the entire buffer to the log object.
124
+ def flush
125
+ return unless @buffer.size > 0
126
+ @log.write(@buffer.slice!(0..-1).join)
127
+ end
128
+
129
+ # Close and remove the current log object.
130
+ def close
131
+ flush
132
+ @log.close if @log.respond_to?(:close) && !@log.tty?
133
+ @log = nil
134
+ end
135
+
136
+ # Appends a message to the log. The methods yield to an optional block and
137
+ # the output of this block will be appended to the message.
138
+ #
139
+ # ==== Parameters
140
+ # string<String>:: The message to be logged. Defaults to nil.
141
+ #
142
+ # ==== Returns
143
+ # String:: The resulting message added to the log file.
144
+ def <<(string = nil)
145
+ message = ""
146
+ message << delimiter
147
+ message << string if string
148
+ message << "\n" unless message[-1] == ?\n
149
+ @buffer << message
150
+ flush if @auto_flush
151
+
152
+ message
153
+ end
154
+ alias :push :<<
155
+
156
+ # Generate the logging methods for Extlib.logger for each log level.
157
+ Levels.each_pair do |name, number|
158
+ class_eval <<-LEVELMETHODS, __FILE__, __LINE__
159
+
160
+ # Appends a message to the log if the log level is at least as high as
161
+ # the log level of the logger.
162
+ #
163
+ # ==== Parameters
164
+ # string<String>:: The message to be logged. Defaults to nil.
165
+ #
166
+ # ==== Returns
167
+ # self:: The logger object for chaining.
168
+ def #{name}(message = nil)
169
+ self << message if #{number} >= level
170
+ self
171
+ end
172
+
173
+ # Appends a message to the log if the log level is at least as high as
174
+ # the log level of the logger. The bang! version of the method also auto
175
+ # flushes the log buffer to disk.
176
+ #
177
+ # ==== Parameters
178
+ # string<String>:: The message to be logged. Defaults to nil.
179
+ #
180
+ # ==== Returns
181
+ # self:: The logger object for chaining.
182
+ def #{name}!(message = nil)
183
+ self << message if #{number} >= level
184
+ flush if #{number} >= level
185
+ self
186
+ end
187
+
188
+ # ==== Returns
189
+ # Boolean:: True if this level will be logged by this logger.
190
+ def #{name}?
191
+ #{number} >= level
192
+ end
193
+ LEVELMETHODS
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -0,0 +1,157 @@
1
+ require 'extlib/hash'
2
+
3
+ # This class has dubious semantics and we only have it so that people can write
4
+ # params[:key] instead of params['key'].
5
+ class Mash < Hash
6
+
7
+ # @param constructor<Object>
8
+ # The default value for the mash. Defaults to an empty hash.
9
+ #
10
+ # @details [Alternatives]
11
+ # If constructor is a Hash, a new mash will be created based on the keys of
12
+ # the hash and no default value will be set.
13
+ def initialize(constructor = {})
14
+ if constructor.is_a?(Hash)
15
+ super()
16
+ update(constructor)
17
+ else
18
+ super(constructor)
19
+ end
20
+ end
21
+
22
+ # @param key<Object> The default value for the mash. Defaults to nil.
23
+ #
24
+ # @details [Alternatives]
25
+ # If key is a Symbol and it is a key in the mash, then the default value will
26
+ # be set to the value matching the key.
27
+ def default(key = nil)
28
+ if key.is_a?(Symbol) && include?(key = key.to_s)
29
+ self[key]
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
36
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
37
+
38
+ # @param key<Object> The key to set.
39
+ # @param value<Object>
40
+ # The value to set the key to.
41
+ #
42
+ # @see Mash#convert_key
43
+ # @see Mash#convert_value
44
+ def []=(key, value)
45
+ regular_writer(convert_key(key), convert_value(value))
46
+ end
47
+
48
+ # @param other_hash<Hash>
49
+ # A hash to update values in the mash with. The keys and the values will be
50
+ # converted to Mash format.
51
+ #
52
+ # @return [Mash] The updated mash.
53
+ def update(other_hash)
54
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
55
+ self
56
+ end
57
+
58
+ alias_method :merge!, :update
59
+
60
+ # @param key<Object> The key to check for. This will be run through convert_key.
61
+ #
62
+ # @return [Boolean] True if the key exists in the mash.
63
+ def key?(key)
64
+ super(convert_key(key))
65
+ end
66
+
67
+ # def include? def has_key? def member?
68
+ alias_method :include?, :key?
69
+ alias_method :has_key?, :key?
70
+ alias_method :member?, :key?
71
+
72
+ # @param key<Object> The key to fetch. This will be run through convert_key.
73
+ # @param *extras<Array> Default value.
74
+ #
75
+ # @return [Object] The value at key or the default value.
76
+ def fetch(key, *extras)
77
+ super(convert_key(key), *extras)
78
+ end
79
+
80
+ # @param *indices<Array>
81
+ # The keys to retrieve values for. These will be run through +convert_key+.
82
+ #
83
+ # @return [Array] The values at each of the provided keys
84
+ def values_at(*indices)
85
+ indices.collect {|key| self[convert_key(key)]}
86
+ end
87
+
88
+ # @param hash<Hash> The hash to merge with the mash.
89
+ #
90
+ # @return [Mash] A new mash with the hash values merged in.
91
+ def merge(hash)
92
+ self.dup.update(hash)
93
+ end
94
+
95
+ # @param key<Object>
96
+ # The key to delete from the mash.\
97
+ def delete(key)
98
+ super(convert_key(key))
99
+ end
100
+
101
+ # @param *rejected<Array[(String, Symbol)] The mash keys to exclude.
102
+ #
103
+ # @return [Mash] A new mash without the selected keys.
104
+ #
105
+ # @example
106
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
107
+ # #=> { "two" => 2, "three" => 3 }
108
+ def except(*keys)
109
+ super(*keys.map {|k| convert_key(k)})
110
+ end
111
+
112
+ # Used to provide the same interface as Hash.
113
+ #
114
+ # @return [Mash] This mash unchanged.
115
+ def stringify_keys!; self end
116
+
117
+ # @return [Hash] The mash as a Hash with symbolized keys.
118
+ def symbolize_keys
119
+ h = Hash.new(default)
120
+ each { |key, val| h[key.to_sym] = val }
121
+ h
122
+ end
123
+
124
+ # @return [Hash] The mash as a Hash with string keys.
125
+ def to_hash
126
+ Hash.new(default).merge(self)
127
+ end
128
+
129
+ protected
130
+ # @param key<Object> The key to convert.
131
+ #
132
+ # @param [Object]
133
+ # The converted key. If the key was a symbol, it will be converted to a
134
+ # string.
135
+ #
136
+ # @api private
137
+ def convert_key(key)
138
+ key.kind_of?(Symbol) ? key.to_s : key
139
+ end
140
+
141
+ # @param value<Object> The value to convert.
142
+ #
143
+ # @return [Object]
144
+ # The converted value. A Hash or an Array of hashes, will be converted to
145
+ # their Mash equivalents.
146
+ #
147
+ # @api private
148
+ def convert_value(value)
149
+ if value.class == Hash
150
+ value.to_mash
151
+ elsif value.is_a?(Array)
152
+ value.collect { |e| convert_value(e) }
153
+ else
154
+ value
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,51 @@
1
+ require 'extlib/object'
2
+ require 'extlib/class'
3
+ require 'extlib/blank'
4
+
5
+ class Module
6
+ def find_const(const_name)
7
+ if const_name[0..1] == '::'
8
+ Object.full_const_get(const_name[2..-1])
9
+ else
10
+ nested_const_lookup(const_name)
11
+ end
12
+ end
13
+
14
+ def try_dup
15
+ self
16
+ end
17
+
18
+ private
19
+
20
+ # Doesn't do any caching since constants can change with remove_const
21
+ def nested_const_lookup(const_name)
22
+ unless equal?(Object)
23
+ constants = []
24
+
25
+ name.split('::').each do |part|
26
+ const = constants.last || Object
27
+ constants << const.const_get(part)
28
+ end
29
+
30
+ parts = const_name.split('::')
31
+
32
+ # from most to least specific constant, use each as a base and try
33
+ # to find a constant with the name const_name within them
34
+ constants.reverse_each do |const|
35
+ # return the nested constant if available
36
+ return const if parts.all? do |part|
37
+ const = if RUBY_VERSION >= '1.9.0'
38
+ const.const_defined?(part, false) ? const.const_get(part, false) : nil
39
+ else
40
+ const.const_defined?(part) ? const.const_get(part) : nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # no relative constant found, fallback to an absolute lookup and
47
+ # use const_missing if not found
48
+ Object.full_const_get(const_name)
49
+ end
50
+
51
+ end # class Module
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def try_dup
3
+ self
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Numeric
2
+ def try_dup
3
+ self
4
+ end
5
+ end
@@ -0,0 +1,178 @@
1
+ require 'set'
2
+ require 'extlib/blank'
3
+
4
+ class Object
5
+ # Extracts the singleton class, so that metaprogramming can be done on it.
6
+ #
7
+ # @return [Class] The meta class.
8
+ #
9
+ # @example [Setup]
10
+ # class MyString < String; end
11
+ #
12
+ # MyString.instance_eval do
13
+ # define_method :foo do
14
+ # puts self
15
+ # end
16
+ # end
17
+ #
18
+ # MyString.meta_class.instance_eval do
19
+ # define_method :bar do
20
+ # puts self
21
+ # end
22
+ # end
23
+ #
24
+ # def String.add_meta_var(var)
25
+ # self.meta_class.instance_eval do
26
+ # define_method var do
27
+ # puts "HELLO"
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # @example
33
+ # MyString.new("Hello").foo #=> "Hello"
34
+ # @example
35
+ # MyString.new("Hello").bar
36
+ # #=> NoMethodError: undefined method `bar' for "Hello":MyString
37
+ # @example
38
+ # MyString.foo
39
+ # #=> NoMethodError: undefined method `foo' for MyString:Class
40
+ # @example
41
+ # MyString.bar
42
+ # #=> MyString
43
+ # @example
44
+ # String.bar
45
+ # #=> NoMethodError: undefined method `bar' for String:Class
46
+ # @example
47
+ # MyString.add_meta_var(:x)
48
+ # MyString.x #=> HELLO
49
+ #
50
+ # @details [Description of Examples]
51
+ # As you can see, using #meta_class allows you to execute code (and here,
52
+ # define a method) on the metaclass itself. It also allows you to define
53
+ # class methods that can be run on subclasses, and then be able to execute
54
+ # code on the metaclass of the subclass (here MyString).
55
+ #
56
+ # In this case, we were able to define a class method (add_meta_var) on
57
+ # String that was executable by the MyString subclass. It was then able to
58
+ # define a method on the subclass by adding it to the MyString metaclass.
59
+ #
60
+ # For more information, you can check out _why's excellent article at:
61
+ # http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
62
+ def meta_class() class << self; self end end
63
+
64
+ # @param name<String> The name of the constant to get, e.g. "Merb::Router".
65
+ #
66
+ # @return [Object] The constant corresponding to the name.
67
+ def full_const_get(name)
68
+ list = name.split("::")
69
+ list.shift if list.first.blank?
70
+ obj = self
71
+ list.each do |x|
72
+ # This is required because const_get tries to look for constants in the
73
+ # ancestor chain, but we only want constants that are HERE
74
+ obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
75
+ end
76
+ obj
77
+ end
78
+
79
+ # @param name<String> The name of the constant to get, e.g. "Merb::Router".
80
+ # @param value<Object> The value to assign to the constant.
81
+ #
82
+ # @return [Object] The constant corresponding to the name.
83
+ def full_const_set(name, value)
84
+ list = name.split("::")
85
+ toplevel = list.first.blank?
86
+ list.shift if toplevel
87
+ last = list.pop
88
+ obj = list.empty? ? Object : Object.full_const_get(list.join("::"))
89
+ obj.const_set(last, value) if obj && !obj.const_defined?(last)
90
+ end
91
+
92
+ # Defines module from a string name (e.g. Foo::Bar::Baz)
93
+ # If module already exists, no exception raised.
94
+ #
95
+ # @param name<String> The name of the full module name to make
96
+ #
97
+ # @return [nil]
98
+ def make_module(str)
99
+ mod = str.split("::")
100
+ current_module = self
101
+ mod.each do |x|
102
+ unless current_module.const_defined?(x)
103
+ current_module.class_eval "module #{x}; end", __FILE__, __LINE__
104
+ end
105
+ current_module = current_module.const_get(x)
106
+ end
107
+ current_module
108
+ end
109
+
110
+ # @param duck<Symbol, Class, Array> The thing to compare the object to.
111
+ #
112
+ # @note
113
+ # The behavior of the method depends on the type of duck as follows:
114
+ # Symbol:: Check whether the object respond_to?(duck).
115
+ # Class:: Check whether the object is_a?(duck).
116
+ # Array::
117
+ # Check whether the object quacks_like? at least one of the options in the
118
+ # array.
119
+ #
120
+ # @return [Boolean]
121
+ # True if the object quacks like duck.
122
+ def quacks_like?(duck)
123
+ case duck
124
+ when Symbol
125
+ self.respond_to?(duck)
126
+ when Class
127
+ self.is_a?(duck)
128
+ when Array
129
+ duck.any? {|d| self.quacks_like?(d) }
130
+ else
131
+ false
132
+ end
133
+ end
134
+
135
+ # Override this in a child if it cannot be dup'ed
136
+ #
137
+ # @return [Object]
138
+ def try_dup
139
+ self.dup
140
+ end
141
+
142
+ # If receiver is callable, calls it and
143
+ # returns result. If not, just returns receiver
144
+ # itself
145
+ #
146
+ # @return [Object]
147
+ def try_call(*args)
148
+ if self.respond_to?(:call)
149
+ self.call(*args)
150
+ else
151
+ self
152
+ end
153
+ end
154
+
155
+ # @param arrayish<#include?> Container to check, to see if it includes the object.
156
+ # @param *more<Array>:: additional args, will be flattened into arrayish
157
+ #
158
+ # @return [Boolean]
159
+ # True if the object is included in arrayish (+ more)
160
+ #
161
+ # @example 1.in?([1,2,3]) #=> true
162
+ # @example 1.in?(1,2,3) #=> true
163
+ def in?(arrayish,*more)
164
+ arrayish = more.unshift(arrayish) unless more.empty?
165
+ arrayish.include?(self)
166
+ end
167
+
168
+ # Add instance_variable_defined? for backward compatibility
169
+ # @param variable<Symbol, String>
170
+ #
171
+ # @return [Boolean]
172
+ # True if the object has the given instance variable defined
173
+ unless respond_to?(:instance_variable_defined?)
174
+ def instance_variable_defined?(variable)
175
+ instance_variables.include?(variable.to_s)
176
+ end
177
+ end
178
+ end