twb 1.0.5 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2014, 2015 Chris Gerrard
1
+ # Copyright (C) 2014, 2015, 2017 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
@@ -19,141 +19,144 @@ module Twb
19
19
 
20
20
  class FieldCalculation
21
21
 
22
- attr_reader :node, :fieldNode, :dataSource
23
- attr_reader :formula, :resolvedFormula, :formulaLines, :formulaFlat
24
- attr_reader :uiname, :techname, :caption
22
+ attr_reader :node, :field, :fieldNode, :dataSource
23
+ attr_reader :uiname, :techname, :caption
24
+ attr_reader :has_formula
25
+ attr_reader :formula
26
+ attr_reader :formulaUC
27
+ attr_reader :formulaResolved
28
+ attr_reader :formulaFlat
29
+ attr_reader :formulaFlatResolved
30
+ attr_reader :formulaLines
31
+ attr_reader :is_tableCalc
32
+ attr_reader :is_lod, :lodCodePos
25
33
  attr_reader :class, :scopeIsolation
26
- attr_reader :fields, :resolvedFields, :remoteFields
34
+ attr_reader :fields, :remoteFields, :calcFields
27
35
  attr_reader :comments
28
36
 
29
- def initialize(calcNode, datasource=nil)
30
- if calcNode
31
- @node = calcNode
32
- @fieldNode = @node.at_xpath('..')
37
+ @@tableCalcs = [ 'FIRST', 'INDEX', 'LAST', 'SIZE',
38
+ 'LOOKUP', 'PREVIOUS_VALUE',
39
+ 'RANK', 'RANK_DENSE', 'RANK_MODIFIED', 'RANK_PERCENTILE', 'RANK_UNIQUE',
40
+ 'RUNNING_AVG', 'RUNNING_COUNT', 'RUNNING_MAX', 'RUNNING_MIN', 'RUNNING_SUM',
41
+ 'SCRIPT_BOOL', 'SCRIPT_INT', 'SCRIPT_REAL', 'SCRIPT_STR',
42
+ 'TOTAL',
43
+ 'WINDOW_SUM', 'WINDOW_AVG', 'WINDOW_COUNT',
44
+ 'WINDOW_MIN', 'WINDOW_MEDIAN', 'WINDOW_MAX', 'WINDOW_PERCENTILE',
45
+ 'WINDOW_STDEV', 'WINDOW_STDEVP',
46
+ 'WINDOW_CORR', 'WINDOW_COVAR', 'WINDOW_COVARP',
47
+ 'WINDOW_VAR', 'WINDOW_VARP'
48
+ ]
49
+
50
+ def initialize(calcField, datasource=nil)
51
+ raise ArgumentError.new("FieldCalculation must be initialized with a CalculatedField, has been provided with a #{calcField.class}") if calcField.class != Twb::CalculatedField
52
+ @field = calcField
53
+ calcNode = calcField.node
54
+ @istableCalc = false
55
+ # puts "FieldCalculation calcNode.nil? :: #{calcNode.nil?} "
56
+ unless calcNode.nil?
57
+ @node = calcNode.at_xpath('./calculation')
58
+ @fieldNode = calcField.node
33
59
  @dataSource = datasource
34
60
  @class = attribText(@node, 'class')
35
61
  @remoteFields = {}
36
62
  if 'categorical-bin'.eql? @class
37
- # puts calcNode
38
63
  # <calculation class='categorical-bin'
39
64
  # column='[Calculation_507569757376950272]'
40
65
  # default='&quot;Other&quot;'
41
66
  # new-bin='true'>
42
- @techname = calcNode.attribute('column').text.gsub(/^\[|\]$/,'') # assumes the column attribute exists
67
+ @techname = @node.attribute('column').text.gsub(/^\[|\]$/,'') # assumes the column attribute exists
43
68
  @caption = attribText(@fieldNode, 'caption')
44
69
  uiname = if datasource.nil?
45
70
  @techName
46
71
  else
47
72
  datasource.fieldUIName(@techname)
48
73
  end
49
- @uiname = "#{uiname} (group)"
50
- @formula = "grouped '#{uiname}' values"
51
- @comments = [ ]
52
- @resolvedFields = [ {:field => uiname, :fieldui => uiname, :source => nil} ]
74
+ @uiname = "#{uiname} <<group>>"
75
+ @has_formula = true
76
+ @formula = "grouped <<#{uiname}>> values"
77
+ @formulaLines = [ @formula ]
78
+ @formulaFlat = @formula
79
+ @comments = [ ]
80
+ @is_lod = false
53
81
  else
54
- # field names
55
- # puts "\n****** "
56
- # puts " fnode: #{@fieldNode}\n****** "
57
- @caption = @fieldNode.attribute('caption').nil? ? nil : @fieldNode.attribute('caption').text
58
- @techname = @fieldNode.attribute('name').text.gsub(/^\[|\]$/,'') # assumes the name attribute exists
59
- @uiname = @caption.nil? ? @techname : @caption
60
- # puts " caption: #{@caption}"
61
- # puts "techname: #{@techname}"
62
- # puts " uiname: #{@uiname}"
82
+ @caption = calcField.caption
83
+ @techname = calcField.name
84
+ @uiname = calcField.uiname
63
85
  #--
64
86
  @scopeIsolation = attribText(@node, 'scope-isolation')
65
87
  #-- Formula --
66
88
  @has_formula = @node.has_attribute?('formula')
67
89
  if @has_formula
68
- @formula = @node.attribute('formula').text
90
+ @formula = @node.attribute('formula').text.gsub(/\r\n/,"\n")
91
+ @formulaUC = @formula.upcase
69
92
  @formulaLines = formula.split(/\n|\r\n/)
70
93
  @formulaFlat = flattenFormula(@formulaLines)
71
94
  @comments = getComments(@formulaLines)
72
- elsif @class.eql? 'categorical-bin'
73
- @formula = '<<Group>>'
74
- @formulaLines = [ @formula ]
75
- @formulaFlat = [ @formula ]
76
- @comments = [ ]
95
+ @lodCodePos = @formula =~ /^[ ]*{[ ]*(fixed|include|exclude)[ ]*/i
96
+ @is_lod = !lodCodePos.nil? && @lodCodePos >= 0
97
+ @is_tableCalc = @@tableCalcs.any? { |tc| @formulaUC.include?(tc) } #assessTableCalc @formula
98
+ # puts "#{@lodCodePos} \t #{@is_lod} \t #{@is_lod.class} \t => #{@formula}"
77
99
  end
78
- #-- Fields --
79
- parseFormFields # establishes @ fields
80
- resolveFields # establishes @ resolvedFields
81
100
  end
82
101
  end
83
102
  end
84
103
 
104
+ # def assessTableCalc formula
105
+ # @@tableCalcs.any? { |tc| string.include?(tc) }
106
+ # end
107
+
85
108
  def attribText(node, attribute)
86
109
  node.attribute(attribute).nil? ? nil : node.attribute(attribute).text
87
110
  end
88
111
 
89
112
 
90
113
  def parseFormFields
91
- @fields = Set.new []
92
- formula = formulaFlat
114
+ @fields = Set.new []
115
+ @calcFields = SortedSet.new []
116
+ formula = @formulaFlat
93
117
  if !formula.nil? && formula.include?('[') && formula.include?(']')
94
- noSqLits = formula.gsub(/'[\[\.\]]+'/, ' ')
118
+ noSqLits = formula.gsub( /'[\[\.\]]+'/, ' ')
95
119
  flatForm = noSqLits.gsub( /\n/, ' ')
96
120
  stripFrt = flatForm.gsub( /^[^\[]*[\[]/ , '[' )
97
121
  stripBck = stripFrt.gsub( /\][^\]]+$/ , ']' )
98
122
  stripMid = stripBck.gsub( /\][^\]]{2,}\[/ , ']]..[[' )
99
123
  stripCom = stripMid.gsub( /\][ ]*,[ ]*\[/ , ']]..[[' )
100
- stripFns = stripMid.gsub( /\][ ]*[\*\/+\-,][ ]*\[/ , ']]..[[' )
124
+ stripFns = stripMid.gsub( /\][ ]*[\*\/+\-,=][ ]*\[/ , ']]..[[' )
101
125
  fields = stripFns.split(']..[')
102
126
  fields.each { |field| @fields.add field.gsub(/^\[|\]$/, '')}
127
+ fields.each do |field|
128
+ cf = CalculationField.new( field.gsub(/^\[|\]$/, ''), @dataSource, @dataSource.workbook )
129
+ @calcFields.add cf
130
+ end
103
131
  end
104
132
  end
105
133
 
106
-
107
- def resolveFields
108
- @resolvedFields = []
109
- @fields.each do |field|
110
- rawField = field.gsub(/^\[|\]$/,'')
111
- parts = rawField.split('].[')
112
- if parts.length > 1
113
- source = parts[0]
114
- fieldCalc = parts[1]
115
- fieldui = fieldCalc
116
- else
117
- source = nil
118
- fieldCalc = parts[0]
119
- fieldui = @dataSource.fieldUIName(fieldCalc)
120
- end
121
- hash = { :field => fieldCalc, :fieldui => fieldui, :source => source }
122
- @resolvedFields << hash
123
- end
124
- end
125
-
126
-
127
- def resolvedFormula
128
- @resolvedFormula ||= resolveFormula
134
+ def formulaResolved
135
+ @formulaResolved ||= resolveFormula
129
136
  end
130
-
137
+
131
138
  def resolveFormula
132
- # puts "resolveFormula :\n-- formula --\n %s \n-- @dataSource.nil? : %s \n-- @resolvedFields.nil? : %s \n--" % [ @formula, @dataSource.nil?, @resolvedFields.nil? ]
133
139
  formula = @formula
134
- unless @dataSource.nil? || @formula.nil? || @resolvedFields.nil?
135
- @resolvedFields.each do |rf|
136
- field = rf[:field]
137
- source = rf[:source]
138
- # puts " field: #{field}"
139
- # puts " src: #{source}"
140
- if source.nil?
141
- fieldInternal = field # "[#{rf[:field]}]"
142
- fieldUI = @dataSource.fieldUIName(field)
143
- # puts " f?: #{formula.include?(field)}"
144
- # puts " fui: #{fieldUI}"
145
- formula = formula.gsub(field,fieldUI) unless fieldUI.nil?
146
- # puts "formula:\n--\n#{formula}"
147
- else
148
- @remoteFields[source] = SortedSet.new unless @remoteFields.include? source
149
- @remoteFields[source].add field
150
- end
140
+ parseFormFields # - extracts the fields from the formula; as persisted they're the internal names
141
+ # puts "::++ #{formula}"
142
+ @calcFields.each do |calcField|
143
+ if calcField.techUIdiff
144
+ # puts ":::: #{calcField.techCode} // #{calcField.uiCode}"
145
+ formula = formula.gsub(calcField.techCode,calcField.uiCode)
146
+ # puts ":--: #{formula}"
151
147
  end
152
148
  end
153
- # puts "resolvedFormula:\n==\n#{formula}\n--"
154
149
  return formula
155
150
  end
156
151
 
152
+ def formulaFlatResolved
153
+ @formulaFlatResolved ||= flattenResolvedFormula
154
+ end
155
+
156
+ def flattenResolvedFormula
157
+ formula = formulaResolved
158
+ formula.gsub(/\n/, ' ')
159
+ end
157
160
 
158
161
  def flattenFormula lines
159
162
  formula = ''
@@ -175,7 +178,80 @@ module Twb
175
178
  return comments.strip
176
179
  end
177
180
 
178
-
179
181
  end # class FieldCalculation
180
182
 
183
+
184
+
185
+ class CalculationField
186
+ # is a field used in a calculation, resolved into it's human-meaningful form
187
+
188
+ include Comparable
189
+
190
+ attr_reader :techName, :techCode
191
+ attr_reader :uiName, :uiCode
192
+ attr_reader :dataSource, :dataSourceRef, :dataSourceExists
193
+ attr_reader :fqName
194
+ attr_reader :techUIdiff
195
+
196
+ def initialize code, datasource, workbook
197
+ # puts "\nCalculationField <= \n\t #{code}"
198
+ @dataSource = datasource.uiname
199
+ @dataSourceRef = :local
200
+ @dataSourceExists = true
201
+ @techUIdiff = false
202
+ @uiName = ''
203
+ rawCode = code.gsub(/^\[|\]$/,'')
204
+ parts = rawCode.split('].[')
205
+ # puts "\n\nField: #{code} \t parts: #{parts.length} - #{parts.inspect}"
206
+ if parts.length == 1
207
+ @techName = parts[0]
208
+ @techCode = "[#{parts[0]}]"
209
+ if datasource.nil?
210
+ @uiName = @techName
211
+ @uiCode = @techCode
212
+ @techUIdiff = false
213
+ else # !datasource.nil?
214
+ # puts "#{@techName} \t:: #{datasource.fieldUIName(@techName).nil?} \t:: #{datasource.fieldUIName(@techName)} "
215
+ @uiName = datasource.fieldUIName(@techName).nil? ? @techName : datasource.fieldUIName(@techName)
216
+ @uiCode = @uiName.nil? ? @techCode : "[#{@uiName}]"
217
+ @techUIdiff = !@techCode.eql?(@uiCode)
218
+ end
219
+ else # parts.length <> 1
220
+ rdstech = parts[0]
221
+ calcField = parts[1]
222
+ @uiName = calcField
223
+ @dataSource = rdstech
224
+ @dataSourceRef = :remote
225
+ @techCode = "[#{rdstech}].[#{calcField}]"
226
+ workbook = datasource.workbook
227
+ remoteds = workbook.nil? ? nil : workbook.datasource(rdstech)
228
+ # puts "\t twb: #{workbook.class} / remoteds: #{remoteds.class} : #{remoteds.nil? ? "<<NOT FOUND:#{rdstech}:>>" : remoteds.uiname} "
229
+ #--
230
+ if remoteds.nil? || remoteds.fieldUIName(calcField).nil?
231
+ @uiName = calcField
232
+ @uiCode = "[<<NOT FOUND>>#{rdstech}].[#{calcField}]"
233
+ @techUIdiff = true
234
+ @dataSourceExists = false
235
+ else !remoteds.nil?
236
+ @dataSource = remoteds.uiname
237
+ @uiName = remoteds.fieldUIName(calcField)
238
+ @uiCode = "[#{@dataSource}].[#{@uiName}]"
239
+ @techUIdiff = !@techCode.eql?(@uiCode)
240
+ @dataSourceExists = true
241
+ end
242
+ end
243
+ @fqName = "#{@dataSource}::#{@uiName}"
244
+ end # initialize
245
+
246
+ def <=>(other)
247
+ # myName = @uiName.nil? ? '' : @uiName
248
+ # otherName = other.uiName.nil? ? "" : other.uiName
249
+ # # puts "#{@uiName} / #{myName} <=> #{otherName} / #{other.uiName}"
250
+ # # puts "#{@uiName.nil?} // #{other.uiName.nil?}"
251
+ # myName <=> otherName
252
+ @fqName <=> other.fqName
253
+ end
254
+
255
+ end # class CalculationField
256
+
181
257
  end # module Twb
@@ -21,44 +21,47 @@ module Twb
21
21
 
22
22
  class LocalField
23
23
 
24
- attr_reader :type, :node, :tableauname, :dbname, :datatype, :role, :type, :hidden, :caption, :aggregation, :uiname, :calculation, :comments
24
+ attr_reader :node, :type, :datatype, :name, :ordinal
25
25
 
26
26
  def initialize fieldNode
27
+ puts "lf:: #{fieldNode}"
27
28
  @node = fieldNode
28
29
  @type = 'local'
29
- @tableauname = @node.attr('name')
30
- @dbname = @node.attr('name').gsub(/^\[/,'').gsub(/\]$/,'')
31
30
  @datatype = @node.attr('datatype')
32
- @role = @node.attr('role')
33
- @type = @node.attr('type')
34
- @hidden = @node.attr('hidden')
35
- @caption = @node.attr('caption')
36
- @aggregation = @node.attr('aggregation')
37
- @calculation = getCalculation
38
- @comments = getComments
39
- @uiname = if @caption.nil? || @caption == '' then @dbname else @caption end
40
- return self
31
+ @name = @node.attr('name')
32
+ @ordinal = @node.attr('ordinal')
33
+ # @dbname = @node.attr('name').gsub(/^\[/,'').gsub(/\]$/,'')
34
+ # @datatype = @node.attr('datatype')
35
+ # @role = @node.attr('role')
36
+ # @type = @node.attr('type')
37
+ # @hidden = @node.attr('hidden')
38
+ # @caption = @node.attr('caption')
39
+ # @aggregation = @node.attr('aggregation')
40
+ # @calculation = getCalculation
41
+ # @comments = getComments
42
+ # @uiname = if @caption.nil? || @caption == '' then @dbname else @caption end
43
+ # return self
41
44
  end
42
45
 
43
- def getCalculation
44
- calcNode = @node.at_xpath("./calculation")
45
- FieldCalculation.new(calcNode) unless calcNode.nil?
46
- end
46
+ # def getCalculation
47
+ # calcNode = @node.at_xpath("./calculation")
48
+ # FieldCalculation.new(calcNode) unless calcNode.nil?
49
+ # end
47
50
 
48
- def getComments
49
- comments = ''
50
- runs = node.xpath('./desc/formatted-text/run')
51
- runs.each do |run|
52
- unless run.nil?
53
- comments += run.text
54
- end
55
- end
56
- return comments
57
- end
51
+ # def getComments
52
+ # comments = ''
53
+ # runs = node.xpath('./desc/formatted-text/run')
54
+ # runs.each do |run|
55
+ # unless run.nil?
56
+ # comments += run.text
57
+ # end
58
+ # end
59
+ # return comments
60
+ # end
58
61
 
59
- def remove_attribute attribute
60
- @node.remove_attribute(attribute)
61
- end
62
+ # def remove_attribute attribute
63
+ # @node.remove_attribute(attribute)
64
+ # end
62
65
 
63
66
  end
64
67
 
@@ -17,37 +17,79 @@ require 'nokogiri'
17
17
 
18
18
  module Twb
19
19
 
20
- # Assumption: A field can only be either a MetadataField or a LocalField, not both in a given Workbook data connection.
21
-
22
20
  class MetadataField
23
21
 
24
- attr_reader :node, :aggregation, :containsnull, :localname, :localtype, :ordinal
25
- attr_reader :parentname, :precision, :remotealias, :remotename, :remotetype, :width, :name
22
+ include Comparable
23
+
24
+ attr_reader :node, :class
25
+ attr_accessor :source
26
+ attr_reader :parentName, :table # parentName is the Db table
27
+ attr_reader :name, :caption, :family
28
+ attr_reader :localName, :localType
29
+ attr_reader :remoteName, :remoteAlias
30
+ attr_reader :remoteType, :aggregation
31
+ attr_reader :containsNull, :ordinal
32
+ attr_reader :precision, :width, :scale
33
+
34
+ # Child nodes of <metadata-record nodes
35
+ # <aggregation
36
+ # <approx-count
37
+ # <attributes
38
+ # <caption
39
+ # <collation
40
+ # <contains-null
41
+ # <family
42
+ # <layered
43
+ # <local-name
44
+ # <local-type
45
+ # <ordinal
46
+ # <parent-name
47
+ # <precision
48
+ # <remote-alias
49
+ # <remote-name
50
+ # <remote-type
51
+ # <scale
52
+ # <statistics
53
+ # <width
54
+
26
55
 
27
56
  def initialize fieldNode
57
+ @class = fieldNode.attribute('class').text
28
58
  @node = fieldNode
59
+ @parentName = load 'parent-name'
60
+ @family = load 'family'
61
+ @table = @family.nil? ? @parentName : @family
62
+ @remoteName = load 'remote-name'
63
+ @remoteAlias = load 'remote-alias'
64
+ @remoteType = load 'remote-type'
65
+ @caption = load 'caption'
66
+ @localName = load 'local-name'
67
+ @name = @caption.nil? ? @localName : @caption
29
68
  @aggregation = load 'aggregation'
30
- @containsnull = load 'contains-null'
31
- @localname = load 'local-name'
32
- @localtype = load 'local-type'
69
+ @containsNull = load 'contains-null'
70
+ @localType = load 'local-type'
33
71
  @ordinal = load 'ordinal'
34
- @parentname = load 'parent-name'
35
72
  @precision = load 'precision'
36
- @remotealias = load 'remote-alias'
37
- @remotename = load 'remote-name'
38
- @name = @remotename
39
- @remotetype = load 'remote-type'
73
+ @scale = load 'scale'
40
74
  @width = load 'width'
41
- return self
75
+ @id = "'%s::%s' " % [@table,@remoteName]
76
+ # return self
42
77
  end
43
78
 
44
79
  def load nodeName
45
80
  node = @node.at_xpath(nodeName)
46
- val = if node.nil? then node else node.text.strip end
47
- # puts "==== MD node:'#{nodeName}' \t nil?'#{node.nil?}' \t == val:#{val} \t = '#{node}' "
81
+ val = node.nil? ? nil : node.text.strip.gsub(/^\[|\]$/,'')
48
82
  return val
49
83
  end
50
84
 
85
+ def to_s
86
+ @id
87
+ end
88
+
89
+ def <=>(other)
90
+ @id <=> other.id
91
+ end
92
+
51
93
  end
52
94
 
53
95
  end