fast_excel 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +10 -4
- data/Gemfile.lock +39 -16
- data/Makefile +1 -1
- data/README.md +60 -12
- data/Rakefile +10 -29
- data/benchmarks/1k_rows.rb +59 -0
- data/benchmarks/20k_rows.rb +26 -0
- data/benchmarks/init.rb +59 -0
- data/benchmarks/memory.rb +49 -0
- data/{example.rb → examples/example.rb} +0 -0
- data/{example_chart.rb → examples/example_chart.rb} +0 -0
- data/{example_image.rb → examples/example_image.rb} +0 -9
- data/fast_excel.gemspec +1 -1
- data/lib/fast_excel.rb +63 -7
- data/lib/fast_excel/binding.rb +663 -7321
- data/lib/fast_excel/binding/chart.rb +2666 -0
- data/lib/fast_excel/binding/format.rb +1177 -0
- data/lib/fast_excel/binding/workbook.rb +338 -0
- data/lib/fast_excel/binding/worksheet.rb +1515 -0
- data/test/default_format_test.rb +19 -0
- data/test/test_helper.rb +3 -3
- data/test/tmpfile_test.rb +23 -0
- metadata +15 -9
- data/.dockerignore +0 -1
- data/BUILD.md +0 -26
- data/build_alpine_linux.docker +0 -16
- data/build_centos_linux.docker +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c5919483e8d5ac1277efefe8399926f14b45d10
|
4
|
+
data.tar.gz: 2a8bd5f6067c8903778f3f7227f90d4c95d31868
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd0f959fe4eb08bd7506f63b4f666c3d47c05765aa8213c775f873264dd561e8aa2c9dd7f61d1ea5f1e6498199248dfef80bb9b5f7693025ec0c3f192671dd27
|
7
|
+
data.tar.gz: 5168323f914545536e1f36ed30aa0f689a98e92a93875abb7137109c20c2adfbd78b905a6cf6339c36e9ae391c23d7461e24a6d15ad92cc28e43ef955b16ce1c
|
data/Gemfile
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
gem 'ffi'
|
4
|
-
gem 'ffi_gen'
|
5
|
-
|
4
|
+
gem 'ffi_gen', require: false
|
5
|
+
|
6
|
+
gem 'roo', '2.7.1', git: 'https://github.com/roo-rb/roo.git'
|
6
7
|
|
7
|
-
gem 'roo'
|
8
8
|
gem 'minitest'
|
9
|
-
gem 'minitest-reporters'
|
9
|
+
gem 'minitest-reporters'
|
10
|
+
|
11
|
+
# For benchmakrs
|
12
|
+
gem 'axlsx', git: 'https://github.com/randym/axlsx.git'
|
13
|
+
gem 'write_xlsx'
|
14
|
+
gem 'benchmark-ips'
|
15
|
+
gem 'process_memory', git: 'https://github.com/paxa/process_memory'
|
data/Gemfile.lock
CHANGED
@@ -1,16 +1,38 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/paxa/process_memory
|
3
|
+
revision: 1aa4df28c9a98903301317236104abe591e086fd
|
4
|
+
specs:
|
5
|
+
process_memory (0.1)
|
6
|
+
|
7
|
+
GIT
|
8
|
+
remote: https://github.com/randym/axlsx.git
|
9
|
+
revision: c8ac844572b25fda358cc01d2104720c4c42f450
|
10
|
+
specs:
|
11
|
+
axlsx (2.1.0.pre)
|
12
|
+
htmlentities (~> 4.3.4)
|
13
|
+
mimemagic (~> 0.3)
|
14
|
+
nokogiri (>= 1.6.6)
|
15
|
+
rubyzip (>= 1.2.1)
|
16
|
+
|
17
|
+
GIT
|
18
|
+
remote: https://github.com/roo-rb/roo.git
|
19
|
+
revision: 6bde8588dfd97d7a18981ee3ac7b444fec165a2c
|
20
|
+
specs:
|
21
|
+
roo (2.7.1)
|
22
|
+
nokogiri (~> 1)
|
23
|
+
rubyzip (>= 1.2.1, < 2.0.0)
|
24
|
+
|
1
25
|
GEM
|
2
26
|
remote: https://rubygems.org/
|
3
27
|
specs:
|
4
28
|
ansi (1.5.0)
|
5
|
-
|
6
|
-
htmlentities (~> 4.3.1)
|
7
|
-
nokogiri (>= 1.4.1)
|
8
|
-
rubyzip (~> 1.0.0)
|
29
|
+
benchmark-ips (2.7.2)
|
9
30
|
builder (3.2.3)
|
10
|
-
ffi (1.9.
|
31
|
+
ffi (1.9.18)
|
11
32
|
ffi_gen (1.2.0)
|
12
33
|
ffi (~> 1.0)
|
13
34
|
htmlentities (4.3.4)
|
35
|
+
mimemagic (0.3.2)
|
14
36
|
mini_portile2 (2.1.0)
|
15
37
|
minitest (5.10.1)
|
16
38
|
minitest-reporters (1.1.14)
|
@@ -18,28 +40,29 @@ GEM
|
|
18
40
|
builder
|
19
41
|
minitest (>= 5.0)
|
20
42
|
ruby-progressbar
|
21
|
-
nokogiri (1.7.
|
43
|
+
nokogiri (1.7.1)
|
22
44
|
mini_portile2 (~> 2.1.0)
|
23
|
-
roo (1.13.2)
|
24
|
-
nokogiri
|
25
|
-
rubyzip
|
26
|
-
spreadsheet (> 0.6.4)
|
27
|
-
ruby-ole (1.2.12.1)
|
28
45
|
ruby-progressbar (1.8.1)
|
29
|
-
rubyzip (1.
|
30
|
-
|
31
|
-
|
46
|
+
rubyzip (1.2.1)
|
47
|
+
write_xlsx (0.83.0)
|
48
|
+
rubyzip (>= 1.0.0)
|
49
|
+
zip-zip
|
50
|
+
zip-zip (0.3)
|
51
|
+
rubyzip (>= 1.0.0)
|
32
52
|
|
33
53
|
PLATFORMS
|
34
54
|
ruby
|
35
55
|
|
36
56
|
DEPENDENCIES
|
37
|
-
axlsx
|
57
|
+
axlsx!
|
58
|
+
benchmark-ips
|
38
59
|
ffi
|
39
60
|
ffi_gen
|
40
61
|
minitest
|
41
62
|
minitest-reporters
|
42
|
-
|
63
|
+
process_memory!
|
64
|
+
roo (= 2.7.1)!
|
65
|
+
write_xlsx
|
43
66
|
|
44
67
|
BUNDLED WITH
|
45
68
|
1.14.5
|
data/Makefile
CHANGED
data/README.md
CHANGED
@@ -22,18 +22,6 @@ worksheet.write_row(0, ["message", "price", "date"], bold)
|
|
22
22
|
|
23
23
|
for i in 1..1000
|
24
24
|
worksheet.write_row(i, ["Hello", (rand * 10_000_000).round(2), Time.now])
|
25
|
-
|
26
|
-
# Or manually
|
27
|
-
# worksheet.write_string(i, 0, "Hello", nil)
|
28
|
-
# worksheet.write_number(i, 1, (rand * 10_000_000).round(2), nil)
|
29
|
-
# date = Libxlsxwriter::Datetime.new
|
30
|
-
# date[:year] = 2017
|
31
|
-
# date[:month] = 2
|
32
|
-
# date[:day] = 24
|
33
|
-
# date[:hour] = i % 24
|
34
|
-
# date[:min] = i % 60
|
35
|
-
# date[:sec] = i % 60
|
36
|
-
# worksheet.write_datetime(i, 2, date, nil)
|
37
25
|
end
|
38
26
|
|
39
27
|
workbook.close
|
@@ -41,3 +29,63 @@ workbook.close
|
|
41
29
|
|
42
30
|
This repositiry and gem contain sources of [libxlsxwriter](https://github.com/jmcnamara/libxlsxwriter)
|
43
31
|
|
32
|
+
## Benchmarks
|
33
|
+
|
34
|
+
1000 rows:
|
35
|
+
```
|
36
|
+
Comparison:
|
37
|
+
FastExcel: 31.7 i/s
|
38
|
+
Axslx: 8.0 i/s - 3.98x slower
|
39
|
+
write_xlsx: 6.9 i/s - 4.62x slower
|
40
|
+
```
|
41
|
+
|
42
|
+
20000 rows:
|
43
|
+
```
|
44
|
+
Comparison:
|
45
|
+
FastExcel: 1.4 i/s
|
46
|
+
Axslx: 0.4 i/s - 3.46x slower
|
47
|
+
write_xlsx: 0.1 i/s - 17.04x slower
|
48
|
+
```
|
49
|
+
|
50
|
+
Max memory usage, generating 100k rows:
|
51
|
+
```
|
52
|
+
FastExcel - 20 MB
|
53
|
+
Axslx - 60 MB
|
54
|
+
write_axslx - 100 MB
|
55
|
+
```
|
56
|
+
|
57
|
+
## Install
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# Gemfile
|
61
|
+
gem 'fast_excel'
|
62
|
+
```
|
63
|
+
Or
|
64
|
+
```
|
65
|
+
gem install fast_excel
|
66
|
+
```
|
67
|
+
|
68
|
+
## API
|
69
|
+
|
70
|
+
This gem is FFI binding for libxlsxwriter C library with some syntax sugar. All original functions is avaliable, for example:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Libxlsxwriter.worksheet_activate(worksheet) # => will call void worksheet_activate(lxw_worksheet *worksheet)
|
74
|
+
# or shorter:
|
75
|
+
worksheet.activate
|
76
|
+
```
|
77
|
+
|
78
|
+
Full libxlsxwriter documentation: [http://libxlsxwriter.github.io/](http://libxlsxwriter.github.io/)
|
79
|
+
|
80
|
+
Helper Methods:
|
81
|
+
|
82
|
+
* `FastExcel.open(filename = nil, constant_memory: false, default_format: {})` - open new workbook, if `filename` is nil - it will craete tmp file, `default_format` will be called with `workbook.default_format.set(...)`
|
83
|
+
* `FastExcel.date_num(time, offset = nil)` - generate Excel's internal date value, number of days since 1900-Jan-01, works faster then creating `Libxlsxwriter::Datetime` struct. `offset` argument is number hours from UTC, e.g. `3.5`
|
84
|
+
* `FastExcel.print_ffi_obj(object)` - print FFI object fields, just for debugging
|
85
|
+
* `workbook.bold_cell_format` - shortcut for creating bold format
|
86
|
+
* `workbook.number_format(num_format)` - create number or date format, for money usually: `"#,##0.00"`, for date: `"[$-409]m/d/yy h:mm AM/PM;@"`
|
87
|
+
* `workbook.read_string` - close workbook, read file to string, delete file (only if tmp file)
|
88
|
+
* `workbook.remove_tmp_file` - delete tmp file (only if tmp file)
|
89
|
+
* `worksheet.write_row(row_num, array_of_mixed_value, formats = nil)` - write values one by one, detecting type automatically. `formats` can be array, or Format object or nil
|
90
|
+
* `format.font_family` - alias for `format.font_name`
|
91
|
+
* `workbook.default_format.font_size` - set it to 0 if you want to use default font size (that what user set in Excel settings)
|
data/Rakefile
CHANGED
@@ -1,31 +1,4 @@
|
|
1
|
-
|
2
|
-
desc "Build mac binary"
|
3
|
-
task :mac do
|
4
|
-
Dir.chdir("./libxlsxwriter") do
|
5
|
-
system("make clean && make")
|
6
|
-
system('cp lib/libxlsxwriter.dylib ../binaries/libxlsxwriter-darwin.dylib')
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
desc "Build linux (libc) binary"
|
11
|
-
task :linux do
|
12
|
-
system("docker build . -f build_centos_linux.docker -t fast_excel_centos")
|
13
|
-
system("docker run -t fast_excel_centos readelf -d lib/libxlsxwriter.so")
|
14
|
-
last_container_id = `docker ps -a | grep fast_excel_centos | head -1 | awk '{print $1;}'`.strip
|
15
|
-
system("docker cp #{last_container_id}:/srv/libxlsxwriter/lib/libxlsxwriter.so ./binaries/libxlsxwriter-glibc.so")
|
16
|
-
system("docker rm #{last_container_id}")
|
17
|
-
end
|
18
|
-
|
19
|
-
desc "Build linux (musl) binary"
|
20
|
-
task :linux_musl do
|
21
|
-
system("docker build . -f build_alpine_linux.docker -t fast_excel_alpine")
|
22
|
-
system("docker run -t fast_excel_alpine readelf -d lib/libxlsxwriter.so")
|
23
|
-
last_container_id = `docker ps -a | grep fast_excel_alpine | head -1 | awk '{print $1;}'`.strip
|
24
|
-
system("docker cp #{last_container_id}:/srv/libxlsxwriter/lib/libxlsxwriter.so ./binaries/libxlsxwriter-alpine.so")
|
25
|
-
system("docker rm #{last_container_id}")
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
1
|
+
desc "Sync github.com:Paxa/libxlsxwriter to ./libxlsxwriter"
|
29
2
|
task :sync do
|
30
3
|
require 'fileutils'
|
31
4
|
FileUtils.rm_rf("./libxlsxwriter")
|
@@ -34,4 +7,12 @@ task :sync do
|
|
34
7
|
system("git show --pretty='format:%cd %h' --date=iso --quiet > version.txt")
|
35
8
|
FileUtils.rm_rf("./.git")
|
36
9
|
end
|
37
|
-
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rake/testtask'
|
13
|
+
|
14
|
+
Rake::TestTask.new do |test|
|
15
|
+
test.test_files = Dir.glob('test/**/*_test.rb')
|
16
|
+
end
|
17
|
+
|
18
|
+
#task :default => :test
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'init'
|
2
|
+
|
3
|
+
HEADERS = ["id", "name", "age", "date"]
|
4
|
+
|
5
|
+
DATA = []
|
6
|
+
1000.times do |n|
|
7
|
+
DATA << [n, "String string #{n}", (n * rand * 10).round, Time.at(n * 1000 + 1492922688)]
|
8
|
+
end
|
9
|
+
|
10
|
+
Benchmark.ips do |x|
|
11
|
+
x.config(time: 10, warmup: 2)
|
12
|
+
|
13
|
+
x.report("FastExcel") do
|
14
|
+
workbook = FastExcel.open(constant_memory: true)
|
15
|
+
worksheet = workbook.add_worksheet("benchmark")
|
16
|
+
|
17
|
+
worksheet.write_row(0, HEADERS)
|
18
|
+
DATA.each_with_index do |row, i|
|
19
|
+
worksheet.write_row(i + 1, row)
|
20
|
+
end
|
21
|
+
workbook.read_string
|
22
|
+
end
|
23
|
+
|
24
|
+
x.report("Axslx") do
|
25
|
+
filename = "#{Dir.mktmpdir}/axslx.xslx"
|
26
|
+
Axlsx::Package.new do |package|
|
27
|
+
package.use_autowidth = false
|
28
|
+
package.workbook.add_worksheet do |sheet|
|
29
|
+
sheet.add_row(HEADERS)
|
30
|
+
DATA.each do |row|
|
31
|
+
sheet.add_row(row)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
package.serialize(filename)
|
35
|
+
File.open(filename, 'rb', &:read)
|
36
|
+
File.delete(filename)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
x.report("write_xlsx") do
|
41
|
+
filename = "#{Dir.mktmpdir}/write_xlsx.xslx"
|
42
|
+
workbook = WriteXLSX.new(filename)
|
43
|
+
worksheet = workbook.add_worksheet
|
44
|
+
HEADERS.each_with_index do |value, i|
|
45
|
+
worksheet.write(0, i, value)
|
46
|
+
end
|
47
|
+
DATA.each_with_index do |row, row_num|
|
48
|
+
worksheet.write_number(row_num + 1, 0, row[0])
|
49
|
+
worksheet.write_string(row_num + 1, 1, row[1])
|
50
|
+
worksheet.write_number(row_num + 1, 2, row[2])
|
51
|
+
worksheet.write_number(row_num + 1, 3, row[3])
|
52
|
+
end
|
53
|
+
workbook.close
|
54
|
+
File.open(filename, 'rb', &:read)
|
55
|
+
File.delete(filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
x.compare!
|
59
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'init'
|
2
|
+
|
3
|
+
HEADERS = ["id", "name", "age", "date", "random"]
|
4
|
+
|
5
|
+
DATA = []
|
6
|
+
20_000.times do |n|
|
7
|
+
DATA << [n, "String string #{n}" * 5, (n * rand * 10).round, Time.at(n * 1000 + 1492922688), n * 100]
|
8
|
+
end
|
9
|
+
|
10
|
+
Benchmark.ips do |x|
|
11
|
+
x.config(time: 10, warmup: 2)
|
12
|
+
|
13
|
+
x.report("FastExcel") do
|
14
|
+
write_fast_excel_20k
|
15
|
+
end
|
16
|
+
|
17
|
+
x.report("Axslx") do
|
18
|
+
write_axslx_20k
|
19
|
+
end
|
20
|
+
|
21
|
+
x.report("write_xlsx") do
|
22
|
+
write_xlsx_20k
|
23
|
+
end
|
24
|
+
|
25
|
+
x.compare!
|
26
|
+
end
|
data/benchmarks/init.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tmpdir'
|
4
|
+
require_relative '../lib/fast_excel'
|
5
|
+
|
6
|
+
# gem install axlsx benchmark-ips write_xlsx
|
7
|
+
|
8
|
+
require "benchmark/ips"
|
9
|
+
require 'axlsx'
|
10
|
+
require 'write_xlsx'
|
11
|
+
require 'process_memory'
|
12
|
+
|
13
|
+
require_relative 'init'
|
14
|
+
|
15
|
+
def write_fast_excel_20k
|
16
|
+
workbook = FastExcel.open(constant_memory: true)
|
17
|
+
worksheet = workbook.add_worksheet("benchmark")
|
18
|
+
|
19
|
+
worksheet.write_row(0, HEADERS)
|
20
|
+
DATA.each_with_index do |row, i|
|
21
|
+
worksheet.write_row(i + 1, row)
|
22
|
+
end
|
23
|
+
workbook.read_string
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_xlsx_20k
|
27
|
+
filename = "#{Dir.mktmpdir}/write_xlsx.xslx"
|
28
|
+
workbook = WriteXLSX.new(filename)
|
29
|
+
worksheet = workbook.add_worksheet
|
30
|
+
HEADERS.each_with_index do |value, i|
|
31
|
+
worksheet.write(0, i, value)
|
32
|
+
end
|
33
|
+
DATA.each_with_index do |row, row_num|
|
34
|
+
worksheet.write_number(row_num + 1, 0, row[0])
|
35
|
+
worksheet.write_string(row_num + 1, 1, row[1])
|
36
|
+
worksheet.write_number(row_num + 1, 2, row[2])
|
37
|
+
worksheet.write_number(row_num + 1, 3, row[3])
|
38
|
+
worksheet.write_number(row_num + 1, 4, row[4])
|
39
|
+
end
|
40
|
+
workbook.close
|
41
|
+
File.open(filename, 'rb', &:read)
|
42
|
+
File.delete(filename)
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_axslx_20k
|
46
|
+
filename = "#{Dir.mktmpdir}/axslx.xslx"
|
47
|
+
Axlsx::Package.new do |package|
|
48
|
+
package.use_autowidth = false
|
49
|
+
package.workbook.add_worksheet do |sheet|
|
50
|
+
sheet.add_row(HEADERS)
|
51
|
+
DATA.each do |row|
|
52
|
+
sheet.add_row(row)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
package.serialize(filename)
|
56
|
+
File.open(filename, 'rb', &:read)
|
57
|
+
File.delete(filename)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative 'init'
|
2
|
+
|
3
|
+
HEADERS = ["id", "name", "age", "date", "random"]
|
4
|
+
|
5
|
+
DATA = []
|
6
|
+
10_000.times do |n|
|
7
|
+
DATA << [n, "String string #{n}" * 5, (n * rand * 10).round, Time.at(n * 1000 + 1492922688), n * 100]
|
8
|
+
end
|
9
|
+
|
10
|
+
puts "warm up..."
|
11
|
+
write_fast_excel_20k
|
12
|
+
write_axslx_20k
|
13
|
+
write_xlsx_20k
|
14
|
+
|
15
|
+
DATA.clear
|
16
|
+
100_000.times do |n|
|
17
|
+
DATA << [n, "String string #{n}" * 5, (n * rand * 10).round, Time.at(n * 1000 + 1492922688), n * 100]
|
18
|
+
end
|
19
|
+
|
20
|
+
GC.start
|
21
|
+
sleep 5
|
22
|
+
|
23
|
+
def measure_memory(title)
|
24
|
+
puts "Running test: #{title}"
|
25
|
+
recorder = ProcessMemory.start_recording
|
26
|
+
yield
|
27
|
+
puts recorder.print("Done!")
|
28
|
+
recorder.stop
|
29
|
+
puts recorder.report_per_second_pretty
|
30
|
+
puts
|
31
|
+
end
|
32
|
+
|
33
|
+
measure_memory("FastExcel") do
|
34
|
+
write_fast_excel_20k
|
35
|
+
end
|
36
|
+
|
37
|
+
GC.start
|
38
|
+
sleep 5
|
39
|
+
|
40
|
+
measure_memory("Axslx") do
|
41
|
+
write_axslx_20k
|
42
|
+
end
|
43
|
+
|
44
|
+
GC.start
|
45
|
+
sleep 5
|
46
|
+
|
47
|
+
measure_memory("write_axslx") do
|
48
|
+
write_axslx_20k
|
49
|
+
end
|