ruport 0.8.14 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +42 -107
- data/Rakefile +29 -32
- data/examples/centered_pdf_text_box.rb +13 -19
- data/examples/example.csv +3 -0
- data/examples/line_plotter.rb +15 -15
- data/examples/pdf_complex_report.rb +10 -23
- data/examples/pdf_table_with_title.rb +12 -12
- data/examples/rope_examples/itunes/Rakefile +22 -1
- data/examples/rope_examples/itunes/config/environment.rb +4 -0
- data/examples/rope_examples/itunes/lib/init.rb +32 -2
- data/examples/rope_examples/itunes/util/build +50 -16
- data/examples/rope_examples/sales_report/README +1 -1
- data/examples/rope_examples/sales_report/Rakefile +22 -1
- data/examples/rope_examples/sales_report/config/environment.rb +4 -0
- data/examples/rope_examples/sales_report/lib/init.rb +32 -2
- data/examples/rope_examples/sales_report/lib/reports/sales.rb +10 -16
- data/examples/rope_examples/sales_report/util/build +50 -16
- data/examples/row_renderer.rb +39 -0
- data/examples/ruport_list/png_embed.rb +61 -0
- data/examples/ruport_list/roadmap.png +0 -0
- data/examples/sample.rb +16 -0
- data/examples/simple_pdf_lines.rb +24 -0
- data/lib/ruport.rb +143 -57
- data/lib/ruport/acts_as_reportable.rb +246 -0
- data/lib/ruport/data.rb +1 -2
- data/lib/ruport/data/grouping.rb +311 -0
- data/lib/ruport/data/record.rb +113 -84
- data/lib/ruport/data/table.rb +275 -174
- data/lib/ruport/formatter.rb +149 -0
- data/lib/ruport/formatter/csv.rb +87 -0
- data/lib/ruport/formatter/html.rb +89 -0
- data/lib/ruport/formatter/pdf.rb +357 -0
- data/lib/ruport/formatter/text.rb +151 -0
- data/lib/ruport/generator.rb +127 -30
- data/lib/ruport/query.rb +46 -99
- data/lib/ruport/renderer.rb +238 -194
- data/lib/ruport/renderer/grouping.rb +67 -0
- data/lib/ruport/renderer/table.rb +25 -98
- data/lib/ruport/report.rb +45 -96
- data/test/acts_as_reportable_test.rb +229 -0
- data/test/csv_formatter_test.rb +97 -0
- data/test/{_test_database.rb → database_test_.rb} +0 -0
- data/test/grouping_test.rb +305 -0
- data/test/html_formatter_test.rb +104 -0
- data/test/pdf_formatter_test.rb +25 -0
- data/test/{test_query.rb → query_test.rb} +32 -121
- data/test/{test_record.rb → record_test.rb} +40 -23
- data/test/renderer_test.rb +344 -0
- data/test/{test_report.rb → report_test.rb} +74 -44
- data/test/samples/ticket_count.csv +124 -0
- data/test/{test_sql_split.rb → sql_split_test.rb} +0 -0
- data/test/{test_table.rb → table_test.rb} +255 -44
- data/test/text_formatter_test.rb +144 -0
- data/util/bench/data/record/bench_as_vs_to.rb +17 -0
- data/util/bench/data/record/bench_constructor.rb +46 -0
- data/util/bench/data/record/bench_indexing.rb +65 -0
- data/util/bench/data/record/bench_reorder.rb +35 -0
- data/util/bench/data/record/bench_to_a.rb +19 -0
- data/util/bench/data/table/bench_column_manip.rb +103 -0
- data/util/bench/data/table/bench_dup.rb +24 -0
- data/util/bench/data/table/bench_init.rb +67 -0
- data/util/bench/data/table/bench_manip.rb +125 -0
- data/util/bench/formatter/bench_csv.rb +14 -0
- data/util/bench/formatter/bench_html.rb +14 -0
- data/util/bench/formatter/bench_pdf.rb +14 -0
- data/util/bench/formatter/bench_text.rb +14 -0
- data/util/bench/samples/tattle.csv +1237 -0
- metadata +121 -143
- data/TODO +0 -21
- data/examples/invoice.rb +0 -142
- data/examples/invoice_report.rb +0 -29
- data/examples/line_graph.rb +0 -38
- data/examples/rope_examples/itunes/config/ruport_config.rb +0 -8
- data/examples/rope_examples/sales_report/config/ruport_config.rb +0 -8
- data/lib/ruport/attempt.rb +0 -63
- data/lib/ruport/config.rb +0 -204
- data/lib/ruport/data/groupable.rb +0 -93
- data/lib/ruport/data/taggable.rb +0 -80
- data/lib/ruport/format.rb +0 -1
- data/lib/ruport/format/csv.rb +0 -29
- data/lib/ruport/format/html.rb +0 -42
- data/lib/ruport/format/latex.rb +0 -47
- data/lib/ruport/format/pdf.rb +0 -233
- data/lib/ruport/format/plugin.rb +0 -31
- data/lib/ruport/format/svg.rb +0 -60
- data/lib/ruport/format/text.rb +0 -103
- data/lib/ruport/format/xml.rb +0 -32
- data/lib/ruport/layout.rb +0 -1
- data/lib/ruport/layout/component.rb +0 -7
- data/lib/ruport/mailer.rb +0 -99
- data/lib/ruport/renderer/graph.rb +0 -46
- data/lib/ruport/report/graph.rb +0 -14
- data/lib/ruport/system_extensions.rb +0 -71
- data/test/test_config.rb +0 -88
- data/test/test_format_text.rb +0 -63
- data/test/test_graph_renderer.rb +0 -97
- data/test/test_groupable.rb +0 -56
- data/test/test_mailer.rb +0 -170
- data/test/test_renderer.rb +0 -151
- data/test/test_ruport.rb +0 -58
- data/test/test_table_renderer.rb +0 -141
- data/test/test_taggable.rb +0 -52
data/examples/invoice_report.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "ruport"
|
3
|
-
require "invoice"
|
4
|
-
|
5
|
-
class SampleReport < Ruport::Report
|
6
|
-
include Invoice
|
7
|
-
|
8
|
-
def generate
|
9
|
-
render_invoice do |i|
|
10
|
-
i.data = [[1,2,3],[4,5,6]].to_table(%w[a b c])
|
11
|
-
|
12
|
-
i.options do |o|
|
13
|
-
o.company_info = "Stone Code Productions\n43 Neagle Street"
|
14
|
-
o.customer_info = "Gregory Brown\n200 Foo Ave."
|
15
|
-
o.comments = "J. Random Comment"
|
16
|
-
o.order_info = "Some info\nabout your order"
|
17
|
-
o.title = "Invoice for 12.15.2006 - 12.31.2006"
|
18
|
-
end
|
19
|
-
|
20
|
-
i.layout do |lay|
|
21
|
-
lay.body_width = 480
|
22
|
-
lay.comments_font_size = 12
|
23
|
-
lay.title_font_size = 10
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
SampleReport.run { |r| r.write "out.pdf" }
|
data/examples/line_graph.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require "ruport"
|
2
|
-
table = Table(%w[jan feb mar apr may jun jul]) do |t|
|
3
|
-
t << [5,7,9,12,14,16,18]
|
4
|
-
t << [21,3,8,19,13,15,1]
|
5
|
-
end
|
6
|
-
|
7
|
-
table[0].tag :ghosts
|
8
|
-
table[1].tag "pirates"
|
9
|
-
|
10
|
-
results = Ruport::Renderer::Graph.render_svg do |r|
|
11
|
-
|
12
|
-
r.data = table
|
13
|
-
|
14
|
-
r.options.title = "Simple Line Graph"
|
15
|
-
|
16
|
-
r.layout do |l|
|
17
|
-
l.width = 700
|
18
|
-
l.height = 500
|
19
|
-
l.theme = r.plugin.themes[:keynote]
|
20
|
-
l.style = :line
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
File.open("line_graph.svg","w") { |f| f << results }
|
25
|
-
|
26
|
-
PAGE = <<-END_HTML
|
27
|
-
<html>
|
28
|
-
<body>
|
29
|
-
<embed
|
30
|
-
src="line_graph.svg"
|
31
|
-
width="700"
|
32
|
-
height="500"
|
33
|
-
/>
|
34
|
-
</body>
|
35
|
-
<html>
|
36
|
-
END_HTML
|
37
|
-
|
38
|
-
File.open("line_graph.html","w") { |f| f << PAGE }
|
data/lib/ruport/attempt.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
require 'timeout'
|
2
|
-
|
3
|
-
class Attempt # :nodoc:
|
4
|
-
VERSION = '0.1.0'
|
5
|
-
|
6
|
-
# Number of attempts to make before failing. The default is 3.
|
7
|
-
attr_accessor :tries
|
8
|
-
|
9
|
-
# Number of seconds to wait between attempts. The default is 60.
|
10
|
-
attr_accessor :interval
|
11
|
-
|
12
|
-
# a level which ruport understands.
|
13
|
-
attr_accessor :log_level
|
14
|
-
|
15
|
-
# If set, this increments the interval with each failed attempt by that
|
16
|
-
# number of seconds.
|
17
|
-
attr_accessor :increment
|
18
|
-
|
19
|
-
# If set, the code block is further wrapped in a timeout block.
|
20
|
-
attr_accessor :timeout
|
21
|
-
|
22
|
-
# Determines which exception level to check when looking for errors to
|
23
|
-
# retry. The default is 'Exception' (i.e. all errors).
|
24
|
-
attr_accessor :level
|
25
|
-
|
26
|
-
# :call-seq:
|
27
|
-
# Attempt.new{ |a| ... }
|
28
|
-
#
|
29
|
-
# Creates and returns a new +Attempt+ object. Use a block to set the
|
30
|
-
# accessors.
|
31
|
-
#
|
32
|
-
def initialize
|
33
|
-
@tries = 3 # Reasonable default
|
34
|
-
@interval = 60 # Reasonable default
|
35
|
-
@increment = nil # Should be an int, if provided
|
36
|
-
@timeout = nil # Wrap the code in a timeout block if provided
|
37
|
-
@level = Exception # Level of exception to be caught
|
38
|
-
|
39
|
-
yield self if block_given?
|
40
|
-
end
|
41
|
-
|
42
|
-
def attempt
|
43
|
-
count = 1
|
44
|
-
begin
|
45
|
-
if @timeout
|
46
|
-
Timeout.timeout(@timeout){ yield }
|
47
|
-
else
|
48
|
-
yield
|
49
|
-
end
|
50
|
-
rescue @level => error
|
51
|
-
@tries -= 1
|
52
|
-
if @tries > 0
|
53
|
-
msg = "Error on attempt # #{count}: #{error}; retrying"
|
54
|
-
count += 1
|
55
|
-
Ruport.log(msg, :level => log_level)
|
56
|
-
@interval += @increment if @increment
|
57
|
-
sleep @interval
|
58
|
-
retry
|
59
|
-
end
|
60
|
-
raise
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
data/lib/ruport/config.rb
DELETED
@@ -1,204 +0,0 @@
|
|
1
|
-
# ruport/config.rb : Ruby Reports configuration system
|
2
|
-
#
|
3
|
-
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
-
#
|
5
|
-
# Copyright (c) 2006, All Rights Reserved.
|
6
|
-
#
|
7
|
-
# This is free software. You may modify and redistribute this freely under
|
8
|
-
# your choice of the GNU General Public License or the Ruby License.
|
9
|
-
#
|
10
|
-
# See LICENSE and COPYING for details
|
11
|
-
#
|
12
|
-
require "ostruct"
|
13
|
-
module Ruport
|
14
|
-
|
15
|
-
# === Overview
|
16
|
-
#
|
17
|
-
# This class serves as the configuration system for Ruport.
|
18
|
-
#
|
19
|
-
# The source and mailer defined as <tt>:default</tt> will become the
|
20
|
-
# fallback values if you don't specify one in <tt>Report::Mailer</tt> or
|
21
|
-
# <tt>Query</tt>, but you may define as many sources as you like and switch
|
22
|
-
# between them later.
|
23
|
-
#
|
24
|
-
# === Example
|
25
|
-
#
|
26
|
-
# The most common way to access your application configuration is through
|
27
|
-
# the <tt>Ruport.configure</tt> method, like this:
|
28
|
-
#
|
29
|
-
# Ruport.configure do |config|
|
30
|
-
#
|
31
|
-
# config.log_file 'foo.log'
|
32
|
-
# config.debug_mode = true
|
33
|
-
#
|
34
|
-
# config.source :default,
|
35
|
-
# :dsn => "dbi:mysql:somedb:db.blixy.org",
|
36
|
-
# :user => "root",
|
37
|
-
# :password => "chunky_bacon"
|
38
|
-
#
|
39
|
-
# config.mailer :default,
|
40
|
-
# :host => "mail.chunkybacon.org",
|
41
|
-
# :address => "chunky@bacon.net",
|
42
|
-
# :user => "cartoon",
|
43
|
-
# :password => "fox",
|
44
|
-
# :port => 25,
|
45
|
-
# :auth_type => :login
|
46
|
-
#
|
47
|
-
# end
|
48
|
-
#
|
49
|
-
# You can accomplish the same thing by opening the class directly:
|
50
|
-
#
|
51
|
-
# class Ruport::Config
|
52
|
-
#
|
53
|
-
# source :default,
|
54
|
-
# :dsn => "dbi:mysql:some_db",
|
55
|
-
# :user => "root"
|
56
|
-
#
|
57
|
-
# mailer :default,
|
58
|
-
# :host => "mail.iheartwhy.com",
|
59
|
-
# :address => "sandal@ruby-harmonix.net",
|
60
|
-
# :user => "sandal",
|
61
|
-
# :password => "abc123"
|
62
|
-
#
|
63
|
-
# logfile 'foo.log'
|
64
|
-
#
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# Saving this config information into a file and then requiring it allows
|
68
|
-
# you to share configurations between Ruport applications.
|
69
|
-
#
|
70
|
-
module Config
|
71
|
-
module_function
|
72
|
-
|
73
|
-
# :call-seq:
|
74
|
-
# source(source_name, options)
|
75
|
-
#
|
76
|
-
# Creates or retrieves a database source configuration. Available options
|
77
|
-
# are:
|
78
|
-
# <b><tt>:user</tt></b>:: The user used to connect to the database.
|
79
|
-
# <b><tt>:password</tt></b>:: The password to use to connect to the
|
80
|
-
# database (optional).
|
81
|
-
# <b><tt>:dsn</tt></b>:: The dsn string that dbi will use to
|
82
|
-
# access the database.
|
83
|
-
#
|
84
|
-
# Example (setting a source):
|
85
|
-
# source :default, :user => "root",
|
86
|
-
# :password => "clyde",
|
87
|
-
# :dsn => "dbi:mysql:blinkybase"
|
88
|
-
#
|
89
|
-
# Example (retrieving a source):
|
90
|
-
# db = source(:default) #=> <OpenStruct ..>
|
91
|
-
# db.dsn #=> "dbi:mysql:blinkybase"
|
92
|
-
#
|
93
|
-
def source(*args)
|
94
|
-
return sources[args.first] if args.length == 1
|
95
|
-
sources[args.first] = OpenStruct.new(*args[1..-1])
|
96
|
-
check_source(sources[args.first],args.first)
|
97
|
-
end
|
98
|
-
|
99
|
-
# :call-seq:
|
100
|
-
# mailer(mailer_name, options)
|
101
|
-
#
|
102
|
-
# Creates or retrieves a mailer configuration. Available options:
|
103
|
-
# <b><tt>:host</tt></b>:: The SMTP host to use.
|
104
|
-
# <b><tt>:address</tt></b>:: Address the email is being sent from.
|
105
|
-
# <b><tt>:user</tt></b>:: The username to use on the SMTP server
|
106
|
-
# <b><tt>:password</tt></b>:: The password to use on the SMTP server.
|
107
|
-
# Optional.
|
108
|
-
# <b><tt>:port</tt></b>:: The SMTP port to use. Optional, defaults
|
109
|
-
# to 25.
|
110
|
-
# <b><tt>:auth_type</tt></b>:: SMTP authorization method. Optional,
|
111
|
-
# defaults to <tt>:plain</tt>.
|
112
|
-
# <b><tt>:mail_klass</tt></b>:: If you don't want to use the default
|
113
|
-
# <tt>MailFactory</tt> object, you can
|
114
|
-
# pass another mailer to use here.
|
115
|
-
#
|
116
|
-
# Example (creating a mailer config):
|
117
|
-
# mailer :alternate, :host => "mail.test.com",
|
118
|
-
# :address => "test@test.com",
|
119
|
-
# :user => "test",
|
120
|
-
# :password => "blinky"
|
121
|
-
# :auth_type => :cram_md5
|
122
|
-
#
|
123
|
-
# Example (retreiving a mailer config):
|
124
|
-
# mail_conf = mailer(:alternate) #=> <OpenStruct ..>
|
125
|
-
# mail_conf.address #=> test@test.com
|
126
|
-
#
|
127
|
-
def mailer(*args)
|
128
|
-
return mailers[args.first] if args.length == 1
|
129
|
-
mailers[args.first] = OpenStruct.new(*args[1..-1])
|
130
|
-
check_mailer(mailers[args.first],args.first)
|
131
|
-
end
|
132
|
-
|
133
|
-
# The file that <tt>Ruport.log()</tt> will write to.
|
134
|
-
def log_file(file)
|
135
|
-
@logger = Logger.new(file)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Same as <tt>Config.log_file</tt>, but accessor style.
|
139
|
-
def log_file=(file)
|
140
|
-
log_file(file)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Alias for <tt>sources[:default]</tt>.
|
144
|
-
def default_source
|
145
|
-
sources[:default]
|
146
|
-
end
|
147
|
-
|
148
|
-
# Alias for <tt>mailers[:default]</tt>.
|
149
|
-
def default_mailer
|
150
|
-
mailers[:default]
|
151
|
-
end
|
152
|
-
|
153
|
-
# Returns all <tt>source</tt>s defined in this <tt>Config</tt>.
|
154
|
-
def sources; @sources ||= {}; end
|
155
|
-
|
156
|
-
# Returns all the <tt>mailer</tt>s defined in this <tt>Config</tt>.
|
157
|
-
def mailers; @mailers ||= {}; end
|
158
|
-
|
159
|
-
# Returns the currently active logger.
|
160
|
-
def logger; @logger; end
|
161
|
-
|
162
|
-
# returns true if in debug mode
|
163
|
-
def debug_mode?; !!@debug_mode; end
|
164
|
-
|
165
|
-
# Verifies that you have provided a DSN for your source.
|
166
|
-
def check_source(settings,label) # :nodoc:
|
167
|
-
unless settings.dsn
|
168
|
-
Ruport.log(
|
169
|
-
"Missing DSN for source #{label}!",
|
170
|
-
:status => :fatal, :level => :log_only,
|
171
|
-
:raises => ArgumentError
|
172
|
-
)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
# Verifies that you have provided a host for your mailer.
|
177
|
-
def check_mailer(settings, label) # :nodoc:
|
178
|
-
unless settings.host
|
179
|
-
Ruport.log(
|
180
|
-
"Missing host for mailer #{label}",
|
181
|
-
:status => :fatal, :level => :log_only,
|
182
|
-
:raises => ArgumentError
|
183
|
-
)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# forces messages with :level of :log_only to be printed
|
188
|
-
def debug_mode=(something)
|
189
|
-
@debug_mode = !!something
|
190
|
-
end
|
191
|
-
|
192
|
-
# Allows users to set their own accessors on the Config module
|
193
|
-
def method_missing(meth, *args)
|
194
|
-
@config ||= OpenStruct.new
|
195
|
-
|
196
|
-
if args.empty? || meth.to_s =~ /.*=/
|
197
|
-
@config.send(meth, *args)
|
198
|
-
else
|
199
|
-
@config.send("#{meth}=".to_sym, *args)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
end
|
204
|
-
end
|
@@ -1,93 +0,0 @@
|
|
1
|
-
module Ruport::Data
|
2
|
-
|
3
|
-
#
|
4
|
-
# === Overview
|
5
|
-
#
|
6
|
-
# This module provides a simple mechanism for grouping objects based on
|
7
|
-
# tags.
|
8
|
-
#
|
9
|
-
module Groupable
|
10
|
-
|
11
|
-
#
|
12
|
-
# Creates a <tt>Record</tt> made up of <tt>Table</tt>s containing all the
|
13
|
-
# records in the original table with the same tag.
|
14
|
-
#
|
15
|
-
# Example:
|
16
|
-
# table = [['inky', 1],
|
17
|
-
# ['blinky',2],
|
18
|
-
# ['pinky', 3],
|
19
|
-
# ['clyde', 4]].to_table(['name','score'])
|
20
|
-
#
|
21
|
-
# table[0].tag("grp_winners")
|
22
|
-
# table[1].tag("grp_losers")
|
23
|
-
# table[2].tag("grp_winners")
|
24
|
-
# table[3].tag("grp_losers")
|
25
|
-
#
|
26
|
-
# r = table.groups
|
27
|
-
# puts r["winners"]
|
28
|
-
# => +---------------+
|
29
|
-
# | name | score |
|
30
|
-
# +---------------+
|
31
|
-
# | inky | 1 |
|
32
|
-
# | pinky | 3 |
|
33
|
-
# +---------------+
|
34
|
-
#
|
35
|
-
# puts r["losers"]
|
36
|
-
# => +----------------+
|
37
|
-
# | name | score |
|
38
|
-
# +----------------+
|
39
|
-
# | blinky | 2 |
|
40
|
-
# | clyde | 4 |
|
41
|
-
# +----------------+
|
42
|
-
#
|
43
|
-
def groups
|
44
|
-
r_tags = group_names_intern
|
45
|
-
tables_hash = Hash.new { |h,k| h[k] = Table(column_names) }
|
46
|
-
r_tags.each { |t|
|
47
|
-
tables_hash[t.gsub(/^grp_/,"")] = sub_table { |r| r.tags.include? t }}
|
48
|
-
r = Record.new tables_hash, :attributes => group_names
|
49
|
-
end
|
50
|
-
|
51
|
-
# Gets the names of the groups
|
52
|
-
def group_names
|
53
|
-
group_names_intern.map { |r| r.gsub(/^grp_/,"") }
|
54
|
-
end
|
55
|
-
|
56
|
-
# Gets a subtable of the rows matching the group name
|
57
|
-
#
|
58
|
-
def group(tag)
|
59
|
-
sub_table { |r| r.tags.include?("grp_#{tag}") }
|
60
|
-
end
|
61
|
-
|
62
|
-
#
|
63
|
-
# Tags each row of the <tt>Table</tt> for which the <tt>block</tt> is not
|
64
|
-
# false with <tt>label</tt>.
|
65
|
-
#
|
66
|
-
# Example:
|
67
|
-
# table = [['inky', 1],
|
68
|
-
# ['blinky',2],
|
69
|
-
# ['pinky', 3]].to_table(['name','score'])
|
70
|
-
#
|
71
|
-
# table.create_group(:cool_kids) {|r| r.score > 1}
|
72
|
-
# groups = table.groups
|
73
|
-
#
|
74
|
-
# puts groups["cool_kids"]
|
75
|
-
# => +----------------+
|
76
|
-
# | name | score |
|
77
|
-
# +----------------+
|
78
|
-
# | blinky | 2 |
|
79
|
-
# | pinky | 3 |
|
80
|
-
# +----------------+
|
81
|
-
#
|
82
|
-
def create_group(label,&block)
|
83
|
-
each { |r| block[r] && r.tag("grp_#{label}") }
|
84
|
-
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
def group_names_intern
|
89
|
-
map { |r| r.tags.select { |r| r =~ /^grp_/ } }.flatten.uniq
|
90
|
-
end
|
91
|
-
|
92
|
-
end
|
93
|
-
end
|