spreet 0.0.1
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.
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/lib/spreet.rb +368 -0
- data/lib/spreet/handlers.rb +25 -0
- data/lib/spreet/handlers/csv.rb +68 -0
- data/lib/spreet/handlers/open_document.rb +76 -0
- data/spreet.gemspec +40 -0
- data/test/helper.rb +17 -0
- data/test/samples/pascal.csv +17 -0
- data/test/test_spreet.rb +91 -0
- metadata +95 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Brice Texier
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
5
|
+
test.libs << 'lib' << 'test'
|
6
|
+
test.pattern = 'test/**/test_*.rb'
|
7
|
+
test.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
# Import all rake files
|
13
|
+
for rakefile in Dir.glob('lib/tasks/*.rake')
|
14
|
+
import(rakefile)
|
15
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/spreet.rb
ADDED
@@ -0,0 +1,368 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Spreet
|
4
|
+
|
5
|
+
module VERSION
|
6
|
+
version = nil
|
7
|
+
File.open("VERSION") {|f| version = f.read.split('.')}
|
8
|
+
MAJOR = version[0].to_i.freeze
|
9
|
+
MINOR = version[1].to_i.freeze
|
10
|
+
TINY = version[2].to_i.freeze
|
11
|
+
PATCH = TINY.freeze
|
12
|
+
PRE = version[3].freeze
|
13
|
+
STRING = version.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class Coordinates
|
18
|
+
# Limit coordinates x and y in 0..65535 but coordinates are in one integer of 32 bits
|
19
|
+
CPU_SEMI_WIDTH = 16 # ((RUBY_PLATFORM.match(/^[^\-]*[^\-0-9]64/) ? 64 : 32) / 2).freeze
|
20
|
+
Y_FILTER = ((1 << CPU_SEMI_WIDTH) - 1).freeze
|
21
|
+
|
22
|
+
BASE_26_BEF = "0123456789abcdefghijklmnop"
|
23
|
+
BASE_26_AFT = "abcdefghijklmnopqrstuvwxyz"
|
24
|
+
|
25
|
+
attr_accessor :x, :y
|
26
|
+
def initialize(*args)
|
27
|
+
value = (args.size == 1 ? args[0] : args)
|
28
|
+
@x, @y = 0, 0
|
29
|
+
if value.is_a? String
|
30
|
+
if value.downcase.match(/^[a-z]+[0-9]+$/)
|
31
|
+
value = value.downcase.split(/([A-Z]+|[0-9]+)/).delete_if{|x| x.size.zero?}
|
32
|
+
@x, @y = value[0].tr(BASE_26_AFT, BASE_26_BEF).to_i(26), value[1].to_i(10)-1
|
33
|
+
elsif value.downcase.match(/^[0-9]+[^0-9]+[0-9]+$/)
|
34
|
+
value = value.downcase.split(/[^0-9]+/)
|
35
|
+
@x, @y = value[0].to_i(10), value[1].to_i(10)
|
36
|
+
end
|
37
|
+
elsif value.is_a? Integer
|
38
|
+
@x, @y = (value >> CPU_SEMI_WIDTH), value & Y_FILTER
|
39
|
+
elsif value.is_a? Coordinates
|
40
|
+
@x, @y = value.x, value.y
|
41
|
+
elsif value.is_a? Array
|
42
|
+
@x, @y = value[0].to_i, value[1].to_i
|
43
|
+
elsif value.is_a? Hash
|
44
|
+
@x, @y = value[:x] || value[:column] || 0, value[:y] || value[:row] || 0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
@x.to_s(26).tr(BASE_26_BEF, BASE_26_AFT).upcase+(@y+1).to_s(10)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_a
|
53
|
+
[@x, @y]
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_hash
|
57
|
+
{:x=>@x, :y=>@y}
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_i
|
61
|
+
(@x << CPU_SEMI_WIDTH) + @y
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other_coordinate)
|
65
|
+
other_coordinate.x == self.x and other_coordinate.y == self.y
|
66
|
+
end
|
67
|
+
|
68
|
+
def <=>(other_coordinate)
|
69
|
+
self.to_i <=> other_coordinate.to_i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Represents a cell in a sheet
|
74
|
+
class Cell
|
75
|
+
attr_reader :text, :value, :type, :sheet, :coordinates
|
76
|
+
|
77
|
+
def initialize(sheet, *args)
|
78
|
+
@sheet = sheet
|
79
|
+
@coordinates = Coordinates.new(*args)
|
80
|
+
self.value = nil
|
81
|
+
@empty = true
|
82
|
+
end
|
83
|
+
|
84
|
+
def value=(val)
|
85
|
+
@value = val
|
86
|
+
@type = determine_type
|
87
|
+
@text = val.to_s
|
88
|
+
@empty = false
|
89
|
+
end
|
90
|
+
|
91
|
+
def empty?
|
92
|
+
@empty
|
93
|
+
end
|
94
|
+
|
95
|
+
def clear!
|
96
|
+
self.value = nil
|
97
|
+
@empty = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove!
|
101
|
+
@sheet.remove(self.coordinates)
|
102
|
+
end
|
103
|
+
|
104
|
+
def <=>(other_cell)
|
105
|
+
self.coordinates <=> other_cell.coordinates
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def determine_type
|
111
|
+
if value.is_a? Date
|
112
|
+
:date
|
113
|
+
elsif value.is_a? Integer
|
114
|
+
:integer
|
115
|
+
elsif value.is_a? Numeric
|
116
|
+
:decimal
|
117
|
+
elsif value.is_a? DateTime
|
118
|
+
:datetime
|
119
|
+
elsif value.is_a?(TrueClass) or value.is_a?(FalseClass)
|
120
|
+
:boolean
|
121
|
+
elsif value.nil?
|
122
|
+
:null
|
123
|
+
else
|
124
|
+
:string
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
class Sheet
|
132
|
+
attr_reader :document, :name, :columns
|
133
|
+
attr_accessor :current_row
|
134
|
+
|
135
|
+
def initialize(document, name=nil)
|
136
|
+
@document = document
|
137
|
+
self.name = name
|
138
|
+
raise ArgumentError.new("Must be a Document") unless document.is_a? Document
|
139
|
+
@current_row = 0
|
140
|
+
@cells = {}
|
141
|
+
end
|
142
|
+
|
143
|
+
def name=(value)
|
144
|
+
unless value
|
145
|
+
value = (@document.sheets.count > 0 ? @document.sheets[-1].name.succ : "Sheet 1")
|
146
|
+
end
|
147
|
+
raise ArgumentError.new("Name of sheet must be given") if value.to_s.strip.size.zero?
|
148
|
+
if @document.sheets[value]
|
149
|
+
raise ArgumentError.new("Name of sheet must be unique")
|
150
|
+
end
|
151
|
+
@name = value
|
152
|
+
end
|
153
|
+
|
154
|
+
def cells
|
155
|
+
@cells.delete_if{|k,v| v.empty?}
|
156
|
+
@cells.values
|
157
|
+
end
|
158
|
+
|
159
|
+
def next_row(increment = 1)
|
160
|
+
@current_row += increment
|
161
|
+
end
|
162
|
+
|
163
|
+
def previous_row(increment = 1)
|
164
|
+
@current_row -= increment
|
165
|
+
end
|
166
|
+
|
167
|
+
def [](*args)
|
168
|
+
coord = Coordinates.new(*args)
|
169
|
+
@cells[coord.to_i] ||= Cell.new(self, coord)
|
170
|
+
return @cells[coord.to_i]
|
171
|
+
end
|
172
|
+
|
173
|
+
def []=(*args)
|
174
|
+
value = args.delete_at(-1)
|
175
|
+
cell = self[*args]
|
176
|
+
cell.value = value
|
177
|
+
@bound = compute_bound
|
178
|
+
end
|
179
|
+
|
180
|
+
def row(*args)
|
181
|
+
options = {}
|
182
|
+
options = args.delete_at(-1) if args[-1].is_a? Hash
|
183
|
+
row = options[:row] || @current_row
|
184
|
+
args.each_index do |index|
|
185
|
+
self[index, row] = args[index]
|
186
|
+
end
|
187
|
+
next_row
|
188
|
+
end
|
189
|
+
|
190
|
+
def each_row(&block)
|
191
|
+
for j in 0..bound.y
|
192
|
+
row = []
|
193
|
+
for i in 0..bound.x
|
194
|
+
row[i] = self[i, j]
|
195
|
+
end
|
196
|
+
yield row
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Find or build cell
|
201
|
+
def cell(*args)
|
202
|
+
return c
|
203
|
+
end
|
204
|
+
|
205
|
+
def bound
|
206
|
+
@bound
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove!(coordinates)
|
210
|
+
raise ArgumentError.new("Must be a Coordinates") unless document.is_a?(Coordinates)
|
211
|
+
@cells.delete(coordinates.to_i)
|
212
|
+
@bound = compute_bound
|
213
|
+
end
|
214
|
+
|
215
|
+
# Moves the sheet to an other position in the list of sheets
|
216
|
+
def move_to(position)
|
217
|
+
@document.sheets.move_at(self, position)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Moves the sheet higher in the list of sheets
|
221
|
+
def move_higher(increment=1)
|
222
|
+
@document.sheets.move(self, increment)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Moves the sheet lower in the list of sheets
|
226
|
+
def move_lower(increment=1)
|
227
|
+
@document.sheets.move(self, -increment)
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def compute_bound
|
233
|
+
bound = Coordinates.new
|
234
|
+
for id, cell in @cells
|
235
|
+
unless cell.empty?
|
236
|
+
bound.x = cell.coordinates.x if cell.coordinates.x > bound.x
|
237
|
+
bound.y = cell.coordinates.y if cell.coordinates.x > bound.y
|
238
|
+
end
|
239
|
+
end
|
240
|
+
return bound
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
class Sheets
|
247
|
+
|
248
|
+
def initialize(document)
|
249
|
+
raise ArgumentError.new("Must be a Document") unless document.is_a?(Document)
|
250
|
+
@document = document
|
251
|
+
@array = []
|
252
|
+
end
|
253
|
+
|
254
|
+
def count
|
255
|
+
@array.size
|
256
|
+
end
|
257
|
+
|
258
|
+
def index(name_or_sheet)
|
259
|
+
if name_or_sheet.is_a? String
|
260
|
+
@array.each_index do |i|
|
261
|
+
return i if @array[i].name == name_or_sheet
|
262
|
+
end
|
263
|
+
elsif name_or_sheet.is_a? Integer
|
264
|
+
return (@array[name_or_sheet].nil? ? nil : name_or_sheet)
|
265
|
+
else
|
266
|
+
return @array.index(name_or_sheet)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def add(name=nil, position=-1)
|
271
|
+
sheet = Sheet.new(@document, name)
|
272
|
+
@array.insert(position, sheet)
|
273
|
+
return sheet
|
274
|
+
end
|
275
|
+
|
276
|
+
def [](sheet)
|
277
|
+
sheet = index(sheet)
|
278
|
+
return (sheet.is_a?(Integer) ? @array[sheet] : nil)
|
279
|
+
end
|
280
|
+
|
281
|
+
def remove(sheet)
|
282
|
+
@array.delete(sheet)
|
283
|
+
end
|
284
|
+
|
285
|
+
def move(sheet, shift=0)
|
286
|
+
move_at(sheet, index(sheet) + shift)
|
287
|
+
end
|
288
|
+
|
289
|
+
def move_at(sheet, position=-1)
|
290
|
+
if i = index(sheet)
|
291
|
+
@array.insert(position, @array.delete_at(i))
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def each(&block)
|
296
|
+
for item in @array
|
297
|
+
yield item
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
|
304
|
+
class Document
|
305
|
+
attr_reader :sheets
|
306
|
+
@@handlers = {}
|
307
|
+
@@associations = {}
|
308
|
+
|
309
|
+
def initialize(option={})
|
310
|
+
@sheets = Sheets.new(self)
|
311
|
+
end
|
312
|
+
|
313
|
+
def to_term
|
314
|
+
text = "Spreet (#{@sheets.count}):\n"
|
315
|
+
for sheet in @sheets
|
316
|
+
text << " - #{sheet.name}:\n"
|
317
|
+
for cell in sheet.cells.sort
|
318
|
+
text << " - #{cell.coordinates.to_s}: #{cell.text.inspect}\n"
|
319
|
+
end
|
320
|
+
end
|
321
|
+
return text
|
322
|
+
end
|
323
|
+
|
324
|
+
def write(file, options={})
|
325
|
+
handler = self.class.extract_handler(file, options.delete(:format))
|
326
|
+
handler.write(self, file, options)
|
327
|
+
end
|
328
|
+
|
329
|
+
class << self
|
330
|
+
|
331
|
+
def register_handler(klass, name, options={})
|
332
|
+
if klass.respond_to?(:read) or klass.respond_to?(:write)
|
333
|
+
if name.is_a?(Symbol)
|
334
|
+
@@handlers[name] = klass # options.merge(:class=>klass)
|
335
|
+
elsif
|
336
|
+
raise ArgumentError.new("Name is invalid. Symbol expected, #{name.class.name} got.")
|
337
|
+
end
|
338
|
+
else
|
339
|
+
raise ArgumentError.new("Handler do not support :read or :write method.")
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def read(file, options={})
|
344
|
+
handler = extract_handler(file, options.delete(:format))
|
345
|
+
return handler.read(file, options)
|
346
|
+
end
|
347
|
+
|
348
|
+
def extract_handler(file, handler_name=nil)
|
349
|
+
file_path = Pathname.new(file)
|
350
|
+
extension = file_path.extname.to_s[1..-1]
|
351
|
+
if !handler_name and extension.size > 0
|
352
|
+
handler_name = extension.to_sym
|
353
|
+
end
|
354
|
+
if @@handlers[handler_name]
|
355
|
+
return @@handlers[handler_name]
|
356
|
+
else
|
357
|
+
raise ArgumentError.new("No corresponding handler (#{handler_name.inspect}). Available: #{@@handlers.keys.collect{|k| k.inspect}.join(', ')}.")
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
|
368
|
+
require 'spreet/handlers'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Spreet
|
2
|
+
|
3
|
+
# Default handler
|
4
|
+
class Handler
|
5
|
+
|
6
|
+
def self.read(file, options={})
|
7
|
+
raise NotImplementedError.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.write(spreet, file, options={})
|
11
|
+
raise NotImplementedError.new
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'spreet/handlers/csv'
|
19
|
+
require 'spreet/handlers/open_document'
|
20
|
+
|
21
|
+
Spreet::Document.register_handler Spreet::Handlers::CSV, :csv
|
22
|
+
Spreet::Document.register_handler Spreet::Handlers::ExcelCSV, :xcsv
|
23
|
+
# Spreet::Document.register_handler Spreet::Handlers::HTML, :html
|
24
|
+
Spreet::Document.register_handler Spreet::Handlers::OpenDocument, :ods
|
25
|
+
# Spreet::Document.register_handler Spreet::Handlers::PDF, :pdf
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'csv'
|
3
|
+
require 'iconv'
|
4
|
+
|
5
|
+
module Spreet
|
6
|
+
# Universal CSV support
|
7
|
+
CSV = (::CSV.const_defined?(:Reader) ? ::FasterCSV : ::CSV).freeze
|
8
|
+
|
9
|
+
module Handlers
|
10
|
+
|
11
|
+
class CSV < Spreet::Handler
|
12
|
+
|
13
|
+
# Read a CSV file and create its Spreet document
|
14
|
+
def self.read(file, options={})
|
15
|
+
spreet = Spreet::Document.new
|
16
|
+
sheet = spreet.sheets.add
|
17
|
+
Spreet::CSV.foreach(file) do |row|
|
18
|
+
sheet.row *row
|
19
|
+
end
|
20
|
+
return spreet
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Write a Spreet to a CSV file
|
25
|
+
def self.write(spreet, file, options={})
|
26
|
+
sheet = spreet.sheets[options[:sheet]||0]
|
27
|
+
Spreet::CSV.open(file, "wb") do |csv|
|
28
|
+
sheet.each_row do |row|
|
29
|
+
csv << row.collect{|c| c.text}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class ExcelCSV < Spreet::Handler
|
37
|
+
|
38
|
+
# Read a CSV file and create its Spreet document
|
39
|
+
def self.read(file, options={})
|
40
|
+
spreet = Spreet::Document.new
|
41
|
+
sheet = spreet.sheets.add
|
42
|
+
options = {:col_sep=>';'}.merge(options)
|
43
|
+
ic = Iconv.new('utf-8', 'cp1252')
|
44
|
+
Spreet::CSV.foreach(file, options) do |row|
|
45
|
+
sheet.row *(row.collect{|v| ic.iconv(v.to_s)})
|
46
|
+
end
|
47
|
+
return spreet
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Write a Spreet to a CSV file
|
52
|
+
def self.write(spreet, file, options={})
|
53
|
+
sheet = spreet.sheets[options[:sheet]||0]
|
54
|
+
options = {:col_sep=>';'}.merge(options)
|
55
|
+
ic = Iconv.new('cp1252', 'utf-8')
|
56
|
+
Spreet::CSV.open(file, "wb", options) do |csv|
|
57
|
+
sheet.each_row do |row|
|
58
|
+
csv << row.collect{|c| ic.iconv(c.text)}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'zip/zip'
|
3
|
+
|
4
|
+
module Spreet
|
5
|
+
module Handlers
|
6
|
+
class OpenDocument < Spreet::Handler
|
7
|
+
DATE_REGEXP = /\%./
|
8
|
+
DATE_ELEMENTS = {
|
9
|
+
"m" => "<number:month number:style=\"long\"/>",
|
10
|
+
"d" => "<number:day number:style=\"long\"/>",
|
11
|
+
"Y" => "<number:year/>"
|
12
|
+
}
|
13
|
+
|
14
|
+
|
15
|
+
def self.mimetype
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.xmlec(string)
|
19
|
+
zs = string.to_s.gsub('&', '&').gsub('\'', ''').gsub('<', '<').gsub('>', '>')
|
20
|
+
zs.force_encoding('US-ASCII') if zs.respond_to?(:force_encoding)
|
21
|
+
return zs
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def self.write(spreet, file, options={})
|
26
|
+
xml_escape = "to_s.gsub('&', '&').gsub('\\'', ''').gsub('<', '<').gsub('>', '>')"
|
27
|
+
xml_escape << ".force_encoding('US-ASCII')" if xml_escape.respond_to?(:force_encoding)
|
28
|
+
mimetype = "application/vnd.oasis.opendocument.spreadsheet"
|
29
|
+
# name = #{table.model.name}.model_name.human.gsub(/[^a-z0-9]/i,'_')
|
30
|
+
Zip::ZipOutputStream.open(file) do |zile|
|
31
|
+
# MimeType in first place
|
32
|
+
zile.put_next_entry('mimetype', nil, nil, Zip::ZipEntry::STORED)
|
33
|
+
zile << mimetype
|
34
|
+
|
35
|
+
# Manifest
|
36
|
+
zile.put_next_entry('META-INF/manifest.xml')
|
37
|
+
zile << ("<?xml version=\"1.0\" encoding=\"UTF-8\"?><manifest:manifest xmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\"><manifest:file-entry manifest:media-type=\"#{mimetype}\" manifest:full-path=\"/\"/><manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"content.xml\"/></manifest:manifest>")
|
38
|
+
zile.put_next_entry('content.xml')
|
39
|
+
|
40
|
+
zile << ("<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:document-content xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\" xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\" xmlns:draw=\"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\" xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\" xmlns:presentation=\"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0\" xmlns:svg=\"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0\" xmlns:chart=\"urn:oasis:names:tc:opendocument:xmlns:chart:1.0\" xmlns:dr3d=\"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0\" xmlns:math=\"http://www.w3.org/1998/Math/MathML\" xmlns:form=\"urn:oasis:names:tc:opendocument:xmlns:form:1.0\" xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\" xmlns:ooo=\"http://openoffice.org/2004/office\" xmlns:ooow=\"http://openoffice.org/2004/writer\" xmlns:oooc=\"http://openoffice.org/2004/calc\" xmlns:dom=\"http://www.w3.org/2001/xml-events\" xmlns:xforms=\"http://www.w3.org/2002/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:field=\"urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:field:1.0\" office:version=\"1.1\"><office:scripts/>")
|
41
|
+
# Styles
|
42
|
+
default_date_format = '%d/%m%Y' # ::I18n.translate("date.formats.default")
|
43
|
+
zile << ("<office:automatic-styles><style:style style:name=\"co1\" style:family=\"table-column\"><style:table-column-properties fo:break-before=\"auto\" style:use-optimal-column-width=\"true\"/></style:style><style:style style:name=\"header\" style:family=\"table-cell\"><style:text-properties fo:font-weight=\"bold\" style:font-weight-asian=\"bold\" style:font-weight-complex=\"bold\"/></style:style><number:date-style style:name=\"K4D\" number:automatic-order=\"true\"><number:text>"+default_date_format.gsub(DATE_REGEXP){|x| "</number:text>"+DATE_ELEMENTS[x[1..1]]+"<number:text>"} +"</number:text></number:date-style><style:style style:name=\"ce1\" style:family=\"table-cell\" style:data-style-name=\"K4D\"/></office:automatic-styles>")
|
44
|
+
|
45
|
+
zile << ("<office:body><office:spreadsheet>")
|
46
|
+
# Tables
|
47
|
+
for sheet in spreet.sheets
|
48
|
+
zile << ("<table:table table:name=\"#{xmlec(sheet.name)}\">")
|
49
|
+
zile << ("<table:table-column table:number-columns-repeated=\"#{sheet.bound.x+1}\"/>")
|
50
|
+
# zile << ("<table:table-header-rows><table:table-row>"+columns_headers(table).collect{|h| "<table:table-cell table:style-name=\"header\" office:value-type=\"string\"><text:p>'+(#{h}).#{xml_escape}+'</text:p></table:table-cell>"}.join+"</table:table-row></table:table-header-rows>")
|
51
|
+
sheet.each_row do |row| # #{record} in #{table.records_variable_name}\n"
|
52
|
+
zile << "<table:table-row>"
|
53
|
+
for cell in row
|
54
|
+
zile << "<table:table-cell"+(if cell.type == :decimal
|
55
|
+
" office:value-type=\"float\" office:value=\"#{xmlec(cell.value)}\""
|
56
|
+
elsif cell.type == :boolean
|
57
|
+
" office:value-type=\"boolean\" office:boolean-value=\"#{xmlec(cell.value ? 'true' : 'false')}\""
|
58
|
+
elsif cell.type == :date
|
59
|
+
" office:value-type=\"date\" table:style-name=\"ce1\" office:date-value=\"#{xmlec(cell.value)}\""
|
60
|
+
else
|
61
|
+
" office:value-type=\"string\""
|
62
|
+
end)+"><text:p>"+xmlec(cell.text)+"</text:p></table:table-cell>"
|
63
|
+
end
|
64
|
+
zile << "</table:table-row>"
|
65
|
+
end
|
66
|
+
zile << ("</table:table>")
|
67
|
+
end
|
68
|
+
zile << ("</office:spreadsheet></office:body></office:document-content>")
|
69
|
+
end
|
70
|
+
# Zile is finished
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/spreet.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = "spreet"
|
4
|
+
File.open("VERSION", "rb") do |f|
|
5
|
+
s.version = f.read
|
6
|
+
end
|
7
|
+
|
8
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
9
|
+
s.authors = ["Brice Texier"]
|
10
|
+
s.date = "2011-09-19"
|
11
|
+
s.summary = "Spr[eadsh]eet handler"
|
12
|
+
s.description = "Spr[eadsh]eet handler for CSV(RW), Excel CSV(RW) and ODS(W). The goal is to read and write in many open formats."
|
13
|
+
s.email = "brice.texier@ekylibre.org"
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE.txt",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.homepage = "http://github.com/burisu/spreet"
|
20
|
+
s.licenses = ["MIT"]
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
if s.respond_to? :specification_version then
|
24
|
+
s.specification_version = 3
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency("fastercsv", [">= 0"])
|
27
|
+
s.add_runtime_dependency("libxml-ruby", [">= 0"])
|
28
|
+
s.add_runtime_dependency("rubyzip", [">= 0.9.4"])
|
29
|
+
else
|
30
|
+
s.add_dependency("fastercsv", [">= 0"])
|
31
|
+
s.add_dependency("libxml-ruby", [">= 0"])
|
32
|
+
s.add_dependency("rubyzip", [">= 0.9.4"])
|
33
|
+
end
|
34
|
+
else
|
35
|
+
s.add_dependency("fastercsv", [">= 0"])
|
36
|
+
s.add_dependency("libxml-ruby", [">= 0"])
|
37
|
+
s.add_dependency("rubyzip", [">= 0.9.4"])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
require 'spreet'
|
15
|
+
|
16
|
+
class Test::Unit::TestCase
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
2
|
+
1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
3
|
+
1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
4
|
+
1,3,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0
|
5
|
+
1,4,6,4,1,0,0,0,0,0,0,0,0,0,0,0,0
|
6
|
+
1,5,10,10,5,1,0,0,0,0,0,0,0,0,0,0,0
|
7
|
+
1,6,15,20,15,6,1,0,0,0,0,0,0,0,0,0,0
|
8
|
+
1,7,21,35,35,21,7,1,0,0,0,0,0,0,0,0,0
|
9
|
+
1,8,28,56,70,56,28,8,1,0,0,0,0,0,0,0,0
|
10
|
+
1,9,36,84,126,126,84,36,9,1,0,0,0,0,0,0,0
|
11
|
+
1,10,45,120,210,252,210,120,45,10,1,0,0,0,0,0,0
|
12
|
+
1,11,55,165,330,462,462,330,165,55,11,1,0,0,0,0,0
|
13
|
+
1,12,66,220,495,792,924,792,495,220,66,12,1,0,0,0,0
|
14
|
+
1,13,78,286,715,1287,1716,1716,1287,715,286,78,13,1,0,0,0
|
15
|
+
1,14,91,364,1001,2002,3003,3432,3003,2002,1001,364,91,14,1,0,0
|
16
|
+
1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1,0
|
17
|
+
1,16,120,560,1820,4368,8008,11440,12870,11440,8008,4368,1820,560,120,16,1
|
data/test/test_spreet.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class TestSpreet < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_coordinates
|
7
|
+
assert_equal Spreet::Coordinates.new(0,0), Spreet::Coordinates.new("A1")
|
8
|
+
assert_equal Spreet::Coordinates.new(0,0), Spreet::Coordinates.new("0-0")
|
9
|
+
assert_equal Spreet::Coordinates.new(1,1), Spreet::Coordinates.new("B2")
|
10
|
+
assert_equal Spreet::Coordinates.new(2,2), Spreet::Coordinates.new(:x=>2, :y=>2)
|
11
|
+
assert_equal Spreet::Coordinates.new(3,3), Spreet::Coordinates.new(3,3)
|
12
|
+
assert_equal Spreet::Coordinates.new(3,3), Spreet::Coordinates.new([3,3])
|
13
|
+
assert_equal Spreet::Coordinates.new(4,4), Spreet::Coordinates.new(Spreet::Coordinates.new(4,4).to_i)
|
14
|
+
assert_equal Spreet::Coordinates.new(5,5), Spreet::Coordinates.new(Spreet::Coordinates.new(5,5))
|
15
|
+
assert_equal("D25", Spreet::Coordinates.new(3,24).to_s)
|
16
|
+
assert_equal([3, 24], Spreet::Coordinates.new(3,24).to_a)
|
17
|
+
assert_equal({:x=>3, :y=>24}, Spreet::Coordinates.new(3,24).to_hash)
|
18
|
+
assert Spreet::Coordinates.new(0,0) <=> Spreet::Coordinates.new(0,1)
|
19
|
+
assert Spreet::Coordinates.new(0,1) <=> Spreet::Coordinates.new(1,0)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_spreet_version
|
23
|
+
assert_not_nil Spreet::VERSION
|
24
|
+
assert_not_nil Spreet::VERSION::MAJOR
|
25
|
+
assert_not_nil Spreet::VERSION::MINOR
|
26
|
+
assert_not_nil Spreet::VERSION::TINY
|
27
|
+
assert_not_nil Spreet::VERSION::PATCH
|
28
|
+
assert_equal(Spreet::VERSION::TINY, Spreet::VERSION::PATCH, "PATCH code must have the same value as TINY")
|
29
|
+
assert((Spreet::VERSION::MAJOR > 0 or Spreet::VERSION::MINOR > 0 or Spreet::VERSION::TINY > 0), "Version cannot be 0.0.0")
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_spreet
|
33
|
+
assert_not_nil Spreet
|
34
|
+
|
35
|
+
spreet = Spreet::Document.new
|
36
|
+
assert_not_nil spreet
|
37
|
+
sheet = spreet.sheets.add
|
38
|
+
assert_not_nil sheet
|
39
|
+
assert_not_nil spreet.sheets.add("Feuille 2")
|
40
|
+
assert_not_nil spreet.sheets.add("ソフト 3")
|
41
|
+
|
42
|
+
assert_equal Spreet::Sheet, spreet.sheets[1].class
|
43
|
+
assert_equal "Feuille 2", spreet.sheets[1].name
|
44
|
+
|
45
|
+
assert_equal Spreet::Sheet, spreet.sheets["ソフト 3"].class
|
46
|
+
assert_equal "ソフト 3", spreet.sheets["ソフト 3"].name
|
47
|
+
|
48
|
+
assert_not_nil sheet[0,0]
|
49
|
+
sheet[0,0] = "Cell A1"
|
50
|
+
spreet.sheets[1][0] = "Cellule A1"
|
51
|
+
spreet.sheets["ソフト 3"]["A1"] = "セル A1"
|
52
|
+
|
53
|
+
assert_not_nil sheet["C30"]
|
54
|
+
|
55
|
+
sheet["F20"] = Date.today
|
56
|
+
|
57
|
+
spreet.write("test/samples/cleaned-nothing.ods")
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def test_handlers
|
62
|
+
doc = nil
|
63
|
+
assert_nothing_raised do
|
64
|
+
doc = Spreet::Document.read("test/samples/pascal.csv")
|
65
|
+
end
|
66
|
+
|
67
|
+
sheet = doc.sheets[0]
|
68
|
+
sheet.each_row do |row|
|
69
|
+
for cell in row
|
70
|
+
if cell.text.to_i == 0
|
71
|
+
cell.clear!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
doc.write("test/samples/cleaned-pascal.csv", :format=>:xcsv)
|
77
|
+
doc.write("test/samples/cleaned-pascal.ods")
|
78
|
+
|
79
|
+
assert_nothing_raised do
|
80
|
+
doc = Spreet::Document.read("test/samples/cleaned-pascal.csv", :format=>:xcsv)
|
81
|
+
end
|
82
|
+
|
83
|
+
FileUtils.rm_f("test/samples/cleaned-pascal.csv")
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spreet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brice Texier
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-19 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: fastercsv
|
16
|
+
requirement: &11182000 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *11182000
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: libxml-ruby
|
27
|
+
requirement: &11180960 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *11180960
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rubyzip
|
38
|
+
requirement: &11180240 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.9.4
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *11180240
|
47
|
+
description: Spr[eadsh]eet handler for CSV(RW), Excel CSV(RW) and ODS(W). The goal
|
48
|
+
is to read and write in many open formats.
|
49
|
+
email: brice.texier@ekylibre.org
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files:
|
53
|
+
- LICENSE.txt
|
54
|
+
- README.rdoc
|
55
|
+
files:
|
56
|
+
- .gitignore
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE.txt
|
59
|
+
- README.rdoc
|
60
|
+
- Rakefile
|
61
|
+
- VERSION
|
62
|
+
- lib/spreet.rb
|
63
|
+
- lib/spreet/handlers.rb
|
64
|
+
- lib/spreet/handlers/csv.rb
|
65
|
+
- lib/spreet/handlers/open_document.rb
|
66
|
+
- spreet.gemspec
|
67
|
+
- test/helper.rb
|
68
|
+
- test/samples/pascal.csv
|
69
|
+
- test/test_spreet.rb
|
70
|
+
homepage: http://github.com/burisu/spreet
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 1.8.11
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: Spr[eadsh]eet handler
|
95
|
+
test_files: []
|