fast_xlsx 0.1.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 +7 -0
- data/CHANGELOG.md +34 -0
- data/Cargo.lock +517 -0
- data/Cargo.toml +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +298 -0
- data/Rakefile +22 -0
- data/bench/Gemfile +14 -0
- data/bench/Gemfile.lock +110 -0
- data/bench/compare.rb +113 -0
- data/bench/memory.rb +43 -0
- data/bench/write.rb +53 -0
- data/examples/showcase.rb +146 -0
- data/ext/fast_xlsx/Cargo.toml +18 -0
- data/ext/fast_xlsx/build.rs +5 -0
- data/ext/fast_xlsx/extconf.rb +6 -0
- data/ext/fast_xlsx/src/lib.rs +1339 -0
- data/lib/fast_xlsx/version.rb +5 -0
- data/lib/fast_xlsx.rb +205 -0
- data/sig/fast_xlsx.rbs +100 -0
- metadata +80 -0
data/lib/fast_xlsx.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fast_xlsx/version"
|
|
4
|
+
|
|
5
|
+
# Fast .xlsx writer backed by rust_xlsxwriter.
|
|
6
|
+
module FastXlsx
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Cell value written as an Excel formula, e.g. Formula.new("SUM(A1:A9)").
|
|
10
|
+
Formula = Data.define(:expression) do
|
|
11
|
+
def initialize(expression:)
|
|
12
|
+
super(expression: expression.to_s)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Cell value written as a hyperlink, e.g. URL.new("https://example.com").
|
|
17
|
+
# text: shown in the cell instead of the URL itself.
|
|
18
|
+
# Subclassed (not a Data.define block) so .new can call super, which lets it
|
|
19
|
+
# accept URL.new(url, text: ...) as well as the usual Data forms.
|
|
20
|
+
class URL < Data.define(:url, :text) # rubocop:disable Style/DataInheritance
|
|
21
|
+
def self.new(*args, **kwargs)
|
|
22
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..2)" if args.size > 2
|
|
23
|
+
|
|
24
|
+
kwargs[:url] = args[0] unless args.empty?
|
|
25
|
+
kwargs[:text] = args[1] if args.size > 1
|
|
26
|
+
super(**kwargs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Also reached by URL[...] and #with, so values are normalized here.
|
|
30
|
+
def initialize(url:, text: nil)
|
|
31
|
+
super(url: url.to_s, text: text&.to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Text with a format per segment, e.g. RichString.new(["Total: ", bold], "1,234").
|
|
36
|
+
# Each segment is a String (default font) or [String, Format].
|
|
37
|
+
class RichString
|
|
38
|
+
attr_reader :segments
|
|
39
|
+
|
|
40
|
+
def initialize(*parts)
|
|
41
|
+
raise ArgumentError, "RichString needs at least one segment" if parts.empty?
|
|
42
|
+
|
|
43
|
+
@segments = parts.map { |part| part.is_a?(Array) ? [part[0].to_s, part[1]] : [part.to_s, nil] }.freeze
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The native extension looks up FastXlsx::Error, so load it after Error is defined.
|
|
48
|
+
require "fast_xlsx/fast_xlsx"
|
|
49
|
+
|
|
50
|
+
# Owns the worksheets; serialize with #to_xlsx or #save.
|
|
51
|
+
class Workbook
|
|
52
|
+
MEMORY_MODES = %i[standard constant low].freeze
|
|
53
|
+
|
|
54
|
+
# memory: :standard keeps every cell in memory until saving. :constant and
|
|
55
|
+
# :low write each finished row to disk, so each worksheet must be filled
|
|
56
|
+
# top to bottom. :constant stores strings inline (memory stays flat); :low
|
|
57
|
+
# keeps Excel's shared string table (memory grows with the number of
|
|
58
|
+
# unique strings, output is standard).
|
|
59
|
+
def self.new(memory: :standard)
|
|
60
|
+
unless MEMORY_MODES.include?(memory)
|
|
61
|
+
raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
_new(memory == :constant, memory == :low)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_worksheet(name = nil)
|
|
68
|
+
_add_worksheet(name).tap { |ws| worksheets << ws }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The same Worksheet objects add_worksheet returned, so their append
|
|
72
|
+
# position is shared.
|
|
73
|
+
def worksheets
|
|
74
|
+
@worksheets ||= []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def worksheet(name)
|
|
78
|
+
worksheets.find { |ws| ws.name == name }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Document properties shown in Excel's File > Info: title:, subject:,
|
|
82
|
+
# author:, manager:, company:, category:, keywords:, comments:, status:.
|
|
83
|
+
# Later calls add to earlier ones.
|
|
84
|
+
def properties(**fields)
|
|
85
|
+
merged = (@properties || {}).merge(fields)
|
|
86
|
+
_properties(merged) # validates before anything is remembered
|
|
87
|
+
@properties = merged
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Cell writer for one sheet; create with Workbook#add_worksheet.
|
|
93
|
+
class Worksheet
|
|
94
|
+
def write(row, col, value, format = nil)
|
|
95
|
+
_write(row, col, value, format)
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def append(values, format: nil)
|
|
100
|
+
_append(values, format)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# columns: a 0-based column index or a Range of them. width is in characters.
|
|
104
|
+
def column_width(columns, width)
|
|
105
|
+
bounds = column_bounds(columns)
|
|
106
|
+
_column_width(*bounds, width)
|
|
107
|
+
@fixed_widths ||= {}
|
|
108
|
+
@fixed_widths.delete(bounds) # re-insert so autofit replays calls in order
|
|
109
|
+
@fixed_widths[bounds] = width
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Sizes columns to the data written so far. Widths set with
|
|
114
|
+
# column_width are kept.
|
|
115
|
+
def autofit
|
|
116
|
+
_autofit
|
|
117
|
+
@fixed_widths&.each { |bounds, width| _column_width(*bounds, width) }
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Merges the range and writes value (any cell type) into its first cell.
|
|
122
|
+
def merge_range(first_row, first_col, last_row, last_col, value, format = nil)
|
|
123
|
+
_merge_range(first_row, first_col, last_row, last_col, value, format)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Highlights cells in the range by rule. type: :cell, :text, :formula,
|
|
127
|
+
# :data_bar or :color_scale; see the README for each type's options.
|
|
128
|
+
def conditional_format(first_row, first_col, last_row, last_col, type:, **)
|
|
129
|
+
_conditional_format(first_row, first_col, last_row, last_col, { type: type, ** })
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Restricts what can be entered in the range. type: :list, :whole_number,
|
|
133
|
+
# :decimal or :text_length; see the README for the options.
|
|
134
|
+
def data_validation(first_row, first_col, last_row, last_col, type:, **)
|
|
135
|
+
_data_validation(first_row, first_col, last_row, last_col, { type: type, ** })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Adds a comment (Excel "note") to a cell.
|
|
139
|
+
def write_comment(row, col, text, author: nil)
|
|
140
|
+
_write_comment(row, col, text, author)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
|
|
144
|
+
# cell. source is a file path or an IO (anything responding to #read).
|
|
145
|
+
# Options: scale: or width:/height: (pixels), x_offset:, y_offset: (pixels), alt_text:.
|
|
146
|
+
def insert_image(row, col, source, **)
|
|
147
|
+
bytes = source.respond_to?(:read) ? source.read : File.binread(source)
|
|
148
|
+
_insert_image(row, col, bytes, { ** })
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Inserts a chart with its top-left corner in the cell. series is an Array
|
|
152
|
+
# of { values:, categories:, name: } with Excel ranges such as
|
|
153
|
+
# "Sheet1!$B$2:$B$13". Options: title:, x_axis:, y_axis:, width:, height:.
|
|
154
|
+
def insert_chart(row, col, type:, series:, **)
|
|
155
|
+
_insert_chart(row, col, { type: type, series: series, ** })
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Turns the range (header row included, total row too when total_row: true)
|
|
159
|
+
# into an Excel table. columns: header Strings or { header:, total:,
|
|
160
|
+
# total_label:, format: }; other options: style:, name:, total_row:,
|
|
161
|
+
# banded_rows:, autofilter:.
|
|
162
|
+
def add_table(first_row, first_col, last_row, last_col, **)
|
|
163
|
+
_add_table(first_row, first_col, last_row, last_col, { ** })
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Printed page header/footer using Excel codes such as "&CPage &P of &N".
|
|
167
|
+
# margin: is in inches.
|
|
168
|
+
def page_header(text, margin: nil)
|
|
169
|
+
_page_header(text)
|
|
170
|
+
margin ? margins(header: margin) : self
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def page_footer(text, margin: nil)
|
|
174
|
+
_page_footer(text)
|
|
175
|
+
margin ? margins(footer: margin) : self
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Print margins in inches; margins not given keep their current value.
|
|
179
|
+
def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
180
|
+
_margins(*[left, right, top, bottom, header, footer].map { |m| m || -1.0 })
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Default format for cells in these columns that are written without one.
|
|
185
|
+
def column_format(columns, format)
|
|
186
|
+
_column_format(*column_bounds(columns), format)
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def column_bounds(columns)
|
|
193
|
+
columns.is_a?(Integer) ? [columns, columns] : columns.minmax
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Cell style, e.g. Format.new(bold: true). Pass to Worksheet#write.
|
|
198
|
+
class Format
|
|
199
|
+
def self.new(**options)
|
|
200
|
+
# Apply border: first so border_left: etc. override it whatever the order.
|
|
201
|
+
options = { border: options[:border], **options.except(:border) } if options.key?(:border)
|
|
202
|
+
_new(options)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/sig/fast_xlsx.rbs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module FastXlsx
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
interface _Reader
|
|
8
|
+
def read: () -> String
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
type cell = Numeric | String | Time | Date | Formula | URL | RichString | bool | nil | _ToS
|
|
12
|
+
|
|
13
|
+
class RichString
|
|
14
|
+
attr_reader segments: Array[[String, Format?]]
|
|
15
|
+
def initialize: (*(_ToS | [_ToS, Format?]) parts) -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Formula
|
|
19
|
+
attr_reader expression: String
|
|
20
|
+
def self.new: (_ToS expression) -> Formula
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class URL
|
|
24
|
+
attr_reader url: String
|
|
25
|
+
attr_reader text: String?
|
|
26
|
+
def self.new: (_ToS url, ?text: _ToS?) -> URL
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Workbook
|
|
30
|
+
MEMORY_MODES: Array[Symbol]
|
|
31
|
+
def self.new: (?memory: :standard | :constant | :low) -> Workbook
|
|
32
|
+
def add_worksheet: (?String? name) -> Worksheet
|
|
33
|
+
def worksheets: () -> Array[Worksheet]
|
|
34
|
+
def worksheet: (String name) -> Worksheet?
|
|
35
|
+
def properties: (?title: String, ?subject: String, ?author: String, ?manager: String,
|
|
36
|
+
?company: String, ?category: String, ?keywords: String,
|
|
37
|
+
?comments: String, ?status: String) -> self
|
|
38
|
+
def to_xlsx: () -> String
|
|
39
|
+
def save: (String path) -> void
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Format
|
|
43
|
+
type color = String | Integer
|
|
44
|
+
type border = :thin | :medium | :thick | :dashed | :dotted | :double | :hair
|
|
45
|
+
|
|
46
|
+
type underline = bool | :single | :double | :single_accounting | :double_accounting
|
|
47
|
+
|
|
48
|
+
def self.new: (?bold: bool, ?italic: bool, ?underline: underline, ?strikeout: bool, ?text_wrap: bool,
|
|
49
|
+
?shrink: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
|
|
50
|
+
?font_color: color, ?bg_color: color, ?font_script: :superscript | :subscript,
|
|
51
|
+
?align: :left | :center | :right, ?valign: :top | :center | :bottom,
|
|
52
|
+
?rotation: Integer, ?indent: Integer,
|
|
53
|
+
?border: border, ?border_left: border, ?border_right: border,
|
|
54
|
+
?border_top: border, ?border_bottom: border, ?border_color: color) -> Format
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Worksheet
|
|
58
|
+
def write: (Integer row, Integer col, cell value, ?Format? format) -> self
|
|
59
|
+
def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
|
|
60
|
+
def <<: (Array[cell] row) -> self
|
|
61
|
+
def concat: (Array[Array[cell]] rows) -> self
|
|
62
|
+
def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
|
|
63
|
+
def column_format: (Integer | Range[Integer] columns, Format format) -> self
|
|
64
|
+
def autofit: () -> self
|
|
65
|
+
def autofilter: (Integer first_row, Integer first_col, Integer last_row, Integer last_col) -> self
|
|
66
|
+
def freeze_panes: (Integer row, Integer col) -> self
|
|
67
|
+
def row_height: (Integer row, Numeric height) -> self
|
|
68
|
+
def page_breaks: (Array[Integer] rows) -> self
|
|
69
|
+
def vertical_page_breaks: (Array[Integer] cols) -> self
|
|
70
|
+
def page_header: (String text, ?margin: Numeric?) -> self
|
|
71
|
+
def page_footer: (String text, ?margin: Numeric?) -> self
|
|
72
|
+
def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
|
|
73
|
+
?header: Numeric?, ?footer: Numeric?) -> self
|
|
74
|
+
def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
|
|
75
|
+
def conditional_format: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
|
|
76
|
+
type: :cell | :text | :formula | :data_bar | :color_scale,
|
|
77
|
+
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
78
|
+
?format: Format, ?colors: 2 | 3) -> self
|
|
79
|
+
def data_validation: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
|
|
80
|
+
type: :list | :whole_number | :decimal | :text_length,
|
|
81
|
+
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
82
|
+
?input_title: String, ?input_message: String,
|
|
83
|
+
?error_title: String, ?error_message: String) -> self
|
|
84
|
+
def write_comment: (Integer row, Integer col, String text, ?author: String?) -> self
|
|
85
|
+
def insert_image: (Integer row, Integer col, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
|
|
86
|
+
?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
|
|
87
|
+
type chart_series = { values: String, ?categories: String, ?name: String }
|
|
88
|
+
|
|
89
|
+
def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
|
|
90
|
+
?title: String, ?x_axis: String, ?y_axis: String,
|
|
91
|
+
?width: Integer, ?height: Integer) -> self
|
|
92
|
+
type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
|
|
93
|
+
|
|
94
|
+
def add_table: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
|
|
95
|
+
?columns: Array[table_column], ?style: Symbol, ?name: String,
|
|
96
|
+
?total_row: bool, ?banded_rows: bool, ?autofilter: bool) -> self
|
|
97
|
+
def name: () -> String
|
|
98
|
+
def next_row: () -> Integer
|
|
99
|
+
end
|
|
100
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fast_xlsx
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zac
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.130
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.130
|
|
26
|
+
description: Writes Excel .xlsx files from Ruby through a native Rust extension (rust_xlsxwriter
|
|
27
|
+
+ magnus).
|
|
28
|
+
email:
|
|
29
|
+
- 579103+7a6163@users.noreply.github.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions:
|
|
32
|
+
- ext/fast_xlsx/extconf.rb
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- Cargo.lock
|
|
37
|
+
- Cargo.toml
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- Rakefile
|
|
41
|
+
- bench/Gemfile
|
|
42
|
+
- bench/Gemfile.lock
|
|
43
|
+
- bench/compare.rb
|
|
44
|
+
- bench/memory.rb
|
|
45
|
+
- bench/write.rb
|
|
46
|
+
- examples/showcase.rb
|
|
47
|
+
- ext/fast_xlsx/Cargo.toml
|
|
48
|
+
- ext/fast_xlsx/build.rs
|
|
49
|
+
- ext/fast_xlsx/extconf.rb
|
|
50
|
+
- ext/fast_xlsx/src/lib.rs
|
|
51
|
+
- lib/fast_xlsx.rb
|
|
52
|
+
- lib/fast_xlsx/version.rb
|
|
53
|
+
- sig/fast_xlsx.rbs
|
|
54
|
+
homepage: https://github.com/7a6163/fast_xlsx
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
github_repo: ssh://github.com/7a6163/fast_xlsx
|
|
59
|
+
homepage_uri: https://github.com/7a6163/fast_xlsx
|
|
60
|
+
source_code_uri: https://github.com/7a6163/fast_xlsx
|
|
61
|
+
changelog_uri: https://github.com/7a6163/fast_xlsx/blob/main/CHANGELOG.md
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.3.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 4.0.10
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Fast xlsx writer for Ruby, powered by rust_xlsxwriter
|
|
80
|
+
test_files: []
|