carray 3.0.3 → 3.0.4
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/CHANGELOG.md +264 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +1 -6
- data/ext/ca_axis_group.c +27 -27
- data/ext/ca_binop_dispatch.c +1 -29
- data/ext/ca_binop_dispatch.h +0 -1
- data/ext/ca_categorical_iterator.c +205 -134
- data/ext/ca_compare.c +7 -18
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1234 -1994
- data/ext/ca_kernel_iterator.h +391 -437
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +96 -39
- data/ext/ca_obj_bincmp.c +24 -21
- data/ext/ca_obj_binop.c +15 -26
- data/ext/ca_obj_bitarray.c +3 -2
- data/ext/ca_obj_bitfield.c +5 -6
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +5 -6
- data/ext/ca_obj_const_string.c +173 -19
- data/ext/ca_obj_face.c +238 -57
- data/ext/ca_obj_face.h +19 -0
- data/ext/ca_obj_fake.c +15 -29
- data/ext/ca_obj_farray.c +6 -3
- data/ext/ca_obj_field.c +17 -5
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +406 -132
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +67 -57
- data/ext/ca_obj_object.c +157 -51
- data/ext/ca_obj_record.c +12 -7
- data/ext/ca_obj_reduce.c +13 -8
- data/ext/ca_obj_refer.c +25 -27
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +6 -5
- data/ext/ca_obj_select.c +14 -9
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +11 -1
- data/ext/ca_obj_stack.c +179 -34
- data/ext/ca_obj_stride.c +162 -115
- data/ext/ca_obj_string.c +71 -4
- data/ext/ca_obj_tile.c +6 -5
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +5 -3
- data/ext/ca_obj_triop.c +23 -32
- data/ext/ca_obj_window.c +190 -26
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +181 -75
- data/ext/carray_access.c +184 -96
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +89 -6
- data/ext/carray_bincount.c +10 -8
- data/ext/carray_broadcast.c +13 -8
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +108 -0
- data/ext/carray_cast.c +142 -24
- data/ext/carray_cast_func.rb +46 -6
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +16 -7
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +147 -55
- data/ext/carray_count.c +50 -20
- data/ext/carray_element.c +76 -33
- data/ext/carray_factorize.c +118 -64
- data/ext/carray_generate.c +191 -50
- data/ext/carray_histogram.c +62 -17
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +336 -29
- data/ext/carray_kernels_bincmp.c +4453 -0
- data/ext/carray_kernels_binop.c +11368 -0
- data/ext/carray_kernels_init.c +1734 -0
- data/ext/carray_kernels_moncmp.c +2104 -0
- data/ext/carray_kernels_monop.c +18369 -0
- data/ext/carray_kernels_reduce_aggregate.c +23818 -0
- data/ext/carray_kernels_reduce_boolean.c +406 -0
- data/ext/carray_kernels_reduce_cumulative.c +10086 -0
- data/ext/carray_kernels_reduce_extreme.c +17634 -0
- data/ext/carray_kernels_reduce_variance.c +3849 -0
- data/ext/carray_kernels_scan.c +4302 -0
- data/ext/carray_kernels_search.c +33829 -0
- data/ext/carray_kernels_sort.c +10906 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +292 -72
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +124 -47
- data/ext/carray_order.c +384 -76
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +67 -13
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +63 -38
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +91 -28
- data/ext/carray_sort.c +35 -35
- data/ext/carray_struct.c +19 -18
- data/ext/carray_test.c +26 -5
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +246 -45
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +198 -240
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +126 -22
- data/lib/carray/autoload_carray.rb +8 -31
- data/lib/carray/autoload_method_extension.rb +14 -5
- data/lib/carray/axis_group.rb +255 -106
- data/lib/carray/basics.rb +78 -10
- data/lib/carray/bincount_nd.rb +12 -2
- data/lib/carray/block_iterator.rb +126 -23
- data/lib/carray/boolean_reduce.rb +17 -17
- data/lib/carray/categorical.rb +165 -43
- data/lib/carray/categorical_iterator.rb +130 -27
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +14 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/convert.rb +69 -0
- data/lib/carray/frame/csv_parser.rb +111 -17
- data/lib/carray/frame/describe.rb +3 -2
- data/lib/carray/frame/frame.rb +246 -21
- data/lib/carray/frame/group.rb +510 -95
- data/lib/carray/frame/io.rb +115 -28
- data/lib/carray/frame/join.rb +312 -51
- data/lib/carray/frame/records.rb +83 -7
- data/lib/carray/frame/reshape.rb +42 -35
- data/lib/carray/frame/sort.rb +7 -19
- data/lib/carray/frame/verbs.rb +188 -48
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +11 -4
- data/lib/carray/fusion.rb +40 -16
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +31 -8
- data/lib/carray/iterator.rb +85 -7
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +182 -183
- data/lib/carray/mask_gap_fill.rb +63 -57
- data/lib/carray/meld_reduce.rb +12 -14
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +16 -0
- data/lib/carray/methods/bit_string.rb +1 -1
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +3 -1
- data/lib/carray/methods/gather_nd.rb +14 -5
- data/lib/carray/methods/insert_block.rb +2 -2
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +53 -36
- data/lib/carray/methods/join.rb +1 -6
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +47 -51
- data/lib/carray/methods/mode.rb +1 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/resize.rb +3 -3
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +3 -3
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +53 -8
- data/lib/carray/segment_iterator.rb +313 -82
- data/lib/carray/serialize.rb +24 -10
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +8 -1
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +19 -8
- data/lib/carray/time.rb +726 -104
- data/lib/carray/window_iterator.rb +122 -16
- data/lib/carray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +4 -0
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +2 -1
- data/yard-stubs/ca_obj_roll.rb +2 -2
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_cast.rb +10 -0
- data/yard-stubs/carray_conversion.rb +2 -1
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +26 -2
- data/yard-stubs/carray_math.rb +27 -1
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +29 -18
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +14 -9
- data/yard-stubs/carray_slab.rb +9 -2
- data/yard-stubs/carray_sort.rb +6 -5
- data/yard-stubs/carray_test.rb +5 -3
- metadata +25 -3
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
data/lib/carray/frame/join.rb
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
# The frame layer just distributes that address to every column.
|
|
8
8
|
|
|
9
9
|
require "carray/methods/is_in" # promote_value_set, for align's Array reference
|
|
10
|
+
require "carray/methods/locate_addr" # nearest_refuse_bytes, for join_asof
|
|
10
11
|
|
|
11
12
|
class CAFrame
|
|
12
13
|
# Default disambiguation for non-key columns present on both sides (§12-C):
|
|
13
14
|
# left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
|
|
14
15
|
# pandas' +_x+/+_y+ for the target audience.
|
|
15
16
|
DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
|
|
17
|
+
Ractor.make_shareable(DEFAULT_JOIN_SUFFIXES)
|
|
16
18
|
private_constant :DEFAULT_JOIN_SUFFIXES
|
|
17
19
|
|
|
18
20
|
# Join +other+ on a shared key column, as SQL joins: each pair of a left
|
|
@@ -36,16 +38,38 @@ class CAFrame
|
|
|
36
38
|
# both sides (+suffixes: ["_left", "_right"]+); pass a 2-element array to pick
|
|
37
39
|
# meaningful names up front (+["_obs", "_fcst"]+), or +suffixes: false+ to
|
|
38
40
|
# raise instead. Rename afterward with +rename+ if needed (§12-C).
|
|
41
|
+
#
|
|
42
|
+
# +on:+ may name several columns, as an Array: rows then match when every
|
|
43
|
+
# key column matches, and a row with any key cell masked matches nothing.
|
|
44
|
+
#
|
|
45
|
+
# obs.join(fcst, on: ["station", "date"])
|
|
46
|
+
#
|
|
47
|
+
# Two modes filter this frame's rows by +other+ instead of pairing them:
|
|
48
|
+
#
|
|
49
|
+
# how: :semi — the rows whose key some row of +other+ has.
|
|
50
|
+
# how: :anti — the rows whose key no row of +other+ has.
|
|
51
|
+
#
|
|
52
|
+
# Each row appears at most once, in this frame's order, and no column of
|
|
53
|
+
# +other+ is added, so no name collides and +suffixes:+ is refused. A row
|
|
54
|
+
# with a masked key matches nothing: :semi drops it and :anti keeps it.
|
|
55
|
+
# The result shares this frame's columns and index, as +filter+'s does.
|
|
56
|
+
#
|
|
57
|
+
# obs.join(stations, on: "station", how: :anti) # stations not listed
|
|
39
58
|
def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
40
|
-
|
|
41
|
-
lkey = join_key(self[on], on, "join")
|
|
42
|
-
rkey = join_key(other[on], on, "join")
|
|
43
|
-
plan = join_name_plan(other, on, suffixes)
|
|
44
|
-
|
|
45
|
-
unless %i[left inner outer right].include?(how)
|
|
59
|
+
unless %i[left inner outer right semi anti].include?(how)
|
|
46
60
|
raise ArgumentError, "unknown join mode #{how.inspect}"
|
|
47
61
|
end
|
|
48
|
-
|
|
62
|
+
if (how == :semi || how == :anti) && !suffixes.equal?(DEFAULT_JOIN_SUFFIXES)
|
|
63
|
+
raise ArgumentError, "join: how: #{how.inspect} adds no column of the other frame, so suffixes: does not apply"
|
|
64
|
+
end
|
|
65
|
+
names = join_key_names(on, "join", "on:")
|
|
66
|
+
lkeys = names.map { |name| key_column(name, "join") }
|
|
67
|
+
rkeys = names.map { |name| other.__send__(:key_column, name, "join") }
|
|
68
|
+
join_check_key_widths(names, lkeys, rkeys, "join")
|
|
69
|
+
return join_filter(lkeys, rkeys, how) if how == :semi || how == :anti
|
|
70
|
+
|
|
71
|
+
plan = join_name_plan(other, names, suffixes)
|
|
72
|
+
join_pairs(other, names, lkeys, rkeys, how, plan)
|
|
49
73
|
end
|
|
50
74
|
|
|
51
75
|
# As-of (nearest-key) left join for irregular series (memo §11.7). Each
|
|
@@ -57,21 +81,43 @@ class CAFrame
|
|
|
57
81
|
# is a number for a numeric key and a duration for a time key
|
|
58
82
|
# ("10 minutes", or a CATimedelta value). Same wiring as the left join,
|
|
59
83
|
# only the address primitive differs.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
#
|
|
85
|
+
# +by:+ names columns that must match exactly, one name or an Array: each
|
|
86
|
+
# left row is matched to the nearest row among the rows of +other+ with
|
|
87
|
+
# the same +by+ values. A row with a masked +by+ cell matches nothing.
|
|
88
|
+
#
|
|
89
|
+
# obs.join_asof(radar, on: "time", by: "station", tolerance: "10 minutes")
|
|
90
|
+
def join_asof(other, on:, by: nil, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
91
|
+
on = column_key(on, "join_asof")
|
|
92
|
+
exact = by.nil? ? [] : join_key_names(by, "join_asof", "by:")
|
|
93
|
+
if exact.include?(on)
|
|
94
|
+
raise ArgumentError, "join_asof: #{on.inspect} is the on: key; by: names the others"
|
|
95
|
+
end
|
|
96
|
+
plan = join_name_plan(other, [on, *exact], suffixes)
|
|
97
|
+
lkey = key_column(on, "join_asof")
|
|
98
|
+
rkey = other.__send__(:key_column, on, "join_asof")
|
|
99
|
+
[lkey, rkey].each do |key|
|
|
100
|
+
key.__send__(:nearest_refuse_bytes, key, "join_asof: the on: key #{on.inspect}",
|
|
101
|
+
"join on it exactly with join")
|
|
102
|
+
end
|
|
103
|
+
addr = if exact.empty?
|
|
104
|
+
lkey.locate_nearest_addr(rkey, direction: direction, tolerance: tolerance)
|
|
105
|
+
else
|
|
106
|
+
lexact = exact.map { |name| key_column(name, "join_asof") }
|
|
107
|
+
rexact = exact.map { |name| other.__send__(:key_column, name, "join_asof") }
|
|
108
|
+
join_check_key_widths(exact, lexact, rexact, "join_asof")
|
|
109
|
+
_, lcode, rcode, k = join_codes(lexact, rexact)
|
|
110
|
+
join_asof_by(lkey, rkey, lcode, rcode, k, direction, tolerance)
|
|
111
|
+
end
|
|
112
|
+
join_by_addr(other, [on, *exact], addr, plan)
|
|
67
113
|
end
|
|
68
114
|
|
|
69
115
|
# Conform every column to an externally supplied reference key set (the
|
|
70
116
|
# asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
|
|
71
117
|
# of key values, each column is gathered onto it by exact key match
|
|
72
|
-
# (+locate_addr+): the aligned key column (or index)
|
|
73
|
-
#
|
|
74
|
-
# every other column.
|
|
118
|
+
# (+locate_addr+): the aligned key column (or index) holds a copy of the
|
|
119
|
+
# reference, with the key's attributes, and a reference key absent from
|
|
120
|
+
# the source comes back UNDEF in every other column.
|
|
75
121
|
#
|
|
76
122
|
# CAFrame measures no interval and generates nothing -- the caller owns the
|
|
77
123
|
# reference. This is the primitive for reindexing to a caller-built axis
|
|
@@ -85,9 +131,9 @@ class CAFrame
|
|
|
85
131
|
# Array is read as values of the key's kind, as +is_in+ reads one: numbers
|
|
86
132
|
# against a numeric key, Time / Date / String instants against a time key.
|
|
87
133
|
def align(key, reference)
|
|
88
|
-
key = key
|
|
134
|
+
key = column_key(key, "align")
|
|
89
135
|
on_index = !@columns.key?(key) && @axis_name == key && @index
|
|
90
|
-
keys =
|
|
136
|
+
keys = key_column(key, "align")
|
|
91
137
|
# An Array reference is read as values of the key's kind, as is_in reads
|
|
92
138
|
# one: 1 and 2.5 against a float key are numbers, and Time or String
|
|
93
139
|
# values against a time key are instants.
|
|
@@ -97,10 +143,15 @@ class CAFrame
|
|
|
97
143
|
"align: the reference has shape #{ref.shape.inspect}; give one key value per row of the result"
|
|
98
144
|
end
|
|
99
145
|
addr = ref.locate_addr(keys)
|
|
146
|
+
# The result's keys are the reference's values, taken as they are now
|
|
147
|
+
# and shown with the key's attributes; the caller's array is left as
|
|
148
|
+
# it is.
|
|
149
|
+
ref = CAFrame.__send__(:key_labels, keys, reference.is_a?(CArray) ? ref.copy : ref)
|
|
100
150
|
|
|
101
151
|
cols = {}
|
|
102
152
|
column_names.each { |name| cols[name] = project_rows(self[name], addr) }
|
|
103
153
|
if on_index
|
|
154
|
+
# key-labels: ref was given the key's attributes above
|
|
104
155
|
CAFrame.new(cols, axis_name: key, index: ref)
|
|
105
156
|
else
|
|
106
157
|
cols[key] = ref
|
|
@@ -157,6 +208,9 @@ class CAFrame
|
|
|
157
208
|
# Detect columns that appear in 2+ frames (collisions across the K inputs).
|
|
158
209
|
name_count = Hash.new(0)
|
|
159
210
|
frames.each { |f| f.column_names.each { |name| name_count[name] += 1 } }
|
|
211
|
+
# The first frame's index keeps its name as the result's row axis, so a
|
|
212
|
+
# column of that name collides with it as with a column.
|
|
213
|
+
name_count[first.axis_name] += 1 if first.index
|
|
160
214
|
collisions = name_count.select { |_, n| n > 1 }.keys
|
|
161
215
|
|
|
162
216
|
if collisions.any?
|
|
@@ -192,17 +246,159 @@ class CAFrame
|
|
|
192
246
|
new(cols, axis_name: first.axis_name, index: first.index)
|
|
193
247
|
end
|
|
194
248
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
249
|
+
# A fixlen key holds a cell-width blob, so a left key and a right key of
|
|
250
|
+
# different widths share no value. Refuse it here, naming the column,
|
|
251
|
+
# rather than from inside the set operation the join runs.
|
|
252
|
+
private def join_check_key_widths(names, lkeys, rkeys, verb)
|
|
253
|
+
names.zip(lkeys, rkeys) do |name, l, r|
|
|
254
|
+
next unless l.data_type == CA_FIXLEN && r.data_type == CA_FIXLEN
|
|
255
|
+
next if l.face? || r.face? || l.bytes == r.bytes
|
|
256
|
+
raise CArray::DataTypeError,
|
|
257
|
+
"#{verb}: the key #{name.inspect} is fixlen #{l.bytes} bytes wide on the left " \
|
|
258
|
+
"and #{r.bytes} on the right; a fixlen value is a cell-width blob, so the widths must match"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The key column names of a join: one name, or an Array of names.
|
|
263
|
+
private def join_key_names(on, verb, option)
|
|
264
|
+
names = on.is_a?(Array) ? on : [on]
|
|
265
|
+
raise ArgumentError, "#{verb}: #{option} names no key column" if names.empty?
|
|
266
|
+
CAFrame.__send__(:refuse_repeated_names, names.map { |name| column_key(name, verb) },
|
|
267
|
+
verb, "#{option} names")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Codes for the rows of both sides of a join on +lkeys+ / +rkeys+ (one
|
|
271
|
+
# array per key column): equal codes for equal keys, in 0...k, and none
|
|
272
|
+
# for a row with a masked key cell. Each key column gets a code in one
|
|
273
|
+
# set of distinct values of both sides, compared as the lookups compare
|
|
274
|
+
# them (type, unit and width reconciled). With several key columns a
|
|
275
|
+
# row's key is the tuple of its codes, coded again one column at a time
|
|
276
|
+
# so the number stays below the row count. Returns [parts, lcode, rcode,
|
|
277
|
+
# k], parts holding each column's [distinct values, left codes, right
|
|
278
|
+
# codes].
|
|
279
|
+
private def join_codes(lkeys, rkeys)
|
|
280
|
+
parts = lkeys.zip(rkeys).map do |lkey, rkey|
|
|
281
|
+
common = CArray.align_addr(lkey, rkey, join: :outer)[0]
|
|
282
|
+
[common, lkey.locate_addr(common), rkey.locate_addr(common)]
|
|
283
|
+
end
|
|
284
|
+
_, lcode, rcode = parts[0]
|
|
285
|
+
k = parts[0][0].elements
|
|
286
|
+
parts.drop(1).each do |common, lc, rc|
|
|
287
|
+
# keys: the two frames are coded on the values they share, which
|
|
288
|
+
# CACategorical.from_keys (one side's own values) cannot give
|
|
289
|
+
lt = lcode * common.elements + lc
|
|
290
|
+
rt = rcode * common.elements + rc
|
|
291
|
+
tuples = CArray.align_addr(lt, rt, join: :outer)[0]
|
|
292
|
+
lcode, rcode, k = lt.locate_addr(tuples), rt.locate_addr(tuples), tuples.elements
|
|
293
|
+
end
|
|
294
|
+
[parts, lcode, rcode, k]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# The rows of a :semi or :anti join: whether each row's key code occurs
|
|
298
|
+
# among the other side's codes. A row without a code (a masked key cell)
|
|
299
|
+
# has no match.
|
|
300
|
+
private def join_filter(lkeys, rkeys, how)
|
|
301
|
+
_, lcode, rcode, k = join_codes(lkeys, rkeys)
|
|
302
|
+
matched = if k == 0
|
|
303
|
+
CArray.boolean(nrow).fill(false)
|
|
304
|
+
else
|
|
305
|
+
rcode.bincount(length: k).project(lcode).strip_mask(0).gt(0)
|
|
306
|
+
end
|
|
307
|
+
select_rows(how == :semi ? matched : matched.not)
|
|
202
308
|
end
|
|
203
309
|
|
|
204
|
-
|
|
205
|
-
|
|
310
|
+
# The other side's row for each left row of an as-of join with exact
|
|
311
|
+
# keys: the nearest by +lkey+ / +rkey+ among the right rows with the
|
|
312
|
+
# same code, as locate_nearest_addr finds it within one group.
|
|
313
|
+
#
|
|
314
|
+
# All groups are searched at once. The right rows are sorted by (code,
|
|
315
|
+
# key, row); sorting the left keys in among them by (code, key) counts,
|
|
316
|
+
# for each left row, the right rows of its group below it, and the
|
|
317
|
+
# nearest is one of the two right rows on either side of that count.
|
|
318
|
+
private def join_asof_by(lkey, rkey, lcode, rcode, k, direction, tolerance)
|
|
319
|
+
# direction: and tolerance: are checked here, on no rows, whether or
|
|
320
|
+
# not any group is in both frames.
|
|
321
|
+
lkey[0...0].locate_nearest_addr(rkey[0...0], direction: direction, tolerance: tolerance)
|
|
322
|
+
limit = tolerance && CArray::DistanceLimit.read(tolerance, lkey, "tolerance:")
|
|
323
|
+
addr = CArray.int64(lkey.elements).fill(UNDEF)
|
|
324
|
+
|
|
325
|
+
# The right rows that can be found: a code, and a key that is present
|
|
326
|
+
# and not NaN. Each group's rows in key order, a tie in row order.
|
|
327
|
+
rvalid = asof_searchable(rkey, rcode)
|
|
328
|
+
rrows = CArray.int64(rkey.elements).seq[rvalid].copy
|
|
329
|
+
return addr if rrows.elements == 0
|
|
330
|
+
rrows = rrows.project(CArray.sort_addr(rcode.project(rrows), rkey.project(rrows), rrows))
|
|
331
|
+
lvalid = asof_searchable(lkey, lcode)
|
|
332
|
+
lrows = CArray.int64(lkey.elements).seq[lvalid].copy
|
|
333
|
+
return addr if lrows.elements == 0
|
|
334
|
+
|
|
335
|
+
rk, qk = lkey.__send__(:nearest_keys, rkey.project(rrows), lkey.project(lrows))
|
|
336
|
+
rc = rcode.project(rrows).int64
|
|
337
|
+
qc = lcode.project(lrows).int64
|
|
338
|
+
n = rrows.elements
|
|
339
|
+
gcount = rc.bincount(length: k).int64
|
|
340
|
+
gstart = gcount.cumsum.int64 - gcount
|
|
341
|
+
|
|
342
|
+
# How many right rows of its group each left row has below it (at or
|
|
343
|
+
# below when +inclusive+): the left keys sorted in among the right
|
|
344
|
+
# ones, a tie putting the right one first when it counts.
|
|
345
|
+
below = lambda do |inclusive|
|
|
346
|
+
tag = CArray.int8(n + qk.elements)
|
|
347
|
+
tag[0...n] = inclusive ? 0 : 1
|
|
348
|
+
tag[n..-1] = inclusive ? 1 : 0
|
|
349
|
+
perm = CArray.sort_addr(CArray.concatenate([rc, qc]), CArray.concatenate([rk, qk]), tag)
|
|
350
|
+
seen = perm.lt(n).cumsum.int64
|
|
351
|
+
query = perm.ge(n)
|
|
352
|
+
count = CArray.int64(qk.elements)
|
|
353
|
+
count[perm[query] - n] = seen[query]
|
|
354
|
+
count - gstart.project(qc)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
size = gcount.project(qc)
|
|
358
|
+
local =
|
|
359
|
+
case direction
|
|
360
|
+
when :floor then below.(true) - 1
|
|
361
|
+
when :ceil then below.(false)
|
|
362
|
+
when :round
|
|
363
|
+
lt = below.(false)
|
|
364
|
+
start = gstart.project(qc)
|
|
365
|
+
has_lo = lt.gt(0)
|
|
366
|
+
has_hi = lt.lt(size)
|
|
367
|
+
lo = rk.project((start + lt - 1).tap { |a| a[has_lo.not] = UNDEF })
|
|
368
|
+
hi = rk.project((start + lt).tap { |a| a[has_hi.not] = UNDEF })
|
|
369
|
+
# an exact match takes its first reference; otherwise the nearer
|
|
370
|
+
# neighbour, the upper one on a tie
|
|
371
|
+
upper = (hi - qk).le(qk - lo).strip_mask(false)
|
|
372
|
+
upper[has_lo.not] = true
|
|
373
|
+
upper[has_hi.not] = false
|
|
374
|
+
upper[hi.eq(qk).strip_mask(false)] = true
|
|
375
|
+
lt - 1 + upper.int64
|
|
376
|
+
end
|
|
377
|
+
local[local.lt(0) | local.ge(size)] = UNDEF
|
|
378
|
+
found = rrows.project(gstart.project(qc) + local)
|
|
379
|
+
if limit
|
|
380
|
+
dist = (rkey.project(found) - lkey.project(lrows)).abs
|
|
381
|
+
found[dist > limit] = UNDEF
|
|
382
|
+
end
|
|
383
|
+
addr[lrows] = found
|
|
384
|
+
addr
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# The rows of a key that an as-of search can place: a group code, and a
|
|
388
|
+
# key cell that is neither masked nor NaN.
|
|
389
|
+
private def asof_searchable(key, code)
|
|
390
|
+
ok = code.is_not_masked.strip_mask(false)
|
|
391
|
+
ok &= key.is_not_masked.strip_mask(false) if key.has_mask?
|
|
392
|
+
ok &= key.is_nan.strip_mask(false).not if key.float?
|
|
393
|
+
ok
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# The left index keeps its name as the result's row axis, so a right
|
|
397
|
+
# column of that name collides with it as with a left column: it takes
|
|
398
|
+
# the right suffix, and there is no left column to take the left one.
|
|
399
|
+
private def join_name_plan(other, names, suffixes)
|
|
400
|
+
taken = @index ? column_names | [@axis_name] : column_names
|
|
401
|
+
collisions = (taken & other.column_names) - names
|
|
206
402
|
return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
|
|
207
403
|
|
|
208
404
|
if suffixes == false
|
|
@@ -221,6 +417,7 @@ class CAFrame
|
|
|
221
417
|
plan[:collisions].include?(name) ? "#{name}#{suffix}" : name
|
|
222
418
|
end
|
|
223
419
|
|
|
420
|
+
# names-check: the output is checked as it is built, a column at a time
|
|
224
421
|
private def put_join_column(cols, name, col)
|
|
225
422
|
if cols.key?(name)
|
|
226
423
|
raise ArgumentError,
|
|
@@ -230,11 +427,11 @@ class CAFrame
|
|
|
230
427
|
cols[name] = col
|
|
231
428
|
end
|
|
232
429
|
|
|
233
|
-
private def join_by_addr(other,
|
|
430
|
+
private def join_by_addr(other, names, addr, plan)
|
|
234
431
|
cols = {}
|
|
235
432
|
column_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
|
|
236
433
|
other.column_names.each do |name|
|
|
237
|
-
next if name
|
|
434
|
+
next if names.include?(name)
|
|
238
435
|
put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
|
|
239
436
|
end
|
|
240
437
|
CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
@@ -242,19 +439,15 @@ class CAFrame
|
|
|
242
439
|
|
|
243
440
|
# The rows of a key join: li / ri are the left / right row of each result
|
|
244
441
|
# row, UNDEF on the side a row has no match on.
|
|
245
|
-
private def join_pairs(other,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# gets none and matches nothing.
|
|
249
|
-
common = CArray.align_addr(lkey, rkey, join: :outer)[0]
|
|
250
|
-
lcode = lkey.locate_addr(common)
|
|
251
|
-
rcode = rkey.locate_addr(common)
|
|
442
|
+
private def join_pairs(other, names, lkeys, rkeys, how, plan)
|
|
443
|
+
parts, lcode, rcode, k = join_codes(lkeys, rkeys)
|
|
444
|
+
extra = nil
|
|
252
445
|
if how == :right
|
|
253
|
-
ri, li = join_expand(rcode, lcode,
|
|
446
|
+
ri, li = join_expand(rcode, lcode, k, true)
|
|
254
447
|
else
|
|
255
|
-
li, ri = join_expand(lcode, rcode,
|
|
448
|
+
li, ri = join_expand(lcode, rcode, k, how != :inner)
|
|
256
449
|
if how == :outer
|
|
257
|
-
lcount = lcode.bincount(length:
|
|
450
|
+
lcount = lcode.bincount(length: k)
|
|
258
451
|
alone = rcode.is_masked | lcount.project(rcode).strip_mask(0).eq(0)
|
|
259
452
|
alone = alone.strip_mask(true)
|
|
260
453
|
extra = CArray.int64(rcode.elements).seq[alone]
|
|
@@ -262,33 +455,35 @@ class CAFrame
|
|
|
262
455
|
undef_rows = CArray.int64(extra.elements).fill(UNDEF)
|
|
263
456
|
li = CArray.concatenate([li, undef_rows])
|
|
264
457
|
ri = CArray.concatenate([ri, extra])
|
|
265
|
-
|
|
266
|
-
|
|
458
|
+
else
|
|
459
|
+
extra = nil
|
|
267
460
|
end
|
|
268
461
|
end
|
|
269
462
|
end
|
|
270
463
|
every_left = how == :left || how == :inner
|
|
271
464
|
identity = every_left && li.elements == nrow &&
|
|
272
465
|
(nrow == 0 || li.eq(CArray.int64(nrow).seq).all)
|
|
466
|
+
rows = { li: li, ri: ri, extra: extra, how: how, every_left: every_left, identity: identity }
|
|
273
467
|
cols = {}
|
|
274
468
|
column_names.each do |name|
|
|
275
|
-
|
|
469
|
+
j = names.index(name)
|
|
470
|
+
col = if j then joined_key(j, parts, lkeys, rkeys, rows)
|
|
471
|
+
elsif identity then self[name]
|
|
276
472
|
elsif every_left then self[name][li, *([nil] * (self[name].ndim - 1))]
|
|
277
|
-
elsif name == on then nil
|
|
278
473
|
else project_rows(self[name], li)
|
|
279
474
|
end
|
|
280
|
-
|
|
281
|
-
col = how == :right ? project_rows(rkey, ri) :
|
|
282
|
-
common.project(code || lcode.project(li))
|
|
283
|
-
end
|
|
284
|
-
name = join_name(name, plan[:lsuf], plan) unless name == on
|
|
475
|
+
name = join_name(name, plan[:lsuf], plan) unless j
|
|
285
476
|
put_join_column(cols, name, col)
|
|
286
477
|
end
|
|
287
478
|
other.column_names.each do |name|
|
|
288
|
-
next if name
|
|
479
|
+
next if names.include?(name)
|
|
289
480
|
put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], ri))
|
|
290
481
|
end
|
|
482
|
+
# A key that is the left index is the result's index, as a key column
|
|
483
|
+
# is a column of it.
|
|
484
|
+
ij = @index && !@columns.key?(@axis_name) ? names.index(@axis_name) : nil
|
|
291
485
|
new_index = if @index.nil? then nil
|
|
486
|
+
elsif ij then joined_key(ij, parts, lkeys, rkeys, rows)
|
|
292
487
|
elsif identity then @index
|
|
293
488
|
elsif every_left then @index[li]
|
|
294
489
|
else project_rows(@index, li)
|
|
@@ -296,14 +491,58 @@ class CAFrame
|
|
|
296
491
|
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
297
492
|
end
|
|
298
493
|
|
|
494
|
+
# The key +j+ of each row of a key join: the left key on the rows from the
|
|
495
|
+
# left, and on an unmatched right row of a right or outer join the right
|
|
496
|
+
# key, so a key is kept once and is never UNDEF where a row has one.
|
|
497
|
+
private def joined_key(j, parts, lkeys, rkeys, rows)
|
|
498
|
+
li, ri = rows[:li], rows[:ri]
|
|
499
|
+
return lkeys[j] if rows[:identity]
|
|
500
|
+
return lkeys[j][li] if rows[:every_left]
|
|
501
|
+
return project_rows(rkeys[j], ri) if rows[:how] == :right
|
|
502
|
+
common, lc, rc = parts[j]
|
|
503
|
+
extra = rows[:extra]
|
|
504
|
+
code = if extra
|
|
505
|
+
nleft = li.elements - extra.elements
|
|
506
|
+
CArray.concatenate([lc.project(li[0...nleft]), rc.project(extra)])
|
|
507
|
+
else
|
|
508
|
+
lc.project(li)
|
|
509
|
+
end
|
|
510
|
+
# The key takes the attributes both sides show alike, as a
|
|
511
|
+
# concatenation of the two would.
|
|
512
|
+
shared = CArray.__send__(:attrs_shared_by, [lkeys[j], rkeys[j]])
|
|
513
|
+
key = joined_key_kind(common.project(code), lkeys[j], rkeys[j])
|
|
514
|
+
key.tap { |c| c.set_attrs(shared) unless shared.empty? }
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# The kind of a key column holding values of both sides, the same
|
|
518
|
+
# whichever side is the receiver: a Face both sides share stays (a
|
|
519
|
+
# categorical with the labels of both, the left's first), and keys of
|
|
520
|
+
# two different Faces, or of a Face and a plain array, are objects.
|
|
521
|
+
private def joined_key_kind(col, lkey, rkey)
|
|
522
|
+
lface = lkey.class if lkey.face?
|
|
523
|
+
rface = rkey.class if rkey.face?
|
|
524
|
+
return col if lface.nil? && rface.nil?
|
|
525
|
+
if lface == rface
|
|
526
|
+
return col unless lface <= CACategorical
|
|
527
|
+
values = CArray.object(col.elements)
|
|
528
|
+
values[] = col.to_a
|
|
529
|
+
return values.categorize(labels: lkey.labels | rkey.labels)
|
|
530
|
+
end
|
|
531
|
+
return col if !col.face? && col.object?
|
|
532
|
+
CArray.object(col.elements).tap { |o| o[] = col.to_a }
|
|
533
|
+
end
|
|
534
|
+
|
|
299
535
|
# Pairs a side's rows (codes +acode+) with the other side's (+bcode+), in
|
|
300
536
|
# a-row order and, for one a-row, in b-row order. An a-row with no match
|
|
301
537
|
# comes once with no b-row when +keep_alone+. Returns [ai, bi].
|
|
302
538
|
private def join_expand(acode, bcode, k, keep_alone)
|
|
303
539
|
na, nb = acode.elements, bcode.elements
|
|
540
|
+
bcount = bcode.bincount(length: k).int64
|
|
541
|
+
if k == 0 || bcount.max <= 1
|
|
542
|
+
return join_expand_unique(acode, bcode, k, bcount, keep_alone)
|
|
543
|
+
end
|
|
304
544
|
# b's rows by code, each code's rows in b-row order.
|
|
305
545
|
border = CArray.sort_addr(bcode, CArray.int64(nb).seq)
|
|
306
|
-
bcount = bcode.bincount(length: k).int64
|
|
307
546
|
bstart = bcount.cumsum.int64 - bcount
|
|
308
547
|
matches = bcount.project(acode).strip_mask(0)
|
|
309
548
|
reps = keep_alone ? matches.pmax(1) : matches
|
|
@@ -325,6 +564,28 @@ class CAFrame
|
|
|
325
564
|
[ai, bi]
|
|
326
565
|
end
|
|
327
566
|
|
|
567
|
+
# join_expand when each code is on at most one b-row: an a-row pairs
|
|
568
|
+
# with that row or with none, so no a-row repeats. The b-row of each
|
|
569
|
+
# code is looked up once, without the repeat-and-scatter of the
|
|
570
|
+
# general case; the pairs come in the same order.
|
|
571
|
+
private def join_expand_unique(acode, bcode, k, bcount, keep_alone)
|
|
572
|
+
na, nb = acode.elements, bcode.elements
|
|
573
|
+
brow = CArray.int64(k).fill(UNDEF)
|
|
574
|
+
if nb > 0
|
|
575
|
+
present = bcode.is_not_masked
|
|
576
|
+
brow[bcode[present]] = CArray.int64(nb).seq![present]
|
|
577
|
+
end
|
|
578
|
+
if keep_alone
|
|
579
|
+
return [CArray.int64(0), CArray.int64(0)] if na == 0
|
|
580
|
+
[CArray.int64(na).seq!, brow.project(acode)]
|
|
581
|
+
else
|
|
582
|
+
hit = bcount.project(acode).strip_mask(0).gt(0)
|
|
583
|
+
ai = CArray.int64(na).seq![hit].copy
|
|
584
|
+
return [CArray.int64(0), CArray.int64(0)] if ai.elements == 0
|
|
585
|
+
[ai, brow.project(acode.project(ai))]
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
328
589
|
private def project_rows(col, addr)
|
|
329
590
|
return col.project(addr) if col.ndim == 1
|
|
330
591
|
trailing = col.shape[1..]
|
data/lib/carray/frame/records.rb
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
class CAFrame
|
|
18
18
|
# Build a frame from an Array of row Hashes. Column set is the union of keys
|
|
19
|
-
# in first-appearance order; keys are stringified
|
|
19
|
+
# in first-appearance order; keys are stringified, and two keys that
|
|
20
|
+
# stringify to one name (+:temp+ and +"temp"+) raise. +types:+ takes
|
|
20
21
|
# anything +cast+ takes as a map (a name => type map, :infer, or a map with
|
|
21
22
|
# +default:+) and casts the built frame with it, with +on_error:+ as for
|
|
22
23
|
# +cast+.
|
|
@@ -27,6 +28,11 @@ class CAFrame
|
|
|
27
28
|
return new if records.empty?
|
|
28
29
|
|
|
29
30
|
keys = record_key_union(records)
|
|
31
|
+
if keys.any? { |k| k.to_s.empty? }
|
|
32
|
+
raise ArgumentError, "from_records: a key names a column, and a column name can not be empty"
|
|
33
|
+
end
|
|
34
|
+
refuse_repeated_names(keys.map(&:to_s), "from_records", "the keys name the column",
|
|
35
|
+
hint: "a String key and a Symbol key spell it both ways; use one spelling")
|
|
30
36
|
n = records.size
|
|
31
37
|
cols = {}
|
|
32
38
|
keys.each do |key|
|
|
@@ -56,15 +62,27 @@ class CAFrame
|
|
|
56
62
|
end
|
|
57
63
|
private_class_method :record_key_union
|
|
58
64
|
|
|
59
|
-
# Arrange one column's values (Ruby objects, nil for missing) into
|
|
60
|
-
#
|
|
61
|
-
#
|
|
65
|
+
# Arrange one column's values (Ruby objects, nil or UNDEF for missing) into
|
|
66
|
+
# a CArray: the one rule a frame types Ruby values by, wherever it takes
|
|
67
|
+
# them (CAFrame.new and df[name]= given an Array, from_records, the values
|
|
68
|
+
# a group's block or Proc gives). All-Array cells of equal length -> N-D
|
|
69
|
+
# native column; numeric scalars -> native scalar column (missing ->
|
|
70
|
+
# UNDEF); Strings -> a text column (CAString), as from_csv reads text;
|
|
71
|
+
# time and duration elements -> a CATime / CATimedelta column in the finest
|
|
72
|
+
# resolution among them; anything else -> object column.
|
|
62
73
|
def self.build_record_column(values, n)
|
|
74
|
+
values = values.map { |v| UNDEF.equal?(v) ? nil : v }
|
|
63
75
|
present = values.reject(&:nil?)
|
|
64
76
|
return mask_missing(CArray.object(n) { values }) if present.empty?
|
|
65
77
|
|
|
66
78
|
if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
|
|
67
79
|
build_nd_column(values, present, n)
|
|
80
|
+
elsif present.all? { |v| v.is_a?(String) }
|
|
81
|
+
CArray.string(values)
|
|
82
|
+
elsif present.all? { |v| v.is_a?(CATime::Element) }
|
|
83
|
+
CArray.time(values)
|
|
84
|
+
elsif present.all? { |v| v.is_a?(CATimedelta::Element) }
|
|
85
|
+
build_duration_column(values, present, n)
|
|
68
86
|
else
|
|
69
87
|
type = numeric_leaf_type(present)
|
|
70
88
|
col = CArray.object(n) { values }
|
|
@@ -73,6 +91,27 @@ class CAFrame
|
|
|
73
91
|
end
|
|
74
92
|
private_class_method :build_record_column
|
|
75
93
|
|
|
94
|
+
# A CATimedelta column of duration elements, in the finest of their
|
|
95
|
+
# resolutions. Durations that have no common resolution (a month beside a
|
|
96
|
+
# second) stay an object column of the elements.
|
|
97
|
+
def self.build_duration_column(values, present, n)
|
|
98
|
+
unit = begin
|
|
99
|
+
present.map(&:unit).reduce { |a, b| CATimeUnitAlgebra.finer(a, b) }
|
|
100
|
+
rescue ArgumentError
|
|
101
|
+
return mask_missing(CArray.object(n) { values })
|
|
102
|
+
end
|
|
103
|
+
ticks = CArray.int64(n)
|
|
104
|
+
values.each_with_index do |v, i|
|
|
105
|
+
ticks[i] =
|
|
106
|
+
if v.nil? then UNDEF
|
|
107
|
+
elsif v.unit == unit then v.value
|
|
108
|
+
else CA_INT64([v.value]).timedelta(unit: v.unit).to_unit(unit).ticks[0]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
ticks.timedelta(unit: unit)
|
|
112
|
+
end
|
|
113
|
+
private_class_method :build_duration_column
|
|
114
|
+
|
|
76
115
|
# A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
|
|
77
116
|
# writes a masked cell as nil, so nil on the way back in is the only
|
|
78
117
|
# spelling a missing cell has; to_type does this conversion for a numeric
|
|
@@ -98,14 +137,35 @@ class CAFrame
|
|
|
98
137
|
end
|
|
99
138
|
|
|
100
139
|
blank = shape.reverse.inject(nil) { |cell, d| Array.new(d) { cell } }
|
|
101
|
-
|
|
140
|
+
# A missing leaf is nil or UNDEF (a masked cell of a CArray cell reads
|
|
141
|
+
# as UNDEF), the same two spellings a scalar cell has.
|
|
142
|
+
nested = values.map { |v| v.nil? ? blank : undef_to_nil(v.is_a?(CArray) ? v.to_a : v) }
|
|
102
143
|
leaves = nested.flatten.compact
|
|
103
|
-
|
|
144
|
+
# Cells given as numeric CArrays keep their data type, the one the
|
|
145
|
+
# cells have in common, when that type holds every cell's values;
|
|
146
|
+
# otherwise (int64 beside uint64, a large int64 beside float32), and
|
|
147
|
+
# for Ruby values, the column is typed by its leaves.
|
|
148
|
+
common = if present.all? { |v| v.is_a?(CArray) && !v.face? && v.numeric? }
|
|
149
|
+
CArray.result_type(*present)
|
|
150
|
+
end
|
|
151
|
+
type = if common && present.all? { |v| holds_values?(v, common) }
|
|
152
|
+
common
|
|
153
|
+
elsif leaves.empty?
|
|
154
|
+
nil
|
|
155
|
+
else
|
|
156
|
+
numeric_leaf_type(leaves)
|
|
157
|
+
end
|
|
104
158
|
table = CArray.object(n, *shape) { nested }
|
|
105
159
|
type ? table.to_type(type) : mask_missing(table)
|
|
106
160
|
end
|
|
107
161
|
private_class_method :build_nd_column
|
|
108
162
|
|
|
163
|
+
def self.undef_to_nil(v)
|
|
164
|
+
return v.map { |e| undef_to_nil(e) } if v.is_a?(Array)
|
|
165
|
+
UNDEF.equal?(v) ? nil : v
|
|
166
|
+
end
|
|
167
|
+
private_class_method :undef_to_nil
|
|
168
|
+
|
|
109
169
|
# The shape of a rectangular nested Array, or nil when it is not
|
|
110
170
|
# rectangular. A leaf (anything but an Array) has shape [].
|
|
111
171
|
def self.nested_shape(v)
|
|
@@ -120,8 +180,24 @@ class CAFrame
|
|
|
120
180
|
# :int64 if every value is an Integer, :float64 for Integers and Floats,
|
|
121
181
|
# :cmplx128 when Complex values join them, otherwise nil (keep object).
|
|
122
182
|
# Other Numerics (Rational, BigDecimal) stay object: a float column would
|
|
123
|
-
# round them, and a Complex one has room for them only as floats too.
|
|
183
|
+
# round them, and a Complex one has room for them only as floats too. So
|
|
184
|
+
# does an Integer an int64 cannot hold: it is kept as it is, not refused.
|
|
185
|
+
INT64_RANGE = (-2**63..2**63 - 1)
|
|
186
|
+
private_constant :INT64_RANGE
|
|
187
|
+
|
|
188
|
+
# Whether converting the numeric CArray +cell+ to +data_type+ keeps every
|
|
189
|
+
# value (a NaN stays a NaN).
|
|
190
|
+
def self.holds_values?(cell, data_type)
|
|
191
|
+
return true if cell.data_type == data_type
|
|
192
|
+
# coerce: the type CArray.result_type chose for the cells, tried on each
|
|
193
|
+
cell.to_type(data_type).to_a.flatten.zip(cell.to_a.flatten).all? do |a, b|
|
|
194
|
+
a == b || (a.respond_to?(:nan?) && b.respond_to?(:nan?) && a.nan? && b.nan?)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
private_class_method :holds_values?
|
|
198
|
+
|
|
124
199
|
def self.numeric_leaf_type(values)
|
|
200
|
+
return nil if values.any? { |v| v.is_a?(Integer) && !INT64_RANGE.cover?(v) }
|
|
125
201
|
if values.all? { |v| v.is_a?(Integer) }
|
|
126
202
|
:int64
|
|
127
203
|
elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) }
|