carray 3.0.0 → 3.0.2
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/.yardopts +2 -1
- data/CHANGELOG.md +845 -3
- data/{NEWS.md → CHANGELOG.v1.md} +1 -1
- data/README.md +33 -33
- data/carray.gemspec +12 -8
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +256 -114
- data/ext/ca_binop_dispatch.c +5 -6
- data/ext/ca_binop_dispatch.h +0 -7
- data/ext/ca_categorical_iterator.c +121 -67
- data/ext/ca_for_each_element.h +6 -8
- data/ext/ca_group_iter.c +1 -1
- data/ext/ca_kernel_iterator.c +386 -74
- data/ext/ca_kernel_iterator.h +145 -45
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_bincmp.c +57 -27
- data/ext/ca_obj_binop.c +86 -30
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +88 -27
- data/ext/ca_obj_face.c +104 -0
- data/ext/ca_obj_face.h +48 -1
- data/ext/ca_obj_fake.c +11 -0
- data/ext/ca_obj_fixlen_string.c +21 -6
- data/ext/ca_obj_grid.c +11 -3
- data/ext/ca_obj_meld.c +125 -22
- data/ext/ca_obj_moncmp.c +33 -14
- data/ext/ca_obj_monop.c +19 -7
- data/ext/ca_obj_object.c +107 -4
- data/ext/ca_obj_record.c +3 -1
- data/ext/ca_obj_refer.c +30 -6
- data/ext/ca_obj_roll.c +13 -5
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_select_axis.c +16 -6
- data/ext/ca_obj_shift.c +3 -3
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +178 -24
- data/ext/ca_obj_string.c +11 -5
- data/ext/ca_obj_tile.c +13 -5
- data/ext/ca_obj_time.c +3 -1
- data/ext/ca_obj_timedelta.c +3 -1
- data/ext/ca_obj_transpose.c +1 -1
- data/ext/ca_obj_triop.c +62 -23
- data/ext/ca_obj_window.c +94 -19
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +1 -5
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +5 -5
- data/ext/ca_sweep_engine.c +362 -155
- data/ext/ca_sweep_engine.h +39 -9
- data/ext/ca_transform_common.c +7 -0
- data/ext/carray.h +148 -132
- data/ext/carray_access.c +83 -49
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_bincount.c +8 -8
- data/ext/carray_broadcast.c +102 -9
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +2010 -335
- data/ext/carray_call_cfunc.h +153 -0
- data/ext/carray_cast.c +184 -72
- data/ext/carray_conversion.c +22 -22
- data/ext/carray_copy.c +67 -32
- data/ext/carray_core.c +140 -13
- data/ext/carray_count.c +10 -11
- data/ext/carray_element.c +7 -7
- data/ext/carray_factorize.c +72 -51
- data/ext/carray_hold.c +1 -1
- data/ext/carray_index_classifier.c +7 -21
- data/ext/carray_internal.h +36 -3
- data/ext/carray_kernels_bincmp.c +1 -0
- data/ext/carray_kernels_binop.c +320 -298
- data/ext/carray_kernels_init.c +1095 -0
- data/ext/carray_kernels_map.c +1 -0
- data/ext/carray_kernels_moncmp.c +1 -0
- data/ext/carray_kernels_monop.c +412 -411
- data/ext/carray_kernels_reduce_aggregate.c +256 -87
- data/ext/carray_kernels_reduce_boolean.c +1 -0
- data/ext/carray_kernels_reduce_cumulative.c +324 -54
- data/ext/carray_kernels_reduce_extreme.c +605 -58
- data/ext/carray_kernels_reduce_variance.c +1 -0
- data/ext/carray_kernels_scan.c +5 -4
- data/ext/carray_kernels_search.c +187 -106
- data/ext/carray_kernels_sort.c +1 -0
- data/ext/carray_kernels_triop.c +1 -0
- data/ext/carray_lazy.c +175 -5
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +29 -13
- data/ext/carray_median_percentile.c +56 -1
- data/ext/carray_memory_view.c +46 -21
- data/ext/carray_operator.c +50 -53
- data/ext/carray_order.c +1 -1
- data/ext/carray_partition.c +4 -4
- data/ext/carray_random.c +391 -47
- data/ext/carray_scatter.c +1 -1
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +22 -24
- data/ext/carray_sort_kernel.c +10 -10
- data/ext/carray_test.c +1 -1
- data/ext/extconf.rb +18 -0
- data/ext/mk_call_cfunc.rb +323 -145
- data/ext/mkkernel.rb +651 -129
- data/ext/ruby_carray.c +28 -17
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +14 -1
- data/lib/carray/axis_group.rb +84 -7
- data/lib/carray/basics.rb +63 -59
- data/lib/carray/bincount_nd.rb +12 -12
- data/lib/carray/block_iterator.rb +111 -25
- data/lib/carray/categorical.rb +154 -37
- data/lib/carray/categorical_iterator.rb +300 -151
- data/lib/carray/conditional.rb +14 -14
- data/lib/carray/const_string.rb +131 -27
- data/lib/carray/construct.rb +61 -0
- data/lib/carray/core_extensions.rb +62 -5
- data/lib/carray/data_type_extension.rb +21 -1
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +3 -3
- data/lib/carray/frame/convert.rb +1 -1
- data/lib/carray/frame/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +85 -21
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +224 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/sort.rb +1 -1
- data/lib/carray/frame/verbs.rb +32 -12
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +16 -16
- data/lib/carray/inspect.rb +43 -16
- data/lib/carray/iterator.rb +147 -3
- data/lib/carray/lazy.rb +88 -73
- data/lib/carray/mask_gap_fill.rb +3 -1
- data/lib/carray/meld_reduce.rb +2 -2
- data/lib/carray/methods/align_addr.rb +1 -1
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/is_in.rb +25 -14
- data/lib/carray/methods/locate_addr.rb +81 -3
- data/lib/carray/methods/mask_duplicates.rb +36 -2
- data/lib/carray/methods/meshgrid.rb +4 -5
- data/lib/carray/methods/mode.rb +2 -2
- data/lib/carray/methods/nunique.rb +23 -2
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/snap.rb +7 -2
- data/lib/carray/methods/unique.rb +44 -4
- data/lib/carray/methods/value_counts.rb +2 -2
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +0 -19
- data/lib/carray/slab_iterator.rb +77 -19
- data/lib/carray/string_operation_extension.rb +10 -6
- data/lib/carray/time.rb +906 -491
- data/lib/carray/window_iterator.rb +421 -27
- data/lib/carray.rb +7 -5
- data/yard-stubs/ca_obj_array.rb +385 -0
- data/yard-stubs/ca_obj_bitarray.rb +38 -0
- data/yard-stubs/ca_obj_bitfield.rb +43 -0
- data/yard-stubs/ca_obj_block.rb +68 -0
- data/yard-stubs/ca_obj_byte_swap.rb +56 -0
- data/yard-stubs/ca_obj_fake.rb +31 -0
- data/yard-stubs/ca_obj_farray.rb +32 -0
- data/yard-stubs/ca_obj_field.rb +45 -0
- data/yard-stubs/ca_obj_grid.rb +35 -0
- data/yard-stubs/ca_obj_refer.rb +72 -0
- data/yard-stubs/ca_obj_roll.rb +45 -0
- data/yard-stubs/ca_obj_shift.rb +43 -0
- data/yard-stubs/ca_obj_stride.rb +181 -0
- data/yard-stubs/ca_obj_tile.rb +29 -0
- data/yard-stubs/ca_obj_transpose.rb +40 -0
- data/yard-stubs/ca_obj_window.rb +57 -0
- data/yard-stubs/carray_access.rb +131 -0
- data/yard-stubs/carray_attribute.rb +246 -0
- data/yard-stubs/carray_broadcast.rb +37 -0
- data/yard-stubs/carray_cast.rb +489 -0
- data/yard-stubs/carray_class.rb +65 -0
- data/yard-stubs/carray_conversion.rb +76 -0
- data/yard-stubs/carray_copy.rb +79 -0
- data/yard-stubs/carray_core.rb +34 -0
- data/yard-stubs/carray_count.rb +84 -0
- data/yard-stubs/carray_element.rb +108 -0
- data/yard-stubs/carray_generate.rb +66 -0
- data/yard-stubs/carray_lazy.rb +228 -0
- data/yard-stubs/carray_loop.rb +140 -0
- data/yard-stubs/carray_mask.rb +259 -0
- data/yard-stubs/carray_math.rb +1615 -0
- data/yard-stubs/carray_mathfunc.rb +45 -0
- data/yard-stubs/carray_median_percentile.rb +103 -0
- data/yard-stubs/carray_memory_view.rb +163 -0
- data/yard-stubs/carray_order.rb +252 -0
- data/yard-stubs/carray_random.rb +89 -0
- data/yard-stubs/carray_scatter.rb +106 -0
- data/yard-stubs/carray_slab.rb +59 -0
- data/yard-stubs/carray_sort.rb +165 -0
- data/yard-stubs/carray_test.rb +85 -0
- data/yard-stubs/carray_undef.rb +64 -0
- data/yard-stubs/carray_utils.rb +97 -0
- data/yard-stubs/ruby_carray.rb +193 -0
- metadata +67 -10
- data/ext/ca_obj_unbound_repeat.c +0 -496
data/lib/carray/frame/io.rb
CHANGED
|
@@ -11,12 +11,27 @@ class CAFrame
|
|
|
11
11
|
# later. Broken cells fail to_type and become UNDEF automatically
|
|
12
12
|
# (parse-mask, §6-2).
|
|
13
13
|
#
|
|
14
|
+
# +source+ is a path, or an open IO -- anything answering +gets+, which a
|
|
15
|
+
# StringIO is. So CSV that is already in memory does not have to go to a
|
|
16
|
+
# temporary file first:
|
|
17
|
+
#
|
|
18
|
+
# CAFrame.from_csv("obs.csv") # a path
|
|
19
|
+
# CAFrame.from_csv(StringIO.new(body)) # text already in hand
|
|
20
|
+
# File.open("obs.csv") { |io| CAFrame.from_csv(io) }
|
|
21
|
+
#
|
|
22
|
+
# A String is always read as a path, never as CSV text: guessing between the
|
|
23
|
+
# two by looking for a newline is the kind of guess that is right until it is
|
|
24
|
+
# not, and StringIO says which one you meant in six characters. An IO is read
|
|
25
|
+
# from where it is and left open -- the caller opened it and closes it.
|
|
26
|
+
#
|
|
14
27
|
# Parsing uses the built-in fast tokenizer (CSVParser). Options:
|
|
15
28
|
# sep: field separator (default ",")
|
|
16
29
|
# quote: quote character (default '"')
|
|
17
30
|
# strip: trim spaces from unquoted fields (default false, RFC spacing)
|
|
18
|
-
# +encoding+:
|
|
19
|
-
#
|
|
31
|
+
# +encoding+: open-mode encoding for a path (default "bom|utf-8", strips a
|
|
32
|
+
# BOM). It has nothing to open when +source+ is an IO, so there
|
|
33
|
+
# the IO's own encoding governs and a BOM is the caller's.
|
|
34
|
+
# parser: a callable source -> [headers, rows] to inject another parser
|
|
20
35
|
# (e.g. the stdlib +csv+, or a typed-table source); when given,
|
|
21
36
|
# sep/quote/strip/encoding and any block are that parser's concern.
|
|
22
37
|
#
|
|
@@ -28,25 +43,25 @@ class CAFrame
|
|
|
28
43
|
# skip 2; header; skip 1; body
|
|
29
44
|
# end
|
|
30
45
|
#
|
|
46
|
+
# A missing field -- an unquoted empty one, or a cell a short row never
|
|
47
|
+
# reached -- is UNDEF in the frame, whether or not the column is cast by
|
|
48
|
+
# +types:+. A quoted empty field ("") is the empty string, which is a value.
|
|
49
|
+
# So the mask +to_csv+ writes comes back as a mask.
|
|
50
|
+
#
|
|
31
51
|
# Columns are handed to the frame as CABlock views over one backing object
|
|
32
52
|
# array (§3.6 view-by-default); casting a column materializes it, and +copy+
|
|
33
53
|
# gives an independent frame.
|
|
34
|
-
def self.from_csv(
|
|
54
|
+
def self.from_csv(source, types: nil,
|
|
35
55
|
sep: ",", quote: '"', strip: false,
|
|
36
56
|
encoding: "bom|utf-8", parser: nil, &block)
|
|
37
57
|
names, rows =
|
|
38
58
|
if parser
|
|
39
|
-
parser.call(
|
|
59
|
+
parser.call(source)
|
|
60
|
+
elsif source.respond_to?(:gets)
|
|
61
|
+
read_csv(source, sep: sep, quote: quote, strip: strip, &block)
|
|
40
62
|
else
|
|
41
|
-
File.open(
|
|
42
|
-
|
|
43
|
-
if block
|
|
44
|
-
block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
|
|
45
|
-
else
|
|
46
|
-
reader.header
|
|
47
|
-
reader.body
|
|
48
|
-
end
|
|
49
|
-
reader.result
|
|
63
|
+
File.open(source, "r:#{encoding}") do |io|
|
|
64
|
+
read_csv(io, sep: sep, quote: quote, strip: strip, &block)
|
|
50
65
|
end
|
|
51
66
|
end
|
|
52
67
|
|
|
@@ -55,6 +70,22 @@ class CAFrame
|
|
|
55
70
|
frame
|
|
56
71
|
end
|
|
57
72
|
|
|
73
|
+
# Drive the reading-control DSL over one open IO and hand back
|
|
74
|
+
# [names, rows]. Shared by the path and the IO source, so the two cannot
|
|
75
|
+
# come to read a file differently.
|
|
76
|
+
def self.read_csv (io, sep:, quote:, strip:, &block)
|
|
77
|
+
reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip)
|
|
78
|
+
if block
|
|
79
|
+
block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
|
|
80
|
+
else
|
|
81
|
+
reader.header
|
|
82
|
+
reader.body
|
|
83
|
+
end
|
|
84
|
+
reader.result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private_class_method :read_csv
|
|
88
|
+
|
|
58
89
|
# Build a frame from parsed [names, rows]. When names is nil (headerless and
|
|
59
90
|
# no column_names) positional names "c0".."cN" are generated from the widest
|
|
60
91
|
# row. Rows are squared off to the column count (short rows padded with nil,
|
|
@@ -62,8 +93,8 @@ class CAFrame
|
|
|
62
93
|
# is a view into it (§3.6).
|
|
63
94
|
# Write the frame as CSV. CSV is a flat table of scalar cells, so this is the
|
|
64
95
|
# text form of the same all-scalar subset +to_ca+ requires (§11.9): every
|
|
65
|
-
# column must be 1-D. Unlike +to_ca+ it does not promote to a common
|
|
66
|
-
# each column is formatted to text independently, so mixed
|
|
96
|
+
# column must be 1-D. Unlike +to_ca+ it does not promote to a common data type --
|
|
97
|
+
# each column is formatted to text independently, so mixed data types (numbers,
|
|
67
98
|
# strings, datetime / categorical Faces) sit side by side. An N-D column has
|
|
68
99
|
# no flat CSV cell and raises; export it per column, or use +to_records+ +
|
|
69
100
|
# JSON for the structured shape (memo §11.9, the N-D escape).
|
|
@@ -115,6 +146,177 @@ class CAFrame
|
|
|
115
146
|
end
|
|
116
147
|
end
|
|
117
148
|
|
|
149
|
+
# Render the frame as an aligned text table for reading:
|
|
150
|
+
#
|
|
151
|
+
# puts df.to_table
|
|
152
|
+
#
|
|
153
|
+
# time temp station
|
|
154
|
+
# ---------- ---- -------
|
|
155
|
+
# 2026-01-01 1.5 Tokyo
|
|
156
|
+
# 2026-01-02 _ Osaka
|
|
157
|
+
#
|
|
158
|
+
# This is the display counterpart of +to_csv+ and shares none of its
|
|
159
|
+
# constraints: it is text meant to be looked at, not read back. Numeric
|
|
160
|
+
# columns are right-aligned, everything else left-aligned; a masked cell
|
|
161
|
+
# shows as +_+, the same marker CArray's own inspect uses. An N-D column
|
|
162
|
+
# (which +to_csv+ rejects, having no flat cell) shows each row's slice as
|
|
163
|
+
# an Array literal.
|
|
164
|
+
#
|
|
165
|
+
# Float cells are rounded to +precision+ decimal places for display only
|
|
166
|
+
# (default 6); +precision: nil+ prints them at full precision, which is
|
|
167
|
+
# faithful but lets one long value set the column width.
|
|
168
|
+
#
|
|
169
|
+
# Long frames are truncated in the middle: +rows+ caps how many rows are
|
|
170
|
+
# printed (default 20, split evenly around an ellipsis row), and
|
|
171
|
+
# +rows: nil+ prints every row. +index: false+ drops the index column.
|
|
172
|
+
def to_table(rows: 20, index: true, precision: 6)
|
|
173
|
+
head = rows && (rows + 1) / 2
|
|
174
|
+
render_table(head: head, tail: rows && rows - head,
|
|
175
|
+
index: index, precision: precision, footer: true)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# +to_s+ is the whole frame, +inspect+ the middle-elided one -- so +puts df+
|
|
179
|
+
# dumps everything and +p df+ stays a screenful. +inspect+ leads with the
|
|
180
|
+
# same summary line it always had (nrow, variable data types, index), so the
|
|
181
|
+
# table under it needs no row-count footer.
|
|
182
|
+
def to_s
|
|
183
|
+
to_table(rows: nil)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The summary line (nrow, variable data types, index) followed by the
|
|
187
|
+
# middle-elided table.
|
|
188
|
+
# @return [String]
|
|
189
|
+
def inspect
|
|
190
|
+
parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
|
|
191
|
+
idx = @index ? " index=#{@axis_name.inspect}" : ""
|
|
192
|
+
head = "#<CAFrame nrow=#{@nrow} vars=[#{parts.join(', ')}]#{idx}>"
|
|
193
|
+
# The table counts the index as a column, so a frame whose only data is its
|
|
194
|
+
# index has one to show. Gate on the same thing render_table does.
|
|
195
|
+
return head if @columns.empty? && @index.nil?
|
|
196
|
+
head + "\n" + render_table(head: 8, tail: 2, index: true, precision: 6,
|
|
197
|
+
footer: false)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private def render_table(head:, tail:, index:, precision:, footer:)
|
|
201
|
+
names = []
|
|
202
|
+
columns = []
|
|
203
|
+
aligns = []
|
|
204
|
+
|
|
205
|
+
if index && @index
|
|
206
|
+
names << @axis_name
|
|
207
|
+
columns << @index
|
|
208
|
+
aligns << (@index.numeric? ? :right : :left)
|
|
209
|
+
end
|
|
210
|
+
@columns.each do |name, col|
|
|
211
|
+
names << name
|
|
212
|
+
columns << col
|
|
213
|
+
aligns << (col.ndim == 1 && col.numeric? ? :right : :left)
|
|
214
|
+
end
|
|
215
|
+
return "" if names.empty?
|
|
216
|
+
|
|
217
|
+
positions = table_row_positions(head, tail)
|
|
218
|
+
body = positions.map do |i|
|
|
219
|
+
if i.nil?
|
|
220
|
+
Array.new(columns.size, ":") # vertical ellipsis for the elided middle
|
|
221
|
+
else
|
|
222
|
+
columns.map { |col| format_table_cell(col, i, precision) }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
widths = names.each_with_index.map do |name, j|
|
|
227
|
+
[display_width(name), *body.map { |cells| display_width(cells[j]) }].max
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
out = +""
|
|
231
|
+
out << table_row(names, widths, aligns) << "\n"
|
|
232
|
+
out << table_row(widths.map { |w| "-" * w }, widths, aligns) << "\n"
|
|
233
|
+
body.each { |cells| out << table_row(cells, widths, aligns) << "\n" }
|
|
234
|
+
if footer && positions.size - positions.count(nil) < @nrow
|
|
235
|
+
out << "(#{plural(@nrow, 'row')}, #{plural(@columns.size, 'variable')})\n"
|
|
236
|
+
end
|
|
237
|
+
out
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Row indices to print, with nil marking the elided middle. A nil head means
|
|
241
|
+
# no cap; a frame that already fits in head + tail is listed whole.
|
|
242
|
+
private def table_row_positions(head, tail)
|
|
243
|
+
return (0...@nrow).to_a if head.nil? || @nrow <= head + tail
|
|
244
|
+
(0...head).to_a + [nil] + ((@nrow - tail)...@nrow).to_a
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private def table_row(cells, widths, aligns)
|
|
248
|
+
line = cells.each_with_index.map do |text, j|
|
|
249
|
+
pad = " " * (widths[j] - display_width(text))
|
|
250
|
+
aligns[j] == :right ? pad + text : text + pad
|
|
251
|
+
end.join(" ")
|
|
252
|
+
line.rstrip
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Column widths are counted in terminal cells, not characters: a CJK
|
|
256
|
+
# ideograph, kana, or full-width form occupies two cells, so counting
|
|
257
|
+
# characters would leave every column holding such a name ragged. The
|
|
258
|
+
# ranges below are the East Asian Wide / Fullwidth blocks; a combining
|
|
259
|
+
# mark takes no cell of its own.
|
|
260
|
+
WIDE_CHAR_RANGES = [
|
|
261
|
+
0x1100..0x115F, 0x2E80..0x303E, 0x3041..0x33FF, 0x3400..0x4DBF,
|
|
262
|
+
0x4E00..0x9FFF, 0xA000..0xA4CF, 0xA960..0xA97F, 0xAC00..0xD7A3,
|
|
263
|
+
0xF900..0xFAFF, 0xFE10..0xFE19, 0xFE30..0xFE6F, 0xFF00..0xFF60,
|
|
264
|
+
0xFFE0..0xFFE6, 0x1F300..0x1F64F, 0x1F900..0x1F9FF, 0x20000..0x3FFFD,
|
|
265
|
+
].freeze
|
|
266
|
+
private_constant :WIDE_CHAR_RANGES
|
|
267
|
+
|
|
268
|
+
COMBINING_RANGES = [0x0300..0x036F, 0x1AB0..0x1AFF, 0x20D0..0x20F0].freeze
|
|
269
|
+
private_constant :COMBINING_RANGES
|
|
270
|
+
|
|
271
|
+
private def display_width(text)
|
|
272
|
+
text.each_char.sum do |ch|
|
|
273
|
+
cp = ch.ord
|
|
274
|
+
if COMBINING_RANGES.any? { |r| r.cover?(cp) }
|
|
275
|
+
0
|
|
276
|
+
elsif WIDE_CHAR_RANGES.any? { |r| r.cover?(cp) }
|
|
277
|
+
2
|
|
278
|
+
else
|
|
279
|
+
1
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private def plural(n, noun)
|
|
285
|
+
"#{n} #{noun}#{n == 1 ? '' : 's'}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Render one N-D cell the way Array#inspect would, except that a masked
|
|
289
|
+
# element prints as the table's missing marker rather than as UNDEF.
|
|
290
|
+
private def format_nested_cell(v)
|
|
291
|
+
case v
|
|
292
|
+
when Array then "[" + v.map { |x| format_nested_cell(x) }.join(", ") + "]"
|
|
293
|
+
else UNDEF.equal?(v) || v.nil? ? "_" : v.inspect
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private def format_table_cell(col, i, precision)
|
|
298
|
+
e = elem_at(col, i)
|
|
299
|
+
if UNDEF.equal?(e) || e.nil?
|
|
300
|
+
"_"
|
|
301
|
+
elsif e.is_a?(Float) && precision
|
|
302
|
+
# Display rounding only: a full-precision float (141.67833333333334)
|
|
303
|
+
# sets the column width for every other row and makes the table hard
|
|
304
|
+
# to read. precision: nil prints the value as Ruby renders it.
|
|
305
|
+
e.round(precision).to_s
|
|
306
|
+
elsif e.is_a?(CArray)
|
|
307
|
+
# An N-D cell renders its elements, and a masked element among them
|
|
308
|
+
# takes the same marker a masked scalar does -- UNDEF's own inspect
|
|
309
|
+
# would put a second spelling of "missing" in the same table.
|
|
310
|
+
format_nested_cell(e.to_a)
|
|
311
|
+
elsif e.is_a?(String)
|
|
312
|
+
e
|
|
313
|
+
elsif e.respond_to?(:iso8601)
|
|
314
|
+
e.iso8601
|
|
315
|
+
else
|
|
316
|
+
e.to_s
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
118
320
|
private def format_csv_column(col)
|
|
119
321
|
col.to_a.map do |e|
|
|
120
322
|
if UNDEF.equal?(e) || e.nil?
|
|
@@ -157,6 +359,13 @@ class CAFrame
|
|
|
157
359
|
end
|
|
158
360
|
end
|
|
159
361
|
table = CArray.object(rows.size, ncol) { rows }
|
|
362
|
+
# A missing field is UNDEF, not a Ruby nil sitting in a cell. The
|
|
363
|
+
# tokenizer says missing with nil (an unquoted empty field; a quoted
|
|
364
|
+
# one is the empty string and stays a value), and an object array will
|
|
365
|
+
# hold that nil quite happily -- so a column read without `types:` used
|
|
366
|
+
# to carry nil where the same column read with one carried UNDEF, and
|
|
367
|
+
# the mask a to_csv had written did not survive the trip back.
|
|
368
|
+
table[:eq, nil] = UNDEF
|
|
160
369
|
names.each_with_index { |name, j| cols[name] = table[nil, j] }
|
|
161
370
|
new(cols)
|
|
162
371
|
end
|
data/lib/carray/frame/records.rb
CHANGED
|
@@ -59,30 +59,44 @@ class CAFrame
|
|
|
59
59
|
# native scalar column (nil -> UNDEF); anything else -> object column.
|
|
60
60
|
def self.build_record_column(values, n)
|
|
61
61
|
present = values.reject(&:nil?)
|
|
62
|
-
return CArray.object(n) { values } if present.empty?
|
|
62
|
+
return mask_missing(CArray.object(n) { values }) if present.empty?
|
|
63
63
|
|
|
64
64
|
if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
|
|
65
65
|
build_nd_column(values, present, n)
|
|
66
66
|
else
|
|
67
67
|
type = numeric_leaf_type(present)
|
|
68
68
|
col = CArray.object(n) { values }
|
|
69
|
-
type ? col.to_type(type) : col
|
|
69
|
+
type ? col.to_type(type) : mask_missing(col)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
private_class_method :build_record_column
|
|
73
73
|
|
|
74
|
+
# A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
|
|
75
|
+
# writes a masked cell as nil, so nil on the way back in is the only
|
|
76
|
+
# spelling a missing cell has; to_type does this conversion for a numeric
|
|
77
|
+
# column, and an object column would otherwise keep the nil as a value --
|
|
78
|
+
# a row with no label coming back as a row labelled nil. The CSV reader
|
|
79
|
+
# takes the same position (see build_frame in io.rb).
|
|
80
|
+
def self.mask_missing(col)
|
|
81
|
+
col[:eq, nil] = UNDEF
|
|
82
|
+
col
|
|
83
|
+
end
|
|
84
|
+
private_class_method :mask_missing
|
|
85
|
+
|
|
74
86
|
# Stack equal-length array cells into an (N, L) column via an object 2-D fill
|
|
75
87
|
# + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
|
|
76
88
|
# non-numeric leaves fall back to a 1-D object column of the raw cells.
|
|
77
89
|
def self.build_nd_column(values, present, n)
|
|
78
90
|
lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
|
|
79
91
|
len = lengths.first
|
|
80
|
-
|
|
92
|
+
unless lengths.all? { |x| x == len }
|
|
93
|
+
return mask_missing(CArray.object(n) { values })
|
|
94
|
+
end
|
|
81
95
|
|
|
82
96
|
nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
|
|
83
97
|
type = numeric_leaf_type(nested.flatten.compact)
|
|
84
98
|
table = CArray.object(n, len) { nested }
|
|
85
|
-
type ? table.to_type(type) : table
|
|
99
|
+
type ? table.to_type(type) : mask_missing(table)
|
|
86
100
|
end
|
|
87
101
|
private_class_method :build_nd_column
|
|
88
102
|
|
data/lib/carray/frame/sort.rb
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
#
|
|
14
14
|
# sort_addr is ascending, so a descending key is expressed by replacing it with
|
|
15
15
|
# its dense descending rank (CArray#order(descending: true, method: :dense)):
|
|
16
|
-
# this works for every
|
|
16
|
+
# this works for every data type (unlike negation, which cannot reverse a string and
|
|
17
17
|
# silently wraps an unsigned integer), and the *dense* rank keeps equal values
|
|
18
18
|
# on one rank so ties fall through to later keys in a multi-key sort.
|
|
19
19
|
# masked_position (CArray.sort_addr's kwarg) places masked key rows first or
|
data/lib/carray/frame/verbs.rb
CHANGED
|
@@ -143,11 +143,28 @@ class CAFrame
|
|
|
143
143
|
# carry, so it raises rather than silently truncate. Make it the index with
|
|
144
144
|
# +set_index+ afterward.
|
|
145
145
|
#
|
|
146
|
+
# A +CATime::Grid+ carries the same (unit, epoch) pair as one value, so a
|
|
147
|
+
# netCDF +units+ attribute goes straight in. It also carries a phase the
|
|
148
|
+
# keyword form cannot: the keyword +epoch+ is read on the +unit+ grid, so
|
|
149
|
+
# an epoch off that grid ("days since 1980-01-01 12:00") loses its
|
|
150
|
+
# time-of-day, while a grid resolves the finer storage that holds it.
|
|
151
|
+
#
|
|
146
152
|
# df.to_time("time", unit: :h, epoch: "1990-01-01").set_index("time")
|
|
147
|
-
|
|
153
|
+
# df.to_time("time", CATime::Grid.parse("hours since 1990-01-01"))
|
|
154
|
+
# df.to_time("time", CATime::Grid.parse("days since 1980-01-01 12:00"))
|
|
155
|
+
def to_time(name, grid = nil, unit: :s, epoch: nil)
|
|
148
156
|
key = name.to_s
|
|
149
157
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
150
158
|
raw = integer_serial_column(col, key)
|
|
159
|
+
grid = unit if unit.is_a?(CATime::Grid)
|
|
160
|
+
if grid.is_a?(CATime::Grid)
|
|
161
|
+
@columns[key] = grid.at(raw)
|
|
162
|
+
return self
|
|
163
|
+
end
|
|
164
|
+
unless grid.nil?
|
|
165
|
+
raise ArgumentError,
|
|
166
|
+
"the positional argument must be a CATime::Grid (got #{grid.class})"
|
|
167
|
+
end
|
|
151
168
|
if epoch
|
|
152
169
|
raw = raw + CArray.time(epoch, unit: unit).ticks[0]
|
|
153
170
|
end
|
|
@@ -213,7 +230,9 @@ class CAFrame
|
|
|
213
230
|
return
|
|
214
231
|
end
|
|
215
232
|
yvalid = col.value.float64[present]
|
|
216
|
-
|
|
233
|
+
# mask_invalid before the cast: linear_fetch marks out of range with NaN,
|
|
234
|
+
# and casting back to an integer column would turn it into a plausible 0.
|
|
235
|
+
col[] = yvalid.linear_fetch(addr).mask_invalid.to_type(col.data_type) # write-through
|
|
217
236
|
end
|
|
218
237
|
|
|
219
238
|
private def cast_one(name, type)
|
|
@@ -250,16 +269,17 @@ class CAFrame
|
|
|
250
269
|
"promote takes a data type Symbol (got #{type.class}); " \
|
|
251
270
|
"class-shaped targets are not promotion destinations"
|
|
252
271
|
end
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
# Check every column before rebinding any. A column that would narrow
|
|
273
|
+
# rejects the whole promote, and rejecting part way through would leave the
|
|
274
|
+
# frame promoted in whichever columns happened to come first.
|
|
275
|
+
#
|
|
276
|
+
# A Face column answers for itself: :object is its surface values, a
|
|
277
|
+
# numeric target is whatever it declares in #to_numeric (and a TypeError
|
|
278
|
+
# naming that method when it declares nothing). result_type has nothing
|
|
279
|
+
# to say about a surface it cannot read, so the widening check -- which
|
|
280
|
+
# is about primitive promotion -- applies to plain columns only.
|
|
281
|
+
@columns.each { |key, col| refuse_narrowing(key, col, type) unless col.face? }
|
|
282
|
+
@columns.each_key { |key| @columns[key] = @columns[key].to_type(type) }
|
|
263
283
|
end
|
|
264
284
|
|
|
265
285
|
private def refuse_narrowing(key, col, type)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# Reading `CArray.fuse { a + b * c }`.
|
|
3
|
+
#
|
|
4
|
+
# The block is not called. Its `a` is the array itself, so calling it would
|
|
5
|
+
# evaluate the expression eagerly -- which is the thing fuse exists to avoid.
|
|
6
|
+
# The source is read instead, every name that holds a CArray is given `.lazy`,
|
|
7
|
+
# and the result is evaluated back in the block's own binding, so `self`,
|
|
8
|
+
# instance variables, methods and constants are what they were.
|
|
9
|
+
#
|
|
10
|
+
# Ruby has no macro, so the alternative was to pass the arrays in and take
|
|
11
|
+
# shadows back -- `fuse(a, b) { |x, y| ... }` -- which names each of them
|
|
12
|
+
# twice. Julia writes `@.` for the same reason and does the same thing to
|
|
13
|
+
# the expression underneath.
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
require "prism"
|
|
17
|
+
|
|
18
|
+
class CArray
|
|
19
|
+
|
|
20
|
+
module FuseSource
|
|
21
|
+
|
|
22
|
+
# Runtime coercion, so the rewrite never has to work out what a name
|
|
23
|
+
# holds: anything that is not an array passes through untouched.
|
|
24
|
+
def self.shadow (value)
|
|
25
|
+
value.is_a?(CArray) ? value.lazy : value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.evaluate (block)
|
|
29
|
+
result = eval(rewrite(body_source(block)), block.binding,
|
|
30
|
+
*block.source_location)
|
|
31
|
+
# An expression that is just an array is that array; the shadow put
|
|
32
|
+
# around it has nothing to fuse.
|
|
33
|
+
result.is_a?(CALazyMarker) ? result.parent : result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# -- the block's own text ---------------------------------------------
|
|
37
|
+
|
|
38
|
+
def self.body_source (block)
|
|
39
|
+
text = extract(block)
|
|
40
|
+
wrapped = "proc " + text
|
|
41
|
+
node = Prism.parse(wrapped).value
|
|
42
|
+
.breadth_first_search { |n| n.is_a?(Prism::BlockNode) }
|
|
43
|
+
inner = node && node.body
|
|
44
|
+
unless inner
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"CArray.fuse could not read an expression out of this block"
|
|
47
|
+
end
|
|
48
|
+
wrapped.byteslice(inner.location.start_offset...inner.location.end_offset)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.extract (block)
|
|
52
|
+
sequence = RubyVM::InstructionSequence.of(block) rescue nil
|
|
53
|
+
location = sequence && sequence.to_a[4][:code_location]
|
|
54
|
+
path = sequence && (sequence.absolute_path || sequence.path)
|
|
55
|
+
unless location && path && File.readable?(path)
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"CArray.fuse cannot read this block's source (defined in irb, " \
|
|
58
|
+
"eval, or a file that is no longer there). Write `.lazy` on " \
|
|
59
|
+
"the operands instead: `a.lazy + b.lazy`."
|
|
60
|
+
end
|
|
61
|
+
lines = File.readlines(path)
|
|
62
|
+
first_line, first_column, last_line, last_column = location
|
|
63
|
+
# The columns count bytes, not characters, so a line with anything
|
|
64
|
+
# multi-byte on it slices in the wrong place unless this does too.
|
|
65
|
+
if first_line == last_line
|
|
66
|
+
lines[first_line - 1].byteslice(first_column...last_column)
|
|
67
|
+
else
|
|
68
|
+
[lines[first_line - 1].byteslice(first_column..),
|
|
69
|
+
*lines[first_line...(last_line - 1)],
|
|
70
|
+
lines[last_line - 1].byteslice(0...last_column)].join
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# -- the rewrite -------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
# The leaves are the names being read. Everything else keeps its shape:
|
|
77
|
+
# calls are inserted around leaves and the expression they sit in is
|
|
78
|
+
# left alone.
|
|
79
|
+
class Leaves < Prism::Visitor
|
|
80
|
+
attr_reader :spots
|
|
81
|
+
|
|
82
|
+
def initialize
|
|
83
|
+
@spots = []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def visit_local_variable_read_node (node) = mark(node)
|
|
87
|
+
def visit_instance_variable_read_node (node) = mark(node)
|
|
88
|
+
def visit_constant_read_node (node) = mark(node)
|
|
89
|
+
|
|
90
|
+
# `Math::PI` is one name, not `Math` with something after it.
|
|
91
|
+
def visit_constant_path_node (node)
|
|
92
|
+
mark(node)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def visit_call_node (node)
|
|
96
|
+
if node.name == :[] || node.name == :[]=
|
|
97
|
+
# An index is a position, not a value to fuse: `a[i]` shadows `a`
|
|
98
|
+
# and leaves `i` alone.
|
|
99
|
+
visit(node.receiver)
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
mark(node) if node.receiver.nil? && node.arguments.nil? && node.block.nil?
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def mark (node)
|
|
109
|
+
@spots << [node.location.start_offset, node.location.end_offset]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.rewrite (source)
|
|
114
|
+
visitor = Leaves.new
|
|
115
|
+
Prism.parse(source).value.accept(visitor)
|
|
116
|
+
out = source.dup
|
|
117
|
+
visitor.spots.sort_by { |start, _| -start }.each do |start, stop|
|
|
118
|
+
out[start...stop] = "::CArray::FuseSource.shadow(#{source[start...stop]})"
|
|
119
|
+
end
|
|
120
|
+
out
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|