rspreadsheet 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba8d928bfac03acfa731a5d4d44f380cdf6a5b49
4
- data.tar.gz: f25cf83032b4d748337fe2601cf2060b69371214
3
+ metadata.gz: d3845679733a12aaed33574dc981b22d49d297fe
4
+ data.tar.gz: c7f151f9ed92317b006eec3e2fc323514b43a5f4
5
5
  SHA512:
6
- metadata.gz: 73e568971ecdbfd2f3c19a5a8e3c171e2f2dba112fb2a3283f687568241b01e0cb292ee3677ca1ad2e223f9e159ce0ed5e9f8f823993d8b761b3590a1f13dd97
7
- data.tar.gz: 22f35a0dd81b4e980d5d1ce2e50b6dbe2e13ca715061d3659378398bd7fe44871da7c50c23563a98108274f87502a292b1970f922477e242951be4e4ad7c7f71
6
+ metadata.gz: 0307c5a79b0e9aebc2ce1cfc56b6f8445542e4cc8dab661a92398c8c7e4debeaa55ed0f6dce7a7a9a47cbda6d53bdf24cc19f95a441e94b4a3458e4c16d947eb
7
+ data.tar.gz: 25acbd10e229976cd2b2112dbc35dbf2d4e8610302928df753a7a1110c64f23a0f052362684188fb33dba9341ab402075a0ed5e2b7635e90cc574d46c3b13ba2
data/.travis.yml CHANGED
@@ -7,12 +7,17 @@ rvm:
7
7
  - 2.3.3
8
8
  - 2.4.0
9
9
  script: 'bundle exec rake'
10
- before_install:
11
- - gem uninstall libxml-ruby
12
- - gem update bundler
10
+ matrix:
11
+ include:
12
+ - rvm: 2.4.0
13
+ before_install:
14
+ - gem uninstall libxml-ruby
15
+ - sudo apt-get install libxml2
16
+ - gem update bundler
17
+
13
18
  notifications:
14
19
  email:
15
20
  recipients:
16
21
  - jatesins@ncsu.edu
17
22
  on_failure: change
18
- on_success: never
23
+ on_success: never
data/DEVEL_BLOG.md CHANGED
@@ -9,6 +9,7 @@ After you have cloned the source run `bundle` command in gem directory to instal
9
9
 
10
10
  ## Ideas/wishlist
11
11
 
12
+ * make it configurable whether Time or DateTime will be used.
12
13
  * In future inntroduce syntax like `sheet.range('C3:E4')` for mass operations.
13
14
  * Trying to make row Enumerable - perhaps skipping empty or undefined cells.
14
15
  * Maybe insted two syntaxes for accessing cell, we make both of them do the same and return Proxy object which would act either as value or cell.
data/GUIDE.md CHANGED
@@ -40,6 +40,9 @@ The file needs to be saved after doing changes.
40
40
  @workbook.save('new_filename.ods') # changes filename and saves
41
41
  @workbook.save(any_io_object) # file can be saved to any IO like object as well
42
42
  ````
43
+ ### Date and Time
44
+ OpenDocument and ruby have different models of date, time and datetime. Ruby containg three different objects. Time and DateTime cover all cases, Date covers dates only. OpenDocument distinguishes two groups - time of a day (time) and everything else (date). To simplify things a little we return cell values containg time of day as Time object and cell values containg datetime of date as DateTime. I am aware that this is very arbitrary choice, but it is very practical. This way and to some extend the types of values from OpenDocument are preserved when read from files, beeing acted upon and written back to spreadshhet.
45
+
43
46
 
44
47
  ## Examples
45
48
 
@@ -47,7 +50,7 @@ The file needs to be saved after doing changes.
47
50
  * [extended examples](https://gist.github.com/gorn/b432e6a69e82628349e6) of lots of alternative syntax
48
51
 
49
52
  ## Conventions
50
- * **all indexes are 1-based**. This applies to rows, cells cordinates, and all array like structures like list od worksheets etc. Spreadsheet world is 1-based, ruby is 0-based do I had to make a decision. I intend to make an global option for this, but in early stage I need to keep things simple.
53
+ * **all indexes are 1-based**. This applies to rows, cells cordinates, and all array like structures like list od worksheets etc. Spreadsheet world is 1-based, ruby is 0-based so I had to make a decision. I intend to make an global option for this, but in early stage I need to keep things simple.
51
54
  * with numeric coordinates row always comes before col as in (row,col)
52
55
  * with alphanumerical col always comes before row as in F12
53
56
  * Shorter syntax worksheet[x,y] returns value, longer syntax worksheet.cell(x,y) return cell objects. This allows to work conviniently with values using short syntax and access the cell object if needed (to access formatting for example).
@@ -5,14 +5,17 @@
5
5
  require 'andand'
6
6
  require 'rspreadsheet/xml_tied_item'
7
7
  require 'date'
8
- require 'time'
8
+ require 'time' # extended functions for time like Time.strptime
9
9
  require 'bigdecimal'
10
10
  require 'bigdecimal/util' # for to_d method
11
11
  require 'helpers/class_extensions'
12
+ require 'rspreadsheet/cell_format' # CellFormat and Border classes
12
13
 
13
14
  module Rspreadsheet
14
15
  using ClassExtensions if RUBY_VERSION > '2.1'
15
16
 
17
+ StartOfEpoch = Time.new(1899,12,30,0,0,0,0)
18
+
16
19
  ###
17
20
  # Represents a cell in spreadsheet which has coordinates, contains value, formula and can be formated.
18
21
  # You can get this object like this (suppose that @worksheet contains {Rspreadsheet::Worksheet} object)
@@ -20,15 +23,14 @@ using ClassExtensions if RUBY_VERSION > '2.1'
20
23
  # @worksheet.cells(5,2)
21
24
  #
22
25
  # Note that when using syntax like `@worksheet[5,2]` or `@worksheet.B5` you won't get this object, but rather the value of the cell.
23
- # More precisely it is equvalient to @worksheet.cells(5,2).value.
26
+ # More precisely it is equvalient to @worksheet.cells(5,2).value. Brief overview can be faound at [README]
24
27
  #
25
28
 
26
29
  class Cell < XMLTiedItem
30
+ # RSpreadsheet::Worksheet in which the cell is contained.
27
31
  attr_accessor :worksheet
32
+ # Row index of a cell. If you want to access the row object, see #row.
28
33
  attr_reader :rowi
29
- InternalDateFormat = '%Y-%m-%d'
30
- InternalTimeFormat = 'PT%HH%MM%SS'
31
- # InternalTimeFormat = 'PT%HH%MM%SS,%LS'
32
34
 
33
35
  # @!group XMLTiedItem related methods and extensions
34
36
  def xml_options; {:xml_items_node_name => 'table-cell', :xml_repeated_attribute => 'number-columns-repeated'} end
@@ -64,8 +66,8 @@ class Cell < XMLTiedItem
64
66
  when gt == nil then nil
65
67
  when gt == Float then xmlnode.attributes['value'].to_f
66
68
  when gt == String then xmlnode.elements.first.andand.content.to_s
67
- when gt == Date then Date.strptime(xmlnode.attributes['date-value'].to_s, InternalDateFormat)
68
- when gt == Time then Time.strptime(xmlnode.attributes['time-value'].to_s, InternalTimeFormat)
69
+ when gt == :datetime then datetime_value
70
+ when gt == :time then time_value
69
71
  when gt == :percentage then xmlnode.attributes['value'].to_f
70
72
  when gt == :currency then xmlnode.attributes['value'].to_d
71
73
  end
@@ -75,6 +77,47 @@ class Cell < XMLTiedItem
75
77
  raise "Unknown cell mode #{self.mode}"
76
78
  end
77
79
  end
80
+
81
+ ## according to http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1417674_253892949
82
+ ## the value od time-value is in a "duration" format defined here https://www.w3.org/TR/xmlschema-2/#duration
83
+ ## this method converts the time-value to Time object. Note that it does not check if the cell is in time-value
84
+ ## or not, this is the responibility of caller. However beware that specification does not specify how the time
85
+ ## should be interpreted. By observing LibreOffice behaviour, I have found these options
86
+ ## 1. "Time only cell" has time is stored as PT16H22M35S (16:22:35) where the duration is duration from midnight.
87
+ ## Because ruby does NOT have TimeOfDay type we need to represent that as DateTime. I have chosen 1899-12-30 00:00:00 as
88
+ ## StartOfEpoch time, because it plays well with case 2.
89
+ ## 2. "DateTime converted to Time only cell" has time stored as PT923451H33M00.000000168S (15:33:00 with date part 2005-05-05
90
+ ## before conversion to time only). It is strange format which seems to have hours meaning number of hours after 1899-12-30 00:00:00
91
+ ##
92
+ ## Returns time-value of the cell. It does not check if cell has or should have this value, it is responibility of caller to do so.
93
+ def time_value
94
+ Cell.parse_time_value(xmlnode.attributes['time-value'].to_s)
95
+ end
96
+ def self.parse_time_value(svalue)
97
+ if (m = /^PT((?<hours>[0-9]+)H)?((?<minutes>[0-9]+)M)?((?<seconds>[0-9]+(\.[0-9]+)?)S)$/.match(svalue.delete(' ')))
98
+ # time was parsed manually
99
+ (StartOfEpoch + m[:hours].to_i*60*60 + m[:minutes].to_i*60 + m[:seconds].to_f.round(5))
100
+ #BASTL: Rounding is here because LibreOffice adds some fractions of seconds randomly
101
+ else
102
+ begin
103
+ Time.strptime(svalue, InternalTimeFormat)
104
+ rescue
105
+ Time.parse(svalue) # maybe add defaults for year-mont-day
106
+ end
107
+ end
108
+ end
109
+ def datetime_value
110
+ vs = xmlnode.attributes['date-value'].to_s
111
+ begin
112
+ DateTime.strptime(vs, InternalDateTimeFormat)
113
+ rescue
114
+ begin
115
+ DateTime.strptime(vs, InternalDateFormat)
116
+ rescue
117
+ DateTime.parse(vs)
118
+ end
119
+ end
120
+ end
78
121
  def value=(avalue)
79
122
  detach_if_needed
80
123
  if self.mode == :regular
@@ -90,13 +133,13 @@ class Cell < XMLTiedItem
90
133
  remove_all_value_attributes_and_content(xmlnode)
91
134
  set_type_attribute('string')
92
135
  xmlnode << Tools.prepare_ns_node('text','p', avalue.to_s)
93
- when gt == Date then
136
+ when gt == :datetime then
94
137
  remove_all_value_attributes_and_content(xmlnode)
95
138
  set_type_attribute('date')
96
- avalue = avalue.strftime(InternalDateFormat)
139
+ avalue = avalue.strftime(InternalDateTimeFormat)
97
140
  Tools.set_ns_attribute(xmlnode,'office','date-value', avalue)
98
141
  xmlnode << Tools.prepare_ns_node('text','p', avalue)
99
- when gt == Time then
142
+ when gt == :time then
100
143
  remove_all_value_attributes_and_content(xmlnode)
101
144
  set_type_attribute('time')
102
145
  Tools.set_ns_attribute(xmlnode,'office','time-value', avalue.strftime(InternalTimeFormat))
@@ -133,8 +176,8 @@ class Cell < XMLTiedItem
133
176
  case
134
177
  when gct == Float then :float
135
178
  when gct == String then :string
136
- when gct == Date then :date
137
- when gct == Time then :time
179
+ when gct == :datetime then :datetime
180
+ when gct == :time then :time
138
181
  when gct == :percentage then :percentage
139
182
  when gct == :unassigned then :unassigned
140
183
  when gct == :currency then :currency
@@ -147,8 +190,8 @@ class Cell < XMLTiedItem
147
190
  # try guessing by value
148
191
  valueguess = case avalue
149
192
  when Numeric then Float
150
- when Time then Time
151
- when Date then Date
193
+ when Time then :time
194
+ when Date, DateTime then :datetime
152
195
  when String,nil then nil
153
196
  else nil
154
197
  end
@@ -160,8 +203,8 @@ class Cell < XMLTiedItem
160
203
  when nil then nil
161
204
  when 'float' then Float
162
205
  when 'string' then String
163
- when 'time' then Time
164
- when 'date' then Date
206
+ when 'time' then :time
207
+ when 'date' then :datetime
165
208
  when 'percentage' then :percentage
166
209
  when 'N/A' then :unassigned
167
210
  when 'currency' then :currency
@@ -231,167 +274,11 @@ class Cell < XMLTiedItem
231
274
  def border_bottom; format.border_bottom end
232
275
  def border_left; format.border_left end
233
276
 
234
- end
235
-
236
- # proxy object to allow cell.format syntax. Also handles all logic for formats.
237
- # @private
238
- class CellFormat
239
- def initialize(cell)
240
- @cell = cell
241
- end
242
- def cellnode; @cell.xmlnode end
243
-
244
- # text style attribute readers
245
- def bold; get_text_style_node_attribute('font-weight') == 'bold' end
246
- alias :bold? :bold
247
- def italic; get_text_style_node_attribute('font-style') == 'italic' end
248
- def color; get_text_style_node_attribute('color') end
249
- def font_size; get_text_style_node_attribute('font-size') end
250
- def get_text_style_node_attribute(attribute_name)
251
- text_style_node.nil? ? nil : Tools.get_ns_attribute_value(text_style_node,'fo',attribute_name)
252
- end
253
- def background_color; get_cell_style_node_attribute('background-color') end
254
- def get_cell_style_node_attribute(attribute_name)
255
- cell_style_node.nil? ? nil : Tools.get_ns_attribute_value(cell_style_node,'fo',attribute_name)
256
- end
257
-
258
- # text style attribute writers
259
- def bold=(value); set_text_style_node_attribute('font-weight', value ? 'bold' : 'normal') end
260
- def italic=(value); set_text_style_node_attribute('font-style', value ? 'italic' : 'normal') end
261
- def color=(value); set_text_style_node_attribute('color', value) end
262
- def font_size=(value);set_text_style_node_attribute('font-size', value) end
263
- def set_text_style_node_attribute(attribute_name,value)
264
- @cell.detach if @cell.mode != :regular
265
- if text_style_node.nil?
266
- self.create_text_style_node
267
- raise 'Style node was not correctly initialized' if text_style_node.nil?
268
- end
269
- Tools.set_ns_attribute(text_style_node,'fo',attribute_name,value)
270
- end
271
- def background_color=(value); set_cell_style_node_attribute('background-color', value) end
272
- def set_cell_style_node_attribute(attribute_name,value)
273
- @cell.detach if @cell.mode != :regular
274
- if cell_style_node.nil?
275
- self.create_cell_style_node
276
- raise 'Style node was not correctly initialized' if cell_style_node.nil?
277
- end
278
- Tools.set_ns_attribute(cell_style_node,'fo',attribute_name,value)
279
- end
280
-
281
- # @!group initialization of style related nodes, if they do not exist
282
- def create_text_style_node
283
- create_style_node if style_name.nil? or style_node.nil?
284
- raise 'text_style_node already exists' unless text_style_node.nil?
285
- style_node << Tools.prepare_ns_node('style','text-properties')
286
- end
287
- def create_cell_style_node
288
- create_style_node if style_name.nil? or style_node.nil?
289
- raise 'cell_style_node already exists' unless cell_style_node.nil?
290
- style_node << Tools.prepare_ns_node('style','table-cell-properties')
291
- end
292
- def create_style_node
293
- if style_name.nil?
294
- proposed_style_name = unused_cell_style_name
295
- Tools.set_ns_attribute(cellnode,'table','style-name',proposed_style_name)
296
- raise 'Style name was not correctly initialized' if style_name!=proposed_style_name
297
- end
298
- anode = Tools.prepare_ns_node('style','style')
299
- Tools.set_ns_attribute(anode, 'style', 'name', proposed_style_name)
300
- Tools.set_ns_attribute(anode, 'style', 'family', 'table-cell')
301
- Tools.set_ns_attribute(anode, 'style', 'parent-style-name', 'Default')
302
- automatic_styles_node << anode
303
- raise 'Style node was not correctly initialized' if style_node.nil?
304
- end
305
-
306
- def unused_cell_style_name
307
- last = (cellnode.nil? ? [] : cellnode.doc.root.find('./office:automatic-styles/style:style')).
308
- collect {|node| node['name']}.
309
- collect{ |name| /^ce(\d*)$/.match(name); $1.andand.to_i}.
310
- compact.max || 0
311
- "ce#{last+1}"
312
- end
313
- def automatic_styles_node; style_node_with_partial_xpath('') end
314
- def style_name; Tools.get_ns_attribute_value(cellnode,'table','style-name',nil) end
315
- def style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]") end
316
- def text_style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]/style:text-properties") end
317
- def cell_style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]/style:table-cell-properties") end
318
- def style_node_with_partial_xpath(xpath)
319
- return nil if cellnode.nil?
320
- cellnode.doc.root.find("./office:automatic-styles#{xpath}").first
321
- end
322
- def currency
323
- Tools.get_ns_attribute_value(cellnode,'office','currency',nil)
324
- end
325
- #returns object representing top border of the cell
326
- def top; @top ||= Border.new(self,:top) end
327
- def bottom; @bottom ||= Border.new(self,:bottom) end
328
- def left; @left ||= Border.new(self,:left) end
329
- def right; @right ||= Border.new(self,:right) end
330
- alias :border_top :top
331
- alias :border_right :right
332
- alias :border_bottom :bottom
333
- alias :border_left :left
334
-
335
- def inspect
336
- "#<Rspreadsheet::CellFormat bold:#{bold?.inspect}, borders:#{top.get_value_string.inspect} #{right.get_value_string.inspect} #{bottom.get_value_string.inspect} #{left.get_value_string.inspect}>"
337
- end
338
-
339
- end
340
-
341
- # represents one of the borders of a cell
342
- class Border
343
- def initialize(cellformat,side)
344
- @cellformat = cellformat
345
- @side = side.to_s
346
- raise "Wrong side of border object, can be top, bottom, left or right" unless ['left','right','top','bottom'].include? @side
347
- end
348
- def cellnode; @cell.xmlnode end
349
- def attribute_name; "border-#{@side}" end
350
-
351
- def width=(value); set_border_string_part(1, value) end
352
- def style=(value); set_border_string_part(2, value.to_s) end
353
- def color=(value); set_border_string_part(3, value) end
354
- def width; get_border_string_part(1).to_f end
355
- def style; get_border_string_part(2) end
356
- def color; get_border_string_part(3) end
357
- def delete
358
- @cellformat.set_cell_style_node_attribute(attribute_name, 'none')
359
- end
360
-
361
-
362
- ## internals
363
-
364
- # set parth-th part of string which represents the border. String looks like "0.06pt solid #00ee00"
365
- # part is 1 for width, 2 for style or 3 for color
366
- def set_border_string_part(part,value)
367
- current_value = @cellformat.get_cell_style_node_attribute(attribute_name)
368
-
369
- if current_value.nil? or (current_value=='none')
370
- value_array = ['0.75pt', 'solid', '#000000'] # set default values
371
- else
372
- value_array = current_value.split(' ')
373
- end
374
- raise 'Strange border attribute value. Does not have 3 parts' unless value_array.length == 3
375
- value_array[part-1]=value
376
- @cellformat.set_cell_style_node_attribute(attribute_name, value_array.join(' '))
377
- end
378
-
379
- def get_border_string_part(part)
380
- current_value = @cellformat.get_cell_style_node_attribute(attribute_name) || @cellformat.get_cell_style_node_attribute('border')
381
- if current_value.nil? or (current_value=='none')
382
- return nil
383
- else
384
- value_array = current_value.split(' ')
385
- raise 'Strange border attribute value. Does not have 3 parts' unless value_array.length == 3
386
- return value_array[part-1]
387
- end
388
- end
389
-
390
- def get_value_string
391
- @cellformat.get_cell_style_node_attribute(attribute_name)
392
- end
393
-
394
- end
277
+ private
278
+ InternalDateFormat = '%Y-%m-%d'
279
+ InternalDateTimeFormat = '%FT%T'
280
+ InternalTimeFormat = 'PT%HH%MM%SS'
281
+ end ## Cell
395
282
 
396
283
  end # module
397
284
 
@@ -410,4 +297,3 @@ end # module
410
297
 
411
298
 
412
299
 
413
-
@@ -0,0 +1,203 @@
1
+ # @markup markdown
2
+ # @author Jakub Tesinsky
3
+ # @title rspreadsheet Cell
4
+
5
+ # require 'andand'
6
+ # require 'rspreadsheet/xml_tied_item'
7
+ # require 'date'
8
+ # require 'time' # extended functions for time like Time.strptime
9
+ # require 'bigdecimal'
10
+ # require 'bigdecimal/util' # for to_d method
11
+ # require 'helpers/class_extensions'
12
+
13
+ module Rspreadsheet
14
+ using ClassExtensions if RUBY_VERSION > '2.1'
15
+
16
+ ###
17
+ # Represents a format of a cell. This object is returned by `@cell.format` method and allows syntax like
18
+ #
19
+ # @cell.format.bold = true
20
+ # @cell.format.italic = false
21
+ # @cell.format.color = '#45AC00'
22
+ #
23
+ # Also handles all logic for formats.
24
+ # @private
25
+ class CellFormat
26
+ def initialize(cell)
27
+ @cell = cell
28
+ end
29
+ def cellnode; @cell.xmlnode end
30
+
31
+ # text style attribute readers
32
+ def bold; get_text_style_node_attribute('font-weight') == 'bold' end
33
+ alias :bold? :bold
34
+ def italic; get_text_style_node_attribute('font-style') == 'italic' end
35
+ def color; get_text_style_node_attribute('color') end
36
+ def font_size; get_text_style_node_attribute('font-size') end
37
+ def get_text_style_node_attribute(attribute_name)
38
+ text_style_node.nil? ? nil : Tools.get_ns_attribute_value(text_style_node,'fo',attribute_name)
39
+ end
40
+ def background_color; get_cell_style_node_attribute('background-color') end
41
+ def get_cell_style_node_attribute(attribute_name)
42
+ cell_style_node.nil? ? nil : Tools.get_ns_attribute_value(cell_style_node,'fo',attribute_name)
43
+ end
44
+
45
+ # text style attribute writers
46
+ def bold=(value); set_text_style_node_attribute('font-weight', value ? 'bold' : 'normal') end
47
+ def italic=(value); set_text_style_node_attribute('font-style', value ? 'italic' : 'normal') end
48
+ def color=(value); set_text_style_node_attribute('color', value) end
49
+ def font_size=(value);set_text_style_node_attribute('font-size', value) end
50
+ def set_text_style_node_attribute(attribute_name,value)
51
+ @cell.detach if @cell.mode != :regular
52
+ if text_style_node.nil?
53
+ self.create_text_style_node
54
+ raise 'Style node was not correctly initialized' if text_style_node.nil?
55
+ end
56
+ Tools.set_ns_attribute(text_style_node,'fo',attribute_name,value)
57
+ end
58
+ def background_color=(value); set_cell_style_node_attribute('background-color', value) end
59
+ def set_cell_style_node_attribute(attribute_name,value)
60
+ @cell.detach if @cell.mode != :regular
61
+ if cell_style_node.nil?
62
+ self.create_cell_style_node
63
+ raise 'Style node was not correctly initialized' if cell_style_node.nil?
64
+ end
65
+ Tools.set_ns_attribute(cell_style_node,'fo',attribute_name,value)
66
+ end
67
+
68
+ # @!group initialization of style related nodes, if they do not exist
69
+ def create_text_style_node
70
+ create_style_node if style_name.nil? or style_node.nil?
71
+ raise 'text_style_node already exists' unless text_style_node.nil?
72
+ style_node << Tools.prepare_ns_node('style','text-properties')
73
+ end
74
+ def create_cell_style_node
75
+ create_style_node if style_name.nil? or style_node.nil?
76
+ raise 'cell_style_node already exists' unless cell_style_node.nil?
77
+ style_node << Tools.prepare_ns_node('style','table-cell-properties')
78
+ end
79
+ def create_style_node
80
+ if style_name.nil?
81
+ proposed_style_name = unused_cell_style_name
82
+ Tools.set_ns_attribute(cellnode,'table','style-name',proposed_style_name)
83
+ raise 'Style name was not correctly initialized' if style_name!=proposed_style_name
84
+ end
85
+ anode = Tools.prepare_ns_node('style','style')
86
+ Tools.set_ns_attribute(anode, 'style', 'name', proposed_style_name)
87
+ Tools.set_ns_attribute(anode, 'style', 'family', 'table-cell')
88
+ Tools.set_ns_attribute(anode, 'style', 'parent-style-name', 'Default')
89
+ automatic_styles_node << anode
90
+ raise 'Style node was not correctly initialized' if style_node.nil?
91
+ end
92
+
93
+ def unused_cell_style_name
94
+ last = (cellnode.nil? ? [] : cellnode.doc.root.find('./office:automatic-styles/style:style')).
95
+ collect {|node| node['name']}.
96
+ collect{ |name| /^ce(\d*)$/.match(name); $1.andand.to_i}.
97
+ compact.max || 0
98
+ "ce#{last+1}"
99
+ end
100
+ def automatic_styles_node; style_node_with_partial_xpath('') end
101
+ def style_name; Tools.get_ns_attribute_value(cellnode,'table','style-name',nil) end
102
+ def style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]") end
103
+ def text_style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]/style:text-properties") end
104
+ def cell_style_node; style_node_with_partial_xpath("/style:style[@style:name=\"#{style_name}\"]/style:table-cell-properties") end
105
+ def style_node_with_partial_xpath(xpath)
106
+ return nil if cellnode.nil?
107
+ cellnode.doc.root.find("./office:automatic-styles#{xpath}").first
108
+ end
109
+ def currency
110
+ Tools.get_ns_attribute_value(cellnode,'office','currency',nil)
111
+ end
112
+ #returns object representing top border of the cell
113
+ def top; @top ||= Border.new(self,:top) end
114
+ def bottom; @bottom ||= Border.new(self,:bottom) end
115
+ def left; @left ||= Border.new(self,:left) end
116
+ def right; @right ||= Border.new(self,:right) end
117
+ alias :border_top :top
118
+ alias :border_right :right
119
+ alias :border_bottom :bottom
120
+ alias :border_left :left
121
+
122
+ def inspect
123
+ "#<Rspreadsheet::CellFormat bold:#{bold?.inspect}, borders:#{top.get_value_string.inspect} #{right.get_value_string.inspect} #{bottom.get_value_string.inspect} #{left.get_value_string.inspect}>"
124
+ end
125
+ # experimental. Allows to assign a style to cell.
126
+ def style_name=(value)
127
+ Tools.set_ns_attribute_value(cellnode,'table','style-name',value)
128
+ end
129
+
130
+ end
131
+
132
+ # represents one of the borders of a cell
133
+ class Border
134
+ def initialize(cellformat,side)
135
+ @cellformat = cellformat
136
+ @side = side.to_s
137
+ raise "Wrong side of border object, can be top, bottom, left or right" unless ['left','right','top','bottom'].include? @side
138
+ end
139
+ def cellnode; @cell.xmlnode end
140
+ def attribute_name; "border-#{@side}" end
141
+
142
+ def width=(value); set_border_string_part(1, value) end
143
+ def style=(value); set_border_string_part(2, value.to_s) end
144
+ def color=(value); set_border_string_part(3, value) end
145
+ def width; get_border_string_part(1).to_f end
146
+ def style; get_border_string_part(2) end
147
+ def color; get_border_string_part(3) end
148
+ def delete
149
+ @cellformat.set_cell_style_node_attribute(attribute_name, 'none')
150
+ end
151
+
152
+
153
+ ## internals
154
+
155
+ # set parth-th part of string which represents the border. String looks like "0.06pt solid #00ee00"
156
+ # part is 1 for width, 2 for style or 3 for color
157
+ def set_border_string_part(part,value)
158
+ current_value = @cellformat.get_cell_style_node_attribute(attribute_name)
159
+
160
+ if current_value.nil? or (current_value=='none')
161
+ value_array = ['0.75pt', 'solid', '#000000'] # set default values
162
+ else
163
+ value_array = current_value.split(' ')
164
+ end
165
+ raise 'Strange border attribute value. Does not have 3 parts' unless value_array.length == 3
166
+ value_array[part-1]=value
167
+ @cellformat.set_cell_style_node_attribute(attribute_name, value_array.join(' '))
168
+ end
169
+
170
+ def get_border_string_part(part)
171
+ current_value = @cellformat.get_cell_style_node_attribute(attribute_name) || @cellformat.get_cell_style_node_attribute('border')
172
+ if current_value.nil? or (current_value=='none')
173
+ return nil
174
+ else
175
+ value_array = current_value.split(' ')
176
+ raise 'Strange border attribute value. Does not have 3 parts' unless value_array.length == 3
177
+ return value_array[part-1]
178
+ end
179
+ end
180
+
181
+ def get_value_string
182
+ @cellformat.get_cell_style_node_attribute(attribute_name)
183
+ end
184
+
185
+ end
186
+
187
+ end # module
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
@@ -185,9 +185,13 @@ module Tools
185
185
  end
186
186
  raise 'Could not get unused filename within sane times of iterations'
187
187
  end
188
-
189
- end
190
188
 
189
+ def self.new_time_value(h,m,s)
190
+ Time.new(StartOfEpoch.year,StartOfEpoch.month,StartOfEpoch.day,h,m,s)
191
+ end
192
+
193
+ end
194
+
191
195
  end
192
196
 
193
197
  # @private
@@ -1,3 +1,3 @@
1
1
  module Rspreadsheet
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.3"
3
3
  end
@@ -73,7 +73,7 @@ class Workbook
73
73
  when @filename.nil? && (io.kind_of?(String) || io.kind_of?(File) || io.kind_of?(IO) || io.kind_of?(StringIO))
74
74
  Zip::File.open(TEMPLATE_FILE_NAME) do |empty_template_zip| # open empty_template file
75
75
  write_zip_to(io) do |output_zip| # open output stream of file
76
- copy_internally_without_content_and_manifest(empty_template_zip,output_zip) # copy empty_template internals
76
+ copy_internally_without_content(empty_template_zip,output_zip) # copy empty_template internals
77
77
  update_manifest_and_content_xml(empty_template_zip,output_zip) # update xmls + pictures
78
78
  end
79
79
  end
@@ -91,7 +91,7 @@ class Workbook
91
91
  when @filename.kind_of?(String) && (io.kind_of?(IO) || io.kind_of?(StringIO))
92
92
  Zip::File.open(@filename) do | old_zip | # open old file
93
93
  write_zip_to(io) do |output_zip_stream| # open output stream
94
- copy_internally_without_content_and_manifest(old_zip,output_zip_stream) # copy the old internals
94
+ copy_internally_without_content(old_zip,output_zip_stream) # copy the old internals
95
95
  update_manifest_and_content_xml(old_zip,output_zip_stream) # update xmls + pictures
96
96
  end
97
97
  end
@@ -118,14 +118,14 @@ class Workbook
118
118
  def update_manifest_xml(input_zip,output_zip)
119
119
  # read manifest
120
120
  @manifest_xml = LibXML::XML::Document.io input_zip.get_input_stream(MANIFEST_FILE_NAME)
121
-
121
+ modified = false
122
122
  # save all pictures - iterate through sheets and pictures and check if they are saved and if not, save them
123
123
  @worksheets.each do |sheet|
124
124
  sheet.images.each do |image|
125
125
  # check if it is saved
126
126
  @ifname = image.internal_filename
127
- if @ifname.nil? or input_zip.find_entry(@ifname).nil?
128
- # if it does not have name -> make up unused name
127
+ if @ifname.nil? or input_zip.find_entry(@ifname).nil?
128
+ # if it does not have name -> make up unused name
129
129
  if @ifname.nil?
130
130
  @ifname = image.internal_filename = Rspreadsheet::Tools.get_unused_filename(input_zip,'Pictures/',File.extname(image.original_filename))
131
131
  end
@@ -141,19 +141,21 @@ class Workbook
141
141
  Tools.set_ns_attribute(node,'manifest','full-path',@ifname)
142
142
  Tools.set_ns_attribute(node,'manifest','media-type',image.mime)
143
143
  @manifest_xml.find_first("//manifest:manifest") << node
144
+ modified = true
144
145
  end
145
146
  end
146
147
  end
147
148
  end
148
149
 
149
- # write manifest
150
- save_entry_to_zip(output_zip, MANIFEST_FILE_NAME, @manifest_xml.to_s)
150
+ # write manifest if it was modified
151
+ save_entry_to_zip(output_zip, MANIFEST_FILE_NAME,
152
+ @manifest_xml.to_s) if modified
151
153
  end
152
154
 
153
- def copy_internally_without_content_and_manifest(input_zip,output_zip)
155
+ def copy_internally_without_content(input_zip,output_zip)
154
156
  input_zip.each do |entry|
155
157
  next unless entry.file?
156
- next if entry.name == CONTENT_FILE_NAME || entry.name == MANIFEST_FILE_NAME
158
+ next if entry.name == CONTENT_FILE_NAME
157
159
  save_entry_to_zip(output_zip, entry.name, entry.get_input_stream.read)
158
160
  end
159
161
  end
@@ -161,7 +163,7 @@ class Workbook
161
163
  def save_entry_to_zip(zip,internal_filename,contents)
162
164
  if zip.kind_of? Zip::File
163
165
  # raise [internal_filename,contents].inspect unless File.exists?(internal_filename)
164
- zip.get_output_stream(internal_filename) do |f|
166
+ zip.get_output_stream(internal_filename) do |f|
165
167
  f.write contents
166
168
  end
167
169
  else
@@ -167,9 +167,11 @@ module XMLTiedArray
167
167
  # array containing subnodes of xmlnode which represent subitems
168
168
  def xmlsubnodes
169
169
  return [] if xmlnode.nil?
170
+ ele = xmlnode.elements
171
+ return [] if ele.empty?
170
172
  so = subitem_xml_options[:xml_items_node_name]
171
173
 
172
- xmlnode.elements.select do |node|
174
+ ele.select do |node|
173
175
  node.andand.name == so
174
176
  end
175
177
  end
data/rspreadsheet.gemspec CHANGED
@@ -22,12 +22,12 @@ Gem::Specification.new do |spec|
22
22
  # if the shell fails, the system command returns nil. In that case we assume that the package is NOT installed. It might be overkill, because I am supresing the stderr as well
23
23
  (system("dpkg-query -l #{pkgname} 2>/dev/null | grep -q '^i'")==true) or # debian based
24
24
  (system("rpm -qa 2>/dev/null | grep -q '#{pkgname}' ")==true) or # rpm based
25
- (system("pkg_info -q -e #{pkgname} >/dev/null 2>&1")==true) # openbsd and alike
25
+ (system("pkg_info -q -e '#{pkgname}-*' >/dev/null 2>&1")==true) # OpenBSD and alike (although OpenBSD registers the gem as a gem, so this is perhaps unnecessary
26
26
  end
27
-
27
+
28
28
  # runtime dependencies
29
29
  unless package_natively_installed?('ruby-libxml')
30
- spec.add_runtime_dependency 'libxml-ruby', '~>2.7' # parsing XML files
30
+ spec.add_runtime_dependency 'libxml-ruby', '2.8' # parsing XML files
31
31
  end
32
32
  spec.add_runtime_dependency 'rubyzip', '~>1.1' # opening zip files
33
33
  spec.add_runtime_dependency 'andand', '~>1.3'
@@ -45,7 +45,7 @@ Gem::Specification.new do |spec|
45
45
  spec.add_development_dependency "guard", '~>2.13'
46
46
  spec.add_development_dependency "guard-rspec", '~>4.6'
47
47
  end
48
-
48
+
49
49
  # spec.add_development_dependency 'equivalent-xml' # implementing xml diff
50
50
 
51
51
  end
data/spec/cell_spec.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
-
2
+
3
3
  describe Rspreadsheet::Cell do
4
4
  before do
5
5
  book1 = Rspreadsheet.new
@@ -38,6 +38,8 @@ describe Rspreadsheet::Cell do
38
38
  @cell.value.should == 'zapate'
39
39
  @sheet1.rows[1][2] = 'zaseste' ## tohle je divoka moznost
40
40
  @cell.value.should == 'zaseste'
41
+
42
+ @sheet1.A11 = 'dalsi test'
41
43
  end
42
44
  it 'can include links' do
43
45
  @sheet2.A12.should == '[http://example.org/]'
@@ -94,7 +96,7 @@ describe Rspreadsheet::Cell do
94
96
  end
95
97
  it 'returns correct type for the cell' do
96
98
  @sheet2.cell(1,2).type.should eq :string
97
- @sheet2.cell(2,2).type.should eq :date
99
+ @sheet2.cell(2,2).type.should eq :datetime
98
100
  @sheet2.cell(3,1).type.should eq :float
99
101
  @sheet2.cell(3,2).type.should eq :percentage
100
102
  @sheet2.cell(4,2).type.should eq :string
@@ -104,7 +106,7 @@ describe Rspreadsheet::Cell do
104
106
  end
105
107
  it 'returns value of correct type' do
106
108
  @sheet2[1,2].should be_kind_of(String)
107
- @sheet2[2,2].should be_kind_of(Date)
109
+ @sheet2[2,2].should be_kind_of(DateTime) # maybe date
108
110
  @sheet2[3,1].should be_kind_of(Float)
109
111
  @sheet2[3,2].should be_kind_of(Float)
110
112
  @sheet2.cell(3,2).type.should eq :percentage
@@ -276,14 +278,38 @@ describe Rspreadsheet::Cell do
276
278
  @cell.value.min.should eq 42
277
279
  @cell.value.sec.should eq 0
278
280
  end
279
- it 'can read various types of times', :pending => 'see is' do
280
- raise @sheet2.cell('D23').xml.inspect
281
+ it 'parse_time_value converts correcty different time values' do
282
+ dyear = 1899; dmonth = 12; dday = 30
283
+ Rspreadsheet::Cell.parse_time_value('PT923451H33M00S').should == Time.new(2005,5,5,3,33,00,0)
284
+ Rspreadsheet::Cell.parse_time_value('PT1H33M00S').should == Time.new(dyear,dmonth,dday,1,33,00,0)
285
+ end
286
+ it 'handles time of day correctly on assignement' do
287
+ @sheet1.A11 = Rspreadsheet::Tools.new_time_value(2,13,27)
288
+ @sheet1.A12 = @sheet1.A11
289
+ @sheet1.A12.should == @sheet1.A11
290
+ @sheet1.cell('A12').type.should == :time
291
+ end
292
+ it 'can read various types of times' do
281
293
  expect {@cell = @sheet2.cell('D22'); @cell.value }.not_to raise_error
294
+ @cell.value.hour.should == 2
295
+ @cell.value.min.should == 22
282
296
  expect {@cell = @sheet2.cell('D23'); @cell.value }.not_to raise_error
283
-
284
- @cell.value.should == Time.new(2005,5,5,3,33,00)
297
+ @cell.value.should == Time.new(2005,5,5,3,33,0,0)
298
+ end
299
+ it 'handles dates and datetimes correctly on assignement' do
300
+ @sheet1.A11 = DateTime.new(2011,1,1,2,13,27,0)
301
+ @sheet1.A12 = @sheet1.A11
302
+ @sheet1.A12.should == @sheet1.A11
303
+ @sheet1.cell('A12').type.should == :datetime
304
+ @sheet1.A11 = Date.new(2012,2,2)
305
+ @sheet1.A12 = @sheet1.A11
306
+ @sheet1.A12.should == @sheet1.A11
307
+ @sheet1.cell('A12').type.should == :datetime
308
+ end
309
+ it 'can read various types of dates' do
310
+ expect {@cell = @sheet2.cell('F22'); @cell.value }.not_to raise_error
311
+ @cell.value.should == DateTime.new(2006,6,6,3,44,21,0)
285
312
  end
286
-
287
313
  it 'can be addressed by even more ways and all are identical' do
288
314
  @cell = @sheet1.cell(2,2)
289
315
  @sheet1.cell('B2').value = 'zaseste'
@@ -408,4 +434,8 @@ describe Rspreadsheet::Cell do
408
434
  @cell.format.top.style = 'none'
409
435
  @cell.border_top.should_not be_nil ## ?????
410
436
  end
437
+
438
+ it 'automatically creates new style, if a style is automatic, some of its attributes changes and there are several cells pointing to it', :penting=>'' do
439
+
440
+ end
411
441
  end
data/spec/testfile1.ods CHANGED
Binary file
data/spec/tools_spec.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Rspreadsheet::Tools, :focus do
3
+ describe Rspreadsheet::Tools do
4
4
  before do
5
5
  @tools = Rspreadsheet::Tools
6
6
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspreadsheet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub A.Těšínský
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-20 00:00:00.000000000 Z
11
+ date: 2017-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: libxml-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: '2.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: '2.8'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rubyzip
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,34 @@ dependencies:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0.7'
125
+ - !ruby/object:Gem::Dependency
126
+ name: guard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.13'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.13'
139
+ - !ruby/object:Gem::Dependency
140
+ name: guard-rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '4.6'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '4.6'
111
153
  description: Manipulating OpenDocument spreadsheets with Ruby. This gem can create
112
154
  new, read existing files abd modify them. When modyfying files, it tries to change
113
155
  as little as possible, making it as much forward compatifle as possible.
@@ -136,6 +178,7 @@ files:
136
178
  - lib/helpers/configuration.rb
137
179
  - lib/rspreadsheet.rb
138
180
  - lib/rspreadsheet/cell.rb
181
+ - lib/rspreadsheet/cell_format.rb
139
182
  - lib/rspreadsheet/column.rb
140
183
  - lib/rspreadsheet/empty_file_template.ods
141
184
  - lib/rspreadsheet/image.rb
@@ -165,8 +208,6 @@ files:
165
208
  - spec/tools_spec.rb
166
209
  - spec/workbook_spec.rb
167
210
  - spec/worksheet_spec.rb
168
- - w/test.rb
169
- - w/time.ods
170
211
  homepage: https://github.com/gorn/rspreadsheet
171
212
  licenses:
172
213
  - GPL
@@ -187,7 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
228
  version: '0'
188
229
  requirements: []
189
230
  rubyforge_project:
190
- rubygems_version: 2.2.2
231
+ rubygems_version: 2.5.2
191
232
  signing_key:
192
233
  specification_version: 4
193
234
  summary: Manipulating spreadsheets with Ruby (read / create / modify OpenDocument
data/w/test.rb DELETED
@@ -1,8 +0,0 @@
1
-
2
-
3
- require 'rspreadsheet'
4
- book = Rspreadsheet.open './time.ods'
5
- sheet = book.worksheets(1)
6
- p cell = sheet.A1 # exception
7
- cell = sheet.cells('A1') # works
8
- p cell.to_s # exception
data/w/time.ods DELETED
Binary file