fast_excel 0.1.7 → 0.2.0
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.
- 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
|