spreet 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,49 @@
1
1
  = Spreet
2
2
 
3
- Universal handler for spr[eadsh]eets.
3
+ Universal handler for spr[eadsh]eets.
4
+
5
+ == Why ?
6
+ This gems is a handler for spreadsheets. With its independent API, it is possible to create, update files in some formats. Today the list is not very long:
7
+
8
+ * CSV: UTF-8 with commas (Read & Write)
9
+ * CSV for Excel: CP1252 with semicolons (Read & Write)
10
+ * ODS: Open Document Format (Read & Write with restrictions)
11
+
12
+ == Installation
13
+
14
+ gem install spreet
15
+
16
+ == How to use it
17
+
18
+ # Create a new document
19
+ doc = Spreet::Document.new
20
+ sheet = doc.sheets.add "My Sheet"
21
+
22
+ # Coordinates can be called with spreadsheet style...
23
+ sheet["A1"] = "Last name"
24
+ # ...or more classic style...
25
+ sheet[1,0] = "First name"
26
+ # ...or if necessary as a Hash
27
+ sheet[:x=>2, :y=>0] = "Born on"
28
+
29
+ sheet.next_row
30
+ for person in People.all
31
+ sheet.row person.last_name, person.first_name, person.born_on
32
+ end
33
+
34
+ # Write it as a classic CSV
35
+ sheet.write("people-1.csv")
36
+ # Write it as a CSV for Excel
37
+ sheet.write("people-2.csv", :format=>:xcsv) # CSV for Excel
38
+ # or write it as an Open Document Spreadsheet
39
+ sheet.write("people-3.ods")
40
+
41
+ == To do soon
42
+
43
+ * Add style management for cells
44
+ * Add Header/Footer
45
+ * HTML Writer
46
+ * PDF Writer like OpenOffice/LibreOffice would make it
47
+
48
+ == Travis
49
+ {<img src="https://secure.travis-ci.org/burisu/spreet.png"/>}[http://travis-ci.org/burisu/spreet]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+
3
+ # Class to manage big hash with lot of pairs
4
+ class BigArray
5
+
6
+ def initialize(klass_name=nil, partition=8, levels=4)
7
+ @partition = partition.to_i
8
+ raise ArgumentError.new("Partition must be an integer > 0") unless @partition > 0
9
+ @levels = levels.to_i
10
+ raise ArgumentError.new("Levels must be an integer > 0") unless @levels > 0
11
+ klass_name ||= "#{self.class.name}#{@partition}_#{@levels}"
12
+ @base_class = "Hash"
13
+ code = ""
14
+ code << "class #{klass_name}\n"
15
+ code << " def initialize()\n"
16
+ code << " @root = #{@base_class}.new\n"
17
+ code << " end\n\n"
18
+
19
+ code << " def [](index)\n"
20
+ code << dive do |pointer|
21
+ "return nil"
22
+ end.strip.gsub(/^/, ' ')+"\n"
23
+ code << " return cursor[#{index_at_level(@levels)}]\n"
24
+ code << " end\n\n"
25
+
26
+ code << " def []=(index, value)\n"
27
+ # code << " index, value = args[0], args[1]\n"
28
+ code << dive do |pointer|
29
+ "#{pointer} = #{@base_class}.new"
30
+ end.strip.gsub(/^/, ' ')+"\n"
31
+ code << " return cursor[#{index_at_level(@levels)}] = value\n"
32
+ code << " end\n\n"
33
+
34
+ code << " def delete(index)\n"
35
+ # code << " index, value = args[0], args[1]\n"
36
+ code << dive do |pointer|
37
+ "return nil"
38
+ end.strip.gsub(/^/, ' ')+"\n"
39
+ code << " return cursor.delete(#{index_at_level(@levels)})\n"
40
+ code << " end\n\n"
41
+
42
+ code << " def each(&block)\n"
43
+ code << browse do
44
+ "yield(index, value)"
45
+ end.strip.gsub(/^/, ' ')+"\n"
46
+ code << " end\n\n"
47
+
48
+ code << " def to_hash\n"
49
+ code << " hash = {}\n"
50
+ code << browse do
51
+ "hash[index] = value"
52
+ end.strip.gsub(/^/, ' ')+"\n"
53
+ code << " return hash\n"
54
+ code << " end\n\n"
55
+
56
+ code << "end\n"
57
+ # raise code
58
+ eval(code)
59
+ return self.class.const_get(klass_name)
60
+ end
61
+
62
+ private
63
+
64
+ def dive(&block)
65
+ code = ""
66
+ for level in 1..(@levels-1)
67
+ pointer = "#{level == 1 ? '@root' : 'cursor'}[#{index_at_level(level)}]"
68
+ code << "unless #{pointer}.is_a?(#{@base_class})\n"
69
+ code << yield(pointer).to_s.strip.gsub(/^/, ' ')+"\n"
70
+ code << "end\n"
71
+ code << "cursor = #{pointer}\n"
72
+ end
73
+ return code
74
+ end
75
+
76
+ def browse(level = 1, &block)
77
+ code = ""
78
+ value = (level == @levels ? 'value' : "h#{level}")
79
+ code << "for l#{level}, #{value} in #{level == 1 ? '@root' : 'h'+(level-1).to_s}\n"
80
+ if level > 1
81
+ i = (level == @levels ? "index" : "i#{level}")
82
+ code << " #{i} = ("+(level>2 ? "i" : "l")+"#{level-1} << #{@partition})|l#{level}\n"
83
+ end
84
+ if level == @levels
85
+ code << yield.to_s.strip.gsub(/^/, ' ')+"\n"
86
+ else
87
+ code << browse(level + 1, &block).to_s.strip.gsub(/^/, ' ')+"\n"
88
+ end
89
+ code << "end\n"
90
+ return code
91
+ end
92
+
93
+ def index_at_level(level, variable="index")
94
+ v = variable
95
+ v = "(#{v} >> #{(@levels-level)*@partition})" if level < @levels
96
+ return "#{v}&#{2**@partition-1}"
97
+ end
98
+
99
+
100
+ end
@@ -0,0 +1,145 @@
1
+ # encoding: utf-8
2
+
3
+ # Class to manage durations (or intervals in SQL vocabulary)
4
+ class Duration
5
+ # 365.25 * 86_400 == 31_557_600
6
+ SECONDS_IN_YEAR = 31_557_600 # 31_556_926 => 31_556_928
7
+ SECONDS_IN_MONTH = SECONDS_IN_YEAR / 12
8
+
9
+ FIELDS = [:years, :months, :days, :hours, :minutes, :seconds]
10
+ attr_accessor *FIELDS
11
+ attr_reader :sign
12
+
13
+ def initialize(*args)
14
+ @years, @months, @days, @hours, @minutes, @seconds = 0, 0, 0, 0, 0, 0
15
+ @sign = 1
16
+ if args.size == 1 and args[0].is_a? String
17
+ self.parse(args[0])
18
+ else
19
+ @years = (args.shift || 0).to_i
20
+ @months = (args.shift || 0).to_i
21
+ @days = (args.shift || 0).to_i
22
+ @hours = (args.shift || 0).to_i
23
+ @minutes = (args.shift || 0).to_i
24
+ @seconds = (args.shift || 0).to_i
25
+ end
26
+ end
27
+
28
+ def parse(string)
29
+ unless string.match(/^\-?P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/)
30
+ raise ArgumentError.new("Malformed string")
31
+ end
32
+ strings = string.split('T')
33
+ strings[0].gsub(/\d+[YMD]/) do |token|
34
+ code, count = token.to_s[-1..-1], token.to_s[0..-2].to_i
35
+ if code == "Y"
36
+ @years = count
37
+ elsif code == "M"
38
+ @months = count
39
+ elsif code == "D"
40
+ @days = count
41
+ end
42
+ token
43
+ end
44
+ strings[1].to_s.gsub(/(\d+[HM]|\d+(\.\d+)?S)/) do |token|
45
+ code, count = token.to_s[-1..-1], token.to_s[0..-2]
46
+ if code == "H"
47
+ @hours = count.to_i
48
+ elsif code == "M"
49
+ @minutes = count.to_i
50
+ elsif code == "S"
51
+ @seconds = count.to_f
52
+ end
53
+ token
54
+ end
55
+ self.sign = (string.match(/^\-/) ? -1 : 1)
56
+ return self
57
+ end
58
+
59
+ def sign=(val)
60
+ @sign = (val >= 0 ? 1 : -1)
61
+ end
62
+
63
+ def to_s(compression = :normal)
64
+ if compression == :maximum
65
+ return (@sign > 0 ? "" : "-")+"P#{@years.to_s+'Y' if @years > 0}#{@months.to_s+'M' if @months > 0}#{@days.to_s+'D' if @days > 0}"+((@hours.zero? and @minutes.zero? and @seconds.zero?) ? '' : "T#{(@hours.to_s+'H') if @hours > 0}#{(@minutes.to_s+'M') if @minutes > 0}#{((@seconds.floor != @seconds ? @seconds.to_s : @seconds.to_i.to_s)+'S') if @seconds > 0}")
66
+ elsif compression == :minimum
67
+ return (@sign > 0 ? "" : "-")+"P"+@years.to_s+"Y"+@months.to_s+"M"+@days.to_s+"DT"+@hours.to_s+"H"+@minutes.to_s+"M"+@seconds.to_s+"S"
68
+ else
69
+ return (@sign > 0 ? "" : "-")+"P"+((@years.zero? and @months.zero? and @days.zero?) ? '' : @years.to_s+"Y"+@months.to_s+"M"+@days.to_s+"D")+((@hours.zero? and @minutes.zero? and @seconds.zero?) ? '' : "T"+@hours.to_s+"H"+@minutes.to_s+"M"+(@seconds.round != @seconds ? @seconds.to_s : @seconds.to_i.to_s)+"S")
70
+ end
71
+ end
72
+
73
+ # Export all values to hash
74
+ def to_hash
75
+ {:years=>@years, :months=>@months, :days=>@days, :hours=>@hours, :minutes=>@minutes, :seconds=>@seconds, :sign=>@sign}
76
+ end
77
+
78
+ # Computes a duration in seconds based on theoric and statistic values
79
+ # Because the relation between some of date parts isn't fixed (such as the number of days in a month),
80
+ # the order relationship between durations is only partial, and the result of a comparison
81
+ # between two durations may be undetermined.
82
+ def to_f
83
+ count = @seconds
84
+ count += 60 * @minutes
85
+ # 60 * 60 = 3_600
86
+ count += 3_600 * @hours
87
+ # 60 * 60 * 24 = 86_400
88
+ count += 86_400 * @days
89
+ # 365.25/12 * 86_400 == 31_557_600/12 == 2_629_800
90
+ count += SECONDS_IN_MONTH * @months
91
+ # 365.25 * 86_400 == 31_557_600
92
+ count += SECONDS_IN_YEAR * @years
93
+ return @sign * count
94
+ end
95
+
96
+ def to_i
97
+ self.to_f.to_i
98
+ end
99
+
100
+ # Normalize seconds, minutes, hours and month with their fixed relations
101
+ def normalize!(normalize_method = :right)
102
+ if normalize_method == :seconds
103
+ count = self.to_f
104
+ @years = (count / SECONDS_IN_YEAR).floor
105
+ count -= @years * SECONDS_IN_YEAR
106
+ @months = (count / SECONDS_IN_MONTH).floor
107
+ count -= @months * SECONDS_IN_MONTH
108
+ @days = (count / 86_400).floor
109
+ count -= @days * 86_400
110
+ @hours = (count / 3_600).floor
111
+ count -= @hours * 3_600
112
+ @minutes = (count / 60).floor
113
+ count -= @minutes * 60
114
+ @seconds = count
115
+ else
116
+ if @seconds >= 60
117
+ minutes = (@seconds / 60).floor
118
+ @seconds -= minutes * 60
119
+ @minutes += minutes
120
+ end
121
+ if @minutes >= 60
122
+ hours = (@minutes / 60).floor
123
+ @minutes -= hours * 60
124
+ @hours += hours
125
+ end
126
+ if @hours >= 24
127
+ days = (@hours / 24).floor
128
+ @hours -= days * 24
129
+ @days += days
130
+ end
131
+ # No way to convert correctly days in month
132
+ if @months >= 12
133
+ years = (@months / 12).floor
134
+ @months -= years * 12
135
+ @years += years
136
+ end
137
+ end
138
+ return self
139
+ end
140
+
141
+ def normalize(normalize_method = :right)
142
+ self.dup.normalize!(normalize_method)
143
+ end
144
+
145
+ end
@@ -1,11 +1,19 @@
1
1
  # encoding: utf-8
2
2
  require 'pathname'
3
+ require 'duration'
4
+ require 'money'
5
+ require 'time'
6
+ require 'big_array'
7
+ require 'spreet/coordinates'
8
+
9
+ # Create class for arrays
10
+ BigArray.new("Cells", 10, 3)
3
11
 
4
12
  module Spreet
5
13
 
6
14
  module VERSION
7
15
  version = nil
8
- File.open("VERSION") {|f| version = f.read.split('.')}
16
+ File.open(File.join(File.dirname(__FILE__), "..", "VERSION")) {|f| version = f.read.split('.')}
9
17
  MAJOR = version[0].to_i.freeze
10
18
  MINOR = version[1].to_i.freeze
11
19
  TINY = version[2].to_i.freeze
@@ -15,83 +23,42 @@ module Spreet
15
23
  end
16
24
 
17
25
 
18
- class Coordinates
19
- # Limit coordinates x and y in 0..65535 but coordinates are in one integer of 32 bits
20
- CPU_SEMI_WIDTH = 16 # ((RUBY_PLATFORM.match(/^[^\-]*[^\-0-9]64/) ? 64 : 32) / 2).freeze
21
- Y_FILTER = ((1 << CPU_SEMI_WIDTH) - 1).freeze
22
-
23
- BASE_26_BEF = "0123456789abcdefghijklmnop"
24
- BASE_26_AFT = "abcdefghijklmnopqrstuvwxyz"
25
-
26
- attr_accessor :x, :y
27
- def initialize(*args)
28
- value = (args.size == 1 ? args[0] : args)
29
- @x, @y = 0, 0
30
- if value.is_a? String
31
- if value.downcase.match(/^[a-z]+[0-9]+$/)
32
- value = value.downcase.split(/([A-Z]+|[0-9]+)/).delete_if{|x| x.size.zero?}
33
- @x, @y = value[0].tr(BASE_26_AFT, BASE_26_BEF).to_i(26), value[1].to_i(10)-1
34
- elsif value.downcase.match(/^[0-9]+[^0-9]+[0-9]+$/)
35
- value = value.downcase.split(/[^0-9]+/)
36
- @x, @y = value[0].to_i(10), value[1].to_i(10)
37
- end
38
- elsif value.is_a? Integer
39
- @x, @y = (value >> CPU_SEMI_WIDTH), value & Y_FILTER
40
- elsif value.is_a? Coordinates
41
- @x, @y = value.x, value.y
42
- elsif value.is_a? Array
43
- @x, @y = value[0].to_i, value[1].to_i
44
- elsif value.is_a? Hash
45
- @x, @y = value[:x] || value[:column] || 0, value[:y] || value[:row] || 0
46
- end
47
- end
48
-
49
- def to_s
50
- @x.to_s(26).tr(BASE_26_BEF, BASE_26_AFT).upcase+(@y+1).to_s(10)
51
- end
52
-
53
- def to_a
54
- [@x, @y]
55
- end
56
-
57
- def to_hash
58
- {:x=>@x, :y=>@y}
59
- end
60
-
61
- def to_i
62
- (@x << CPU_SEMI_WIDTH) + @y
63
- end
64
-
65
- def ==(other_coordinate)
66
- other_coordinate.x == self.x and other_coordinate.y == self.y
67
- end
68
-
69
- def <=>(other_coordinate)
70
- self.to_i <=> other_coordinate.to_i
71
- end
72
- end
73
-
74
26
  # Represents a cell in a sheet
75
27
  class Cell
76
28
  attr_reader :text, :value, :type, :sheet, :coordinates
29
+ attr_accessor :annotation
77
30
 
78
31
  def initialize(sheet, *args)
79
32
  @sheet = sheet
80
33
  @coordinates = Coordinates.new(*args)
81
34
  self.value = nil
82
35
  @empty = true
36
+ @covered = false # determine_covered
37
+ @annotation = nil
83
38
  end
84
39
 
85
40
  def value=(val)
86
- @value = val
87
- @type = determine_type
88
- @text = val.to_s
89
- @empty = false
41
+ if val.is_a?(Cell)
42
+ @value = val.value
43
+ @type = val.type
44
+ self.text = val.text
45
+ @empty = val.empty?
46
+ @annotation = val.annotation
47
+ else
48
+ @value = val
49
+ @type = determine_type
50
+ self.text = val
51
+ @empty = false
52
+ end
90
53
  end
91
54
 
92
55
  def empty?
93
56
  @empty
94
57
  end
58
+
59
+ def covered?
60
+ @covered
61
+ end
95
62
 
96
63
  def clear!
97
64
  self.value = nil
@@ -106,29 +73,35 @@ module Spreet
106
73
  self.coordinates <=> other_cell.coordinates
107
74
  end
108
75
 
76
+ def text=(val)
77
+ @text = val.to_s
78
+ end
79
+
80
+ def inspect
81
+ "<#{self.coordinates}: #{self.text.inspect}#{'('+self.value.inspect+')' if self.text != self.value}>"
82
+ end
83
+
109
84
  private
85
+
110
86
 
111
87
  def determine_type
112
- if value.is_a? Date
88
+ if value.is_a? Date or value.is_a? DateTime
113
89
  :date
114
- elsif value.is_a? Integer
115
- :integer
116
- elsif value.is_a? Numeric
117
- :decimal
118
- elsif value.is_a? DateTime
119
- :datetime
90
+ elsif value.is_a? Numeric # or percentage
91
+ :float
92
+ elsif value.is_a? Money
93
+ :currency
94
+ elsif value.is_a? Duration
95
+ :time
120
96
  elsif value.is_a?(TrueClass) or value.is_a?(FalseClass)
121
97
  :boolean
122
- elsif value.nil?
123
- :null
124
- else
98
+ else # if value.is_a?(String)
125
99
  :string
126
100
  end
127
101
  end
128
102
 
129
103
  end
130
104
 
131
-
132
105
  class Sheet
133
106
  attr_reader :document, :name, :columns
134
107
  attr_accessor :current_row
@@ -138,7 +111,8 @@ module Spreet
138
111
  self.name = name
139
112
  raise ArgumentError.new("Must be a Document") unless document.is_a? Document
140
113
  @current_row = 0
141
- @cells = {}
114
+ @cells = {} # BigArray::Cells.new
115
+ @bound = compute_bound
142
116
  end
143
117
 
144
118
  def name=(value)
@@ -152,11 +126,6 @@ module Spreet
152
126
  @name = value
153
127
  end
154
128
 
155
- def cells
156
- @cells.delete_if{|k,v| v.empty?}
157
- @cells.values
158
- end
159
-
160
129
  def next_row(increment = 1)
161
130
  @current_row += increment
162
131
  end
@@ -175,7 +144,7 @@ module Spreet
175
144
  value = args.delete_at(-1)
176
145
  cell = self[*args]
177
146
  cell.value = value
178
- @bound = compute_bound
147
+ @updated = true
179
148
  end
180
149
 
181
150
  def row(*args)
@@ -188,13 +157,17 @@ module Spreet
188
157
  next_row
189
158
  end
190
159
 
160
+ def rows(index)
161
+ row = []
162
+ for i in 0..bound.x
163
+ row[i] = self[i, index]
164
+ end
165
+ return row
166
+ end
167
+
191
168
  def each_row(&block)
192
169
  for j in 0..bound.y
193
- row = []
194
- for i in 0..bound.x
195
- row[i] = self[i, j]
196
- end
197
- yield row
170
+ yield rows(j)
198
171
  end
199
172
  end
200
173
 
@@ -204,13 +177,17 @@ module Spreet
204
177
  end
205
178
 
206
179
  def bound
207
- @bound
180
+ if @updated
181
+ compute_bound
182
+ else
183
+ @bound
184
+ end
208
185
  end
209
186
 
210
187
  def remove!(coordinates)
211
188
  raise ArgumentError.new("Must be a Coordinates") unless document.is_a?(Coordinates)
212
189
  @cells.delete(coordinates.to_i)
213
- @bound = compute_bound
190
+ @updated = true
214
191
  end
215
192
 
216
193
  # Moves the sheet to an other position in the list of sheets
@@ -231,14 +208,17 @@ module Spreet
231
208
  private
232
209
 
233
210
  def compute_bound
234
- bound = Coordinates.new
235
- for id, cell in @cells
211
+ bound = Coordinates.new(0,0)
212
+ for index, cell in @cells
213
+ # for cell in @cells.compact
236
214
  unless cell.empty?
237
215
  bound.x = cell.coordinates.x if cell.coordinates.x > bound.x
238
- bound.y = cell.coordinates.y if cell.coordinates.x > bound.y
216
+ bound.y = cell.coordinates.y if cell.coordinates.y > bound.y
239
217
  end
240
218
  end
241
- return bound
219
+ @updated = false
220
+ @bound = bound
221
+ return @bound
242
222
  end
243
223
 
244
224
  end
@@ -280,11 +260,14 @@ module Spreet
280
260
  end
281
261
 
282
262
  def remove(sheet)
283
- @array.delete(sheet)
263
+ @array.delete_at(index(sheet))
284
264
  end
285
265
 
286
266
  def move(sheet, shift=0)
287
- move_at(sheet, index(sheet) + shift)
267
+ position = index(sheet) + shift
268
+ position = 0 if position < 0
269
+ position = self.count-1 if position >= self.count
270
+ move_at(sheet, position)
288
271
  end
289
272
 
290
273
  def move_at(sheet, position=-1)
@@ -310,17 +293,6 @@ module Spreet
310
293
  def initialize(option={})
311
294
  @sheets = Sheets.new(self)
312
295
  end
313
-
314
- def to_term
315
- text = "Spreet (#{@sheets.count}):\n"
316
- for sheet in @sheets
317
- text << " - #{sheet.name}:\n"
318
- for cell in sheet.cells.sort
319
- text << " - #{cell.coordinates.to_s}: #{cell.text.inspect}\n"
320
- end
321
- end
322
- return text
323
- end
324
296
 
325
297
  def write(file, options={})
326
298
  handler = self.class.extract_handler(file, options.delete(:format))