carray 3.0.1 → 3.0.3

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
data/lib/carray/time.rb CHANGED
@@ -55,6 +55,21 @@ module CATimeCivil
55
55
  doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
56
56
  era * 146097 + doe - 719468
57
57
  end
58
+
59
+ # Same algebra for one date, on plain Integers. A literal is parsed one
60
+ # at a time, and routing each one through the vectorized form costs a
61
+ # one-cell CArray and a kernel call per literal -- which is where nearly
62
+ # all the time in a bulk column parse used to go (170k rows: 3.5 s, of
63
+ # which 3.1 s was this). Ruby's Integer `/` floors, as CArray's does,
64
+ # so the two forms read alike and agree everywhere.
65
+ def days_from_civil_1 (y, m, d)
66
+ y -= 1 if m <= 2
67
+ era = y / 400
68
+ yoe = y - era * 400
69
+ doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + (d - 1)
70
+ doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
71
+ era * 146097 + doe - 719468
72
+ end
58
73
  end
59
74
 
60
75
  # Unit algebra for the time surface: how two units relate (same group, which
@@ -78,6 +93,19 @@ module CATimeUnitAlgebra
78
93
 
79
94
  module_function
80
95
 
96
+ # An operand of a time or duration operator, taken through its to_ca when
97
+ # it exports MemoryView and defines one (an Apache Arrow duration array, given
98
+ # a bridge that adds to_ca), as the core binary operators take it; other
99
+ # objects are returned as they are, for the operator to accept or refuse.
100
+ def operand(other)
101
+ if !other.is_a?(CArray) && other.respond_to?(:to_ca) &&
102
+ CArray.memory_view_available?(other)
103
+ other.to_ca
104
+ else
105
+ other
106
+ end
107
+ end
108
+
81
109
  # seconds- (fixed) or months- (calendar) per base tick.
82
110
  def base_ratio(base)
83
111
  FIXED[base] || CALENDAR[base]
@@ -110,6 +138,8 @@ module CATimeUnitAlgebra
110
138
  g = rgcd(a.tick_ratio, b.tick_ratio)
111
139
  CATime::Resolution.new(Integer(g / base_ratio(fb)), fb)
112
140
  end
141
+ # Alias of {#common}, used where the caller reads the result as "the finer
142
+ # of the two grids" rather than "the grid they share".
113
143
  alias_method :finer, :common
114
144
  module_function :finer
115
145
 
@@ -191,7 +221,7 @@ module CATimeUnitAlgebra
191
221
  # fine->coarse divides only when every value is exact; cross-group ALWAYS
192
222
  # raises -- a :M / :Y duration has no fixed ratio to days (a month is
193
223
  # calendar-variable), so it genuinely cannot scale to seconds.
194
- def convert_scale!(storage, from, to)
224
+ def convert_scale!(storage, from, to, instant: false)
195
225
  a = res(from); b = res(to)
196
226
  return storage if a == b
197
227
  r = ratio(a, b)
@@ -204,6 +234,11 @@ module CATimeUnitAlgebra
204
234
  scaled = widen(storage, r.numerator) # coarse -> fine: lossless multiply
205
235
  return scaled if r.denominator == 1
206
236
  unless (scaled % r.denominator).eq(0).all
237
+ if instant
238
+ raise ArgumentError,
239
+ "cannot convert times in #{a} to #{b} without loss: " \
240
+ "some are not on the #{b} grid (they would be moved to it)"
241
+ end
207
242
  raise ArgumentError,
208
243
  "cannot scale duration #{a} to #{b} without loss: " \
209
244
  "some values are not a whole multiple of #{b} " \
@@ -254,7 +289,7 @@ module CATimeUnitAlgebra
254
289
  def convert_instant!(storage, from, to)
255
290
  a = res(from); b = res(to)
256
291
  return storage if a == b
257
- return convert_scale!(storage, a, b) if ratio(a, b) # same group
292
+ return convert_scale!(storage, a, b, instant: true) if ratio(a, b) # same group
258
293
  if CALENDAR.key?(a.base)
259
294
  convert_instant_calendar_to_fixed(storage, a, b) # widen
260
295
  else
@@ -368,7 +403,48 @@ module CATimeUnitAlgebra
368
403
  end
369
404
  end
370
405
 
406
+ # A reduction of a time Face takes `fill_value:` as a value of the Face (an
407
+ # Element, or what an assignment to the Face accepts), the same as
408
+ # `strip_mask` and `[]=` read it. The reduction runs on the storage, so the
409
+ # fill goes in after the result is a Face again: a whole reduction keeps its
410
+ # axes for that, so the fill lands in a typed cell, and is then taken out.
411
+ module CATimeSurfaceFill
412
+
413
+ private
414
+
415
+ def with_surface_fill (opts)
416
+ fill = opts[:fill_value]
417
+ return yield(opts) if fill.nil? || fill.equal?(UNDEF)
418
+ o = opts.reject { |k, _| k == :fill_value }
419
+ o[:keep_axis] = true
420
+ r = yield(o)
421
+ kept = nil
422
+ unless opts[:keep_axis]
423
+ axes = opts[:axis].nil? ? (0...ndim).to_a :
424
+ Array(opts[:axis]).map { |a| a < 0 ? a + ndim : a }
425
+ kept = shape.each_with_index.reject { |_, i| axes.include?(i) }.map(&:first)
426
+ end
427
+ surface_fill(r, fill, kept)
428
+ end
429
+
430
+ # `kept` is the shape to return to, or nil to keep the reduced axes.
431
+ def surface_fill (r, fill, kept)
432
+ case r
433
+ when Array then r.map { |x| surface_fill(x, fill, kept) }
434
+ when CArray
435
+ r = r.strip_mask(fill) if r.has_mask?
436
+ if kept.nil? then r
437
+ elsif kept.empty? then r[*Array.new(r.ndim, 0)]
438
+ else r.reshape(*kept)
439
+ end
440
+ else r
441
+ end
442
+ end
443
+
444
+ end
445
+
371
446
  class CATime
447
+ include CATimeSurfaceFill
372
448
  # @overload new(*shape, unit: :ns)
373
449
  # Allocates a new int64 storage CArray of the given `shape` and
374
450
  # wraps it as a CATime Face with the given `unit`. The
@@ -455,7 +531,7 @@ class CATime
455
531
  # @return [String]
456
532
  def to_s
457
533
  case unit.base
458
- when :Y then format("%04d", to_date.year)
534
+ when :Y then to_date.strftime("%Y")
459
535
  when :M then to_date.strftime("%Y-%m")
460
536
  when :W, :D then to_date.strftime("%Y-%m-%d")
461
537
  else to_time.iso8601(fractional_second_digits) # :h .. :as (time shown)
@@ -585,6 +661,7 @@ class CATime
585
661
  end
586
662
 
587
663
  class CATimedelta
664
+ include CATimeSurfaceFill
588
665
  # @overload new(*shape, unit: :ns)
589
666
  # Allocates a new int64 storage CArray of the given `shape` and
590
667
  # wraps it as a CATimedelta Face with the given `unit`.
@@ -799,16 +876,19 @@ class CATime
799
876
  # cross-group calendar duration raises). Adding two datetimes is
800
877
  # ill-defined.
801
878
  # @param other [CATimedelta]
879
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
880
+ # duration array)
802
881
  # @return [CATime]
803
882
  # @raise [TypeError] on a non-timedelta operand.
804
883
  def +(other)
884
+ given, other = other, CATimeUnitAlgebra.operand(other)
805
885
  case other
806
886
  when CATimedelta
807
887
  (parent + CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
808
888
  when CATime
809
889
  raise TypeError, "CATime + CATime is ill-defined"
810
890
  else
811
- raise TypeError, "CATime + #{other.class} is not allowed (use CATimedelta)"
891
+ raise TypeError, "CATime + #{given.class} is not allowed (use CATimedelta)"
812
892
  end
813
893
  end
814
894
 
@@ -818,9 +898,12 @@ class CATime
818
898
  # another {CATime} yields a {CATimedelta} at the finer of the two
819
899
  # units (cross-group falls to the fixed unit).
820
900
  # @param other [CATimedelta, CATime]
901
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
902
+ # duration or timestamp array)
821
903
  # @return [CATime, CATimedelta]
822
904
  # @raise [TypeError] on an unsupported operand.
823
905
  def -(other)
906
+ given, other = other, CATimeUnitAlgebra.operand(other)
824
907
  case other
825
908
  when CATimedelta
826
909
  (parent - CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
@@ -830,7 +913,7 @@ class CATime
830
913
  b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
831
914
  (a - b).timedelta(unit: u)
832
915
  else
833
- raise TypeError, "CATime - #{other.class} is not allowed"
916
+ raise TypeError, "CATime - #{given.class} is not allowed"
834
917
  end
835
918
  end
836
919
 
@@ -841,38 +924,19 @@ class CATime
841
924
  # base CArray#<=> composed from the (gated) > and <.
842
925
 
843
926
  # Element decode convention via the C-layer macro
844
- # `CA_FACE_STORAGE_TO_SCALAR_IF_FACE`: no Ruby `[]` override is needed, a
927
+ # `CA_FACE_STORAGE_TO_ELEMENT_IF_FACE`: no Ruby `[]` override is needed, a
845
928
  # scalar auto-decodes on all scalar-return paths such as `ca[i,j,k]` /
846
- # `ca[k]`. storage_to_scalar itself lives in C (ext/ca_obj_datetime.c)
929
+ # `ca[k]`. storage_to_element itself lives in C (ext/ca_obj_datetime.c)
847
930
  # so the hot path stays off Ruby method dispatch. Its write counterpart
848
- # is scalar_to_storage (below, surface -> storage on store).
931
+ # is element_to_storage (below, surface -> storage on store).
849
932
 
850
933
  # @!group Reductions
851
934
 
852
- # min / max are order-structure reductions: they ride the core reduce
853
- # Face gate (ORDERABLE storage descent + output re-lift, see
854
- # ext/mkkernel.rb face_gate: :relift), so no Ruby override is needed --
855
- # the inherited CArray#min / #max handle the Element / CATime return
856
- # shapes directly. minmax is the paired form; it has no core gate, so
857
- # re-lift both extremes here. They are actual elements, so they keep the
858
- # storage unit (no rounding, unlike mean).
859
-
860
- # @overload minmax(*axes, **opts)
861
- # Returns the earliest and latest time as a `[min, max]` pair (§8).
862
- # @return [Array(Element, Element), Array(CATime, CATime)]
863
- def minmax(*args, **opts)
864
- lo, hi = parent.minmax(*args, **opts)
865
- [relift_extremum(lo), relift_extremum(hi)]
866
- end
867
-
868
- def relift_extremum(r)
869
- case r
870
- when Integer then Element.new(r, unit)
871
- when CArray then r.time(unit: unit)
872
- else r
873
- end
874
- end
875
- private :relift_extremum
935
+ # min / max / minmax are order-structure reductions: they ride the core
936
+ # reduce Face gate (ORDERABLE storage descent + output re-lift, see
937
+ # ext/mkkernel.rb face_gate: :relift), so the inherited methods return the
938
+ # Element / CATime shapes directly. The extremes are actual elements, so
939
+ # they keep the storage unit (no rounding, unlike mean).
876
940
 
877
941
  # The centroid / spread / order reductions report on the array's own grid:
878
942
  # they run on the storage ticks and round the result back to the nearest
@@ -936,22 +1000,43 @@ class CATime
936
1000
 
937
1001
  # @overload sum(*)
938
1002
  # Not supported; use {#mean} for a centroid.
939
- # @raise [TypeError] always.
1003
+ # @raise [CArray::DataTypeError] always.
940
1004
  def sum(*)
941
- raise TypeError, "CATime#sum is ill-defined; use mean for centroid"
1005
+ raise CArray::DataTypeError, "CATime#sum is ill-defined; use mean for centroid"
942
1006
  end
943
1007
 
1008
+ # @overload accumulate(*)
1009
+ # Not supported, for the same reason as {#sum}.
1010
+ # @raise [CArray::DataTypeError] always.
1011
+ def accumulate(*); raise CArray::DataTypeError, "CATime#accumulate is ill-defined (a sum of instants)"; end
1012
+
1013
+ # @overload cumsum(*)
1014
+ # Not supported, for the same reason as {#sum}. The running position
1015
+ # relative to the first instant is `(t - t[0])` cumulated as durations.
1016
+ # @raise [CArray::DataTypeError] always.
1017
+ def cumsum(*); raise CArray::DataTypeError, "CATime#cumsum is ill-defined (a sum of instants)"; end
1018
+
1019
+ # @overload prod(*)
1020
+ # Not supported: a product of instants has no meaning.
1021
+ # @raise [CArray::DataTypeError] always.
1022
+ def prod(*); raise CArray::DataTypeError, "CATime#prod is ill-defined (a product of instants)"; end
1023
+
1024
+ # @overload cumprod(*)
1025
+ # Not supported, for the same reason as {#prod}.
1026
+ # @raise [CArray::DataTypeError] always.
1027
+ def cumprod(*); raise CArray::DataTypeError, "CATime#cumprod is ill-defined (a product of instants)"; end
1028
+
944
1029
  # @overload variance(*)
945
1030
  # Not supported: the variance of instants has squared-time units, which
946
1031
  # no type represents (ill-defined, like {#sum}). Use {#stddev} for the
947
1032
  # spread as a duration.
948
- # @raise [TypeError] always.
949
- def variance(*); raise TypeError, "CATime#variance is ill-defined (squared-time units); use stddev"; end
1033
+ # @raise [CArray::DataTypeError] always.
1034
+ def variance(*); raise CArray::DataTypeError, "CATime#variance is ill-defined (squared-time units); use stddev"; end
950
1035
 
951
1036
  # @overload variancep(*)
952
1037
  # Not supported, for the same reason as {#variance}. Use {#stddevp}.
953
- # @raise [TypeError] always.
954
- def variancep(*); raise TypeError, "CATime#variancep is ill-defined (squared-time units); use stddevp"; end
1038
+ # @raise [CArray::DataTypeError] always.
1039
+ def variancep(*); raise CArray::DataTypeError, "CATime#variancep is ill-defined (squared-time units); use stddevp"; end
955
1040
  # @!endgroup
956
1041
 
957
1042
  # sort / partition ride the core sort Face gate (ORDERABLE storage
@@ -969,11 +1054,15 @@ class CATime
969
1054
  # different unit or shape), so the gate routes the operand through here.
970
1055
  #
971
1056
  # Accepted operands: another {CATime} (unit-rescaled to self),
972
- # a {Element} (lifted to a length-1 CATime), a Ruby `Time`, and a
973
- # Ruby `DateTime` (both absolute instants converted to self's unit,
974
- # Unix epoch, UTC). A `String` is out of scope (parsing is a separate,
975
- # opposite-direction mechanism). A bare Integer / other type raises;
976
- # descend to `ca.parent` to compare the hidden storage directly.
1057
+ # a {Element} (lifted to a length-1 CATime), and an instant written
1058
+ # outside CArray -- a Ruby `Time`, `Date`, `DateTime`, or a `String` in
1059
+ # the grammar {CArray.time} reads (UTC unless it carries an offset).
1060
+ # Those are read at the precision they carry, not at self's unit, and
1061
+ # then go through the same conversion as a CATime: "2024-01-02 09:00"
1062
+ # against a day-unit array raises rather than matching 2024-01-02. A
1063
+ # bare Integer / other type raises; descend to `ca.parent` to compare
1064
+ # the hidden storage directly. An Array of any of these is a 1-D set
1065
+ # of instants (the value set of {#is_in}).
977
1066
  #
978
1067
  # The rescale is an INSTANT conversion (convert_instant!), lossless: a
979
1068
  # coarser->finer unit always converts; a finer->coarser unit converts only
@@ -983,10 +1072,24 @@ class CATime
983
1072
  # :D and finer, and a fixed operand coarsens to :M/:Y only when it sits on
984
1073
  # the calendar boundary (:W is the one exception -- month/year starts are
985
1074
  # not week-aligned, so :Y/:M <-> :W raises).
986
- # @param operand [CATime, CATime::Element, Time, DateTime]
1075
+ # @param operand [CATime, CATime::Element, Time, Date, DateTime, String, Array]
987
1076
  # @return [CATime] in self's unit.
988
1077
  # @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
1078
+ # Returns `[self, other]` in one unit that holds every value of both
1079
+ # exactly: the finer grid of two same-group units, or the fixed-length one
1080
+ # of a calendar / fixed pair (a month begins on a day). A lookup between
1081
+ # two arrays of different units (is_in, the set operations, locate_addr and
1082
+ # the joins on them) meets there, so it does not depend on which array is
1083
+ # the receiver.
1084
+ # @api private
1085
+ def in_common_unit_with (other)
1086
+ return [self, other] if other.unit == unit
1087
+ u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
1088
+ [to_unit(u), other.to_unit(u)]
1089
+ end
1090
+
989
1091
  def to_comparable (operand)
1092
+ require 'date' # Date is one of the operand kinds below
990
1093
  case operand
991
1094
  when CATime
992
1095
  return operand if operand.unit == unit
@@ -995,12 +1098,19 @@ class CATime
995
1098
  when CATime::Element
996
1099
  lifted = CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
997
1100
  to_comparable(lifted)
998
- when Time
999
- # Reuse the single-literal builder; it yields a length-1 CATime
1000
- # in the requested (= self's) resolution.
1001
- CArray.time(operand, unit: unit)
1002
- when defined?(DateTime) && DateTime
1003
- to_comparable(operand.to_time.utc)
1101
+ when Time, String, Date
1102
+ to_comparable(CATimeLiteral.exact_instant(operand))
1103
+ when Array
1104
+ # A set of instants written one by one (is_in(["2024-01-02", ...])).
1105
+ CATime.wrap(CA_INT64(operand.map { |v| to_comparable(v).parent[0] }),
1106
+ unit: unit)
1107
+ when CArray
1108
+ # An array that is not a time array: each value read as an instant (an
1109
+ # object column of Time values, a string column), a masked cell kept
1110
+ # masked.
1111
+ require "carray/methods/is_in"
1112
+ send(:values_in_face, self, operand) ||
1113
+ CArray.int64(*operand.shape).fill(UNDEF).time(unit: unit)
1004
1114
  else
1005
1115
  raise TypeError,
1006
1116
  "CATime cannot reconcile #{operand.class} " \
@@ -1008,23 +1118,22 @@ class CATime
1008
1118
  end
1009
1119
  end
1010
1120
 
1011
- # @overload scalar_to_storage(surface)
1012
- # Write-direction counterpart of storage_to_scalar (the store hook fired
1121
+ # @overload element_to_storage(surface)
1122
+ # Write-direction counterpart of storage_to_element (the store hook fired
1013
1123
  # from rb_ca_obj2ptr): brings a surface value object into this Face's
1014
1124
  # int64 storage (count in self's unit since the Unix epoch) so a scalar
1015
- # store round-trips with a fetch. A {Element} / `Time` / `DateTime` is
1016
- # reconciled to self's unit via {#to_comparable} (same lossless
1017
- # discipline: a cross-group unit or a non-exact finer->coarser cast
1018
- # raises). A bare Integer (the documented `.parent` raw-storage escape)
1019
- # and a String (parsing is a separate, opposite-direction mechanism)
1020
- # pass through unchanged to the storage cast.
1021
- # @param surface [Element, Time, DateTime, Integer, String]
1125
+ # store round-trips with a fetch. A {Element} / `Time` / `Date` /
1126
+ # `DateTime` / `String` is reconciled to self's unit via {#to_comparable}
1127
+ # (same lossless discipline: a cross-group unit or a non-exact
1128
+ # finer->coarser cast raises). A bare Integer (the documented `.parent`
1129
+ # raw-storage escape) passes through unchanged to the storage cast.
1130
+ # @param surface [Element, Time, Date, DateTime, Integer, String]
1022
1131
  # @return [Integer, Object] the storage-domain value, or `surface`
1023
1132
  # unchanged for a pass-through type.
1024
1133
  # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
1025
- def scalar_to_storage (surface)
1134
+ def element_to_storage (surface)
1026
1135
  case surface
1027
- when Integer, String
1136
+ when Integer
1028
1137
  surface
1029
1138
  else
1030
1139
  to_comparable(surface).parent[0]
@@ -1231,7 +1340,9 @@ class CATime
1231
1340
  # the centroid of month numbers (a caller wanting the day-space answer
1232
1341
  # writes to_unit(:D) first) -- and the result is rounded back to a tick.
1233
1342
  def reduce_on_own_grid(op, kind, args, opts)
1234
- round_and_relift(parent.public_send(op, *args, **opts), kind)
1343
+ with_surface_fill(opts) do |o|
1344
+ round_and_relift(parent.public_send(op, *args, **o), kind)
1345
+ end
1235
1346
  end
1236
1347
 
1237
1348
  # Round-to-nearest onto the storage grid, then wear the Face again. An
@@ -1256,6 +1367,41 @@ class CATime
1256
1367
  end
1257
1368
  end
1258
1369
 
1370
+ # inspect's name for the array: the class and the tick, which the storage
1371
+ # type name (fixlen[8]) does not tell.
1372
+ private def inspect_label
1373
+ "#{self.class}[#{unit}]"
1374
+ end
1375
+
1376
+ # inspect's cell: the instant as Element#to_s writes it, but with the
1377
+ # digits of a second that some cell of the array needs, the same in every
1378
+ # cell so that the cells line up; the unit is in the label.
1379
+ private def inspect_cell_formatter
1380
+ digits = inspect_second_digits
1381
+ lambda do |e|
1382
+ case e.unit.base
1383
+ when :Y, :M, :W, :D then e.to_s
1384
+ else e.to_time.iso8601(digits)
1385
+ end
1386
+ end
1387
+ end
1388
+
1389
+ # The digits of a second inspect writes: 0, 3, 6 or 9, the fewest that
1390
+ # show every present cell exactly (9 at most, as Time holds).
1391
+ private def inspect_second_digits
1392
+ base_digits = { ms: 3, us: 6, ns: 9, ps: 12, fs: 15, as: 18 }[unit.base]
1393
+ return 0 unless base_digits
1394
+ count = unit.count
1395
+ [0, 3, 6, 9].each do |d|
1396
+ break if d >= base_digits
1397
+ m = 10**(base_digits - d)
1398
+ # ticks * count is a multiple of m when ticks is one of m / gcd(m, count),
1399
+ # which does not multiply out of int64.
1400
+ return d if (ticks % (m / m.gcd(count))).eq(0).all
1401
+ end
1402
+ [base_digits, 9].min
1403
+ end
1404
+
1259
1405
  end
1260
1406
 
1261
1407
  # ============================================================================
@@ -1302,9 +1448,12 @@ class CATimedelta
1302
1448
  # {CATimedelta}; a {CATime} operand delegates to
1303
1449
  # {CATime#+} (commutative).
1304
1450
  # @param other [CATimedelta, CATime]
1451
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1452
+ # duration or timestamp array)
1305
1453
  # @return [CATimedelta, CATime]
1306
1454
  # @raise [TypeError] on incompatible operands.
1307
1455
  def +(other)
1456
+ given, other = other, CATimeUnitAlgebra.operand(other)
1308
1457
  case other
1309
1458
  when CATimedelta
1310
1459
  u = common_duration_unit(other.unit)
@@ -1314,7 +1463,7 @@ class CATimedelta
1314
1463
  when CATime
1315
1464
  other + self # commutative -> time's unit
1316
1465
  else
1317
- raise TypeError, "CATimedelta + #{other.class} is not allowed"
1466
+ raise TypeError, "CATimedelta + #{given.class} is not allowed"
1318
1467
  end
1319
1468
  end
1320
1469
 
@@ -1322,9 +1471,12 @@ class CATimedelta
1322
1471
  # Returns the difference of two {CATimedelta} at the finer of the two
1323
1472
  # units (a cross-group pair raises).
1324
1473
  # @param other [CATimedelta]
1474
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1475
+ # duration array)
1325
1476
  # @return [CATimedelta]
1326
1477
  # @raise [TypeError] on a non-timedelta operand.
1327
1478
  def -(other)
1479
+ given, other = other, CATimeUnitAlgebra.operand(other)
1328
1480
  case other
1329
1481
  when CATimedelta
1330
1482
  u = common_duration_unit(other.unit)
@@ -1332,7 +1484,7 @@ class CATimedelta
1332
1484
  b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1333
1485
  (a - b).timedelta(unit: u)
1334
1486
  else
1335
- raise TypeError, "CATimedelta - #{other.class} is not allowed"
1487
+ raise TypeError, "CATimedelta - #{given.class} is not allowed"
1336
1488
  end
1337
1489
  end
1338
1490
 
@@ -1368,9 +1520,12 @@ class CATimedelta
1368
1520
  # {CATimedelta}, by another {CATimedelta} with matching `unit`
1369
1521
  # yields a dimensionless CArray.
1370
1522
  # @param other [Integer, CATimedelta]
1523
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1524
+ # duration array)
1371
1525
  # @return [CATimedelta, CArray]
1372
1526
  # @raise [TypeError] on incompatible operands.
1373
1527
  def /(other)
1528
+ given, other = other, CATimeUnitAlgebra.operand(other)
1374
1529
  case other
1375
1530
  when Integer
1376
1531
  CATimeUnitAlgebra.trunc_divide(parent, other).timedelta(unit: unit)
@@ -1379,7 +1534,7 @@ class CATimedelta
1379
1534
  CATimeUnitAlgebra.trunc_divide(
1380
1535
  CATimeUnitAlgebra.convert_scale!(parent, unit, u),
1381
1536
  CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u))
1382
- else raise TypeError, "CATimedelta / #{other.class} is not allowed"
1537
+ else raise TypeError, "CATimedelta / #{given.class} is not allowed"
1383
1538
  end
1384
1539
  end
1385
1540
 
@@ -1395,14 +1550,41 @@ class CATimedelta
1395
1550
  # or as a {CATimedelta} view for per-axis reduction.
1396
1551
  # @return [Element, CATimedelta]
1397
1552
  def sum(*args, **opts)
1398
- round_and_relift(parent.sum(*args, **opts))
1553
+ with_surface_fill(opts) { |o| round_and_relift(parent.sum(*args, **o)) }
1554
+ end
1555
+
1556
+ # @overload accumulate(*args, **opts)
1557
+ # Returns the sum of durations in the storage's own integer type, as
1558
+ # {#sum} does in float64.
1559
+ # @return [Element, CATimedelta]
1560
+ def accumulate(*args, **opts)
1561
+ with_surface_fill(opts) { |o| round_and_relift(parent.accumulate(*args, **o)) }
1399
1562
  end
1400
1563
 
1564
+ # @overload cumsum(axis: nil)
1565
+ # Returns the running sum of durations as a {CATimedelta} on `self`'s
1566
+ # unit. A masked cell holds the running sum, as {CArray#cumsum} does.
1567
+ # @return [CATimedelta]
1568
+ def cumsum(*args, **opts)
1569
+ round_and_relift(parent.cumsum(*args, **opts))
1570
+ end
1571
+
1572
+ # @overload prod(*)
1573
+ # Not supported: a product of durations has the units of a power of
1574
+ # time, which no type represents.
1575
+ # @raise [CArray::DataTypeError] always.
1576
+ def prod(*); raise CArray::DataTypeError, "CATimedelta#prod is ill-defined (powers of time units)"; end
1577
+
1578
+ # @overload cumprod(*)
1579
+ # Not supported, for the same reason as {#prod}.
1580
+ # @raise [CArray::DataTypeError] always.
1581
+ def cumprod(*); raise CArray::DataTypeError, "CATimedelta#cumprod is ill-defined (powers of time units)"; end
1582
+
1401
1583
  # @overload mean(axis: nil, **opts)
1402
1584
  # Returns the mean duration rounded to the nearest unit count.
1403
1585
  # @return [Element, CATimedelta]
1404
1586
  def mean(*args, **opts)
1405
- round_and_relift(parent.mean(*args, **opts))
1587
+ with_surface_fill(opts) { |o| round_and_relift(parent.mean(*args, **o)) }
1406
1588
  end
1407
1589
 
1408
1590
  # median / percentile / quantile / stddev / stddevp report on this array's
@@ -1414,7 +1596,7 @@ class CATimedelta
1414
1596
  # Returns the median duration on `self`'s unit.
1415
1597
  # @return [Element, CATimedelta]
1416
1598
  def median(*args, **opts)
1417
- round_and_relift(parent.median(*args, **opts))
1599
+ with_surface_fill(opts) { |o| round_and_relift(parent.median(*args, **o)) }
1418
1600
  end
1419
1601
 
1420
1602
  # @overload percentile(*p, axis: nil, **opts)
@@ -1423,7 +1605,7 @@ class CATimedelta
1423
1605
  # two or more give an Array).
1424
1606
  # @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
1425
1607
  def percentile(*args, **opts)
1426
- round_and_relift(parent.percentile(*args, **opts))
1608
+ with_surface_fill(opts) { |o| round_and_relift(parent.percentile(*args, **o)) }
1427
1609
  end
1428
1610
 
1429
1611
  # @overload quantile(axis: nil, **opts)
@@ -1431,7 +1613,7 @@ class CATimedelta
1431
1613
  # `self`'s unit.
1432
1614
  # @return [Array<Element>, Array<CATimedelta>]
1433
1615
  def quantile(*args, **opts)
1434
- round_and_relift(parent.quantile(*args, **opts))
1616
+ with_surface_fill(opts) { |o| round_and_relift(parent.quantile(*args, **o)) }
1435
1617
  end
1436
1618
 
1437
1619
  # @overload stddev(axis: nil, **opts)
@@ -1440,7 +1622,7 @@ class CATimedelta
1440
1622
  # the precision matters.
1441
1623
  # @return [Element, CATimedelta]
1442
1624
  def stddev(*args, **opts)
1443
- round_and_relift(parent.stddev(*args, **opts))
1625
+ with_surface_fill(opts) { |o| round_and_relift(parent.stddev(*args, **o)) }
1444
1626
  end
1445
1627
 
1446
1628
  # @overload stddevp(axis: nil, **opts)
@@ -1448,7 +1630,7 @@ class CATimedelta
1448
1630
  # {#stddev}.
1449
1631
  # @return [Element, CATimedelta]
1450
1632
  def stddevp(*args, **opts)
1451
- round_and_relift(parent.stddevp(*args, **opts))
1633
+ with_surface_fill(opts) { |o| round_and_relift(parent.stddevp(*args, **o)) }
1452
1634
  end
1453
1635
 
1454
1636
  def round_and_relift(r)
@@ -1465,39 +1647,21 @@ class CATimedelta
1465
1647
  # Not supported: the variance of durations has squared-time units, which
1466
1648
  # no type represents -- the same reason {CATime#variance} refuses. Use
1467
1649
  # {#stddev} for the spread as a duration.
1468
- # @raise [TypeError] always.
1650
+ # @raise [CArray::DataTypeError] always.
1469
1651
  def variance(*)
1470
- raise TypeError, "CATimedelta#variance is ill-defined (squared-time units); use stddev"
1652
+ raise CArray::DataTypeError, "CATimedelta#variance is ill-defined (squared-time units); use stddev"
1471
1653
  end
1472
1654
 
1473
1655
  # @overload variancep(*)
1474
1656
  # Not supported, for the same reason as {#variance}. Use {#stddevp}.
1475
- # @raise [TypeError] always.
1657
+ # @raise [CArray::DataTypeError] always.
1476
1658
  def variancep(*)
1477
- raise TypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
1478
- end
1479
-
1480
- # min / max ride the core reduce Face gate (ORDERABLE storage descent +
1481
- # output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
1482
- # CArray#min / #max return the Element / CATimedelta shapes directly.
1483
- # minmax has no core gate, so re-lift both extremes here.
1484
-
1485
- # @overload minmax(*axes, **opts)
1486
- # Returns the shortest and longest duration as a `[min, max]` pair.
1487
- # @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
1488
- def minmax(*args, **opts)
1489
- lo, hi = parent.minmax(*args, **opts)
1490
- [relift_extremum(lo), relift_extremum(hi)]
1659
+ raise CArray::DataTypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
1491
1660
  end
1492
1661
 
1493
- def relift_extremum(r)
1494
- case r
1495
- when Integer then Element.new(r, unit)
1496
- when CArray then r.timedelta(unit: unit)
1497
- else r
1498
- end
1499
- end
1500
- private :relift_extremum
1662
+ # min / max / minmax ride the core reduce Face gate (ORDERABLE storage
1663
+ # descent + output re-lift); the inherited methods return the Element /
1664
+ # CATimedelta shapes directly.
1501
1665
  # @!endgroup
1502
1666
 
1503
1667
  # sort / partition ride the core sort Face gate (ORDERABLE storage
@@ -1518,6 +1682,23 @@ class CATimedelta
1518
1682
  # @param operand [CATimedelta, CATimedelta::Element]
1519
1683
  # @return [CATimedelta] in self's unit.
1520
1684
  # @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
1685
+ # Returns `[self, other]` in one unit, the finer grid of the two, which
1686
+ # holds every value of both exactly. A lookup between two duration
1687
+ # arrays of different units meets there, so it does not depend on which
1688
+ # array is the receiver. A calendar and a fixed-length duration have no
1689
+ # common unit and raise.
1690
+ # @api private
1691
+ def in_common_unit_with (other)
1692
+ return [self, other] if other.unit == unit
1693
+ unless CATimeUnitAlgebra.same_group?(unit, other.unit)
1694
+ raise ArgumentError,
1695
+ "cannot compare a #{unit} duration with a #{other.unit} duration: " \
1696
+ "a calendar length (months, years) has no fixed length in days"
1697
+ end
1698
+ u = CATimeUnitAlgebra.finer(unit, other.unit)
1699
+ [to_unit(u), other.to_unit(u)]
1700
+ end
1701
+
1521
1702
  def to_comparable (operand)
1522
1703
  case operand
1523
1704
  when CATimedelta
@@ -1534,8 +1715,8 @@ class CATimedelta
1534
1715
  end
1535
1716
  end
1536
1717
 
1537
- # @overload scalar_to_storage(surface)
1538
- # Write-direction counterpart of storage_to_scalar: brings a surface
1718
+ # @overload element_to_storage(surface)
1719
+ # Write-direction counterpart of storage_to_element: brings a surface
1539
1720
  # value object into this Face's int64 storage (count in self's unit) so a
1540
1721
  # scalar store round-trips with a fetch. A {Element} is reconciled to
1541
1722
  # self's unit via {#to_comparable} (lossless discipline; a `Time` /
@@ -1546,7 +1727,7 @@ class CATimedelta
1546
1727
  # @return [Integer, Object] the storage-domain value, or `surface`
1547
1728
  # unchanged for a pass-through type.
1548
1729
  # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
1549
- def scalar_to_storage (surface)
1730
+ def element_to_storage (surface)
1550
1731
  case surface
1551
1732
  when Integer, String
1552
1733
  surface
@@ -1592,6 +1773,17 @@ class CATimedelta
1592
1773
  end
1593
1774
  CATimeUnitAlgebra.finer(unit, ou)
1594
1775
  end
1776
+
1777
+ # inspect's name for the array: the class and the tick, which the storage
1778
+ # type name (fixlen[8]) does not tell.
1779
+ private def inspect_label
1780
+ "#{self.class}[#{unit}]"
1781
+ end
1782
+
1783
+ # inspect's cell: the duration in the base unit of the tick.
1784
+ private def inspect_cell_formatter
1785
+ lambda { |e| "#{e.value * e.unit.count}#{e.unit.base}" }
1786
+ end
1595
1787
  end
1596
1788
 
1597
1789
  # Reads a time literal (Time / DateTime / Integer unix-seconds / String) into
@@ -1604,8 +1796,12 @@ module CATimeLiteral
1604
1796
 
1605
1797
  # Date fields of a String literal, UTC. Ruby's Date._parse does not read
1606
1798
  # the calendar-grid forms CATime#to_s prints -- "2019-09" comes back as a
1607
- # month of 20 with a zone, and a bare "2019" as a month and a day -- so
1608
- # those two are read here and everything else is left to Date._parse.
1799
+ # month of 20 with a zone, and a bare "2019" as a month and a day -- nor an
1800
+ # hour alone after the date ("2024-01-01T05", ISO 8601 reduced to the
1801
+ # hour), so those are read here and everything else is left to Date._parse.
1802
+ # Date._parse passes over what it cannot read, so text with a time after
1803
+ # the date that comes back without an hour is refused rather than read as
1804
+ # midnight.
1609
1805
  #
1610
1806
  # What Date._parse returns is then checked, because an out-of-range field
1611
1807
  # used to flow into the civil kernel and normalise into a different date:
@@ -1617,23 +1813,71 @@ module CATimeLiteral
1617
1813
  if format then Date._strptime(spec, format)
1618
1814
  elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
1619
1815
  elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
1816
+ elsif (hour = hour_only_fields(spec)) then hour
1620
1817
  else Date._parse(spec)
1621
1818
  end
1622
1819
  unless h && h[:year]
1623
1820
  raise ArgumentError, "cannot parse time #{spec.inspect}"
1624
1821
  end
1822
+ if !format && !h[:hour] &&
1823
+ (spec =~ /\d\s*T\s*\d/ || spec =~ /\A\s*[+-]?\d{4,}[-\/]\d{1,2}[-\/]\d{1,2}\s+\d/)
1824
+ raise ArgumentError, "cannot parse time #{spec.inspect}: the time after the date is not read"
1825
+ end
1826
+ # Text left over after the format is not part of a time in that format.
1827
+ if h[:leftover] && !h[:leftover].strip.empty?
1828
+ raise ArgumentError,
1829
+ "cannot parse time #{spec.inspect}: #{h[:leftover].inspect} is " \
1830
+ "left over after #{format.inspect}"
1831
+ end
1625
1832
  y, m, d = h[:year], h[:mon] || 1, h[:mday] || 1
1626
1833
  unless (1..12).cover?(m) && Date.valid_date?(y, m, d, Date::GREGORIAN)
1627
1834
  raise ArgumentError,
1628
1835
  "cannot parse time #{spec.inspect}: it reads as year #{y}, " \
1629
1836
  "month #{m}, day #{d}, which is not a date"
1630
1837
  end
1838
+ # Hour 24 is the end of the day, 24:00:00, and nothing past it.
1839
+ if h[:hour] == 24 && [h[:min], h[:sec], h[:sec_fraction]].any? { |v| v && v != 0 }
1840
+ raise ArgumentError,
1841
+ "cannot parse time #{spec.inspect}: hour 24 is only 24:00:00"
1842
+ end
1631
1843
  h
1632
1844
  end
1633
1845
  private_class_method :parse_date_fields
1634
1846
 
1847
+ # "YYYY-MM-DDThh" with an optional zone ("Z", "+hh", "+hhmm", "+hh:mm").
1848
+ def hour_only_fields(spec)
1849
+ m = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2})(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?\z/.match(spec)
1850
+ return nil unless m
1851
+ h = { year: m[1].to_i, mon: m[2].to_i, mday: m[3].to_i, hour: m[4].to_i }
1852
+ raise ArgumentError, "cannot parse time #{spec.inspect}: hour #{h[:hour]}" if h[:hour] > 23
1853
+ if m[6]
1854
+ oh, om = m[7].to_i, m[8].to_i
1855
+ raise ArgumentError, "cannot parse time #{spec.inspect}: offset" if oh > 23 || om > 59
1856
+ h[:offset] = (oh * 3600 + om * 60) * (m[6] == "-" ? -1 : 1)
1857
+ elsif m[5]
1858
+ h[:offset] = 0
1859
+ end
1860
+ h
1861
+ end
1862
+ private_class_method :hour_only_fields
1863
+
1864
+ # An instant written outside CArray (Time / Date / DateTime / String) as a
1865
+ # length-1 CATime in the coarsest of :s / :ms / :us / :ns that holds it
1866
+ # exactly, so a comparison sees the instant it names and not that instant
1867
+ # floored onto some other grid. Finer than a nanosecond raises.
1868
+ def exact_instant(spec)
1869
+ sec = epoch_seconds(spec)
1870
+ [:s, :ms, :us, :ns].each_with_index do |u, k|
1871
+ ticks = sec * (10 ** (3 * k))
1872
+ if ticks.denominator == 1
1873
+ return CATime.wrap(CA_INT64([ticks.to_i]), unit: u)
1874
+ end
1875
+ end
1876
+ raise ArgumentError, "#{spec.inspect} is finer than a nanosecond"
1877
+ end
1878
+
1635
1879
  # Exact Rational seconds since the Unix epoch for a start literal (Time /
1636
- # DateTime / Integer unix-seconds / String). UTC default.
1880
+ # Date / DateTime / Integer unix-seconds / String). UTC default.
1637
1881
  def epoch_seconds(spec, format = nil)
1638
1882
  require 'date'
1639
1883
  require 'time'
@@ -1654,8 +1898,7 @@ module CATimeLiteral
1654
1898
  end
1655
1899
  when String
1656
1900
  h = parse_date_fields(spec, format)
1657
- days = CATimeCivil.days_from_civil(CA_INT64([h[:year]]), CA_INT64([h[:mon] || 1]),
1658
- CA_INT64([h[:mday] || 1]))[0]
1901
+ days = CATimeCivil.days_from_civil_1(h[:year], h[:mon] || 1, h[:mday] || 1)
1659
1902
  sec = Rational(days * 86400 + (h[:hour] || 0) * 3600 +
1660
1903
  (h[:min] || 0) * 60 + (h[:sec] || 0))
1661
1904
  sec += h[:sec_fraction] if h[:sec_fraction]
@@ -1664,6 +1907,8 @@ module CATimeLiteral
1664
1907
  else
1665
1908
  if defined?(DateTime) && spec.is_a?(DateTime)
1666
1909
  spec.to_time.to_r
1910
+ elsif defined?(Date) && spec.is_a?(Date)
1911
+ Rational(CATimeCivil.days_from_civil_1(spec.year, spec.month, spec.day) * 86400)
1667
1912
  else
1668
1913
  raise ArgumentError, "cannot parse time #{spec.class}"
1669
1914
  end
@@ -1711,6 +1956,189 @@ module CATimeLiteral
1711
1956
  end
1712
1957
  end
1713
1958
 
1959
+ # A format :infer can choose: a strptime format, read strictly (see
1960
+ # __strptime_fields__), and what the text carries (:date, :clock, or
1961
+ # :fraction of a second).
1962
+ TimeFormat = Struct.new(:format, :kind)
1963
+
1964
+ INFER_DATES = %w[%d/%m/%Y %m/%d/%Y %d-%m-%Y %m-%d-%Y %d.%m.%Y %d/%m/%y %m/%d/%y] +
1965
+ ["%d %b %Y", "%d-%b-%Y", "%d-%b-%y", "%b %d, %Y", "%b %d %Y", "%Y%m%d"]
1966
+
1967
+ # A space in a format matches any run of spaces, none included.
1968
+ INFER_CLOCKS = [
1969
+ ["%H:%M", :clock], ["%H:%M:%S", :clock], ["%H:%M:%S.%N", :fraction],
1970
+ ["%I:%M %p", :clock], ["%I:%M:%S %p", :clock],
1971
+ ].freeze
1972
+
1973
+ # A time after a date: none, or a clock with or without a zone. %z takes
1974
+ # an offset, Z, UTC or GMT, not a zone named for a place (some of those
1975
+ # names mean different offsets in different places).
1976
+ INFER_TIMES = ([["", :date]] +
1977
+ INFER_CLOCKS.flat_map { |clock, kind| [[" #{clock}", kind], [" #{clock} %z", kind]] }
1978
+ ).freeze
1979
+
1980
+ # A date written in Japanese, year first ("2024<year>1<month>2<day>"),
1981
+ # takes a time in the same markers ("3<hour>4<minute>", "...5<second>")
1982
+ # or with colons.
1983
+ JA_YEAR, JA_MONTH, JA_DAY = "年", "月", "日"
1984
+ JA_HOUR, JA_MINUTE, JA_SECOND = "時", "分", "秒"
1985
+ INFER_JA_TIMES = ([["", :date]] +
1986
+ ([["%H#{JA_HOUR}%M#{JA_MINUTE}", :clock],
1987
+ ["%H#{JA_HOUR}%M#{JA_MINUTE}%S#{JA_SECOND}", :clock]] + INFER_CLOCKS)
1988
+ .map { |clock, kind| [" #{clock}", kind] }).freeze
1989
+
1990
+ INFER_FORMATS = (
1991
+ INFER_DATES.product(INFER_TIMES).map { |date, (time, kind)|
1992
+ TimeFormat.new(date + time, kind)
1993
+ } +
1994
+ INFER_JA_TIMES.map { |time, kind|
1995
+ TimeFormat.new("%Y#{JA_YEAR}%m#{JA_MONTH}%d#{JA_DAY}#{time}", kind)
1996
+ }
1997
+ ).freeze
1998
+ INFER_FORMAT_STRINGS = INFER_FORMATS.map(&:format).freeze
1999
+
2000
+ # The one format of INFER_FORMATS that the text of object array `x` is
2001
+ # written in, `first` being its first present cell, as
2002
+ # [format, fields, address]: the fields __strptime_fields__ read in it,
2003
+ # and the address of the first cell not in it, or nil.
2004
+ #
2005
+ # The first cell gives the candidates and each later cell drops those it
2006
+ # does not fit, until one is left. Each candidate is read over the whole
2007
+ # array once, and the candidate a later cell leaves last is the one left:
2008
+ # no candidate for the first cell or none left (the last ones dropped by
2009
+ # one cell) raises CAFrame::UnreadableColumn, and more than one left at the
2010
+ # end raises CAFrame::AmbiguousTimeFormat (CAFrame is its only caller). A cell
2011
+ # after the choice that is not in the format is returned for the caller
2012
+ # to report.
2013
+ def infer_time_format(x, first)
2014
+ candidates = CArray.__strptime_fits__(first, INFER_FORMAT_STRINGS)
2015
+ if candidates.empty?
2016
+ raise CAFrame::UnreadableColumn,
2017
+ "cannot infer a time format from #{first.inspect}; " \
2018
+ "pass a strptime format, or :mixed to guess at each cell"
2019
+ end
2020
+ runs = candidates.map do |i|
2021
+ fields = x.__strptime_fields__(INFER_FORMATS[i].format, true)
2022
+ [INFER_FORMATS[i], fields, fields[7].first || Float::INFINITY]
2023
+ end
2024
+ last = runs.map(&:last).max
2025
+ left = runs.select { |run| run[2] == last }
2026
+ if left.size > 1
2027
+ if last == Float::INFINITY
2028
+ formats = left.map { |run| run[0].format }
2029
+ raise CAFrame::AmbiguousTimeFormat.new(
2030
+ "#{first.inspect} could be read as " \
2031
+ "#{formats.map(&:inspect).join(' or ')}, " \
2032
+ "and no cell tells which; pass the format",
2033
+ formats: formats)
2034
+ end
2035
+ raise CAFrame::UnreadableColumn,
2036
+ "#{x.flatten[last].inspect} is not in the format of #{first.inspect}; " \
2037
+ "pass a strptime format, or :mixed to guess at each cell"
2038
+ end
2039
+ f, fields, bad = left.first
2040
+ [f, fields, bad == Float::INFINITY ? nil : bad]
2041
+ end
2042
+
2043
+ # [kind, per] for __time_ticks_from_fields__: ticks of `per` nanoseconds
2044
+ # (:fixed) or months (:calendar), or nil when `res` is finer than a
2045
+ # nanosecond.
2046
+ def tick_kind(res)
2047
+ if CATimeUnitAlgebra::FIXED.key?(res.base)
2048
+ per = res.tick_ratio * 1_000_000_000
2049
+ kind = :fixed
2050
+ else
2051
+ per = res.tick_ratio
2052
+ kind = :calendar
2053
+ end
2054
+ (per.denominator == 1 && per >= 1) ? [kind, per.to_i] : nil
2055
+ end
2056
+
2057
+ # The ticks of every cell of `x` on the `res` grid, as an int64 array of
2058
+ # x's shape (missing and, under on_error: :mask, unparseable cells
2059
+ # masked), or nil when `res` is finer than a nanosecond. A strptime
2060
+ # format that __strptime_fields__ reads is read for the whole array in C;
2061
+ # otherwise each String is parsed into fields here. Either way the fields
2062
+ # become ticks for the whole array at once, and a cell that is not a
2063
+ # String goes through tick_index.
2064
+ def ticks_from_cells(x, res, format, on_error)
2065
+ require 'date'
2066
+ kind, per = tick_kind(res)
2067
+ return nil unless kind
2068
+ if format.is_a?(String)
2069
+ text = (x.data_type == CA_OBJECT && !x.face?) ? x : x.to_type(:object)
2070
+ fields = text.__strptime_fields__(format, false)
2071
+ return ticks_from_fields(fields, x, res, format, on_error) if fields
2072
+ end
2073
+ cells = x.flatten.to_a
2074
+ n = cells.size
2075
+ year = Array.new(n, 1970)
2076
+ mon = Array.new(n, 1)
2077
+ day = Array.new(n, 1)
2078
+ sod = Array.new(n, 0)
2079
+ frac = Array.new(n, 0)
2080
+ off = Array.new(n, 0)
2081
+ present = Array.new(n, 0)
2082
+ direct = {}
2083
+ cells.each_with_index do |s, i|
2084
+ next if s.nil? || UNDEF.equal?(s)
2085
+ begin
2086
+ if s.is_a?(String)
2087
+ h = parse_date_fields(s, format)
2088
+ year[i] = h[:year]
2089
+ mon[i] = h[:mon] || 1
2090
+ day[i] = h[:mday] || 1
2091
+ sod[i] = (h[:hour] || 0) * 3600 + (h[:min] || 0) * 60 + (h[:sec] || 0)
2092
+ frac[i] = (h[:sec_fraction] * 1_000_000_000).floor if h[:sec_fraction]
2093
+ off[i] = h[:offset] if h[:offset]
2094
+ present[i] = 1
2095
+ else
2096
+ direct[i] = tick_index(s, res, format)
2097
+ end
2098
+ rescue ArgumentError, TypeError
2099
+ raise if on_error == :raise
2100
+ end
2101
+ end
2102
+ raw = CArray.__time_ticks_from_fields__(
2103
+ CA_INT64(year), CA_INT64(mon), CA_INT64(day), CA_INT64(sod),
2104
+ CA_INT64(frac), CA_INT64(off), CA_UINT8(present), kind, per)
2105
+ check_ticks_fit(raw, present, present.count(1), res) { cells }
2106
+ direct.each { |i, t| raw[i] = t }
2107
+ raw.reshape(*x.shape)
2108
+ end
2109
+
2110
+ # Ticks from what __strptime_fields__ read; the cells it deferred are
2111
+ # read one by one through tick_index.
2112
+ def ticks_from_fields(fields, x, res, format, on_error)
2113
+ kind, per = tick_kind(res)
2114
+ year, mon, day, sod, frac, off, present, unreadable, deferred, = fields
2115
+ cells = nil
2116
+ if on_error == :raise && !unreadable.empty?
2117
+ cells = x.flatten.to_a
2118
+ raise ArgumentError, "cannot parse time #{cells[unreadable.first].inspect}"
2119
+ end
2120
+ raw = CArray.__time_ticks_from_fields__(year, mon, day, sod, frac, off,
2121
+ present, kind, per)
2122
+ check_ticks_fit(raw, present, present.sum, res) { cells ||= x.flatten.to_a }
2123
+ unless deferred.empty?
2124
+ cells ||= x.flatten.to_a
2125
+ deferred.each do |i|
2126
+ raw[i] = tick_index(cells[i], res, format)
2127
+ rescue ArgumentError, TypeError
2128
+ raise if on_error == :raise
2129
+ end
2130
+ end
2131
+ raw.reshape(*x.shape)
2132
+ end
2133
+
2134
+ # A tick that does not fit int64 is out of range, not unparseable.
2135
+ def check_ticks_fit(raw, present, present_count, res)
2136
+ return if raw.count_not_masked >= present_count
2137
+ cells = yield
2138
+ i = (0...raw.elements).find { |k| present[k] == 1 && UNDEF.equal?(raw[k]) }
2139
+ raise RangeError, "time #{cells[i].inspect} does not fit int64 ticks of #{res}"
2140
+ end
2141
+
1714
2142
  # Single-literal build for {.time}: a 1-element CATime, honouring
1715
2143
  # the on_error policy (raise, or a masked cell).
1716
2144
  def to_time_array(literal, res, format, on_error)
@@ -1820,6 +2248,8 @@ class CArray
1820
2248
  unless x.is_a?(CArray)
1821
2249
  return CATimeLiteral.to_time_array(x, res, format, on_error)
1822
2250
  end
2251
+ raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error)
2252
+ return raw.time(unit: res) if raw
1823
2253
  raw = CArray.int64(*x.shape)
1824
2254
  x.each_index do |*idx|
1825
2255
  s = x[*idx]