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.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CArray
4
+
5
+ # The length a match by distance is limited by -- the +tolerance:+ of
6
+ # locate_nearest_addr (and of align_nearest_addr and CAFrame#join_asof
7
+ # through it), the +before:+, +after:+ and +interval:+ of a key window --
8
+ # read the same way wherever it is given: a length of the key's kind,
9
+ # not below 0.
10
+ #
11
+ # - For a number key it is a real number; NaN, which no distance is
12
+ # within or beyond, and a negative length are refused, as is a String.
13
+ # - For a time or duration key it is a duration: a String such as
14
+ # "10 minutes" (and "0 minutes") or a CATimedelta value, not negative.
15
+ # A bare number is refused, since its length would depend on the key's
16
+ # unit.
17
+ #
18
+ # Every refusal is ArgumentError naming +label+, the keyword as the
19
+ # caller says it ("tolerance:", "windows: before:").
20
+ module DistanceLimit
21
+
22
+ ZERO_DURATION = /\A\s*0+\s+([A-Za-z]+)\s*\z/
23
+
24
+ # +width+ as a Numeric (number key) or a CATimedelta::Element (time or
25
+ # duration key); +nil+ passes through.
26
+ def self.read (width, key, label)
27
+ return nil if width.nil?
28
+ if key.is_a?(CATime) || key.is_a?(CATimedelta)
29
+ duration(width, label)
30
+ else
31
+ number(width, key, label)
32
+ end
33
+ end
34
+
35
+ def self.number (width, key, label)
36
+ if width.is_a?(String) || width.is_a?(CATimedelta::Element)
37
+ raise ArgumentError,
38
+ "#{label} #{width.inspect} is a duration, but the key is " \
39
+ "#{CArray::Inspector.type_label(key)}; give a number"
40
+ end
41
+ unless width.is_a?(Numeric) && width.real? && !(width.is_a?(Float) && width.nan?) && width >= 0
42
+ raise ArgumentError, "#{label} is a number not below 0 (got #{width.inspect})"
43
+ end
44
+ width
45
+ end
46
+
47
+ def self.duration (width, label)
48
+ element =
49
+ case width
50
+ when CATimedelta::Element
51
+ width
52
+ when String
53
+ if (zero = width.match(ZERO_DURATION))
54
+ CATimedelta::Element.new(0, CATime::Resolution.parse(zero[1]).base)
55
+ else
56
+ res = begin
57
+ CATime::Resolution.parse(width)
58
+ rescue ArgumentError
59
+ raise ArgumentError,
60
+ "#{label} #{width.inspect} is not a duration " \
61
+ "(use e.g. \"10 minutes\" / \"1 hour\")"
62
+ end
63
+ CATimedelta::Element.new(res.count, res.base)
64
+ end
65
+ end
66
+ unless element
67
+ raise ArgumentError,
68
+ "#{label} a time key needs a duration such as \"10 minutes\" " \
69
+ "or a CATimedelta value (got #{width.inspect})"
70
+ end
71
+ if element.value < 0
72
+ raise ArgumentError, "#{label} is a duration not below 0 (got #{width.inspect})"
73
+ end
74
+ element
75
+ end
76
+ end
77
+ end
@@ -46,7 +46,7 @@ class CArray
46
46
  # @return [CAFixlenString]
47
47
  def self.fixlen_string (arg, bytes: nil, truncate: :error, &block)
48
48
  if arg.is_a?(CArray)
49
- return string_face_of(arg).to_fixlen_string(bytes: bytes, truncate: truncate)
49
+ return string_face_of(arg, "CArray.fixlen_string").to_fixlen_string(bytes: bytes, truncate: truncate)
50
50
  end
51
51
  unless [:error, :silent].include?(truncate)
52
52
  raise ArgumentError, "truncate: must be :error or :silent (got #{truncate.inspect})"
@@ -90,6 +90,12 @@ end
90
90
  # Mutable fixed-width String Face: L1 ops + in-place variants. In-place
91
91
  # transforms re-store through the native fixlen path (silent truncate).
92
92
  class CAFixlenString
93
+ # inspect's name for the array: the class and the cell width, which is the
94
+ # longest string a cell holds.
95
+ private def inspect_label
96
+ "#{self.class}[#{bytes}]"
97
+ end
98
+
93
99
  include CArray::StringOperationMixin
94
100
  include CArray::StringOperationMixin::Mutable
95
101
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CAFrame
4
+
5
+ # The one rule of a frame-wide verb over the frame's columns -- the group
6
+ # reductions and transforms, the windowed reductions -- kept in one place
7
+ # so that every verb answers in the same shape:
8
+ #
9
+ # - given names, the CArray (one) or the Array of them (several), as
10
+ # +df[...]+ returns columns; a named column whose type has no such
11
+ # operation is refused with CArray::DataTypeError, as the core refuses
12
+ # it, since it was asked for;
13
+ # - given none, every column whose type has it, the key columns left out,
14
+ # as a Hash of name to result that the caller makes a frame of.
15
+ #
16
+ # Whether a type has the operation is asked of the core on two copies of
17
+ # the column's first row (two blank cells of its kind when it has no
18
+ # rows), so the answer is about the type and not the data: a refusal
19
+ # there means the operation does not apply, while an error in the verb
20
+ # itself is left to raise. An N-D column (each row a profile) is asked,
21
+ # and computed, along the rows.
22
+ module ColumnVerbs
23
+
24
+ private
25
+
26
+ def over_named_columns (verb, names, key_names, args = [], keywords = {})
27
+ if names.empty?
28
+ cols = {}
29
+ @frame.column_names.each do |name|
30
+ next if key_names.include?(name)
31
+ col = @frame[name]
32
+ next unless column_has?(col, verb, args, keywords)
33
+ cols[name] = yield(col)
34
+ end
35
+ return cols
36
+ end
37
+ results = names.map do |name|
38
+ col = @frame[@frame.__send__(:column_key, name, verb)]
39
+ unless column_has?(col, verb, args, keywords)
40
+ raise CArray::DataTypeError,
41
+ "#{verb}: column #{name.inspect} " \
42
+ "(#{CArray::Inspector.type_label(col)}) has no #{verb}"
43
+ end
44
+ yield(col)
45
+ end
46
+ results.size == 1 ? results.first : results
47
+ end
48
+
49
+ # count with no value counts the present cells, which every type has;
50
+ # a Face's own count takes a value (CACategorical#count(label)), so the
51
+ # type is asked about count_not_masked instead.
52
+ def column_has? (col, verb, args, keywords)
53
+ verb = :count_not_masked if verb == :count && args.empty?
54
+ # Two cells: one cell does not reach what combines two (a String's
55
+ # median interpolates, its sum adds), and an empty column answers
56
+ # with the identity without asking the type at all.
57
+ tail = [nil] * (col.ndim - 1)
58
+ probe = if col.shape[0] > 0
59
+ col[CA_INT64([0, 0]), *tail]
60
+ else
61
+ col.resize(2, *col.shape[1..])
62
+ end
63
+ keywords = keywords.merge(axis: 0) if col.ndim > 1
64
+ probe.public_send(verb, *args, **keywords)
65
+ true
66
+ rescue CArray::DataTypeError, TypeError, ArgumentError
67
+ false
68
+ end
69
+ end
70
+ end
@@ -99,3 +99,72 @@ class CAFrame
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ class CArray
104
+ # @overload to_frame(*names, axis: -1)
105
+ # Shows this array as a table: a CAFrame whose rows run along axis 0 and
106
+ # whose columns are the positions along `axis`. Column `j` is the view of
107
+ # `self` with `j` at `axis` and every other axis whole, so its shape is
108
+ # `self`'s shape without `axis`. A 2-D array gives 1-D columns, an array
109
+ # of more dimensions gives N-D columns:
110
+ #
111
+ # ```
112
+ # a = CArray.float64(3, 2).seq!
113
+ # df = a.to_frame("x", "y")
114
+ # df["y"] # => a[nil, 1], shape [3]
115
+ #
116
+ # img = CArray.uint8(480, 640, 3)
117
+ # img.to_frame("r", "g", "b")["r"].shape # => [480, 640]
118
+ # ```
119
+ #
120
+ # Nothing is copied: each column is a view, so writes through the frame
121
+ # reach `self` and writes to `self` show in the frame, values and mask
122
+ # alike. Use `copy` on the frame for an independent one.
123
+ #
124
+ # Without names the columns are called `"c0"`, `"c1"`, ... A 1-D array
125
+ # has no axis to take columns from; give it one with `a[nil, :_]`.
126
+ #
127
+ # @param names [Array<String>] column names, one per position along `axis`.
128
+ # @param axis [Integer] the axis the columns are taken along; it cannot
129
+ # be axis 0, which holds the rows.
130
+ # @return [CAFrame]
131
+ # @raise [TypeError] when a name is not a String or `axis` is not an
132
+ # Integer.
133
+ # @raise [ArgumentError] when `axis` is out of range or is the row axis,
134
+ # when the number of names differs from the length of `axis`, or when
135
+ # a name repeats.
136
+ def to_frame (*names, axis: -1)
137
+ axis = -1 if axis.nil?
138
+ if ndim < 2
139
+ raise ArgumentError,
140
+ "to_frame: a #{ndim}-D array has no axis to take columns from " \
141
+ "besides the row axis (axis 0); give it one with a[nil, :_]"
142
+ end
143
+ ax = normalize_axis(axis, "to_frame")
144
+ if ax == 0
145
+ raise ArgumentError,
146
+ "to_frame: axis #{axis} is the row axis (axis 0); columns are " \
147
+ "taken along another axis"
148
+ end
149
+ ncol = shape[ax]
150
+ if names.empty?
151
+ names = Array.new(ncol) { |j| "c#{j}" }
152
+ else
153
+ names.each { |name| CAFrame.__send__(:column_key, name, "to_frame") }
154
+ raise ArgumentError, "to_frame: a column name can not be empty" if names.include?("")
155
+ unless names.size == ncol
156
+ raise ArgumentError,
157
+ "to_frame: #{names.size} names for #{ncol} columns " \
158
+ "(axis #{axis} has length #{ncol})"
159
+ end
160
+ CAFrame.__send__(:refuse_repeated_names, names, "to_frame")
161
+ end
162
+ index = Array.new(ndim, nil)
163
+ columns = {}
164
+ names.each_with_index do |name, j|
165
+ index[ax] = j
166
+ columns[name] = self[*index]
167
+ end
168
+ CAFrame.new(columns)
169
+ end
170
+ end
@@ -354,11 +354,18 @@ class CAFrame
354
354
  end
355
355
 
356
356
  # Read one record. With no argument it becomes the column names; with a
357
- # name it is a secondary header (e.g. units) -- read, returned, not used as
358
- # names. Returns the record's fields either way.
357
+ # name it is a secondary header (e.g. units): its field j becomes the
358
+ # attribute +name+ of column j (an empty field gives none). Returns the
359
+ # record's fields either way.
359
360
  def header(name = nil)
360
- fields = @tok.read(@io)
361
+ # A named header of a file with at most one column may be an empty
362
+ # line: that is how to_csv writes a column without the attribute, so
363
+ # the line is the header's, as an empty line is a row in the data.
364
+ blank_is_row = !name.nil? && !@names.nil? && @names.size <= 1
365
+ fields = @tok.read(@io, blank_is_row: blank_is_row)
361
366
  if fields.nil?
367
+ # A file with no columns has nothing to give a named header.
368
+ return [] if !name.nil? && @names&.empty?
362
369
  # An input with no record at all is a table with no columns: that is
363
370
  # what to_csv writes for a frame without columns or index. Once lines
364
371
  # have been skipped to reach the header, a missing header is an error.
@@ -372,11 +379,18 @@ class CAFrame
372
379
  lineno: @tok.lineno + 1)
373
380
  end
374
381
  if name.nil?
375
- @names = fields.map(&:to_s)
382
+ # An empty field names no column: the column takes the name a file
383
+ # without a header gives it, from its place in the line.
384
+ @names = fields.each_with_index.map { |f, j| f.to_s.empty? ? "c#{j}" : f.to_s }
376
385
  @header_line = @tok.record_line
377
386
  @names_to_check = nil
378
387
  else
379
- (@named_headers ||= {})[name.to_s] = fields
388
+ key = name.to_s
389
+ @named_headers ||= {}
390
+ if @named_headers.key?(key)
391
+ raise ArgumentError, "from_csv: header(#{name.inspect}) is read twice"
392
+ end
393
+ @named_headers[key] = [fields, @tok.record_line, name]
380
394
  end
381
395
  fields
382
396
  end
@@ -388,6 +402,9 @@ class CAFrame
388
402
  # one.
389
403
  def column_names(*names)
390
404
  names = names.flatten.map(&:to_s)
405
+ if names.include?("")
406
+ raise ArgumentError, "from_csv: column_names: a column name can not be empty"
407
+ end
391
408
  if @header_line
392
409
  check_column_names(names, @names.size, @header_line)
393
410
  else
@@ -401,13 +418,24 @@ class CAFrame
401
418
  # indexes of the file's columns from 0 (Integers or Ranges), in the order
402
419
  # the frame is to have them. The fields of the others are read past, so
403
420
  # a few columns of a wide file cost little more than a narrow file.
421
+ #
422
+ # A Hash selects by its keys, as above (a name or an index, not a Range),
423
+ # and names the columns read with its values:
424
+ #
425
+ # r.columns(2 => "code", "c7" => "area1")
404
426
  def columns(*selection)
427
+ @renames = nil
428
+ if selection.size == 1 && selection[0].is_a?(Hash)
429
+ @renames = column_renames(selection[0])
430
+ selection = selection[0].keys
431
+ end
405
432
  selection = selection.flatten
406
433
  if selection.empty?
407
434
  raise ArgumentError, "from_csv: columns: names no column"
408
435
  end
409
436
  selection.each do |c|
410
437
  next if c.is_a?(String)
438
+ CAFrame.__send__(:column_key, c, "from_csv", "columns: a column name") if c.is_a?(Symbol)
411
439
  next if c.is_a?(Integer) && c >= 0
412
440
  next if c.is_a?(Range) && (c.begin.nil? || (c.begin.is_a?(Integer) && c.begin >= 0)) &&
413
441
  (c.end.nil? || c.end.is_a?(Integer))
@@ -418,6 +446,24 @@ class CAFrame
418
446
  self
419
447
  end
420
448
 
449
+ # The new names a Hash given to +columns+ holds, checked.
450
+ private def column_renames(map)
451
+ map.each do |key, name|
452
+ if key.is_a?(Range)
453
+ raise ArgumentError, "from_csv: columns: a Hash selects one column per key, " \
454
+ "not a Range (got #{key.inspect})"
455
+ end
456
+ CAFrame.__send__(:column_key, name, "from_csv", "columns: a column name",
457
+ hint: "given for #{key.inspect}")
458
+ if name.empty?
459
+ raise ArgumentError, "from_csv: columns: a column name can not be empty " \
460
+ "(given for #{key.inspect})"
461
+ end
462
+ end
463
+ CAFrame.__send__(:refuse_repeated_names, map.values, "from_csv", "columns: names")
464
+ map.values
465
+ end
466
+
421
467
  # The indexes of the selected columns, in the order asked for, among
422
468
  # ncol columns named +names+ (nil: by index only). nil when ncol is not
423
469
  # known yet and the selection needs it.
@@ -425,8 +471,12 @@ class CAFrame
425
471
  indexes = @selection.flat_map do |c|
426
472
  case c
427
473
  when String
428
- j = names&.index(c)
429
- raise KeyError, "from_csv: columns: names no column #{c.inspect}" if j.nil?
474
+ # Without a header the columns are named c0, c1, ... by position.
475
+ j = names ? names.index(c) : c[/\Ac(0|[1-9]\d*)\z/, 1]&.to_i
476
+ if j.nil?
477
+ raise KeyError, "from_csv: columns: names no column #{c.inspect}" +
478
+ (names ? "" : " (with no header the columns are c0, c1, ...)")
479
+ end
430
480
  j
431
481
  when Integer then c
432
482
  else
@@ -438,10 +488,7 @@ class CAFrame
438
488
  (first..last).to_a
439
489
  end
440
490
  end
441
- dup = indexes.tally.select { |_, k| k > 1 }.keys
442
- unless dup.empty?
443
- raise ArgumentError, "from_csv: columns: selects column #{dup.first} more than once"
444
- end
491
+ CAFrame.__send__(:refuse_repeated_names, indexes, "from_csv", "columns: selects column")
445
492
  if ncol && (bad = indexes.find { |j| j >= ncol })
446
493
  raise ArgumentError, "from_csv: columns: #{bad} is past the #{ncol} columns of the file"
447
494
  end
@@ -484,22 +531,29 @@ class CAFrame
484
531
  # or to none (false) when column_names: is given; data: is the first line
485
532
  # of the data or a Range of lines, by default from the line after the
486
533
  # header. A record that starts within data: is read whole.
487
- def layout(header: nil, data: nil, column_names: nil, columns: nil)
534
+ def layout(header: nil, data: nil, column_names: nil, columns: nil, attrs: nil)
488
535
  header = column_names ? false : 0 if header.nil?
489
536
  unless header == false || (header.is_a?(Integer) && header >= 0)
490
537
  raise ArgumentError, "from_csv: header: takes the index of a line, from 0, " \
491
538
  "or false (got #{header.inspect})"
492
539
  end
540
+ attr_keys = attrs.nil? ? [] : CAFrame.send(:csv_attr_keys, attrs, "from_csv")
541
+ if !header && !attr_keys.empty?
542
+ raise ArgumentError, "from_csv: attrs: reads the lines below the header, " \
543
+ "so it needs a header line"
544
+ end
493
545
  first, last = CSVReader.data_lines(data)
494
546
  if header
495
547
  skip(header - @tok.lineno) if header > @tok.lineno
496
548
  self.header
549
+ attr_keys.each { |key| self.header(key.to_sym) }
497
550
  end
498
551
  column_names(*column_names) if column_names
499
552
  self.columns(*(columns.is_a?(Array) ? columns : [columns])) if columns
500
553
  first ||= @tok.lineno
501
554
  if first < @tok.lineno
502
- raise ArgumentError, "from_csv: data: #{first} does not come after header: #{header}"
555
+ raise ArgumentError, "from_csv: data: #{first} does not come after header: #{header}" +
556
+ (attr_keys.empty? ? "" : " and the #{attr_keys.size} attrs: lines")
503
557
  end
504
558
  skip(first - @tok.lineno)
505
559
  read_data(last && last + 1)
@@ -575,7 +629,7 @@ class CAFrame
575
629
  private def read_body_into_const_string_columns
576
630
  ncol = @names && !@names_to_check ? @names.size : 0
577
631
  keep = columns_to_read
578
- text = @io.read || ""
632
+ text = read_rest
579
633
  n, buffer, pairs, records = CArray.__csv_read_body_as_const_string_columns__(text, @tok.sep, @tok.quote, ncol, keep)
580
634
  unless buffer
581
635
  @rows = []
@@ -609,7 +663,7 @@ class CAFrame
609
663
  loop do
610
664
  text = if chunked then next_chunk(chunk_bytes)
611
665
  elsif last then lines_through(last)
612
- else @io.read || ""
666
+ else read_rest
613
667
  end
614
668
  break if text.nil?
615
669
  n, flat, records = CArray.__csv_read_body_as_string_cells__(text, @tok.sep, @tok.quote, ncol, keep)
@@ -675,6 +729,20 @@ class CAFrame
675
729
  buf
676
730
  end
677
731
 
732
+ # The rest of the input, in the IO's encoding. Read in pieces when the IO
733
+ # does not transcode: IO#read without a length checks the text's encoding
734
+ # as it reads, and the C reader checks it again faster.
735
+ private def read_rest
736
+ unless @io.respond_to?(:internal_encoding) && @io.internal_encoding.nil?
737
+ return @io.read || ""
738
+ end
739
+ buf = +""
740
+ while (piece = @io.read(CHUNK_BYTES))
741
+ buf << piece
742
+ end
743
+ buf.force_encoding(@io.external_encoding || Encoding.default_external)
744
+ end
745
+
678
746
  # The next chunk_bytes of the input, read on to the end of its line and
679
747
  # past any quoted field still open, in the IO's encoding; nil at EOF.
680
748
  private def next_chunk(chunk_bytes)
@@ -690,6 +758,32 @@ class CAFrame
690
758
  buf
691
759
  end
692
760
 
761
+ # The attributes the named headers give the columns +result+ returns, as
762
+ # [{key => value}, ...] in the same order. A column of the file the
763
+ # selection left out takes no part.
764
+ def column_attrs (ncol)
765
+ return nil unless @named_headers
766
+ indexes = @selection ? selected_indexes(@names, ncol) : (0...ncol).to_a
767
+ attrs = indexes.map { {} }
768
+ @named_headers.each do |key, (fields, line, name)|
769
+ if fields.size > ncol
770
+ raise ArgumentError, "from_csv: header(#{name.inspect}) on line #{line} " \
771
+ "has #{fields.size} fields for the #{ncol} columns"
772
+ end
773
+ indexes.each_with_index do |j, k|
774
+ value = fields[j]
775
+ attrs[k][key] = value unless value.nil?
776
+ end
777
+ end
778
+ attrs
779
+ end
780
+
781
+ # The number of columns of the file, once the data is read.
782
+ def file_ncol
783
+ return @table_ncol if @table
784
+ @names ? @names.size : ((@rows || []).map(&:size).max || 0)
785
+ end
786
+
693
787
  # [names_or_nil, rows] for CAFrame.from_csv to build from. names is nil when
694
788
  # neither header nor column_names ran (positional names are generated).
695
789
  def result
@@ -703,7 +797,7 @@ class CAFrame
703
797
  return [@names, rows] unless @selection
704
798
  ncol = @names ? @names.size : (rows.map(&:size).max || 0)
705
799
  indexes = selected_indexes(@names, ncol)
706
- names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
800
+ names = @renames || indexes.map { |j| @names ? @names[j] : "c#{j}" }
707
801
  [names, rows.map { |r| r.values_at(*indexes) }]
708
802
  end
709
803
 
@@ -711,7 +805,7 @@ class CAFrame
711
805
  private def table_result
712
806
  return [@names, @table] unless @selection
713
807
  indexes = selected_indexes(@names, @table_ncol)
714
- names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
808
+ names = @renames || indexes.map { |j| @names ? @names[j] : "c#{j}" }
715
809
  kept = @table_keep || (0...@table_ncol).to_a
716
810
  order = indexes.map { |j| kept.index(j) }
717
811
  table =
@@ -33,7 +33,7 @@ class CAFrame
33
33
  # @param names [Array<String>] the columns to summarize; default all.
34
34
  # @return [CAFrame] a new frame with one row per column.
35
35
  def describe(*names)
36
- names = names.empty? ? column_names : names.map(&:to_s)
36
+ names = names.empty? ? column_names : column_keys(names, "describe")
37
37
  rows = names.map { |name| describe_column(self[name]) }
38
38
  stats = %w[type count masked unique min max mean stddev]
39
39
  cols = stats.to_h do |stat|
@@ -51,6 +51,7 @@ class CAFrame
51
51
  end
52
52
 
53
53
  DESCRIBE_TIME_FACES = %w[CATime CATimedelta].freeze
54
+ Ractor.make_shareable(DESCRIBE_TIME_FACES)
54
55
  private_constant :DESCRIBE_TIME_FACES
55
56
 
56
57
  private def describe_column(col)
@@ -64,7 +65,7 @@ class CAFrame
64
65
  else
65
66
  :other
66
67
  end
67
- type = col.face? ? col.class.name : col.data_type_name
68
+ type = CArray::Inspector.type_label(col)
68
69
  type += col.shape[1..].inspect if col.ndim > 1
69
70
  row = { "type" => type,
70
71
  "count" => col.count_not_masked, "masked" => col.count_masked,