twb 4.6.1 → 4.6.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 199291219b0d5c06c822c4eedce63579a9a7cf80a1c4acfc53429403aeddc7f6
4
- data.tar.gz: c6889a7c889c8c0890bde9f6e9e8b8a016af6a319d75dc3634d266ce36a7cbab
3
+ metadata.gz: 24b17be9e4300e5a4b463cd41aac99d2a1802973d0ff6f348588dfc9004c8624
4
+ data.tar.gz: 74a3828145f626709a332b7e678444d7a1063f2c14e77cd799d19fbfaa49a9d5
5
5
  SHA512:
6
- metadata.gz: 74f35bd36b750b031999d6fbdfc3f57113114ab54c8fbec33e9874203402e09c24374619ec74af683cd5c687e5dea5d8e5a7a2ec5f99a06851f975590aeceabc
7
- data.tar.gz: 92af9190a8661aee7e1e45fe689c7737c802b617ba74eac8efd409c5c7b5569bbfa5a822bd16e4b9b4a18f7d297c889c8e27c1ab566df105722ae32db1b01bcb
6
+ metadata.gz: 767720b5f5ec250225eac1e5ad72c3b913251341f210ef2e663ba442962d17bd675335f12b07b93ac1343a589b2b807b944ff498985f687508a7567bb5230e06
7
+ data.tar.gz: f70f2834c01269f5b2ebde7aa363e6536db5db413781b39e9881b4ba2a1953883b6f147e31e4c3af92586cb5f01b08e6d75f57636982da0f9c6c2d32a1f113f0
data/lib/twb.rb CHANGED
@@ -72,5 +72,5 @@ require_relative 'twb/analysis/sheets/dashsheetsanalyzer'
72
72
  # Represents Tableau Workbooks, their contents, and classes that analyze and manipulate them.
73
73
  #
74
74
  module Twb
75
- VERSION = '4.6.1'
75
+ VERSION = '4.6.2'
76
76
  end
@@ -13,29 +13,512 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
15
 
16
- require 'twb'
16
+ # require 'nokogiri'
17
+ # require 'twb'
18
+ # require 'set'
19
+ require 'csv'
17
20
 
18
21
  module Twb
19
22
  module Analysis
20
23
 
21
-
22
24
  class CalculatedFieldsAnalyzer
23
25
 
24
- include TabTool
26
+ include TabTool
27
+ include Graph
28
+
29
+ attr_reader :calculatedFieldsCount, :referencedFieldsCount, :metrics
30
+ attr_accessor :ttdocdir
31
+
32
+ @@ttlogfile = 'CalculatedFieldsAnalyzer.ttlog'
33
+ @@gvDotLocation = 'C:\\tech\\graphviz\\Graphviz2.38\\bin\\dot.exe'
34
+ @@processName = '.CalculatedFields'
35
+
36
+ @@calcFieldsCSVFileName = 'CalculatedFields.csv'
37
+ @@calcFieldsCSVFileHeader = ['Record #',
38
+ 'Workbook',
39
+ # 'Workbook Modified',
40
+ 'Data Source', 'Data Source Caption', 'Data Source Name (tech)',
41
+ 'Field Name', 'Field Caption', 'Field Name (tech)',
42
+ 'Data Source + Field Name (tech)',
43
+ 'Data Type', 'Role', 'Type',
44
+ 'Class',
45
+ 'Scope Isolation',
46
+ 'Formula Length',
47
+ 'Formula',
48
+ 'Formula (tech)',
49
+ 'Formula Comments',
50
+ 'Formula LOD?'
51
+ ]
52
+
53
+ @@calcLinesCSVFileName = 'CalculatedFieldsFormulaLines.csv'
54
+ @@calcLinesCSVFileHeader = ['Record #',
55
+ 'Workbook',
56
+ # 'Workbook Modified',
57
+ 'Data Source', 'Data Source Caption', 'Data Source Name (tech)',
58
+ 'Field Name', 'Field Caption', 'Field Name (tech)',
59
+ 'Formula', 'Formula Line #', 'Formula Line'
60
+ ]
61
+
62
+ @@formFieldsCSVFileName = 'CalculatedFieldsReferenced.csv'
63
+ @@formFieldsCSVFileHeader = ['Record #',
64
+ 'Workbook',
65
+ # 'Workbook Modified',
66
+ 'Data Source',
67
+ 'Field - Calculated',
68
+ 'Formula (tech)',
69
+ 'Formula',
70
+ 'Field - Referenced (tech)',
71
+ 'Field - Referenced',
72
+ 'Data Source + Field - Referenced',
73
+ 'Table'
74
+ ]
75
+
76
+ @techUINames = {}
77
+ @fieldTables = {}
78
+
79
+
80
+ @@dotHeader = <<DOTHEADER
81
+ digraph g {
82
+ graph [rankdir="LR" splines=line];
83
+ node [shape="box" width="2"];
84
+
85
+ DOTHEADER
25
86
 
26
87
  def initialize(**args)
88
+ emit "initialize CalculatedFieldsAnalyzer args #{args}"
89
+ @args = args
90
+ @recordDir = !@args.nil? && @args[:recordDir] == true
91
+ @ttdocdir = @args[:ttdocdir]
92
+ @csvAdd = @args[:csvMode] == :add
93
+ @csvMode = @csvAdd ? 'a' : 'w'
94
+ init
95
+ @funcdoc = {:class=>self.class, :blurb=>'Analyze Calculated Fields', :description=>'Calculated fields can be complex, this tool provides robust coverage.',}
96
+ #-- CSV records collectors
97
+ @csvCalculatedFields = Array.new
98
+ @csvFormulaFields = Array.new
99
+ @csvFormulaLines = Array.new
100
+ #-- Counters setup --
101
+ @twbCount = 0
102
+ @dataSourcesCount = 0
103
+ @calculatedFieldsCount = 0
104
+ @referencedFieldsCount = 0
105
+ #--
106
+ @referencedFields = SortedSet.new
107
+ #--
108
+ twbdirLabel = @recordDir.nil? ? nil : 'Workbook Dir'
109
+ @csvCF = initCSV(@@calcFieldsCSVFileName, 'Calculated fields and their formulas.', @@calcFieldsCSVFileHeader )
110
+ @csvCFLs = initCSV(@@calcLinesCSVFileName, "Calculated fields and their formulas' individual lines.", @@calcLinesCSVFileHeader )
111
+ @csvFF = initCSV(@@formFieldsCSVFileName, 'Calculated fields and the fields their formulas reference.', @@formFieldsCSVFileHeader )
112
+ # TODO migrate addition of 'Workbook Dir' to CSV header to TabTool
113
+ #--
114
+ @localEmit = false
115
+ @imageFiles = Array.new
116
+ #--
117
+ @doGraph = config(:dograph)
27
118
  end
28
119
 
29
120
  def processTWB workbook
121
+ @twb = workbook.is_a?(String) ? Twb::Workbook.new(workbook) : workbook
122
+ throw Exception unless @twb.is_a? Twb::Workbook
123
+ emit "- Workbook: #{workbook}"
124
+ emit " version: #{@twb.version}"
125
+ @twbDir = @twb.dir #File.dirname(File.expand_path(workbook))
126
+ @modTime = @twb.modtime
127
+ @edges = Set.new
128
+ #-- processing
129
+ dss = @twb.datasources
130
+ # puts " # data sources: #{dss.length}"
131
+ @twbRootFields = Set.new
132
+ @twbFields = Hash.new { |h,k| h[k] = [] }
133
+ @nodes = Set.new
134
+ dss.each do |ds|
135
+ @dataSourcesCount += 1
136
+ # puts "\t\t - #{ds.uiname} \t\t #{ds.calculatedFields.length}"
137
+ next if ds.Parameters? # don't process the Parameters data source - Parameters' fields aren't Calculated fields for our purposes
138
+ # dataSourceNode = Twb::Util::Graphnode.new(name: ds.uiname, id: ds.id, type: ds, properties: {workbook: workbook})
139
+ # @nodes.add dataSourceNode
140
+ # ds.calculatedFields.each do |calcField|
141
+ # end
142
+ processDataSource ds
143
+ end
144
+ mapTwb
145
+ emitGml
146
+ @twbCount += 1
147
+ finis
30
148
  end
31
149
 
32
150
  def metrics
33
- Array.new
151
+ @metrics ||= loadMetrics
152
+ end
153
+
154
+ def loadMetrics
155
+ @metrics = {
156
+ '# of Workbooks' => @twbCount,
157
+ '# of Data Sources' => @dataSourcesCount,
158
+ '# of Calculated Fields' => @calculatedFieldsCount,
159
+ '# of Referenced Fields' => @referencedFieldsCount,
160
+ }
34
161
  end
35
162
 
36
163
  #-- private methods begin here, to end of class
37
164
 
38
-
165
+ private
166
+
167
+ def emitGml
168
+ gml = Twb::Util::GML.new
169
+ gml.fileName = @twb.name
170
+ gml.nodes = @nodes
171
+ gml.edges = @edges
172
+ gml.render
173
+ end
174
+
175
+ def processDataSource ds
176
+ emit "======= DATA SOURCE: #{ds.uiname} ====== "
177
+ dsNodes = Set.new
178
+ dsEdges = Set.new
179
+ dsFields = {}
180
+ @twbFields[ds.uiname] = dsFields
181
+ calculatedFields = SortedSet.new
182
+ fieldFormulaLines = Array.new
183
+ referencedFields = SortedSet.new
184
+ # if @doGraph
185
+ dataSourceNode = Twb::Util::Graphnode.new(name: ds.uiname, id: ds.id, type: ds, properties: {workbook: @twb.name})
186
+ @nodes.add dataSourceNode
187
+ # end
188
+ #-- process Calculatred Fields
189
+ ds.calculatedFields.each do |calcField|
190
+ emit "Calculated Field: #{calcField}"
191
+ calculatedFields.add calcField.id
192
+ dsFields[calcField.uiname] = calcField
193
+ # if @doGraph
194
+ calcFieldNode = Twb::Util::Graphnode.new(name: calcField.uiname, id: calcField.id, type: calcField, properties: {:DataSource => ds.uiname})
195
+ @nodes.add calcFieldNode
196
+ dsFieldEdge = Twb::Util::Graphedge.new(from: dataSourceNode, to: calcFieldNode, relationship: 'contains')
197
+ @edges.add dsFieldEdge
198
+ # end
199
+ calculation = calcField.calculation
200
+ if calculation.has_formula
201
+ #-- collect field formulas as single lines
202
+ @csvCalculatedFields.push [
203
+ @calculatedFieldsCount += 1,
204
+ @twb.name,
205
+ # @modTime,
206
+ ds.uiname,
207
+ ds.caption,
208
+ ds.name,
209
+ calcField.uiname,
210
+ calcField.caption,
211
+ calcField.name,
212
+ ds.name + '::' + calcField.name,
213
+ calcField.datatype,
214
+ calcField.role,
215
+ calcField.type,
216
+ calculation.class,
217
+ calculation.scopeIsolation,
218
+ calculation.formulaFlat.length,
219
+ calculation.formulaFlatResolved,
220
+ calculation.formulaFlat,
221
+ calculation.comments,
222
+ calculation.is_lod
223
+ ]
224
+ #-- collect individual formula lines
225
+ flnum = 0
226
+ emit "@@ calcField.uiname: #{calcField.uiname}"
227
+ calculation.formulaResolvedLines.each do |fl|
228
+ emit "@@ resolved line:: => '#{fl}'"
229
+ fieldFormulaLines << [ @calculatedFieldsCount, # 'Calc Field #',
230
+ @twb.name, # 'Workbook',
231
+ # @modTime,
232
+ ds.uiname, # 'Data Source',
233
+ ds.caption, # 'Data Source Caption',
234
+ ds.name, # 'Data Source Name (tech)',
235
+ calcField.uiname, # 'Field Name',
236
+ calcField.caption, # 'Field Caption',
237
+ calcField.name, # 'Field Name (tech)',
238
+ calcField.calculation.formulaFlatResolved, # 'Formula'
239
+ flnum += 1, # 'Formula Line #',
240
+ fl.start_with?(" ") ? "'#{fl}" : fl # 'Formula Line' - THIS IS A STUPID HACK NEEDED BECAUSE TABLEAU STRIPS LEADING BLANKS FROM CSV VALUES
241
+ ]
242
+ end
243
+ #-- collect fields referenced in formula
244
+ emit "# Calculated Fields: #{calculation.calcFields.length}"
245
+ calculation.calcFields.each do |rf|
246
+ emit " referenced field ::'#{rf}'"
247
+ emit " referenced field.name ::'#{rf.name.nil?}' :: '#{rf.name}'"
248
+ emit " referenced field.uiname::'#{rf.uiname}'"
249
+ # if @doGraph
250
+ unless rf.uiname.nil?
251
+ properties = {'DataSource' => ds.uiname, 'DataSourceReference' => 'local', :source => rf}
252
+ refFieldNode = Twb::Util::Graphnode.new(name: rf.uiname, id: rf.id, type: rf.type, properties: properties)
253
+ @nodes.add refFieldNode
254
+ fieldFieldEdge = Twb::Util::Graphedge.new(from: calcFieldNode, to: refFieldNode, relationship: 'references')
255
+ @edges.add fieldFieldEdge
256
+ end
257
+ # end
258
+ referencedFields.add rf.id
259
+ refFieldTable = ds.fieldTable(rf.name)
260
+ emit "refFieldTable.nil? : #{refFieldTable.nil?}"
261
+ unless refFieldTable.nil?
262
+ tableID = refFieldTable + ':::' + ds.uiname
263
+ tableName = "||#{refFieldTable}||"
264
+ # if @doGraph
265
+ tableNode = Twb::Util::Graphnode.new(name: tableName, id: tableID, type: :DBTable, properties: properties)
266
+ @nodes.add tableNode
267
+ fieldFieldEdge = Twb::Util::Graphedge.new(from: refFieldNode, to: tableNode, relationship: 'is a field in')
268
+ @edges.add fieldFieldEdge
269
+ # end
270
+ # fldToDsNode = tableNode
271
+ end
272
+ @csvFormulaFields << [
273
+ @referencedFieldsCount += 1,
274
+ @twb.name,
275
+ # @modTime,
276
+ ds.uiname,
277
+ calcField.uiname,
278
+ calculation.formulaFlat,
279
+ calculation.formulaFlatResolved,
280
+ rf.name,
281
+ rf.uiname,
282
+ rf.id,
283
+ refFieldTable
284
+ ]
285
+ end # resolvedFields.each do
286
+ end # if calculation.has_formula
287
+ end # ds.calculatedFields.each
288
+
289
+ dsRootFields = calculatedFields - referencedFields
290
+ @referencedFields.merge referencedFields
291
+ @twbRootFields.merge dsRootFields
292
+ if @doGraph
293
+ cypher @twb.name
294
+ cypherPy @twb.name
295
+ end
296
+ emit "#######################"
297
+ #--
298
+ #-- record calculated fields
299
+ twbDirCSV = @recordDir.nil? ? nil : @twbDir
300
+ emit "@@ record calculated fields ds: #{ds.uiname}"
301
+ @csvCalculatedFields.each do |r|
302
+ @csvCF << r
303
+ end
304
+ #-- record individual formula lines
305
+ emit "@@ individual formula lines ds: #{ds.uiname}"
306
+ fieldFormulaLines.each do |ffl|
307
+ @csvCFLs << ffl
308
+ end
309
+ #-- record formula-referenced fields
310
+ emit "@@ formula-referenced fields ds: #{ds.uiname}"
311
+ @csvFormulaFields.each do |r|
312
+ @csvFF << r
313
+ end
314
+ #--
315
+ return @imageFiles
316
+ end # def processDataSource
317
+
318
+ def emitCalcfield calcField
319
+ emit "\t FIELD cap :: #{calcField.caption} "
320
+ emit "\t tname:: #{calcField.name}"
321
+ emit "\t uiname:: #{calcField.uiname}"
322
+ emit "\t formula:: #{calculation.formulaFlat}"
323
+ end
324
+
325
+ def mapTwb
326
+ twb = @twb.name
327
+ rootFields = @twbRootFields
328
+ dotStuff = initDot twb
329
+ dotFile = dotStuff[:file]
330
+ dotFileName = dotStuff[:name]
331
+ dotFile.puts "\n // subgraph cluster_1 {"
332
+ dotFile.puts " // color= grey;"
333
+ dotFile.puts ""
334
+ edgesAsStrings = SortedSet.new
335
+ # this two step process coalesces the edges into a unique set, avoiding duplicating the dot
336
+ # file entries, and can be shrunk when graph edges expose the bits necessary for management by Set
337
+ emit "\n========================\nLoading Edges\n========================\n From DC? Referenced? Edge \n %s %s %s" % ['--------', '-----------', '-'*45]
338
+ @edges.each do |e|
339
+ # don't want to emit edge which is from a Data Connection to a
340
+ # Calculated Field which is also referenced by another calculated field
341
+ isFromDC = e.from.type == :TwbDataConnection
342
+ isRefField = @referencedFields.include?(e.to.id)
343
+ edgesAsStrings.add(e.dot) unless isFromDC && isRefField
344
+ # emit " ES #{e.dot}"
345
+ # emit " ES from #{e.from}"
346
+ # emit " ES to #{e.to}"
347
+ end
348
+ emit "------------------------\n "
349
+ edgesAsStrings.each do |es|
350
+ dotFile.puts " #{es}"
351
+ end
352
+ emit "========================\n "
353
+ dotFile.puts ""
354
+ dotFile.puts " // }"
355
+ dotFile.puts "\n\n // 4 NODES --------------------------------------------------------------------"
356
+ @nodes.each do |n|
357
+ dotFile.puts n.dotLabel
358
+ end
359
+ dotFile.puts "\n\n // 5--------------------------------------------------------------------"
360
+ emitTypes( dotFile )
361
+ closeDot( dotFile, twb )
362
+ emit "Rendering DOT file - #{twb}"
363
+ renderDot(twb,dotFileName,'pdf')
364
+ renderDot(twb,dotFileName,'png')
365
+ renderDot(twb,dotFileName,'svg')
366
+ # emitEdges
367
+ end
368
+
369
+ def cypher twbName
370
+ if @doGraph
371
+ cypher = Twb::Util::Cypher.new
372
+ cypher.fileName = "#{twbName}.calcFields"
373
+ cypher.nodes = @nodes
374
+ cypher.edges = @edges
375
+ cypher.render
376
+ end
377
+ end
378
+
379
+ def cypherPy twbName
380
+ if @doGraph
381
+ cypher = Twb::Util::CypherPython.new
382
+ cypher.fileName = "#{twbName}.calcFields"
383
+ cypher.nodes = @nodes
384
+ cypher.edges = @edges
385
+ cypher.render
386
+ end
387
+ end
388
+
389
+ # def graphEdges twb
390
+ # # graphFile = File.new(twb + '.cypher', 'w')
391
+ # # # graphFile.puts "OKEY DOKE, graphing away"
392
+ # # cypherCode = Set.new
393
+ # # @edges.each do |edge|
394
+ # # cypherCode.add edge.from.cypherCreate
395
+ # # cypherCode.add edge.to.cypherCreate
396
+ # # cypherCode.add edge.cypherCreate
397
+ # # end
398
+ # # cypherCode.each do |cc|
399
+ # # graphFile.puts cc
400
+ # # end
401
+ # # graphFile.puts "\nreturn *"
402
+ # # graphFile.close unless graphFile.nil?
403
+ # # @imageFiles << File.basename(graphFile)
404
+ # end
405
+
406
+ def emitEdges
407
+ emit " %-15s %s" % ['type', 'Edge']
408
+ emit " %-15s %s" % ['-'*15, '-'*35]
409
+ @edges.each do |edge|
410
+ emit " %-15s %s" % [edge.from.type, edge.from]
411
+ emit " %-15s %s" % [edge.to.type, edge.to]
412
+ emit "\n "
413
+ end
414
+ end
415
+
416
+ def emitTypes dotFile
417
+ typedNodes = {}
418
+ dotFile.puts "\n\n // 2--------------------------------------------------------------------"
419
+ @edges.each do |edge|
420
+ emit " EDGE :: #{edge}"
421
+ loadNodeType typedNodes, edge.from
422
+ loadNodeType typedNodes, edge.to
423
+ end
424
+ typedNodes.each do |type, nodes|
425
+ # emit "+++++++++ typedNodes of '#{type}'' "
426
+ # nodes.each do |node|
427
+ # emit " -n- #{node}"
428
+ # end
429
+ rankSame(dotFile, type, nodes) unless type.eql? 'CalculatedField' # == :CalculatedField
430
+ end
431
+ # labelTypes dotFile, edges
432
+ end
433
+
434
+ def loadNodeType set, node
435
+ type = node.type
436
+ set[type] = Set.new unless set.include? type
437
+ set[type].add node
438
+ end
439
+
440
+ @@unrankedTypes = ['CalculationField']
441
+ def rankSame dotFile, type, nodes
442
+ return if @@unrankedTypes.include? type.to_s
443
+ @lines = SortedSet.new
444
+ nodes.each do |node|
445
+ @lines << node.id
446
+ end
447
+ dotFile.puts "\n // '#{type}' --------------------------------------------------------------------"
448
+ dotFile.puts "\n {rank=same "
449
+ @lines.each do |line|
450
+ dotFile.puts " \"#{line}\""
451
+ end
452
+ dotFile.puts " }"
453
+ end
454
+
455
+ def rankRootFields dotFile, dsRootFields
456
+ dotFile.puts "\n // Unreferenced (root) Calculated Fields -----------------------------------------"
457
+ dotFile.puts "\n {rank=same "
458
+ dsRootFields.each do |rf|
459
+ emit "ROOT FIELD: #{rf.class} :: #{rf}"
460
+ dotFile.puts " \"#{rf}\""
461
+ end
462
+ dotFile.puts " }"
463
+ end
464
+
465
+ def labelTypes dotFile
466
+ fromTos = Set.new
467
+ @edges.each do |edge|
468
+ # fromTos.add "\"Alien Data Source\" -> \"Alien Data Source\""
469
+ fromTos.add "\"#{edge.from.type}\""
470
+ fromTos.add "\"#{edge.to.type}\""
471
+ end
472
+ return if fromTos.empty?
473
+ dotFile.puts "\n // 3--------------------------------------------------------------------"
474
+ dotFile.puts ' subgraph cluster_0 {'
475
+ dotFile.puts ' color=white;'
476
+ dotFile.puts ' node [shape="box3d" style="filled" ];'
477
+ fromTos.each do |ft|
478
+ dotFile.puts " #{ft}"
479
+ end
480
+ dotFile.puts ' }'
481
+ end
482
+
483
+ def initDot twb
484
+ dotFileName = docFile("#{twb}#{@@processName}.dot")
485
+ dotFile = File.open(dotFileName,'w')
486
+ dotFile.puts @@dotHeader
487
+ return {:file => dotFile, :name => dotFileName}
488
+ end
489
+
490
+ def closeDot dotFile, twb
491
+ dotFile.puts ' '
492
+ dotFile.puts '// -------------------------------------------------------------'
493
+ dotFile.puts ' '
494
+ dotFile.puts ' subgraph cluster_1 {'
495
+ # dotFile.puts ' color=white;'
496
+ dotFile.puts ' style=invis;'
497
+ # dotFile.puts ' border=0;'
498
+ dotFile.puts ' node [border=blue];'
499
+ dotFile.puts ' '
500
+ dotFile.puts ' "" [style=invis]'
501
+ dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{twb}'\\n#{Time.new.ctime}\" [penwidth=0]"
502
+ # dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
503
+ dotFile.puts ' '
504
+ dotFile.puts ' }'
505
+ dotFile.puts ' '
506
+ dotFile.puts '}'
507
+ dotFile.close
508
+ end
509
+
510
+
511
+ def renderDot twb, dot, format
512
+ imageType = '-T' + format
513
+ imageFile = './ttdoc/' + twb + @@processName + 'Graph.' + format
514
+ imageParam = '-o"' + imageFile + '"'
515
+ emit "system #{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
516
+ system "#{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
517
+ emit " - #{imageFile}"
518
+ @imageFiles << imageFile
519
+ return imageFile
520
+ end
521
+
39
522
  end # class
40
523
 
41
524
  end # module Analysis
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.1
4
+ version: 4.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Gerrard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-20 00:00:00.000000000 Z
11
+ date: 2019-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: creek