corefoundation 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +8 -0
- data/README.md +39 -0
- data/lib/corefoundation.rb +11 -0
- data/lib/corefoundation/array.rb +123 -0
- data/lib/corefoundation/base.rb +197 -0
- data/lib/corefoundation/boolean.rb +25 -0
- data/lib/corefoundation/data.rb +38 -0
- data/lib/corefoundation/date.rb +32 -0
- data/lib/corefoundation/dictionary.rb +114 -0
- data/lib/corefoundation/extensions.rb +81 -0
- data/lib/corefoundation/null.rb +11 -0
- data/lib/corefoundation/number.rb +98 -0
- data/lib/corefoundation/string.rb +77 -0
- data/lib/corefoundation/version.rb +4 -0
- data/spec/array_spec.rb +92 -0
- data/spec/boolean_spec.rb +24 -0
- data/spec/data_spec.rb +26 -0
- data/spec/date_spec.rb +25 -0
- data/spec/dictionary_spec.rb +81 -0
- data/spec/extensions_spec.rb +63 -0
- data/spec/null_spec.rb +7 -0
- data/spec/number_spec.rb +52 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/string_spec.rb +39 -0
- metadata +150 -0
data/LICENSE
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
The MIT License
|
2
|
+
Copyright (c) 2012 Frederick Cheung
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
5
|
+
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
7
|
+
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
CoreFoundation
|
3
|
+
==============
|
4
|
+
|
5
|
+
FFI based wrappers for a subset of core foundation: various bits of CFString, CFData, CFArray, CFDictionary are available. Not that useful on its own but a useful building block for writing ffi wrappers of other OS X libraries.
|
6
|
+
|
7
|
+
Although the CF collection classes can store arbitrary pointer sized values this wrapper only supports storing CFTypes.
|
8
|
+
|
9
|
+
The CF namespace has the raw FFI generated method calls but it's usually easier to use the wrapper classes: `CF::String`, `CF::Date`, `CF::Array`, `CF::Dictionary`, `CF::Boolean` which try to present a rubyish view of the world (for example `CF::Array` implements `Enumerable`)
|
10
|
+
|
11
|
+
These implement methods for creating new instances from ruby objects (eg `CF::String.from_string("hello world")`) but you can also pass build them from an `FFI::Pointer`).
|
12
|
+
|
13
|
+
Converting
|
14
|
+
===========
|
15
|
+
|
16
|
+
`CF::Base` objects has a `to_ruby` that creates a ruby object of the most approprite type (`String` for `CF::String`, `Time` for `CF::Date`, `Integer` or `Float` for `CF::Number` etc). The collection classes call `to_ruby` on their contents too.
|
17
|
+
|
18
|
+
In addition to the methods on the wrapper classes themselves, the ruby classes are extended with a `to_cf` method. Because CoreFoundation strings aren't arbitrary collections of bytes, `String#to_cf` will return a `CF::Data` if the string has the ASCII-8BIT encoding and a `CF::String` if not.
|
19
|
+
|
20
|
+
If you have an `FFI::Pointer` or a raw address then you can create a wrapper by passing it to `new`, for example `CF::String.new(some_pointer)`. This does *not* check that the pointer is actually a `CFString`. You can use `CF::Base.typecast` to construct an instance of the appropriate subclass, for example `CF::Base.typecast(some_pointer)` would return a `CF::String` if `some_pointer` was in fact a `CFStringRef`.
|
21
|
+
|
22
|
+
Memory Management
|
23
|
+
=================
|
24
|
+
|
25
|
+
The convenience methods for creating CF objects will release the cf object when they are garbage collected. Methods on the convenience classes will usually retain the result and mark it for releasing when they are garbage collected (for example `CF::Dictionary#[]` retains the returned value). You don't need to do any extra memory management on these.
|
26
|
+
|
27
|
+
If you pass an `FFI::Pointer` to `new` or `typecast` no assumptions are made for you. You should call `retain` or `release` to manage it manually. As an alternative to calling `release` manually, the `release_on_gc` method adds a finalizer to the wrapper that will call `CFRelease` on the Core Foundation object when the wrapper is garbage collected. You will almost certainly crash your ruby interpreter if you overrelease an object and you will leak memory if you overretain one.
|
28
|
+
|
29
|
+
If you use the raw api (eg `CF.CFArrayCreate`) then you're on your own.
|
30
|
+
|
31
|
+
|
32
|
+
Compatibility
|
33
|
+
=============
|
34
|
+
Requires ruby 1.9 due to use of encoding related methods. Should work in MRI and jruby. Not compatible with rubinius due to rubinius' ffi implemenation not supporting certain features.
|
35
|
+
|
36
|
+
License
|
37
|
+
=======
|
38
|
+
|
39
|
+
Released under the MIT license. See LICENSE
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
require 'corefoundation/base'
|
3
|
+
require 'corefoundation/null'
|
4
|
+
require 'corefoundation/string'
|
5
|
+
require 'corefoundation/array'
|
6
|
+
require 'corefoundation/boolean'
|
7
|
+
require 'corefoundation/data'
|
8
|
+
require 'corefoundation/dictionary'
|
9
|
+
require 'corefoundation/number'
|
10
|
+
require 'corefoundation/date'
|
11
|
+
require 'corefoundation/extensions'
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module CF
|
2
|
+
typedef :pointer, :cfarrayref
|
3
|
+
|
4
|
+
# @private
|
5
|
+
class ArrayCallbacks < FFI::Struct
|
6
|
+
layout :version, :cfindex, #cfindex
|
7
|
+
:retain, :pointer,
|
8
|
+
:release, :pointer,
|
9
|
+
:copyDescription, :pointer,
|
10
|
+
:equal, :pointer
|
11
|
+
end
|
12
|
+
|
13
|
+
attach_variable :kCFTypeArrayCallBacks, ArrayCallbacks
|
14
|
+
attach_function :CFArrayCreate, [:pointer, :pointer, :cfindex, :pointer], :cfarrayref
|
15
|
+
attach_function :CFArrayCreateMutable, [:pointer, :cfindex, :pointer], :cfarrayref
|
16
|
+
attach_function :CFArrayGetValueAtIndex, [:pointer, :cfindex], :pointer
|
17
|
+
attach_function :CFArraySetValueAtIndex, [:pointer, :cfindex, :pointer], :void
|
18
|
+
attach_function :CFArrayAppendValue, [:pointer, :pointer], :void
|
19
|
+
attach_function :CFArrayGetCount, [:pointer], :cfindex
|
20
|
+
|
21
|
+
callback :each_applier, [:pointer, :pointer], :void
|
22
|
+
|
23
|
+
attach_function :CFArrayApplyFunction, [:cfarrayref, CF::Range.by_value, :each_applier, :pointer], :void
|
24
|
+
|
25
|
+
|
26
|
+
# Wrapper class for CFArrayRef. It implements enumberable so you can use a lot of your favourite ruby methods on it.
|
27
|
+
#
|
28
|
+
# Values returned by the accessor methods or yielded by the block are retained and marked as releasable on garbage collection
|
29
|
+
# This means you can safely use the returned values even if the CFArray itself has been destroyed
|
30
|
+
#
|
31
|
+
# Unlike ruby arrays you cannot set arbitary array indexes - You can only set indexes in the range 0..length
|
32
|
+
class Array < Base
|
33
|
+
include Enumerable
|
34
|
+
register_type("CFArray")
|
35
|
+
|
36
|
+
# Whether the array is mutable
|
37
|
+
#
|
38
|
+
# WARNING: this only works for arrays created by CF::Array, there is no public api for telling whether an arbitrary
|
39
|
+
# CFTypeRef is a mutable array or not
|
40
|
+
def mutable?
|
41
|
+
@mutable
|
42
|
+
end
|
43
|
+
|
44
|
+
# Iterates over the array yielding the value to the block
|
45
|
+
# The value is wrapped in the appropriate CF::Base subclass and retained (but marked for releasing upon garbage collection)
|
46
|
+
# @return self
|
47
|
+
def each
|
48
|
+
range = CF::Range.new
|
49
|
+
range[:location] = 0
|
50
|
+
range[:length] = length
|
51
|
+
callback = lambda do |value, _|
|
52
|
+
yield Base.typecast(value).retain.release_on_gc
|
53
|
+
end
|
54
|
+
CF.CFArrayApplyFunction(self, range, callback, nil)
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a new, immutable CFArray from a ruby array of cf objects
|
59
|
+
# @param [Array<CF::Base>] array The objects to place in the array. They must inherit from CF::Base
|
60
|
+
# @return [CF::Array] A CF::Array containing the objects, setup to release the array upon garbage collection
|
61
|
+
def self.immutable(array)
|
62
|
+
if bad_element = array.detect {|value| !value.is_a?(CF::Base)}
|
63
|
+
raise TypeError, "Array contains non cftype #{bad_element.inspect}"
|
64
|
+
end
|
65
|
+
m = FFI::MemoryPointer.new(:pointer, array.length)
|
66
|
+
m.write_array_of_pointer(array)
|
67
|
+
new(CF.CFArrayCreate(nil,m,array.length,CF::kCFTypeArrayCallBacks.to_ptr)).release_on_gc
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates a new, empty mutable CFArray
|
71
|
+
# @return [CF::Array] A mutable CF::Array containing the objects, setup to release the array upon garbage collection
|
72
|
+
def self.mutable
|
73
|
+
result = new(CF.CFArrayCreateMutable nil, 0, CF::kCFTypeArrayCallBacks.to_ptr).release_on_gc
|
74
|
+
result.instance_variable_set(:@mutable, true)
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the object at the index
|
79
|
+
# @param [Integer] index the 0 based index of the item to retrieve. Behaviour is undefined if it is not in the range 0...size
|
80
|
+
# @return [CF::Base] a subclass of CF::Base
|
81
|
+
def [](index)
|
82
|
+
Base.typecast(CF.CFArrayGetValueAtIndex(self, index)).retain.release_on_gc
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sets object at the index
|
86
|
+
# @param [Integer] index the 0 based index of the item to retrieve. Behaviour is undefined if it is not in the range 0..size
|
87
|
+
# It is legal to set the value at index n of a n item array - this is equivalent to appending the object
|
88
|
+
#
|
89
|
+
# @param [CF::Base] value the value to store
|
90
|
+
# @return [CF::Base] the store value
|
91
|
+
def []=(index, value)
|
92
|
+
raise TypeError, "instance is not mutable" unless mutable?
|
93
|
+
self.class.check_cftype(value)
|
94
|
+
CF.CFArraySetValueAtIndex(self, index, value)
|
95
|
+
value
|
96
|
+
end
|
97
|
+
|
98
|
+
# Appends a value to the array
|
99
|
+
#
|
100
|
+
# @return [CF::Array] self
|
101
|
+
def <<(value)
|
102
|
+
raise TypeError, "instance is not mutable" unless mutable?
|
103
|
+
self.class.check_cftype(value)
|
104
|
+
CF.CFArrayAppendValue(self, value)
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns a ruby array containing the result of calling to_ruby on each of the array's elements
|
109
|
+
#
|
110
|
+
def to_ruby
|
111
|
+
collect(&:to_ruby)
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :push, :<<
|
115
|
+
|
116
|
+
# Returns the number of elements the array contains
|
117
|
+
# @return [Integer]
|
118
|
+
def length
|
119
|
+
CF.CFArrayGetCount(self)
|
120
|
+
end
|
121
|
+
alias_method :size, :length
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
|
2
|
+
# The top level namespace for the corefoundation library. The raw FFI generated methods are attached here
|
3
|
+
#
|
4
|
+
#
|
5
|
+
module CF
|
6
|
+
extend FFI::Library
|
7
|
+
ffi_lib '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation'
|
8
|
+
|
9
|
+
if FFI::Platform::ARCH == 'x86_64'
|
10
|
+
typedef :long_long, :cfindex
|
11
|
+
typedef :long_long, :cfcomparisonresult
|
12
|
+
typedef :ulong_long, :cfoptionflags
|
13
|
+
typedef :ulong_long, :cftypeid
|
14
|
+
typedef :ulong_long, :cfhashcode
|
15
|
+
else
|
16
|
+
typedef :long, :cfindex
|
17
|
+
typedef :long, :cfcomparisonresult
|
18
|
+
typedef :ulong, :cfoptionflags
|
19
|
+
typedef :ulong, :cftypeid
|
20
|
+
typedef :ulong, :cfhashcode
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# @private
|
25
|
+
class Range < FFI::Struct
|
26
|
+
layout :location, :cfindex,
|
27
|
+
:length, :cfindex
|
28
|
+
end
|
29
|
+
|
30
|
+
typedef :pointer, :cftyperef
|
31
|
+
|
32
|
+
#general utility functions
|
33
|
+
attach_function :show, 'CFShow', [:cftyperef], :void
|
34
|
+
attach_function :release, 'CFRelease', [:cftyperef], :void
|
35
|
+
attach_function :retain, 'CFRetain', [:cftyperef], :cftyperef
|
36
|
+
attach_function 'CFEqual', [:cftyperef, :cftyperef], :char
|
37
|
+
attach_function 'CFHash', [:cftyperef], :cfhashcode
|
38
|
+
attach_function 'CFCopyDescription', [:cftyperef], :cftyperef
|
39
|
+
attach_function 'CFGetTypeID', [:cftyperef], :cftypeid
|
40
|
+
|
41
|
+
# The base class for all of the wrapper classes
|
42
|
+
#
|
43
|
+
# @abstract
|
44
|
+
class Base
|
45
|
+
@@type_map = {}
|
46
|
+
|
47
|
+
# @private
|
48
|
+
class Releaser
|
49
|
+
def initialize(ptr)
|
50
|
+
@address = ptr.address
|
51
|
+
end
|
52
|
+
|
53
|
+
def call *ignored
|
54
|
+
if @address != 0
|
55
|
+
CF.release(@address)
|
56
|
+
@address = 0
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class << self
|
62
|
+
# Raises an exception if the argument does not inherit from CF::Base
|
63
|
+
#
|
64
|
+
# @param cftyperef object to check
|
65
|
+
def check_cftype(cftyperef)
|
66
|
+
raise TypeError, "#{cftyperef.inspect} is not a cftype" unless cftyperef.is_a?(CF::Base)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Wraps an FFI::Pointer with the appropriate subclass of CF::Base
|
70
|
+
# If the pointer is not a CFTypeRef behaviour is undefined
|
71
|
+
# @param [FFI::Pointer] cftyperef Object to wrap
|
72
|
+
# @return A wrapper object inheriting from CF::Base
|
73
|
+
def typecast(cftyperef)
|
74
|
+
klass = klass_from_cf_type cftyperef
|
75
|
+
klass.new(cftyperef)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def klass_from_cf_type cftyperef
|
80
|
+
klass = @@type_map[CF.CFGetTypeID(cftyperef)]
|
81
|
+
if !klass
|
82
|
+
raise TypeError, "No class registered for cf type #{cftyperef.inspect}"
|
83
|
+
end
|
84
|
+
klass
|
85
|
+
end
|
86
|
+
|
87
|
+
def register_type(type_name)
|
88
|
+
CF.attach_function "#{type_name}GetTypeID", [], :cftypeid
|
89
|
+
@@type_map[CF.send("#{type_name}GetTypeID")] = self
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# @param [FFI::Pointer] ptr The pointer to wrap
|
97
|
+
def initialize(ptr)
|
98
|
+
@ptr = FFI::Pointer.new(ptr)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Whether the instance is the CFNull singleton
|
102
|
+
#
|
103
|
+
# @return [Boolean]
|
104
|
+
def null?
|
105
|
+
equals?(CF::NULL)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the wrapped pointer
|
109
|
+
# @return [FFI::Pointer]
|
110
|
+
def to_ptr
|
111
|
+
@ptr
|
112
|
+
end
|
113
|
+
|
114
|
+
# Sets the wrapper pointer. You should only do this rarely. If you have used release_on_gc then that
|
115
|
+
# finalizer will still be called on the original pointer value
|
116
|
+
#
|
117
|
+
def ptr= ptr
|
118
|
+
@ptr = ptr
|
119
|
+
end
|
120
|
+
|
121
|
+
# Calls CFRetain on the wrapped pointer
|
122
|
+
# @return Returns self
|
123
|
+
def retain
|
124
|
+
CF.retain(self)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calls CFRelease on the wrapped pointer
|
129
|
+
# @return Returns self
|
130
|
+
def release
|
131
|
+
CF.release(self)
|
132
|
+
self
|
133
|
+
end
|
134
|
+
|
135
|
+
# Installs a finalizer on the wrapper object that will cause it to call CFRelease on the pointer
|
136
|
+
# when the wrapper is garbage collected
|
137
|
+
# @return Returns self
|
138
|
+
def release_on_gc
|
139
|
+
ObjectSpace.define_finalizer(@ptr, Releaser.new(@ptr))
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns a ruby string containing the output of CFCopyDescription for the wrapped object
|
144
|
+
#
|
145
|
+
# @return [String]
|
146
|
+
def inspect
|
147
|
+
cf = CF::String.new(CF.CFCopyDescription(self))
|
148
|
+
cf.to_s.tap {cf.release}
|
149
|
+
end
|
150
|
+
|
151
|
+
# Uses CFHash to return a hash code
|
152
|
+
#
|
153
|
+
# @return [Integer]
|
154
|
+
def hash
|
155
|
+
CF.CFHash(self)
|
156
|
+
end
|
157
|
+
|
158
|
+
# eql? (and ==) are implemented using CFEqual
|
159
|
+
#
|
160
|
+
#
|
161
|
+
def eql?(other)
|
162
|
+
if other.is_a?(CF::Base)
|
163
|
+
CF.CFEqual(self, other) != 0
|
164
|
+
else
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# equals? is defined as returning true if the wrapped pointer is the same
|
170
|
+
#
|
171
|
+
#
|
172
|
+
def equals?(other)
|
173
|
+
if other.is_a?(CF::Base)
|
174
|
+
@ptr.address == other.to_ptr.address
|
175
|
+
else
|
176
|
+
false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
alias_method :==, :eql?
|
181
|
+
|
182
|
+
# Converts the CF object into a ruby object. Subclasses typically override this to return
|
183
|
+
# an appropriate object (for example CF::String returns a String)
|
184
|
+
#
|
185
|
+
def to_ruby
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
# to_cf is a no-op on CF::Base and its descendants - it always returns self
|
190
|
+
#
|
191
|
+
#
|
192
|
+
def to_cf
|
193
|
+
self
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CF
|
2
|
+
attach_variable 'kCFBooleanTrue', :pointer
|
3
|
+
attach_variable 'kCFBooleanFalse', :pointer
|
4
|
+
attach_function 'CFBooleanGetValue', [:pointer], :uchar
|
5
|
+
|
6
|
+
# Wrapper for CFBooleanRef.
|
7
|
+
# Typically you use the CF::Boolean::TRUE and CF::Boolean::FALSE constants
|
8
|
+
#
|
9
|
+
class Boolean < Base
|
10
|
+
register_type("CFBoolean")
|
11
|
+
# A constant containing kCFBooleanTrue
|
12
|
+
TRUE = new(CF.kCFBooleanTrue)
|
13
|
+
# A constant containing kCFBooleanFalse
|
14
|
+
FALSE = new(CF.kCFBooleanFalse)
|
15
|
+
|
16
|
+
# returns a ruby true/false value
|
17
|
+
#
|
18
|
+
# @return [Boolean]
|
19
|
+
def value
|
20
|
+
CF.CFBooleanGetValue(self) != 0
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :to_ruby, :value
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module CF
|
2
|
+
typedef :pointer, :cfdataref
|
3
|
+
|
4
|
+
attach_function 'CFDataCreate', [:pointer, :buffer_in, :cfindex], :cfdataref
|
5
|
+
attach_function 'CFDataGetLength', [:cfdataref], :cfindex
|
6
|
+
attach_function 'CFDataGetBytePtr', [:cfdataref], :pointer
|
7
|
+
|
8
|
+
# Wrapper for CFData
|
9
|
+
#
|
10
|
+
#
|
11
|
+
class Data < Base
|
12
|
+
register_type("CFData")
|
13
|
+
|
14
|
+
# Creates a CFData from a ruby string
|
15
|
+
# @param [String] s the string to use
|
16
|
+
# @return [CF::Data]
|
17
|
+
def self.from_string(s)
|
18
|
+
new(CF.CFDataCreate(nil, s, s.bytesize)).release_on_gc
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates a ruby string from the wrapped data. The encoding will always be ASCII_8BIT
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def to_s
|
25
|
+
ptr = CF.CFDataGetBytePtr(self)
|
26
|
+
ptr.read_string(CF.CFDataGetLength(self)).force_encoding(Encoding::ASCII_8BIT)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The size in bytes of the CFData
|
30
|
+
# @return [Integer]
|
31
|
+
def size
|
32
|
+
CF.CFDataGetLength(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :to_ruby, :to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|