twb 4.9.10 → 5.2.3

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: ae42176483937996d559e926704bfeb458797941bf1b886ab07a0826589c6a47
4
- data.tar.gz: cb1995d3c14a0db492631bb00f2d635b0a2abd2df3b04e7dee4639a95e7fe84d
3
+ metadata.gz: ebe8daee637317b5fb8024b346e2eed50883e00839a0d21a031672a55e116f44
4
+ data.tar.gz: 509985403a45d56d8b3fc3690d3d14c934700fe6879d356de9579cd3df6792bc
5
5
  SHA512:
6
- metadata.gz: ee6e513b77ddbfc5eaa758ddc1281c78cad2ce20eb376d7144975d1e05371bef9e05ba32df08d0f587777113bcfa3242e0508c891e97d0690d8a31602740deed
7
- data.tar.gz: dc69868cb98f02ca25ce5cf208ca935c3e16c4a6b18e36dbc90cada288f864aeb7df90df9d891545177a0c66c3f0edf24c2cac711d37797b47c1fd98b4693b74
6
+ metadata.gz: da4d761cceb4011cdd9442ef2ce959a3d74e9b2d1cdc004ef484c6418bfa7bc6d48e1848950a11d73d3af403de7ded1c8a3b4b07b8262b881ac9f2cab0c3c05e
7
+ data.tar.gz: 50fab8909339cb928ed2d717409b66a965c1588b96c47100842e8f667c7a85bdf9c680f66482ca6244ccd23437998b623c4885f2970a8e5a00539a2254560d1d
data/lib/twb.rb CHANGED
@@ -54,6 +54,7 @@ require_relative 'twb/analysis/documentedfieldsmarkdownemitter'
54
54
  require_relative 'twb/analysis/annotatedfieldscsvemitter'
55
55
  require_relative 'twb/analysis/workbooksummaryanalyzer'
56
56
  require_relative 'twb/analysis/calculatedfields/calculatedfieldsanalyzer'
57
+ require_relative 'twb/analysis/calculatedfields/dotanalyzer'
57
58
  require_relative 'twb/analysis/calculatedfields/groupfieldsanalyzer'
58
59
  require_relative 'twb/analysis/calculatedfields/markdownemitter'
59
60
  require_relative 'twb/analysis/calculatedfields/csvemitter'
@@ -80,5 +81,5 @@ require_relative 'twb/analysis/sheets/sheetsintooltipanalyzer'
80
81
  # Represents Tableau Workbooks, their contents, and classes that analyze and manipulate them.
81
82
  #
82
83
  module Twb
83
- VERSION = '4.9.10'
84
+ VERSION = '5.2.3'
84
85
  end
@@ -78,12 +78,12 @@ module Analysis
78
78
  @fieldTables = {}
79
79
 
80
80
 
81
- @@dotHeader = <<DOTHEADER
82
- digraph g {
83
- graph [rankdir="LR" splines=line];
84
- node [shape="box" width="2"];
81
+ # @@dotHeader = <<DOTHEADER
82
+ # digraph g {
83
+ # graph [rankdir="LR" splines=line];
84
+ # node [shape="box" width="2"];
85
85
 
86
- DOTHEADER
86
+ # DOTHEADER
87
87
 
88
88
  def initialize(**args)
89
89
  emit "initialize CalculatedFieldsAnalyzer args #{args}"
@@ -141,7 +141,7 @@ DOTHEADER
141
141
  # end
142
142
  processDataSource ds
143
143
  end
144
- mapTwb
144
+ # mapTwb
145
145
  emitGml
146
146
  @twbCount += 1
147
147
  finis
@@ -195,10 +195,10 @@ DOTHEADER
195
195
  calculatedFields.add calcField.id
196
196
  dsFields[calcField.uiname] = calcField
197
197
  # if @doGraph
198
- calcFieldNode = Twb::Util::Graphnode.new(name: calcField.uiname, id: calcField.id, type: calcField, properties: {:DataSource => ds.uiname})
199
- @nodes.add calcFieldNode
200
- dsFieldEdge = Twb::Util::Graphedge.new(from: dataSourceNode, to: calcFieldNode, relationship: 'contains')
201
- @edges.add dsFieldEdge
198
+ calcFieldNode = Twb::Util::Graphnode.new(name: calcField.uiname, id: calcField.id, type: calcField, properties: {:DataSource => ds.uiname})
199
+ @nodes.add calcFieldNode
200
+ dsFieldEdge = Twb::Util::Graphedge.new(from: dataSourceNode, to: calcFieldNode, relationship: 'contains')
201
+ @edges.add dsFieldEdge
202
202
  # end
203
203
  calculation = calcField.calculation
204
204
  if calculation.has_formula
@@ -249,47 +249,26 @@ DOTHEADER
249
249
  ]
250
250
  end
251
251
  #-- collect fields referenced in formula
252
- emit "# Calculated Fields: #{calculation.calcFields.length}"
253
- calculation.calcFields.each do |rf|
254
- emit " referenced field ::'#{rf}'"
255
- emit " referenced field.name ::'#{rf.name.nil?}' :: '#{rf.name}'"
256
- emit " referenced field.uiname::'#{rf.uiname}'"
257
- # if @doGraph
258
- unless rf.uiname.nil?
259
- properties = {'DataSource' => ds.uiname, 'DataSourceReference' => 'local', :source => rf}
260
- refFieldNode = Twb::Util::Graphnode.new(name: rf.uiname, id: rf.id, type: rf.type, properties: properties)
261
- @nodes.add refFieldNode
262
- fieldFieldEdge = Twb::Util::Graphedge.new(from: calcFieldNode, to: refFieldNode, relationship: 'references')
263
- @edges.add fieldFieldEdge
264
- end
265
- # end
266
- referencedFields.add rf.id
267
- refFieldTable = ds.fieldTable(rf.name)
268
- emit "refFieldTable.nil? : #{refFieldTable.nil?}"
269
- unless refFieldTable.nil?
270
- tableID = refFieldTable + ':::' + ds.uiname
271
- tableName = "||#{refFieldTable}||"
272
- # if @doGraph
273
- tableNode = Twb::Util::Graphnode.new(name: tableName, id: tableID, type: :DBTable, properties: properties)
274
- @nodes.add tableNode
275
- fieldFieldEdge = Twb::Util::Graphedge.new(from: refFieldNode, to: tableNode, relationship: 'is a field in')
276
- @edges.add fieldFieldEdge
277
- # end
278
- # fldToDsNode = tableNode
279
- end
252
+ emit "# Calculated Fields: #{calculation.referencedFields.length}"
253
+ calculation.referencedFields.each do |rf|
254
+ emit " referenced field :: %12s %s " % [ rf.dataSourceName, rf.uiname ]
280
255
  @csvFormulaFields << [
281
256
  @referencedFieldsCount += 1,
282
257
  @twb.name,
283
258
  # @modTime,
284
- ds.uiname,
259
+ rf.dataSourceName, # ds.uiname,
285
260
  calcField.uiname,
286
261
  calculation.formulaFlat,
287
262
  calculation.formulaFlatResolved,
288
263
  rf.name,
289
- rf.uiname,
290
- rf.id,
291
- refFieldTable
264
+ rf.uiname, #.uiname,
265
+ '', # rf.id,
266
+ '', #refFieldTable
292
267
  ]
268
+ refFieldNode = Twb::Util::Graphnode.new(name: rf.uiname, id: rf.id, type: rf, properties: {:DataSource => ds.uiname})
269
+ @nodes.add refFieldNode
270
+ refFieldEdge = Twb::Util::Graphedge.new(from: calcFieldNode, to: refFieldNode , relationship: 'references')
271
+ @edges.add refFieldEdge
293
272
  end # resolvedFields.each do
294
273
  end # if calculation.has_formula
295
274
  end # ds.calculatedFields.each
@@ -330,49 +309,49 @@ DOTHEADER
330
309
  emit "\t formula:: #{calculation.formulaFlat}"
331
310
  end
332
311
 
333
- def mapTwb
334
- twb = @twb.name
335
- rootFields = @twbRootFields
336
- dotStuff = initDot twb
337
- dotFile = dotStuff[:file]
338
- dotFileName = dotStuff[:name]
339
- dotFile.puts "\n // subgraph cluster_1 {"
340
- dotFile.puts " // color= grey;"
341
- dotFile.puts ""
342
- edgesAsStrings = SortedSet.new
343
- # this two step process coalesces the edges into a unique set, avoiding duplicating the dot
344
- # file entries, and can be shrunk when graph edges expose the bits necessary for management by Set
345
- emit "\n========================\nLoading Edges\n========================\n From DC? Referenced? Edge \n %s %s %s" % ['--------', '-----------', '-'*45]
346
- @edges.each do |e|
347
- # don't want to emit edge which is from a Data Connection to a
348
- # Calculated Field which is also referenced by another calculated field
349
- isFromDC = e.from.type == :TwbDataConnection
350
- isRefField = @referencedFields.include?(e.to.id)
351
- edgesAsStrings.add(e.dot) unless isFromDC && isRefField
352
- # emit " ES #{e.dot}"
353
- # emit " ES from #{e.from}"
354
- # emit " ES to #{e.to}"
355
- end
356
- emit "------------------------\n "
357
- edgesAsStrings.each do |es|
358
- dotFile.puts " #{es}"
359
- end
360
- emit "========================\n "
361
- dotFile.puts ""
362
- dotFile.puts " // }"
363
- dotFile.puts "\n\n // 4 NODES --------------------------------------------------------------------"
364
- @nodes.each do |n|
365
- dotFile.puts n.dotLabel
366
- end
367
- dotFile.puts "\n\n // 5--------------------------------------------------------------------"
368
- emitTypes( dotFile )
369
- closeDot( dotFile, twb )
370
- emit "Rendering DOT file - #{twb}"
371
- renderDot(twb,dotFileName,'pdf')
372
- renderDot(twb,dotFileName,'png')
373
- renderDot(twb,dotFileName,'svg')
374
- # emitEdges
375
- end
312
+ # def mapTwb
313
+ # twb = @twb.name
314
+ # rootFields = @twbRootFields
315
+ # dotStuff = initDot twb
316
+ # dotFile = dotStuff[:file]
317
+ # dotFileName = dotStuff[:name]
318
+ # dotFile.puts "\n // subgraph cluster_1 {"
319
+ # dotFile.puts " // color= grey;"
320
+ # dotFile.puts ""
321
+ # edgesAsStrings = SortedSet.new
322
+ # # this two step process coalesces the edges into a unique set, avoiding duplicating the dot
323
+ # # file entries, and can be shrunk when graph edges expose the bits necessary for management by Set
324
+ # emit "\n========================\nLoading Edges\n========================\n From DC? Referenced? Edge \n %s %s %s" % ['--------', '-----------', '-'*45]
325
+ # @edges.each do |e|
326
+ # # don't want to emit edge which is from a Data Connection to a
327
+ # # Calculated Field which is also referenced by another calculated field
328
+ # isFromDC = e.from.type == :TwbDataConnection
329
+ # isRefField = @referencedFields.include?(e.to.id)
330
+ # edgesAsStrings.add(e.dot) unless isFromDC && isRefField
331
+ # # emit " ES #{e.dot}"
332
+ # # emit " ES from #{e.from}"
333
+ # # emit " ES to #{e.to}"
334
+ # end
335
+ # emit "------------------------\n "
336
+ # edgesAsStrings.each do |es|
337
+ # dotFile.puts " #{es}"
338
+ # end
339
+ # emit "========================\n "
340
+ # dotFile.puts ""
341
+ # dotFile.puts " // }"
342
+ # dotFile.puts "\n\n // 4 NODES --------------------------------------------------------------------"
343
+ # @nodes.each do |n|
344
+ # dotFile.puts n.dotLabel
345
+ # end
346
+ # dotFile.puts "\n\n // 5--------------------------------------------------------------------"
347
+ # emitTypes( dotFile )
348
+ # closeDot( dotFile, twb )
349
+ # emit "Rendering DOT file - #{twb}"
350
+ # renderDot(twb,dotFileName,'pdf')
351
+ # renderDot(twb,dotFileName,'png')
352
+ # renderDot(twb,dotFileName,'svg')
353
+ # # emitEdges
354
+ # end
376
355
 
377
356
  def cypher twbName
378
357
  if @doGraph
@@ -488,44 +467,44 @@ DOTHEADER
488
467
  dotFile.puts ' }'
489
468
  end
490
469
 
491
- def initDot twb
492
- dotFileName = docFile("#{twb}#{@@processName}.dot")
493
- dotFile = File.open(dotFileName,'w')
494
- dotFile.puts @@dotHeader
495
- return {:file => dotFile, :name => dotFileName}
496
- end
470
+ # def initDot twb
471
+ # dotFileName = docFile("#{twb}#{@@processName}.dot")
472
+ # dotFile = File.open(dotFileName,'w')
473
+ # dotFile.puts @@dotHeader
474
+ # return {:file => dotFile, :name => dotFileName}
475
+ # end
497
476
 
498
- def closeDot dotFile, twb
499
- dotFile.puts ' '
500
- dotFile.puts '// -------------------------------------------------------------'
501
- dotFile.puts ' '
502
- dotFile.puts ' subgraph cluster_1 {'
503
- # dotFile.puts ' color=white;'
504
- dotFile.puts ' style=invis;'
505
- # dotFile.puts ' border=0;'
506
- dotFile.puts ' node [border=blue];'
507
- dotFile.puts ' '
508
- dotFile.puts ' "" [style=invis]'
509
- dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{twb}'\\n#{Time.new.ctime}\" [penwidth=0]"
510
- # dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
511
- dotFile.puts ' '
512
- dotFile.puts ' }'
513
- dotFile.puts ' '
514
- dotFile.puts '}'
515
- dotFile.close
516
- end
477
+ # def closeDot dotFile, twb
478
+ # dotFile.puts ' '
479
+ # dotFile.puts '// -------------------------------------------------------------'
480
+ # dotFile.puts ' '
481
+ # dotFile.puts ' subgraph cluster_1 {'
482
+ # # dotFile.puts ' color=white;'
483
+ # dotFile.puts ' style=invis;'
484
+ # # dotFile.puts ' border=0;'
485
+ # dotFile.puts ' node [border=blue];'
486
+ # dotFile.puts ' '
487
+ # dotFile.puts ' "" [style=invis]'
488
+ # dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{twb}'\\n#{Time.new.ctime}\" [penwidth=0]"
489
+ # # dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
490
+ # dotFile.puts ' '
491
+ # dotFile.puts ' }'
492
+ # dotFile.puts ' '
493
+ # dotFile.puts '}'
494
+ # dotFile.close
495
+ # end
517
496
 
518
497
 
519
- def renderDot twb, dot, format
520
- imageType = '-T' + format
521
- imageFile = './ttdoc/' + twb + @@processName + 'Graph.' + format
522
- imageParam = '-o"' + imageFile + '"'
523
- emit "system #{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
524
- system "#{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
525
- emit " - #{imageFile}"
526
- @imageFiles << imageFile
527
- return imageFile
528
- end
498
+ # def renderDot twb, dot, format
499
+ # imageType = '-T' + format
500
+ # imageFile = './ttdoc/' + twb + @@processName + 'Graph.' + format
501
+ # imageParam = '-o"' + imageFile + '"'
502
+ # emit "system #{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
503
+ # system "#{@@gvDotLocation} #{imageType} #{imageParam} \"#{dot}\""
504
+ # emit " - #{imageFile}"
505
+ # @imageFiles << imageFile
506
+ # return imageFile
507
+ # end
529
508
 
530
509
  end # class
531
510
 
@@ -0,0 +1,139 @@
1
+ # dotanalyzer.rb - this Ruby script Copyright 2017, 2018 Christopher Gerrard
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+
17
+ module Twb
18
+ module Analysis
19
+ module CalculatedFields
20
+
21
+ class DotAnalyzer
22
+ include TabTool
23
+
24
+ attr_reader :docFileName
25
+
26
+ @@gvDotLocation = 'C:\\tech\\graphviz\\Graphviz2.38\\bin\\dot.exe'
27
+ @@imageTypes = ['pdf', 'png', 'svg']
28
+
29
+ def initialize(**args)
30
+ @args = args
31
+ init
32
+ @funcdoc = {:class=>self.class, :blurb=>'Create Dot files documenting Calculated Fields', :description=>'Analyze Calculated Fields - create Dot files' }
33
+ @metrics = {}
34
+ @imageFiles = Array.new
35
+ end
36
+
37
+ def processTWB twb
38
+ # twb = File.basename(twb)
39
+ @twb = twb #Twb::Workbook.new twb
40
+ addDocFile @dotFile, @dotFileName, "Dot file of Calculated fields for Workbook '#{@twb.name}'"
41
+ @twb.datasources.each do |ds|
42
+ unless ds.calculatedFields.empty?
43
+ initDotFile ds.uiname
44
+ # @dotFile.puts "\n ## #{ds.uiname} \n "
45
+ # @dotFile.puts "__has #{ds.calculatedFields.length} calculated fields__\n "
46
+ @cfCnt = 0
47
+ calcFields = Set.new
48
+ refFields = Set.new
49
+ edges = Set.new
50
+ ds.calculatedFields.each do |cf|
51
+ @cfCnt += 1
52
+ calcFields << cf.uiname
53
+ edges << " \"#{ds.uiname}\" -> \"#{cf.uiname}\" [tailport=e, headport=w] "
54
+ cf.referencedFields.each do |rf|
55
+ refFields << rf.uiname
56
+ edges << " \"#{cf.uiname}\" -> \"#{rf.uiname}\" [tailport=e, headport=w] "
57
+ end
58
+ end # ds.calculatedFields.each
59
+ # "federated.17h7owt0rsacke17cql8o0w2ittk" -> "New AO Actuals Query in PP+ (AO Variance Data)::vs Prior Year [YTD]"
60
+ # "federated.01s5lca037ted31gxs9sg0t9mnnt" [label="Controls" ]
61
+ edges.each do |edge|
62
+ @dotFile.puts "\t #{edge.strip}"
63
+ end
64
+ @dotFile.puts " "
65
+ allFields = calcFields + refFields
66
+ allFields.each do |f|
67
+ @dotFile.puts "\t \"#{f}\" [label=\"#{f}\"]"
68
+ end
69
+ endPointFields = allFields - calcFields
70
+ rankSame(endPointFields) unless endPointFields.nil? || endPointFields.empty?
71
+ closeDotFile
72
+ @@imageTypes.each do |type|
73
+ renderDot type
74
+ end
75
+ end
76
+ end # twb.datasources.each
77
+ finis
78
+ end # def processTwb twb
79
+
80
+ private
81
+
82
+ def initDotFile dsName
83
+ @dotFileName = './ttdoc/' + @twb.name + '.' + dsName + '.CalculatedFields.dot'
84
+ @dotFile = File.open(@dotFileName,'w')
85
+ # @dotFile.puts @@dotHeader
86
+ @dotFile.puts ' digraph g {'
87
+ @dotFile.puts ' graph [rankdir="LR" splines=line];'
88
+ @dotFile.puts ' node [shape="box" width="2"];'
89
+ @dotFile.puts ' '
90
+ @dotFile.puts ' subgraph cluster_0 {'
91
+ end
92
+
93
+ def closeDotFile
94
+ # @dotFile.puts "\n # counted #{@cfCnt} calculated fields\n "
95
+ # @dotFile.puts "\n }"
96
+ @dotFile.puts ' }'
97
+ @dotFile.puts ' '
98
+ @dotFile.puts '// -------------------------------------------------------------'
99
+ @dotFile.puts ' '
100
+ @dotFile.puts ' subgraph cluster_1 {'
101
+ #@dotFile.puts ' color=white;'
102
+ @dotFile.puts ' style=invis;'
103
+ #@dotFile.puts ' border=0;'
104
+ @dotFile.puts ' node [border=blue];'
105
+ @dotFile.puts ' '
106
+ @dotFile.puts ' "" [style=invis]'
107
+ @dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{@twb.name}'\\n#{Time.new.ctime}\" [penwidth=0]"
108
+ #@dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
109
+ @dotFile.puts ' '
110
+ @dotFile.puts ' }'
111
+ @dotFile.puts ' '
112
+ @dotFile.puts '}'
113
+ @dotFile.close
114
+ end
115
+
116
+ def rankSame fields
117
+ @dotFile.puts "\n {rank=same "
118
+ fields.each do |f|
119
+ @dotFile.puts "\t \"#{f}\" "
120
+ end
121
+ @dotFile.puts " } "
122
+ end
123
+
124
+ def renderDot format
125
+ imageType = '-T' + format
126
+ imageFile = @dotFileName + '.Graph.' + format
127
+ imageParam = '-o"' + imageFile + '"'
128
+ emit "system #{@@gvDotLocation} #{imageType} #{imageParam} \"#{@dotFileName}\""
129
+ system "#{@@gvDotLocation} #{imageType} #{imageParam} \"#{@dotFileName}\""
130
+ # emit " - #{imageFile}"
131
+ @imageFiles << imageFile
132
+ return imageFile
133
+ end
134
+
135
+ end # class DotAnalyzer
136
+
137
+ end # nodule CalculatedFields
138
+ end # module Analysis
139
+ end # module Twb
@@ -52,10 +52,10 @@ module CalculatedFields
52
52
  @docFile.puts "#{l.gsub('<<','[').gsub('>>',']')}"
53
53
  end
54
54
  @docFile.puts "```"
55
- if cf.calcFields.length > 0
55
+ if cf.referencedFields.length > 0
56
56
  fieldsRefOrder = []
57
57
  fieldsSortSet = SortedSet.new
58
- cf.calcFields.each do |field|
58
+ cf.referencedFields.each do |field|
59
59
  fieldsRefOrder.push field.uiname
60
60
  fieldsSortSet << field.uiname
61
61
  end
@@ -0,0 +1,6 @@
1
+ require 'twb'
2
+
3
+ twb = Twb::Workbook.new 'MS.twb'
4
+ ds = twb.datasources.to_a.last
5
+ cfs = ds.calculatedFields
6
+ cfs.each { |cf| puts "\n\n--\n#{cf.calculation.formula}\n--"; cf.calcFields.each { |f| puts " %-12s %s " % [f[:ds], f[:field] ] } }
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2014, 2015, 2017 Chris Gerrard
1
+ # Copyright (C) 2014, 2015, 2020 Chris Gerrard
2
2
  #
3
3
  # This program is free software: you can redistribute it and/or modify
4
4
  # it under the terms of the GNU General Public License as published by
@@ -26,7 +26,7 @@ module Twb
26
26
  attr_reader :node, :properties
27
27
  attr_reader :caption, :name, :uiname
28
28
  attr_reader :datatype, :role, :propType
29
- attr_reader :calculation, :calcFields
29
+ attr_reader :calculation, :referencedFields
30
30
  attr_reader :isGroup, :groupMembers
31
31
  attr_reader :hidden
32
32
 
@@ -52,8 +52,8 @@ module Twb
52
52
  @properties ||= loadProperties
53
53
  end
54
54
 
55
- def calcFields
56
- @calculation.calcFields
55
+ def referencedFields
56
+ @calculation.referencedFields
57
57
  end
58
58
 
59
59
  def formulaLines
@@ -14,6 +14,7 @@
14
14
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
15
 
16
16
  require 'nokogiri'
17
+ require 'pry'
17
18
 
18
19
  module Twb
19
20
 
@@ -85,7 +86,6 @@ module Twb
85
86
  @aggregation = load 'aggregation'
86
87
  @autoColumn = load 'auto-column'
87
88
  @datatypeCustomized = load 'datatype-customized'
88
- # @calcField = loadCalcField
89
89
  end
90
90
 
91
91
  def id
@@ -94,7 +94,7 @@ module Twb
94
94
 
95
95
  def load nodeName
96
96
  attr = @node.attribute(nodeName)
97
- val = attr.nil? ? nil : attr.text.strip.gsub(/^\[|\]$/,'')
97
+ val = attr.nil? ? nil : attr.text.strip
98
98
  return val
99
99
  end
100
100
 
@@ -56,7 +56,7 @@ module Twb
56
56
  attr_reader :tableFieldsMap
57
57
  attr_reader :fieldUINames
58
58
  attr_reader :aliases
59
- attr_reader :calculatedFields, :calculatedFieldNamesMap, :calculatedFieldNames, :calculatedField
59
+ attr_reader :calculatedFields, :calculatedFieldsMap, :calculatedFieldNames, :calculatedField
60
60
  attr_reader :allFields
61
61
  attr_reader :groups
62
62
  attr_reader :filters
@@ -391,40 +391,6 @@ module Twb
391
391
  return @fieldUINames
392
392
  end
393
393
 
394
- def loadFieldUINames
395
- # puts 'loadFieldUINames'
396
- @fieldUINames = {}
397
- # puts 'metadataFields'
398
- metadataFields.each do |fld|
399
- # puts " - name:%-45s | uiname:%-45s | localName:%-45s " % [ "'#{fld.name}'", "'#{fld.uiname}'", "'#{fld.localName}'"]
400
- @fieldUINames[fld.uiname] = fld.uiname
401
- @fieldUINames[fld.localName] = fld.uiname unless fld.localName.nil?
402
- @fieldUINames[fld.name] = fld.uiname unless fld.name.nil?
403
- end
404
- # puts 'calculatedFields'
405
- calculatedFields.each do |fld|
406
- # puts " - name:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.uiname}'"]
407
- @fieldUINames[fld.name] = fld.uiname
408
- @fieldUINames[fld.uiname] = fld.uiname
409
- end
410
- # puts 'localFields'
411
- localFields.each do |fld|
412
- # puts " - name:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.uiname}'"]
413
- @fieldUINames[fld.name] = fld.uiname
414
- @fieldUINames[fld.uiname] = fld.uiname
415
- end
416
- # puts "columnFields: #{columnFields.length}"
417
- columnFields.each do |fld|
418
- @fieldUINames[fld.name] = fld.uiname
419
- @fieldUINames[fld.uiname] = fld.uiname
420
- end
421
- groups.each do |fld|
422
- @fieldUINames[fld.name] = fld.uiname
423
- @fieldUINames[fld.uiname] = fld.uiname
424
- end
425
- return @fieldUINames
426
- end
427
-
428
394
  def tableFields
429
395
  @tableFieldsMap.values
430
396
  end
@@ -482,8 +448,7 @@ module Twb
482
448
 
483
449
  def loadAllFields
484
450
  @allFields = SortedSet.new
485
- dbf = dbFields
486
- @allFields << dbf
451
+ @allFields << dbFields
487
452
  @allFields << calculatedFieldNames
488
453
  end
489
454
 
@@ -532,6 +497,53 @@ module Twb
532
497
 
533
498
  private
534
499
 
500
+ def loadFieldUINames
501
+ @fieldUINames = {}
502
+ # puts"metadataFields: #{metadataFields.length}"
503
+ metadataFields.each do |fld|
504
+ # puts" - name:%-45s | caption:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.caption}'", "'#{fld.uiname}'"]
505
+ unless fld.name.nil?
506
+ @fieldUINames[fld.uiname] = fld.uiname
507
+ @fieldUINames[fld.localName] = fld.uiname unless fld.localName.nil?
508
+ @fieldUINames[fld.name] = fld.uiname
509
+ end
510
+ end
511
+ # puts"localFields: #{localFields.length}"
512
+ localFields.each do |fld|
513
+ # puts" - name:%-45s | caption:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.caption}'", "'#{fld.uiname}'"]
514
+ unless fld.caption.nil?
515
+ @fieldUINames[fld.name] = fld.caption
516
+ @fieldUINames[fld.uiname] = fld.caption
517
+ end
518
+ end
519
+ # puts"groups: #{groups.length}"
520
+ groups.each do |fld|
521
+ # puts" - name:%-45s | caption:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.caption}'", "'#{fld.uiname}'"]
522
+ unless fld.caption.nil?
523
+ @fieldUINames[fld.name] = fld.caption
524
+ @fieldUINames[fld.uiname] = fld.caption
525
+ end
526
+ end
527
+ # puts"calculatedFields: #{calculatedFields.length}"
528
+ calculatedFields.each do |fld|
529
+ # puts" - name:%-45s | caption:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.caption}'", "'#{fld.uiname}'"]
530
+ unless fld.caption.nil?
531
+ @fieldUINames[fld.name] = fld.caption
532
+ @fieldUINames[fld.uiname] = fld.caption
533
+ end
534
+ end
535
+ # puts"columnFields: #{columnFields.length}"
536
+ columnFields.each do |fld|
537
+ # puts" - name:%-45s | caption:%-45s | uiname:%-45s " % [ "'#{fld.name}'", "'#{fld.caption}'", "'#{fld.uiname}'"]
538
+ unless fld.caption.nil?
539
+ # @fieldUINames[fld.name] = fld.caption
540
+ @fieldUINames[fld.name.gsub(/^[\[]|[\]]$/,'')] = fld.caption
541
+ @fieldUINames[fld.uiname] = fld.caption
542
+ end
543
+ end
544
+ return @fieldUINames
545
+ end
546
+
535
547
  def loadGroups
536
548
  @groups = []
537
549
  groupNodes = @node.xpath('.//group')
@@ -16,6 +16,7 @@
16
16
  require 'nokogiri'
17
17
  require 'digest/md5'
18
18
  require 'csv'
19
+ require 'pry'
19
20
 
20
21
  module Twb
21
22
 
@@ -35,7 +36,7 @@ module Twb
35
36
  attr_reader :is_tableCalc
36
37
  attr_reader :is_lod, :lodCodePos
37
38
  attr_reader :class, :scopeIsolation
38
- attr_reader :fields, :remoteFields, :calcFields
39
+ attr_reader :fields, :remoteFields, :referencedFields
39
40
  attr_reader :comments, :uuid
40
41
 
41
42
  # attr_accessor :ttlogfile
@@ -120,59 +121,33 @@ module Twb
120
121
  end
121
122
 
122
123
  def formulaResolved
123
- @formulaResolved ||= @formulaResolved = resolveFormula
124
+ @formulaResolved ||= resolveFormula
124
125
  end
125
126
 
126
127
  def resolveFormula
127
- # puts "\ndef resolveFormula:\n--\n#{@formula}"
128
- formula = @formula
129
- parseFormFields # - extracts the fields from the formula; as persisted they're the internal names
130
- @calcFields.each do |calcField|
131
- if calcField.techUIdiff
132
- # puts ":::: #{calcField.techCode} // #{calcField.uiCode}"
133
- formula = formula.gsub(calcField.techCode,calcField.uiCode)
134
- # puts ":--: #{formula}"
135
- end
128
+ # emit "\ndef resolveFormula:\n--\n#{@formula}"
129
+ resolved = @formula
130
+ # emit "\t formula:#{resolved}:"
131
+ # parseFormFields # - extracts the fields from the formula; as persisted they're the internal names
132
+ referencedFields.each do |refField|
133
+ # emit "\t refField: "
134
+ resolved.gsub!(refField.techCode,refField.uiCode)
135
+ # if calcField.techUIdiff
136
+ # # puts ":::: #{calcField.techCode} // #{calcField.uiCode}"
137
+ # formula = formula.gsub(calcField.techCode,calcField.uiCode)
138
+ # # puts ":--: #{formula}"
139
+ # end
136
140
  end
137
- return formula
141
+ # emit "\t formula:#{resolved}:"
142
+ return resolved
138
143
  end
139
144
 
140
- def calcFields
141
- @calcFields ||= parseFormFields
145
+ def referencedFields
146
+ @referencedFields ||= parseFormFields
142
147
  end
143
148
 
144
- def parseFormFields
145
- # puts "--parseFormFields"
146
- @fields = Set.new
147
- @calcFields = Set.new
148
- formula = @formulaFlat
149
- if !formula.nil? && formula.include?('[') && formula.include?(']')
150
- fields = Set.new
151
- # noSqLits = formula.gsub( /'[\[\.\]]+'/, ' ')
152
- quotes = formula.gsub('"',"'")
153
- noSqLits = quotes.gsub( /'[\[\.\]]+'/, ' ')
154
- flatForm = noSqLits.gsub( /\n/, ' ')
155
- stripFrt = flatForm.gsub( /^[^\[]*[\[]/ , '[' )
156
- stripBck = stripFrt.gsub( /\][^\]]+$/ , ']' )
157
- stripMid = stripBck.gsub( /\][^\]]{2,}\[/ , ']]..[[' )
158
- stripCom = stripMid.gsub( /\][ ]*,[ ]*\[/ , ']]..[[' )
159
- stripFns = stripMid.gsub( /\][ ]*[\*\/+\-><,=][ ]*\[/ , ']]..[[' )
160
- fields = stripFns.split(']..[')
161
- emit "::self::: #{self} :: #{__LINE__} :: fields:'#{fields.inspect}'"
162
- fields.each do |field|
163
- emit "::self::: #{self} :: #{__LINE__} :: field:'#{field}'"
164
- cf = CalculationField.new( field.gsub(/^\[|\]$/, ''), @dataSource )
165
- @calcFields.add cf
166
- @fields.add field.gsub(/^\[|\]$/, '')
167
- end
168
- end
169
- return @calcFields
170
- end
171
149
 
172
150
  def formulaResolvedLines
173
- # puts "\ndef formulaResolvedLines\n--\n#{formulaResolved}"
174
- # puts "--\n#{formulaResolved.split(/\n|\r\n/)}"
175
- # puts "--\n#{formulaResolved.split(/\n|\r\n/)}"
176
151
  formulaResolved.split(/\n|\r\n/)
177
152
  end
178
153
 
@@ -212,11 +187,149 @@ module Twb
212
187
  return comments.strip
213
188
  end
214
189
 
215
- end # class FieldCalculation
190
+ private
216
191
 
217
192
 
193
+ def pullString chars
194
+ delim1 = chars.shift
195
+ delim2 = delim1+delim1
196
+ field = delim1
197
+ done = false
198
+ until done | chars.empty?
199
+ s01 = chars[0..1].join
200
+ if !delim1.eql? chars[0]
201
+ field += chars.shift
202
+ else
203
+ case s01
204
+ when delim2
205
+ field += delim2
206
+ chars.shift(2)
207
+ when delim1
208
+ field += delim1
209
+ chars.shift
210
+ done = true
211
+ else
212
+ field += delim1
213
+ chars.shift
214
+ done = true
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ def pullField chars
221
+ # chars = str.split ''
222
+ done = false
223
+ ds = ''
224
+ field = ''
225
+ until done
226
+ s01 = chars[0..1].join
227
+ s02 = chars[0..2].join
228
+ if ']'.eql? chars[0]
229
+ case s01
230
+ when ']]'
231
+ field += ']]'
232
+ chars.shift(2)
233
+ when ']'
234
+ field += chars.shift
235
+ done = true
236
+ else
237
+ if '].['.eql?(s02)
238
+ ds = field + ']'
239
+ chars.shift(2)
240
+ # fldstr = chars.join
241
+ field = pullField(chars)[:field]
242
+ done = true
243
+ else
244
+ field += ']'
245
+ chars.shift
246
+ done = true
247
+ end
248
+ end
249
+ else
250
+ field += chars[0]
251
+ chars.shift
252
+ end
253
+ end
254
+ # puts "field: '#{field}' \t\t ds: #{ds}"
255
+ return {:field => field.sub(/\[/,'').sub(/\]$/,''), :ds => ds.sub(/\[/,'').sub(/\]$/,'') }
256
+ end
218
257
 
219
- class CalculationField
258
+ def parseFormFields # formula
259
+ @referencedFields = Array.new
260
+ rawFields = Array.new
261
+ if !@formula.nil? && @formula.include?('[') && @formula.include?(']')
262
+ chars = formula.split('')
263
+ until chars.empty?
264
+ char0 = chars[0]
265
+ case char0
266
+ when '"', "'"
267
+ pullString(chars)
268
+ when '['
269
+ rawFields << pullField(chars)
270
+ else
271
+ unless chars.nil? | chars.empty?
272
+ chars.shift
273
+ end
274
+ end
275
+ end
276
+ rawFields.each do |rf|
277
+ ds = rf[:ds]
278
+ dataSource = if ''.eql? ds
279
+ @dataSource
280
+ else
281
+ @dataSource.workbook.datasource(ds)
282
+ end
283
+ # fieldUIName = dataSource.fieldUIName(rf[:field])
284
+ refField = ReferencedField.new(rf[:field], dataSource)
285
+ @referencedFields << refField
286
+ end
287
+ end
288
+ return @referencedFields
289
+ end
290
+
291
+ def parseFormFieldsx # formula
292
+ rawFields = Set.new
293
+ if !@formula.nil? && @formula.include?('[') && @formula.include?(']')
294
+ noComms = @formula.gsub(/\/\/.*\r\n/,' ')
295
+ formBase = noComms.gsub(/\r\n/,' ')
296
+ formLen = formBase.length
297
+ formChars = formBase.split ''
298
+ until formChars.empty?
299
+ c = formChars.shift
300
+ case c
301
+ when '"', "'"
302
+ pullString(formChars, c)
303
+ when '['
304
+ rawFields << pullField(formChars, ']', @referencedFields)
305
+ end
306
+ end
307
+ end
308
+ @referencedFields = Set.new
309
+ rawFields.each do |rf|
310
+ # @referencedFields << rf
311
+ dataSource = if ''.eql? rf[:ds]
312
+ @dataSource
313
+ else
314
+ @dataSource.workbook.datasource(rf[:ds])
315
+ end
316
+ # if dataSource.nil?
317
+ # binding.pry
318
+ # end
319
+ fieldUIName = dataSource.fieldUIName(rf[:field])
320
+ # binding.pry
321
+ refField = ReferencedField.new(rf[:field], dataSource)
322
+ # binding.pry
323
+ @referencedFields << refField
324
+ end
325
+ return @referencedFields
326
+ end
327
+
328
+ end # class FieldCalculation
329
+
330
+
331
+ # class CalculationField
332
+ class ReferencedField
220
333
  # is a field used in a calculation, resolved into its human-meaningful form
221
334
 
222
335
  include Comparable
@@ -228,65 +341,29 @@ module Twb
228
341
  attr_reader :fqName, :type
229
342
  attr_reader :techUIdiff
230
343
 
231
- def initialize code, datasource
232
- # puts "\n\nCalculationField :: %-25s | %s " % [datasource.uiname, code]
344
+ def initialize name, datasource
345
+ # puts "\n\nReferencedField :: ds: %-25s | n: %s " % [datasource, name]
346
+ @name = name
233
347
  @dataSource = datasource
234
- @dataSourceName = datasource.uiname
348
+ @dataSourceName = datasource.nil? ? nil : datasource.uiname
235
349
  @dataSourceRef = :local
236
350
  @dataSourceExists = true
351
+ @techCode = "[#{name}]"
237
352
  @techUIdiff = false
238
- @uiname = ''
239
- rawCode = code.gsub(/^\[|\]$/,'')
240
- parts = rawCode.split('].[')
241
- #puts "Field: #{code} \t parts: #{parts.length} - #{parts.inspect}"
242
- if parts.length == 1
243
- @name = parts[0]
244
- @techCode = "[#{parts[0]}]"
245
- #puts "@name: #{@name}"
246
- if datasource.nil?
247
- # puts 'a'
248
- @uiname = @name
249
- @uiCode = @techCode
250
- @techUIdiff = false
251
- else # !datasource.nil?
252
- # puts 'b'
253
- #puts "b - found uiname for '#{@name}'?: #{!datasource.fieldUIName(@name).nil?} \t is:#{datasource.fieldUIName(@name)} "
254
- @uiname = datasource.fieldUIName(@name).nil? ? @name : datasource.fieldUIName(@name)
255
- @uiCode = @uiname.nil? ? @techCode : "[#{@uiname}]"
256
- @techUIdiff = !@techCode.eql?(@uiCode)
257
- # puts ":b #{datasource.fieldUIName(@name).nil?} ... #{@name} ... #{@uiname}"
258
- # puts "CalculationField :: uin: %-25s | @name:%-s" % [@uiname,@name]
259
- end
260
- else # parts.length <> 1
261
- # puts 'c'
262
- rdstech = parts[0]
263
- calcField = parts[1]
264
- @uiname = calcField
265
- @dataSourceName = rdstech
266
- @dataSourceRef = :remote
267
- @techCode = "[#{rdstech}].[#{calcField}]"
268
- workbook = datasource.workbook
269
- @dataSource = workbook.nil? ? nil : workbook.datasource(rdstech)
270
- # puts "\t twb: #{workbook.class} / remoteds: #{remoteds.class} : #{remoteds.nil? ? "<<NOT FOUND:#{rdstech}:>>" : remoteds.uiname} "
271
- #--
272
- if @dataSource.nil? || @dataSource.fieldUIName(calcField).nil?
273
- # puts 'd'
274
- @uiname = calcField
275
- @uiCode = "[<<NOT FOUND>>#{rdstech}].[#{calcField}]"
276
- @techUIdiff = true
277
- @dataSourceExists = false
278
- else # !remoteds.nil?
279
- # puts 'e'
280
- @dataSourceName = @dataSource.uiname
281
- @uiname = @dataSource.fieldUIName(calcField)
282
- @uiCode = "[#{@dataSourceName}].[#{@uiname}]"
283
- @techUIdiff = !@techCode.eql?(@uiCode)
284
- @dataSourceExists = true
285
- end
353
+ if dataSource.nil?
354
+ # puts 'a'
355
+ @uiname = @name
356
+ @uiCode = @techCode
357
+ @techUIdiff = false
358
+ else # !datasource.nil?
359
+ # puts 'b'
360
+ # puts "b - found uiname for '#{@name}'?: #{!datasource.fieldUIName(@name).nil?} \t is:#{datasource.fieldUIName(@name)} "
361
+ @uiname = datasource.fieldUIName(@name).nil? ? @name : datasource.fieldUIName(@name)
362
+ @uiCode = @uiname.nil? ? @techCode : "[#{@uiname}]"
363
+ @techUIdiff = !@techCode.eql?(@uiCode)
364
+ # puts ":b #{datasource.fieldUIName(@name).nil?} ... #{@name} ... #{@uiname}"
365
+ # puts "CalculationField :: uin: %-25s | @name:%-s" % [@uiname,@name]
286
366
  end
287
- # puts "\t dsName: #{@dataSourceName}"
288
- # puts "\t @name: #{@name}"
289
- # puts "\t uiname: #{@uiname}"
290
367
  @fqName = "#{@dataSourceName}::#{@uiname}"
291
368
  @type = if @dataSource.nil?
292
369
  :CalculatedField
@@ -308,6 +385,6 @@ module Twb
308
385
  @fqName <=> other.fqName
309
386
  end
310
387
 
311
- end # class CalculationField
388
+ end # class ReferencedField
312
389
 
313
390
  end # module Twb
@@ -120,7 +120,7 @@ module Twb
120
120
  # filter class='relative-date' column='[Sample - Superstore].[none:Order Date:qk]' first-period='1' include-future='true' include-null='false' last-period='1' period-type='year' />
121
121
  def resolveRelativeDate
122
122
  emit "resolveRelativeDate"
123
- periodType = @node['period-type']
123
+ periodType = @node['period-type'].nil? ? '' : @node['period-type']
124
124
  inclFuture = @node['include-future'] == 'true'
125
125
  firstPeriod = @node['first-period'].to_i
126
126
  lastPeriod = @node['last-period'].to_i
@@ -16,7 +16,9 @@
16
16
  require 'nokogiri'
17
17
 
18
18
  #require 'twb'
19
- require 'C:\tech\Tableau\tools\Ruby\gems\twb\lib\twb.rb'
19
+ # require 'C:\tech\Tableau\tools\Ruby\gems\twb\lib\twb.rb'
20
+ # require 'C:\tech\Tableau\Tableau Tools\Ruby\gems\twb\lib\twb.rb'
21
+ require 'twb'
20
22
  require "test/unit"
21
23
 
22
24
  system "cls"
@@ -25,10 +27,11 @@ class TestFieldCalculation < Test::Unit::TestCase
25
27
 
26
28
  def test_fragment1
27
29
  doc = Nokogiri::XML::Document.parse <<-EOHTML
28
- <calculation class='tableau' formula='abc' />
30
+ <calculation class='tableau' name='I am a formular field' formula='abc' datatype='datatype' role='' type='' class='calcfield' />
29
31
  EOHTML
30
- calcNode = doc.at_xpath('./calculation')
31
- calc = Twb::FieldCalculation.new(calcNode)
32
+ calcNode = doc.at_xpath('./calculation')
33
+ calcField = Twb::CalculatedField.new calcNode
34
+ calc = Twb::FieldCalculation.new(calcField)
32
35
  assert(!calc.nil?)
33
36
  #puts "node: #{calcNode}"
34
37
  #puts "formula: #{calc.formula}"
@@ -41,10 +44,11 @@ EOHTML
41
44
 
42
45
  def test_fragment2
43
46
  doc = Nokogiri::XML::Document.parse <<-EOHTML
44
- <calculation class='tableau' formula='// this is the number of days between the order and shipment&#13;&#10;&#13;&#10;datediff(&apos;day&apos;,[Order Date] , [other].[Ship Date])' />
47
+ <calculation class='tableau' name='Another formula fied' formula='// this is the number of days between the order and shipment&#13;&#10;&#13;&#10;datediff(&apos;day&apos;,[Order Date] , [other].[Ship Date])' />
45
48
  EOHTML
46
49
  calcNode = doc.at_xpath('./calculation')
47
- calc = Twb::FieldCalculation.new(calcNode)
50
+ calcField = Twb::CalculatedField calcNode
51
+ calc = Twb::FieldCalculation.new(calcField)
48
52
  assert(!calc.nil?)
49
53
  #puts "node: #{calcNode}"
50
54
  #puts "formula: #{calc.formula}"
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.9.10
4
+ version: 5.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Gerrard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-04 00:00:00.000000000 Z
11
+ date: 2020-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: creek
@@ -64,9 +64,11 @@ files:
64
64
  - lib/twb/analysis/annotatedfieldscsvemitter.rb
65
65
  - lib/twb/analysis/calculatedfields/calculatedfieldsanalyzer.rb
66
66
  - lib/twb/analysis/calculatedfields/csvemitter.rb
67
+ - lib/twb/analysis/calculatedfields/dotanalyzer.rb
67
68
  - lib/twb/analysis/calculatedfields/fieldsaliasesanalyzer.rb
68
69
  - lib/twb/analysis/calculatedfields/groupfieldsanalyzer.rb
69
70
  - lib/twb/analysis/calculatedfields/markdownemitter.rb
71
+ - lib/twb/analysis/calculatedfields/t.rb
70
72
  - lib/twb/analysis/datasources/categoricalcolorcodinganalyzer.rb
71
73
  - lib/twb/analysis/datasources/datasourcefieldsanalyzer.rb
72
74
  - lib/twb/analysis/datasources/datasourcefieldscsvemitter.rb