carray 3.0.2 → 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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  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 +66 -71
  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 +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  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 +132 -22
  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 +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -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 +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  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 +543 -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 +47 -49
  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 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  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 +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
data/lib/carray/time.rb CHANGED
@@ -93,6 +93,19 @@ module CATimeUnitAlgebra
93
93
 
94
94
  module_function
95
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
+
96
109
  # seconds- (fixed) or months- (calendar) per base tick.
97
110
  def base_ratio(base)
98
111
  FIXED[base] || CALENDAR[base]
@@ -208,7 +221,7 @@ module CATimeUnitAlgebra
208
221
  # fine->coarse divides only when every value is exact; cross-group ALWAYS
209
222
  # raises -- a :M / :Y duration has no fixed ratio to days (a month is
210
223
  # calendar-variable), so it genuinely cannot scale to seconds.
211
- def convert_scale!(storage, from, to)
224
+ def convert_scale!(storage, from, to, instant: false)
212
225
  a = res(from); b = res(to)
213
226
  return storage if a == b
214
227
  r = ratio(a, b)
@@ -221,6 +234,11 @@ module CATimeUnitAlgebra
221
234
  scaled = widen(storage, r.numerator) # coarse -> fine: lossless multiply
222
235
  return scaled if r.denominator == 1
223
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
224
242
  raise ArgumentError,
225
243
  "cannot scale duration #{a} to #{b} without loss: " \
226
244
  "some values are not a whole multiple of #{b} " \
@@ -271,7 +289,7 @@ module CATimeUnitAlgebra
271
289
  def convert_instant!(storage, from, to)
272
290
  a = res(from); b = res(to)
273
291
  return storage if a == b
274
- 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
275
293
  if CALENDAR.key?(a.base)
276
294
  convert_instant_calendar_to_fixed(storage, a, b) # widen
277
295
  else
@@ -385,7 +403,48 @@ module CATimeUnitAlgebra
385
403
  end
386
404
  end
387
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
+
388
446
  class CATime
447
+ include CATimeSurfaceFill
389
448
  # @overload new(*shape, unit: :ns)
390
449
  # Allocates a new int64 storage CArray of the given `shape` and
391
450
  # wraps it as a CATime Face with the given `unit`. The
@@ -472,7 +531,7 @@ class CATime
472
531
  # @return [String]
473
532
  def to_s
474
533
  case unit.base
475
- when :Y then format("%04d", to_date.year)
534
+ when :Y then to_date.strftime("%Y")
476
535
  when :M then to_date.strftime("%Y-%m")
477
536
  when :W, :D then to_date.strftime("%Y-%m-%d")
478
537
  else to_time.iso8601(fractional_second_digits) # :h .. :as (time shown)
@@ -602,6 +661,7 @@ class CATime
602
661
  end
603
662
 
604
663
  class CATimedelta
664
+ include CATimeSurfaceFill
605
665
  # @overload new(*shape, unit: :ns)
606
666
  # Allocates a new int64 storage CArray of the given `shape` and
607
667
  # wraps it as a CATimedelta Face with the given `unit`.
@@ -816,16 +876,19 @@ class CATime
816
876
  # cross-group calendar duration raises). Adding two datetimes is
817
877
  # ill-defined.
818
878
  # @param other [CATimedelta]
879
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
880
+ # duration array)
819
881
  # @return [CATime]
820
882
  # @raise [TypeError] on a non-timedelta operand.
821
883
  def +(other)
884
+ given, other = other, CATimeUnitAlgebra.operand(other)
822
885
  case other
823
886
  when CATimedelta
824
887
  (parent + CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
825
888
  when CATime
826
889
  raise TypeError, "CATime + CATime is ill-defined"
827
890
  else
828
- raise TypeError, "CATime + #{other.class} is not allowed (use CATimedelta)"
891
+ raise TypeError, "CATime + #{given.class} is not allowed (use CATimedelta)"
829
892
  end
830
893
  end
831
894
 
@@ -835,9 +898,12 @@ class CATime
835
898
  # another {CATime} yields a {CATimedelta} at the finer of the two
836
899
  # units (cross-group falls to the fixed unit).
837
900
  # @param other [CATimedelta, CATime]
901
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
902
+ # duration or timestamp array)
838
903
  # @return [CATime, CATimedelta]
839
904
  # @raise [TypeError] on an unsupported operand.
840
905
  def -(other)
906
+ given, other = other, CATimeUnitAlgebra.operand(other)
841
907
  case other
842
908
  when CATimedelta
843
909
  (parent - CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
@@ -847,7 +913,7 @@ class CATime
847
913
  b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
848
914
  (a - b).timedelta(unit: u)
849
915
  else
850
- raise TypeError, "CATime - #{other.class} is not allowed"
916
+ raise TypeError, "CATime - #{given.class} is not allowed"
851
917
  end
852
918
  end
853
919
 
@@ -858,38 +924,19 @@ class CATime
858
924
  # base CArray#<=> composed from the (gated) > and <.
859
925
 
860
926
  # Element decode convention via the C-layer macro
861
- # `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
862
928
  # scalar auto-decodes on all scalar-return paths such as `ca[i,j,k]` /
863
- # `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)
864
930
  # so the hot path stays off Ruby method dispatch. Its write counterpart
865
- # is scalar_to_storage (below, surface -> storage on store).
931
+ # is element_to_storage (below, surface -> storage on store).
866
932
 
867
933
  # @!group Reductions
868
934
 
869
- # min / max are order-structure reductions: they ride the core reduce
870
- # Face gate (ORDERABLE storage descent + output re-lift, see
871
- # ext/mkkernel.rb face_gate: :relift), so no Ruby override is needed --
872
- # the inherited CArray#min / #max handle the Element / CATime return
873
- # shapes directly. minmax is the paired form; it has no core gate, so
874
- # re-lift both extremes here. They are actual elements, so they keep the
875
- # storage unit (no rounding, unlike mean).
876
-
877
- # @overload minmax(*axes, **opts)
878
- # Returns the earliest and latest time as a `[min, max]` pair (§8).
879
- # @return [Array(Element, Element), Array(CATime, CATime)]
880
- def minmax(*args, **opts)
881
- lo, hi = parent.minmax(*args, **opts)
882
- [relift_extremum(lo), relift_extremum(hi)]
883
- end
884
-
885
- def relift_extremum(r)
886
- case r
887
- when Integer then Element.new(r, unit)
888
- when CArray then r.time(unit: unit)
889
- else r
890
- end
891
- end
892
- 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).
893
940
 
894
941
  # The centroid / spread / order reductions report on the array's own grid:
895
942
  # they run on the storage ticks and round the result back to the nearest
@@ -953,22 +1000,43 @@ class CATime
953
1000
 
954
1001
  # @overload sum(*)
955
1002
  # Not supported; use {#mean} for a centroid.
956
- # @raise [TypeError] always.
1003
+ # @raise [CArray::DataTypeError] always.
957
1004
  def sum(*)
958
- raise TypeError, "CATime#sum is ill-defined; use mean for centroid"
1005
+ raise CArray::DataTypeError, "CATime#sum is ill-defined; use mean for centroid"
959
1006
  end
960
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
+
961
1029
  # @overload variance(*)
962
1030
  # Not supported: the variance of instants has squared-time units, which
963
1031
  # no type represents (ill-defined, like {#sum}). Use {#stddev} for the
964
1032
  # spread as a duration.
965
- # @raise [TypeError] always.
966
- 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
967
1035
 
968
1036
  # @overload variancep(*)
969
1037
  # Not supported, for the same reason as {#variance}. Use {#stddevp}.
970
- # @raise [TypeError] always.
971
- 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
972
1040
  # @!endgroup
973
1041
 
974
1042
  # sort / partition ride the core sort Face gate (ORDERABLE storage
@@ -986,11 +1054,15 @@ class CATime
986
1054
  # different unit or shape), so the gate routes the operand through here.
987
1055
  #
988
1056
  # Accepted operands: another {CATime} (unit-rescaled to self),
989
- # a {Element} (lifted to a length-1 CATime), a Ruby `Time`, and a
990
- # Ruby `DateTime` (both absolute instants converted to self's unit,
991
- # Unix epoch, UTC). A `String` is out of scope (parsing is a separate,
992
- # opposite-direction mechanism). A bare Integer / other type raises;
993
- # 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}).
994
1066
  #
995
1067
  # The rescale is an INSTANT conversion (convert_instant!), lossless: a
996
1068
  # coarser->finer unit always converts; a finer->coarser unit converts only
@@ -1000,10 +1072,24 @@ class CATime
1000
1072
  # :D and finer, and a fixed operand coarsens to :M/:Y only when it sits on
1001
1073
  # the calendar boundary (:W is the one exception -- month/year starts are
1002
1074
  # not week-aligned, so :Y/:M <-> :W raises).
1003
- # @param operand [CATime, CATime::Element, Time, DateTime]
1075
+ # @param operand [CATime, CATime::Element, Time, Date, DateTime, String, Array]
1004
1076
  # @return [CATime] in self's unit.
1005
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
+
1006
1091
  def to_comparable (operand)
1092
+ require 'date' # Date is one of the operand kinds below
1007
1093
  case operand
1008
1094
  when CATime
1009
1095
  return operand if operand.unit == unit
@@ -1012,12 +1098,19 @@ class CATime
1012
1098
  when CATime::Element
1013
1099
  lifted = CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
1014
1100
  to_comparable(lifted)
1015
- when Time
1016
- # Reuse the single-literal builder; it yields a length-1 CATime
1017
- # in the requested (= self's) resolution.
1018
- CArray.time(operand, unit: unit)
1019
- when defined?(DateTime) && DateTime
1020
- 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)
1021
1114
  else
1022
1115
  raise TypeError,
1023
1116
  "CATime cannot reconcile #{operand.class} " \
@@ -1025,23 +1118,22 @@ class CATime
1025
1118
  end
1026
1119
  end
1027
1120
 
1028
- # @overload scalar_to_storage(surface)
1029
- # 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
1030
1123
  # from rb_ca_obj2ptr): brings a surface value object into this Face's
1031
1124
  # int64 storage (count in self's unit since the Unix epoch) so a scalar
1032
- # store round-trips with a fetch. A {Element} / `Time` / `DateTime` is
1033
- # reconciled to self's unit via {#to_comparable} (same lossless
1034
- # discipline: a cross-group unit or a non-exact finer->coarser cast
1035
- # raises). A bare Integer (the documented `.parent` raw-storage escape)
1036
- # and a String (parsing is a separate, opposite-direction mechanism)
1037
- # pass through unchanged to the storage cast.
1038
- # @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]
1039
1131
  # @return [Integer, Object] the storage-domain value, or `surface`
1040
1132
  # unchanged for a pass-through type.
1041
1133
  # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
1042
- def scalar_to_storage (surface)
1134
+ def element_to_storage (surface)
1043
1135
  case surface
1044
- when Integer, String
1136
+ when Integer
1045
1137
  surface
1046
1138
  else
1047
1139
  to_comparable(surface).parent[0]
@@ -1248,7 +1340,9 @@ class CATime
1248
1340
  # the centroid of month numbers (a caller wanting the day-space answer
1249
1341
  # writes to_unit(:D) first) -- and the result is rounded back to a tick.
1250
1342
  def reduce_on_own_grid(op, kind, args, opts)
1251
- 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
1252
1346
  end
1253
1347
 
1254
1348
  # Round-to-nearest onto the storage grid, then wear the Face again. An
@@ -1273,6 +1367,41 @@ class CATime
1273
1367
  end
1274
1368
  end
1275
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
+
1276
1405
  end
1277
1406
 
1278
1407
  # ============================================================================
@@ -1319,9 +1448,12 @@ class CATimedelta
1319
1448
  # {CATimedelta}; a {CATime} operand delegates to
1320
1449
  # {CATime#+} (commutative).
1321
1450
  # @param other [CATimedelta, CATime]
1451
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1452
+ # duration or timestamp array)
1322
1453
  # @return [CATimedelta, CATime]
1323
1454
  # @raise [TypeError] on incompatible operands.
1324
1455
  def +(other)
1456
+ given, other = other, CATimeUnitAlgebra.operand(other)
1325
1457
  case other
1326
1458
  when CATimedelta
1327
1459
  u = common_duration_unit(other.unit)
@@ -1331,7 +1463,7 @@ class CATimedelta
1331
1463
  when CATime
1332
1464
  other + self # commutative -> time's unit
1333
1465
  else
1334
- raise TypeError, "CATimedelta + #{other.class} is not allowed"
1466
+ raise TypeError, "CATimedelta + #{given.class} is not allowed"
1335
1467
  end
1336
1468
  end
1337
1469
 
@@ -1339,9 +1471,12 @@ class CATimedelta
1339
1471
  # Returns the difference of two {CATimedelta} at the finer of the two
1340
1472
  # units (a cross-group pair raises).
1341
1473
  # @param other [CATimedelta]
1474
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1475
+ # duration array)
1342
1476
  # @return [CATimedelta]
1343
1477
  # @raise [TypeError] on a non-timedelta operand.
1344
1478
  def -(other)
1479
+ given, other = other, CATimeUnitAlgebra.operand(other)
1345
1480
  case other
1346
1481
  when CATimedelta
1347
1482
  u = common_duration_unit(other.unit)
@@ -1349,7 +1484,7 @@ class CATimedelta
1349
1484
  b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1350
1485
  (a - b).timedelta(unit: u)
1351
1486
  else
1352
- raise TypeError, "CATimedelta - #{other.class} is not allowed"
1487
+ raise TypeError, "CATimedelta - #{given.class} is not allowed"
1353
1488
  end
1354
1489
  end
1355
1490
 
@@ -1385,9 +1520,12 @@ class CATimedelta
1385
1520
  # {CATimedelta}, by another {CATimedelta} with matching `unit`
1386
1521
  # yields a dimensionless CArray.
1387
1522
  # @param other [Integer, CATimedelta]
1523
+ # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1524
+ # duration array)
1388
1525
  # @return [CATimedelta, CArray]
1389
1526
  # @raise [TypeError] on incompatible operands.
1390
1527
  def /(other)
1528
+ given, other = other, CATimeUnitAlgebra.operand(other)
1391
1529
  case other
1392
1530
  when Integer
1393
1531
  CATimeUnitAlgebra.trunc_divide(parent, other).timedelta(unit: unit)
@@ -1396,7 +1534,7 @@ class CATimedelta
1396
1534
  CATimeUnitAlgebra.trunc_divide(
1397
1535
  CATimeUnitAlgebra.convert_scale!(parent, unit, u),
1398
1536
  CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u))
1399
- else raise TypeError, "CATimedelta / #{other.class} is not allowed"
1537
+ else raise TypeError, "CATimedelta / #{given.class} is not allowed"
1400
1538
  end
1401
1539
  end
1402
1540
 
@@ -1412,14 +1550,41 @@ class CATimedelta
1412
1550
  # or as a {CATimedelta} view for per-axis reduction.
1413
1551
  # @return [Element, CATimedelta]
1414
1552
  def sum(*args, **opts)
1415
- 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)) }
1562
+ end
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))
1416
1570
  end
1417
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
+
1418
1583
  # @overload mean(axis: nil, **opts)
1419
1584
  # Returns the mean duration rounded to the nearest unit count.
1420
1585
  # @return [Element, CATimedelta]
1421
1586
  def mean(*args, **opts)
1422
- round_and_relift(parent.mean(*args, **opts))
1587
+ with_surface_fill(opts) { |o| round_and_relift(parent.mean(*args, **o)) }
1423
1588
  end
1424
1589
 
1425
1590
  # median / percentile / quantile / stddev / stddevp report on this array's
@@ -1431,7 +1596,7 @@ class CATimedelta
1431
1596
  # Returns the median duration on `self`'s unit.
1432
1597
  # @return [Element, CATimedelta]
1433
1598
  def median(*args, **opts)
1434
- round_and_relift(parent.median(*args, **opts))
1599
+ with_surface_fill(opts) { |o| round_and_relift(parent.median(*args, **o)) }
1435
1600
  end
1436
1601
 
1437
1602
  # @overload percentile(*p, axis: nil, **opts)
@@ -1440,7 +1605,7 @@ class CATimedelta
1440
1605
  # two or more give an Array).
1441
1606
  # @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
1442
1607
  def percentile(*args, **opts)
1443
- round_and_relift(parent.percentile(*args, **opts))
1608
+ with_surface_fill(opts) { |o| round_and_relift(parent.percentile(*args, **o)) }
1444
1609
  end
1445
1610
 
1446
1611
  # @overload quantile(axis: nil, **opts)
@@ -1448,7 +1613,7 @@ class CATimedelta
1448
1613
  # `self`'s unit.
1449
1614
  # @return [Array<Element>, Array<CATimedelta>]
1450
1615
  def quantile(*args, **opts)
1451
- round_and_relift(parent.quantile(*args, **opts))
1616
+ with_surface_fill(opts) { |o| round_and_relift(parent.quantile(*args, **o)) }
1452
1617
  end
1453
1618
 
1454
1619
  # @overload stddev(axis: nil, **opts)
@@ -1457,7 +1622,7 @@ class CATimedelta
1457
1622
  # the precision matters.
1458
1623
  # @return [Element, CATimedelta]
1459
1624
  def stddev(*args, **opts)
1460
- round_and_relift(parent.stddev(*args, **opts))
1625
+ with_surface_fill(opts) { |o| round_and_relift(parent.stddev(*args, **o)) }
1461
1626
  end
1462
1627
 
1463
1628
  # @overload stddevp(axis: nil, **opts)
@@ -1465,7 +1630,7 @@ class CATimedelta
1465
1630
  # {#stddev}.
1466
1631
  # @return [Element, CATimedelta]
1467
1632
  def stddevp(*args, **opts)
1468
- round_and_relift(parent.stddevp(*args, **opts))
1633
+ with_surface_fill(opts) { |o| round_and_relift(parent.stddevp(*args, **o)) }
1469
1634
  end
1470
1635
 
1471
1636
  def round_and_relift(r)
@@ -1482,39 +1647,21 @@ class CATimedelta
1482
1647
  # Not supported: the variance of durations has squared-time units, which
1483
1648
  # no type represents -- the same reason {CATime#variance} refuses. Use
1484
1649
  # {#stddev} for the spread as a duration.
1485
- # @raise [TypeError] always.
1650
+ # @raise [CArray::DataTypeError] always.
1486
1651
  def variance(*)
1487
- 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"
1488
1653
  end
1489
1654
 
1490
1655
  # @overload variancep(*)
1491
1656
  # Not supported, for the same reason as {#variance}. Use {#stddevp}.
1492
- # @raise [TypeError] always.
1657
+ # @raise [CArray::DataTypeError] always.
1493
1658
  def variancep(*)
1494
- raise TypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
1659
+ raise CArray::DataTypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
1495
1660
  end
1496
1661
 
1497
- # min / max ride the core reduce Face gate (ORDERABLE storage descent +
1498
- # output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
1499
- # CArray#min / #max return the Element / CATimedelta shapes directly.
1500
- # minmax has no core gate, so re-lift both extremes here.
1501
-
1502
- # @overload minmax(*axes, **opts)
1503
- # Returns the shortest and longest duration as a `[min, max]` pair.
1504
- # @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
1505
- def minmax(*args, **opts)
1506
- lo, hi = parent.minmax(*args, **opts)
1507
- [relift_extremum(lo), relift_extremum(hi)]
1508
- end
1509
-
1510
- def relift_extremum(r)
1511
- case r
1512
- when Integer then Element.new(r, unit)
1513
- when CArray then r.timedelta(unit: unit)
1514
- else r
1515
- end
1516
- end
1517
- 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.
1518
1665
  # @!endgroup
1519
1666
 
1520
1667
  # sort / partition ride the core sort Face gate (ORDERABLE storage
@@ -1535,6 +1682,23 @@ class CATimedelta
1535
1682
  # @param operand [CATimedelta, CATimedelta::Element]
1536
1683
  # @return [CATimedelta] in self's unit.
1537
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
+
1538
1702
  def to_comparable (operand)
1539
1703
  case operand
1540
1704
  when CATimedelta
@@ -1551,8 +1715,8 @@ class CATimedelta
1551
1715
  end
1552
1716
  end
1553
1717
 
1554
- # @overload scalar_to_storage(surface)
1555
- # 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
1556
1720
  # value object into this Face's int64 storage (count in self's unit) so a
1557
1721
  # scalar store round-trips with a fetch. A {Element} is reconciled to
1558
1722
  # self's unit via {#to_comparable} (lossless discipline; a `Time` /
@@ -1563,7 +1727,7 @@ class CATimedelta
1563
1727
  # @return [Integer, Object] the storage-domain value, or `surface`
1564
1728
  # unchanged for a pass-through type.
1565
1729
  # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
1566
- def scalar_to_storage (surface)
1730
+ def element_to_storage (surface)
1567
1731
  case surface
1568
1732
  when Integer, String
1569
1733
  surface
@@ -1609,6 +1773,17 @@ class CATimedelta
1609
1773
  end
1610
1774
  CATimeUnitAlgebra.finer(unit, ou)
1611
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
1612
1787
  end
1613
1788
 
1614
1789
  # Reads a time literal (Time / DateTime / Integer unix-seconds / String) into
@@ -1621,8 +1796,12 @@ module CATimeLiteral
1621
1796
 
1622
1797
  # Date fields of a String literal, UTC. Ruby's Date._parse does not read
1623
1798
  # the calendar-grid forms CATime#to_s prints -- "2019-09" comes back as a
1624
- # month of 20 with a zone, and a bare "2019" as a month and a day -- so
1625
- # 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.
1626
1805
  #
1627
1806
  # What Date._parse returns is then checked, because an out-of-range field
1628
1807
  # used to flow into the civil kernel and normalise into a different date:
@@ -1634,23 +1813,71 @@ module CATimeLiteral
1634
1813
  if format then Date._strptime(spec, format)
1635
1814
  elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
1636
1815
  elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
1816
+ elsif (hour = hour_only_fields(spec)) then hour
1637
1817
  else Date._parse(spec)
1638
1818
  end
1639
1819
  unless h && h[:year]
1640
1820
  raise ArgumentError, "cannot parse time #{spec.inspect}"
1641
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
1642
1832
  y, m, d = h[:year], h[:mon] || 1, h[:mday] || 1
1643
1833
  unless (1..12).cover?(m) && Date.valid_date?(y, m, d, Date::GREGORIAN)
1644
1834
  raise ArgumentError,
1645
1835
  "cannot parse time #{spec.inspect}: it reads as year #{y}, " \
1646
1836
  "month #{m}, day #{d}, which is not a date"
1647
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
1648
1843
  h
1649
1844
  end
1650
1845
  private_class_method :parse_date_fields
1651
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
+
1652
1879
  # Exact Rational seconds since the Unix epoch for a start literal (Time /
1653
- # DateTime / Integer unix-seconds / String). UTC default.
1880
+ # Date / DateTime / Integer unix-seconds / String). UTC default.
1654
1881
  def epoch_seconds(spec, format = nil)
1655
1882
  require 'date'
1656
1883
  require 'time'
@@ -1680,6 +1907,8 @@ module CATimeLiteral
1680
1907
  else
1681
1908
  if defined?(DateTime) && spec.is_a?(DateTime)
1682
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)
1683
1912
  else
1684
1913
  raise ArgumentError, "cannot parse time #{spec.class}"
1685
1914
  end
@@ -1727,6 +1956,189 @@ module CATimeLiteral
1727
1956
  end
1728
1957
  end
1729
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
+
1730
2142
  # Single-literal build for {.time}: a 1-element CATime, honouring
1731
2143
  # the on_error policy (raise, or a masked cell).
1732
2144
  def to_time_array(literal, res, format, on_error)
@@ -1836,6 +2248,8 @@ class CArray
1836
2248
  unless x.is_a?(CArray)
1837
2249
  return CATimeLiteral.to_time_array(x, res, format, on_error)
1838
2250
  end
2251
+ raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error)
2252
+ return raw.time(unit: res) if raw
1839
2253
  raw = CArray.int64(*x.shape)
1840
2254
  x.each_index do |*idx|
1841
2255
  s = x[*idx]