carray 3.0.6 → 3.0.7

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +24 -3
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_group.c +6 -6
  6. data/ext/ca_binop_dispatch.c +8 -0
  7. data/ext/ca_binop_dispatch.h +1 -0
  8. data/ext/ca_categorical_iterator.c +7 -7
  9. data/ext/ca_kernel_iterator.h +6 -4
  10. data/ext/ca_obj_array.c +4 -4
  11. data/ext/ca_obj_bincmp.c +19 -3
  12. data/ext/ca_obj_binop.c +36 -11
  13. data/ext/ca_obj_bitarray.c +2 -4
  14. data/ext/ca_obj_bitfield.c +86 -4
  15. data/ext/ca_obj_byte_swap.c +2 -4
  16. data/ext/ca_obj_const_string.c +422 -257
  17. data/ext/ca_obj_face.c +71 -33
  18. data/ext/ca_obj_fake.c +112 -13
  19. data/ext/ca_obj_farray.c +7 -1
  20. data/ext/ca_obj_fixlen_string.c +51 -17
  21. data/ext/ca_obj_grid.c +12 -7
  22. data/ext/ca_obj_meld.c +5 -5
  23. data/ext/ca_obj_monop.c +65 -39
  24. data/ext/ca_obj_object.c +9 -9
  25. data/ext/ca_obj_reduce.c +2 -4
  26. data/ext/ca_obj_refer.c +104 -10
  27. data/ext/ca_obj_repeat.c +1 -1
  28. data/ext/ca_obj_roll.c +2 -4
  29. data/ext/ca_obj_select.c +8 -6
  30. data/ext/ca_obj_select_axis.c +8 -6
  31. data/ext/ca_obj_stack.c +5 -5
  32. data/ext/ca_obj_stride.c +46 -8
  33. data/ext/ca_obj_string.c +2 -2
  34. data/ext/ca_obj_tile.c +2 -4
  35. data/ext/ca_obj_triop.c +15 -12
  36. data/ext/ca_obj_window.c +22 -15
  37. data/ext/ca_op_ipower.c +63 -15
  38. data/ext/ca_op_powi.h +6 -29
  39. data/ext/ca_sort_kernels.h +3 -0
  40. data/ext/ca_sweep_engine.c +90 -23
  41. data/ext/ca_sweep_engine.h +5 -6
  42. data/ext/caframe_csv_reader.c +1 -1
  43. data/ext/carray.h +3 -1
  44. data/ext/carray_access.c +98 -39
  45. data/ext/carray_address_basis.c +98 -24
  46. data/ext/carray_broadcast.c +2 -2
  47. data/ext/carray_call_cfunc.c +5 -8
  48. data/ext/carray_cast.c +311 -37
  49. data/ext/carray_cast_func.rb +37 -5
  50. data/ext/carray_conversion.c +56 -2
  51. data/ext/carray_copy.c +24 -0
  52. data/ext/carray_core.c +69 -6
  53. data/ext/carray_count.c +28 -9
  54. data/ext/carray_element.c +9 -3
  55. data/ext/carray_factorize.c +11 -11
  56. data/ext/carray_histogram.c +2 -2
  57. data/ext/carray_index_classifier.c +7 -5
  58. data/ext/carray_internal.h +94 -0
  59. data/ext/carray_kernels_binop.c +55 -55
  60. data/ext/carray_kernels_init.c +14 -14
  61. data/ext/carray_kernels_monop.c +90 -140
  62. data/ext/carray_lazy.c +16 -1
  63. data/ext/carray_loop.c +9 -2
  64. data/ext/carray_mask.c +118 -11
  65. data/ext/carray_median_percentile.c +13 -8
  66. data/ext/carray_memory_view.c +41 -22
  67. data/ext/carray_operator.c +72 -11
  68. data/ext/carray_order.c +52 -9
  69. data/ext/carray_partition.c +1 -1
  70. data/ext/carray_random.c +1 -1
  71. data/ext/carray_round.c +24 -1
  72. data/ext/carray_scatter.c +4 -0
  73. data/ext/carray_segment.c +1 -1
  74. data/ext/carray_slab.c +13 -4
  75. data/ext/carray_sort.c +6 -3
  76. data/ext/carray_sort_kernel.c +24 -0
  77. data/ext/carray_struct.c +21 -6
  78. data/ext/carray_test.c +85 -39
  79. data/ext/carray_time_text.c +6 -0
  80. data/ext/carray_utils.c +124 -54
  81. data/ext/mk_call_cfunc.rb +6 -11
  82. data/ext/mkkernel.rb +37 -27
  83. data/ext/version.h +4 -4
  84. data/lib/carray/axis_group.rb +9 -6
  85. data/lib/carray/basics.rb +26 -8
  86. data/lib/carray/bincount_nd.rb +8 -2
  87. data/lib/carray/block_iterator.rb +48 -21
  88. data/lib/carray/categorical.rb +21 -8
  89. data/lib/carray/categorical_iterator.rb +2 -1
  90. data/lib/carray/conditional.rb +52 -4
  91. data/lib/carray/const_string.rb +74 -0
  92. data/lib/carray/construct.rb +2 -2
  93. data/lib/carray/core_extensions.rb +19 -68
  94. data/lib/carray/data_type_extension.rb +1 -0
  95. data/lib/carray/fixlen_string.rb +83 -1
  96. data/lib/carray/frame/column_verbs.rb +61 -6
  97. data/lib/carray/frame/concat.rb +50 -6
  98. data/lib/carray/frame/csv_parser.rb +13 -4
  99. data/lib/carray/frame/frame.rb +38 -6
  100. data/lib/carray/frame/group.rb +67 -40
  101. data/lib/carray/frame/io.rb +10 -5
  102. data/lib/carray/frame/join.rb +20 -6
  103. data/lib/carray/frame/records.rb +6 -4
  104. data/lib/carray/frame/reshape.rb +25 -5
  105. data/lib/carray/frame/verbs.rb +78 -25
  106. data/lib/carray/frame/windows.rb +30 -6
  107. data/lib/carray/fuse_source.rb +4 -2
  108. data/lib/carray/iterator.rb +8 -0
  109. data/lib/carray/key_windows.rb +10 -4
  110. data/lib/carray/lazy.rb +12 -17
  111. data/lib/carray/methods/bin.rb +3 -0
  112. data/lib/carray/methods/composition.rb +1 -1
  113. data/lib/carray/methods/factorize.rb +2 -0
  114. data/lib/carray/methods/gather_nd.rb +4 -1
  115. data/lib/carray/methods/is_in.rb +23 -3
  116. data/lib/carray/methods/locate_addr.rb +63 -12
  117. data/lib/carray/methods/repeat.rb +11 -1
  118. data/lib/carray/record_order.rb +35 -6
  119. data/lib/carray/runtime.rb +10 -0
  120. data/lib/carray/segment_iterator.rb +19 -16
  121. data/lib/carray/serialize.rb +22 -0
  122. data/lib/carray/slab_iterator.rb +1 -1
  123. data/lib/carray/stack.rb +3 -3
  124. data/lib/carray/string.rb +6 -6
  125. data/lib/carray/string_operation_extension.rb +25 -4
  126. data/lib/carray/struct_builder.rb +4 -1
  127. data/lib/carray/time.rb +190 -46
  128. data/lib/carray/window_iterator.rb +48 -12
  129. data/yard-stubs/ca_obj_bitfield.rb +4 -1
  130. data/yard-stubs/ca_obj_refer.rb +3 -0
  131. data/yard-stubs/ca_obj_window.rb +2 -1
  132. data/yard-stubs/carray_access.rb +4 -2
  133. data/yard-stubs/carray_cast.rb +6 -0
  134. data/yard-stubs/carray_count.rb +6 -3
  135. data/yard-stubs/carray_math.rb +5 -0
  136. data/yard-stubs/carray_order.rb +6 -0
  137. data/yard-stubs/carray_scatter.rb +2 -1
  138. data/yard-stubs/carray_test.rb +18 -5
  139. metadata +2 -2
@@ -111,7 +111,7 @@ class CAFrame
111
111
  #
112
112
  # What the block returns becomes the column as it is: a view stays a view
113
113
  # of what it was made from, so a write to a cell reaches it (+copy+ cuts
114
- # that).
114
+ # that). A lazy expression is evaluated, as a column holds its values.
115
115
  #
116
116
  # @param names [Array<String, Range, Regexp>] the columns; default all.
117
117
  # @yieldparam col [CArray] a column.
@@ -190,6 +190,9 @@ class CAFrame
190
190
  pieces.each { |p| p[] = UNDEF }
191
191
  time_text_of(col).to_a.each_with_index do |cell, i|
192
192
  next if UNDEF.equal?(cell) || cell.nil?
193
+ unless cell.is_a?(String)
194
+ raise ArgumentError, "split_column: row #{i} of #{key.inspect} holds #{cell.inspect}, which is not text"
195
+ end
193
196
  cell.split(sep, n).each_with_index { |part, k| pieces[k][i] = part }
194
197
  end
195
198
  rebuilt = {}
@@ -327,7 +330,15 @@ class CAFrame
327
330
  raise ArgumentError,
328
331
  "stack_columns: shape: #{layer.inspect} does not hold the #{names.size} columns"
329
332
  end
330
- col = CArray.stack(parts, axis: 1)
333
+ CAFrame.__send__(:refuse_rounded_pieces, parts, "stack_columns", "column #{names.first.inspect}")
334
+ begin
335
+ col = CArray.stack(parts, axis: 1)
336
+ rescue ArgumentError => e
337
+ raise unless e.message.start_with?("stack: ")
338
+ raise ArgumentError,
339
+ "stack_columns: the columns do not meet as one column " \
340
+ "(#{e.message.delete_prefix("stack: ")}); cast them to one type first"
341
+ end
331
342
  col = col.reshape(nrow, *layer, *trailing) if layer.size > 1
332
343
  rebuilt = {}
333
344
  @columns.each do |k, v|
@@ -548,7 +559,7 @@ class CAFrame
548
559
  end
549
560
  resolve_cast_map(name_or_map, label)
550
561
  else
551
- { column_key(name_or_map, label) => type }
562
+ resolve_cast_map({ column_key(name_or_map, label) => type }, label)
552
563
  end
553
564
  # Read every column before rebinding any, so a raise leaves the frame as
554
565
  # it was.
@@ -579,13 +590,15 @@ class CAFrame
579
590
  named.each_key do |name|
580
591
  refuse_missing_column(name, label.chomp(":")) unless @columns.key?(name)
581
592
  end
582
- rest = @columns.keys - named.keys
593
+ inferred, named = named.partition { |_, t| t == :infer }.map(&:to_h)
594
+ rest = @columns.keys - named.keys - inferred.keys
583
595
  types =
584
596
  case default
585
597
  when nil then {}
586
598
  when :infer then rest.empty? ? {} : select(*rest).infer_types
587
599
  else rest.to_h { |name| [name, default] }
588
600
  end
601
+ types.merge!(select(*inferred.keys).infer_types) unless inferred.empty?
589
602
  types.merge!(named.compact)
590
603
  end
591
604
 
@@ -610,11 +623,13 @@ class CAFrame
610
623
  # none present is not listed.
611
624
  #
612
625
  # A number with a leading zero ("007") is a code and keeps its column as
613
- # text, and so does an integer too long for int64 (an identifier), which a
614
- # float would round. A day-first or month-first date ("01/02/2024") is not
615
- # read as time, since which one it is cannot be told. Only text columns
616
- # are looked at -- an object column or a string Face (CAString,
617
- # CAConstString); columns that already have a type are not listed.
626
+ # text, and so does an integer too long for int64 (an identifier), or one
627
+ # past 2**53 among decimals, which a float would round. A day-first or
628
+ # month-first date ("01/02/2024") is not read as time, since which one it
629
+ # is cannot be told. Only text columns are looked at -- an object column
630
+ # or a string Face (CAString, CAConstString); columns that already have a
631
+ # type are not listed. An object column's cells are read as their text,
632
+ # so Ruby numbers there are typed as that text would be.
618
633
  #
619
634
  # df.cast(df.infer_types) # what cast(:infer) does
620
635
  # df.cast(df.infer_types.merge("code" => :int32)) # as cast(default: :infer, "code" => :int32)
@@ -622,18 +637,30 @@ class CAFrame
622
637
  @columns.each_with_object({}) do |(key, col), types|
623
638
  next unless string_column?(col)
624
639
  type = text_for_decimal_reading(col).__infer_number_type_of_text__
640
+ next if type == :float64 && integer_text_past_float?(col)
625
641
  type ||= :boolean if boolean_word_column?(col)
626
642
  type ||= :time if first_present_cell_can_be_time_text?(col) && time_text_column?(time_text_of(col))
627
643
  types[key] = type if type
628
644
  end
629
645
  end
630
646
 
647
+ # Whether a cell is an integer a float64 would round. Fewer than 16 digits
648
+ # cannot be, which leaves a const string column's other cells unread.
649
+ private def integer_text_past_float?(col)
650
+ long = col.is_a?(CAConstString) ? col[col.byte_length.ge(16).strip_mask(false)].to_a : time_text_of(col).to_a
651
+ long.any? do |text|
652
+ next text.abs > 2**53 if text.is_a?(Integer) # an object cell read as its text
653
+ text.is_a?(String) && text.match?(/\A\s*[-+]?\d{16,}\s*\z/) && Integer(text.strip, 10).abs > 2**53
654
+ end
655
+ end
656
+
631
657
  # Bring every column to one common data type and rebind them (memo §11.4).
632
658
  # The frame-level counterpart of +CArray.promote_list+: where +cast+ forces
633
659
  # named columns to a type, +promote+ widens the whole table until it has a
634
- # single type. Returns a new frame; like +cast+ it allocates fresh columns,
635
- # so this frame keeps its own. The index is not a column and is left
636
- # alone.
660
+ # single type. Returns a new frame; a column that changes type is a fresh
661
+ # one, and one that has the type already is shared with this frame, as
662
+ # +promote+ with no argument leaves it. The index is not a column and is
663
+ # left alone.
637
664
  #
638
665
  # df.promote # the common type CArray.result_type would pick
639
666
  # df.promote(:object) # force the widest type: anything fits
@@ -829,6 +856,9 @@ class CAFrame
829
856
  # A constant fill, in place, naming the column when the value does not go
830
857
  # into it, as strip_mask names it.
831
858
  private def fill_constant (key, col, value)
859
+ if col.read_only?
860
+ raise "fill: column #{key.inspect} (#{CArray::Inspector.type_label(col)}) can not modify read-only array"
861
+ end
832
862
  col.unmask(value)
833
863
  rescue ArgumentError, TypeError, RangeError, FloatDomainError => e
834
864
  raise e.exception("fill: column #{key.inspect} (#{CArray::Inspector.type_label(col)}) " \
@@ -962,6 +992,7 @@ class CAFrame
962
992
  "fill: give a method or value after the column #{how.inspect}; " \
963
993
  "a String alone is a column name"
964
994
  end
995
+ refuse_unknown_fill_method(how, "fill")
965
996
  map = @columns.select { |key, col| !key_names.include?(key) && col.any_masked? }
966
997
  .keys.to_h { |key| [key, how] }
967
998
  return map.empty? ? nil : map
@@ -970,16 +1001,18 @@ class CAFrame
970
1001
  return names_selected(names, "fill").to_h { |key| [key, how] }
971
1002
  end
972
1003
 
1004
+ private def refuse_unknown_fill_method (how, verb)
1005
+ return unless how.is_a?(Symbol) && !%i[ffill bfill linear].include?(how)
1006
+ raise ArgumentError,
1007
+ "#{verb}: unknown method #{how.inspect} (:ffill | :bfill | :linear, or a constant fill value)"
1008
+ end
1009
+
973
1010
  private def fill_by_map (map, verb)
974
1011
  raise ArgumentError, "#{verb}: the Hash names no column" if map.empty?
975
1012
  refuse_if_frozen
976
1013
  keys = column_keys(map.keys, verb)
977
1014
  keys.each { |key| refuse_missing_column(key) unless @columns.key?(key) }
978
- map.each_value do |how|
979
- next unless how.is_a?(Symbol) && !%i[ffill bfill linear].include?(how)
980
- raise ArgumentError,
981
- "#{verb}: unknown method #{how.inspect} (:ffill | :bfill | :linear, or a constant fill value)"
982
- end
1015
+ map.each_value { |how| refuse_unknown_fill_method(how, verb) }
983
1016
  pairs = keys.zip(map.values)
984
1017
  pairs.each { |key, how| yield self, key, how if @columns[key].read_only? }
985
1018
  filled = pairs.map do |key, how|
@@ -988,10 +1021,18 @@ class CAFrame
988
1021
  yield probe, key, how
989
1022
  [key, probe[key]]
990
1023
  end
991
- filled.compact.each { |key, col| @columns[key][] = col }
1024
+ filled.compact.each { |key, col| write_filled_cells(@columns[key], col) }
992
1025
  return self
993
1026
  end
994
1027
 
1028
+ # Only the cells a fill gave a value: a view may show one stored cell
1029
+ # twice, and writing a copy that stayed masked would mask it again.
1030
+ private def write_filled_cells(col, filled)
1031
+ return unless col.has_mask?
1032
+ target = col.is_masked & filled.is_not_masked
1033
+ col[target] = filled[target] if target.any
1034
+ end
1035
+
995
1036
  private def fill_linear_column(key, col, verb = "fill")
996
1037
  # A Face column interpolates through its own linear_fetch (a time
997
1038
  # column's keeps the Face and its unit), as the core gap fill does.
@@ -1021,7 +1062,7 @@ class CAFrame
1021
1062
  return
1022
1063
  end
1023
1064
  # The same fill as the core, with the index as x.
1024
- col[] = col.send(:linear_fill_along, @index) # write-through
1065
+ write_filled_cells(col, col.send(:linear_fill_along, @index))
1025
1066
  end
1026
1067
 
1027
1068
  # Text cells are data, not Ruby literals: an object column cast to a
@@ -1038,7 +1079,7 @@ class CAFrame
1038
1079
  if type == :string
1039
1080
  return col if col.is_a?(CArray::StringOperationMixin)
1040
1081
  begin
1041
- return CArray.string(col)
1082
+ return CArray.string(col).copy
1042
1083
  rescue CArray::DataTypeError, TypeError => e
1043
1084
  raise e.class, "column #{key.inspect} cannot be cast to :string: #{e.message}"
1044
1085
  end
@@ -1154,9 +1195,11 @@ class CAFrame
1154
1195
  next unless present[addr]
1155
1196
  text = cells[addr]
1156
1197
  m = text.is_a?(String) && TIMEDELTA_TEXT.match(text)
1157
- if m
1158
- values[addr] = Integer(m[1], 10)
1159
- units[addr] = CATime::Resolution.new(m[2] ? Integer(m[2], 10) : 1, m[3].to_sym)
1198
+ value = m && Integer(m[1], 10)
1199
+ count = m && (m[2] ? Integer(m[2], 10) : 1)
1200
+ if m && value.bit_length < 64 && count.positive? && count.bit_length < 64
1201
+ values[addr] = value
1202
+ units[addr] = CATime::Resolution.new(count, m[3].to_sym)
1160
1203
  elsif !text.nil? && !(text.is_a?(String) && text.strip.empty?)
1161
1204
  bad << addr
1162
1205
  end
@@ -1175,7 +1218,11 @@ class CAFrame
1175
1218
  ticks[nil] = UNDEF
1176
1219
  distinct.each do |u|
1177
1220
  addrs = units.each_index.select { |i| units[i] == u }
1178
- scaled = CATimeUnitAlgebra.convert_scale!(CA_INT64(addrs.map { |i| values[i] }), u, unit)
1221
+ begin
1222
+ scaled = CATimeUnitAlgebra.convert_scale!(CA_INT64(addrs.map { |i| values[i] }), u, unit)
1223
+ rescue RangeError
1224
+ raise RangeError, "cast: column #{key.inspect} holds durations too long for their finest unit, #{unit}"
1225
+ end
1179
1226
  ticks[CA_INT64(addrs)] = scaled
1180
1227
  end
1181
1228
  ticks.reshape(*col.shape).timedelta(unit: unit)
@@ -1421,7 +1468,13 @@ class CAFrame
1421
1468
  # to say about a surface it cannot read, so the widening check -- which
1422
1469
  # is about primitive promotion -- applies to plain columns only.
1423
1470
  @columns.each { |key, col| refuse_narrowing(key, col, type) unless col.face? }
1424
- @columns.each_key { |key| @columns[key] = @columns[key].to_type(type) }
1471
+ # A column that has the type already stays as it is, as promote leaves it
1472
+ # with no target.
1473
+ name = CArray.data_type_name(type)
1474
+ @columns.each_key do |key|
1475
+ col = @columns[key]
1476
+ @columns[key] = col.to_type(type) unless !col.face? && col.data_type_name == name
1477
+ end
1425
1478
  end
1426
1479
 
1427
1480
  private def refuse_narrowing(key, col, type)
@@ -61,7 +61,7 @@ class WindowedFrame
61
61
  # Returns the CAKeyWindowIterator of one column.
62
62
  # @return [CAKeyWindowIterator]
63
63
  def [] (name)
64
- @plan.for_value(@frame[@frame.__send__(:column_key, name, "windows")])
64
+ @plan.for_value(@frame.__send__(:named_column, @frame.__send__(:column_key, name, "windows"), "windows"))
65
65
  end
66
66
 
67
67
  # What each window covered, one value per row: see CAKeyWindowIterator.
@@ -113,13 +113,14 @@ class WindowedFrame
113
113
  # @param names [Array<String>] the columns; default every column that has
114
114
  # a quantile.
115
115
  # @return [CAFrame]
116
- def quantile (*names)
116
+ def quantile (*names, **keywords)
117
+ refuse_keywords(keywords)
117
118
  named = {}
118
119
  over_named_columns(:quantile, names, @key_names) do |col, name|
119
120
  named[name] = @plan.for_value(col).quantile
120
121
  end
121
122
  cols = CAFrame.__send__(:quantile_columns, named)
122
- return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index) unless @at
123
+ return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index&.copy) unless @at
123
124
  key = @frame.__send__(:key_column, @key_names.first, "windows")
124
125
  CAFrame.new(cols, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
125
126
  end
@@ -130,17 +131,40 @@ class WindowedFrame
130
131
  window_reduce(reduction, names, min_count: min_count, fill_value: fill_value)
131
132
  end
132
133
 
133
- def window_value (reduction, *names)
134
+ def window_value (reduction, *names, **keywords)
135
+ refuse_keywords(keywords)
134
136
  window_reduce(reduction, names)
135
137
  end
136
138
 
139
+ # These reductions take no keyword. Without the **keywords above, Ruby
140
+ # would hand a keyword in as a column name.
141
+ def refuse_keywords (keywords)
142
+ return if keywords.empty?
143
+ list = keywords.keys.map(&:inspect).join(", ")
144
+ raise ArgumentError, "unknown keyword#{keywords.size > 1 ? "s" : ""}: #{list}"
145
+ end
146
+
137
147
  # An N-D column (each row a profile) is reduced along the rows, each
138
148
  # window keeping the trailing shape, as a group reduces it.
139
149
  def window_reduce (reduction, names, args = [], **keywords)
140
150
  keywords = keywords.compact
151
+ if @at && names.include?(@key_names.first)
152
+ raise ArgumentError, "#{reduction}: #{@key_names.first.inspect} is the key, already the result's index"
153
+ end
154
+ fills = keywords.delete(:fill_value)
155
+ refuse_unknown_fill_columns(reduction, fills)
156
+ refuse_wrong_reduction_arguments(reduction, args, keywords)
141
157
  named = {}
142
158
  out = over_named_columns(reduction, names, @key_names, args) do |col, name|
143
- named[name] = @plan.for_value(col).public_send(reduction, *args, **keywords)
159
+ fill = fills.is_a?(Hash) ? fills[name] : fills
160
+ kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
161
+ named[name] =
162
+ if %i[first last].include?(reduction) && col.ndim == 1 && args.empty? && kw.empty? &&
163
+ col.face? && !col.__send__(:holds_new_values?)
164
+ col[@plan.for_value(present_rows(col)).public_send(reduction)].copy
165
+ else
166
+ @plan.for_value(col).public_send(reduction, *args, **kw)
167
+ end
144
168
  end
145
169
  out = named if @at && !names.empty?
146
170
  return out if out.is_a?(CArray) && !@at
@@ -149,7 +173,7 @@ class WindowedFrame
149
173
  key = @frame.__send__(:key_column, @key_names.first, "windows")
150
174
  CAFrame.new(out, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
151
175
  else
152
- CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
176
+ CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index&.copy)
153
177
  end
154
178
  end
155
179
  end
@@ -185,7 +185,9 @@ class CArray
185
185
  # block that starts on the sequence's first line is found in the file's
186
186
  # syntax tree. Only when two start on that line is the sequence itself
187
187
  # dumped: InstructionSequence#to_a reads the instructions while another
188
- # Ractor may be running them, and that can crash the process.
188
+ # Ractor may be running them, and that can crash the process. Two Ractors
189
+ # dumping one sequence for the first time crash Ruby before 4.1, so the
190
+ # dump goes through a lock.
189
191
  def self.code_location (sequence, path)
190
192
  line = sequence.first_lineno
191
193
  found = []
@@ -206,7 +208,7 @@ class CArray
206
208
  start, whole = found.first
207
209
  [start.start_line, start.start_column, whole.end_line, whole.end_column]
208
210
  else
209
- sequence.to_a[4][:code_location]
211
+ CArray.send(:instruction_sequence_to_a, sequence)[4][:code_location]
210
212
  end
211
213
  end
212
214
 
@@ -256,6 +256,14 @@ class CAIterator
256
256
  end))
257
257
  end
258
258
 
259
+ # CArray#count over a view: CABlock / CAWindow shadow #count with a geometry
260
+ # accessor, so it is dispatched explicitly, while a Face keeps a #count of
261
+ # its own (CAConstString reads its cells as Strings).
262
+ private def count_cells (view, *args, **kw)
263
+ return view.count(*args, **kw) if view.face?
264
+ CArray.instance_method(:count).bind_call(view, *args, **kw)
265
+ end
266
+
259
267
  # Recommended (should) surface -- template methods. These are well-defined
260
268
  # for some members and structurally impossible for others, so they are not
261
269
  # required: `map` is a per-piece element-wise transform scattered back to the
@@ -76,7 +76,9 @@ class CAKeyWindowIterator < CAIterator
76
76
  raise ArgumentError, "windows: at: is a one-dimensional CArray of points"
77
77
  end
78
78
  end
79
- @key = on
79
+ # Taken as it is now, as the windows are: first_key, last_key and bounds
80
+ # read it later.
81
+ @key = on.copy
80
82
  @n = value.dim0
81
83
  # Taken as they are now, as the windows are: bounds reads them later.
82
84
  @points = at&.copy
@@ -412,9 +414,13 @@ class CAKeyWindowIterator < CAIterator
412
414
  if sample.is_a?(Array)
413
415
  return sample.each_index.map { |k| lay_out(results.map { |r| r[k] }, sample[k], trail) }
414
416
  end
415
- out = CArray.new(sample.data_type, [@nout, *trail], bytes: sample.bytes)
417
+ # A Face that holds no new value cannot be written cell by cell, so its
418
+ # values are held as the surface objects they are.
419
+ surface = sample.face? && !sample.__send__(:holds_new_values?)
420
+ shape = [@nout, *trail]
421
+ out = surface ? CArray.new(CA_OBJECT, shape) : sample.__send__(:blank_like, shape)
416
422
  flat = out.reshape(@nout, results.size)
417
- results.each_with_index { |r, j| flat[nil, j] = r }
423
+ results.each_with_index { |r, j| flat[nil, j] = surface ? r.to_a : r }
418
424
  out
419
425
  end
420
426
 
@@ -548,7 +554,7 @@ class CAKeyWindowIterator < CAIterator
548
554
 
549
555
  def window_bound (width, op)
550
556
  key = @points || @key
551
- return key if width.nil?
557
+ return key.copy if width.nil?
552
558
  if key.is_a?(CATime) || key.is_a?(CATimedelta)
553
559
  key = key.to_unit(CATimeUnitAlgebra.finer(key.unit, width.unit))
554
560
  end
data/lib/carray/lazy.rb CHANGED
@@ -430,26 +430,20 @@ class CArray
430
430
  private :clamp_between
431
431
  LazyDispatch.send(:private, :clamp_between)
432
432
 
433
- # `**` integer-exponent lazy fast path.
433
+ # `**` with an Integer exponent.
434
434
  #
435
- # The default OP_POW path above goes through the `power` binop kernel,
436
- # which uses `pow(x, p)` / `cpow(x, p)` (transcendental, ~20-50 ns/cell)
437
- # for float / complex parents. When `other` is an Integer, the eager
438
- # `rb_ca_pow` dispatches to `rb_ca_ipower` (= binary exponentiation via
439
- # `op_powi_<type>`, ~1-3 mul/cell, 3-5x faster than the transcendental
440
- # form). Mirror that dispatch here so `a.lazy ** 2` rides the same
441
- # fast path.
442
- #
443
- # Routing rule (matches eager `rb_ca_pow`):
435
+ # The eager `rb_ca_pow` sends a Float / Complex array to an Integer power
436
+ # through `rb_ca_ipower` (ext/ca_op_ipower.c), which keeps the exponent
437
+ # an int64 and computes each cell with pow / powf / cpow, the answer the
438
+ # `power` kernel gives for the same exponent held in an array. The lazy
439
+ # form builds the matching CABinOp(OP_IPOWER) node:
444
440
  # - lazy parent + Integer exponent + Float/Complex parent data_type
445
441
  # -> wrap exponent as CScalar.int64 + CABinOp(OP_IPOWER)
446
- # - lazy parent (other cases) -> CABinOp(OP_POW) [existing]
447
- # - else -> eager (which itself dispatches to ipower internally)
442
+ # - lazy parent (other cases) -> CABinOp(OP_POW)
443
+ # - else -> eager
448
444
  #
449
- # Integer parents intentionally stay on OP_POW: the mkkernel-generated
450
- # `power` binop kernel already uses `op_powi_<type>` for integer data_types
451
- # via the `int:` expr in MkKernel.binop :power, so OP_POW + int parent
452
- # is already fast. The lazy-substrate gap was Float/Complex only.
445
+ # Integer parents stay on OP_POW: the `power` kernel's `int:` expr is
446
+ # `op_powi_<type>`, binary exponentiation in the integer type.
453
447
  module LazyDispatch
454
448
  def **(other)
455
449
  if ( lazy_view? || (other.is_a?(CArray) && other.lazy_view?) ) &&
@@ -498,7 +492,8 @@ class CArray
498
492
  # `eq` / `feq`.
499
493
  #
500
494
  # A comparison with UNDEF goes to the eager method, which answers `eq` /
501
- # `ne` from the mask and refuses the others.
495
+ # `ne` from the mask and refuses the others, and so does one with nil on
496
+ # an array that can not hold it.
502
497
  #
503
498
  # `feq` is arity 1 in eager (compile-time FLT_EPSILON / DBL_EPSILON);
504
499
  # we mirror that here. Runtime eps is a future extension (struct field
@@ -105,6 +105,9 @@ class CArray
105
105
  # @raise [ArgumentError] when `edges` is not 1-D, has fewer than 2
106
106
  # values, has a masked value or NaN, or decreases anywhere. Equal
107
107
  # edges (a bin of zero width) and infinite ones are accepted.
108
+ # Values and edges are compared as float64, so int64 values beyond
109
+ # 2**53 are compared at that precision.
110
+ #
108
111
  # @example
109
112
  # e = CA_FLOAT64([0, 1, 10, 100])
110
113
  # v = CA_FLOAT64([0.5, 5.0, 50.0, -1.0, 200.0])
@@ -140,7 +140,7 @@ class CArray
140
140
  # consistency required across rows / columns).
141
141
  def self.__ragged_paste (list, tdim, axis, data_type, name)
142
142
  shared_attrs = attrs_shared_by(list)
143
- list = CArray.promote_list(list, data_type: data_type)
143
+ list = promote_list_for(name, list, data_type: data_type)
144
144
  # promote_list has already enforced homogeneity (= common data_type, and
145
145
  # for Face elements: same Face class + portable + state-compatible). So,
146
146
  # mirroring CArray.stack, paste at the storage level and re-wrap the
@@ -44,6 +44,8 @@ class CArray
44
44
  if excluded.count(true) > 0
45
45
  codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
46
46
  end
47
+ # A const string's discovery reads its bytes and gives Ruby Strings.
48
+ levels = CArray.const_string(levels, encoding: encoding) if is_a?(CAConstString)
47
49
  [codes, levels]
48
50
  end
49
51
 
@@ -94,7 +94,10 @@ class CArray
94
94
  # The write goes through a flattened view, which carries a frozen
95
95
  # array's read-only flag but not its frozenness; say what the array is.
96
96
  raise FrozenError.new("can't modify frozen #{self.class}", receiver: self) if frozen?
97
- flat_addr, _outer, _rest = gather_nd_flat_addr(indices, "put_nd")
97
+ flat_addr, outer, rest = gather_nd_flat_addr(indices, "put_nd")
98
+ # The values lie as gather_nd gives them back.
99
+ out_shape = outer + rest
100
+ flat_addr = flat_addr.reshape(*out_shape) unless out_shape.empty?
98
101
  if flat_addr.has_mask?
99
102
  # A masked coordinate writes nothing. The values are laid out over
100
103
  # the coordinates by the store itself, so they mean what they mean
@@ -40,8 +40,9 @@ class CArray
40
40
  # `Array#include?` does, so they are equal there. To match by value
41
41
  # across Integer and Float, convert the array to a numeric type first.
42
42
  #
43
- # Masked cells of `values` do not enter the set. Masked cells of
44
- # `self` stay masked in the result (membership is unknown), so
43
+ # Masked cells of `values` do not enter the set, and neither does `nil`
44
+ # unless `self` is an `:object` array, which can hold it. Masked cells
45
+ # of `self` stay masked in the result (membership is unknown), so
45
46
  # `is_in` propagates `self`'s mask like an element-wise comparison.
46
47
  #
47
48
  # For a per-fiber "does this fiber contain any of these values"
@@ -59,7 +60,7 @@ class CArray
59
60
  # @return [CArray] boolean CArray of the same shape as `self`.
60
61
  def is_in (values)
61
62
  refuse_range_set(values, "is_in")
62
- a, b = promote_value_set(values)
63
+ a, b = promote_value_set(values_of_set(values))
63
64
  a.__send__(:__is_in__, b)
64
65
  end
65
66
 
@@ -132,6 +133,25 @@ class CArray
132
133
  "((#{values.inspect}).to_a), or test an interval with is_between(#{values.inspect})"
133
134
  end
134
135
 
136
+ # The set as the array reads it. nil is no value of an array that can not
137
+ # hold it, so it enters no set, and true and false are the numbers 1 and 0
138
+ # to an integer, float or complex array, as they are to eq.
139
+ def values_of_set (values)
140
+ return values if data_type == CA_OBJECT
141
+ numeric = !face? && (integer? || float? || complex?)
142
+ case values
143
+ when Array
144
+ values = values.compact
145
+ numeric ? values.map { |v| (v == true) ? 1 : (v == false) ? 0 : v } : values
146
+ when CArray
147
+ ( values.data_type == CA_OBJECT ) ? values.mask_eq(nil) : values
148
+ when true, false
149
+ numeric ? (values ? 1 : 0) : values
150
+ else
151
+ values
152
+ end
153
+ end
154
+
135
155
  # Reconcile a set-valued argument with self to a common data_type via
136
156
  # CArray.result_type (the single-source promotion rule the eager binop and
137
157
  # lazy CABinOp share), returning [self', set']. Only data types are reconciled,
@@ -135,7 +135,9 @@ class CArray
135
135
  # `self`; cells with no match in the direction and beyond-tolerance
136
136
  # cells are masked.
137
137
  # @raise [ArgumentError] when `direction` is not one of the accepted
138
- # symbols, or `tolerance` is not a duration for a time key.
138
+ # symbols, `tolerance` is not a duration for a time key, or `:round` /
139
+ # `tolerance` is asked of an object key whose values have no `-` (a
140
+ # String is ordered but has no distance).
139
141
  def locate_nearest_addr (ref, direction: :round, tolerance: nil)
140
142
  unless [:round, :floor, :ceil].include?(direction)
141
143
  raise ArgumentError,
@@ -150,8 +152,8 @@ class CArray
150
152
  # one has no place among the others either; it sorts after them.
151
153
  ri = ref.sort_addr
152
154
  ri = ri[0...ref.count_not_masked] if ref.has_mask?
153
- if ref.float? && ri.elements > 0
154
- ri = ri[0...(ri.elements - ref[ri].is_nan.count(true))]
155
+ if ri.elements > 0 && (nan = nearest_nan_cells(ref[ri]))
156
+ ri = ri[0...(ri.elements - nan.count(true))]
155
157
  end
156
158
  if ri.elements == 0
157
159
  out = CArray.int64(*shape)
@@ -160,6 +162,7 @@ class CArray
160
162
  end
161
163
  rs = ref[ri]
162
164
  n = rs.elements
165
+ nearest_refuse_distanceless(rs, direction, limit)
163
166
  rk, qk = nearest_keys(rs, self)
164
167
  # The position of each cell among the sorted references is counted, not
165
168
  # interpolated: :floor takes the last reference at or below it, :ceil the
@@ -172,17 +175,69 @@ class CArray
172
175
  when :round then nearest_round(rk, qk, nearest_count_below(rk, qk, false))
173
176
  end
174
177
  si[si.lt(0) | si.ge(n)] = UNDEF
175
- si[qk.is_nan.strip_mask(false)] = UNDEF if qk.float?
178
+ nan = nearest_nan_cells(qk)
179
+ si[nan.strip_mask(false)] = UNDEF if nan
176
180
  si = si.reshape(*shape)
177
181
  si[is_masked] = UNDEF if has_mask?
178
182
  idx = ri.project(si)
179
183
  if limit
180
- dist = (ref.project(idx) - self).abs
181
- idx[dist > limit] = UNDEF
184
+ idx[nearest_beyond(ref.project(idx), self, limit)] = UNDEF
182
185
  end
183
186
  idx
184
187
  end
185
188
 
189
+ # The NaN cells of a key, nil for a key that has none. A NaN in an object
190
+ # key is the cell that differs from itself.
191
+ private def nearest_nan_cells(key)
192
+ return key.is_nan if key.float?
193
+ return key.ne(key) if key.data_type == CA_OBJECT && !key.face?
194
+ nil
195
+ end
196
+
197
+ # :round and tolerance: measure how far a key is from its neighbours. An
198
+ # object key whose values have no subtraction (a String) is ordered but
199
+ # has no distance: only :floor and :ceil apply to it.
200
+ private def nearest_refuse_distanceless(key, direction, limit, subject = "locate_nearest_addr")
201
+ return unless key.data_type == CA_OBJECT && !key.face?
202
+ return unless direction == :round || limit
203
+ value = key.to_a.find { |v| !v.equal?(UNDEF) && v == v }
204
+ return if value.nil? || value.respond_to?(:-)
205
+ asked = (direction == :round) ? "direction: :round" : "tolerance:"
206
+ raise ArgumentError,
207
+ "#{subject}: #{asked} measures a distance, and a key of #{value.class} values " \
208
+ "has none (#{value.class} has no -); use direction: :floor or :ceil without tolerance:"
209
+ end
210
+
211
+ # Whether each pair of keys lies farther apart than +limit+. A time key is
212
+ # measured in ticks of the unit both keys meet in, as an integer key is:
213
+ # two instants an int64 holds can lie farther apart than an int64 holds (a
214
+ # nanosecond key spanning more than 292 years).
215
+ private def nearest_beyond(a, b, limit)
216
+ time_key = (a.is_a?(CATime) || a.is_a?(CATimedelta)) && a.class == b.class
217
+ r = time_key && CATimeUnitAlgebra.ratio(limit.unit, CATimeUnitAlgebra.diff_unit(a.unit, b.unit))
218
+ return nearest_distance(a, b) > limit unless r
219
+ a, b = a.in_common_unit_with(b)
220
+ threshold = (limit.value * r).floor
221
+ out = nearest_distance(a.ticks, b.ticks) > [threshold, 2**64 - 1].min
222
+ out[] = false if threshold >= 2**64 - 1
223
+ out
224
+ end
225
+
226
+ # How far apart two keys are. Integer keys are measured from the smaller to
227
+ # the larger, which fits an unsigned integer however far apart they are:
228
+ # the difference in their own type would wrap.
229
+ private def nearest_distance(a, b)
230
+ return (a - b).abs if a.face? || b.face?
231
+ t = a.send(:query_match_type, b)
232
+ return (a - b).abs if t == CA_OBJECT
233
+ a, b = a.to_type(t), b.to_type(t)
234
+ return (a - b).abs unless a.integer?
235
+ unless t == CA_UINT64
236
+ a, b = a.to_type(CA_INT64), b.to_type(CA_INT64)
237
+ end
238
+ (a.pmax(b) - a.pmin(b)).to_type(CA_UINT64)
239
+ end
240
+
186
241
  # A key held as fixed-length bytes (raw fixlen, or a fixlen string) has
187
242
  # no nearest: the search counts keys as numbers. A Face whose storage is
188
243
  # a number (a time) is searched by that number.
@@ -207,11 +262,7 @@ class CArray
207
262
  # that holds both.
208
263
  private def nearest_keys(rs, q)
209
264
  if rs.face? || q.face?
210
- if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
211
- u = CATimeUnitAlgebra.finer(rs.unit, q.unit)
212
- rs = rs.to_unit(u)
213
- q = q.to_unit(u)
214
- end
265
+ rs, q = rs.in_common_unit_with(q) if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
215
266
  q = rs.__send__(:reconcile_operand, q, "locate_nearest_addr")
216
267
  rs = rs.__send__(:face_storage)
217
268
  end
@@ -248,7 +299,7 @@ class CArray
248
299
  private def nearest_round(rk, qk, lt)
249
300
  lo = rk.project(lt - 1)
250
301
  hi = rk.project(lt)
251
- upper = (hi - qk).le(qk - lo).strip_mask(false)
302
+ upper = nearest_distance(hi, qk).le(nearest_distance(qk, lo)).strip_mask(false)
252
303
  upper[lo.is_masked] = true if lo.has_mask?
253
304
  upper[hi.is_masked] = false if hi.has_mask?
254
305
  upper[hi.eq(qk).strip_mask(false)] = true