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.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastXlsx
4
+ VERSION = "0.1.0"
5
+ end
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: []