twb 4.9.10 → 5.2.3

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: 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