twb 4.6.1 → 4.6.2

Sign up to get free protection for your applications and to get access to all the features.
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