rmasalov-surpass 0.1.0

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