carray 3.0.3 → 3.0.5

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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +469 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_descriptor.h +27 -24
  7. data/ext/ca_axis_dispatch.c +14 -26
  8. data/ext/ca_axis_group.c +27 -27
  9. data/ext/ca_binop_dispatch.c +1 -29
  10. data/ext/ca_binop_dispatch.h +0 -1
  11. data/ext/ca_categorical_iterator.c +231 -153
  12. data/ext/ca_compare.c +9 -20
  13. data/ext/ca_composite_dispatch.c +9 -9
  14. data/ext/ca_group_iter.c +16 -17
  15. data/ext/ca_kernel_iterator.c +1240 -2000
  16. data/ext/ca_kernel_iterator.h +502 -519
  17. data/ext/ca_monop_dispatch.c +82 -59
  18. data/ext/ca_monop_dispatch.h +1 -0
  19. data/ext/ca_obj_array.c +152 -45
  20. data/ext/ca_obj_bincmp.c +25 -21
  21. data/ext/ca_obj_binop.c +16 -26
  22. data/ext/ca_obj_bitarray.c +8 -3
  23. data/ext/ca_obj_bitfield.c +63 -49
  24. data/ext/ca_obj_block.c +18 -9
  25. data/ext/ca_obj_byte_swap.c +7 -7
  26. data/ext/ca_obj_const_string.c +188 -22
  27. data/ext/ca_obj_face.c +280 -58
  28. data/ext/ca_obj_face.h +19 -0
  29. data/ext/ca_obj_fake.c +15 -29
  30. data/ext/ca_obj_farray.c +6 -3
  31. data/ext/ca_obj_field.c +30 -8
  32. data/ext/ca_obj_fixlen_string.c +2 -3
  33. data/ext/ca_obj_grid.c +455 -134
  34. data/ext/ca_obj_meld.c +172 -34
  35. data/ext/ca_obj_moncmp.c +23 -8
  36. data/ext/ca_obj_monop.c +113 -67
  37. data/ext/ca_obj_object.c +210 -52
  38. data/ext/ca_obj_record.c +15 -7
  39. data/ext/ca_obj_reduce.c +17 -8
  40. data/ext/ca_obj_refer.c +42 -35
  41. data/ext/ca_obj_remap.c +30 -9
  42. data/ext/ca_obj_repeat.c +9 -10
  43. data/ext/ca_obj_roll.c +31 -10
  44. data/ext/ca_obj_select.c +48 -14
  45. data/ext/ca_obj_select_axis.c +15 -47
  46. data/ext/ca_obj_shift.c +41 -7
  47. data/ext/ca_obj_stack.c +180 -34
  48. data/ext/ca_obj_stride.c +190 -134
  49. data/ext/ca_obj_string.c +80 -4
  50. data/ext/ca_obj_tile.c +32 -11
  51. data/ext/ca_obj_time.c +22 -6
  52. data/ext/ca_obj_timedelta.c +22 -6
  53. data/ext/ca_obj_transpose.c +13 -7
  54. data/ext/ca_obj_triop.c +26 -32
  55. data/ext/ca_obj_window.c +267 -74
  56. data/ext/ca_op_ipower.c +4 -4
  57. data/ext/ca_sort_kernels.h +6 -3
  58. data/ext/ca_sweep_engine.c +2 -2
  59. data/ext/caframe_csv_reader.c +80 -1
  60. data/ext/carray.h +214 -75
  61. data/ext/carray_access.c +200 -99
  62. data/ext/carray_address_basis.c +343 -87
  63. data/ext/carray_attribute.c +113 -6
  64. data/ext/carray_bincount.c +10 -8
  65. data/ext/carray_broadcast.c +13 -8
  66. data/ext/carray_build_flags.h +3 -0
  67. data/ext/carray_call_cfunc.c +108 -0
  68. data/ext/carray_cast.c +375 -96
  69. data/ext/carray_cast_func.rb +90 -20
  70. data/ext/carray_class.c +4 -3
  71. data/ext/carray_config.h +28 -0
  72. data/ext/carray_conversion.c +18 -9
  73. data/ext/carray_copy.c +51 -14
  74. data/ext/carray_core.c +150 -55
  75. data/ext/carray_count.c +56 -20
  76. data/ext/carray_element.c +81 -33
  77. data/ext/carray_factorize.c +138 -84
  78. data/ext/carray_generate.c +199 -52
  79. data/ext/carray_histogram.c +63 -18
  80. data/ext/carray_hold.c +12 -15
  81. data/ext/carray_index_classifier.c +388 -176
  82. data/ext/carray_internal.h +348 -29
  83. data/ext/carray_kernels_bincmp.c +4453 -0
  84. data/ext/carray_kernels_binop.c +11368 -0
  85. data/ext/carray_kernels_init.c +1734 -0
  86. data/ext/carray_kernels_moncmp.c +2104 -0
  87. data/ext/carray_kernels_monop.c +18369 -0
  88. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  89. data/ext/carray_kernels_reduce_boolean.c +406 -0
  90. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  91. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  92. data/ext/carray_kernels_reduce_variance.c +3849 -0
  93. data/ext/carray_kernels_scan.c +4302 -0
  94. data/ext/carray_kernels_search.c +33829 -0
  95. data/ext/carray_kernels_sort.c +10897 -0
  96. data/ext/carray_kernels_triop.c +1864 -0
  97. data/ext/carray_key_windows.c +220 -0
  98. data/ext/carray_lazy.c +170 -111
  99. data/ext/carray_mask.c +337 -94
  100. data/ext/carray_median_percentile.c +82 -28
  101. data/ext/carray_memory_view.c +669 -561
  102. data/ext/carray_operator.c +129 -52
  103. data/ext/carray_order.c +604 -77
  104. data/ext/carray_partition.c +28 -27
  105. data/ext/carray_random.c +85 -31
  106. data/ext/carray_round.c +273 -0
  107. data/ext/carray_scatter.c +87 -45
  108. data/ext/carray_segment.c +3 -3
  109. data/ext/carray_slab.c +123 -37
  110. data/ext/carray_sort.c +259 -51
  111. data/ext/carray_sort_kernel.c +5 -7
  112. data/ext/carray_struct.c +54 -61
  113. data/ext/carray_test.c +26 -5
  114. data/ext/carray_time_text.c +4 -2
  115. data/ext/carray_undef.c +4 -0
  116. data/ext/carray_utils.c +309 -66
  117. data/ext/mk_call_cfunc.rb +19 -2
  118. data/ext/mkkernel.rb +403 -350
  119. data/ext/ruby_carray.c +10 -4
  120. data/ext/version.h +4 -4
  121. data/lib/carray/attribute.rb +139 -22
  122. data/lib/carray/autoload_carray.rb +8 -31
  123. data/lib/carray/autoload_method_extension.rb +14 -5
  124. data/lib/carray/axis_group.rb +255 -106
  125. data/lib/carray/basics.rb +105 -17
  126. data/lib/carray/bincount_nd.rb +12 -2
  127. data/lib/carray/block_iterator.rb +126 -23
  128. data/lib/carray/boolean_reduce.rb +17 -17
  129. data/lib/carray/categorical.rb +195 -59
  130. data/lib/carray/categorical_iterator.rb +132 -29
  131. data/lib/carray/complex.rb +5 -5
  132. data/lib/carray/conditional.rb +67 -18
  133. data/lib/carray/const_string.rb +24 -5
  134. data/lib/carray/construct.rb +15 -11
  135. data/lib/carray/data_type_extension.rb +9 -1
  136. data/lib/carray/distance_limit.rb +77 -0
  137. data/lib/carray/fixlen_string.rb +48 -3
  138. data/lib/carray/frame/column_verbs.rb +71 -0
  139. data/lib/carray/frame/concat.rb +17 -3
  140. data/lib/carray/frame/convert.rb +69 -0
  141. data/lib/carray/frame/csv_parser.rb +111 -17
  142. data/lib/carray/frame/describe.rb +3 -2
  143. data/lib/carray/frame/frame.rb +252 -28
  144. data/lib/carray/frame/group.rb +603 -101
  145. data/lib/carray/frame/io.rb +117 -28
  146. data/lib/carray/frame/join.rb +312 -51
  147. data/lib/carray/frame/records.rb +83 -7
  148. data/lib/carray/frame/reshape.rb +42 -35
  149. data/lib/carray/frame/sort.rb +7 -19
  150. data/lib/carray/frame/verbs.rb +188 -48
  151. data/lib/carray/frame/windows.rb +117 -0
  152. data/lib/carray/frame.rb +2 -0
  153. data/lib/carray/fuse_source.rb +42 -6
  154. data/lib/carray/fusion.rb +40 -16
  155. data/lib/carray/histogram.rb +6 -1
  156. data/lib/carray/inspect.rb +31 -8
  157. data/lib/carray/iterator.rb +85 -7
  158. data/lib/carray/key_windows.rb +620 -0
  159. data/lib/carray/lazy.rb +217 -183
  160. data/lib/carray/mask_gap_fill.rb +63 -57
  161. data/lib/carray/meld_reduce.rb +12 -14
  162. data/lib/carray/methods/bin.rb +26 -18
  163. data/lib/carray/methods/bincount.rb +16 -0
  164. data/lib/carray/methods/bit_string.rb +1 -1
  165. data/lib/carray/methods/choose.rb +16 -3
  166. data/lib/carray/methods/composition.rb +3 -1
  167. data/lib/carray/methods/gather_nd.rb +14 -5
  168. data/lib/carray/methods/insert_block.rb +2 -2
  169. data/lib/carray/methods/is_between.rb +59 -0
  170. data/lib/carray/methods/is_in.rb +61 -37
  171. data/lib/carray/methods/join.rb +1 -6
  172. data/lib/carray/methods/lag.rb +49 -0
  173. data/lib/carray/methods/locate_addr.rb +47 -51
  174. data/lib/carray/methods/mode.rb +1 -1
  175. data/lib/carray/methods/nunique.rb +11 -6
  176. data/lib/carray/methods/resize.rb +3 -3
  177. data/lib/carray/methods/value_counts.rb +4 -11
  178. data/lib/carray/record_order.rb +3 -3
  179. data/lib/carray/rng.rb +3 -0
  180. data/lib/carray/runtime.rb +53 -8
  181. data/lib/carray/segment_iterator.rb +323 -88
  182. data/lib/carray/serialize.rb +45 -14
  183. data/lib/carray/slab_iterator.rb +50 -0
  184. data/lib/carray/stack.rb +15 -12
  185. data/lib/carray/string.rb +113 -5
  186. data/lib/carray/string_operation_extension.rb +21 -6
  187. data/lib/carray/struct.rb +9 -21
  188. data/lib/carray/struct_builder.rb +24 -11
  189. data/lib/carray/time.rb +819 -158
  190. data/lib/carray/window_iterator.rb +122 -16
  191. data/lib/carray.rb +7 -0
  192. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  193. data/yard-stubs/ca_obj_field.rb +4 -0
  194. data/yard-stubs/ca_obj_grid.rb +21 -10
  195. data/yard-stubs/ca_obj_refer.rb +2 -1
  196. data/yard-stubs/ca_obj_roll.rb +2 -2
  197. data/yard-stubs/ca_obj_shift.rb +5 -1
  198. data/yard-stubs/ca_obj_tile.rb +1 -1
  199. data/yard-stubs/ca_obj_transpose.rb +5 -4
  200. data/yard-stubs/ca_obj_window.rb +8 -3
  201. data/yard-stubs/carray_access.rb +6 -1
  202. data/yard-stubs/carray_attribute.rb +3 -6
  203. data/yard-stubs/carray_cast.rb +12 -0
  204. data/yard-stubs/carray_conversion.rb +7 -2
  205. data/yard-stubs/carray_copy.rb +4 -2
  206. data/yard-stubs/carray_count.rb +4 -4
  207. data/yard-stubs/carray_element.rb +17 -10
  208. data/yard-stubs/carray_generate.rb +21 -1
  209. data/yard-stubs/carray_mask.rb +32 -3
  210. data/yard-stubs/carray_math.rb +41 -5
  211. data/yard-stubs/carray_median_percentile.rb +5 -2
  212. data/yard-stubs/carray_memory_view.rb +40 -23
  213. data/yard-stubs/carray_order.rb +129 -22
  214. data/yard-stubs/carray_random.rb +13 -4
  215. data/yard-stubs/carray_scatter.rb +17 -10
  216. data/yard-stubs/carray_slab.rb +9 -2
  217. data/yard-stubs/carray_sort.rb +6 -5
  218. data/yard-stubs/carray_test.rb +5 -3
  219. metadata +25 -3
  220. data/lib/carray/arrow_tensor.rb +0 -401
  221. 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,25 @@ 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. The
70
+ # fields are read as the data fields are, so strip: trims them
71
+ # too
59
72
  #
60
73
  # CAFrame.from_csv("obs.csv", header: 2) # a title above
61
74
  # CAFrame.from_csv("obs.csv", header: 0, data: 3) # units on lines 1-2
62
75
  # CAFrame.from_csv("big.csv", data: 1..100) # the first 100 lines
63
76
  # CAFrame.from_csv("raw.csv", column_names: %w[date temp rh])
64
77
  # CAFrame.from_csv("wide.csv", columns: %w[date temp])
78
+ # CAFrame.from_csv("raw.csv", header: false, columns: { 0 => "date", 3 => "temp" })
79
+ # CAFrame.from_csv("obs.csv", attrs: [:units]) # units on line 1
65
80
  #
66
81
  # A block, given the reader, reads in any other order with +skip+ /
67
82
  # +header+ / +header(name)+ / +column_names+ / +columns+ / +data+ (see
@@ -97,14 +112,19 @@ class CAFrame
97
112
  # independent frame.
98
113
  def self.from_csv(source, types: nil, on_error: :mask, missing: nil,
99
114
  header: nil, data: nil, column_names: nil, columns: nil,
100
- sep: ",", quote: '"', strip: false,
115
+ attrs: nil, sep: ",", quote: '"', strip: false,
101
116
  encoding: "bom|utf-8", parser: nil, &block)
102
117
  layout = { header: header, data: data, column_names: column_names,
103
- columns: columns }.compact
118
+ columns: columns, attrs: attrs }.compact
104
119
  if block && !layout.empty?
105
120
  raise ArgumentError, "from_csv: give #{layout.keys.map { |k| "#{k}:" }.join(', ')} " \
106
121
  "or a reading block, not both"
107
122
  end
123
+ # The parser reads the header, the data and the columns itself.
124
+ if parser && !layout.empty?
125
+ raise ArgumentError, "from_csv: give #{layout.keys.map { |k| "#{k}:" }.join(', ')} " \
126
+ "or parser:, not both"
127
+ end
108
128
  if block && block.arity != 1
109
129
  raise ArgumentError, "from_csv: the reading block takes the reader as its " \
110
130
  "parameter, as in from_csv(path) { |r| r.skip 2; r.header; r.data }"
@@ -116,7 +136,7 @@ class CAFrame
116
136
  # body goes straight to Strings. missing: masks text in place, which
117
137
  # wants the Strings first, so it takes that path too.
118
138
  read_text_as_const_string = !types.nil? && missing.nil?
119
- names, rows =
139
+ names, rows, column_attrs =
120
140
  if parser
121
141
  parser.call(source)
122
142
  elsif source.respond_to?(:gets)
@@ -140,6 +160,9 @@ class CAFrame
140
160
  mask_missing_tokens(frame, missing) if missing
141
161
  frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
142
162
  convert_const_string_columns_to_castring(frame)
163
+ column_attrs&.each_with_index do |attrs, j|
164
+ frame[frame.column_names[j]].set_attrs(attrs) unless attrs.empty?
165
+ end
143
166
  frame
144
167
  end
145
168
 
@@ -166,11 +189,9 @@ class CAFrame
166
189
  missing.each do |key, value|
167
190
  case key
168
191
  when :default then default = value
169
- when String then per_column[key] = value
170
192
  else
171
- raise ArgumentError,
172
- "#{verb}: missing: takes column names (Strings) and :default " \
173
- "as keys (got #{key.inspect})"
193
+ per_column[CAFrame.__send__(:column_key, key, verb, "missing: a column name",
194
+ hint: ":default is the only Symbol key")] = value
174
195
  end
175
196
  end
176
197
  [default, per_column]
@@ -205,18 +226,40 @@ class CAFrame
205
226
  end
206
227
  end
207
228
 
208
- private_class_method :split_missing, :missing_tokens, :mask_missing_tokens
229
+ # The attribute keys an +attrs:+ argument names, as Strings: an Array of
230
+ # Symbols or Strings, none twice.
231
+ def self.csv_attr_keys(attrs, verb)
232
+ unless attrs.is_a?(Array)
233
+ raise TypeError, "#{verb}: attrs: takes an Array of attribute keys " \
234
+ "(got #{attrs.inspect})"
235
+ end
236
+ keys = attrs.map do |key|
237
+ unless key.is_a?(Symbol) || key.is_a?(String)
238
+ raise TypeError, "#{verb}: attrs: an attribute key is a Symbol or String " \
239
+ "(got #{key.inspect})"
240
+ end
241
+ key.to_s
242
+ end
243
+ # names-check: attribute keys, not column names
244
+ dup = keys.tally.find { |_, n| n > 1 }
245
+ raise ArgumentError, "#{verb}: attrs: names #{dup.first.inspect} twice" if dup
246
+ keys
247
+ end
248
+
249
+ private_class_method :split_missing, :missing_tokens, :mask_missing_tokens,
250
+ :csv_attr_keys
209
251
 
210
252
  # 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.
253
+ # [names, rows, column_attrs]. Shared by the path and the IO source, so the
254
+ # two cannot come to read a file differently.
213
255
  def self.read_csv (io, sep:, quote:, strip:, layout:, read_text_as_const_string: false, &block)
214
256
  reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip,
215
257
  read_text_as_const_string: read_text_as_const_string)
216
258
  reader.reporting do
217
259
  block ? block.call(reader) : reader.layout(**layout)
218
260
  end
219
- reader.result
261
+ names, rows = reader.result
262
+ [names, rows, reader.column_attrs(reader.file_ncol)]
220
263
  end
221
264
 
222
265
  private_class_method :read_csv
@@ -281,14 +324,27 @@ class CAFrame
281
324
  #
282
325
  # df.to_csv("out.csv", missing: "-999")
283
326
  # df.to_csv("out.csv", missing: { default: "-999", "comment" => "" })
327
+ #
328
+ # +attrs:+ writes, below the name row, one line per attribute key given,
329
+ # in that order: field j is +attr(key)+ of column j (the index's for the
330
+ # index column). A column without the attribute gets an empty field, an
331
+ # empty String a quoted "", and a number its +to_s+.
332
+ # +from_csv(attrs:)+ with the same keys reads them back, as Strings.
333
+ #
334
+ # df.to_csv("out.csv", attrs: [:units, :long_name])
284
335
  def to_csv(path = nil, sep: ",", quote: '"', header: true, index: true,
285
- encoding: nil, missing: nil)
336
+ encoding: nil, missing: nil, attrs: [])
286
337
  default, per_column = CAFrame.send(:split_missing, missing, "to_csv")
287
338
  [default, *per_column.values].each do |text|
288
339
  next if text.nil? || text.is_a?(String)
289
340
  raise ArgumentError, "to_csv: missing: is the String written for a masked " \
290
341
  "cell (got #{text.inspect})"
291
342
  end
343
+ attr_keys = attrs.nil? ? [] : CAFrame.send(:csv_attr_keys, attrs, "to_csv")
344
+ if !header && !attr_keys.empty?
345
+ raise ArgumentError, "to_csv: attrs: writes lines below the name row, " \
346
+ "so it needs header: true"
347
+ end
292
348
  nd = @columns.find { |_, c| c.ndim != 1 }
293
349
  if nd
294
350
  raise ArgumentError,
@@ -308,13 +364,19 @@ class CAFrame
308
364
 
309
365
  names = []
310
366
  formatted = []
367
+ written = []
311
368
  if index && @index
312
369
  names << @axis_name
313
370
  formatted << format_csv_column(@index)
371
+ written << @index
314
372
  end
315
373
  @columns.each do |name, col|
316
374
  names << name
317
375
  formatted << format_csv_column(col)
376
+ written << col
377
+ end
378
+ attr_rows = attr_keys.map do |key|
379
+ written.each_with_index.map { |col, j| csv_attr_text(col, key, names[j]) }
318
380
  end
319
381
  unknown = per_column.keys - names
320
382
  raise KeyError, "to_csv: missing: names no column #{unknown.first.inspect}" unless unknown.empty?
@@ -331,7 +393,7 @@ class CAFrame
331
393
  end
332
394
 
333
395
  out = begin
334
- csv_text(names, formatted, header, sep, quote)
396
+ csv_text(names, attr_rows, formatted, header, sep, quote)
335
397
  rescue Encoding::CompatibilityError
336
398
  nil
337
399
  end
@@ -340,17 +402,22 @@ class CAFrame
340
402
  # UTF-8 text pays nothing for the check.
341
403
  unless out && out.encoding == Encoding::UTF_8 && out.valid_encoding?
342
404
  names = names.each_with_index.map { |t, j| csv_text_in_utf8(t, "the name of column #{j}") }
405
+ attr_rows = attr_rows.each_with_index.map do |row, k|
406
+ row.each_with_index.map do |t, j|
407
+ csv_text_in_utf8(t, "attribute #{attr_keys[k].inspect} of #{names[j].inspect}")
408
+ end
409
+ end
343
410
  formatted = formatted.each_with_index.map do |fcol, j|
344
411
  fcol.each_with_index.map { |t, i| csv_text_in_utf8(t, "row #{i} of #{names[j].inspect}") }
345
412
  end
346
- out = csv_text(names, formatted, header, sep, quote)
413
+ out = csv_text(names, attr_rows, formatted, header, sep, quote)
347
414
  end
348
415
 
349
416
  if encoding
350
417
  out = begin
351
418
  out.encode(encoding)
352
419
  rescue Encoding::UndefinedConversionError => e
353
- raise csv_unwritable_cell(names, formatted, encoding) || e
420
+ raise csv_unwritable_cell(names, attr_keys, attr_rows, formatted, encoding) || e
354
421
  end
355
422
  end
356
423
 
@@ -403,7 +470,10 @@ class CAFrame
403
470
  # middle-elided table.
404
471
  # @return [String]
405
472
  def inspect
406
- parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
473
+ parts = @columns.map do |k, v|
474
+ type = CArray::Inspector.type_label(v)
475
+ "#{k}:#{type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}"
476
+ end
407
477
  idx = @index ? " index=#{@axis_name.inspect}" : ""
408
478
  head = "#<CAFrame nrow=#{@nrow} columns=[#{parts.join(', ')}]#{idx}>"
409
479
  # The table counts the index as a column, so a frame whose only data is its
@@ -479,9 +549,11 @@ class CAFrame
479
549
  0xF900..0xFAFF, 0xFE10..0xFE19, 0xFE30..0xFE6F, 0xFF00..0xFF60,
480
550
  0xFFE0..0xFFE6, 0x1F300..0x1F64F, 0x1F900..0x1F9FF, 0x20000..0x3FFFD,
481
551
  ].freeze
552
+ Ractor.make_shareable(WIDE_CHAR_RANGES)
482
553
  private_constant :WIDE_CHAR_RANGES
483
554
 
484
555
  COMBINING_RANGES = [0x0300..0x036F, 0x1AB0..0x1AFF, 0x20D0..0x20F0].freeze
556
+ Ractor.make_shareable(COMBINING_RANGES)
485
557
  private_constant :COMBINING_RANGES
486
558
 
487
559
  private def display_width(text)
@@ -547,7 +619,7 @@ class CAFrame
547
619
  # from_csv reads back as the same value, not as the double it widens
548
620
  # to ("0.10000000149011612"); cmplx64 has that in each part.
549
621
  if !col.face? && (col.data_type == CA_FLOAT32 || col.data_type == CA_CMPLX64)
550
- col = col.__shortest_float64__
622
+ col = col.__widen_keeping_shortest_decimal__
551
623
  end
552
624
  # A boolean goes out as 0 / 1, as it does in every other serialization;
553
625
  # from_csv reads it back with types: { name => :boolean }.
@@ -565,17 +637,33 @@ class CAFrame
565
637
  end
566
638
  end
567
639
 
568
- private def csv_text(names, formatted, header, sep, quote)
640
+ private def csv_text(names, attr_rows, formatted, header, sep, quote)
569
641
  out = +""
570
642
  if header
571
643
  out << names.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
572
644
  end
645
+ attr_rows.each do |row|
646
+ out << row.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
647
+ end
573
648
  @nrow.times do |i|
574
649
  out << formatted.map { |fcol| quote_csv_field(fcol[i], sep, quote) }.join(sep) << "\n"
575
650
  end
576
651
  out
577
652
  end
578
653
 
654
+ # The field +to_csv(attrs:)+ writes for attribute +key+ of +col+: nil for
655
+ # none (the empty field), the String itself, or a scalar's to_s.
656
+ private def csv_attr_text(col, key, name)
657
+ value = col.attr(key)
658
+ case value
659
+ when nil, String then value
660
+ when Array, Hash
661
+ raise ArgumentError, "to_csv: attribute #{key.inspect} of #{name.inspect} is " \
662
+ "#{value.class}, which a CSV field cannot hold"
663
+ else value.to_s
664
+ end
665
+ end
666
+
579
667
  # The text of a cell as UTF-8, the encoding the CSV is built in: text in
580
668
  # another encoding is transcoded; bytes that are not valid in their
581
669
  # encoding, or have none (ASCII-8BIT), raise naming the cell.
@@ -611,8 +699,13 @@ class CAFrame
611
699
  end
612
700
 
613
701
  # The error for the first cell (or name) +encoding+ cannot hold, or nil.
614
- private def csv_unwritable_cell(names, formatted, encoding)
702
+ private def csv_unwritable_cell(names, attr_keys, attr_rows, formatted, encoding)
615
703
  cells = names.each_with_index.map { |t, j| [t, "the name of column #{j}"] }
704
+ attr_rows.each_with_index do |row, k|
705
+ row.each_with_index do |t, j|
706
+ cells << [t, "attribute #{attr_keys[k].inspect} of #{names[j].inspect}"] if t
707
+ end
708
+ end
616
709
  formatted.each_with_index do |fcol, j|
617
710
  fcol.each_with_index { |t, i| cells << [t, "row #{i} of #{names[j].inspect}"] if t }
618
711
  end
@@ -654,13 +747,9 @@ class CAFrame
654
747
  names ||= Array.new(ncol) { |j| "c#{j}" }
655
748
  # A frame holds one column per name, so a repeated one would keep only
656
749
  # 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
750
+ CAFrame.__send__(:refuse_repeated_names, names, "from_csv", "the header row names",
751
+ hint: "pass header: false for the default column names " \
752
+ "(c0, c1, ...), or name the columns with column_names:")
664
753
 
665
754
  cols = {}
666
755
  # Columns the C reader built as CAConstStrings over the text, for