twb 1.0.5 → 1.9.1

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.
@@ -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