amazing_print 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/Appraisals +60 -0
- data/CHANGELOG.md +2 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +356 -0
- data/Rakefile +23 -0
- data/lib/amazing_print.rb +46 -0
- data/lib/amazing_print/colorize.rb +25 -0
- data/lib/amazing_print/core_ext/awesome_method_array.rb +82 -0
- data/lib/amazing_print/core_ext/class.rb +23 -0
- data/lib/amazing_print/core_ext/kernel.rb +25 -0
- data/lib/amazing_print/core_ext/logger.rb +21 -0
- data/lib/amazing_print/core_ext/method.rb +21 -0
- data/lib/amazing_print/core_ext/object.rb +23 -0
- data/lib/amazing_print/core_ext/string.rb +42 -0
- data/lib/amazing_print/custom_defaults.rb +57 -0
- data/lib/amazing_print/ext/action_view.rb +22 -0
- data/lib/amazing_print/ext/active_record.rb +103 -0
- data/lib/amazing_print/ext/active_support.rb +45 -0
- data/lib/amazing_print/ext/mongo_mapper.rb +125 -0
- data/lib/amazing_print/ext/mongoid.rb +68 -0
- data/lib/amazing_print/ext/nobrainer.rb +53 -0
- data/lib/amazing_print/ext/nokogiri.rb +45 -0
- data/lib/amazing_print/ext/ostruct.rb +27 -0
- data/lib/amazing_print/ext/ripple.rb +71 -0
- data/lib/amazing_print/ext/sequel.rb +55 -0
- data/lib/amazing_print/formatter.rb +120 -0
- data/lib/amazing_print/formatters.rb +14 -0
- data/lib/amazing_print/formatters/array_formatter.rb +139 -0
- data/lib/amazing_print/formatters/base_formatter.rb +148 -0
- data/lib/amazing_print/formatters/class_formatter.rb +24 -0
- data/lib/amazing_print/formatters/dir_formatter.rb +21 -0
- data/lib/amazing_print/formatters/file_formatter.rb +21 -0
- data/lib/amazing_print/formatters/hash_formatter.rb +106 -0
- data/lib/amazing_print/formatters/method_formatter.rb +21 -0
- data/lib/amazing_print/formatters/object_formatter.rb +82 -0
- data/lib/amazing_print/formatters/simple_formatter.rb +20 -0
- data/lib/amazing_print/formatters/struct_formatter.rb +74 -0
- data/lib/amazing_print/indentator.rb +17 -0
- data/lib/amazing_print/inspector.rb +175 -0
- data/lib/amazing_print/version.rb +10 -0
- data/lib/ap.rb +10 -0
- data/spec/active_record_helper.rb +30 -0
- data/spec/colors_spec.rb +114 -0
- data/spec/core_ext/logger_spec.rb +44 -0
- data/spec/core_ext/string_spec.rb +20 -0
- data/spec/ext/action_view_spec.rb +17 -0
- data/spec/ext/active_record_spec.rb +297 -0
- data/spec/ext/active_support_spec.rb +26 -0
- data/spec/ext/mongo_mapper_spec.rb +259 -0
- data/spec/ext/mongoid_spec.rb +66 -0
- data/spec/ext/nobrainer_spec.rb +58 -0
- data/spec/ext/nokogiri_spec.rb +50 -0
- data/spec/ext/ostruct_spec.rb +22 -0
- data/spec/ext/ripple_spec.rb +47 -0
- data/spec/formats_spec.rb +779 -0
- data/spec/methods_spec.rb +478 -0
- data/spec/misc_spec.rb +245 -0
- data/spec/objects_spec.rb +219 -0
- data/spec/spec_helper.rb +106 -0
- data/spec/support/active_record_data.rb +20 -0
- data/spec/support/active_record_data/3_2_diana.txt +24 -0
- data/spec/support/active_record_data/3_2_diana_legacy.txt +24 -0
- data/spec/support/active_record_data/3_2_multi.txt +50 -0
- data/spec/support/active_record_data/3_2_multi_legacy.txt +50 -0
- data/spec/support/active_record_data/4_0_diana.txt +98 -0
- data/spec/support/active_record_data/4_0_multi.txt +198 -0
- data/spec/support/active_record_data/4_1_diana.txt +97 -0
- data/spec/support/active_record_data/4_1_multi.txt +196 -0
- data/spec/support/active_record_data/4_2_diana.txt +109 -0
- data/spec/support/active_record_data/4_2_diana_legacy.txt +109 -0
- data/spec/support/active_record_data/4_2_multi.txt +220 -0
- data/spec/support/active_record_data/4_2_multi_legacy.txt +220 -0
- data/spec/support/active_record_data/5_0_diana.txt +105 -0
- data/spec/support/active_record_data/5_0_multi.txt +212 -0
- data/spec/support/active_record_data/5_1_diana.txt +104 -0
- data/spec/support/active_record_data/5_1_multi.txt +210 -0
- data/spec/support/active_record_data/5_2_diana.txt +104 -0
- data/spec/support/active_record_data/5_2_multi.txt +210 -0
- data/spec/support/active_record_data/6_0_diana.txt +104 -0
- data/spec/support/active_record_data/6_0_multi.txt +210 -0
- data/spec/support/ext_verifier.rb +41 -0
- data/spec/support/mongoid_versions.rb +22 -0
- data/spec/support/rails_versions.rb +50 -0
- metadata +243 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# Copyright (c) 2010-2016 Michael Dvorkin and contributors
|
2
|
+
#
|
3
|
+
# AmazingPrint is freely distributable under the terms of MIT license.
|
4
|
+
# See LICENSE file or http://www.opensource.org/licenses/mit-license.php
|
5
|
+
#------------------------------------------------------------------------------
|
6
|
+
require 'amazing_print/formatters'
|
7
|
+
|
8
|
+
module AmazingPrint
|
9
|
+
class Formatter
|
10
|
+
include Colorize
|
11
|
+
|
12
|
+
attr_reader :inspector, :options
|
13
|
+
|
14
|
+
CORE_FORMATTERS = %i[array bigdecimal class dir file hash method rational set struct unboundmethod].freeze
|
15
|
+
|
16
|
+
def initialize(inspector)
|
17
|
+
@inspector = inspector
|
18
|
+
@options = inspector.options
|
19
|
+
end
|
20
|
+
|
21
|
+
# Main entry point to format an object.
|
22
|
+
#------------------------------------------------------------------------------
|
23
|
+
def format(object, type = nil)
|
24
|
+
core_class = cast(object, type)
|
25
|
+
awesome = if core_class != :self
|
26
|
+
send(:"awesome_#{core_class}", object) # Core formatters.
|
27
|
+
else
|
28
|
+
awesome_self(object, type) # Catch all that falls back to object.inspect.
|
29
|
+
end
|
30
|
+
awesome
|
31
|
+
end
|
32
|
+
|
33
|
+
# Hook this when adding custom formatters. Check out lib/amazing_print/ext
|
34
|
+
# directory for custom formatters that ship with amazing_print.
|
35
|
+
#------------------------------------------------------------------------------
|
36
|
+
def cast(_object, type)
|
37
|
+
CORE_FORMATTERS.include?(type) ? type : :self
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Catch all method to format an arbitrary object.
|
43
|
+
#------------------------------------------------------------------------------
|
44
|
+
def awesome_self(object, type)
|
45
|
+
if @options[:raw] && object.instance_variables.any?
|
46
|
+
awesome_object(object)
|
47
|
+
elsif (hash = convert_to_hash(object))
|
48
|
+
awesome_hash(hash)
|
49
|
+
else
|
50
|
+
awesome_simple(object.inspect.to_s, type, @inspector)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def awesome_bigdecimal(n)
|
55
|
+
o = n.to_s('F')
|
56
|
+
type = :bigdecimal
|
57
|
+
awesome_simple(o, type, @inspector)
|
58
|
+
end
|
59
|
+
|
60
|
+
def awesome_rational(n)
|
61
|
+
o = n.to_s
|
62
|
+
type = :rational
|
63
|
+
awesome_simple(o, type, @inspector)
|
64
|
+
end
|
65
|
+
|
66
|
+
def awesome_simple(o, type, inspector = @inspector)
|
67
|
+
AmazingPrint::Formatters::SimpleFormatter.new(o, type, inspector).format
|
68
|
+
end
|
69
|
+
|
70
|
+
def awesome_array(a)
|
71
|
+
Formatters::ArrayFormatter.new(a, @inspector).format
|
72
|
+
end
|
73
|
+
|
74
|
+
def awesome_set(s)
|
75
|
+
Formatters::ArrayFormatter.new(s.to_a, @inspector).format
|
76
|
+
end
|
77
|
+
|
78
|
+
def awesome_hash(h)
|
79
|
+
Formatters::HashFormatter.new(h, @inspector).format
|
80
|
+
end
|
81
|
+
|
82
|
+
def awesome_object(o)
|
83
|
+
Formatters::ObjectFormatter.new(o, @inspector).format
|
84
|
+
end
|
85
|
+
|
86
|
+
def awesome_struct(s)
|
87
|
+
Formatters::StructFormatter.new(s, @inspector).format
|
88
|
+
end
|
89
|
+
|
90
|
+
def awesome_method(m)
|
91
|
+
Formatters::MethodFormatter.new(m, @inspector).format
|
92
|
+
end
|
93
|
+
alias awesome_unboundmethod awesome_method
|
94
|
+
|
95
|
+
def awesome_class(c)
|
96
|
+
Formatters::ClassFormatter.new(c, @inspector).format
|
97
|
+
end
|
98
|
+
|
99
|
+
def awesome_file(f)
|
100
|
+
Formatters::FileFormatter.new(f, @inspector).format
|
101
|
+
end
|
102
|
+
|
103
|
+
def awesome_dir(d)
|
104
|
+
Formatters::DirFormatter.new(d, @inspector).format
|
105
|
+
end
|
106
|
+
|
107
|
+
# Utility methods.
|
108
|
+
#------------------------------------------------------------------------------
|
109
|
+
def convert_to_hash(object)
|
110
|
+
return nil unless object.respond_to?(:to_hash)
|
111
|
+
|
112
|
+
return nil if object.method(:to_hash).arity != 0
|
113
|
+
|
114
|
+
hash = object.to_hash
|
115
|
+
return nil if !hash.respond_to?(:keys) || !hash.respond_to?(:[])
|
116
|
+
|
117
|
+
hash
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module AmazingPrint
|
2
|
+
module Formatters
|
3
|
+
require 'amazing_print/formatters/object_formatter'
|
4
|
+
require 'amazing_print/formatters/struct_formatter'
|
5
|
+
require 'amazing_print/formatters/hash_formatter'
|
6
|
+
require 'amazing_print/formatters/array_formatter'
|
7
|
+
require 'amazing_print/formatters/simple_formatter'
|
8
|
+
require 'amazing_print/formatters/method_formatter'
|
9
|
+
require 'amazing_print/formatters/class_formatter'
|
10
|
+
require 'amazing_print/formatters/dir_formatter'
|
11
|
+
require 'amazing_print/formatters/file_formatter'
|
12
|
+
require 'amazing_print/colorize'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require_relative 'base_formatter'
|
2
|
+
|
3
|
+
module AmazingPrint
|
4
|
+
module Formatters
|
5
|
+
class ArrayFormatter < BaseFormatter
|
6
|
+
attr_reader :array, :inspector, :options
|
7
|
+
|
8
|
+
def initialize(array, inspector)
|
9
|
+
@array = array
|
10
|
+
@inspector = inspector
|
11
|
+
@options = inspector.options
|
12
|
+
end
|
13
|
+
|
14
|
+
def format
|
15
|
+
if array.length.zero?
|
16
|
+
'[]'
|
17
|
+
elsif methods_array?
|
18
|
+
methods_array
|
19
|
+
else
|
20
|
+
simple_array
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def methods_array?
|
27
|
+
array.instance_variable_defined?('@__awesome_methods__')
|
28
|
+
end
|
29
|
+
|
30
|
+
def simple_array
|
31
|
+
if options[:multiline]
|
32
|
+
multiline_array
|
33
|
+
else
|
34
|
+
'[ ' << array.map { |item| inspector.awesome(item) }.join(', ') << ' ]'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def multiline_array
|
39
|
+
data = if should_be_limited?
|
40
|
+
limited(generate_printable_array, width(array))
|
41
|
+
else
|
42
|
+
generate_printable_array
|
43
|
+
end
|
44
|
+
|
45
|
+
%([\n#{data.join(",\n")}\n#{outdent}])
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_printable_array
|
49
|
+
array.map.with_index do |item, index|
|
50
|
+
array_prefix(index, width(array)).tap do |temp|
|
51
|
+
indented { temp << inspector.awesome(item) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def array_prefix(iteration, width)
|
57
|
+
generic_prefix(iteration, width)
|
58
|
+
end
|
59
|
+
|
60
|
+
def methods_array
|
61
|
+
array.map!(&:to_s).sort!
|
62
|
+
|
63
|
+
data = generate_printable_tuples.join("\n")
|
64
|
+
|
65
|
+
"[\n#{data}\n#{outdent}]"
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_printable_tuples
|
69
|
+
tuples.map.with_index do |item, index|
|
70
|
+
tuple_prefix(index, width(tuples)).tap do |temp|
|
71
|
+
indented { temp << tuple_template(item) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def tuple_template(item)
|
77
|
+
name_width, args_width = name_and_args_width
|
78
|
+
|
79
|
+
[
|
80
|
+
colorize(item[0].rjust(name_width), :method),
|
81
|
+
colorize(item[1].ljust(args_width), :args),
|
82
|
+
' ',
|
83
|
+
colorize(item[2], :class)
|
84
|
+
].join
|
85
|
+
end
|
86
|
+
|
87
|
+
def tuples
|
88
|
+
@tuples ||= array.map { |name| generate_tuple(name) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def name_and_args_width
|
92
|
+
name_and_args = tuples.transpose
|
93
|
+
|
94
|
+
[name_and_args[0].map(&:size).max, name_and_args[1].map(&:size).max]
|
95
|
+
end
|
96
|
+
|
97
|
+
def tuple_prefix(iteration, width)
|
98
|
+
generic_prefix(iteration, width, ' ')
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_tuple(name)
|
102
|
+
meth = case name
|
103
|
+
when Symbol, String
|
104
|
+
find_method(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
meth ? method_tuple(meth) : [name.to_s, '(?)', '?']
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_method(name)
|
111
|
+
object = array.instance_variable_get('@__awesome_methods__')
|
112
|
+
|
113
|
+
meth = begin
|
114
|
+
object.method(name)
|
115
|
+
rescue NameError, ArgumentError
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
119
|
+
meth || begin
|
120
|
+
object.instance_method(name)
|
121
|
+
rescue NameError
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def generic_prefix(iteration, width, padding = '')
|
127
|
+
if options[:index]
|
128
|
+
indent + colorize("[#{iteration.to_s.rjust(width)}] ", :array)
|
129
|
+
else
|
130
|
+
indent + padding
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def width(items)
|
135
|
+
(items.size - 1).to_s.size
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative '../colorize'
|
2
|
+
|
3
|
+
module AmazingPrint
|
4
|
+
module Formatters
|
5
|
+
class BaseFormatter
|
6
|
+
include Colorize
|
7
|
+
|
8
|
+
DEFAULT_LIMIT_SIZE = 7
|
9
|
+
|
10
|
+
# To support limited output, for example:
|
11
|
+
#
|
12
|
+
# ap ('a'..'z').to_a, :limit => 3
|
13
|
+
# [
|
14
|
+
# [ 0] "a",
|
15
|
+
# [ 1] .. [24],
|
16
|
+
# [25] "z"
|
17
|
+
# ]
|
18
|
+
#
|
19
|
+
# ap (1..100).to_a, :limit => true # Default limit is 7.
|
20
|
+
# [
|
21
|
+
# [ 0] 1,
|
22
|
+
# [ 1] 2,
|
23
|
+
# [ 2] 3,
|
24
|
+
# [ 3] .. [96],
|
25
|
+
# [97] 98,
|
26
|
+
# [98] 99,
|
27
|
+
# [99] 100
|
28
|
+
# ]
|
29
|
+
#------------------------------------------------------------------------------
|
30
|
+
def should_be_limited?
|
31
|
+
options[:limit] || (options[:limit].is_a?(Integer) && (options[:limit] > 0))
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_limit_size
|
35
|
+
case options[:limit]
|
36
|
+
when true
|
37
|
+
DEFAULT_LIMIT_SIZE
|
38
|
+
else
|
39
|
+
options[:limit]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def limited(data, width, is_hash = false)
|
44
|
+
limit = get_limit_size
|
45
|
+
if data.length <= limit
|
46
|
+
data
|
47
|
+
else
|
48
|
+
# Calculate how many elements to be displayed above and below the separator.
|
49
|
+
head = limit / 2
|
50
|
+
tail = head - (limit - 1) % 2
|
51
|
+
|
52
|
+
# Add the proper elements to the temp array and format the separator.
|
53
|
+
temp = data[0, head] + [nil] + data[-tail, tail]
|
54
|
+
|
55
|
+
temp[head] = if is_hash
|
56
|
+
"#{indent}#{data[head].strip} .. #{data[data.length - tail - 1].strip}"
|
57
|
+
else
|
58
|
+
"#{indent}[#{head.to_s.rjust(width)}] .. [#{data.length - tail - 1}]"
|
59
|
+
end
|
60
|
+
|
61
|
+
temp
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_tuple(method)
|
66
|
+
if method.respond_to?(:parameters) # Ruby 1.9.2+
|
67
|
+
# See http://readruby.chengguangnan.com/methods#method-objects-parameters
|
68
|
+
# (mirror: http://archive.is/XguCA#selection-3381.1-3381.11)
|
69
|
+
args = method.parameters.inject([]) do |arr, (type, name)|
|
70
|
+
name ||= (type == :block ? 'block' : "arg#{arr.size + 1}")
|
71
|
+
arr << case type
|
72
|
+
when :req then name.to_s
|
73
|
+
when :opt, :rest then "*#{name}"
|
74
|
+
when :block then "&#{name}"
|
75
|
+
else '?'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
else # See http://ruby-doc.org/core/classes/Method.html#M001902
|
79
|
+
args = (1..method.arity.abs).map { |i| "arg#{i}" }
|
80
|
+
args[-1] = "*#{args[-1]}" if method.arity < 0
|
81
|
+
end
|
82
|
+
|
83
|
+
# method.to_s formats to handle:
|
84
|
+
#
|
85
|
+
# #<Method: Fixnum#zero?>
|
86
|
+
# #<Method: Fixnum(Integer)#years>
|
87
|
+
# #<Method: User(#<Module:0x00000103207c00>)#_username>
|
88
|
+
# #<Method: User(id: integer, username: string).table_name>
|
89
|
+
# #<Method: User(id: integer, username: string)(ActiveRecord::Base).current>
|
90
|
+
# #<Method: #<Class:0x100c567f>(ActiveRecord::Querying)#first>
|
91
|
+
# #<UnboundMethod: Hello#world>
|
92
|
+
# #<UnboundMethod: Hello#world() /home/hs/code/amazing_print/spec/methods_spec.rb:68>
|
93
|
+
#
|
94
|
+
if method.to_s =~ /(Unbound)*Method: ((#<)?[^\/#]*)[#\.]/
|
95
|
+
unbound = Regexp.last_match(1) && '(unbound)'
|
96
|
+
klass = Regexp.last_match(2)
|
97
|
+
if klass && klass =~ /(\(\w+:\s.*?\))/ # Is this ActiveRecord-style class?
|
98
|
+
klass.sub!(Regexp.last_match(1), '') # Yes, strip the fields leaving class name only.
|
99
|
+
end
|
100
|
+
|
101
|
+
owner = "#{klass}#{unbound}".gsub('(', ' (')
|
102
|
+
end
|
103
|
+
|
104
|
+
[method.name.to_s, "(#{args.join(', ')})", owner.to_s]
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Indentation related methods
|
109
|
+
#-----------------------------------------
|
110
|
+
def indentation
|
111
|
+
inspector.current_indentation
|
112
|
+
end
|
113
|
+
|
114
|
+
def indented(&blk)
|
115
|
+
inspector.increase_indentation(&blk)
|
116
|
+
end
|
117
|
+
|
118
|
+
# precompute common indentations
|
119
|
+
INDENT_CACHE = (0..100).map { |i| ' ' * i }.map(&:freeze).freeze
|
120
|
+
|
121
|
+
def indent(n = indentation)
|
122
|
+
INDENT_CACHE[n] || ' ' * n
|
123
|
+
end
|
124
|
+
|
125
|
+
def outdent
|
126
|
+
' ' * (indentation - options[:indent].abs)
|
127
|
+
i = indentation - options[:indent].abs
|
128
|
+
|
129
|
+
INDENT_CACHE[i] || ' ' * i
|
130
|
+
end
|
131
|
+
|
132
|
+
def align(value, width)
|
133
|
+
if options[:multiline]
|
134
|
+
indent_option = options[:indent]
|
135
|
+
if indent_option > 0
|
136
|
+
value.rjust(width)
|
137
|
+
elsif indent_option == 0
|
138
|
+
"#{indent}#{value.ljust(width)}"
|
139
|
+
else
|
140
|
+
"#{indent(indentation + indent_option)}#{value.ljust(width)}"
|
141
|
+
end
|
142
|
+
else
|
143
|
+
value
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'base_formatter'
|
2
|
+
|
3
|
+
module AmazingPrint
|
4
|
+
module Formatters
|
5
|
+
class ClassFormatter < BaseFormatter
|
6
|
+
attr_reader :klass, :inspector, :options
|
7
|
+
|
8
|
+
def initialize(klass, inspector)
|
9
|
+
@klass = klass
|
10
|
+
@inspector = inspector
|
11
|
+
@options = inspector.options
|
12
|
+
end
|
13
|
+
|
14
|
+
def format
|
15
|
+
superclass = klass.superclass
|
16
|
+
if superclass
|
17
|
+
colorize("#{klass.inspect} < #{superclass}", :class)
|
18
|
+
else
|
19
|
+
colorize(klass.inspect, :class)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|