rmasalov-surpass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ class Formula
2
+ NO_CALCS=0x00
3
+ RECALC_ALWAYS=0x01
4
+ CALC_ON_OPEN=0x02
5
+ PART_OF_SHARED_FORMULA=0x08
6
+
7
+ attr_reader :parser
8
+
9
+ def initialize(formula_string)
10
+ raise "formulas not available" unless FORMULAS_AVAILABLE
11
+ @lexer = ExcelFormula::Lexer.new(formula_string)
12
+ @parser = ExcelFormula::Parser.new(@lexer)
13
+ begin
14
+ @parser.formula
15
+ rescue RuntimeError => e
16
+ puts e
17
+ raise "invalid Excel formula"
18
+ end
19
+ end
20
+
21
+ def to_biff
22
+ rpn = @parser.rpn
23
+ [rpn.length].pack('v') + rpn
24
+ end
25
+ end
@@ -0,0 +1,173 @@
1
+ class Row
2
+ attr_accessor :index
3
+ attr_accessor :parent
4
+ attr_accessor :parent_wb
5
+ attr_accessor :cells
6
+ attr_accessor :min_col_index
7
+ attr_accessor :max_col_index
8
+ attr_accessor :total_str
9
+ attr_accessor :xf_index
10
+ attr_accessor :has_default_format
11
+ attr_accessor :height_in_pixels
12
+
13
+ attr_accessor :height
14
+ attr_accessor :has_default_height
15
+ attr_accessor :height_mismatch
16
+ attr_accessor :level
17
+ attr_accessor :collapse
18
+ attr_accessor :hidden
19
+ attr_accessor :space_above
20
+ attr_accessor :space_below
21
+
22
+ def initialize(index, parent_sheet)
23
+ is_int = index.is_a?(Integer)
24
+ in_range = (index >= 0) && (index <= 65535)
25
+ raise "row index #{index} is not valid" unless is_int && in_range
26
+
27
+ @index = index
28
+ @parent = parent_sheet
29
+ @parent_wb = parent_sheet.parent()
30
+ @cells = []
31
+ @min_col_index = 0
32
+ @max_col_index = 0
33
+ @total_str = 0
34
+ @xf_index = 0x0F
35
+ @has_default_format = 0
36
+ @height_in_pixels = 0x11
37
+
38
+ @height = 0x00FF
39
+ @has_default_height = 0x00
40
+ @height_mismatch = 0
41
+ @level = 0
42
+ @collapse = 0
43
+ @hidden = 0
44
+ @space_above = 0
45
+ @space_below = 0
46
+ end
47
+
48
+ def adjust_height(style)
49
+ twips = style.font.height
50
+ points = twips/20.0
51
+ # Cell height in pixels can be calcuted by following approx. formula:
52
+ # cell height in pixels = font height in points * 83/50 + 2/5
53
+ # It works when screen resolution is 96 dpi
54
+ pix = (points*83.0/50.0 + 2.0/5.0).round
55
+ @height_in_pixels = pix if (pix > @height_in_pixels)
56
+ end
57
+
58
+ def set_height(height)
59
+ @height = height * 20 #This seems to correspond to row height in excel.
60
+ @height_mismatch = 1
61
+ end
62
+
63
+ def adjust_boundary_column_indexes(*args)
64
+ args.each do |a|
65
+ is_int = (a.to_i == a)
66
+ in_range = (0 <= a) && (a <= 255)
67
+ raise "invalid boundary index #{a}" unless is_int && in_range
68
+ @min_col_index = a if a < @min_col_index
69
+ @max_col_index = a if a > @max_col_index
70
+ end
71
+ end
72
+
73
+ # TODO can we get rid of this? Tests pass if it is commented out.
74
+ def style=(style)
75
+ adjust_height(style)
76
+ @xf_index = @parent_wb.styles.add(style)
77
+ @has_default_format = 1
78
+ end
79
+
80
+ def cells_count
81
+ @cells.length
82
+ end
83
+
84
+ ### @export "to-biff"
85
+ def to_biff
86
+ height_options = (@height & 0x07FFF)
87
+ height_options |= (@has_default_height & 0x01) << 15
88
+
89
+ options = (@level & 0x07) << 0
90
+ options |= (@collapse & 0x01) << 4
91
+ options |= (@hidden & 0x01) << 5
92
+ options |= (@height_mismatch & 0x01) << 6
93
+ options |= (@has_default_format & 0x01) << 7
94
+ options |= (0x01 & 0x01) << 8
95
+ options |= (@xf_index & 0x0FFF) << 16
96
+ options |= (@space_above & 0x01) << 28
97
+ options |= (@space_below & 0x01) << 29
98
+
99
+ args = [@index, @min_col_index, @max_col_index, height_options, options]
100
+ RowRecord.new(*args).to_biff
101
+ end
102
+
103
+ def cells_biff
104
+ cells.collect {|c| c.to_biff }.join
105
+ end
106
+ ### @end
107
+
108
+ def cell(col_index)
109
+ cells.select {|c| c.index == col_index}.first
110
+ end
111
+
112
+ def write(col, label, style)
113
+ case style
114
+ when StyleFormat
115
+ # leave it alone
116
+ when Hash
117
+ style = StyleFormat.new(style)
118
+ ### @export "autoformats"
119
+ when TrueClass # Automatically apply a nice numeric format.
120
+ case label
121
+ when DateTime, Time
122
+ style = @parent_wb.styles.default_datetime_style
123
+ when Date
124
+ style = @parent_wb.styles.default_date_style
125
+ when Float
126
+ style = @parent_wb.styles.default_float_style
127
+ else
128
+ style = @parent_wb.styles.default_style
129
+ end
130
+ ### @end
131
+ when NilClass
132
+ style = @parent_wb.styles.default_style
133
+ else
134
+ raise "I don't know how to use this to format a cell #{style.inspect}"
135
+ end
136
+
137
+ style_index = @parent_wb.styles.add(style)
138
+
139
+ raise "trying to write to cell #{self.index}, #{col} - already exists!" if cell(col)
140
+
141
+ adjust_height(style)
142
+ adjust_boundary_column_indexes(col)
143
+
144
+ ### @export "label-classes"
145
+ case label
146
+ when TrueClass, FalseClass
147
+ @cells << BooleanCell.new(self, col, style_index, label)
148
+ when String, NilClass
149
+ if label.to_s.length == 0
150
+ @cells << BlankCell.new(self, col, style_index)
151
+ else
152
+ @cells << StringCell.new(self, col, style_index, @parent_wb.sst.add_str(label))
153
+ @total_str += 1
154
+ end
155
+ when Numeric
156
+ @cells << NumberCell.new(self, col, style_index, label)
157
+ when Date, DateTime, Time
158
+ @cells << NumberCell.new(self, col, style_index, as_excel_date(label))
159
+ when Formula
160
+ @cells << FormulaCell.new(self, col, style_index, label)
161
+ else
162
+ raise "You are trying to write an object of class #{label.class.name} to a spreadsheet. Please convert this to a supported class such as String."
163
+ end
164
+ ### @end
165
+ end
166
+
167
+ def write_blanks(c1, c2, style)
168
+ raise unless c1 <= c2
169
+ adjust_height(style)
170
+ adjust_boundary_column_indexes(c1, c2)
171
+ @cells << MulBlankCell.new(self, c1, c2, @parent_wb.styles.add(style))
172
+ end
173
+ end
@@ -0,0 +1,194 @@
1
+ class StyleFormat
2
+ attr_accessor :number_format_string
3
+ attr_accessor :font
4
+ attr_accessor :alignment
5
+ attr_accessor :borders
6
+ attr_accessor :pattern
7
+ attr_accessor :protection
8
+
9
+ def initialize(hash = {})
10
+ @number_format_string = hash[:number_format_string] || 'General'
11
+
12
+ @font = Font.new(hash_select(hash, /^font_/))
13
+ @alignment = Alignment.new(hash_select(hash, /^text_/))
14
+ @borders = Borders.new(hash_select(hash, /^border_/))
15
+ @pattern = Pattern.new(hash_select(hash, /^(fill|pattern)_/))
16
+ @protection = Protection.new
17
+ end
18
+
19
+ def hash_select(hash, pattern)
20
+ new_hash = {}
21
+ hash.keys.each do |k|
22
+ next unless k.to_s =~ pattern
23
+ new_key = k.to_s.gsub(pattern, '').to_sym
24
+ new_hash[new_key] = hash[k]
25
+ end
26
+ new_hash
27
+ end
28
+ end
29
+
30
+ class StyleCollection
31
+ attr_accessor :fonts
32
+ attr_accessor :number_formats
33
+ attr_accessor :styles
34
+ attr_accessor :default_style
35
+ attr_accessor :default_format
36
+
37
+ FIRST_USER_DEFINED_NUM_FORMAT_INDEX = 164
38
+
39
+ STANDARD_NUMBER_FORMATS = [
40
+ 'General',
41
+ '0',
42
+ '0.00',
43
+ '#,##0',
44
+ '#,##0.00',
45
+ '"$"#,##0_);("$"#,##',
46
+ '"$"#,##0_);[Red]("$"#,##',
47
+ '"$"#,##0.00_);("$"#,##',
48
+ '"$"#,##0.00_);[Red]("$"#,##',
49
+ '0%',
50
+ '0.00%',
51
+ '0.00E+00',
52
+ '# ?/?',
53
+ '# ??/??',
54
+ 'M/D/YY',
55
+ 'D-MMM-YY',
56
+ 'D-MMM',
57
+ 'MMM-YY',
58
+ 'h:mm AM/PM',
59
+ 'h:mm:ss AM/PM',
60
+ 'h:mm',
61
+ 'h:mm:ss',
62
+ 'M/D/YY h:mm',
63
+ '_(#,##0_);(#,##0)',
64
+ '_(#,##0_);[Red](#,##0)',
65
+ '_(#,##0.00_);(#,##0.00)',
66
+ '_(#,##0.00_);[Red](#,##0.00)',
67
+ '_("$"* #,##0_);_("$"* (#,##0);_("$"* "-"_);_(@_)',
68
+ '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)',
69
+ '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)',
70
+ '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)',
71
+ 'mm:ss',
72
+ '[h]:mm:ss',
73
+ 'mm:ss.0',
74
+ '##0.0E+0',
75
+ '@'
76
+ ]
77
+
78
+ def initialize
79
+ # Populate default font list.
80
+ @fonts = {}
81
+ # Initialize blank fonts into slots 0,1,2,3,5 in order to skip slot 4.
82
+ [0,1,2,3,5].each do |i|
83
+ @fonts[i] = Font.new
84
+ end
85
+
86
+ # Populate default number format list.
87
+ @number_formats = {}
88
+ STANDARD_NUMBER_FORMATS.each_with_index do |s, i|
89
+ index = (i <= 23) ? i : i + 14
90
+ @number_formats[index] = s
91
+ end
92
+
93
+ @styles = {}
94
+ @default_style = StyleFormat.new
95
+
96
+ # Store the 6 parameters of the default_style
97
+ @default_format = add_style(@default_style)[0]
98
+ end
99
+
100
+ ### @export "autoformats"
101
+ def default_date_style
102
+ @default_date_style ||= StyleFormat.new(:number_format_string => 'dd-mmm-yyyy')
103
+ end
104
+
105
+ def default_datetime_style
106
+ @default_datetime_style ||= StyleFormat.new(:number_format_string => 'dd-mmm-yyyy hh:mm:ss')
107
+ end
108
+
109
+ def default_float_style
110
+ @default_float_style ||= StyleFormat.new(:number_format_string => '#,##0.00')
111
+ end
112
+ ### @end
113
+
114
+ def add(style)
115
+ if style.nil?
116
+ 0x10 # Return the index of the default style.
117
+ else
118
+ # TODO find way to freeze style so if someone modifies a StyleFormat instance it won't affect previously formatted cells.
119
+ add_style(style)[1] # Return the index of the style just stored.
120
+ end
121
+ end
122
+
123
+ def number_format_index(number_format_string)
124
+ index = @number_formats.index(number_format_string)
125
+ if index.nil?
126
+ # TODO implement regex to check if valid string
127
+ index = FIRST_USER_DEFINED_NUM_FORMAT_INDEX + @number_formats.length - STANDARD_NUMBER_FORMATS.length
128
+ @number_formats[index] = number_format_string
129
+ end
130
+ index
131
+ end
132
+
133
+ def font_index(font)
134
+ index = @fonts.index(font)
135
+ if index.nil?
136
+ index = @fonts.length + 1
137
+ @fonts[index] = font
138
+ end
139
+ index
140
+ end
141
+
142
+ def format_index(format)
143
+ index = @styles.index(format)
144
+ if index.nil?
145
+ index = 0x10 + @styles.length
146
+ @styles[index] = format
147
+ end
148
+ index
149
+ end
150
+
151
+ private
152
+ # This is private, please use add(style) instead.
153
+ def add_style(style)
154
+ number_format_index = number_format_index(style.number_format_string)
155
+ font_index = font_index(style.font)
156
+
157
+ format = [font_index, number_format_index, style.alignment, style.borders, style.pattern, style.protection]
158
+ [format, format_index(format)]
159
+ end
160
+
161
+ public
162
+ def to_biff
163
+ fonts_biff + number_formats_biff + cell_styles_biff + StyleRecord.new.to_biff
164
+ end
165
+
166
+ # TODO use inject here?
167
+ def fonts_biff
168
+ result = ''
169
+ @fonts.sort.each do |i, f|
170
+ result += f.to_biff
171
+ end
172
+ result
173
+ end
174
+
175
+ def number_formats_biff
176
+ result = ''
177
+ @number_formats.sort.each do |i, f|
178
+ next if i < FIRST_USER_DEFINED_NUM_FORMAT_INDEX
179
+ result += NumberFormatRecord.new(i, f).to_biff
180
+ end
181
+ result
182
+ end
183
+
184
+ def cell_styles_biff
185
+ result = ''
186
+ 0.upto(15) do |i|
187
+ result += XFRecord.new(@default_format, 'style').to_biff
188
+ end
189
+ @styles.sort.each do |i, f|
190
+ result += XFRecord.new(f).to_biff
191
+ end
192
+ result
193
+ end
194
+ end
@@ -0,0 +1,187 @@
1
+ class SurpassCell
2
+ attr_reader :index
3
+
4
+ def set_style(style)
5
+ style = StyleFormat.new(style) if style.is_a?(Hash)
6
+ @format_index = @parent.parent_wb.styles.add(style)
7
+ end
8
+
9
+ def row
10
+ @parent
11
+ end
12
+
13
+ def col
14
+ @index
15
+ end
16
+ end
17
+
18
+ ### @export "string-cell"
19
+ class StringCell < SurpassCell
20
+ def initialize(parent, index, format_index, sst_index)
21
+ @parent = parent
22
+ @index = index
23
+ @format_index = format_index
24
+ @sst_index = sst_index
25
+ end
26
+
27
+ def to_biff
28
+ LabelSSTRecord.new(@parent.index, @index, @format_index, @sst_index).to_biff
29
+ end
30
+ end
31
+
32
+ ### @export "blank-cell"
33
+ class BlankCell < SurpassCell
34
+ def initialize(parent, index, format_index)
35
+ @parent = parent
36
+ @index = index
37
+ @format_index = format_index
38
+ end
39
+
40
+ def to_biff
41
+ BlankRecord.new(@parent.index, @index, @format_index).to_biff
42
+ end
43
+ end
44
+ ### @end
45
+
46
+ class NumberCell < SurpassCell
47
+ def initialize(parent, index, format_index, number)
48
+ @parent = parent
49
+ @index = index
50
+ @format_index = format_index
51
+ @number = number
52
+ end
53
+
54
+ def rk_record(rk_encoded)
55
+ RKRecord.new(@parent.index, @index, @format_index, rk_encoded).to_biff
56
+ end
57
+
58
+ # TODO test this section to be sure numbers are categorized and packed correctly.
59
+ def to_biff
60
+ # 30 bit signed int
61
+ in_range = (-0x20000000 <= @number) && (@number < 0x20000000)
62
+ is_int = (@number.to_i == @number)
63
+ if in_range && is_int
64
+ rk_encoded = 2 | (@number.to_i << 2)
65
+ return rk_record(rk_encoded)
66
+ end
67
+
68
+ # try scaling by 100 then using a 30 bit signed int
69
+ in_range = (-0x20000000 <= @number * 100) && (@number * 100 < 0x20000000)
70
+ round_trip = (@number.to_i*100) == @number*100
71
+ if in_range && round_trip
72
+ rk_encoded = (3 | (@number.to_i*100 << 2))
73
+ return rk_record(rk_encoded)
74
+ end
75
+
76
+ w0, w1, w2, w3 = [@number].pack('E').unpack('v4')
77
+
78
+ is_float_rk = (w0 == 0) && (w1 == 0) && (w2 & 0xFFFC) == w2
79
+ if is_float_rk
80
+ rk_encoded = (w3 << 16) | w2
81
+ return rk_record(rk_encoded)
82
+ end
83
+
84
+ w0, w1, w2, w3 = [@number * 100].pack('E').unpack('v4')
85
+
86
+ is_float_rk_100 = w0 == 0 && w1 == 0 && w2 & 0xFFFC == w2
87
+ if is_float_rk_100
88
+ rk_encoded = 1 | (w3 << 16) | w2
89
+ return rk_record(rk_encoded)
90
+ end
91
+
92
+ # If not an RK value, use a NumberRecord instead.
93
+ NumberRecord.new(@parent.index, @index, @format_index, @number).to_biff
94
+ end
95
+ end
96
+
97
+ class MulNumberCell < SurpassCell
98
+ def initialize(parent, index, format_index, sst_index)
99
+ @parent = parent
100
+ @index = index
101
+ @format_index = format_index
102
+ @sst_index = sst_index
103
+ end
104
+
105
+ def to_biff
106
+ raise "not implemented"
107
+ end
108
+ end
109
+
110
+ class MulBlankCell < SurpassCell
111
+ def initialize(parent, col1, col2, xf_idx)
112
+ raise unless col1 < col2
113
+ @parent = parent
114
+ @col1 = col1
115
+ @col2 = col2
116
+ @xf_idx = xf_idx
117
+ end
118
+
119
+ def to_biff
120
+ MulBlankRecord.new(@parent.index, @col1, @col2, @xf_idx).to_biff
121
+ end
122
+ end
123
+
124
+ ### @export "formula-cell"
125
+ class FormulaCell < SurpassCell
126
+ def initialize(parent, index, format_index, formula, calc_flags = 0)
127
+ @parent = parent
128
+ @index = index
129
+ @format_index = format_index
130
+ @formula = formula
131
+ @calc_flags = calc_flags
132
+ end
133
+
134
+ def to_biff
135
+ args = [@parent.index, @index, @format_index, @formula.to_biff, @calc_flags]
136
+ FormulaRecord.new(*args).to_biff
137
+ end
138
+ end
139
+ ### @end
140
+
141
+ class BooleanCell < SurpassCell
142
+ def initialize(parent, index, format_index, number)
143
+ @parent = parent
144
+ @index = index
145
+ @format_index = format_index
146
+ @number = number
147
+ @is_error = 0
148
+ end
149
+
150
+ def to_biff
151
+ number = @number ? 1 : 0
152
+ BoolErrRecord.new(@parent.index, @index, @format_index, number, @is_error).to_biff
153
+ end
154
+ end
155
+
156
+ class ErrorCell < SurpassCell
157
+ ERROR_CODES = {
158
+ 0x00 => 0, # Intersection of two cell ranges is empty
159
+ 0x07 => 7, # Division by zero
160
+ 0x0F => 15, # Wrong type of operand
161
+ 0x17 => 23, # Illegal or deleted cell reference
162
+ 0x1D => 29, # Wrong function or range name
163
+ 0x24 => 36, # Value range overflow
164
+ 0x2A => 42, # Argument or function not available
165
+ '#NULL!' => 0, # Intersection of two cell ranges is empty
166
+ '#DIV/0!' => 7, # Division by zero
167
+ '#VALUE!' => 36, # Wrong type of operand
168
+ '#REF!' => 23, # Illegal or deleted cell reference
169
+ '#NAME?' => 29, # Wrong function or range name
170
+ '#NUM!' => 36, # Value range overflow
171
+ '#N/A!' => 42 # Argument or function not available
172
+ }
173
+
174
+ def initialize(parent, index, format_index, error_string_or_code)
175
+ @parent = parent
176
+ @index = index
177
+ @format_index = format_index
178
+ @number = ERROR_CODES[error_string_or_code]
179
+ @is_error = 1
180
+
181
+ raise "invalid error code #{error_string_or_code}" if @number.nil?
182
+ end
183
+
184
+ def to_biff
185
+ BoolErrRecord.new(@parent.index, @index, @format_index, @number, @is_error)
186
+ end
187
+ end