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/verbs.rb
CHANGED
|
@@ -9,18 +9,50 @@ class CAFrame
|
|
|
9
9
|
# Add (or replace) a column, returning a new frame — the column set changes,
|
|
10
10
|
# so the result is a different table (memo §3.8). The column is shared by
|
|
11
11
|
# reference; the envelope is cheap. Chain or reassign: +df = df.append(...)+.
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
#
|
|
13
|
+
# The column goes at the end, or where a column of that name already is.
|
|
14
|
+
# +before:+ / +after:+ name a column to put it next to instead; a column
|
|
15
|
+
# of that name already in the frame is moved there.
|
|
16
|
+
#
|
|
17
|
+
# df.append("id", ids, before: df.column_names.first) # leftmost
|
|
18
|
+
def append(name, col, before: nil, after: nil)
|
|
19
|
+
key = column_name_arg(name, "append")
|
|
14
20
|
ca = coerce_column(col)
|
|
15
|
-
|
|
21
|
+
unless before.nil? || after.nil?
|
|
22
|
+
raise ArgumentError, "append: give before: or after:, not both"
|
|
23
|
+
end
|
|
24
|
+
# Checked here: the new frame would take N from whichever witness comes
|
|
25
|
+
# first, and blame the frame's own column for the new one's length. A
|
|
26
|
+
# column that replaces the only one is held to N too, as df[name] = is;
|
|
27
|
+
# only a frame with no column and no index takes N from the new column.
|
|
28
|
+
if (@columns.any? || @index) && ca.shape[0] != @nrow
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"append: column #{key.inspect} has axis-0 length #{ca.shape[0]}, expected #{@nrow}"
|
|
31
|
+
end
|
|
32
|
+
anchor = before.nil? ? after : before
|
|
33
|
+
return rebuild(@columns.merge(key => ca)) if anchor.nil?
|
|
34
|
+
|
|
35
|
+
anchor = column_key(anchor, "append")
|
|
36
|
+
unless @columns.key?(anchor)
|
|
37
|
+
raise KeyError, "append: no column #{anchor.inspect} to put #{key.inspect} #{before.nil? ? "after" : "before"}"
|
|
38
|
+
end
|
|
39
|
+
return rebuild(@columns.merge(key => ca)) if anchor == key
|
|
40
|
+
|
|
41
|
+
cols = {}
|
|
42
|
+
@columns.each do |k, v|
|
|
43
|
+
next if k == key
|
|
44
|
+
cols[key] = ca if !before.nil? && k == anchor
|
|
45
|
+
cols[k] = v
|
|
46
|
+
cols[key] = ca if !after.nil? && k == anchor
|
|
47
|
+
end
|
|
48
|
+
rebuild(cols)
|
|
16
49
|
end
|
|
17
50
|
|
|
18
51
|
# Remove one or more columns, returning a new frame (column set changes,
|
|
19
52
|
# memo §3.8). Column data is untouched and shared with the original.
|
|
20
53
|
def drop(*names)
|
|
21
54
|
cols = @columns.dup
|
|
22
|
-
names.each do |
|
|
23
|
-
key = name.to_s
|
|
55
|
+
column_keys(names, "drop").each do |key|
|
|
24
56
|
raise KeyError, "no column #{key.inspect}" unless cols.key?(key)
|
|
25
57
|
cols.delete(key)
|
|
26
58
|
end
|
|
@@ -33,8 +65,8 @@ class CAFrame
|
|
|
33
65
|
def rename(mapping)
|
|
34
66
|
norm = {}
|
|
35
67
|
mapping.each do |old, new|
|
|
36
|
-
o = old
|
|
37
|
-
n = new
|
|
68
|
+
o = column_key(old, "rename")
|
|
69
|
+
n = column_name_arg(new, "rename")
|
|
38
70
|
raise KeyError, "no column #{o.inspect}" unless @columns.key?(o)
|
|
39
71
|
if n != o && @columns.key?(n)
|
|
40
72
|
raise ArgumentError, "rename target #{n.inspect} already exists"
|
|
@@ -65,7 +97,7 @@ class CAFrame
|
|
|
65
97
|
# @raise [ArgumentError] when the column is not text or 1-D, +into:+ names
|
|
66
98
|
# fewer than two columns or one twice, or names a column already there.
|
|
67
99
|
def split_column(name, sep, into:)
|
|
68
|
-
key = name
|
|
100
|
+
key = column_key(name, "split_column")
|
|
69
101
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
70
102
|
unless string_column?(col) && col.ndim == 1
|
|
71
103
|
raise ArgumentError, "split_column: #{key.inspect} is not a one-dimensional text column"
|
|
@@ -73,11 +105,12 @@ class CAFrame
|
|
|
73
105
|
unless sep.is_a?(String) || sep.is_a?(Regexp)
|
|
74
106
|
raise ArgumentError, "split_column: sep must be a String or a Regexp (got #{sep.inspect})"
|
|
75
107
|
end
|
|
76
|
-
names = Array(into).map(
|
|
77
|
-
if names.size < 2
|
|
108
|
+
names = Array(into).map { |n| column_name_arg(n, "split_column") }
|
|
109
|
+
if names.size < 2
|
|
78
110
|
raise ArgumentError,
|
|
79
|
-
"split_column: into: names two or more columns
|
|
111
|
+
"split_column: into: names two or more columns (got #{names.inspect})"
|
|
80
112
|
end
|
|
113
|
+
CAFrame.__send__(:refuse_repeated_names, names, "split_column", "into: names")
|
|
81
114
|
taken = names & (@columns.keys - [key])
|
|
82
115
|
unless taken.empty?
|
|
83
116
|
raise ArgumentError, "split_column: #{taken.inspect} already a column"
|
|
@@ -101,6 +134,85 @@ class CAFrame
|
|
|
101
134
|
rebuild(rebuilt)
|
|
102
135
|
end
|
|
103
136
|
|
|
137
|
+
# Replace a categorical column by numeric columns, one per category, for a
|
|
138
|
+
# model that takes numbers (see CACategorical#contrast):
|
|
139
|
+
#
|
|
140
|
+
# df.contrast_columns("weather") # "weather_cloud", "weather_rain"
|
|
141
|
+
# df.contrast_columns("weather", base: "rain")
|
|
142
|
+
# df.contrast_columns("weather", full: true) # every category kept
|
|
143
|
+
#
|
|
144
|
+
# The new columns take the column's place and are named after it and the
|
|
145
|
+
# label, "weather_cloud"; +into:+ names them instead. A column that is not
|
|
146
|
+
# a CACategorical is categorized first with its labels in ascending order,
|
|
147
|
+
# so the base -- the first label unless +base:+ says otherwise -- does not
|
|
148
|
+
# depend on the order of the rows. A masked cell is UNDEF in every new
|
|
149
|
+
# column.
|
|
150
|
+
#
|
|
151
|
+
# @param name [String] the column to expand.
|
|
152
|
+
# @param scheme [Symbol] the coding; only :dummy is defined.
|
|
153
|
+
# @param base [Object] the label whose column is left out.
|
|
154
|
+
# @param full [Boolean] keep every column.
|
|
155
|
+
# @param into [Array<String>, nil] the names of the new columns.
|
|
156
|
+
# @return [CAFrame] a new frame; the other columns are shared.
|
|
157
|
+
# @raise [ArgumentError] when the column is not one-dimensional, +into:+
|
|
158
|
+
# names a different number of columns or one twice, or a new name is
|
|
159
|
+
# already a column; and as CACategorical#contrast raises.
|
|
160
|
+
def contrast_columns(name, scheme = :dummy, base: nil, full: false, into: nil)
|
|
161
|
+
key = column_key(name, "contrast_columns")
|
|
162
|
+
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
163
|
+
unless col.ndim == 1
|
|
164
|
+
raise ArgumentError, "contrast_columns: #{key.inspect} is not a one-dimensional column"
|
|
165
|
+
end
|
|
166
|
+
# The labels are sorted as categorize(sort_labels: true) sorts them;
|
|
167
|
+
# labels of an object column that cannot be put in order are named here.
|
|
168
|
+
if col.object? && !col.is_a?(CACategorical)
|
|
169
|
+
col.unique.to_a.sort do |a, b|
|
|
170
|
+
CArray.__send__(:object_order_cmp, a, b) or
|
|
171
|
+
raise ArgumentError,
|
|
172
|
+
"contrast_columns: #{key.inspect} has the labels #{a.inspect} and " \
|
|
173
|
+
"#{b.inspect}, which cannot be put in order; categorize it with labels: first"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
cat = col.is_a?(CACategorical) ? col : col.categorize(sort_labels: true)
|
|
177
|
+
coded = cat.contrast(scheme, base: base, full: full)
|
|
178
|
+
m = coded.shape[1]
|
|
179
|
+
# The base is found among the labels as contrast finds it.
|
|
180
|
+
dropped = full ? nil : (base.nil? ? 0 : cat.__send__(:label_position, base))
|
|
181
|
+
labels = cat.labels.reject.with_index { |_, i| i == dropped }
|
|
182
|
+
names =
|
|
183
|
+
if into
|
|
184
|
+
Array(into).map { |n| column_name_arg(n, "contrast_columns") }
|
|
185
|
+
else
|
|
186
|
+
labels.map { |label| "#{key}_#{label}" }
|
|
187
|
+
end
|
|
188
|
+
unless names.size == m
|
|
189
|
+
raise ArgumentError,
|
|
190
|
+
"contrast_columns: into: names #{names.size} columns for #{m} (got #{names.inspect})"
|
|
191
|
+
end
|
|
192
|
+
if into
|
|
193
|
+
CAFrame.__send__(:refuse_repeated_names, names, "contrast_columns", "into: names")
|
|
194
|
+
else
|
|
195
|
+
CAFrame.__send__(:refuse_repeated_names, names, "contrast_columns", "column names",
|
|
196
|
+
hint: "name them with into:")
|
|
197
|
+
end
|
|
198
|
+
taken = names & (@columns.keys - [key])
|
|
199
|
+
unless taken.empty?
|
|
200
|
+
message = "contrast_columns: #{taken.inspect} already a column"
|
|
201
|
+
message += "; name them with into:" unless into
|
|
202
|
+
raise ArgumentError, message
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
rebuilt = {}
|
|
206
|
+
@columns.each do |k, v|
|
|
207
|
+
if k == key
|
|
208
|
+
names.each_with_index { |nm, j| rebuilt[nm] = coded[nil, j] }
|
|
209
|
+
else
|
|
210
|
+
rebuilt[k] = v
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
rebuild(rebuilt)
|
|
214
|
+
end
|
|
215
|
+
|
|
104
216
|
# Stack several columns into one N-D column: the columns side by side in a
|
|
105
217
|
# file -- a value per month, say -- become one column whose trailing axis
|
|
106
218
|
# (or axes, with +shape:+) runs over them. The columns are given in the
|
|
@@ -177,7 +289,7 @@ class CAFrame
|
|
|
177
289
|
#
|
|
178
290
|
# @return [CAFrame] a new frame; the other columns are shared.
|
|
179
291
|
def unstack_column(name, into: nil)
|
|
180
|
-
key = name
|
|
292
|
+
key = column_key(name, "unstack_column")
|
|
181
293
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
182
294
|
if col.ndim < 2
|
|
183
295
|
raise ArgumentError, "unstack_column: #{key.inspect} is one-dimensional; there is nothing to unstack"
|
|
@@ -198,10 +310,11 @@ class CAFrame
|
|
|
198
310
|
trailing.map { |d| (0...d).to_a }.inject { |a, b| a.product(b).map(&:flatten) }
|
|
199
311
|
end
|
|
200
312
|
names = given || positions.map { |pos| "#{key}_#{pos.join('_')}" }
|
|
201
|
-
unless names.size == count
|
|
313
|
+
unless names.size == count
|
|
202
314
|
raise ArgumentError,
|
|
203
|
-
"unstack_column: into: names #{names.size} columns
|
|
315
|
+
"unstack_column: into: names #{names.size} columns; #{key.inspect} has #{count} positions"
|
|
204
316
|
end
|
|
317
|
+
CAFrame.__send__(:refuse_repeated_names, names, "unstack_column", "into: names")
|
|
205
318
|
taken = names & (@columns.keys - [key])
|
|
206
319
|
unless taken.empty?
|
|
207
320
|
raise ArgumentError, "unstack_column: #{taken.inspect} already a column"
|
|
@@ -218,12 +331,10 @@ class CAFrame
|
|
|
218
331
|
rebuild(rebuilt)
|
|
219
332
|
end
|
|
220
333
|
|
|
221
|
-
# A column name given as an argument: a non-empty
|
|
222
|
-
# column keys are.
|
|
334
|
+
# A column name given as an argument for a column to make: a non-empty
|
|
335
|
+
# String, as the frame's column keys are.
|
|
223
336
|
private def column_name_arg(name, verb)
|
|
224
|
-
|
|
225
|
-
raise TypeError, "#{verb}: a column name is a String (got #{name.inspect})"
|
|
226
|
-
end
|
|
337
|
+
column_key(name, verb)
|
|
227
338
|
raise ArgumentError, "#{verb}: a column name can not be empty" if name.empty?
|
|
228
339
|
name
|
|
229
340
|
end
|
|
@@ -246,7 +357,7 @@ class CAFrame
|
|
|
246
357
|
end
|
|
247
358
|
keys[i..(selection.exclude_end? ? j - 1 : j)]
|
|
248
359
|
when Array
|
|
249
|
-
selection.map(
|
|
360
|
+
selection.map { |n| column_key(n, verb) }.each do |n|
|
|
250
361
|
raise KeyError, "#{verb}: no column #{n.inspect}" unless @columns.key?(n)
|
|
251
362
|
end
|
|
252
363
|
when Regexp
|
|
@@ -258,10 +369,7 @@ class CAFrame
|
|
|
258
369
|
if names.empty?
|
|
259
370
|
raise ArgumentError, "#{verb}: #{selection.inspect} selects no column"
|
|
260
371
|
end
|
|
261
|
-
|
|
262
|
-
raise ArgumentError, "#{verb}: #{selection.inspect} names a column more than once"
|
|
263
|
-
end
|
|
264
|
-
names
|
|
372
|
+
CAFrame.__send__(:refuse_repeated_names, names, verb)
|
|
265
373
|
end
|
|
266
374
|
|
|
267
375
|
# Mask cells of a column that equal +value+ (sentinel -> mask, memo §11.4).
|
|
@@ -270,7 +378,7 @@ class CAFrame
|
|
|
270
378
|
# raises — recode by rebuilding and rebinding instead.
|
|
271
379
|
def mask_eq(name, value)
|
|
272
380
|
refuse_if_frozen
|
|
273
|
-
self[name][:eq, value] = UNDEF
|
|
381
|
+
self[column_key(name, "mask_eq")][:eq, value] = UNDEF
|
|
274
382
|
self
|
|
275
383
|
end
|
|
276
384
|
|
|
@@ -281,7 +389,10 @@ class CAFrame
|
|
|
281
389
|
# column cast to :time is parsed into a CATime column, in the finest unit
|
|
282
390
|
# its text shows (:D for dates alone, :s with a time of day, :ms / :us /
|
|
283
391
|
# :ns for fractions of a second); parse_to_time takes a format and a
|
|
284
|
-
# unit.
|
|
392
|
+
# unit. A column cast to :categorical becomes a CACategorical of its
|
|
393
|
+
# values, the labels in the order they first appear (categorize); a text
|
|
394
|
+
# column read by from_csv with types: gets there without a String made per
|
|
395
|
+
# cell. Other targets use to_type.
|
|
285
396
|
# The call shapes are told apart by the fact that column names are always
|
|
286
397
|
# Strings and types always Symbols (§3.7):
|
|
287
398
|
#
|
|
@@ -304,7 +415,7 @@ class CAFrame
|
|
|
304
415
|
# cast(default: :float64, "station" => nil) # every other column to float64
|
|
305
416
|
#
|
|
306
417
|
# A name that is not a column raises KeyError, and a Symbol key other than
|
|
307
|
-
# +:default+ raises
|
|
418
|
+
# +:default+ raises TypeError; both before any column is read. Every
|
|
308
419
|
# column is read before any is rebound, so a raise leaves the frame as it
|
|
309
420
|
# was. Returns self so calls chain.
|
|
310
421
|
#
|
|
@@ -332,6 +443,13 @@ class CAFrame
|
|
|
332
443
|
# keywords next to on_error:.
|
|
333
444
|
unless map.empty?
|
|
334
445
|
unless name_or_map.nil?
|
|
446
|
+
# Symbol keys next to a name or a map are a misspelt keyword
|
|
447
|
+
# (on_erorr:), not a second map; :default is a map key.
|
|
448
|
+
if map.keys.all?(Symbol) && !map.key?(:default)
|
|
449
|
+
raise ArgumentError,
|
|
450
|
+
"#{label}: unknown keyword#{map.size > 1 ? 's' : ''}: " \
|
|
451
|
+
"#{map.keys.map(&:inspect).join(', ')} (the keyword is on_error:)"
|
|
452
|
+
end
|
|
335
453
|
raise ArgumentError, "#{label} takes a name and a type, or a map, not both"
|
|
336
454
|
end
|
|
337
455
|
name_or_map = map
|
|
@@ -353,11 +471,15 @@ class CAFrame
|
|
|
353
471
|
end
|
|
354
472
|
resolve_cast_map(name_or_map, label)
|
|
355
473
|
else
|
|
356
|
-
{ name_or_map
|
|
474
|
+
{ column_key(name_or_map, label) => type }
|
|
357
475
|
end
|
|
358
476
|
# Read every column before rebinding any, so a raise leaves the frame as
|
|
359
477
|
# it was.
|
|
360
|
-
|
|
478
|
+
# A conversion keeps what the column is, so the column's attributes
|
|
479
|
+
# come along whatever the target type.
|
|
480
|
+
casted = types.map do |key, t|
|
|
481
|
+
[key, @columns[key].__send__(:carry_attrs, cast_column(key, t, on_error))]
|
|
482
|
+
end
|
|
361
483
|
casted.each { |key, col| @columns[key] = col }
|
|
362
484
|
self
|
|
363
485
|
end
|
|
@@ -372,10 +494,9 @@ class CAFrame
|
|
|
372
494
|
if key == :default
|
|
373
495
|
default = t
|
|
374
496
|
elsif key.is_a?(Symbol)
|
|
375
|
-
|
|
376
|
-
"as keys (got #{key.inspect})"
|
|
497
|
+
CAFrame.__send__(:column_key, key, label, hint: ":default is the only Symbol key")
|
|
377
498
|
else
|
|
378
|
-
Array(key).each { |name| named[name
|
|
499
|
+
Array(key).each { |name| named[column_key(name, label)] = t }
|
|
379
500
|
end
|
|
380
501
|
end
|
|
381
502
|
named.each_key do |name|
|
|
@@ -396,6 +517,7 @@ class CAFrame
|
|
|
396
517
|
# The targets a text column is read as numbers for.
|
|
397
518
|
CAST_NUMBER_TYPES = %w[int8 int16 int32 int64 uint8 uint16 uint32 uint64
|
|
398
519
|
float32 float64 cmplx64 cmplx128].freeze
|
|
520
|
+
Ractor.make_shareable(CAST_NUMBER_TYPES)
|
|
399
521
|
private_constant :CAST_ON_ERROR, :CAST_NUMBER_TYPES
|
|
400
522
|
|
|
401
523
|
# The types the text columns read as, as a map +cast+ takes:
|
|
@@ -470,9 +592,10 @@ class CAFrame
|
|
|
470
592
|
# For those, +format+ is a strptime format, :infer to find the one format
|
|
471
593
|
# the column is written in (see infer_time_format; a cell not in it
|
|
472
594
|
# raises CAFrame::UnreadableColumn, whatever +on_error+ says), or :mixed
|
|
473
|
-
# to guess at each cell (much slower). +unit+ is the storage resolution
|
|
474
|
-
#
|
|
475
|
-
#
|
|
595
|
+
# to guess at each cell (much slower). +unit+ is the storage resolution,
|
|
596
|
+
# by default the finest the text shows -- with a format, the finest field
|
|
597
|
+
# the format writes ("%d/%m/%Y" :D, "%H:%M" :s, "%L" :ms), so a fraction
|
|
598
|
+
# of a second is not cut off. Masked / nil and unparseable cells become
|
|
476
599
|
# UNDEF; +on_error:+ :warn / :raise reports the unparseable ones as +cast+
|
|
477
600
|
# does. Make it the index with +set_index+ afterward.
|
|
478
601
|
# +cast(name => :time)+ is the call without a format.
|
|
@@ -486,14 +609,14 @@ class CAFrame
|
|
|
486
609
|
raise ArgumentError,
|
|
487
610
|
"on_error: must be :mask, :warn or :raise (got #{on_error.inspect})"
|
|
488
611
|
end
|
|
489
|
-
key = name
|
|
612
|
+
key = column_key(name, "parse_to_time")
|
|
490
613
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
491
614
|
unless string_column?(col)
|
|
492
615
|
raise ArgumentError,
|
|
493
616
|
"parse_to_time needs a string column (object / CAString / " \
|
|
494
617
|
"CAConstString / CAFixlenString); #{key.inspect} is #{col.data_type}"
|
|
495
618
|
end
|
|
496
|
-
@columns[key] = parse_time_column(key, col, format, unit, on_error)
|
|
619
|
+
@columns[key] = col.__send__(:carry_attrs, parse_time_column(key, col, format, unit, on_error))
|
|
497
620
|
self
|
|
498
621
|
end
|
|
499
622
|
|
|
@@ -508,7 +631,7 @@ class CAFrame
|
|
|
508
631
|
#
|
|
509
632
|
# df.infer_time_format("date") # => "%d/%m/%Y"
|
|
510
633
|
def infer_time_format(name)
|
|
511
|
-
key = name
|
|
634
|
+
key = column_key(name, "infer_time_format")
|
|
512
635
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
513
636
|
unless string_column?(col)
|
|
514
637
|
raise ArgumentError, "column #{key.inspect} is #{col.data_type}, not text"
|
|
@@ -539,12 +662,12 @@ class CAFrame
|
|
|
539
662
|
# df.to_time("time", CATime::Grid.parse("days since 1980-01-01 12:00"))
|
|
540
663
|
def to_time(name, grid = nil, unit: :s, epoch: nil)
|
|
541
664
|
refuse_if_frozen
|
|
542
|
-
key = name
|
|
665
|
+
key = column_key(name, "to_time")
|
|
543
666
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
544
667
|
raw = integer_serial_column(col, key)
|
|
545
668
|
grid = unit if unit.is_a?(CATime::Grid)
|
|
546
669
|
if grid.is_a?(CATime::Grid)
|
|
547
|
-
@columns[key] = grid.at(raw)
|
|
670
|
+
@columns[key] = col.__send__(:carry_attrs, grid.at(raw))
|
|
548
671
|
return self
|
|
549
672
|
end
|
|
550
673
|
unless grid.nil?
|
|
@@ -552,9 +675,10 @@ class CAFrame
|
|
|
552
675
|
"the positional argument must be a CATime::Grid (got #{grid.class})"
|
|
553
676
|
end
|
|
554
677
|
if epoch
|
|
555
|
-
|
|
678
|
+
# An Integer epoch is an instant in Unix seconds, whatever the unit.
|
|
679
|
+
raw = raw + CATimeLiteral.to_time_array(epoch, CATime::Resolution.parse(unit), nil, :raise).ticks[0]
|
|
556
680
|
end
|
|
557
|
-
@columns[key] = raw.time(unit: unit)
|
|
681
|
+
@columns[key] = col.__send__(:carry_attrs, raw.time(unit: unit))
|
|
558
682
|
self
|
|
559
683
|
end
|
|
560
684
|
|
|
@@ -576,7 +700,7 @@ class CAFrame
|
|
|
576
700
|
# `df.append(name, df[name].strip_mask(method: :forward))`.
|
|
577
701
|
def fill(name, method_or_value)
|
|
578
702
|
refuse_if_frozen
|
|
579
|
-
key = name
|
|
703
|
+
key = column_key(name, "fill")
|
|
580
704
|
raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
|
|
581
705
|
col = @columns[key]
|
|
582
706
|
# An N-D column is filled down the rows: each position on its trailing
|
|
@@ -599,7 +723,7 @@ class CAFrame
|
|
|
599
723
|
private def fill_linear_column(key, col)
|
|
600
724
|
# A Face column interpolates through its own linear_fetch (a time
|
|
601
725
|
# column's keeps the Face and its unit), as the core gap fill does.
|
|
602
|
-
unless col.__send__(:
|
|
726
|
+
unless col.__send__(:interpolable?)
|
|
603
727
|
raise ArgumentError,
|
|
604
728
|
"fill(#{key.inspect}, :linear): numeric or time column required " \
|
|
605
729
|
"(got #{col.data_type_name})"
|
|
@@ -625,7 +749,7 @@ class CAFrame
|
|
|
625
749
|
return
|
|
626
750
|
end
|
|
627
751
|
# The same fill as the core, with the index as x.
|
|
628
|
-
col[] = col.send(:
|
|
752
|
+
col[] = col.send(:linear_fill_along, @index) # write-through
|
|
629
753
|
end
|
|
630
754
|
|
|
631
755
|
# Text cells are data, not Ruby literals: an object column cast to a
|
|
@@ -635,6 +759,9 @@ class CAFrame
|
|
|
635
759
|
key = name.to_s
|
|
636
760
|
col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
|
|
637
761
|
return cast_time_column(key, col, on_error) if type == :time
|
|
762
|
+
if type == :categorical
|
|
763
|
+
return col.is_a?(CACategorical) ? col : col.categorize
|
|
764
|
+
end
|
|
638
765
|
if CArray.data_type_name(type) == "boolean"
|
|
639
766
|
return read_boolean_text(key, col, on_error) if string_column?(col)
|
|
640
767
|
if !col.face? && INTEGER_TYPES.include?(col.data_type)
|
|
@@ -729,9 +856,12 @@ class CAFrame
|
|
|
729
856
|
private def parse_time_column(key, col, format, unit, on_error)
|
|
730
857
|
case format
|
|
731
858
|
when nil then read_time_text(key, col, unit, on_error)
|
|
732
|
-
when String
|
|
859
|
+
when String
|
|
860
|
+
parse_time_by_format(key, col, format, unit || CATimeLiteral.format_unit(format), on_error)
|
|
733
861
|
when :infer then parse_time_inferred(key, col, unit)
|
|
734
|
-
when :mixed
|
|
862
|
+
when :mixed
|
|
863
|
+
unit ||= CATimeLiteral.unit_shown(time_text_of(col), nil)
|
|
864
|
+
parse_time_by_format(key, col, nil, unit, on_error)
|
|
735
865
|
else
|
|
736
866
|
raise ArgumentError,
|
|
737
867
|
"time format must be a strptime String, :infer or :mixed " \
|
|
@@ -854,9 +984,18 @@ class CAFrame
|
|
|
854
984
|
|
|
855
985
|
# nil, UNDEF and a blank String are missing values.
|
|
856
986
|
# Every present cell is true or false in some case, and one is present.
|
|
987
|
+
# The first present cell settles most columns, so the distinct words are
|
|
988
|
+
# gathered only when it is one.
|
|
857
989
|
private def boolean_word_column?(col)
|
|
990
|
+
first = nil
|
|
991
|
+
col.each { |v| unless missing_text?(v) then first = v; break end }
|
|
992
|
+
return false unless first && boolean_word?(first)
|
|
858
993
|
words = time_text_of(col).unique.to_a.reject { |v| missing_text?(v) }
|
|
859
|
-
|
|
994
|
+
words.all? { |v| boolean_word?(v) }
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
private def boolean_word?(cell)
|
|
998
|
+
%w[true false].include?(cell.to_s.strip.downcase)
|
|
860
999
|
end
|
|
861
1000
|
|
|
862
1001
|
private def missing_text?(cell)
|
|
@@ -925,6 +1064,7 @@ class CAFrame
|
|
|
925
1064
|
@columns.keys.each_with_index do |key, i|
|
|
926
1065
|
coerced = promoted[i]
|
|
927
1066
|
next if coerced.equal?(cols[i])
|
|
1067
|
+
# coerce: the type promote_list chose
|
|
928
1068
|
@columns[key] = cols[i].to_type(coerced.data_type)
|
|
929
1069
|
end
|
|
930
1070
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# CAFrame windows bounded by a width of a key column.
|
|
2
|
+
|
|
3
|
+
class CAFrame
|
|
4
|
+
# Moving windows bounded by a width of the key column +on+ (a time, a
|
|
5
|
+
# duration or a number) instead of a count of rows: the window of a row
|
|
6
|
+
# whose key is t holds the rows whose key lies between t - before and
|
|
7
|
+
# t + after. +exclude:+ leaves out the lower end (default), the upper end,
|
|
8
|
+
# both or neither, as CArray#is_between does. +by:+ names columns that
|
|
9
|
+
# must match exactly, so each window stays within one station; +interval:+
|
|
10
|
+
# gives the key's grid step for WindowedFrame#is_complete. The rows need
|
|
11
|
+
# not be sorted; the answers come in row order. A row whose key is
|
|
12
|
+
# masked, NaN or infinite is in no window and has no window.
|
|
13
|
+
#
|
|
14
|
+
# w = df.windows(on: "time", before: "1 hour", by: "station")
|
|
15
|
+
# w.mean("temp") # => CArray, one value per row
|
|
16
|
+
# w.mean # => frame of the windowed mean of every column
|
|
17
|
+
# w.elements # => the number of rows in each window
|
|
18
|
+
#
|
|
19
|
+
# Given +at:+, the windows sit at those points of the key instead of at
|
|
20
|
+
# the rows, and every answer is one per point: a frame of the reductions
|
|
21
|
+
# has a row per point, the points as its index and the on: column's name
|
|
22
|
+
# as its row axis name. The points need not be sorted or on the rows'
|
|
23
|
+
# keys. A grid of points gives windows on a grid; a window with no row
|
|
24
|
+
# answers as an empty reduction does (+mean+ UNDEF, +count+ 0). +at:+
|
|
25
|
+
# cannot be given with +by:+.
|
|
26
|
+
#
|
|
27
|
+
# grid = CArray.time_range(t0, t1, unit: "1 hour")
|
|
28
|
+
# df.windows(on: "time", at: grid, after: "3 hours", exclude: :upper).mean
|
|
29
|
+
# # => a frame with a row per hour, the mean over [t, t + 3 hours)
|
|
30
|
+
#
|
|
31
|
+
# @return [WindowedFrame]
|
|
32
|
+
def windows (on:, before: nil, after: nil, exclude: :lower, by: nil, interval: nil, at: nil)
|
|
33
|
+
on_name = column_key(on, "windows")
|
|
34
|
+
key = key_column(on_name, "windows")
|
|
35
|
+
by_names = by.nil? ? [] : column_keys(by.is_a?(Array) ? by : [by], "windows")
|
|
36
|
+
by_cols = by_names.map { |b| key_column(b, "windows") }
|
|
37
|
+
# The points are taken as they are now: a later write to the array
|
|
38
|
+
# given changes neither the windows nor a result's index.
|
|
39
|
+
at = at.copy if at.is_a?(CArray)
|
|
40
|
+
plan = CAKeyWindowIterator.new(CArray.int64(nrow).seq, on: key,
|
|
41
|
+
before: before, after: after, exclude: exclude,
|
|
42
|
+
by: by_cols.empty? ? nil : by_cols,
|
|
43
|
+
interval: interval, at: at)
|
|
44
|
+
WindowedFrame.new(self, plan, [on_name, *by_names], at: at)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The windows of a CAFrame (CAFrame#windows): the reductions over each row's
|
|
49
|
+
# window, one answer per row, and what each window covered.
|
|
50
|
+
class WindowedFrame
|
|
51
|
+
include CAFrame::ColumnVerbs
|
|
52
|
+
|
|
53
|
+
def initialize (frame, plan, key_names, at: nil)
|
|
54
|
+
@frame = frame
|
|
55
|
+
@plan = plan
|
|
56
|
+
@key_names = key_names
|
|
57
|
+
@at = at
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @overload [](name)
|
|
61
|
+
# Returns the CAKeyWindowIterator of one column.
|
|
62
|
+
# @return [CAKeyWindowIterator]
|
|
63
|
+
def [] (name)
|
|
64
|
+
@plan.for_value(@frame[@frame.__send__(:column_key, name, "windows")])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# What each window covered, one value per row: see CAKeyWindowIterator.
|
|
68
|
+
[:first_addr, :last_addr, :first_key, :last_key, :elements, :is_complete].each do |m|
|
|
69
|
+
define_method(m, &Ractor.make_shareable(proc { @plan.public_send(m) }))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @overload bounds
|
|
73
|
+
# Returns the range each window was asked to cover, as two arrays of the
|
|
74
|
+
# key's kind.
|
|
75
|
+
# @return [Array(CArray, CArray)]
|
|
76
|
+
def bounds
|
|
77
|
+
@plan.bounds
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The windowed reductions. Without names, a frame of every column whose
|
|
81
|
+
# type has the reduction (the on: and by: columns left out), on the
|
|
82
|
+
# frame's rows and index; given names, the CArray (one) or the Array of
|
|
83
|
+
# them (several), as +df[...]+ returns columns.
|
|
84
|
+
CArray.send(:define_named_methods, self,
|
|
85
|
+
[:sum, :mean, :min, :max, :prod, :accumulate, :median,
|
|
86
|
+
:variance, :stddev, :variancep, :stddevp], :window_reduction)
|
|
87
|
+
|
|
88
|
+
CArray.send(:define_named_methods, self,
|
|
89
|
+
[:first, :last, :nunique, :count, :all, :any], :window_value)
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def window_reduction (reduction, *names, min_count: nil, fill_value: nil)
|
|
94
|
+
window_reduce(reduction, names, min_count: min_count, fill_value: fill_value)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def window_value (reduction, *names)
|
|
98
|
+
window_reduce(reduction, names)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# An N-D column (each row a profile) is reduced along the rows, each
|
|
102
|
+
# window keeping the trailing shape, as a group reduces it.
|
|
103
|
+
def window_reduce (reduction, names, **keywords)
|
|
104
|
+
keywords = keywords.compact
|
|
105
|
+
out = over_named_columns(reduction, names, @key_names) do |col|
|
|
106
|
+
@plan.for_value(col).public_send(reduction, **keywords)
|
|
107
|
+
end
|
|
108
|
+
return out unless names.empty?
|
|
109
|
+
if @at
|
|
110
|
+
# Each result has an index of its own, as a group's result has.
|
|
111
|
+
key = @frame.__send__(:key_column, @key_names.first, "windows")
|
|
112
|
+
CAFrame.new(out, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
|
|
113
|
+
else
|
|
114
|
+
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/carray/frame.rb
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
# devel/MEMO_DATAFRAME_ON_CARRAY.md for the design.
|
|
7
7
|
|
|
8
8
|
require 'carray/frame/frame'
|
|
9
|
+
require 'carray/frame/column_verbs'
|
|
9
10
|
require 'carray/frame/verbs'
|
|
10
11
|
require 'carray/frame/convert'
|
|
11
12
|
require 'carray/frame/io'
|
|
12
13
|
require 'carray/frame/records'
|
|
13
14
|
require 'carray/frame/group'
|
|
14
15
|
require 'carray/frame/join'
|
|
16
|
+
require 'carray/frame/windows'
|
|
15
17
|
require 'carray/frame/concat'
|
|
16
18
|
require 'carray/frame/sort'
|
|
17
19
|
require 'carray/frame/reshape'
|
data/lib/carray/fuse_source.rb
CHANGED
|
@@ -44,13 +44,20 @@ class CArray
|
|
|
44
44
|
# every time the same block is called and a different one for each
|
|
45
45
|
# block, even two on one line. Where Ruby has weak keys, a block whose
|
|
46
46
|
# code is unloaded takes its entry with it.
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
def self.new_sites
|
|
48
|
+
defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new
|
|
49
|
+
: {}.compare_by_identity
|
|
50
|
+
end
|
|
51
|
+
SITES = new_sites
|
|
49
52
|
|
|
53
|
+
# A Ractor other than the main one keeps its own table, and evaluates
|
|
54
|
+
# every block in its frame: the empty scope a lambda is compiled in is
|
|
55
|
+
# the top of a file, which only the main Ractor has.
|
|
50
56
|
def self.compiled (block)
|
|
51
57
|
site = RubyVM::InstructionSequence.of(block)
|
|
52
58
|
return compile(block) unless site
|
|
53
|
-
SITES
|
|
59
|
+
sites = Ractor.current == Ractor.main ? SITES : (Ractor.current[:carray_fuse_sites] ||= new_sites)
|
|
60
|
+
sites[site] || (sites[site] = compile(block))
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
# The block's free locals, handed to a lambda that was compiled without
|
|
@@ -88,7 +95,7 @@ class CArray
|
|
|
88
95
|
file, line = block.source_location
|
|
89
96
|
scope = block.binding
|
|
90
97
|
locals, needs_frame = free_locals(body, scope.local_variables)
|
|
91
|
-
return InFrame.new(text, file, line) if needs_frame
|
|
98
|
+
return InFrame.new(text, file, line) if needs_frame || Ractor.current != Ractor.main
|
|
92
99
|
Compiled.new(locals, compile_lambda(text, locals, scope, file, line))
|
|
93
100
|
end
|
|
94
101
|
|