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
@@ -1,6 +1,7 @@
1
1
  # CAFrame CSV input/output (memo §11.2, §6-2).
2
2
 
3
3
  require "carray/frame/csv_parser"
4
+ require "carray/inspect" # CArray::Inspector.face_label, for the column list
4
5
 
5
6
  class CAFrame
6
7
  # Read a CSV into a frame. The header row supplies column names (Strings,
@@ -14,6 +15,7 @@ class CAFrame
14
15
  # CAFrame.from_csv("obs.csv", types: :infer)
15
16
  # CAFrame.from_csv("obs.csv", types: { default: :infer, "code" => :int32, "id" => nil })
16
17
  # CAFrame.from_csv("obs.csv", types: { default: :float64, "station" => nil })
18
+ # CAFrame.from_csv("obs.csv", types: { "station" => :categorical })
17
19
  #
18
20
  # +source+ is a path, or an open IO -- anything answering +gets+, which a
19
21
  # StringIO is. So CSV that is already in memory does not have to go to a
@@ -40,7 +42,8 @@ class CAFrame
40
42
  # Japanese.
41
43
  # parser: a callable source -> [headers, rows] to inject another parser
42
44
  # (e.g. the stdlib +csv+, or a typed-table source); when given,
43
- # sep/quote/strip/encoding and any block are that parser's concern.
45
+ # sep/quote/strip/encoding and any block are that parser's concern,
46
+ # and header:/data:/column_names:/columns:/attrs: are refused.
44
47
  # on_error: what a +types:+ cast does with a cell that does not read
45
48
  # (:mask default, :warn, :raise); see +cast+.
46
49
  #
@@ -55,13 +58,23 @@ class CAFrame
55
58
  # header then defaults to none
56
59
  # columns: the columns to read, by name or by index from 0 (Integers
57
60
  # and Ranges), in the order the frame has them; the others are
58
- # read past without a String made for their fields
61
+ # read past without a String made for their fields. Without a
62
+ # header the columns are named c0, c1, ..., and those names
63
+ # select too. A Hash selects by its keys and names the columns
64
+ # with its values: { 2 => "code", "c7" => "area" }; types: and
65
+ # missing: then use the new names
66
+ # attrs: attribute keys for the lines right below the header, one
67
+ # key per line: field j of a line becomes that attribute of
68
+ # column j (an empty field gives none). The data then starts
69
+ # below them. What to_csv(attrs:) writes, it reads back
59
70
  #
60
71
  # CAFrame.from_csv("obs.csv", header: 2) # a title above
61
72
  # CAFrame.from_csv("obs.csv", header: 0, data: 3) # units on lines 1-2
62
73
  # CAFrame.from_csv("big.csv", data: 1..100) # the first 100 lines
63
74
  # CAFrame.from_csv("raw.csv", column_names: %w[date temp rh])
64
75
  # CAFrame.from_csv("wide.csv", columns: %w[date temp])
76
+ # CAFrame.from_csv("raw.csv", header: false, columns: { 0 => "date", 3 => "temp" })
77
+ # CAFrame.from_csv("obs.csv", attrs: [:units]) # units on line 1
65
78
  #
66
79
  # A block, given the reader, reads in any other order with +skip+ /
67
80
  # +header+ / +header(name)+ / +column_names+ / +columns+ / +data+ (see
@@ -97,14 +110,19 @@ class CAFrame
97
110
  # independent frame.
98
111
  def self.from_csv(source, types: nil, on_error: :mask, missing: nil,
99
112
  header: nil, data: nil, column_names: nil, columns: nil,
100
- sep: ",", quote: '"', strip: false,
113
+ attrs: nil, sep: ",", quote: '"', strip: false,
101
114
  encoding: "bom|utf-8", parser: nil, &block)
102
115
  layout = { header: header, data: data, column_names: column_names,
103
- columns: columns }.compact
116
+ columns: columns, attrs: attrs }.compact
104
117
  if block && !layout.empty?
105
118
  raise ArgumentError, "from_csv: give #{layout.keys.map { |k| "#{k}:" }.join(', ')} " \
106
119
  "or a reading block, not both"
107
120
  end
121
+ # The parser reads the header, the data and the columns itself.
122
+ if parser && !layout.empty?
123
+ raise ArgumentError, "from_csv: give #{layout.keys.map { |k| "#{k}:" }.join(', ')} " \
124
+ "or parser:, not both"
125
+ end
108
126
  if block && block.arity != 1
109
127
  raise ArgumentError, "from_csv: the reading block takes the reader as its " \
110
128
  "parameter, as in from_csv(path) { |r| r.skip 2; r.header; r.data }"
@@ -116,7 +134,7 @@ class CAFrame
116
134
  # body goes straight to Strings. missing: masks text in place, which
117
135
  # wants the Strings first, so it takes that path too.
118
136
  read_text_as_const_string = !types.nil? && missing.nil?
119
- names, rows =
137
+ names, rows, column_attrs =
120
138
  if parser
121
139
  parser.call(source)
122
140
  elsif source.respond_to?(:gets)
@@ -140,6 +158,9 @@ class CAFrame
140
158
  mask_missing_tokens(frame, missing) if missing
141
159
  frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
142
160
  convert_const_string_columns_to_castring(frame)
161
+ column_attrs&.each_with_index do |attrs, j|
162
+ frame[frame.column_names[j]].set_attrs(attrs) unless attrs.empty?
163
+ end
143
164
  frame
144
165
  end
145
166
 
@@ -166,11 +187,9 @@ class CAFrame
166
187
  missing.each do |key, value|
167
188
  case key
168
189
  when :default then default = value
169
- when String then per_column[key] = value
170
190
  else
171
- raise ArgumentError,
172
- "#{verb}: missing: takes column names (Strings) and :default " \
173
- "as keys (got #{key.inspect})"
191
+ per_column[CAFrame.__send__(:column_key, key, verb, "missing: a column name",
192
+ hint: ":default is the only Symbol key")] = value
174
193
  end
175
194
  end
176
195
  [default, per_column]
@@ -205,18 +224,40 @@ class CAFrame
205
224
  end
206
225
  end
207
226
 
208
- private_class_method :split_missing, :missing_tokens, :mask_missing_tokens
227
+ # The attribute keys an +attrs:+ argument names, as Strings: an Array of
228
+ # Symbols or Strings, none twice.
229
+ def self.csv_attr_keys(attrs, verb)
230
+ unless attrs.is_a?(Array)
231
+ raise TypeError, "#{verb}: attrs: takes an Array of attribute keys " \
232
+ "(got #{attrs.inspect})"
233
+ end
234
+ keys = attrs.map do |key|
235
+ unless key.is_a?(Symbol) || key.is_a?(String)
236
+ raise TypeError, "#{verb}: attrs: an attribute key is a Symbol or String " \
237
+ "(got #{key.inspect})"
238
+ end
239
+ key.to_s
240
+ end
241
+ # names-check: attribute keys, not column names
242
+ dup = keys.tally.find { |_, n| n > 1 }
243
+ raise ArgumentError, "#{verb}: attrs: names #{dup.first.inspect} twice" if dup
244
+ keys
245
+ end
246
+
247
+ private_class_method :split_missing, :missing_tokens, :mask_missing_tokens,
248
+ :csv_attr_keys
209
249
 
210
250
  # Drive the reading-control DSL over one open IO and hand back
211
- # [names, rows]. Shared by the path and the IO source, so the two cannot
212
- # come to read a file differently.
251
+ # [names, rows, column_attrs]. Shared by the path and the IO source, so the
252
+ # two cannot come to read a file differently.
213
253
  def self.read_csv (io, sep:, quote:, strip:, layout:, read_text_as_const_string: false, &block)
214
254
  reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip,
215
255
  read_text_as_const_string: read_text_as_const_string)
216
256
  reader.reporting do
217
257
  block ? block.call(reader) : reader.layout(**layout)
218
258
  end
219
- reader.result
259
+ names, rows = reader.result
260
+ [names, rows, reader.column_attrs(reader.file_ncol)]
220
261
  end
221
262
 
222
263
  private_class_method :read_csv
@@ -281,14 +322,27 @@ class CAFrame
281
322
  #
282
323
  # df.to_csv("out.csv", missing: "-999")
283
324
  # df.to_csv("out.csv", missing: { default: "-999", "comment" => "" })
325
+ #
326
+ # +attrs:+ writes, below the name row, one line per attribute key given,
327
+ # in that order: field j is +attr(key)+ of column j (the index's for the
328
+ # index column). A column without the attribute gets an empty field, an
329
+ # empty String a quoted "", and a number its +to_s+.
330
+ # +from_csv(attrs:)+ with the same keys reads them back, as Strings.
331
+ #
332
+ # df.to_csv("out.csv", attrs: [:units, :long_name])
284
333
  def to_csv(path = nil, sep: ",", quote: '"', header: true, index: true,
285
- encoding: nil, missing: nil)
334
+ encoding: nil, missing: nil, attrs: [])
286
335
  default, per_column = CAFrame.send(:split_missing, missing, "to_csv")
287
336
  [default, *per_column.values].each do |text|
288
337
  next if text.nil? || text.is_a?(String)
289
338
  raise ArgumentError, "to_csv: missing: is the String written for a masked " \
290
339
  "cell (got #{text.inspect})"
291
340
  end
341
+ attr_keys = attrs.nil? ? [] : CAFrame.send(:csv_attr_keys, attrs, "to_csv")
342
+ if !header && !attr_keys.empty?
343
+ raise ArgumentError, "to_csv: attrs: writes lines below the name row, " \
344
+ "so it needs header: true"
345
+ end
292
346
  nd = @columns.find { |_, c| c.ndim != 1 }
293
347
  if nd
294
348
  raise ArgumentError,
@@ -308,13 +362,19 @@ class CAFrame
308
362
 
309
363
  names = []
310
364
  formatted = []
365
+ written = []
311
366
  if index && @index
312
367
  names << @axis_name
313
368
  formatted << format_csv_column(@index)
369
+ written << @index
314
370
  end
315
371
  @columns.each do |name, col|
316
372
  names << name
317
373
  formatted << format_csv_column(col)
374
+ written << col
375
+ end
376
+ attr_rows = attr_keys.map do |key|
377
+ written.each_with_index.map { |col, j| csv_attr_text(col, key, names[j]) }
318
378
  end
319
379
  unknown = per_column.keys - names
320
380
  raise KeyError, "to_csv: missing: names no column #{unknown.first.inspect}" unless unknown.empty?
@@ -331,7 +391,7 @@ class CAFrame
331
391
  end
332
392
 
333
393
  out = begin
334
- csv_text(names, formatted, header, sep, quote)
394
+ csv_text(names, attr_rows, formatted, header, sep, quote)
335
395
  rescue Encoding::CompatibilityError
336
396
  nil
337
397
  end
@@ -340,17 +400,22 @@ class CAFrame
340
400
  # UTF-8 text pays nothing for the check.
341
401
  unless out && out.encoding == Encoding::UTF_8 && out.valid_encoding?
342
402
  names = names.each_with_index.map { |t, j| csv_text_in_utf8(t, "the name of column #{j}") }
403
+ attr_rows = attr_rows.each_with_index.map do |row, k|
404
+ row.each_with_index.map do |t, j|
405
+ csv_text_in_utf8(t, "attribute #{attr_keys[k].inspect} of #{names[j].inspect}")
406
+ end
407
+ end
343
408
  formatted = formatted.each_with_index.map do |fcol, j|
344
409
  fcol.each_with_index.map { |t, i| csv_text_in_utf8(t, "row #{i} of #{names[j].inspect}") }
345
410
  end
346
- out = csv_text(names, formatted, header, sep, quote)
411
+ out = csv_text(names, attr_rows, formatted, header, sep, quote)
347
412
  end
348
413
 
349
414
  if encoding
350
415
  out = begin
351
416
  out.encode(encoding)
352
417
  rescue Encoding::UndefinedConversionError => e
353
- raise csv_unwritable_cell(names, formatted, encoding) || e
418
+ raise csv_unwritable_cell(names, attr_keys, attr_rows, formatted, encoding) || e
354
419
  end
355
420
  end
356
421
 
@@ -403,7 +468,10 @@ class CAFrame
403
468
  # middle-elided table.
404
469
  # @return [String]
405
470
  def inspect
406
- parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
471
+ parts = @columns.map do |k, v|
472
+ type = CArray::Inspector.type_label(v)
473
+ "#{k}:#{type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}"
474
+ end
407
475
  idx = @index ? " index=#{@axis_name.inspect}" : ""
408
476
  head = "#<CAFrame nrow=#{@nrow} columns=[#{parts.join(', ')}]#{idx}>"
409
477
  # The table counts the index as a column, so a frame whose only data is its
@@ -479,9 +547,11 @@ class CAFrame
479
547
  0xF900..0xFAFF, 0xFE10..0xFE19, 0xFE30..0xFE6F, 0xFF00..0xFF60,
480
548
  0xFFE0..0xFFE6, 0x1F300..0x1F64F, 0x1F900..0x1F9FF, 0x20000..0x3FFFD,
481
549
  ].freeze
550
+ Ractor.make_shareable(WIDE_CHAR_RANGES)
482
551
  private_constant :WIDE_CHAR_RANGES
483
552
 
484
553
  COMBINING_RANGES = [0x0300..0x036F, 0x1AB0..0x1AFF, 0x20D0..0x20F0].freeze
554
+ Ractor.make_shareable(COMBINING_RANGES)
485
555
  private_constant :COMBINING_RANGES
486
556
 
487
557
  private def display_width(text)
@@ -547,7 +617,7 @@ class CAFrame
547
617
  # from_csv reads back as the same value, not as the double it widens
548
618
  # to ("0.10000000149011612"); cmplx64 has that in each part.
549
619
  if !col.face? && (col.data_type == CA_FLOAT32 || col.data_type == CA_CMPLX64)
550
- col = col.__shortest_float64__
620
+ col = col.__widen_keeping_shortest_decimal__
551
621
  end
552
622
  # A boolean goes out as 0 / 1, as it does in every other serialization;
553
623
  # from_csv reads it back with types: { name => :boolean }.
@@ -565,17 +635,33 @@ class CAFrame
565
635
  end
566
636
  end
567
637
 
568
- private def csv_text(names, formatted, header, sep, quote)
638
+ private def csv_text(names, attr_rows, formatted, header, sep, quote)
569
639
  out = +""
570
640
  if header
571
641
  out << names.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
572
642
  end
643
+ attr_rows.each do |row|
644
+ out << row.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
645
+ end
573
646
  @nrow.times do |i|
574
647
  out << formatted.map { |fcol| quote_csv_field(fcol[i], sep, quote) }.join(sep) << "\n"
575
648
  end
576
649
  out
577
650
  end
578
651
 
652
+ # The field +to_csv(attrs:)+ writes for attribute +key+ of +col+: nil for
653
+ # none (the empty field), the String itself, or a scalar's to_s.
654
+ private def csv_attr_text(col, key, name)
655
+ value = col.attr(key)
656
+ case value
657
+ when nil, String then value
658
+ when Array, Hash
659
+ raise ArgumentError, "to_csv: attribute #{key.inspect} of #{name.inspect} is " \
660
+ "#{value.class}, which a CSV field cannot hold"
661
+ else value.to_s
662
+ end
663
+ end
664
+
579
665
  # The text of a cell as UTF-8, the encoding the CSV is built in: text in
580
666
  # another encoding is transcoded; bytes that are not valid in their
581
667
  # encoding, or have none (ASCII-8BIT), raise naming the cell.
@@ -611,8 +697,13 @@ class CAFrame
611
697
  end
612
698
 
613
699
  # The error for the first cell (or name) +encoding+ cannot hold, or nil.
614
- private def csv_unwritable_cell(names, formatted, encoding)
700
+ private def csv_unwritable_cell(names, attr_keys, attr_rows, formatted, encoding)
615
701
  cells = names.each_with_index.map { |t, j| [t, "the name of column #{j}"] }
702
+ attr_rows.each_with_index do |row, k|
703
+ row.each_with_index do |t, j|
704
+ cells << [t, "attribute #{attr_keys[k].inspect} of #{names[j].inspect}"] if t
705
+ end
706
+ end
616
707
  formatted.each_with_index do |fcol, j|
617
708
  fcol.each_with_index { |t, i| cells << [t, "row #{i} of #{names[j].inspect}"] if t }
618
709
  end
@@ -654,13 +745,9 @@ class CAFrame
654
745
  names ||= Array.new(ncol) { |j| "c#{j}" }
655
746
  # A frame holds one column per name, so a repeated one would keep only
656
747
  # the last of its columns.
657
- dup = names.tally.select { |_, count| count > 1 }.keys
658
- unless dup.empty?
659
- raise ArgumentError,
660
- "the header names #{dup.map(&:inspect).join(', ')} more than once; " \
661
- "name the columns yourself: from_csv(path, header: 0, " \
662
- "column_names: [...])"
663
- end
748
+ CAFrame.__send__(:refuse_repeated_names, names, "from_csv", "the header row names",
749
+ hint: "pass header: false for the default column names " \
750
+ "(c0, c1, ...), or name the columns with column_names:")
664
751
 
665
752
  cols = {}
666
753
  # Columns the C reader built as CAConstStrings over the text, for