carray 3.0.2 → 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 +568 -0
- data/README.md +6 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +220 -0
- data/ext/ca_axis_group.c +201 -123
- data/ext/ca_binop_dispatch.c +46 -52
- data/ext/ca_binop_dispatch.h +1 -1
- data/ext/ca_categorical_iterator.c +249 -183
- data/ext/ca_compare.c +7 -12
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +38 -23
- data/ext/ca_kernel_iterator.c +1765 -2076
- data/ext/ca_kernel_iterator.h +584 -566
- data/ext/ca_monop_dispatch.c +86 -63
- data/ext/ca_monop_dispatch.h +5 -0
- data/ext/ca_obj_array.c +223 -56
- data/ext/ca_obj_bincmp.c +172 -138
- data/ext/ca_obj_binop.c +116 -230
- data/ext/ca_obj_bitarray.c +40 -26
- data/ext/ca_obj_bitfield.c +100 -68
- data/ext/ca_obj_block.c +19 -9
- data/ext/ca_obj_byte_swap.c +45 -46
- data/ext/ca_obj_const_string.c +269 -44
- data/ext/ca_obj_face.c +612 -89
- data/ext/ca_obj_face.h +111 -47
- data/ext/ca_obj_fake.c +36 -79
- data/ext/ca_obj_farray.c +7 -3
- data/ext/ca_obj_field.c +33 -8
- data/ext/ca_obj_fixlen_string.c +23 -24
- data/ext/ca_obj_grid.c +541 -210
- data/ext/ca_obj_meld.c +331 -111
- data/ext/ca_obj_moncmp.c +80 -65
- data/ext/ca_obj_monop.c +177 -149
- data/ext/ca_obj_object.c +664 -248
- data/ext/ca_obj_record.c +63 -23
- data/ext/ca_obj_reduce.c +18 -16
- data/ext/ca_obj_refer.c +140 -58
- data/ext/ca_obj_remap.c +85 -24
- data/ext/ca_obj_repeat.c +24 -9
- data/ext/ca_obj_roll.c +121 -16
- data/ext/ca_obj_select.c +61 -58
- data/ext/ca_obj_select_axis.c +73 -107
- data/ext/ca_obj_shift.c +32 -102
- data/ext/ca_obj_stack.c +372 -122
- data/ext/ca_obj_stride.c +348 -140
- data/ext/ca_obj_string.c +88 -22
- data/ext/ca_obj_tile.c +38 -21
- data/ext/ca_obj_time.c +45 -31
- data/ext/ca_obj_timedelta.c +43 -29
- data/ext/ca_obj_transpose.c +17 -6
- data/ext/ca_obj_triop.c +125 -141
- data/ext/ca_obj_window.c +330 -44
- data/ext/ca_op_ipower.c +24 -11
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +104 -81
- data/ext/ca_sweep_engine.h +7 -20
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +542 -0
- data/ext/carray.h +241 -92
- data/ext/carray_access.c +608 -276
- data/ext/carray_address_basis.c +494 -97
- data/ext/carray_attribute.c +129 -4
- data/ext/carray_bincount.c +39 -7
- data/ext/carray_broadcast.c +56 -19
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_call_cfunc.c +108 -28
- data/ext/carray_cast.c +1576 -223
- data/ext/carray_cast_func.rb +77 -24
- data/ext/carray_class.c +4 -3
- data/ext/carray_conversion.c +208 -75
- data/ext/carray_copy.c +82 -24
- data/ext/carray_core.c +646 -162
- data/ext/carray_count.c +171 -88
- data/ext/carray_element.c +94 -60
- data/ext/carray_factorize.c +670 -324
- data/ext/carray_generate.c +260 -83
- data/ext/carray_histogram.c +209 -106
- data/ext/carray_hold.c +14 -16
- data/ext/carray_index_classifier.c +422 -243
- data/ext/carray_internal.h +692 -3
- data/ext/carray_kernels_bincmp.c +7 -0
- data/ext/carray_kernels_binop.c +487 -120
- data/ext/carray_kernels_init.c +653 -50
- data/ext/carray_kernels_moncmp.c +15 -8
- data/ext/carray_kernels_monop.c +99 -43
- data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
- data/ext/carray_kernels_reduce_boolean.c +112 -36
- data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
- data/ext/carray_kernels_reduce_extreme.c +3032 -2892
- data/ext/carray_kernels_reduce_variance.c +439 -500
- data/ext/carray_kernels_scan.c +1097 -488
- data/ext/carray_kernels_search.c +3787 -2176
- data/ext/carray_kernels_sort.c +1200 -920
- data/ext/carray_kernels_triop.c +479 -7
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +967 -153
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +910 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +299 -100
- data/ext/carray_memory_view.c +872 -758
- data/ext/carray_operator.c +1171 -364
- data/ext/carray_order.c +505 -133
- data/ext/carray_partition.c +112 -65
- data/ext/carray_random.c +327 -239
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +346 -78
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +249 -86
- data/ext/carray_sort.c +224 -227
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +118 -39
- data/ext/carray_test.c +193 -69
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +579 -113
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +19 -4
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1202 -1014
- data/ext/ruby_carray.c +17 -5
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +160 -17
- data/lib/carray/autoload_carray.rb +23 -27
- data/lib/carray/autoload_method_extension.rb +20 -5
- data/lib/carray/axis_group.rb +360 -63
- data/lib/carray/basics.rb +162 -28
- data/lib/carray/bincount_nd.rb +18 -2
- data/lib/carray/block_iterator.rb +231 -58
- data/lib/carray/boolean_reduce.rb +34 -42
- data/lib/carray/categorical.rb +242 -52
- data/lib/carray/categorical_iterator.rb +437 -827
- data/lib/carray/complex.rb +41 -34
- data/lib/carray/conditional.rb +150 -14
- data/lib/carray/const_string.rb +59 -4
- 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/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +82 -7
- data/lib/carray/frame/csv_parser.rb +688 -87
- data/lib/carray/frame/describe.rb +80 -0
- data/lib/carray/frame/frame.rb +369 -45
- data/lib/carray/frame/group.rb +631 -90
- data/lib/carray/frame/io.rb +506 -79
- data/lib/carray/frame/join.rb +411 -62
- data/lib/carray/frame/records.rb +116 -19
- data/lib/carray/frame/reshape.rb +455 -0
- data/lib/carray/frame/sort.rb +30 -18
- data/lib/carray/frame/verbs.rb +868 -61
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +4 -0
- data/lib/carray/fuse_source.rb +118 -2
- data/lib/carray/fusion.rb +257 -42
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +54 -13
- data/lib/carray/iterator.rb +99 -9
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +216 -193
- data/lib/carray/mask_gap_fill.rb +112 -92
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +52 -21
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +18 -2
- data/lib/carray/methods/bit_string.rb +15 -4
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +18 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +25 -5
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +140 -10
- data/lib/carray/methods/join.rb +2 -7
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +182 -45
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/repeat.rb +1 -20
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +11 -2
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +65 -17
- data/lib/carray/segment_iterator.rb +1221 -0
- data/lib/carray/serialize.rb +214 -30
- data/lib/carray/slab_iterator.rb +101 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +12 -4
- data/lib/carray/struct.rb +137 -58
- data/lib/carray/struct_builder.rb +124 -17
- data/lib/carray/time.rb +1222 -186
- data/lib/carray/window_iterator.rb +242 -81
- data/lib/carray.rb +9 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +5 -1
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +5 -4
- data/yard-stubs/ca_obj_roll.rb +2 -3
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +9 -5
- data/yard-stubs/carray_access.rb +11 -5
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +88 -43
- data/yard-stubs/carray_conversion.rb +15 -2
- data/yard-stubs/carray_copy.rb +12 -5
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +60 -12
- data/yard-stubs/carray_math.rb +52 -14
- data/yard-stubs/carray_median_percentile.rb +15 -8
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +61 -29
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +21 -9
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +10 -2
- data/yard-stubs/carray_sort.rb +49 -8
- data/yard-stubs/carray_test.rb +15 -3
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -5
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_kernels_map.c +0 -3467
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
data/lib/carray/frame/frame.rb
CHANGED
|
@@ -11,13 +11,38 @@
|
|
|
11
11
|
# storage, and +copy+ is the way to an independent frame.
|
|
12
12
|
|
|
13
13
|
class CAFrame
|
|
14
|
+
# Raised when the contents of a column cannot be read as asked: a cell
|
|
15
|
+
# that does not read as the type a +cast+ (or +from_csv+'s +types:+) asks
|
|
16
|
+
# for under on_error: :raise, a cell +parse_to_time+ cannot read, a column
|
|
17
|
+
# whose time format +infer_time_format+ / parse_to_time(name, :infer)
|
|
18
|
+
# cannot find, or a float column +to_time+ cannot read as whole counts.
|
|
19
|
+
# The message names the column and the cell. The arguments were fine; the
|
|
20
|
+
# column's contents are what did not read.
|
|
21
|
+
class UnreadableColumn < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Raised when +infer_time_format+ / parse_to_time(name, :infer) finds more
|
|
24
|
+
# than one format every cell fits ("01/02/2024" and "03/04/2024" are both
|
|
25
|
+
# day-first and month-first). The text reads in each of them, so it is
|
|
26
|
+
# not an UnreadableColumn; +formats+ gives the candidates, to choose one and
|
|
27
|
+
# pass it as the format.
|
|
28
|
+
class AmbiguousTimeFormat < StandardError
|
|
29
|
+
attr_reader :formats
|
|
30
|
+
|
|
31
|
+
def initialize(message = nil, formats: [])
|
|
32
|
+
@formats = formats
|
|
33
|
+
super(message)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
14
37
|
# Integer data_types that select rows positionally when used as a df[] key.
|
|
15
38
|
INTEGER_TYPES = [:int8, :int16, :int32, :int64,
|
|
16
39
|
:uint8, :uint16, :uint32, :uint64].freeze
|
|
40
|
+
Ractor.make_shareable(INTEGER_TYPES)
|
|
17
41
|
private_constant :INTEGER_TYPES
|
|
18
42
|
|
|
19
43
|
# @!visibility private
|
|
20
44
|
DEFAULT_AXIS_NAME = "row"
|
|
45
|
+
Ractor.make_shareable(DEFAULT_AXIS_NAME)
|
|
21
46
|
|
|
22
47
|
# Build a frame from a Hash of +name => column+. Columns may be CArrays or
|
|
23
48
|
# anything that answers +to_ca+ (Ruby Array, lazy view). All columns must
|
|
@@ -28,22 +53,34 @@ class CAFrame
|
|
|
28
53
|
# y)+). +:axis_name+ / +:index+ are control options; any remaining
|
|
29
54
|
# (string-keyed) options are treated as columns.
|
|
30
55
|
#
|
|
31
|
-
# Column names are
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
56
|
+
# Column names are Strings, in an explicit column hash as in inline pairs.
|
|
57
|
+
# A stray Symbol among the keywords is a mistyped control option
|
|
58
|
+
# (+:axis_name+ / +:index+), not a column, and is named as such.
|
|
59
|
+
#
|
|
60
|
+
# +types:+ casts the built frame as +from_records+ does: it takes anything
|
|
61
|
+
# +cast+ takes as a map (a name => type map, :infer, or a map with
|
|
62
|
+
# +default:+), with +on_error:+ as for +cast+. A cast column is a new array,
|
|
63
|
+
# so a CArray given for it is no longer shared with the frame.
|
|
35
64
|
def initialize(columns = {}, **opts)
|
|
36
65
|
axis_name = opts.delete(:axis_name)
|
|
37
66
|
index = opts.delete(:index)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
attrs = opts.delete(:attrs)
|
|
68
|
+
types = opts.delete(:types)
|
|
69
|
+
on_error = opts.key?(:on_error) ? opts.delete(:on_error) : :mask
|
|
70
|
+
opts.each_key do |k|
|
|
71
|
+
CAFrame.__send__(:column_key, k, "CAFrame.new",
|
|
72
|
+
hint: "axis_name:, index:, attrs:, types: and on_error: are the options")
|
|
73
|
+
end
|
|
74
|
+
@attrs = nil
|
|
75
|
+
unless attrs.nil?
|
|
76
|
+
raise TypeError, "CAFrame.new: attrs: takes a Hash (got #{attrs.class})" unless attrs.is_a?(Hash)
|
|
77
|
+
set_attrs(attrs)
|
|
42
78
|
end
|
|
43
79
|
columns = columns.merge(opts) unless opts.empty?
|
|
44
80
|
|
|
45
81
|
@columns = {}
|
|
46
|
-
@axis_name = axis_name
|
|
82
|
+
@axis_name = axis_name.nil? ? DEFAULT_AXIS_NAME :
|
|
83
|
+
CAFrame.__send__(:column_key, axis_name, "CAFrame.new", "axis_name:")
|
|
47
84
|
@index = nil
|
|
48
85
|
# The row axis name this frame had before set_index promoted a column over
|
|
49
86
|
# it, so reset_index can put it back. A frame built with an index never
|
|
@@ -54,7 +91,8 @@ class CAFrame
|
|
|
54
91
|
n = nil
|
|
55
92
|
n_from = nil
|
|
56
93
|
columns.each do |name, col|
|
|
57
|
-
key = name.
|
|
94
|
+
key = column_key(name, "CAFrame.new")
|
|
95
|
+
raise ArgumentError, "CAFrame.new: a column name can not be empty" if key.empty?
|
|
58
96
|
ca = coerce_column(col)
|
|
59
97
|
len = ca.shape[0]
|
|
60
98
|
if n.nil?
|
|
@@ -92,6 +130,8 @@ class CAFrame
|
|
|
92
130
|
raise ArgumentError,
|
|
93
131
|
"axis_name #{@axis_name.inspect} collides with a column of the same name"
|
|
94
132
|
end
|
|
133
|
+
|
|
134
|
+
cast(types, on_error: on_error, option_name: :types) unless types.nil?
|
|
95
135
|
end
|
|
96
136
|
|
|
97
137
|
# --- df[...] : key type decides the axis (memo §13.2) ------------------
|
|
@@ -112,7 +152,7 @@ class CAFrame
|
|
|
112
152
|
if keys.size > 1
|
|
113
153
|
unless keys.all? { |k| k.is_a?(String) }
|
|
114
154
|
raise ArgumentError,
|
|
115
|
-
"
|
|
155
|
+
"df[...] with several keys returns columns; every key must be a String " \
|
|
116
156
|
"(use df.select(...) for a subset frame)"
|
|
117
157
|
end
|
|
118
158
|
return keys.map { |k| @columns.fetch(k) { raise KeyError, "no column #{k.inspect}" } }
|
|
@@ -142,7 +182,7 @@ class CAFrame
|
|
|
142
182
|
"CArray df[] key must be boolean or integer (got #{key.data_type})"
|
|
143
183
|
end
|
|
144
184
|
when Symbol
|
|
145
|
-
raise
|
|
185
|
+
raise ArgumentError, "df[symbol] is reserved for predicate keys"
|
|
146
186
|
else
|
|
147
187
|
raise ArgumentError, "unsupported df[] key: #{key.class}"
|
|
148
188
|
end
|
|
@@ -197,6 +237,7 @@ class CAFrame
|
|
|
197
237
|
# write path always propagates to derived views (unchanged from
|
|
198
238
|
# previous behaviour).
|
|
199
239
|
def []=(*keys, value)
|
|
240
|
+
refuse_if_frozen
|
|
200
241
|
if keys.size != 1
|
|
201
242
|
raise ArgumentError,
|
|
202
243
|
"df[...] = takes one key (a column name or a row selector); " \
|
|
@@ -222,6 +263,56 @@ class CAFrame
|
|
|
222
263
|
value
|
|
223
264
|
end
|
|
224
265
|
|
|
266
|
+
# Freezing a frame fixes its columns, rows and index; the columns' cells are
|
|
267
|
+
# their own arrays' to guard, as an Array's elements are. The check comes
|
|
268
|
+
# first so a refused edit leaves the frame as it was: the edits change the
|
|
269
|
+
# column table before any instance variable.
|
|
270
|
+
private def refuse_if_frozen
|
|
271
|
+
return unless frozen?
|
|
272
|
+
# The size, not the table: inspect prints every row.
|
|
273
|
+
size = "#{@nrow} row#{@nrow == 1 ? "" : "s"}, #{ncol} column#{ncol == 1 ? "" : "s"}"
|
|
274
|
+
raise FrozenError.new("can't modify frozen CAFrame (#{size})",
|
|
275
|
+
receiver: self)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Make this frame read-only and return it. It is +freeze+ extended to the
|
|
279
|
+
# cells: the column set, the rows and the index are fixed as by +freeze+,
|
|
280
|
+
# and the cells and masks of every column and of the index refuse writes
|
|
281
|
+
# made through this frame.
|
|
282
|
+
#
|
|
283
|
+
# Each column (and the index) is replaced by a read-only view of itself, so
|
|
284
|
+
# the arrays the frame was built from are left as they were: whoever holds
|
|
285
|
+
# them can still write, and the writes show through this frame. Frames
|
|
286
|
+
# derived from a protected frame by views (+select+, +filter+, a row slice)
|
|
287
|
+
# read through those views and refuse cell writes too, but are not frozen.
|
|
288
|
+
# +copy+ gives a frame that can be written again.
|
|
289
|
+
#
|
|
290
|
+
# Calling it on a protected frame returns the frame unchanged. A frame
|
|
291
|
+
# frozen with +freeze+ cannot be protected afterwards, because its columns
|
|
292
|
+
# can no longer be replaced: raises +FrozenError+.
|
|
293
|
+
def protect
|
|
294
|
+
return self if protected?
|
|
295
|
+
refuse_if_frozen
|
|
296
|
+
@columns = @columns.transform_values { |col| read_only_view(col) }
|
|
297
|
+
@index = read_only_view(@index) if @index
|
|
298
|
+
@protected = true
|
|
299
|
+
freeze
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# True if this frame was made read-only by +protect+. A +dup+ is not frozen
|
|
303
|
+
# and so not protected, though its columns stay the read-only views it
|
|
304
|
+
# shares; a +clone+ keeps both.
|
|
305
|
+
def protected?
|
|
306
|
+
frozen? && @protected == true
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
private def read_only_view(col)
|
|
310
|
+
return col if col.read_only?
|
|
311
|
+
view = col.refer
|
|
312
|
+
view.set_read_only_flag
|
|
313
|
+
view
|
|
314
|
+
end
|
|
315
|
+
|
|
225
316
|
# df["name"] = ... : the column forms of []=. Unlike the verbs, +[]=+ can
|
|
226
317
|
# only ever mutate the receiver -- Ruby hands the right-hand side back as
|
|
227
318
|
# the value of an assignment, so there is no way to return a new frame.
|
|
@@ -238,7 +329,7 @@ class CAFrame
|
|
|
238
329
|
mask_column(key)
|
|
239
330
|
elsif value.is_a?(CAFrame)
|
|
240
331
|
raise ArgumentError,
|
|
241
|
-
"df[name] = takes a column, not a CAFrame;
|
|
332
|
+
"df[name] = takes a column, not a CAFrame; take one with " \
|
|
242
333
|
"other[\"name\"], or splice rows with df[rows] = other"
|
|
243
334
|
else
|
|
244
335
|
rebind_column(key, value)
|
|
@@ -255,6 +346,7 @@ class CAFrame
|
|
|
255
346
|
# leaves the old one alone (the same rule +cast+ follows, for the physical
|
|
256
347
|
# reason that a type change cannot reuse the buffer).
|
|
257
348
|
private def rebind_column(key, column)
|
|
349
|
+
raise ArgumentError, "df[name] =: a column name can not be empty" if key.empty?
|
|
258
350
|
if @index && key == @axis_name
|
|
259
351
|
raise ArgumentError,
|
|
260
352
|
"#{key.inspect} names the index, not a column; " \
|
|
@@ -299,8 +391,7 @@ class CAFrame
|
|
|
299
391
|
def select(*names)
|
|
300
392
|
raise ArgumentError, "select requires at least one column name" if names.empty?
|
|
301
393
|
cols = {}
|
|
302
|
-
names.each do |
|
|
303
|
-
key = name.to_s
|
|
394
|
+
column_keys(names, "select").each do |key|
|
|
304
395
|
raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
|
|
305
396
|
cols[key] = @columns[key]
|
|
306
397
|
end
|
|
@@ -329,20 +420,78 @@ class CAFrame
|
|
|
329
420
|
mask = yield(self)
|
|
330
421
|
unless mask.is_a?(CArray) && mask.data_type == :boolean
|
|
331
422
|
raise ArgumentError,
|
|
332
|
-
"filter block must return a boolean CArray (got #{mask.class})"
|
|
423
|
+
"filter block must return a boolean CArray (got #{mask.is_a?(CArray) ? "a #{CArray::Inspector.type_label(mask)} CArray" : mask.class})"
|
|
333
424
|
end
|
|
334
|
-
unless mask.shape
|
|
425
|
+
unless mask.shape == [@nrow]
|
|
335
426
|
raise ArgumentError,
|
|
336
|
-
"filter mask has
|
|
427
|
+
"filter mask has shape #{mask.shape.inspect}, expected [#{@nrow}] (one value per row)"
|
|
337
428
|
end
|
|
338
429
|
select_rows(mask, keep_masked: keep_masked)
|
|
339
430
|
end
|
|
340
431
|
|
|
432
|
+
# Judge every row against named rules, and return the verdicts as a frame:
|
|
433
|
+
#
|
|
434
|
+
# report = df.verify(
|
|
435
|
+
# "age is not negative" => ->(f) { f["age"] >= 0 },
|
|
436
|
+
# "end after start" => ->(f) { f["end"] > f["start"] },
|
|
437
|
+
# )
|
|
438
|
+
# df.filter { |f| report["end after start"].not } # the rows that break it
|
|
439
|
+
#
|
|
440
|
+
# A rule is called with the frame, as a +filter+ block is, and returns a
|
|
441
|
+
# boolean CArray with one entry per row on its first axis; true means the
|
|
442
|
+
# rule holds. A rule over an N-D column may answer per cell, in the
|
|
443
|
+
# column's own shape, so the cells that break it can be masked directly.
|
|
444
|
+
# A cell the rule could not judge -- it read a masked value -- stays
|
|
445
|
+
# UNDEF rather than becoming false.
|
|
446
|
+
#
|
|
447
|
+
# The result has one column per rule, named after it, and this frame's
|
|
448
|
+
# index and row axis name.
|
|
449
|
+
#
|
|
450
|
+
# @param rules [Hash{String => #call}] the rules by name.
|
|
451
|
+
# @return [CAFrame]
|
|
452
|
+
# @raise [ArgumentError] when there are no rules, a name is empty, or a
|
|
453
|
+
# rule does not return a boolean CArray of this frame's length.
|
|
454
|
+
# @raise [TypeError] when a name is not a String.
|
|
455
|
+
def verify(rules)
|
|
456
|
+
unless rules.is_a?(Hash)
|
|
457
|
+
raise ArgumentError, "verify: the rules are a Hash of name => rule (got #{rules.class})"
|
|
458
|
+
end
|
|
459
|
+
raise ArgumentError, "verify: there are no rules" if rules.empty?
|
|
460
|
+
verdicts = {}
|
|
461
|
+
rules.each do |name, rule|
|
|
462
|
+
key = column_name_arg(name, "verify")
|
|
463
|
+
if @index && key == @axis_name
|
|
464
|
+
raise ArgumentError,
|
|
465
|
+
"verify: rule #{key.inspect} has the name of the row axis, " \
|
|
466
|
+
"which the index has in the result; name the rule otherwise"
|
|
467
|
+
end
|
|
468
|
+
unless rule.respond_to?(:call)
|
|
469
|
+
raise ArgumentError, "verify: rule #{key.inspect} is not callable (got #{rule.class})"
|
|
470
|
+
end
|
|
471
|
+
verdict = rule.call(self)
|
|
472
|
+
unless verdict.is_a?(CArray) && verdict.data_type == :boolean
|
|
473
|
+
raise ArgumentError,
|
|
474
|
+
"verify: rule #{key.inspect} must return a boolean CArray (got " \
|
|
475
|
+
"#{verdict.is_a?(CArray) ? "a #{verdict.data_type} CArray" : verdict.class})"
|
|
476
|
+
end
|
|
477
|
+
unless verdict.shape[0] == @nrow
|
|
478
|
+
raise ArgumentError,
|
|
479
|
+
"verify: rule #{key.inspect} returned shape #{verdict.shape.inspect}, " \
|
|
480
|
+
"expected #{@nrow} rows on the first axis"
|
|
481
|
+
end
|
|
482
|
+
verdicts[key] = verdict
|
|
483
|
+
end
|
|
484
|
+
# The verdicts are a table about the rows, not a part of this one, so the
|
|
485
|
+
# frame's attributes are not carried.
|
|
486
|
+
CAFrame.new(verdicts, axis_name: @axis_name, index: @index)
|
|
487
|
+
end
|
|
488
|
+
|
|
341
489
|
# Move a column to the index and name the row axis after it (memo §3).
|
|
342
490
|
# An index-role change: the data is unchanged, so this mutates self and
|
|
343
491
|
# returns it (memo §3.8). Any existing index is replaced.
|
|
344
492
|
def set_index(name)
|
|
345
|
-
|
|
493
|
+
refuse_if_frozen
|
|
494
|
+
key = column_key(name, "set_index")
|
|
346
495
|
raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
|
|
347
496
|
idx = @columns[key]
|
|
348
497
|
unless idx.ndim == 1
|
|
@@ -370,6 +519,7 @@ class CAFrame
|
|
|
370
519
|
# Also an index-role change, so it mutates self (memo §3.8). The former
|
|
371
520
|
# index becomes the first column, named after the row axis.
|
|
372
521
|
def reset_index
|
|
522
|
+
refuse_if_frozen
|
|
373
523
|
return self unless @index
|
|
374
524
|
@columns = { @axis_name => @index }.merge(@columns)
|
|
375
525
|
@index = nil
|
|
@@ -394,7 +544,11 @@ class CAFrame
|
|
|
394
544
|
|
|
395
545
|
# The single row whose index label equals +label+, as a Ruby Hash (memo
|
|
396
546
|
# §13.2b). +label+ is matched exactly against the index (+index.eq+), so any
|
|
397
|
-
# orderable / object / datetime / categorical index works
|
|
547
|
+
# orderable / object / datetime / categorical index works, and +label+ is
|
|
548
|
+
# whatever +index.eq+ takes -- for a time index, a Time, Date or a String
|
|
549
|
+
# such as "2024-01-02". A NaN label finds the NaN label, as join and
|
|
550
|
+
# group_by match NaN keys. A numeric index takes a number; a String or
|
|
551
|
+
# true / false raises. The return type is
|
|
398
552
|
# a row Hash and stays that way: zero matches raise KeyError, and duplicate
|
|
399
553
|
# labels raise (go through +filter { |f| f.index.eq(label) }+ for the
|
|
400
554
|
# multi-row, frame-returning path). Positional access is +df[i]+.
|
|
@@ -411,16 +565,29 @@ class CAFrame
|
|
|
411
565
|
"at(UNDEF): an undefined label identifies no row; " \
|
|
412
566
|
"use filter { |f| f.index.is_masked } for the rows with no label"
|
|
413
567
|
end
|
|
414
|
-
|
|
568
|
+
# A NaN label is the NaN value, as join and group_by take it: eq answers
|
|
569
|
+
# false for NaN against anything, itself included.
|
|
570
|
+
# A numeric index is looked up with a number: a String or true/false would
|
|
571
|
+
# be read differently by each numeric type, or not at all.
|
|
572
|
+
if !@index.face? && (@index.integer? || @index.float? || @index.complex?) &&
|
|
573
|
+
!label.is_a?(Numeric)
|
|
574
|
+
raise ArgumentError,
|
|
575
|
+
"at: the index is #{@index.data_type}, so the label must be a number " \
|
|
576
|
+
"(got #{label.inspect}); convert it first, e.g. at(Float(s)) or at(Integer(s))"
|
|
577
|
+
end
|
|
578
|
+
nan = label.is_a?(Float) && label.nan?
|
|
579
|
+
hit = nan ? @index.is_in([label]) : @index.eq(label)
|
|
580
|
+
pos = hit.where
|
|
415
581
|
case pos.elements
|
|
416
582
|
when 0
|
|
417
583
|
raise KeyError, "no row with index label #{label.inspect}"
|
|
418
584
|
when 1
|
|
419
585
|
row(pos[0])
|
|
420
586
|
else
|
|
587
|
+
test = nan ? "f.index.is_nan" : "f.index.eq(label)"
|
|
421
588
|
raise ArgumentError,
|
|
422
589
|
"index label #{label.inspect} matches #{pos.elements} rows; " \
|
|
423
|
-
"use filter { |f|
|
|
590
|
+
"use filter { |f| #{test} } for duplicate labels"
|
|
424
591
|
end
|
|
425
592
|
end
|
|
426
593
|
|
|
@@ -428,7 +595,8 @@ class CAFrame
|
|
|
428
595
|
def copy
|
|
429
596
|
cols = {}
|
|
430
597
|
@columns.each { |k, v| cols[k] = v.copy }
|
|
431
|
-
CAFrame.new(cols, axis_name: @axis_name, index: @index && @index.copy
|
|
598
|
+
CAFrame.new(cols, axis_name: @axis_name, index: @index && @index.copy,
|
|
599
|
+
attrs: @attrs)
|
|
432
600
|
end
|
|
433
601
|
|
|
434
602
|
# +dup+ / +clone+ share the column data -- that is the CArray dup contract
|
|
@@ -439,25 +607,103 @@ class CAFrame
|
|
|
439
607
|
private def initialize_copy(other)
|
|
440
608
|
super
|
|
441
609
|
@columns = @columns.dup
|
|
610
|
+
@attrs = @attrs.dup if @attrs
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Marshal writes the instance variables as they are. Loading stores the
|
|
614
|
+
# attribute values again as set_attr does, as frozen copies: Marshal
|
|
615
|
+
# brings back a value thawed.
|
|
616
|
+
private def marshal_dump
|
|
617
|
+
instance_variables.to_h { |name| [name, instance_variable_get(name)] }
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
private def marshal_load(ivars)
|
|
621
|
+
ivars.each { |name, value| instance_variable_set(name, value) }
|
|
622
|
+
if @attrs
|
|
623
|
+
@attrs = @attrs.transform_values { |v| CArray.__send__(:attr_coerce_value, v) }
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# --- attributes of the frame ------------------------------------------
|
|
628
|
+
#
|
|
629
|
+
# Key/value metadata about the table as a whole (a title, a source), apart
|
|
630
|
+
# from each column's own attributes. Keys and values follow the rules of
|
|
631
|
+
# CArray#set_attr: a Symbol or String key, stored as a String, and a
|
|
632
|
+
# JSON-compatible value, stored as a frozen copy.
|
|
633
|
+
#
|
|
634
|
+
# A frame made from this one takes its attributes when it holds the same
|
|
635
|
+
# table: a selection of rows (df[rows], filter, head, tail, sort_by,
|
|
636
|
+
# join(how: :semi / :anti)), a change of columns (select, append, drop,
|
|
637
|
+
# rename, ...), copy and dup. A frame computed from it -- a summary, a
|
|
638
|
+
# join, a reshape -- does not; ask the frame it came from. Each frame has
|
|
639
|
+
# its own Hash: setting an attribute on a frame made from this one leaves
|
|
640
|
+
# this one unchanged.
|
|
641
|
+
|
|
642
|
+
# The value of the attribute +key+, or nil when the frame has none.
|
|
643
|
+
def attr(key)
|
|
644
|
+
@attrs ? @attrs[CArray.__send__(:attr_normalize_key, key)] : nil
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Set the attribute +key+ to +value+ and return the stored value.
|
|
648
|
+
def set_attr(key, value)
|
|
649
|
+
refuse_if_frozen
|
|
650
|
+
CArray.__send__(:attr_validate_value, value)
|
|
651
|
+
(@attrs ||= {})[CArray.__send__(:attr_normalize_key, key)] =
|
|
652
|
+
CArray.__send__(:attr_coerce_value, value)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Set every key of +hash+, as set_attr sets one, and return self. Every
|
|
656
|
+
# value is checked before any is written.
|
|
657
|
+
def set_attrs(hash)
|
|
658
|
+
refuse_if_frozen
|
|
659
|
+
raise TypeError, "set_attrs takes a Hash (got #{hash.class})" unless hash.is_a?(Hash)
|
|
660
|
+
entries = hash.map do |key, value|
|
|
661
|
+
CArray.__send__(:attr_validate_value, value)
|
|
662
|
+
[CArray.__send__(:attr_normalize_key, key), CArray.__send__(:attr_coerce_value, value)]
|
|
663
|
+
end
|
|
664
|
+
unless entries.empty?
|
|
665
|
+
h = (@attrs ||= {})
|
|
666
|
+
entries.each { |k, v| h[k] = v }
|
|
667
|
+
end
|
|
668
|
+
self
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Remove the attribute +key+ and return its value, or nil.
|
|
672
|
+
def delete_attr(key)
|
|
673
|
+
k = CArray.__send__(:attr_normalize_key, key)
|
|
674
|
+
return nil unless @attrs&.key?(k)
|
|
675
|
+
refuse_if_frozen
|
|
676
|
+
@attrs.delete(k)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# A frozen Hash of the frame's attributes.
|
|
680
|
+
def attrs
|
|
681
|
+
(@attrs ? @attrs.dup : {}).freeze
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Whether the frame has any attribute, or the attribute +key+.
|
|
685
|
+
def has_attr?(key = nil)
|
|
686
|
+
return !(@attrs.nil? || @attrs.empty?) if key.nil?
|
|
687
|
+
!!@attrs&.key?(CArray.__send__(:attr_normalize_key, key))
|
|
442
688
|
end
|
|
443
689
|
|
|
444
690
|
# --- metadata readers (memo §13.2) ------------------------------------
|
|
445
691
|
# Each returns a fresh object; the live columns Hash is never exposed.
|
|
446
692
|
|
|
447
|
-
#
|
|
448
|
-
def
|
|
693
|
+
# Column names in column order. Returns +Array<String>+.
|
|
694
|
+
def column_names
|
|
449
695
|
@columns.keys
|
|
450
696
|
end
|
|
451
697
|
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
def
|
|
698
|
+
# The columns (raw CArrays) in column order. Returns +Array<CArray>+.
|
|
699
|
+
# Equivalent to +df[*column_names]+ but built directly from the columns
|
|
700
|
+
# Hash.
|
|
701
|
+
def columns
|
|
456
702
|
@columns.values
|
|
457
703
|
end
|
|
458
704
|
|
|
459
|
-
# Number of
|
|
460
|
-
def
|
|
705
|
+
# Number of columns.
|
|
706
|
+
def ncol
|
|
461
707
|
@columns.size
|
|
462
708
|
end
|
|
463
709
|
|
|
@@ -470,22 +716,32 @@ class CAFrame
|
|
|
470
716
|
# Index column (CArray) or nil.
|
|
471
717
|
attr_reader :index
|
|
472
718
|
|
|
473
|
-
# name => data_type, freshly derived
|
|
719
|
+
# name => data_type, freshly derived; a Face column gives its class
|
|
720
|
+
# (CATime, CAString, ...), since a Face's data_type is the surface its
|
|
721
|
+
# kernels dispatch on, not what the column holds.
|
|
474
722
|
def data_types
|
|
475
723
|
h = {}
|
|
476
|
-
@columns.each { |k, v| h[k] = v.data_type }
|
|
724
|
+
@columns.each { |k, v| h[k] = v.face? ? v.class : v.data_type } # type-label: data_types returns the class itself, a value
|
|
477
725
|
h
|
|
478
726
|
end
|
|
479
727
|
|
|
728
|
+
# A frame of the same table with a different set of columns: it takes this
|
|
729
|
+
# frame's attributes.
|
|
480
730
|
private def rebuild(cols)
|
|
481
|
-
CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
731
|
+
CAFrame.new(cols, axis_name: @axis_name, index: @index, attrs: @attrs)
|
|
482
732
|
end
|
|
483
733
|
|
|
734
|
+
# A column given to the frame: a CArray as it is; a Ruby Array typed by its
|
|
735
|
+
# values as from_records types a record's (build_record_column); anything
|
|
736
|
+
# else through its to_ca.
|
|
484
737
|
private def coerce_column(col)
|
|
738
|
+
if col.is_a?(Array)
|
|
739
|
+
return CAFrame.__send__(:build_record_column, col, col.size)
|
|
740
|
+
end
|
|
485
741
|
if col.is_a?(CAFrame)
|
|
486
742
|
raise ArgumentError,
|
|
487
743
|
"a CAFrame cannot be a column (it answers to_ca as a 2-D matrix); " \
|
|
488
|
-
"
|
|
744
|
+
"take a column with df[\"name\"] or df.to_ca first"
|
|
489
745
|
end
|
|
490
746
|
unless col.respond_to?(:to_ca)
|
|
491
747
|
raise ArgumentError, "column must be a CArray or Array (got #{col.class})"
|
|
@@ -498,10 +754,11 @@ class CAFrame
|
|
|
498
754
|
end
|
|
499
755
|
|
|
500
756
|
private def row(i)
|
|
501
|
-
i
|
|
502
|
-
unless
|
|
757
|
+
pos = i < 0 ? i + @nrow : i
|
|
758
|
+
unless pos >= 0 && pos < @nrow
|
|
503
759
|
raise IndexError, "row index #{i} out of range (nrow=#{@nrow})"
|
|
504
760
|
end
|
|
761
|
+
i = pos
|
|
505
762
|
h = {}
|
|
506
763
|
h[@axis_name] = elem_at(@index, i) if @index
|
|
507
764
|
@columns.each { |name, col| h[name] = elem_at(col, i) }
|
|
@@ -562,10 +819,10 @@ class CAFrame
|
|
|
562
819
|
# promote and the row count shifts by other.nrow - span.
|
|
563
820
|
private def splice_rows(selector, other)
|
|
564
821
|
lo, hi = contiguous_span(selector)
|
|
565
|
-
unless other.
|
|
822
|
+
unless other.column_names.sort == column_names.sort
|
|
566
823
|
raise ArgumentError,
|
|
567
|
-
"splice frame has columns #{other.
|
|
568
|
-
"expected the same set as #{
|
|
824
|
+
"splice frame has columns #{other.column_names.inspect}, " \
|
|
825
|
+
"expected the same set as #{column_names.inspect}"
|
|
569
826
|
end
|
|
570
827
|
new_cols = {}
|
|
571
828
|
@columns.each do |name, col|
|
|
@@ -590,8 +847,10 @@ class CAFrame
|
|
|
590
847
|
# column beforehand — silent
|
|
591
848
|
# promotion in a splice would
|
|
592
849
|
# hide schema drift.
|
|
850
|
+
splice_keep_attrs(new_cols[name], col)
|
|
593
851
|
end
|
|
594
852
|
new_index = splice_index(other, lo, hi)
|
|
853
|
+
splice_keep_attrs(new_index, @index) if new_index
|
|
595
854
|
@columns = new_cols
|
|
596
855
|
@index = new_index
|
|
597
856
|
# Read the new count off something the frame now holds, rather than
|
|
@@ -607,6 +866,13 @@ class CAFrame
|
|
|
607
866
|
self
|
|
608
867
|
end
|
|
609
868
|
|
|
869
|
+
# A splice edits this frame's table, so a spliced column keeps the
|
|
870
|
+
# attributes this frame's column had, whatever the rows put in carry.
|
|
871
|
+
private def splice_keep_attrs(result, own)
|
|
872
|
+
(result.attrs.keys - own.attrs.keys).each { |k| result.delete_attr(k) }
|
|
873
|
+
result.set_attrs(own.attrs)
|
|
874
|
+
end
|
|
875
|
+
|
|
610
876
|
# The index for a splice: head + other's index + tail. When this frame has
|
|
611
877
|
# an index, the spliced-in frame must have one too (its rows need labels).
|
|
612
878
|
private def splice_index(other, lo, hi)
|
|
@@ -684,18 +950,27 @@ class CAFrame
|
|
|
684
950
|
cols[name] = col[selector, *([nil] * (col.ndim - 1))]
|
|
685
951
|
end
|
|
686
952
|
new_index = @index ? @index[selector] : nil
|
|
687
|
-
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
953
|
+
CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
|
|
688
954
|
end
|
|
689
955
|
|
|
690
956
|
private def select_rows_keep_masked(selector)
|
|
691
957
|
keep = selector.strip_mask(1) # true where selected OR undetermined
|
|
692
958
|
undet_kept = selector.is_masked[keep] # among kept rows, which were undetermined
|
|
693
959
|
cols = {}
|
|
960
|
+
any_undet = undet_kept.any
|
|
694
961
|
@columns.each do |name, col|
|
|
695
962
|
tail = [nil] * (col.ndim - 1)
|
|
696
|
-
g = col[keep, *tail]
|
|
697
|
-
|
|
698
|
-
|
|
963
|
+
g = col[keep, *tail]
|
|
964
|
+
if any_undet
|
|
965
|
+
# mask_where copies and masks in one step, so a read-only Face
|
|
966
|
+
# (categorical, CAConstString) takes the mask as any column does.
|
|
967
|
+
cells = CArray.boolean(*g.shape)
|
|
968
|
+
cells[] = false
|
|
969
|
+
cells[undet_kept, *tail] = true
|
|
970
|
+
cols[name] = g.mask_where(cells)
|
|
971
|
+
else
|
|
972
|
+
cols[name] = g.copy
|
|
973
|
+
end
|
|
699
974
|
end
|
|
700
975
|
# The index is copied along with the columns. Its values are carried over
|
|
701
976
|
# unmasked (an undetermined row keeps its label, as mask_rows leaves the
|
|
@@ -703,8 +978,57 @@ class CAFrame
|
|
|
703
978
|
# aliased index would write through to the parent from a frame whose columns
|
|
704
979
|
# do not.
|
|
705
980
|
new_index = @index ? @index[keep].copy : nil
|
|
706
|
-
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
981
|
+
CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
# A column name given as an argument: a String, as the frame's column
|
|
985
|
+
# keys are. Symbols are the frame's options and types, never names.
|
|
986
|
+
private def column_key(name, verb)
|
|
987
|
+
CAFrame.__send__(:column_key, name, verb)
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
# A name a verb is given for a column -- or, with +what+, for the row
|
|
991
|
+
# axis -- is a String: a Symbol is a frame's option or type, never a
|
|
992
|
+
# name. Anything else is refused with TypeError "<verb>: <what> is a
|
|
993
|
+
# String (got :x)", +hint+ appended. A +verb+ that is an option, as
|
|
994
|
+
# "types:", already ends in its colon.
|
|
995
|
+
def self.column_key(name, verb, what = "a column name", hint: nil)
|
|
996
|
+
return name if name.is_a?(String)
|
|
997
|
+
message = "#{verb.end_with?(":") ? verb : "#{verb}:"} #{what} is a String (got #{name.inspect})"
|
|
998
|
+
message += "; #{hint}" if hint
|
|
999
|
+
raise TypeError, message
|
|
1000
|
+
end
|
|
1001
|
+
private_class_method :column_key
|
|
1002
|
+
|
|
1003
|
+
# A list of column names as a verb takes them: each a String, and none
|
|
1004
|
+
# twice.
|
|
1005
|
+
private def column_keys(names, verb)
|
|
1006
|
+
CAFrame.__send__(:refuse_repeated_names, names.map { |n| column_key(n, verb) }, verb)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# Column names a verb makes or takes, none of them twice. A frame holds
|
|
1010
|
+
# one column per name, so a repeated name among new columns would keep
|
|
1011
|
+
# only the last of them, and among the columns a verb takes would be
|
|
1012
|
+
# taken once or twice depending on the verb. Refused with ArgumentError
|
|
1013
|
+
# "<verb>: <what> <name> more than once", +hint+ appended. df[...] alone
|
|
1014
|
+
# takes a name twice, as Hash#values_at does.
|
|
1015
|
+
def self.refuse_repeated_names(names, verb, what = "names", hint: nil)
|
|
1016
|
+
dup = names.tally.find { |_, count| count > 1 }
|
|
1017
|
+
return names unless dup
|
|
1018
|
+
message = "#{verb}: #{what} #{dup.first.inspect} more than once"
|
|
1019
|
+
message += "; #{hint}" if hint
|
|
1020
|
+
raise ArgumentError, message
|
|
1021
|
+
end
|
|
1022
|
+
private_class_method :refuse_repeated_names
|
|
1023
|
+
|
|
1024
|
+
# The labels an index is made of from a key -- a group's first values,
|
|
1025
|
+
# the resample bins, the sorted pivot keys -- as the key column shows
|
|
1026
|
+
# them: +labels+, already of the key's data type, Face and unit, given
|
|
1027
|
+
# the key's attributes. Returns +labels+.
|
|
1028
|
+
def self.key_labels(key, labels)
|
|
1029
|
+
key.__send__(:carry_attrs, labels)
|
|
707
1030
|
end
|
|
1031
|
+
private_class_method :key_labels
|
|
708
1032
|
|
|
709
1033
|
private def positional_range?(range)
|
|
710
1034
|
(range.begin.nil? || range.begin.is_a?(Integer)) &&
|