hypertabular 0.7.0-x86_64-linux-gnu
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/LICENSE +21 -0
- data/README.md +179 -0
- data/lib/hypertabular/3.4/hypertabular_native.so +0 -0
- data/lib/hypertabular/4.0/hypertabular_native.so +0 -0
- data/lib/hypertabular/batch.rb +204 -0
- data/lib/hypertabular/column.rb +170 -0
- data/lib/hypertabular/delimited_reader.rb +309 -0
- data/lib/hypertabular/dialect.rb +42 -0
- data/lib/hypertabular/runtime/columns.rb +46 -0
- data/lib/hypertabular/runtime/delimited.rb +204 -0
- data/lib/hypertabular/runtime/workbook.rb +299 -0
- data/lib/hypertabular/runtime.rb +142 -0
- data/lib/hypertabular/tabular_error.rb +99 -0
- data/lib/hypertabular/workbook.rb +173 -0
- data/lib/hypertabular.rb +103 -0
- metadata +111 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
module HyperTabular
|
|
2
|
+
# One sheet of a workbook, as Workbook#sheets lists it: its name, and whether the workbook
|
|
3
|
+
# hides it. A hidden sheet reads like any other.
|
|
4
|
+
SheetInfo = Data.define(:name, :hidden)
|
|
5
|
+
|
|
6
|
+
# How a sheet is read: whether its first row is a header (exposed through Sheet#header
|
|
7
|
+
# and never delivered as a row), whether a row with no cells is skipped, and the most rows
|
|
8
|
+
# a batch holds.
|
|
9
|
+
SheetOptions = Data.define(:has_header, :skip_empty_rows, :batch_rows) do
|
|
10
|
+
# Every option has a default; ArgumentError unless +batch_rows+ is a positive Integer.
|
|
11
|
+
def initialize(has_header: true, skip_empty_rows: true, batch_rows: DelimitedReader::DEFAULT_BATCH_ROWS)
|
|
12
|
+
raise ArgumentError, "batch_rows must be a positive Integer; got #{batch_rows.inspect}" unless
|
|
13
|
+
batch_rows.is_a?(Integer) && batch_rows.positive?
|
|
14
|
+
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A header, empty rows skipped, DelimitedReader::DEFAULT_BATCH_ROWS rows a batch.
|
|
20
|
+
SheetOptions::DEFAULT = SheetOptions.new
|
|
21
|
+
|
|
22
|
+
# An XLSX or ODS workbook held in memory, its sheets listed and its shared strings and
|
|
23
|
+
# styles loaded: what a Sheet reads from.
|
|
24
|
+
#
|
|
25
|
+
# book = HyperTabular::Workbook.open("orders.xlsx")
|
|
26
|
+
# book.sheets # => [#<data HyperTabular::SheetInfo name="Orders", hidden=false>]
|
|
27
|
+
# sheet = book.sheet("Orders", HyperTabular::SheetOptions::DEFAULT, plan)
|
|
28
|
+
# while (batch = sheet.read) ... end
|
|
29
|
+
#
|
|
30
|
+
# A workbook that cannot be read — not a zip, encrypted, a part missing or broken — is a
|
|
31
|
+
# TabularError from Workbook.new. Its sheets may be read at once, each with buffers of its
|
|
32
|
+
# own. Not thread-safe.
|
|
33
|
+
class Workbook
|
|
34
|
+
# Reads the file at +path+ into memory and opens it.
|
|
35
|
+
def self.open(path)
|
|
36
|
+
new(File.binread(path))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Opens the workbook in +bytes+, a String, which is read in place: a frozen String is
|
|
40
|
+
# the workbook's as it is, anything else is copied once.
|
|
41
|
+
def initialize(bytes)
|
|
42
|
+
raise ArgumentError, "bytes must be a String; got #{bytes.class}" unless bytes.is_a?(String)
|
|
43
|
+
|
|
44
|
+
bytes = bytes.b.freeze unless bytes.frozen?
|
|
45
|
+
@book = Runtime::Book::Opened.new(bytes)
|
|
46
|
+
@sheets = @book.sheets.map { |name, hidden, _part, _index| SheetInfo.new(name: name, hidden: hidden) }.freeze
|
|
47
|
+
rescue Runtime::Book::StructureError => e
|
|
48
|
+
raise TabularError.from(e.failure)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The workbook's sheets, in its own order, as SheetInfo. Sheets that hold no cells
|
|
52
|
+
# (chart sheets, macro sheets) are not among them.
|
|
53
|
+
attr_reader :sheets
|
|
54
|
+
|
|
55
|
+
# Which kind of workbook this is: :xlsx or :ods.
|
|
56
|
+
def format
|
|
57
|
+
@book.format
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The date system the workbook's serials count in — what a date-formatted number is
|
|
61
|
+
# read by: :y1900 or :y1904, as HyperCast names them.
|
|
62
|
+
def date_system
|
|
63
|
+
HyperCast::EXCEL_EPOCHS.key(@book.epoch)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Starts a read of one sheet — +which+ is its index in #sheets or its name — through
|
|
67
|
+
# +plan+, as +options+ (SheetOptions) says. With a header declared the header row is
|
|
68
|
+
# read here. IndexError for an index the workbook has no sheet at, KeyError for a name
|
|
69
|
+
# it has no sheet by.
|
|
70
|
+
def sheet(which, options, plan)
|
|
71
|
+
index =
|
|
72
|
+
case which
|
|
73
|
+
when Integer
|
|
74
|
+
raise IndexError, "the workbook has #{@sheets.size} sheets, and no sheet #{which}" unless
|
|
75
|
+
which >= 0 && which < @sheets.size
|
|
76
|
+
|
|
77
|
+
which
|
|
78
|
+
when String
|
|
79
|
+
found = @sheets.index { |sheet| sheet.name == which }
|
|
80
|
+
raise KeyError, "the workbook has no sheet named #{which.inspect}" if found.nil?
|
|
81
|
+
|
|
82
|
+
found
|
|
83
|
+
else raise ArgumentError, "which must be an Integer or a String; got #{which.inspect}"
|
|
84
|
+
end
|
|
85
|
+
Sheet.new(@book, @book.sheets[index], options, plan)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The workbook in a line.
|
|
89
|
+
def inspect
|
|
90
|
+
"#<#{self.class.name} #{format} sheets=#{@sheets.size}>"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A forward-only read of one sheet of a Workbook, a batch at a time, through a plan — into
|
|
95
|
+
# the same Batch delimited text is read into.
|
|
96
|
+
#
|
|
97
|
+
# A typed cell is converted directly by its door — a stored 42.0 never passes through text
|
|
98
|
+
# to become an Integer — and a text cell goes through the door as delimited text would. A
|
|
99
|
+
# sheet that is structurally broken raises a TabularError after every intact row before
|
|
100
|
+
# the break has been delivered, and the same error again on every later read.
|
|
101
|
+
class Sheet
|
|
102
|
+
# The SheetOptions the sheet is read with.
|
|
103
|
+
attr_reader :options
|
|
104
|
+
|
|
105
|
+
# The plan: the output Columns, in output order.
|
|
106
|
+
attr_reader :plan
|
|
107
|
+
|
|
108
|
+
# The header row's names — a typed cell said the way the text door says it — as frozen
|
|
109
|
+
# UTF-8 Strings, or nil when the options declare no header.
|
|
110
|
+
attr_reader :header
|
|
111
|
+
|
|
112
|
+
# Made by Workbook#sheet.
|
|
113
|
+
def initialize(book, sheet, options, plan)
|
|
114
|
+
raise ArgumentError, "options must be a HyperTabular::SheetOptions; got #{options.inspect}" unless
|
|
115
|
+
options.is_a?(SheetOptions)
|
|
116
|
+
|
|
117
|
+
@plan = Array(plan).dup.freeze
|
|
118
|
+
@plan.each_with_index do |column, index|
|
|
119
|
+
raise ArgumentError, "plan column #{index} must be a HyperTabular::Column; got #{column.inspect}" unless
|
|
120
|
+
column.is_a?(Column)
|
|
121
|
+
end
|
|
122
|
+
@book = book
|
|
123
|
+
@options = options
|
|
124
|
+
width = (@plan.map(&:ordinal).max || -1) + 1
|
|
125
|
+
@reading = Runtime::Book::Reading.new(book, sheet, options.has_header, options.skip_empty_rows,
|
|
126
|
+
@plan.map(&:packed), @plan.map(&:value_bytes), options.batch_rows, width)
|
|
127
|
+
@header = @reading.header
|
|
128
|
+
@pending = nil
|
|
129
|
+
@failure = nil
|
|
130
|
+
rescue Runtime::Book::StructureError => e
|
|
131
|
+
raise TabularError.from(e.failure)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Reads the next batch: a Batch of up to the options' batch_rows rows, or nil once the
|
|
135
|
+
# sheet has no more.
|
|
136
|
+
def read
|
|
137
|
+
if @pending
|
|
138
|
+
@failure = @pending
|
|
139
|
+
@pending = nil
|
|
140
|
+
end
|
|
141
|
+
raise @failure if @failure
|
|
142
|
+
|
|
143
|
+
rows, cells, arena, failure = @reading.fill
|
|
144
|
+
if failure
|
|
145
|
+
broken = TabularError.from(failure)
|
|
146
|
+
raise @failure = broken if rows.zero?
|
|
147
|
+
|
|
148
|
+
@pending = broken
|
|
149
|
+
end
|
|
150
|
+
return nil if rows.zero?
|
|
151
|
+
|
|
152
|
+
columns = @reading.columns
|
|
153
|
+
values = Array.new(@plan.size) { |column| columns.values(column, rows) }
|
|
154
|
+
Batch.new(@plan, rows, values, Array.new(@plan.size) { |column| columns.verdicts(column, rows) },
|
|
155
|
+
cells, @reading.per_row, @book.strings, 0, arena, workbook: true)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Every remaining batch. Without a block, an Enumerator.
|
|
159
|
+
def each_batch
|
|
160
|
+
return to_enum(:each_batch) unless block_given?
|
|
161
|
+
|
|
162
|
+
while (batch = read)
|
|
163
|
+
yield batch
|
|
164
|
+
end
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The sheet in a line.
|
|
169
|
+
def inspect
|
|
170
|
+
"#<#{self.class.name} columns=#{@plan.size}>"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/hypertabular.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
require "hypercast"
|
|
3
|
+
require_relative "hypertabular/runtime"
|
|
4
|
+
require_relative "hypertabular/runtime/columns"
|
|
5
|
+
require_relative "hypertabular/runtime/delimited"
|
|
6
|
+
require_relative "hypertabular/runtime/workbook"
|
|
7
|
+
|
|
8
|
+
# Tabular parsing with a HyperCast verdict for every cell: delimited text — CSV, TSV, any
|
|
9
|
+
# single-byte ASCII separator — and workbooks — XLSX and ODS — read a batch at a time into
|
|
10
|
+
# typed columns by one Rust core (libhypertabular), called through Fiddle.
|
|
11
|
+
#
|
|
12
|
+
# plan = [HyperTabular::Column.i32(0), HyperTabular::Column.text(1)]
|
|
13
|
+
# reader = HyperTabular::DelimitedReader.new("id,name\n1,alice\nx,bob\n", HyperTabular::Dialect::CSV, plan)
|
|
14
|
+
# reader.header # => ["id", "name"]
|
|
15
|
+
# batch = reader.read # => #<HyperTabular::Batch rows=2 columns=2>
|
|
16
|
+
# batch.values(1) # => ["alice", "bob"]
|
|
17
|
+
# batch.get(0, 1) # => #<data HyperCast::Fault reason=:malformed, offset=0, length=1>
|
|
18
|
+
# batch.raw(0, 1) # => "x"
|
|
19
|
+
# batch.line(1) # => 3
|
|
20
|
+
#
|
|
21
|
+
# book = HyperTabular::Workbook.open("orders.xlsx")
|
|
22
|
+
# sheet = book.sheet("Orders", HyperTabular::SheetOptions::DEFAULT, plan)
|
|
23
|
+
# sheet.read # => the same Batch
|
|
24
|
+
#
|
|
25
|
+
# Nothing is sniffed: the Dialect states the separator, the quoting and the header,
|
|
26
|
+
# SheetOptions a sheet's header and whether empty rows are skipped, and the plan states each
|
|
27
|
+
# Column's door and, for numbers, its notation. HyperCast is the judge:
|
|
28
|
+
# the verdicts (HyperCast::Success, HyperCast::Fault), the notation (HyperCast::NumFormat),
|
|
29
|
+
# the exact decimal (HyperCast::Decimal) and the declared options are that gem's own types,
|
|
30
|
+
# and a cell means exactly what HyperCast's door would say of the same text. A bad value is
|
|
31
|
+
# a verdict; a broken file is a TabularError.
|
|
32
|
+
module HyperTabular
|
|
33
|
+
# This gem's own version — kept in lockstep with hypertabular.gemspec and the core's
|
|
34
|
+
# rust/Cargo.toml.
|
|
35
|
+
VERSION = "0.7.0"
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Whether the native core loaded and exports the ABI this binding was built against.
|
|
39
|
+
# Probed once (a native_version round trip: the cheapest call the core has), cached,
|
|
40
|
+
# and never raises: a missing shared library and an unsupported platform both answer
|
|
41
|
+
# false. A reader keeps its own behavior — building one without the core raises the
|
|
42
|
+
# precise LoadError — this only answers the question quietly.
|
|
43
|
+
def available?
|
|
44
|
+
return @available unless @available.nil?
|
|
45
|
+
|
|
46
|
+
@available = begin
|
|
47
|
+
native_version.is_a?(String)
|
|
48
|
+
rescue LoadError, StandardError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The version of the native core actually loaded, as "major.minor.patch" — read from
|
|
54
|
+
# the library itself, not from this gem, so a consumer can prove the core behind the
|
|
55
|
+
# reader is the one this binding was built against.
|
|
56
|
+
def native_version
|
|
57
|
+
HyperCast::Interop.version(Runtime::Delimited.version)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
require_relative "hypertabular/dialect"
|
|
63
|
+
require_relative "hypertabular/column"
|
|
64
|
+
require_relative "hypertabular/tabular_error"
|
|
65
|
+
require_relative "hypertabular/batch"
|
|
66
|
+
require_relative "hypertabular/delimited_reader"
|
|
67
|
+
require_relative "hypertabular/workbook"
|
|
68
|
+
|
|
69
|
+
# --- backend selection, as hypercast makes its own: the Magnus extension, when present,
|
|
70
|
+
# replaces the Fiddle crossing in place. Every native call and every byte of native memory is
|
|
71
|
+
# behind HyperTabular::Runtime (Runtime::Delimited, Runtime::Book::Opened and ::Reading, and
|
|
72
|
+
# Runtime.unescape), so that is all the extension redefines (rust/src/ruby_ext.rs): the
|
|
73
|
+
# constructors hand back objects of its own that answer the same methods with the same bytes,
|
|
74
|
+
# and everything above them — the readers, the workbook, Batch and every value and verdict it
|
|
75
|
+
# builds — is the same Ruby on either backend. It is how the precompiled platform gems ship,
|
|
76
|
+
# carrying no Fiddle library at all, and how the hypertabular-wasm gem runs in ruby.wasm,
|
|
77
|
+
# where there is no Fiddle to call. The Fiddle definitions stay the universal zero-compile
|
|
78
|
+
# fallback, for a Ruby or a platform no platform gem covers.
|
|
79
|
+
#
|
|
80
|
+
# HYPERTABULAR_PURE forces Fiddle, as HYPERCAST_PURE does for hypercast. It is a testing and
|
|
81
|
+
# diagnostic switch — CI runs the whole suite through it — read for presence, not value.
|
|
82
|
+
# Inside a platform gem there is no library for it to load: BACKEND still reads :fiddle,
|
|
83
|
+
# HyperTabular.available? answers false, and the first reader raises the LoadError naming the
|
|
84
|
+
# missing library.
|
|
85
|
+
HyperTabular::BACKEND =
|
|
86
|
+
if ENV["HYPERTABULAR_PURE"]
|
|
87
|
+
:fiddle
|
|
88
|
+
else
|
|
89
|
+
# A platform gem carries one extension per Ruby ABI under lib/hypertabular/<minor>/, and
|
|
90
|
+
# `rake native:dev` stages its own build there too; CI's in-job staging drops a single
|
|
91
|
+
# one flat at lib/. A miss on both is not an error: it is what the Fiddle backend is for.
|
|
92
|
+
begin
|
|
93
|
+
require "hypertabular/#{RUBY_VERSION[/\d+\.\d+/]}/hypertabular_native"
|
|
94
|
+
:native
|
|
95
|
+
rescue LoadError
|
|
96
|
+
begin
|
|
97
|
+
require "hypertabular_native"
|
|
98
|
+
:native
|
|
99
|
+
rescue LoadError
|
|
100
|
+
:fiddle
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hypertabular
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.7.0
|
|
5
|
+
platform: x86_64-linux-gnu
|
|
6
|
+
authors:
|
|
7
|
+
- Brian Buvinghausen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: hypercast
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.7.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.7.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: yard
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
54
|
+
description: |
|
|
55
|
+
CSV, TSV and any single-byte ASCII separator, and XLSX and ODS workbooks, read by a
|
|
56
|
+
native Rust core into typed column batches. The core owns no memory: this gem allocates the buffers once and the
|
|
57
|
+
core fills them in one native call per batch. Every cell is a HyperCast verdict — the
|
|
58
|
+
value, or a reason plus the offending span — and a file that is not rows of cells at
|
|
59
|
+
all is an exception raised after the intact rows. Two backends behind one surface,
|
|
60
|
+
selected automatically and both shipped prebuilt: a Magnus extension where a precompiled
|
|
61
|
+
platform gem matches, and stdlib Fiddle everywhere else. No compile on install.
|
|
62
|
+
executables: []
|
|
63
|
+
extensions: []
|
|
64
|
+
extra_rdoc_files: []
|
|
65
|
+
files:
|
|
66
|
+
- LICENSE
|
|
67
|
+
- README.md
|
|
68
|
+
- lib/hypertabular.rb
|
|
69
|
+
- lib/hypertabular/3.4/hypertabular_native.so
|
|
70
|
+
- lib/hypertabular/4.0/hypertabular_native.so
|
|
71
|
+
- lib/hypertabular/batch.rb
|
|
72
|
+
- lib/hypertabular/column.rb
|
|
73
|
+
- lib/hypertabular/delimited_reader.rb
|
|
74
|
+
- lib/hypertabular/dialect.rb
|
|
75
|
+
- lib/hypertabular/runtime.rb
|
|
76
|
+
- lib/hypertabular/runtime/columns.rb
|
|
77
|
+
- lib/hypertabular/runtime/delimited.rb
|
|
78
|
+
- lib/hypertabular/runtime/workbook.rb
|
|
79
|
+
- lib/hypertabular/tabular_error.rb
|
|
80
|
+
- lib/hypertabular/workbook.rb
|
|
81
|
+
homepage: https://github.com/SkunkWerkx/HyperTabular
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata:
|
|
85
|
+
source_code_uri: https://github.com/SkunkWerkx/HyperTabular
|
|
86
|
+
bug_tracker_uri: https://github.com/SkunkWerkx/HyperTabular/issues
|
|
87
|
+
changelog_uri: https://github.com/SkunkWerkx/HyperTabular/blob/master/CHANGELOG.md
|
|
88
|
+
documentation_uri: https://github.com/SkunkWerkx/HyperTabular/tree/master/ruby#readme
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '3.4'
|
|
98
|
+
- - "<"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '4.1'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 4.0.20
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: Delimited text and workbooks read a batch at a time into typed columns, a
|
|
110
|
+
HyperCast verdict for every cell
|
|
111
|
+
test_files: []
|