rspreadsheet 0.2.15 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ require 'helpers/class_extensions'
2
+ require 'rspreadsheet/xml_tied_array' # BASTL to je jen kvuli XMLTied class
3
+
4
+ module Rspreadsheet
5
+
6
+ using ClassExtensions if RUBY_VERSION > '2.1'
7
+
8
+ # Note for developers: In case you want to represent a node containing many identical subnodes
9
+ # (like row contains cells, worksheet contains rows or images part includes images) than
10
+ # 1. Include module XMLTiedArray into parent class
11
+ # 2. Subclass XMLTiedItem by object which will represent individual items
12
+ # 3. Implement methods which are mentioned at both of these and the rest should work itself.
13
+ # 4. prepare_subitem calls method new of the item and usually sends index and parent + some
14
+ # more so subitem can implement parent and index methods.
15
+
16
+ # @private
17
+ # abstract class. All successors have some options. MUST implement:
18
+ # * xml_options
19
+ #
20
+ # If you override intializer make sure you call initialize_xml_tied_item(aparent,aindex).
21
+ #
22
+ # Optionally you may implement method which makes index accessible under more meaningful name
23
+ # like column or so. Optionally you may implement method which makes parent accessible under
24
+ # more meaningful name like worksheet or so.
25
+ #
26
+ # By default parent is stored at initialization and never changed. If you do not want to cache
27
+ # it like this (to prevent inconsistencies) just override parent method to dfind the parent
28
+ # dynamically (see Cell). If you do so you may want to disable the default parent handling by
29
+ # calling initialize_xml_tied_item(nil,index) in initializer (or do not call it at all if you
30
+ # have overridden index as well.
31
+ #
32
+ # Note: If there is a object representing parent (presumably using XMLTiedArray) than initialize
33
+ # signature must be reflected in parent prepare_subitem method
34
+ #
35
+ class XMLTiedItem < XMLTied
36
+
37
+ def initialize(aparent,aindex)
38
+ initialize_xml_tied_item(aparent,aindex)
39
+ end
40
+ def parent; @xml_tied_parent end
41
+ def index; @xml_tied_item_index end
42
+ def set_index(aindex); @xml_tied_item_index=aindex end
43
+ def index=(aindex); @xml_tied_item_index=aindex end
44
+
45
+ # `xml_options[:xml_items_node_name]` gives the name of the tag representing cell
46
+ # `xml_options[:number-columns-repeated]` gives the name of the previous tag which sais how many times the item is repeated
47
+ def xml_options; abstract end
48
+
49
+ def initialize_xml_tied_item(aparent,aindex)
50
+ @xml_tied_parent = aparent unless aparent.nil?
51
+ @xml_tied_item_index = aindex unless aindex.nil?
52
+ end
53
+
54
+ def mode
55
+ case
56
+ when xmlnode.nil? then :outbound
57
+ when repeated>1 then :repeated
58
+ else :regular
59
+ end
60
+ end
61
+ def repeated; (Tools.get_ns_attribute_value(xmlnode, 'table', xml_options[:xml_repeated_attribute]) || 1 ).to_i end
62
+ def repeated?; mode==:repeated || mode==:outbound end
63
+ alias :is_repeated? :repeated?
64
+ def xmlnode
65
+ if parent.xmlnode.nil?
66
+ nil
67
+ else
68
+ parent.my_subnode(index)
69
+ end
70
+ end
71
+ def detach_if_needed
72
+ detach if repeated? # item did not exist individually yet, detach it within its parent and therefore make it individally editable
73
+ end
74
+ def detach
75
+ parent.detach_if_needed if parent.respond_to?(:detach_if_needed)
76
+ parent.detach_my_subnode_respect_repeated(index)
77
+ self
78
+ end
79
+ def _shift_by(diff)
80
+ set_index(index + diff)
81
+ end
82
+ def range
83
+ parent.my_subnode_range(index)
84
+ end
85
+ def invalid_reference?; false end
86
+ # destroys the object so it can not be used, this is necessarry to prevent
87
+ # accessing cells and rows which has been long time ago deleted and do not represent
88
+ # any physical object anymore
89
+ def invalidate_myself
90
+ raise_destroyed_cell_error = Proc.new {|*params| raise "Calling method of already destroyed Cell."}
91
+ (self.methods - Object.methods + [:nil?]).each do |method| # "undefine" all methods
92
+ self.singleton_class.send(:define_method, method, raise_destroyed_cell_error)
93
+ end
94
+ self.singleton_class.send(:define_method, :inspect, -> { "#<%s:0x%x destroyed cell>" % [self.class,object_id] }) # define descriptive inspect
95
+ self.singleton_class.send(:define_method, :invalid_reference?, -> { true }) # define invalid_reference? method
96
+ # invalidate variables
97
+ @xml_tied_parent=nil
98
+ @xml_tied_item_index=nil
99
+ # self.instance_variables.each do |variable|
100
+ # instance_variable_set(variable,nil)
101
+ # end
102
+ end
103
+ def delete
104
+ parent.delete_subitem(index)
105
+ invalidate_myself
106
+ end
107
+
108
+ end
109
+
110
+
111
+ end
@@ -0,0 +1,151 @@
1
+ require 'helpers/class_extensions'
2
+ require 'rspreadsheet/xml_tied_array'
3
+
4
+ module Rspreadsheet
5
+
6
+ using ClassExtensions if RUBY_VERSION > '2.1'
7
+
8
+
9
+ # Abstract class similar to XMLTiedArray but with support to "repeatable" items. This is notion specific
10
+ # to OpenDocument files - whnewer a row repeats more times (or a cell), one can either make many identical
11
+ # copies of the same xml or only make one xml representing one item and add attribute xml_repeated.
12
+ #
13
+ # This class is made to be included, not subclassed - the reason is in delete method which calls super.
14
+ # This class is also made to handle automatic creation of outbound items.
15
+ # @private
16
+
17
+ module XMLTiedArray_WithRepeatableItems
18
+ include XMLTiedArray
19
+
20
+ def my_subnode_range(aindex)
21
+ node, range = find_subnode_with_range(aindex)
22
+ return range
23
+ end
24
+
25
+ # vrátí xmlnode na souřadnici aindex
26
+ def my_subnode(aindex)
27
+ result1, result2 = find_subnode_with_range(aindex)
28
+ return result1
29
+ end
30
+
31
+ def find_subnode_with_range(aindex)
32
+ options = subitem_xml_options
33
+ rightindex = 0
34
+ xmlsubnodes.each do |node|
35
+ repeated = (node.attributes[options[:xml_repeated_attribute]] || 1).to_i
36
+ leftindex = rightindex + 1
37
+ rightindex = rightindex+repeated
38
+ if rightindex>= aindex
39
+ return node, leftindex..rightindex
40
+ end
41
+ end
42
+ return nil, rightindex+1..Float::INFINITY
43
+ end
44
+
45
+ # @!group inserting new subnodes
46
+
47
+ def insert_new_empty_subnode_before(aindex)
48
+ insert_new_empty_subnode_before_respect_repeatable(aindex)
49
+ end
50
+
51
+ def insert_new_empty_subnode_before_respect_repeatable(aindex)
52
+ axmlnode = xmlnode
53
+ options = subitem_xml_options
54
+ node,index_range = find_subnode_with_range(aindex)
55
+
56
+ if !node.nil? # found the node, now do the insert
57
+ [index_range.begin..aindex-1,aindex..index_range.end].reject {|range| range.size<1}.each do |range| # split original node by cloning
58
+ clone_before_and_set_repeated_attribute(node,range.size,options)
59
+ end
60
+ node.prev.prev = prepare_repeated_subnode(1, options) # insert new node
61
+ node.remove! # remove the original node
62
+ else # insert outbound xmlnode
63
+ [index+1..aindex-1,aindex..aindex].reject {|range| range.size<1}.each do |range|
64
+ axmlnode << XMLTiedArray_WithRepeatableItems.prepare_repeated_subnode(range.size, options)
65
+ end
66
+ end #TODO: Out of bounds indexes handling
67
+ return my_subnode(aindex)
68
+ end
69
+
70
+ def prepare_repeated_subnode(times_repeated,options)
71
+ result = prepare_empty_subnode
72
+ Tools.set_ns_attribute(result,'table',options[:xml_repeated_attribute],times_repeated, 1)
73
+ result
74
+ end
75
+
76
+ def clone_before_and_set_repeated_attribute(node,times_repeated,options)
77
+ newnode = node.copy(true)
78
+ Tools.set_ns_attribute(newnode,'table',options[:xml_repeated_attribute],times_repeated,1)
79
+ node.prev = newnode
80
+ end
81
+
82
+ # detaches subnode with aindex
83
+ def detach_my_subnode_respect_repeated(aindex)
84
+ axmlnode = xmlnode
85
+ options = subitem_xml_options
86
+ node,index_range = find_subnode_with_range(aindex)
87
+ if index_range.size > 1 # pokud potřebuje vůbec detachovat
88
+ if !node.nil? # detach subnode
89
+ [index_range.begin..aindex-1,aindex..aindex,aindex+1..index_range.end].reject {|range| range.size<1}.each do |range| # create new structure by cloning
90
+ clone_before_and_set_repeated_attribute(node,range.size,options)
91
+ end
92
+ node.remove! # remove the original node
93
+ else # add outbound xmlnode
94
+ [index_range.begin..aindex-1,aindex..aindex].reject {|range| range.size<1}.each do |range|
95
+ axmlnode << prepare_repeated_subnode(range.size, options)
96
+ end
97
+ end
98
+ end
99
+ return my_subnode(aindex)
100
+ end
101
+
102
+ def delete_my_subnode_respect_repeated(aindex)
103
+ detach_my_subnode_respect_repeated(aindex) #TODO: tohle neni uplne spravne, protoze to zanecha skupinu rozdelenou na dve casti
104
+ subitem(aindex).xmlnode.remove!
105
+ end
106
+
107
+ def how_many_times_node_is_repeated(node) # adding respect to repeated nodes
108
+ (node.attributes[subitem_xml_options[:xml_repeated_attribute]] || 1).to_i
109
+ end
110
+
111
+
112
+ # clean up item from xml (handle possible detachments) and itemcache. leave the object invalidation on the object
113
+ # this should not be called from nowhere but XMLTiedItem.delete
114
+ def delete_subitem(aindex)
115
+ options = subitem_xml_options
116
+ delete_my_subnode_respect_repeated(aindex) # vymaž node z xml
117
+ @itemcache.delete(aindex)
118
+ @itemcache.keys.sort.select{|i| i>=aindex+1 }.each do |i|
119
+ @itemcache[i-1]=@itemcache.delete(i)
120
+ @itemcache[i-1]._shift_by(-1)
121
+ end
122
+ end
123
+
124
+ def delete
125
+ @itemcache.each do |key,item|
126
+ item.delete # delete item - this destroys its subitems, xmlnode and invalidates it
127
+ @itemcache.delete(key) # delete the entry from the hash, normally this would mean this ceases to exist, if user does not have reference stored somewhere. Of he does, the object is invalidated anyways
128
+ end
129
+ super # this for example for Row objects calls XMLTiedItem.delete because Row is subclass of XMLTiedItem
130
+ end
131
+
132
+ def find_nonempty_subnode_indexes(axmlnode, options)
133
+ index = 0
134
+ result = []
135
+ axmlnode.elements.select{|node| node.name == options[:xml_items_node_name]}.each do |node|
136
+ repeated = (node.attributes[options[:xml_repeated_attribute]] || 1).to_i
137
+ index = index + repeated
138
+ if !(node.content.nil? or node.content.empty? or node.content =='') and (repeated==1)
139
+ result << index
140
+ end
141
+ end
142
+ return result
143
+ end
144
+
145
+ # truncate the item completely, deleting all its subitems
146
+ def truncate
147
+ subitems.reverse.each{ |subitem| subitem.delete } # reverse je tu jen kvuli performanci, aby to mazal zezadu
148
+ end
149
+ end
150
+
151
+ end
@@ -1,8 +1,8 @@
1
1
  #!/bin/sh
2
2
 
3
3
  # remove previously build gems
4
- rm -f pkg/rspreadsheet-0.*.gem
5
- rm -f Gemfile.lock
4
+ sudo rm -f pkg/rspreadsheet-0.*.gem
5
+ sudo rm -f Gemfile.lock
6
6
 
7
7
  # this is to update git index, because git lsfiles is used in .gemspec
8
8
  git add .
data/rspreadsheet.gemspec CHANGED
@@ -18,8 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ def self.package_installed?(pkgname)
22
+ system("dpkg-query -l #{pkgname} | grep -q '^i'")
23
+ end
24
+
21
25
  # runtime dependencies
22
- spec.add_runtime_dependency 'libxml-ruby', '~>2.7' # parsing XML files
26
+ unless package_installed?('ruby-libxml')
27
+ spec.add_runtime_dependency 'libxml-ruby', '~>2.7' # parsing XML files
28
+ end
23
29
  spec.add_runtime_dependency 'rubyzip', '~>1.1' # opening zip files
24
30
  spec.add_runtime_dependency 'andand', '~>1.3'
25
31
 
@@ -32,8 +38,14 @@ Gem::Specification.new do |spec|
32
38
 
33
39
  # optional and testing
34
40
  spec.add_development_dependency "coveralls", '~>0.7'
35
- spec.add_development_dependency "guard", '~>2.6'
36
- spec.add_development_dependency "guard-rspec", '~>2.6'
41
+
42
+ if RUBY_VERSION.split('.')[0] != "1"
43
+ # ruby_dep starts to require ruby 2.2.5 which raises errors with ruby 1.9.3
44
+ # spec.add_development_dependency "guard", '~>2.13'
45
+ # spec.add_development_dependency "guard-rspec", '~>4.6'
46
+ end
47
+
37
48
  # spec.add_development_dependency 'equivalent-xml' # implementing xml diff
38
49
 
39
50
  end
51
+
data/spec/cell_spec.rb CHANGED
@@ -269,6 +269,21 @@ describe Rspreadsheet::Cell do
269
269
  @cell.value.month.should eq 1
270
270
  @cell.value.day.should eq 2
271
271
  end
272
+ it 'stores time correctly' do
273
+ @cell = @sheet1.cell(1,1)
274
+ @cell.value= Time.parse('2:42 pm')
275
+ @cell.value.hour.should eq 14
276
+ @cell.value.min.should eq 42
277
+ @cell.value.sec.should eq 0
278
+ end
279
+ it 'can read various types of times', :pending => 'see is' do
280
+ raise @sheet2.cell('D23').xml.inspect
281
+ expect {@cell = @sheet2.cell('D22'); @cell.value }.not_to raise_error
282
+ expect {@cell = @sheet2.cell('D23'); @cell.value }.not_to raise_error
283
+
284
+ @cell.value.should == Time.new(2005,5,5,3,33,00)
285
+ end
286
+
272
287
  it 'can be addressed by even more ways and all are identical' do
273
288
  @cell = @sheet1.cell(2,2)
274
289
  @sheet1.cell('B2').value = 'zaseste'
@@ -292,10 +307,14 @@ describe Rspreadsheet::Cell do
292
307
  @cell.format.bold = true
293
308
  @cell.format.bold.should be_truthy
294
309
  @cell.mode.should eq :regular
310
+
311
+ @cell = @sheet1.cell(2,2)
312
+ @cell.format.background_color = '#ffeeaa'
313
+ @cell.format.background_color.should == '#ffeeaa'
314
+ @cell.mode.should eq :regular
295
315
  end
296
316
  it 'remembers formula when set' do
297
317
  @cell = @sheet1.cell(1,1)
298
- # bold
299
318
  @cell.formula.should be_nil
300
319
  @cell.formula='=1+5'
301
320
  @cell.formula.should eq '=1+5'
@@ -327,4 +346,66 @@ describe Rspreadsheet::Cell do
327
346
  @czkcell.value.should == 344.to_d
328
347
  @czkcell.format.currency.should == 'CZK'
329
348
  end
349
+ it 'is possible to manipulate borders of cells' do
350
+ @cell = @sheet1.cell(1,1)
351
+
352
+ [@cell.format.top,@cell.format.left,@cell.format.right,@cell.format.bottom].each do |border|
353
+ border.style = 'dashed'
354
+ border.style.should == 'dashed'
355
+ border.width = 0.5
356
+ border.width.should == 0.5
357
+ border.color = '#005500'
358
+ border.color.should == '#005500'
359
+ end
360
+ end
361
+ it 'returns correct border parameters for the cell' do
362
+ @sheet2.cell('C8').format.top.style.should == 'solid'
363
+ @sheet2.cell('E8').format.left.color.should == '#ff3333'
364
+ @sheet2.cell('E8').format.left.style.should == 'solid'
365
+ @sheet2.cell('F8').format.top.color.should == '#009900'
366
+ @sheet2.cell('F8').format.top.style.should == 'dotted'
367
+ end
368
+ it 'modifies borders correctly' do
369
+ ## initially solid everywhere
370
+ @sheet2.cell('C8').format.top.style.should == 'solid'
371
+ @sheet2.cell('C8').format.bottom.style.should == 'solid'
372
+ @sheet2.cell('C8').format.left.style.should == 'solid'
373
+ @sheet2.cell('C8').format.right.style.should == 'solid'
374
+ ## change top and right to dotted and observe
375
+ @sheet2.cell('C8').format.top.style = 'dotted'
376
+ @sheet2.cell('C8').format.right.style = 'dotted'
377
+ @sheet2.cell('C8').format.bottom.style.should == 'solid'
378
+ @sheet2.cell('C8').format.left.style.should == 'solid'
379
+ @sheet2.cell('C8').format.top.style.should == 'dotted'
380
+ @sheet2.cell('C8').format.right.style.should == 'dotted'
381
+ end
382
+ it 'deletes borders correctly', :pending=> 'consider how to deal with deleted borders' do
383
+ @cell = @sheet1.cell(1,1)
384
+
385
+ [@cell.format.top,@cell.format.left,@cell.format.right,@cell.format.bottom].each do |border|
386
+ border.style = 'dashed'
387
+ border.should_not be_nil
388
+ border.delete
389
+ border.should be_nil
390
+ end
391
+
392
+ # delete right border in existing file and observe
393
+ @sheet2.cell('C8').format.right.delete
394
+ @sheet2.cell('C8').format.right.should == nil
395
+ end
396
+
397
+ it 'can delete borders in many ways', :pending => 'consider what syntax to support' do
398
+ @cell=@sheet2.cell('C8')
399
+ @cell.border_right.should_not be_nil
400
+ @cell.border_right.delete
401
+ @cell.border_right.should be_nil
402
+
403
+ @cell.border_left.should_not be_nil
404
+ @cell.border_left = nil
405
+ @cell.border_left.should be_nil
406
+
407
+ @cell.format.top.should_not_be_nil
408
+ @cell.format.top.style = 'none'
409
+ @cell.border_top.should_not be_nil ## ?????
410
+ end
330
411
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ if RUBY_VERSION > '2.1'
4
+ # testing ClassExtensionsForSpec
5
+ describe LibXML::XML::Node do
6
+ before do
7
+ @n = LibXML::XML::Node.new('a')
8
+ @n << LibXML::XML::Node.new('i','italic')
9
+ b = LibXML::XML::Node.new('p','paragraph')
10
+ b << LibXML::XML::Node.new('b','boldtext')
11
+ @n << b
12
+ @n << LibXML::XML::Node.new_text('textnode')
13
+
14
+ @m = LibXML::XML::Node.new('a')
15
+ @m << LibXML::XML::Node.new('i','italic')
16
+ c = LibXML::XML::Node.new('p','paragraph')
17
+ c << LibXML::XML::Node.new('b','boldtext')
18
+ @m << c
19
+ @m << LibXML::XML::Node.new_text('textnode')
20
+
21
+ @m2 = LibXML::XML::Node.new('a')
22
+ end
23
+ it 'can compare nodes' do
24
+ @n.should == @m
25
+ @n.should_not == @m2
26
+ end
27
+ it 'has correct elements' do
28
+ # raise @n.first_diff(@m).inspect
29
+ end
30
+ end
31
+
32
+ # testing ClassExtensions
33
+ begin
34
+ using ClassExtensions
35
+
36
+ describe Array do
37
+ it 'can sum simple array' do
38
+ a = [1,2,3,4]
39
+ a.sum.should == 10
40
+ end
41
+ it 'ignores text and nils while summing' do
42
+ a = [1,nil, nil,2,3,'foo',5.0]
43
+ a.sum.should == 11
44
+ [nil, 'nic'].sum.should == 0
45
+ [].sum.should == 0
46
+ end
47
+ end
48
+ end
49
+ end