carray 2.0.0 → 3.0.0

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 (339) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -25
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE +1 -1
  5. data/NEWS.md +3 -0
  6. data/README.md +128 -44
  7. data/carray.gemspec +22 -24
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1208 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +125 -0
  15. data/ext/ca_binop_dispatch.h +159 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +241 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4321 -0
  26. data/ext/ca_kernel_iterator.h +2603 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +809 -0
  33. data/ext/ca_obj_binop.c +892 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +965 -0
  39. data/ext/ca_obj_face.c +670 -0
  40. data/ext/ca_obj_face.h +247 -0
  41. data/ext/ca_obj_fake.c +228 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +306 -0
  45. data/ext/ca_obj_grid.c +858 -440
  46. data/ext/ca_obj_meld.c +1034 -0
  47. data/ext/ca_obj_moncmp.c +569 -0
  48. data/ext/ca_obj_monop.c +1111 -0
  49. data/ext/ca_obj_object.c +774 -298
  50. data/ext/ca_obj_record.c +468 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +569 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +616 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1296 -0
  58. data/ext/ca_obj_shift.c +230 -792
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2501 -0
  62. data/ext/ca_obj_string.c +268 -0
  63. data/ext/ca_obj_tile.c +614 -0
  64. data/ext/ca_obj_time.c +546 -0
  65. data/ext/ca_obj_timedelta.c +435 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +746 -0
  68. data/ext/ca_obj_unbound_repeat.c +208 -241
  69. data/ext/ca_obj_window.c +1131 -563
  70. data/ext/ca_op_byte_swap.c +175 -0
  71. data/ext/ca_op_ipower.c +319 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +430 -0
  75. data/ext/ca_sweep_engine.h +157 -0
  76. data/ext/ca_transform_common.c +228 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +795 -402
  80. data/ext/carray_access.c +831 -711
  81. data/ext/carray_attribute.c +98 -330
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +283 -0
  84. data/ext/carray_call_cfunc.c +1360 -828
  85. data/ext/carray_call_cfunc.h +160 -0
  86. data/ext/carray_cast.c +1212 -301
  87. data/ext/carray_cast_func.rb +81 -40
  88. data/ext/carray_class.c +53 -63
  89. data/ext/carray_config.h +28 -0
  90. data/ext/carray_conversion.c +350 -346
  91. data/ext/carray_copy.c +156 -268
  92. data/ext/carray_core.c +1342 -199
  93. data/ext/carray_count.c +312 -0
  94. data/ext/carray_data_type.c +43 -19
  95. data/ext/carray_element.c +585 -213
  96. data/ext/carray_factorize.c +2542 -0
  97. data/ext/carray_generate.c +230 -559
  98. data/ext/carray_histogram.c +490 -0
  99. data/ext/carray_hold.c +228 -0
  100. data/ext/carray_index_classifier.c +1035 -0
  101. data/ext/carray_index_classifier.h +27 -0
  102. data/ext/carray_internal.h +120 -0
  103. data/ext/carray_kernels_bincmp.c +4445 -0
  104. data/ext/carray_kernels_binop.c +10979 -0
  105. data/ext/carray_kernels_init.c +36 -0
  106. data/ext/carray_kernels_map.c +3466 -0
  107. data/ext/carray_kernels_moncmp.c +2096 -0
  108. data/ext/carray_kernels_monop.c +18312 -0
  109. data/ext/carray_kernels_reduce_aggregate.c +25836 -0
  110. data/ext/carray_kernels_reduce_boolean.c +329 -0
  111. data/ext/carray_kernels_reduce_cumulative.c +14592 -0
  112. data/ext/carray_kernels_reduce_extreme.c +16947 -0
  113. data/ext/carray_kernels_reduce_variance.c +3909 -0
  114. data/ext/carray_kernels_scan.c +3692 -0
  115. data/ext/carray_kernels_search.c +32137 -0
  116. data/ext/carray_kernels_sort.c +10625 -0
  117. data/ext/carray_kernels_triop.c +1391 -0
  118. data/ext/carray_lazy.c +567 -0
  119. data/ext/carray_loop.c +88 -200
  120. data/ext/carray_mask.c +848 -154
  121. data/ext/carray_math_kernel.h +120 -0
  122. data/ext/carray_mathfunc.c +10 -241
  123. data/ext/carray_median_percentile.c +1257 -0
  124. data/ext/carray_memory_view.c +1625 -0
  125. data/ext/carray_operator.c +1526 -318
  126. data/ext/carray_order.c +664 -1394
  127. data/ext/carray_partition.c +416 -0
  128. data/ext/carray_random.c +518 -0
  129. data/ext/carray_scatter.c +357 -0
  130. data/ext/carray_slab.c +1219 -0
  131. data/ext/carray_slab.h +84 -0
  132. data/ext/carray_sort.c +829 -0
  133. data/ext/carray_sort_kernel.c +620 -0
  134. data/ext/carray_struct.c +695 -0
  135. data/ext/carray_test.c +343 -229
  136. data/ext/carray_undef.c +34 -17
  137. data/ext/carray_utils.c +175 -74
  138. data/ext/extconf.rb +216 -55
  139. data/ext/mk_call_cfunc.rb +480 -0
  140. data/ext/mkkernel.rb +8842 -0
  141. data/ext/ruby_carray.c +202 -101
  142. data/ext/version.h +4 -14
  143. data/ext/version.rb +5 -13
  144. data/lib/carray/arrow_tensor.rb +401 -0
  145. data/lib/carray/attribute.rb +166 -0
  146. data/lib/carray/autoload_carray.rb +220 -0
  147. data/lib/carray/autoload_method_extension.rb +44 -0
  148. data/lib/carray/axis_group.rb +711 -0
  149. data/lib/carray/basics.rb +481 -0
  150. data/lib/carray/bincount_nd.rb +358 -0
  151. data/lib/carray/block_iterator.rb +604 -0
  152. data/lib/carray/boolean_reduce.rb +109 -0
  153. data/lib/carray/categorical.rb +561 -0
  154. data/lib/carray/categorical_iterator.rb +1062 -0
  155. data/lib/carray/complex.rb +150 -0
  156. data/lib/carray/conditional.rb +216 -0
  157. data/lib/carray/const_string.rb +228 -0
  158. data/lib/carray/construct.rb +139 -328
  159. data/lib/carray/core_extensions.rb +240 -0
  160. data/lib/carray/data_type_extension.rb +233 -0
  161. data/lib/carray/fixlen_string.rb +95 -0
  162. data/lib/carray/frame/concat.rb +132 -0
  163. data/lib/carray/frame/convert.rb +95 -0
  164. data/lib/carray/frame/csv_parser.rb +211 -0
  165. data/lib/carray/frame/frame.rb +649 -0
  166. data/lib/carray/frame/group.rb +186 -0
  167. data/lib/carray/frame/io.rb +164 -0
  168. data/lib/carray/frame/join.rb +248 -0
  169. data/lib/carray/frame/records.rb +99 -0
  170. data/lib/carray/frame/sort.rb +113 -0
  171. data/lib/carray/frame/verbs.rb +299 -0
  172. data/lib/carray/frame.rb +16 -0
  173. data/lib/carray/histogram.rb +512 -0
  174. data/lib/carray/inspect.rb +37 -20
  175. data/lib/carray/iterator.rb +57 -349
  176. data/lib/carray/lazy.rb +889 -0
  177. data/lib/carray/mask_gap_fill.rb +200 -0
  178. data/lib/carray/math.rb +78 -342
  179. data/lib/carray/meld_reduce.rb +289 -0
  180. data/lib/carray/methods/align_addr.rb +116 -0
  181. data/lib/carray/methods/bin.rb +128 -0
  182. data/lib/carray/methods/bincount.rb +87 -0
  183. data/lib/carray/methods/bit_string.rb +92 -0
  184. data/lib/carray/methods/broadcast.rb +63 -0
  185. data/lib/carray/methods/choose.rb +39 -0
  186. data/lib/carray/methods/composition.rb +280 -0
  187. data/lib/carray/methods/gather_nd.rb +206 -0
  188. data/lib/carray/methods/index.rb +39 -0
  189. data/lib/carray/methods/insert_block.rb +99 -0
  190. data/lib/carray/methods/is_in.rb +141 -0
  191. data/lib/carray/methods/join.rb +90 -0
  192. data/lib/carray/methods/locate_addr.rb +47 -0
  193. data/lib/carray/methods/mask_duplicates.rb +41 -0
  194. data/lib/carray/methods/meshgrid.rb +91 -0
  195. data/lib/carray/methods/mode.rb +126 -0
  196. data/lib/carray/methods/nunique.rb +46 -0
  197. data/lib/carray/methods/resize.rb +56 -0
  198. data/lib/carray/methods/snap.rb +156 -0
  199. data/lib/carray/methods/string_format.rb +57 -0
  200. data/lib/carray/methods/unique.rb +47 -0
  201. data/lib/carray/methods/value_counts.rb +71 -0
  202. data/lib/carray/mkmf.rb +124 -101
  203. data/lib/carray/runtime.rb +108 -0
  204. data/lib/carray/serialize.rb +478 -167
  205. data/lib/carray/slab_iterator.rb +292 -0
  206. data/lib/carray/stack.rb +291 -0
  207. data/lib/carray/string.rb +56 -180
  208. data/lib/carray/string_operation_extension.rb +289 -0
  209. data/lib/carray/struct.rb +335 -323
  210. data/lib/carray/struct_builder.rb +697 -0
  211. data/lib/carray/table.rb +41 -2
  212. data/lib/carray/time.rb +2255 -38
  213. data/lib/carray/window_iterator.rb +655 -0
  214. data/lib/carray.rb +55 -57
  215. metadata +163 -130
  216. data/Rakefile +0 -51
  217. data/TODO.md +0 -18
  218. data/ext/ca_iter_block.c +0 -257
  219. data/ext/ca_iter_dimension.c +0 -299
  220. data/ext/ca_iter_window.c +0 -214
  221. data/ext/ca_obj_mapping.c +0 -644
  222. data/ext/carray_iterator.c +0 -641
  223. data/ext/carray_math.rb +0 -850
  224. data/ext/carray_numeric.c +0 -259
  225. data/ext/carray_sort_addr.c +0 -254
  226. data/ext/carray_stat.c +0 -2100
  227. data/ext/carray_stat_proc.rb +0 -1999
  228. data/ext/mkmath.rb +0 -741
  229. data/ext/ruby_ccomplex.c +0 -509
  230. data/ext/ruby_float_func.c +0 -86
  231. data/lib/carray/array.rb +0 -8
  232. data/lib/carray/autoload/autoload_base.rb +0 -19
  233. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  234. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  235. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  236. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  237. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  238. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  239. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  240. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  241. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  242. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  243. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  244. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  245. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  246. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  247. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  248. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  249. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  250. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  251. data/lib/carray/autoload.rb +0 -141
  252. data/lib/carray/basic.rb +0 -191
  253. data/lib/carray/broadcast.rb +0 -101
  254. data/lib/carray/compose.rb +0 -315
  255. data/lib/carray/convert.rb +0 -115
  256. data/lib/carray/info.rb +0 -110
  257. data/lib/carray/io/imagemagick.rb +0 -235
  258. data/lib/carray/mask.rb +0 -102
  259. data/lib/carray/math/histogram.rb +0 -177
  260. data/lib/carray/math/recurrence.rb +0 -93
  261. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  262. data/lib/carray/object/ca_obj_link.rb +0 -50
  263. data/lib/carray/object/ca_obj_pack.rb +0 -99
  264. data/lib/carray/obsolete.rb +0 -256
  265. data/lib/carray/ordering.rb +0 -181
  266. data/lib/carray/testing.rb +0 -51
  267. data/lib/carray/transform.rb +0 -109
  268. data/misc/Methods.ja.md +0 -182
  269. data/misc/NOTE +0 -51
  270. data/spec/Classes/CABitfield_spec.rb +0 -58
  271. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  272. data/spec/Classes/CABlock_spec.rb +0 -205
  273. data/spec/Classes/CAField_spec.rb +0 -39
  274. data/spec/Classes/CAGrid_spec.rb +0 -75
  275. data/spec/Classes/CAMap_spec.rb +0 -0
  276. data/spec/Classes/CAMapping_spec.rb +0 -105
  277. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  278. data/spec/Classes/CAObject_spec.rb +0 -33
  279. data/spec/Classes/CARefer_spec.rb +0 -93
  280. data/spec/Classes/CARepeat_spec.rb +0 -65
  281. data/spec/Classes/CASelect_spec.rb +0 -22
  282. data/spec/Classes/CAShift_spec.rb +0 -16
  283. data/spec/Classes/CAStruct_spec.rb +0 -71
  284. data/spec/Classes/CATranspose_spec.rb +0 -60
  285. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  286. data/spec/Classes/CAWindow_spec.rb +0 -54
  287. data/spec/Classes/CAWrap_spec.rb +0 -8
  288. data/spec/Classes/CArray_spec.rb +0 -184
  289. data/spec/Classes/CScalar_spec.rb +0 -55
  290. data/spec/Classes/ex1.rb +0 -46
  291. data/spec/Features/feature_130_spec.rb +0 -19
  292. data/spec/Features/feature_attributes_spec.rb +0 -280
  293. data/spec/Features/feature_boolean_spec.rb +0 -98
  294. data/spec/Features/feature_broadcast.rb +0 -116
  295. data/spec/Features/feature_cast_function.rb +0 -19
  296. data/spec/Features/feature_cast_spec.rb +0 -33
  297. data/spec/Features/feature_class_spec.rb +0 -84
  298. data/spec/Features/feature_complex_spec.rb +0 -42
  299. data/spec/Features/feature_composite_spec.rb +0 -124
  300. data/spec/Features/feature_convert_spec.rb +0 -46
  301. data/spec/Features/feature_copy_spec.rb +0 -123
  302. data/spec/Features/feature_creation_spec.rb +0 -84
  303. data/spec/Features/feature_element_spec.rb +0 -144
  304. data/spec/Features/feature_extream_spec.rb +0 -54
  305. data/spec/Features/feature_generate_spec.rb +0 -74
  306. data/spec/Features/feature_index_spec.rb +0 -69
  307. data/spec/Features/feature_mask_spec.rb +0 -580
  308. data/spec/Features/feature_math_spec.rb +0 -97
  309. data/spec/Features/feature_order_spec.rb +0 -146
  310. data/spec/Features/feature_ref_store_spec.rb +0 -209
  311. data/spec/Features/feature_serialization_spec.rb +0 -125
  312. data/spec/Features/feature_stat_spec.rb +0 -397
  313. data/spec/Features/feature_virtual_spec.rb +0 -48
  314. data/spec/Features/method_eq_spec.rb +0 -81
  315. data/spec/Features/method_is_nan_spec.rb +0 -12
  316. data/spec/Features/method_map_spec.rb +0 -54
  317. data/spec/Features/method_max_with.rb +0 -20
  318. data/spec/Features/method_min_with.rb +0 -19
  319. data/spec/Features/method_ne_spec.rb +0 -18
  320. data/spec/Features/method_project_spec.rb +0 -188
  321. data/spec/Features/method_ref_spec.rb +0 -27
  322. data/spec/Features/method_round_spec.rb +0 -11
  323. data/spec/Features/method_s_linspace_spec.rb +0 -48
  324. data/spec/Features/method_s_span_spec.rb +0 -14
  325. data/spec/Features/method_seq_spec.rb +0 -47
  326. data/spec/Features/method_sort_with.rb +0 -43
  327. data/spec/Features/method_sorted_with.rb +0 -29
  328. data/spec/Features/method_span_spec.rb +0 -42
  329. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  330. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  331. data/spec/spec_all.rb +0 -10
  332. data/utils/ca_ase.rb +0 -21
  333. data/utils/ca_methods.rb +0 -15
  334. data/utils/cast_checker.rb +0 -30
  335. data/utils/convert_test.rb +0 -73
  336. data/utils/extract_yard.rb +0 -22
  337. data/utils/guess_shape.rb +0 -76
  338. data/utils/monkey_patch_methods.rb +0 -62
  339. data/utils/remove_resource_fork.sh +0 -5
data/lib/carray/time.rb CHANGED
@@ -1,76 +1,2293 @@
1
- # ----------------------------------------------------------------------------
1
+ # lib/carray/time.rb
2
2
  #
3
- # carray/time.rb
3
+ # Ruby surface for the CATime / CATimedelta Faces: construction
4
+ # entry points, arithmetic / comparison operators, reductions, and the
5
+ # pandas-style field accessors.
4
6
  #
5
- # This file is part of Ruby/CArray extension library.
7
+ # CATime / CATimedelta construction entry points:
8
+ # - `CATime.new(*shape, unit: :ns)` — new allocate + Face wrap
9
+ # - `CATime.wrap(raw_int64, unit: :ns)` — zero-copy mask over existing entity (C side)
10
+ # - `CArray#time(unit: :ns)` — instance-method Face-conversion sugar (= sugar over `.wrap`)
11
+ # - Same pattern for CATimedelta
6
12
  #
7
- # Copyright (C) 2005-2020 Hiroki Motoyoshi
13
+ # Base epoch fixed to 1970-01-01 (Unix epoch).
14
+ # NetCDF CF convention (= reference time epoch) is delegated to external gems (timesteps etc.).
15
+
16
+ # Internal: unit-conversion algebra shared by CATime / CATimedelta
17
+ # (used by to_comparable to align a search query to the reference unit).
8
18
  #
9
- # ----------------------------------------------------------------------------
19
+ # The units split into two groups that are NOT inter-convertible by a fixed
20
+ # ratio: fixed-length (W/D/h/m/s/ms/.../as, ratios in seconds) and calendar
21
+ # (Y/M, ratio in months -- W and below are calendar-dependent in days).
22
+ # Within a group any pair is an exact integer ratio (coarse = fine * N), so a
23
+ # coarse->fine cast is lossless (multiply), and a fine->coarse cast is lossless
24
+ # only when every value is divisible by the divisor.
25
+ module CATimeUnitAlgebra
26
+ # seconds per base unit (Rational)
27
+ FIXED = {
28
+ W: 604800r, D: 86400r, h: 3600r, m: 60r, s: 1r,
29
+ ms: Rational(1, 10**3), us: Rational(1, 10**6), ns: Rational(1, 10**9),
30
+ ps: Rational(1, 10**12), fs: Rational(1, 10**15), as: Rational(1, 10**18),
31
+ }.freeze
32
+ # months per base unit (Rational)
33
+ CALENDAR = { Y: 12r, M: 1r }.freeze
34
+
35
+ # Base units from finest to coarsest granularity. Every fixed-length unit is
36
+ # finer than every calendar unit (a week < a month), so this is a total
37
+ # order used to pick the base that two operands both convert into exactly.
38
+ GRANULARITY = %i[as fs ps ns us ms s m h D W M Y].freeze
39
+
40
+ module_function
41
+
42
+ # seconds- (fixed) or months- (calendar) per base tick.
43
+ def base_ratio(base)
44
+ FIXED[base] || CALENDAR[base]
45
+ end
46
+
47
+ # Normalize a unit spec (Resolution / Symbol / String) to a Resolution.
48
+ # A bare Symbol / String routes through Resolution.parse (count-1 base).
49
+ def res(u)
50
+ u.is_a?(CATime::Resolution) ? u : CATime::Resolution.parse(u)
51
+ end
52
+
53
+ # Greatest common divisor of two positive Rationals (both in lowest terms).
54
+ def rgcd(a, b)
55
+ Rational(a.numerator.gcd(b.numerator), a.denominator.lcm(b.denominator))
56
+ end
57
+
58
+ # Whether two units are in the same group (both calendar or both fixed).
59
+ def same_group?(u1, u2)
60
+ CALENDAR.key?(res(u1).base) == CALENDAR.key?(res(u2).base)
61
+ end
62
+
63
+ # The common grid two same-group resolutions both convert into exactly: the
64
+ # resolution whose tick is the gcd of the two ticks (finest common base +
65
+ # the whole multiplier). For equal resolutions this is the resolution
66
+ # itself; for `(1,:D)` & `(1,:h)` it is `(1,:h)`; for `(5,:m)` & `(2,:m)` it
67
+ # is `(1,:m)`. Both operands are same-group (checked by the caller).
68
+ def common(u1, u2)
69
+ a = res(u1); b = res(u2)
70
+ fb = GRANULARITY.index(a.base) <= GRANULARITY.index(b.base) ? a.base : b.base
71
+ g = rgcd(a.tick_ratio, b.tick_ratio)
72
+ CATime::Resolution.new(Integer(g / base_ratio(fb)), fb)
73
+ end
74
+ alias_method :finer, :common
75
+ module_function :finer
76
+
77
+ # Common resolution for a time difference (E): same group -> the common
78
+ # grid; cross-group -> the fixed-group resolution (a :M/:Y difference only
79
+ # arises from two calendar operands), coarsened to `(1,:D)` when the fixed
80
+ # side is a week (a week is not calendar-alignable, but both sides convert
81
+ # into days exactly).
82
+ def diff_unit(u1, u2)
83
+ a = res(u1); b = res(u2)
84
+ return common(a, b) if same_group?(a, b)
85
+ fx = CALENDAR.key?(a.base) ? b : a
86
+ fx.base == :W ? CATime::Resolution.new(1, :D) : fx
87
+ end
88
+
89
+ # Tick ratio from `from` to `to` (how many `to` ticks per `from` tick), or
90
+ # nil if they are in different groups (not inter-convertible by a fixed
91
+ # ratio). Folds each resolution's count.
92
+ def ratio(from, to)
93
+ a = res(from); b = res(to)
94
+ return nil unless same_group?(a, b)
95
+ a.tick_ratio / b.tick_ratio
96
+ end
97
+
98
+ # Target ticks per source tick for the strict unit-change surface
99
+ # (CATime#to_unit / CATimedelta#to_unit): accepted only when the source tick
100
+ # is a whole multiple of the target tick, so every value re-expresses
101
+ # exactly on the finer grid. A coarser target (which would round) and a
102
+ # cross-group pair (no fixed ratio at all) both raise -- unlike
103
+ # convert_scale! / convert_instant!, which coarsen when the values happen to
104
+ # allow it, this decides on the units alone.
105
+ def multiple_factor(from, to)
106
+ a = res(from); b = res(to)
107
+ return 1 if a == b
108
+ r = ratio(a, b)
109
+ if r.nil?
110
+ raise ArgumentError,
111
+ "cannot express #{a} in #{b}: calendar units (:Y/:M) and " \
112
+ "fixed-length units (:W/:D/:h/:s/...) have no fixed ratio"
113
+ end
114
+ unless r.denominator == 1
115
+ raise ArgumentError,
116
+ "cannot express #{a} in whole #{b} ticks " \
117
+ "(a #{a} tick is not a whole multiple of a #{b} tick)"
118
+ end
119
+ r.numerator
120
+ end
121
+
122
+ # `storage * factor` with a loud overflow guard: widening a wide time range
123
+ # into a fine unit can exceed int64, and a silent wrap would give a wrong
124
+ # instant / duration. Checks the extremes (they bound every element), then
125
+ # multiplies. Shared by every coarse->fine conversion (arithmetic,
126
+ # comparison, search).
127
+ def widen(storage, factor)
128
+ return storage if factor == 1
129
+ if storage.elements > 0
130
+ raw = storage.has_mask? ? storage.value : storage
131
+ lo = raw.min
132
+ unless lo == UNDEF
133
+ lim = 2**63 - 1
134
+ [lo, raw.max].each do |x|
135
+ next if (Integer(x) * factor).abs <= lim
136
+ raise RangeError,
137
+ "time unit conversion overflows int64: the time range is " \
138
+ "too wide to widen into this resolution (x#{factor})"
139
+ end
140
+ end
141
+ end
142
+ storage * factor
143
+ end
144
+
145
+ # SCALE conversion (durations / timedelta): convert an int64 storage CArray
146
+ # from `from` unit to `to` unit by the fixed ratio. coarse->fine multiplies;
147
+ # fine->coarse divides only when every value is exact; cross-group ALWAYS
148
+ # raises -- a :M / :Y duration has no fixed ratio to days (a month is
149
+ # calendar-variable), so it genuinely cannot scale to seconds.
150
+ def convert_scale!(storage, from, to)
151
+ a = res(from); b = res(to)
152
+ return storage if a == b
153
+ r = ratio(a, b)
154
+ if r.nil?
155
+ raise ArgumentError,
156
+ "cannot scale duration #{a} to #{b}: calendar units " \
157
+ "(:Y/:M) and fixed-length units (:W/:D/:h/:s/...) have no fixed " \
158
+ "ratio (a month / year is calendar-variable)"
159
+ end
160
+ if r.denominator == 1
161
+ widen(storage, r.numerator) # coarse -> fine: lossless multiply
162
+ else
163
+ divisor = r.denominator # fine -> coarse: exact only
164
+ unless (storage % divisor).eq(0).all
165
+ raise ArgumentError,
166
+ "cannot scale duration #{a} to #{b} without loss: " \
167
+ "some values are not a whole multiple of #{b} " \
168
+ "(finer resolution would be truncated)"
169
+ end
170
+ storage / divisor
171
+ end
172
+ end
173
+
174
+ # SCALE conversion with truncation: like convert_scale! but a fine->coarse
175
+ # conversion drops the sub-`to` remainder (truncating toward zero) instead of
176
+ # raising. Used for dt +/- td, where the result keeps the time's unit
177
+ # and a finer duration is truncated to it (a :D time + a 5 h duration is
178
+ # + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
179
+ # duration has no fixed ratio to a fixed unit).
180
+ def convert_scale_trunc(storage, from, to)
181
+ a = res(from); b = res(to)
182
+ return storage if a == b
183
+ r = ratio(a, b)
184
+ if r.nil?
185
+ raise ArgumentError,
186
+ "cannot combine a #{a} duration with a #{b} time across the " \
187
+ "calendar/fixed boundary (a calendar duration has no fixed ratio)"
188
+ end
189
+ r.denominator == 1 ? widen(storage, r.numerator) : storage / r.denominator
190
+ end
191
+
192
+ # INSTANT conversion (absolute datetimes): unlike a duration, a time :M
193
+ # value HAS a well-defined instant (the month's first midnight), so a
194
+ # cross-group cast is possible via civil-date algebra even though no fixed
195
+ # ratio exists (a :M time casts to :s, a :M duration cannot). Same-group
196
+ # falls back to the ratio.
197
+ # - calendar (:M/:Y) -> fixed (<= :D): always exact (widen to the finer
198
+ # grid). :W is rejected (month / year starts are not week-aligned).
199
+ # - fixed -> calendar: exact only when the instant lands on the calendar
200
+ # boundary (midnight of the 1st), else raises.
201
+ def convert_instant!(storage, from, to)
202
+ a = res(from); b = res(to)
203
+ return storage if a == b
204
+ return convert_scale!(storage, a, b) if ratio(a, b) # same group
205
+ if CALENDAR.key?(a.base)
206
+ _instant_cal_to_fixed(storage, a, b) # widen
207
+ else
208
+ _instant_fixed_to_cal(storage, a, b) # coarsen (exact-or-raise)
209
+ end
210
+ end
211
+
212
+ # calendar time (Resolution `from`) -> days since the epoch (int64
213
+ # CArray). Folds the resolution count (value = count-Y/M buckets).
214
+ def _cal_days(storage, from)
215
+ ones = CArray.int64(*storage.shape) { 1 }
216
+ if from.base == :M
217
+ abs = storage * from.count + 1970 * 12 # absolute month ordinal
218
+ y = CATime.send(:_floordiv_i, abs, 12)
219
+ m = abs - y * 12 + 1
220
+ CATime.send(:_days_from_civil, y, m, ones)
221
+ else # :Y
222
+ CATime.send(:_days_from_civil, storage * from.count + 1970, ones, ones)
223
+ end
224
+ end
225
+
226
+ # calendar time -> fixed-length grid: the widening half of
227
+ # convert_instant!. Goes through the day count, so the target grid has
228
+ # to tile a day exactly (:W is rejected -- month starts are not
229
+ # week-aligned). Always exact once that holds.
230
+ def _instant_cal_to_fixed(storage, from, to)
231
+ r = ratio(CATime::Resolution.new(1, :D), to) # ticks of `to` per day
232
+ unless r.denominator == 1
233
+ raise ArgumentError,
234
+ "cannot convert calendar time #{from} to #{to} " \
235
+ "(a day boundary is not aligned to the #{to} grid)"
236
+ end
237
+ widen(_cal_days(storage, from), r.numerator)
238
+ end
239
+
240
+ # fixed-length grid -> calendar time: the coarsening half of
241
+ # convert_instant!, exact-or-raise. Every instant must land on a day
242
+ # boundary and then on the calendar boundary itself (the 1st, and
243
+ # January too for :Y), since a mid-month instant has no :M value.
244
+ def _instant_fixed_to_cal(storage, from, to)
245
+ rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
246
+ days =
247
+ if rd.denominator == 1
248
+ n = rd.numerator
249
+ unless (storage % n).eq(0).all
250
+ raise ArgumentError,
251
+ "cannot convert time #{from} to #{to} without loss " \
252
+ "(instant is not on a day boundary)"
253
+ end
254
+ storage / n
255
+ else # coarser than a day (:W)
256
+ storage * (from.tick_ratio / 86400r).to_i
257
+ end
258
+ y, m, d = CATime.send(:_civil_from_days, days)
259
+ on_boundary = d.eq(1)
260
+ on_boundary &= m.eq(1) if to.base == :Y
261
+ unless on_boundary.all
262
+ raise ArgumentError,
263
+ "cannot convert time #{from} to #{to} without loss " \
264
+ "(instant is not on a #{to} boundary)"
265
+ end
266
+ ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
267
+ if to.count > 1
268
+ unless (ord % to.count).eq(0).all
269
+ raise ArgumentError,
270
+ "cannot convert time #{from} to #{to} without loss " \
271
+ "(instant is not on a #{to} boundary)"
272
+ end
273
+ ord = CATime.send(:_floordiv_i, ord, to.count)
274
+ end
275
+ ord
276
+ end
277
+ end
278
+
279
+ class CATime
280
+ # @overload new(*shape, unit: :ns)
281
+ # Allocates a new int64 storage CArray of the given `shape` and
282
+ # wraps it as a CATime Face with the given `unit`. The
283
+ # reference epoch is the Unix epoch (1970-01-01 UTC).
284
+ # @param shape [Array<Integer>] shape of the new CATime.
285
+ # @param unit [Symbol] resolution unit (`:Y`, `:M`, `:W`, `:D`,
286
+ # `:h`, `:m`, `:s`, `:ms`, `:us`, `:ns`, ...).
287
+ # @return [CATime]
288
+ def self.new(*shape, unit: :ns)
289
+ raw = CArray.int64(*shape)
290
+ wrap(raw, unit: unit)
291
+ end
292
+
293
+ # @overload wrap(raw, unit: :ns)
294
+ # Zero-copy Face wrap of an existing int64 CArray. `unit` is a
295
+ # {Resolution} (or a Symbol / String it parses from).
296
+ # @param raw [CArray] int64 storage.
297
+ # @param unit [Resolution, Symbol, String] tick resolution.
298
+ # @return [CATime]
299
+ def self.wrap(raw, unit: :ns)
300
+ __wrap__(raw, unit)
301
+ end
302
+
303
+ # Element-return class: a single time element carries both its
304
+ # int64 count and its resolution, so scalar access (ca[i,j]) round-trips
305
+ # through this rather than a bare Integer.
306
+ class Element
307
+ include Comparable
308
+
309
+ # @!visibility private
310
+ UNIT_TO_SECONDS = {
311
+ Y: 365.25 * 86400, M: 30.5 * 86400, W: 7 * 86400, D: 86400,
312
+ h: 3600, m: 60, s: 1,
313
+ ms: 1.0e-3, us: 1.0e-6, ns: 1.0e-9,
314
+ ps: 1.0e-12, fs: 1.0e-15, as: 1.0e-18
315
+ }.freeze
316
+
317
+ # storage + value/unit accessors + initialize are C-implemented in
318
+ # ext/ca_obj_datetime.c (= TypedData_Make_Struct fast path).
319
+ # Methods below are Ruby-side; they call value/unit which are C accessors.
320
+ #
321
+ # Calendar breakdown is delegated to Ruby Date / Time (the scalar pays one
322
+ # object's cost, unlike the vectorized array accessors), but exactly: a
323
+ # :M / :Y value decodes through Date#next_month / #next_year (NOT the
324
+ # 30.5-day UNIT_TO_SECONDS approximation, which drifts), and a fixed-unit
325
+ # value through an exact Rational-second Time.at. All UTC.
326
+
327
+ EPOCH_JD = 2440588 # Julian Day Number of 1970-01-01
328
+
329
+ # @return [Time] the instant this scalar denotes (UTC). For a calendar
330
+ # unit this is the first instant (midnight of day 1) of the granule.
331
+ def to_time
332
+ require 'time'
333
+ require 'date'
334
+ case unit.base
335
+ when :Y then d = _epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
336
+ when :M then d = _epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
337
+ else
338
+ Time.at(Rational(value) * unit.tick_ratio, in: 'UTC') # exact seconds
339
+ end
340
+ end
341
+
342
+ # @return [Date] the date this scalar denotes (UTC). A sub-day unit is
343
+ # floored to its day.
344
+ def to_date
345
+ require 'date'
346
+ case unit.base
347
+ when :Y then _epoch_date.next_year(value * unit.count)
348
+ when :M then _epoch_date.next_month(value * unit.count)
349
+ when :W then Date.jd(EPOCH_JD + value * unit.count * 7, Date::GREGORIAN)
350
+ when :D then Date.jd(EPOCH_JD + value * unit.count, Date::GREGORIAN)
351
+ else Date.jd(EPOCH_JD + _floor_days, Date::GREGORIAN) # sub-day: floor to day
352
+ end
353
+ end
354
+
355
+ # @return [DateTime] the instant this scalar denotes (UTC).
356
+ def to_datetime
357
+ to_time.to_datetime
358
+ end
359
+
360
+ # Unit-aware string: coarse units print at their own granularity so the
361
+ # `(value, unit)` pair stays recoverable (a :M scalar is "2024-03", not
362
+ # "2024-03-01T00:00:00Z", which is indistinguishable from a :D).
363
+ # @return [String]
364
+ def to_s
365
+ case unit.base
366
+ when :Y then format("%04d", to_date.year)
367
+ when :M then to_date.strftime("%Y-%m")
368
+ when :W, :D then to_date.strftime("%Y-%m-%d")
369
+ else to_time.iso8601(_precision_digits) # :h .. :as (time shown)
370
+ end
371
+ end
372
+
373
+ # @return [String] the instant plus its storage unit.
374
+ def inspect
375
+ tag = unit.count == 1 ? "#{value}#{unit.base}" : "#{value} @ #{unit}"
376
+ "#<CATime::Element #{to_s} (#{tag})>"
377
+ end
378
+
379
+ # Three-way compare by INSTANT, reconciling a different unit (both are
380
+ # brought to a common unit both reach exactly, via `diff_unit`), so two
381
+ # datetimes are always ordered regardless of unit. Returns nil for a
382
+ # non-time operand (Time / DateTime are accepted); per the Comparable
383
+ # contract that makes `==` false and `<` raise, so no explicit `==` is
384
+ # defined (Comparable derives it: same instant -> equal, even cross-unit).
385
+ def <=>(other)
386
+ case other
387
+ when Element
388
+ u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
389
+ _instant_in(u) <=> other._instant_in(u)
390
+ when Time
391
+ to_time <=> other.getutc
392
+ when (defined?(DateTime) ? DateTime : nil)
393
+ to_time <=> other.to_time.getutc
394
+ end
395
+ rescue ArgumentError
396
+ nil
397
+ end
398
+
399
+ # Hash-key identity is unit-strict (mirrors Ruby: 1 == 1.0 but not
400
+ # 1.eql?(1.0)), so a :s and a :ms scalar at the same instant compare `==`
401
+ # yet key a Hash separately.
402
+ def eql?(other)
403
+ other.is_a?(Element) && other.unit == unit && other.value == value
404
+ end
405
+
406
+ # @return [Integer] a hash consistent with #eql? (unit-strict).
407
+ def hash
408
+ [value, unit].hash
409
+ end
410
+
411
+ # @overload +(td)
412
+ # Returns this instant advanced by a {CATimedelta::Element}. The unit
413
+ # promotes to the finer of the two (numpy-style: :D + :h -> :h). Only a
414
+ # duration in the SAME group (both calendar or both fixed) is accepted --
415
+ # a cross-group step (a :s time + a :M duration) is calendar
416
+ # arithmetic; use `to_date` + `Date#next_month` / `#next_year` for that.
417
+ # @param td [CATimedelta::Element]
418
+ # @return [CATime::Element]
419
+ # @raise [TypeError, ArgumentError] on a non-timedelta / cross-group operand.
420
+ def +(td)
421
+ _combine(td, 1)
422
+ end
423
+
424
+ # @overload -(other)
425
+ # Subtracting a {CATimedelta::Element} yields a {CATime::Element}
426
+ # (same rule as `+`); subtracting another {CATime::Element} yields
427
+ # the elapsed duration as a {CATimedelta::Element}, in the `diff_unit`
428
+ # of the two (same group -> finer unit; cross-group -> the fixed unit).
429
+ # @param other [CATimedelta::Element, CATime::Element]
430
+ # @return [CATime::Element, CATimedelta::Element]
431
+ def -(other)
432
+ case other
433
+ when Element
434
+ u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
435
+ CATimedelta::Element.new(_instant_in(u) - other.send(:_instant_in, u), u)
436
+ when CATimedelta::Element
437
+ _combine(other, -1)
438
+ else
439
+ raise TypeError, "CATime::Element - #{other.class} is not allowed"
440
+ end
441
+ end
442
+
443
+ protected
444
+
445
+ # This instant's storage value expressed in `to` unit (exact; raises if the
446
+ # instant does not land on `to`'s grid).
447
+ def _instant_in(to)
448
+ return value if to == unit
449
+ CATimeUnitAlgebra.convert_instant!(CA_INT64([value]), unit, to)[0]
450
+ end
451
+
452
+ private
453
+
454
+ # s +/- td: the time is the anchor, so the result keeps self's unit and
455
+ # the duration is converted into it (truncated toward zero when finer; a
456
+ # cross-group calendar duration raises). Mirrors the array CATime +/-.
457
+ def _combine(td, sign)
458
+ unless td.is_a?(CATimedelta::Element)
459
+ raise TypeError,
460
+ "CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
461
+ "allowed (add / subtract a CATimedelta)"
462
+ end
463
+ tv = CATimeUnitAlgebra.convert_scale_trunc(CA_INT64([td.value]), td.unit, unit)[0]
464
+ Element.new(value + sign * tv, unit)
465
+ end
466
+
467
+ # Base epoch as a Date. next_month / next_year clamp end-of-month
468
+ # (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
469
+ # do not move the base off the first of the month without revisiting.
470
+ def _epoch_date
471
+ require 'date'
472
+ Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
473
+ end
474
+
475
+ # Days since the epoch for a fixed sub-day unit (floored toward the past).
476
+ def _floor_days
477
+ sec = Rational(value) * unit.tick_ratio # exact seconds
478
+ (sec / 86400).floor
479
+ end
480
+
481
+ def _precision_digits
482
+ case unit.base
483
+ when :ms then 3
484
+ when :us then 6
485
+ when :ns, :ps, :fs, :as then 9 # Time caps at nanoseconds; finer is not shown
486
+ else 0
487
+ end
488
+ end
489
+ end
490
+ end
491
+
492
+ class CATimedelta
493
+ # @overload new(*shape, unit: :ns)
494
+ # Allocates a new int64 storage CArray of the given `shape` and
495
+ # wraps it as a CATimedelta Face with the given `unit`.
496
+ # @param shape [Array<Integer>] shape of the new CATimedelta.
497
+ # @param unit [Symbol] duration unit.
498
+ # @return [CATimedelta]
499
+ def self.new(*shape, unit: :ns)
500
+ raw = CArray.int64(*shape)
501
+ wrap(raw, unit: unit)
502
+ end
503
+
504
+ # @overload wrap(raw, unit: :ns)
505
+ # Zero-copy Face wrap of an existing int64 CArray as a duration.
506
+ # @param raw [CArray] int64 storage.
507
+ # @param unit [CATime::Resolution, Symbol, String] tick resolution.
508
+ # @return [CATimedelta]
509
+ def self.wrap(raw, unit: :ns)
510
+ __wrap__(raw, unit)
511
+ end
512
+
513
+ # A single duration: an integer tick count plus the resolution those ticks
514
+ # are counted in. What a scalar read of a {CATimedelta} returns.
515
+ class Element
516
+ include Comparable
517
+ # @!visibility private
518
+ UNIT_TO_SECONDS = CATime::Element::UNIT_TO_SECONDS
519
+
520
+ # storage + value/unit accessors + initialize are C-implemented in
521
+ # ext/ca_obj_timedelta.c (= TypedData_Make_Struct fast path).
522
+ # Methods below are Ruby-side; they call value/unit which are C accessors.
523
+
524
+ # @return [Rational] the exact duration in seconds. A :Y / :M duration
525
+ # has no exact second count (a month / year is calendar-variable), so it
526
+ # raises rather than silently handing back a 30.5-day approximation --
527
+ # use {#to_seconds_approx} when an estimate is acceptable.
528
+ # @raise [ArgumentError] for a :Y / :M unit.
529
+ def to_seconds
530
+ unless CATimeUnitAlgebra::FIXED.key?(unit.base)
531
+ raise ArgumentError,
532
+ "CATimedelta #{unit} has no exact seconds (a :#{unit.base} is " \
533
+ "calendar-variable); use to_seconds_approx for an estimate"
534
+ end
535
+ Rational(value) * unit.tick_ratio # exact seconds
536
+ end
537
+
538
+ # @return [Float] an approximate duration in seconds, using nominal
539
+ # lengths (a month = 30.5 days, a year = 365.25 days). The name makes
540
+ # the approximation explicit at the call site.
541
+ def to_seconds_approx
542
+ value * unit.count * (UNIT_TO_SECONDS[unit.base] || 1)
543
+ end
544
+
545
+ # @return [String] the value followed by its unit.
546
+ def to_s
547
+ unit.count == 1 ? "#{value}#{unit.base}" : "#{value} @ #{unit}"
548
+ end
549
+
550
+ # @return [String]
551
+ def inspect
552
+ "#<CATimedelta::Element #{to_s}>"
553
+ end
554
+
555
+ # Three-way compare, reconciling a different unit by the fixed ratio. A
556
+ # cross-group pair (a :M vs a :s duration) has no order -- there is no fixed
557
+ # ratio -- so it returns nil (Comparable then makes `<` raise and `==`
558
+ # false). No explicit `==` is defined; Comparable derives it.
559
+ def <=>(other)
560
+ return nil unless other.is_a?(Element)
561
+ return nil unless CATimeUnitAlgebra.same_group?(unit, other.unit)
562
+ u = CATimeUnitAlgebra.finer(unit, other.unit)
563
+ _scale_in(u) <=> other._scale_in(u)
564
+ rescue ArgumentError
565
+ nil
566
+ end
567
+
568
+ def eql?(other)
569
+ other.is_a?(Element) && other.unit == unit && other.value == value
570
+ end
571
+
572
+ # @return [Integer] a hash consistent with #eql? (unit-strict).
573
+ def hash
574
+ [value, unit].hash
575
+ end
576
+
577
+ # @overload +(other)
578
+ # Returns the sum of two durations, promoting to the finer unit. A
579
+ # cross-group operand (a :M + a :s duration) has no fixed relation and
580
+ # raises.
581
+ # @param other [CATimedelta::Element]
582
+ # @return [CATimedelta::Element]
583
+ def +(other) = _combine(other, 1)
584
+
585
+ # @overload -(other)
586
+ # Returns the difference of two durations (finer unit; cross-group raises).
587
+ # @param other [CATimedelta::Element]
588
+ # @return [CATimedelta::Element]
589
+ def -(other) = _combine(other, -1)
590
+
591
+ # @overload *(n)
592
+ # Returns this duration scaled by an Integer.
593
+ # @param n [Integer]
594
+ # @return [CATimedelta::Element]
595
+ def *(n)
596
+ raise TypeError, "CATimedelta::Element * #{n.class} is not allowed (Integer only)" unless n.is_a?(Integer)
597
+ Element.new(value * n, unit)
598
+ end
599
+
600
+ # @overload /(other)
601
+ # By an Integer -> a scaled {CATimedelta::Element}; by another
602
+ # {CATimedelta::Element} -> their dimensionless ratio as a `Rational`
603
+ # (both brought to the finer unit; cross-group raises).
604
+ # @param other [Integer, CATimedelta::Element]
605
+ # @return [CATimedelta::Element, Rational]
606
+ def /(other)
607
+ case other
608
+ when Integer then Element.new(value / other, unit)
609
+ when Element
610
+ u = CATimeUnitAlgebra.finer(unit, other.unit)
611
+ Rational(_scale_in(u), other.send(:_scale_in, u))
612
+ else
613
+ raise TypeError, "CATimedelta::Element / #{other.class} is not allowed"
614
+ end
615
+ end
616
+
617
+ protected
618
+
619
+ # This duration's value expressed in `to` unit (exact scale; raises on a
620
+ # cross-group or non-whole conversion).
621
+ def _scale_in(to)
622
+ return value if to == unit
623
+ CATimeUnitAlgebra.convert_scale!(CA_INT64([value]), unit, to)[0]
624
+ end
625
+
626
+ private
627
+
628
+ def _combine(other, sign)
629
+ unless other.is_a?(Element)
630
+ raise TypeError,
631
+ "CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
632
+ end
633
+ u = CATimeUnitAlgebra.finer(unit, other.unit)
634
+ Element.new(_scale_in(u) + sign * other.send(:_scale_in, u), u)
635
+ end
636
+ end
637
+ end
638
+
639
+ # ============================================================================
640
+ # Ruby surface operators (CATime)
641
+ # ============================================================================
642
+
643
+ class CATime
644
+ # NOTE: for internal ops accessing the storage CArray (= int64), use `parent`
645
+ # directly. `value` is a user-facing canonical method (= mask-strip CARefer
646
+ # view) with a lift hook deployed in the C layer; do not shadow.
647
+
648
+ # Sanctioned external accessor that pairs with the existing `unit` reader.
649
+ # An interop bridge (carray-xarray, carray-pycall consumers, etc.) can
650
+ # read out the int64 count + unit without reaching into `parent` (= an
651
+ # internal-contract accessor). The reference epoch is the implicit Unix
652
+ # epoch (1970-01-01 UTC) baked into the convention at the top of this
653
+ # file and into the C layer's Time.at-based decoding; it is not a
654
+ # per-instance state and therefore is not exposed as an accessor.
655
+ #
656
+ # ca.ticks # => CArray (int64), tick counts since 1970-01-01 UTC
657
+ # ca.unit # => Symbol (:Y :M :W :D :h :m :s :ms :us :ns ...)
658
+ # @overload ticks
659
+ # Returns the underlying int64 CArray of tick counts since the
660
+ # Unix epoch (1970-01-01 UTC) — the k-th tick of this array's
661
+ # resolution (see §4 of docs/CATime.md).
662
+ # @return [CArray]
663
+ def ticks
664
+ parent
665
+ end
666
+
667
+ # @overload to_unit(unit)
668
+ # Returns the same instants re-expressed on a finer grid: a new
669
+ # {CATime} whose storage is `self`'s ticks widened into `unit`.
670
+ # Accepted only when `self`'s tick is a whole multiple of `unit`'s, so
671
+ # every element lands exactly on the new grid and no instant moves
672
+ # (`:D` -> `:h`, `"1 hour"` -> `"10 minutes"`, `:Y` -> `:M`). A coarser
673
+ # or partially-overlapping target raises rather than rounding silently;
674
+ # use {#floor} / {#ceil} / {#round} to move to a coarser grid explicitly.
675
+ # @param unit [Resolution, Symbol, String] target resolution.
676
+ # @return [CATime]
677
+ # @raise [ArgumentError] when `self`'s tick is not a whole multiple of
678
+ # `unit`'s (including any calendar / fixed-length pair, where no fixed
679
+ # ratio exists).
680
+ # @raise [RangeError] when the widened ticks overflow int64.
681
+ def to_unit(unit)
682
+ to = CATime::Resolution.parse(unit)
683
+ CATimeUnitAlgebra.widen(parent,
684
+ CATimeUnitAlgebra.multiple_factor(self.unit, to)).time(unit: to)
685
+ end
686
+
687
+ # @overload +(other)
688
+ # Returns `self + other` for a {CATimedelta}: the time is the
689
+ # anchor, so the result keeps `self`'s unit and the duration is converted
690
+ # into it (a duration finer than `self`'s unit is truncated to it; a
691
+ # cross-group calendar duration raises). Adding two datetimes is
692
+ # ill-defined.
693
+ # @param other [CATimedelta]
694
+ # @return [CATime]
695
+ # @raise [TypeError] on a non-timedelta operand.
696
+ def +(other)
697
+ case other
698
+ when CATimedelta
699
+ (parent + CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
700
+ when CATime
701
+ raise TypeError, "CATime + CATime is ill-defined"
702
+ else
703
+ raise TypeError, "CATime + #{other.class} is not allowed (use CATimedelta)"
704
+ end
705
+ end
706
+
707
+ # @overload -(other)
708
+ # Subtracting a {CATimedelta} yields a {CATime} at `self`'s unit (the
709
+ # duration is converted into it, truncated when finer); subtracting
710
+ # another {CATime} yields a {CATimedelta} at the finer of the two
711
+ # units (cross-group falls to the fixed unit).
712
+ # @param other [CATimedelta, CATime]
713
+ # @return [CATime, CATimedelta]
714
+ # @raise [TypeError] on an unsupported operand.
715
+ def -(other)
716
+ case other
717
+ when CATimedelta
718
+ (parent - CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
719
+ when CATime
720
+ u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
721
+ a = CATimeUnitAlgebra.convert_instant!(parent, unit, u)
722
+ b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
723
+ (a - b).timedelta(unit: u)
724
+ else
725
+ raise TypeError, "CATime - #{other.class} is not allowed"
726
+ end
727
+ end
728
+
729
+ # Element-wise comparison (< <= > >= <=>) rides the core comparison Face
730
+ # gate (ca_face_reconcile_comparison in ext/ca_obj_face.c): the inherited
731
+ # CArray operators descend to storage and reconcile a CATime RHS in a
732
+ # different unit via to_comparable, returning a boolean CArray. <=> is the
733
+ # base CArray#<=> composed from the (gated) > and <.
734
+
735
+ # Element decode convention via the C-layer macro
736
+ # `CA_FACE_STORAGE_TO_SCALAR_IF_FACE`: no Ruby `[]` override is needed, a
737
+ # scalar auto-decodes on all scalar-return paths such as `ca[i,j,k]` /
738
+ # `ca[k]`. storage_to_scalar itself lives in C (ext/ca_obj_datetime.c)
739
+ # so the hot path stays off Ruby method dispatch. Its write counterpart
740
+ # is scalar_to_storage (below, surface -> storage on store).
741
+
742
+ # @!group Reductions
743
+
744
+ # min / max are order-structure reductions: they ride the core reduce
745
+ # Face gate (ORDERABLE storage descent + output re-lift, see
746
+ # ext/mkkernel.rb face_gate: :relift), so no Ruby override is needed --
747
+ # the inherited CArray#min / #max handle the Element / CATime return
748
+ # shapes directly. minmax is the paired form; it has no core gate, so
749
+ # re-lift both extremes here. They are actual elements, so they keep the
750
+ # storage unit (no rounding, unlike mean).
751
+
752
+ # @overload minmax(*axes, **opts)
753
+ # Returns the earliest and latest time as a `[min, max]` pair (§8).
754
+ # @return [Array(Element, Element), Array(CATime, CATime)]
755
+ def minmax(*args, **opts)
756
+ lo, hi = parent.minmax(*args, **opts)
757
+ [_lift_extremum(lo), _lift_extremum(hi)]
758
+ end
759
+
760
+ def _lift_extremum(r)
761
+ case r
762
+ when Integer then Element.new(r, unit)
763
+ when CArray then r.time(unit: unit)
764
+ else r
765
+ end
766
+ end
767
+ private :_lift_extremum
768
+
769
+ # The centroid / spread / order reductions report on the array's own grid:
770
+ # they run on the storage ticks and round the result back to the nearest
771
+ # tick of `unit`. A caller who needs a finer answer declares the grid
772
+ # first (`t.to_unit(:ms).mean`) -- the same rule linear_fetch follows.
773
+ # Rounding is to nearest, not toward zero: a truncating round would flip
774
+ # the direction of the error at the epoch.
775
+
776
+ # @overload mean(axis: nil, **opts)
777
+ # Returns the centroid time on `self`'s unit (§8), rounded to the
778
+ # nearest tick.
779
+ # @return [Element, CATime]
780
+ def mean(*args, **opts)
781
+ _reduce_dt(:mean, :time, args, opts)
782
+ end
783
+
784
+ # @overload median(axis: nil, **opts)
785
+ # Returns the median time on `self`'s unit (§8). An odd-count full
786
+ # reduction is an actual element (exact); the even-count / per-axis
787
+ # cases interpolate and round to the nearest tick.
788
+ # @return [Element, CATime]
789
+ def median(*args, **opts)
790
+ _reduce_dt(:median, :time, args, opts)
791
+ end
792
+
793
+ # @overload percentile(*p, axis: nil, **opts)
794
+ # Returns the percentile instants on `self`'s unit, in the shapes the
795
+ # plain {CArray#percentile} uses: one `p` reduces to a single value
796
+ # (an {Element}, or a {CATime} with `axis:`), two or more `p` give an
797
+ # Array of those.
798
+ # @return [Element, CATime, Array<Element>, Array<CATime>]
799
+ def percentile(*args, **opts)
800
+ _reduce_dt(:percentile, :time, args, opts)
801
+ end
802
+
803
+ # @overload quantile(axis: nil, **opts)
804
+ # Returns the five quartile instants `[p0, p25, p50, p75, p100]` on
805
+ # `self`'s unit (shorthand for `percentile(0, 25, 50, 75, 100)`).
806
+ # @return [Array<Element>, Array<CATime>]
807
+ def quantile(*args, **opts)
808
+ _reduce_dt(:quantile, :time, args, opts)
809
+ end
810
+
811
+ # @overload stddev(axis: nil, **opts)
812
+ # Returns the spread of the instants as a {CATimedelta} (a duration) on
813
+ # `self`'s unit. A spread is not a lattice point, so the rounding costs
814
+ # more here than for a centroid: on a coarse unit use
815
+ # `t.to_unit(:h).stddev` when the precision matters (§8).
816
+ # @return [CATimedelta::Element, CATimedelta]
817
+ def stddev(*args, **opts)
818
+ _reduce_dt(:stddev, :timedelta, args, opts)
819
+ end
820
+
821
+ # @overload stddevp(axis: nil, **opts)
822
+ # Returns the population spread as a {CATimedelta}, in the same shapes
823
+ # as {#stddev}.
824
+ # @return [CATimedelta::Element, CATimedelta]
825
+ def stddevp(*args, **opts)
826
+ _reduce_dt(:stddevp, :timedelta, args, opts)
827
+ end
828
+
829
+ # @overload sum(*)
830
+ # Not supported; use {#mean} for a centroid.
831
+ # @raise [TypeError] always.
832
+ def sum(*)
833
+ raise TypeError, "CATime#sum is ill-defined; use mean for centroid"
834
+ end
835
+
836
+ # @overload variance(*)
837
+ # Not supported: the variance of instants has squared-time units, which
838
+ # no type represents (ill-defined, like {#sum}). Use {#stddev} for the
839
+ # spread as a duration.
840
+ # @raise [TypeError] always.
841
+ def variance(*); raise TypeError, "CATime#variance is ill-defined (squared-time units); use stddev"; end
842
+
843
+ # @overload variancep(*)
844
+ # Not supported, for the same reason as {#variance}. Use {#stddevp}.
845
+ # @raise [TypeError] always.
846
+ def variancep(*); raise TypeError, "CATime#variancep is ill-defined (squared-time units); use stddevp"; end
847
+ # @!endgroup
848
+
849
+ # sort / partition ride the core sort Face gate (ORDERABLE storage
850
+ # descent + output re-lift): the inherited CArray#sort / #partition
851
+ # return a sorted CATime with the same unit, so no override is
852
+ # needed.
853
+
854
+ # @overload to_comparable(operand)
855
+ # Brings `operand` into `self`'s unit space for a direct storage
856
+ # comparison (comparison operators / search family / linear_section).
857
+ # `self` is the reference Face -- always one of our classes -- so it
858
+ # class-dispatches the operand rather than requiring every operand type
859
+ # to know every Face (which a core class like Time could not).
860
+ # CATime is ORDERABLE but not COMPARABLE (an operand may carry a
861
+ # different unit or shape), so the gate routes the operand through here.
862
+ #
863
+ # Accepted operands: another {CATime} (unit-rescaled to self),
864
+ # a {Element} (lifted to a length-1 CATime), a Ruby `Time`, and a
865
+ # Ruby `DateTime` (both absolute instants converted to self's unit,
866
+ # Unix epoch, UTC). A `String` is out of scope (parsing is a separate,
867
+ # opposite-direction mechanism). A bare Integer / other type raises;
868
+ # descend to `ca.parent` to compare the hidden storage directly.
869
+ #
870
+ # The rescale is an INSTANT conversion (convert_instant!), lossless: a
871
+ # coarser->finer unit always converts; a finer->coarser unit converts only
872
+ # when every value lands exactly on the coarser grid, else raises. Unlike
873
+ # a duration, a cross-group time cast IS possible via civil-date
874
+ # algebra (a :M value has a well-defined instant): :M/:Y widen exactly to
875
+ # :D and finer, and a fixed operand coarsens to :M/:Y only when it sits on
876
+ # the calendar boundary (:W is the one exception -- month/year starts are
877
+ # not week-aligned, so :Y/:M <-> :W raises).
878
+ # @param operand [CATime, CATime::Element, Time, DateTime]
879
+ # @return [CATime] in self's unit.
880
+ # @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
881
+ def to_comparable (operand)
882
+ case operand
883
+ when CATime
884
+ return operand if operand.unit == unit
885
+ CATimeUnitAlgebra.convert_instant!(operand.parent, operand.unit, unit)
886
+ .time(unit: unit)
887
+ when CATime::Element
888
+ lifted = CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
889
+ to_comparable(lifted)
890
+ when Time
891
+ # Reuse the single-literal builder; it yields a length-1 CATime
892
+ # in the requested (= self's) resolution.
893
+ CArray.time(operand, unit: unit)
894
+ when defined?(DateTime) && DateTime
895
+ to_comparable(operand.to_time.utc)
896
+ else
897
+ raise TypeError,
898
+ "CATime cannot reconcile #{operand.class} " \
899
+ "(use ca.parent to compare the raw int64 storage directly)"
900
+ end
901
+ end
902
+
903
+ # @overload scalar_to_storage(surface)
904
+ # Write-direction counterpart of storage_to_scalar (the store hook fired
905
+ # from rb_ca_obj2ptr): brings a surface value object into this Face's
906
+ # int64 storage (count in self's unit since the Unix epoch) so a scalar
907
+ # store round-trips with a fetch. A {Element} / `Time` / `DateTime` is
908
+ # reconciled to self's unit via {#to_comparable} (same lossless
909
+ # discipline: a cross-group unit or a non-exact finer->coarser cast
910
+ # raises). A bare Integer (the documented `.parent` raw-storage escape)
911
+ # and a String (parsing is a separate, opposite-direction mechanism)
912
+ # pass through unchanged to the storage cast.
913
+ # @param surface [Element, Time, DateTime, Integer, String]
914
+ # @return [Integer, Object] the storage-domain value, or `surface`
915
+ # unchanged for a pass-through type.
916
+ # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
917
+ def scalar_to_storage (surface)
918
+ case surface
919
+ when Integer, String
920
+ surface
921
+ else
922
+ to_comparable(surface).parent[0]
923
+ end
924
+ end
925
+
926
+ # NOTE: search (bsearch / search / bsearch_addr) and linear_section need NO
927
+ # per-method override. The C kernel gate descends an ORDERABLE Face
928
+ # reference to storage and, for a Face query, calls to_comparable itself to
929
+ # reconcile it -- so declaring ORDERABLE (init flag) and defining
930
+ # to_comparable (above) is the whole contract. linear_fetch is the one
931
+ # exception: it returns a value rather than a position, so it re-lifts
932
+ # (below).
933
+
934
+ # @overload linear_fetch(addr, axis: nil)
935
+ # Returns the time at the fractional position `addr` on this array's
936
+ # grid, interpolating between the two bracketing instants. The inverse
937
+ # of `linear_section`, and the reason for the override: `linear_fetch`
938
+ # returns a *value*, so the result is a {CATime} again, whereas
939
+ # `linear_section` returns a position and stays a plain index.
940
+ #
941
+ # The result keeps `self`'s unit -- the array's grid is the output grid,
942
+ # so an instant that lands between two ticks is rounded to the nearest
943
+ # one. Widen the grid first when the interpolation needs finer
944
+ # resolution: `t.to_unit(:ms).linear_fetch(addr)` interpolates on the
945
+ # millisecond grid. (Unlike {#mean} / {#median}, which collapse the axis
946
+ # and therefore have no output grid to preserve, so they refine the
947
+ # resolution instead.)
948
+ #
949
+ # An out-of-range `addr` yields UNDEF rather than the NaN that a plain
950
+ # float axis returns -- int64 storage has no NaN to carry a sentinel. A
951
+ # masked `addr` cell stays UNDEF (the kernel's own rule: an undetermined
952
+ # query gets an undetermined answer).
953
+ # @param addr [Float, CArray] fractional position(s) into `self`.
954
+ # @param axis [Integer, nil]
955
+ # @return [Element, CATime] an {Element} for a scalar `addr`, `nil` when
956
+ # that single answer is undetermined.
957
+ def linear_fetch (addr, **opts)
958
+ r = parent.float64.linear_fetch(addr, **opts)
959
+ case r
960
+ when CArray
961
+ # mask_invalid before the int64 cast: the cast turns a NaN into 0, which
962
+ # would read as the epoch instead of "no answer".
963
+ r.mask_invalid.round.int64.time(unit: unit)
964
+ when Numeric
965
+ # Scalar query. Out of range -> nil, joining the nil the kernel already
966
+ # returns for a masked scalar query: one "no answer" on this path.
967
+ r.to_f.nan? ? nil : Element.new(r.round, unit)
968
+ else
969
+ r # nil (masked scalar query)
970
+ end
971
+ end
972
+
973
+ # ====================================================================
974
+ # pandas .dt.* style field accessors
975
+ # ====================================================================
976
+ #
977
+ # Face-strip semantic: each accessor returns a CArray of Integer / bool
978
+ # (= year / month / etc. are not time, they descend to the storage
979
+ # type), computed directly from the storage int64.
980
+ # ====================================================================
981
+
982
+ # @overload to_time
983
+ # Converts every element to a Ruby `Time` (UTC), returned as an object
984
+ # CArray. Array-level counterpart to {Element#to_time}.
985
+ # @return [CArray]
986
+ def to_time
987
+ require 'time'
988
+ if CATimeUnitAlgebra::FIXED.key?(unit.base)
989
+ f = unit.tick_ratio # exact seconds / tick (Rational)
990
+ parent.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
991
+ else # calendar: exact granule midnight
992
+ (_field_days * 86400).convert(:object) {|v| Time.at(v, in: 'UTC')}
993
+ end
994
+ end
995
+
996
+ # @overload to_date
997
+ # Converts every element to a Ruby `Date` (UTC), returned as an object
998
+ # CArray. A sub-day unit floors to its day. Array-level counterpart to
999
+ # {Element#to_date}.
1000
+ # @return [CArray]
1001
+ def to_date
1002
+ require 'date'
1003
+ # 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
1004
+ _field_days.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
1005
+ end
1006
+
1007
+ # @overload to_datetime
1008
+ # Converts every element to a Ruby `DateTime` (UTC offset 0), returned
1009
+ # as an object CArray.
1010
+ # @return [CArray]
1011
+ def to_datetime
1012
+ require 'date'
1013
+ to_time.convert(:object) {|t| t.to_datetime}
1014
+ end
1015
+
1016
+ # @!group Field accessors
1017
+ # Each accessor returns an integer CArray with the requested calendar /
1018
+ # clock field extracted from every element (UTC), computed by vectorized
1019
+ # civil-date algebra directly on the int64 storage -- no per-cell Time.
1020
+ # Exact for every unit, including :M / :Y (where the old Time.at path drifted
1021
+ # by using 30.5-day / 365.25-day approximations). Fields finer than the
1022
+ # storage unit collapse to their zero point, and the input mask propagates.
1023
+ # @return [CArray]
1024
+ def year
1025
+ case unit.base
1026
+ when :Y then parent * unit.count + 1970
1027
+ when :M then self.class.send(:_floordiv_i, parent * unit.count + 1970 * 12, 12)
1028
+ else self.class.send(:_civil_from_days, _field_days)[0]
1029
+ end
1030
+ end
1031
+
1032
+ # Calendar month, 1..12. Every cell is 1 for `:Y` storage, which does not
1033
+ # resolve months.
1034
+ # @return [CArray] integer.
1035
+ def month
1036
+ case unit.base
1037
+ when :Y then parent * 0 + 1
1038
+ when :M
1039
+ mo = parent * unit.count + 1970 * 12
1040
+ mo - self.class.send(:_floordiv_i, mo, 12) * 12 + 1
1041
+ else self.class.send(:_civil_from_days, _field_days)[1]
1042
+ end
1043
+ end
1044
+
1045
+ # Day of the month, 1..31. Every cell is 1 for `:Y` / `:M` storage, which
1046
+ # does not resolve days.
1047
+ # @return [CArray] integer.
1048
+ def day
1049
+ case unit.base
1050
+ when :Y, :M then parent * 0 + 1
1051
+ else self.class.send(:_civil_from_days, _field_days)[2]
1052
+ end
1053
+ end
1054
+
1055
+ # Hour of the day, 0..23; 0 when the storage unit is coarser than an hour.
1056
+ # @return [CArray] integer.
1057
+ def hour; _clock_field(:h); end
1058
+ # Minute of the hour, 0..59; 0 when the storage unit is coarser than a minute.
1059
+ # @return [CArray] integer.
1060
+ def minute; _clock_field(:m); end
1061
+ # Second of the minute, 0..59; 0 when the storage unit is coarser than a second.
1062
+ # @return [CArray] integer.
1063
+ def second; _clock_field(:s); end
1064
+
1065
+ # Day of the week, Sunday = 0 .. Saturday = 6.
1066
+ # @return [CArray] integer.
1067
+ def weekday
1068
+ # 1970-01-01 is a Thursday (wday 4); Sun=0..Sat=6.
1069
+ n = _field_days + 4
1070
+ n - self.class.send(:_floordiv_i, n, 7) * 7 # floor-mod 7
1071
+ end
1072
+
1073
+ # Day of the year, 1..366.
1074
+ # @return [CArray] integer.
1075
+ def yday
1076
+ d = _field_days
1077
+ y = self.class.send(:_civil_from_days, d)[0]
1078
+ ones = CArray.int64(*shape) { 1 }
1079
+ d - self.class.send(:_days_from_civil, y, ones, ones) + 1
1080
+ end
1081
+ # @!endgroup
1082
+
1083
+ # @overload jd
1084
+ # Returns the Julian Day Number for each element.
1085
+ # @return [CArray]
1086
+ def jd
1087
+ require 'date'
1088
+ to_time.convert(:int) {|t| t.to_date.jd}
1089
+ end
1090
+
1091
+ # @overload ajd
1092
+ # Returns the Astronomical Julian Day (float, offset by half a
1093
+ # day) for each element.
1094
+ # @return [CArray]
1095
+ def ajd
1096
+ require 'date'
1097
+ to_time.convert(:double) {|t| t.to_datetime.ajd.to_f}
1098
+ end
1099
+
1100
+ # @overload is_leap
1101
+ # Returns a boolean CArray flagging elements that fall in a
1102
+ # leap year (UTC).
1103
+ # @return [CArray]
1104
+ def is_leap
1105
+ y = year
1106
+ ((y % 4).eq(0) & (y % 100).ne(0)) | (y % 400).eq(0)
1107
+ end
1108
+
1109
+ # @overload strftime(fmt)
1110
+ # Returns a {CAString} whose elements are the per-cell
1111
+ # `Time#strftime(fmt)` result (UTC). The input mask propagates.
1112
+ # @param fmt [String] strftime format string.
1113
+ # @return [CAString]
1114
+ def strftime(fmt)
1115
+ CAString.wrap(to_time.convert(:object) {|t| t.strftime(fmt)})
1116
+ end
1117
+
1118
+ private
1119
+
1120
+ # Reduce to `kind` (:time -> CATime, :timedelta -> CATimedelta) on the
1121
+ # array's own grid. The reduction runs on the storage ticks -- for a
1122
+ # calendar unit that means the month / year ordinal, so a :M centroid is
1123
+ # the centroid of month numbers (a caller wanting the day-space answer
1124
+ # writes to_unit(:D) first) -- and the result is rounded back to a tick.
1125
+ def _reduce_dt(op, kind, args, opts)
1126
+ _lift_reduced(parent.send(op, *args, **opts), kind)
1127
+ end
1128
+
1129
+ # Round-to-nearest onto the storage grid, then wear the Face again. An
1130
+ # Array arrives from the multi-p percentile / quantile shapes; UNDEF and
1131
+ # nil (empty or all-masked) pass through untouched.
1132
+ def _lift_reduced(r, kind)
1133
+ case r
1134
+ when Numeric
1135
+ v = r.round
1136
+ kind == :timedelta ? CATimedelta::Element.new(v, unit) : Element.new(v, unit)
1137
+ when CArray
1138
+ # A cell with no answer arrives masked and the mask survives the cast.
1139
+ # NaN does not: it has no int64 image, so a `fill_value: Float::NAN`
1140
+ # cell would cast to garbage bits. mask_invalid turns it into UNDEF,
1141
+ # which is what "no answer" means here.
1142
+ iv = r.round.mask_invalid.int64
1143
+ kind == :timedelta ? iv.timedelta(unit: unit) : iv.time(unit: unit)
1144
+ when Array
1145
+ r.map {|x| _lift_reduced(x, kind)}
1146
+ else
1147
+ r # UNDEF / nil passthrough (empty or all-masked reduction)
1148
+ end
1149
+ end
1150
+
1151
+ end
1152
+
1153
+ # ============================================================================
1154
+ # Ruby surface operators (CATimedelta)
1155
+ # ============================================================================
1156
+
1157
+ class CATimedelta
1158
+ # NOTE: internal ops use `parent` directly. `value` is a C-layer canonical
1159
+ # method (mask-strip) with a lift hook deployed in the C layer; do not
1160
+ # shadow.
1161
+
1162
+ # @overload ticks
1163
+ # Returns the underlying int64 CArray of tick counts — the duration
1164
+ # measured in this array's resolution (see {CATime#ticks}).
1165
+ # @return [CArray]
1166
+ def ticks
1167
+ parent
1168
+ end
1169
+
1170
+ # @overload to_unit(unit)
1171
+ # Returns the same durations re-expressed on a finer grid: a new
1172
+ # {CATimedelta} whose storage is `self`'s ticks widened into `unit`.
1173
+ # Accepted only when `self`'s tick is a whole multiple of `unit`'s
1174
+ # (`:D` -> `:h`, `"1 hour"` -> `"10 minutes"`), so no duration changes.
1175
+ # A coarser target raises rather than truncating silently; a calendar /
1176
+ # fixed-length pair raises because a month has no fixed length.
1177
+ # @param unit [Resolution, Symbol, String] target resolution.
1178
+ # @return [CATimedelta]
1179
+ # @raise [ArgumentError] when `self`'s tick is not a whole multiple of
1180
+ # `unit`'s.
1181
+ # @raise [RangeError] when the widened ticks overflow int64.
1182
+ def to_unit(unit)
1183
+ to = CATime::Resolution.parse(unit)
1184
+ CATimeUnitAlgebra.widen(parent,
1185
+ CATimeUnitAlgebra.multiple_factor(self.unit, to)).timedelta(unit: to)
1186
+ end
1187
+
1188
+ # @overload +(other)
1189
+ # Returns `self + other`. A {CATimedelta} operand yields a
1190
+ # {CATimedelta}; a {CATime} operand delegates to
1191
+ # {CATime#+} (commutative).
1192
+ # @param other [CATimedelta, CATime]
1193
+ # @return [CATimedelta, CATime]
1194
+ # @raise [TypeError] on incompatible operands.
1195
+ def +(other)
1196
+ case other
1197
+ when CATimedelta
1198
+ u = _finer_duration(other.unit)
1199
+ a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
1200
+ b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1201
+ (a + b).timedelta(unit: u)
1202
+ when CATime
1203
+ other + self # commutative -> time's unit
1204
+ else
1205
+ raise TypeError, "CATimedelta + #{other.class} is not allowed"
1206
+ end
1207
+ end
1208
+
1209
+ # @overload -(other)
1210
+ # Returns the difference of two {CATimedelta} at the finer of the two
1211
+ # units (a cross-group pair raises).
1212
+ # @param other [CATimedelta]
1213
+ # @return [CATimedelta]
1214
+ # @raise [TypeError] on a non-timedelta operand.
1215
+ def -(other)
1216
+ case other
1217
+ when CATimedelta
1218
+ u = _finer_duration(other.unit)
1219
+ a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
1220
+ b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1221
+ (a - b).timedelta(unit: u)
1222
+ else
1223
+ raise TypeError, "CATimedelta - #{other.class} is not allowed"
1224
+ end
1225
+ end
1226
+
1227
+ # @overload abs
1228
+ # Returns the magnitude of each duration as a {CATimedelta} on the same
1229
+ # unit.
1230
+ # @return [CATimedelta]
1231
+ def abs
1232
+ parent.abs.timedelta(unit: unit)
1233
+ end
1234
+
1235
+ # @overload -@
1236
+ # Returns each duration with its sign reversed, on the same unit.
1237
+ # @return [CATimedelta]
1238
+ def -@
1239
+ (-parent).timedelta(unit: unit)
1240
+ end
1241
+
1242
+ # @overload *(other)
1243
+ # Returns `self` scaled by an Integer.
1244
+ # @param other [Integer]
1245
+ # @return [CATimedelta]
1246
+ # @raise [TypeError] when `other` is not an Integer.
1247
+ def *(other)
1248
+ case other
1249
+ when Integer then (parent * other).timedelta(unit: unit)
1250
+ else raise TypeError, "CATimedelta * #{other.class} is not allowed (Integer only)"
1251
+ end
1252
+ end
1253
+
1254
+ # @overload /(other)
1255
+ # Returns element-wise division: by Integer yields a
1256
+ # {CATimedelta}, by another {CATimedelta} with matching `unit`
1257
+ # yields a dimensionless CArray.
1258
+ # @param other [Integer, CATimedelta]
1259
+ # @return [CATimedelta, CArray]
1260
+ # @raise [TypeError] on incompatible operands.
1261
+ def /(other)
1262
+ case other
1263
+ when Integer then (parent / other).timedelta(unit: unit)
1264
+ when CATimedelta
1265
+ u = _finer_duration(other.unit)
1266
+ CATimeUnitAlgebra.convert_scale!(parent, unit, u) /
1267
+ CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1268
+ else raise TypeError, "CATimedelta / #{other.class} is not allowed"
1269
+ end
1270
+ end
1271
+
1272
+ # Element-wise comparison (< <= > >= <=>) rides the core comparison Face
1273
+ # gate (ca_face_reconcile_comparison): the inherited CArray operators
1274
+ # descend to storage and reconcile a CATimedelta RHS in a different unit
1275
+ # via to_comparable, returning a boolean CArray.
1276
+
1277
+ # @!group Reductions
1278
+
1279
+ # @overload sum(*args, **opts)
1280
+ # Returns the sum of durations as a {Element} for full reduction
1281
+ # or as a {CATimedelta} view for per-axis reduction.
1282
+ # @return [Element, CATimedelta]
1283
+ def sum(*args, **opts)
1284
+ _lift_reduced(parent.sum(*args, **opts))
1285
+ end
1286
+
1287
+ # @overload mean(axis: nil, **opts)
1288
+ # Returns the mean duration rounded to the nearest unit count.
1289
+ # @return [Element, CATimedelta]
1290
+ def mean(*args, **opts)
1291
+ _lift_reduced(parent.mean(*args, **opts))
1292
+ end
1293
+
1294
+ # median / percentile / quantile / stddev / stddevp report on this array's
1295
+ # own unit, rounded to the nearest tick -- the same rule {CATime} follows.
1296
+ # A caller who needs a finer answer declares the grid first
1297
+ # (`td.to_unit(:ms).stddev`).
1298
+
1299
+ # @overload median(axis: nil, **opts)
1300
+ # Returns the median duration on `self`'s unit.
1301
+ # @return [Element, CATimedelta]
1302
+ def median(*args, **opts)
1303
+ _lift_reduced(parent.median(*args, **opts))
1304
+ end
1305
+
1306
+ # @overload percentile(*p, axis: nil, **opts)
1307
+ # Returns the percentile durations on `self`'s unit, in the shapes the
1308
+ # plain {CArray#percentile} uses (one `p` reduces to a single value,
1309
+ # two or more give an Array).
1310
+ # @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
1311
+ def percentile(*args, **opts)
1312
+ _lift_reduced(parent.percentile(*args, **opts))
1313
+ end
1314
+
1315
+ # @overload quantile(axis: nil, **opts)
1316
+ # Returns the five quartile durations `[p0, p25, p50, p75, p100]` on
1317
+ # `self`'s unit.
1318
+ # @return [Array<Element>, Array<CATimedelta>]
1319
+ def quantile(*args, **opts)
1320
+ _lift_reduced(parent.quantile(*args, **opts))
1321
+ end
1322
+
1323
+ # @overload stddev(axis: nil, **opts)
1324
+ # Returns the spread of the durations on `self`'s unit. A spread is not
1325
+ # a lattice point, so on a coarse unit use `td.to_unit(:h).stddev` when
1326
+ # the precision matters.
1327
+ # @return [Element, CATimedelta]
1328
+ def stddev(*args, **opts)
1329
+ _lift_reduced(parent.stddev(*args, **opts))
1330
+ end
1331
+
1332
+ # @overload stddevp(axis: nil, **opts)
1333
+ # Returns the population spread on `self`'s unit, in the same shapes as
1334
+ # {#stddev}.
1335
+ # @return [Element, CATimedelta]
1336
+ def stddevp(*args, **opts)
1337
+ _lift_reduced(parent.stddevp(*args, **opts))
1338
+ end
1339
+
1340
+ # variance / variancep are the one pair in this family that cannot carry a
1341
+ # unit: their value is in squared ticks, and no Face represents squared
1342
+ # time. They are left as the inherited plain Float (unlike
1343
+ # {CATime#variance}, which raises -- a squared instant is meaningless,
1344
+ # while a squared duration is a real quantity, s^2). Read the result as
1345
+ # "in units of `self.unit` squared", or take {#stddev} for a typed answer.
1346
+
1347
+ def _lift_reduced(r)
1348
+ case r
1349
+ when Numeric then Element.new(r.round, unit)
1350
+ when CArray then r.round.mask_invalid.int64.timedelta(unit: unit)
1351
+ when Array then r.map {|x| _lift_reduced(x)}
1352
+ else r
1353
+ end
1354
+ end
1355
+ private :_lift_reduced
1356
+
1357
+ # min / max ride the core reduce Face gate (ORDERABLE storage descent +
1358
+ # output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
1359
+ # CArray#min / #max return the Element / CATimedelta shapes directly.
1360
+ # @overload variance(*)
1361
+ # Not supported: the variance of durations has squared-time units, which
1362
+ # no type represents -- the same reason {CATime#variance} refuses. Use
1363
+ # {#stddev} for the spread as a duration.
1364
+ # @raise [TypeError] always.
1365
+ def variance(*)
1366
+ raise TypeError, "CATimedelta#variance is ill-defined (squared-time units); use stddev"
1367
+ end
1368
+
1369
+ # @overload variancep(*)
1370
+ # Not supported, for the same reason as {#variance}. Use {#stddevp}.
1371
+ # @raise [TypeError] always.
1372
+ def variancep(*)
1373
+ raise TypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
1374
+ end
1375
+
1376
+ # minmax has no core gate, so re-lift both extremes here.
1377
+
1378
+ # @overload minmax(*axes, **opts)
1379
+ # Returns the shortest and longest duration as a `[min, max]` pair.
1380
+ # @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
1381
+ def minmax(*args, **opts)
1382
+ lo, hi = parent.minmax(*args, **opts)
1383
+ [_lift_extremum(lo), _lift_extremum(hi)]
1384
+ end
1385
+
1386
+ def _lift_extremum(r)
1387
+ case r
1388
+ when Integer then Element.new(r, unit)
1389
+ when CArray then r.timedelta(unit: unit)
1390
+ else r
1391
+ end
1392
+ end
1393
+ private :_lift_extremum
1394
+ # @!endgroup
1395
+
1396
+ # sort / partition ride the core sort Face gate (ORDERABLE storage
1397
+ # descent + output re-lift); the inherited CArray#sort / #partition
1398
+ # return a sorted CATimedelta with the same unit.
1399
+
1400
+ # @overload to_comparable(operand)
1401
+ # Brings `operand` into `self`'s unit space for a direct storage
1402
+ # comparison (see {CATime#to_comparable} for the reference-side
1403
+ # contract). `self` is the reference Face and class-dispatches the
1404
+ # operand. Coverage is deliberately narrower than CATime: only
1405
+ # another {CATimedelta} (unit-rescaled to self) and a {Element} (lifted
1406
+ # to a length-1 CATimedelta) are accepted. `Time` / `DateTime` are
1407
+ # absolute instants, not durations, so they raise -- a Face owns its own
1408
+ # coverage. Auto-casts to self's unit when lossless (coarser->finer
1409
+ # always; finer->coarser only when every value is exact), otherwise
1410
+ # raises.
1411
+ # @param operand [CATimedelta, CATimedelta::Element]
1412
+ # @return [CATimedelta] in self's unit.
1413
+ # @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
1414
+ def to_comparable (operand)
1415
+ case operand
1416
+ when CATimedelta
1417
+ return operand if operand.unit == unit
1418
+ CATimeUnitAlgebra.convert_scale!(operand.parent, operand.unit, unit)
1419
+ .timedelta(unit: unit)
1420
+ when CATimedelta::Element
1421
+ lifted = CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
1422
+ to_comparable(lifted)
1423
+ else
1424
+ raise TypeError,
1425
+ "CATimedelta cannot reconcile #{operand.class} " \
1426
+ "(use ca.parent to compare the raw int64 storage directly)"
1427
+ end
1428
+ end
10
1429
 
11
- require "date"
1430
+ # @overload scalar_to_storage(surface)
1431
+ # Write-direction counterpart of storage_to_scalar: brings a surface
1432
+ # value object into this Face's int64 storage (count in self's unit) so a
1433
+ # scalar store round-trips with a fetch. A {Element} is reconciled to
1434
+ # self's unit via {#to_comparable} (lossless discipline; a `Time` /
1435
+ # `DateTime` is an absolute instant, not a duration, so it raises there).
1436
+ # A bare Integer (the `.parent` raw-storage escape) and a String pass
1437
+ # through unchanged.
1438
+ # @param surface [Element, Integer, String]
1439
+ # @return [Integer, Object] the storage-domain value, or `surface`
1440
+ # unchanged for a pass-through type.
1441
+ # @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
1442
+ def scalar_to_storage (surface)
1443
+ case surface
1444
+ when Integer, String
1445
+ surface
1446
+ else
1447
+ to_comparable(surface).parent[0]
1448
+ end
1449
+ end
1450
+
1451
+ # Search / linear_section need no per-method override: the C kernel gate
1452
+ # descends an ORDERABLE Face reference and reconciles a Face query via
1453
+ # to_comparable. See {CATime}.
1454
+
1455
+ # @overload linear_fetch(addr, axis: nil)
1456
+ # Returns the duration at the fractional position `addr` on this array's
1457
+ # grid, interpolating between the two bracketing durations. Keeps
1458
+ # `self`'s unit and rounds to it (widen the grid with {#to_unit} first
1459
+ # when the interpolation needs finer resolution), which matches {#mean} /
1460
+ # {#sum} -- a duration reduction stays on the grid too. An out-of-range
1461
+ # `addr` yields UNDEF; see {CATime#linear_fetch} for the full contract.
1462
+ # @param addr [Float, CArray] fractional position(s) into `self`.
1463
+ # @param axis [Integer, nil]
1464
+ # @return [Element, CATimedelta] an {Element} for a scalar `addr`, `nil`
1465
+ # when that single answer is undetermined.
1466
+ def linear_fetch (addr, **opts)
1467
+ r = parent.float64.linear_fetch(addr, **opts)
1468
+ case r
1469
+ when CArray then r.mask_invalid.round.int64.timedelta(unit: unit)
1470
+ when Numeric then r.to_f.nan? ? nil : Element.new(r.round, unit)
1471
+ else r
1472
+ end
1473
+ end
1474
+
1475
+ private
1476
+
1477
+ # The finer of `self`'s unit and `ou` for a duration + duration / ratio. A
1478
+ # cross-group pair (a month vs a second) has no common duration grid, so it
1479
+ # raises rather than mixing calendar and fixed durations.
1480
+ def _finer_duration(ou)
1481
+ unless CATimeUnitAlgebra.same_group?(unit, ou)
1482
+ raise ArgumentError,
1483
+ "cannot combine a #{unit} duration with a #{ou} duration " \
1484
+ "across the calendar/fixed boundary"
1485
+ end
1486
+ CATimeUnitAlgebra.finer(unit, ou)
1487
+ end
1488
+ end
12
1489
 
13
1490
  class CArray
14
-
15
- def str_to_datetime (template = nil)
16
- if template
17
- return convert() {|v| DateTime.strptime(v, template) }
1491
+ # Time is stored as an int64 tick index on a grid whose resolution is
1492
+ # `unit` (= a {CATime::Resolution}, tick = count * base). The value
1493
+ # is the k-th tick since the Unix epoch (1970-01-01 UTC): e.g. unit
1494
+ # `"10 minutes"` value 3 = 1970-01-01T00:30:00Z. All parsing is UTC (an
1495
+ # explicit offset is honoured; otherwise UTC), DateTime-independent
1496
+ # (Date._parse + civil kernel).
1497
+
1498
+ # Exact Rational seconds since the Unix epoch for a start literal (Time /
1499
+ # DateTime / Integer unix-seconds / String). UTC default.
1500
+ def self._epoch_seconds_exact(spec, format = nil)
1501
+ require 'date'
1502
+ require 'time'
1503
+ case spec
1504
+ when Time then spec.to_r
1505
+ when Integer then Rational(spec)
1506
+ when String
1507
+ h = format ? Date._strptime(spec, format) : Date._parse(spec)
1508
+ unless h && h[:year] && h[:mon] && h[:mday]
1509
+ raise ArgumentError, "cannot parse time #{spec.inspect}"
1510
+ end
1511
+ days = CATime.send(:_days_from_civil,
1512
+ CA_INT64([h[:year]]), CA_INT64([h[:mon]]),
1513
+ CA_INT64([h[:mday]]))[0]
1514
+ sec = Rational(days * 86400 + (h[:hour] || 0) * 3600 +
1515
+ (h[:min] || 0) * 60 + (h[:sec] || 0))
1516
+ sec += h[:sec_fraction] if h[:sec_fraction]
1517
+ sec -= h[:offset] if h[:offset] # east-of-UTC offset -> UTC
1518
+ sec
1519
+ else
1520
+ if defined?(DateTime) && spec.is_a?(DateTime)
1521
+ spec.to_time.to_r
1522
+ else
1523
+ raise ArgumentError, "cannot parse time #{spec.class}"
1524
+ end
1525
+ end
1526
+ end
1527
+
1528
+ # [year, month] (UTC) of a start literal, for a calendar-resolution grid.
1529
+ def self._epoch_year_month(spec, format = nil)
1530
+ require 'date'
1531
+ require 'time'
1532
+ case spec
1533
+ when Time then t = spec.utc; [t.year, t.month]
1534
+ when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
1535
+ when String
1536
+ h = format ? Date._strptime(spec, format) : Date._parse(spec)
1537
+ unless h && h[:year]
1538
+ raise ArgumentError, "cannot parse time #{spec.inspect}"
1539
+ end
1540
+ [h[:year], h[:mon] || 1]
18
1541
  else
19
- return convert() {|v| DateTime.parse(v) }
1542
+ if defined?(DateTime) && spec.is_a?(DateTime)
1543
+ t = spec.to_time.utc; [t.year, t.month]
1544
+ else
1545
+ raise ArgumentError, "cannot parse time #{spec.class}"
1546
+ end
20
1547
  end
21
1548
  end
22
1549
 
23
- def str_to_time (template = nil)
24
- if template
25
- return str_strptime(template)
1550
+ # Tick index of `spec`'s instant on the `res` grid (floor toward the past).
1551
+ def self._epoch_tick_index(spec, res, format = nil)
1552
+ if CATimeUnitAlgebra::FIXED.key?(res.base)
1553
+ (_epoch_seconds_exact(spec, format) / res.tick_ratio).floor
26
1554
  else
27
- return convert() {|v| Time.parse(v) }
1555
+ y, m = _epoch_year_month(spec, format)
1556
+ months = (y - 1970) * 12 + (m - 1)
1557
+ (Rational(months) / res.tick_ratio).floor
1558
+ end
1559
+ end
1560
+
1561
+ # @overload time_range(start, last, unit:, step: nil, format: nil)
1562
+ # Returns a {CATime} from `start` to `last` inclusive on the `unit`
1563
+ # grid, spaced `step` apart. `unit` is the resolution the result is
1564
+ # stored on and `step` is the spacing, so an hourly grid sampled once a
1565
+ # day is `unit: :h, step: "1 day"`. With no `step` the spacing is one
1566
+ # `unit` tick (consecutive ticks). Off-grid endpoints floor to their
1567
+ # bucket head (toward the past); the phase is anchored at `start`, and
1568
+ # `last` is a bound rather than a member -- the series stops at the last
1569
+ # step at or before it.
1570
+ # @param start [Time, String, Integer, DateTime] first instant.
1571
+ # @param last [Time, String, Integer, DateTime] last instant (inclusive).
1572
+ # @param unit [Resolution, Symbol, String] grid resolution (tick).
1573
+ # @param step [Resolution, Symbol, String, nil] spacing between elements
1574
+ # (default: one `unit` tick). Must be a whole multiple of `unit`.
1575
+ # @param format [String, nil] optional strptime format for String inputs.
1576
+ # @return [CATime]
1577
+ # @raise [ArgumentError] when `step` is not a whole multiple of `unit`
1578
+ # (including a calendar `step` on a fixed-length `unit`, e.g. a month
1579
+ # step on an hour grid -- a month is not a fixed number of hours).
1580
+ def self.time_range(start, last, unit:, step: nil, format: nil)
1581
+ res = CATime::Resolution.parse(unit)
1582
+ stride = step.nil? ? 1 :
1583
+ CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
1584
+ s = _epoch_tick_index(start, res, format)
1585
+ e = _epoch_tick_index(last, res, format)
1586
+ n = e < s ? 0 : (e - s) / stride + 1
1587
+ CArray.int64(n) {|i| s + i * stride }.time(unit: res)
1588
+ end
1589
+
1590
+ # @overload time_series(start, count:, unit:, step: nil, format: nil)
1591
+ # Returns a {CATime} of `count` instants starting at `start` on the
1592
+ # `unit` grid, spaced `step` apart. `unit` is the resolution the result
1593
+ # is stored on and `step` is the spacing, so an hourly grid sampled once
1594
+ # a day is `unit: :h, step: "1 day"`. With no `step` the spacing is one
1595
+ # `unit` tick (consecutive ticks, as before).
1596
+ # @param start [Time, String, Integer, DateTime] first instant.
1597
+ # @param count [Integer] number of elements.
1598
+ # @param unit [Resolution, Symbol, String] grid resolution (tick).
1599
+ # @param step [Resolution, Symbol, String, nil] spacing between elements
1600
+ # (default: one `unit` tick). Must be a whole multiple of `unit`.
1601
+ # @param format [String, nil] optional strptime format for String inputs.
1602
+ # @return [CATime]
1603
+ # @raise [ArgumentError] when `step` is not a whole multiple of `unit`
1604
+ # (including a calendar `step` on a fixed-length `unit`, e.g. a month
1605
+ # step on an hour grid -- a month is not a fixed number of hours).
1606
+ def self.time_series(start, count:, unit:, step: nil, format: nil)
1607
+ res = CATime::Resolution.parse(unit)
1608
+ stride = step.nil? ? 1 :
1609
+ CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
1610
+ s = _epoch_tick_index(start, res, format)
1611
+ CArray.int64(count) {|i| s + i * stride }.time(unit: res)
1612
+ end
1613
+
1614
+ # @overload time(x, unit: :s, format: nil, on_error: :raise)
1615
+ # Builds a {CATime} on the `unit` grid from time value(s). `x`
1616
+ # is either a single literal (Time / ISO 8601 String / Unix-seconds
1617
+ # Integer / DateTime) — giving a 1-element result — or a CArray of such
1618
+ # literals — giving a same-shape result parsed per cell. Parsing is UTC
1619
+ # and DateTime-independent.
1620
+ #
1621
+ # A value that cannot be parsed raises by default (`on_error: :raise`);
1622
+ # pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
1623
+ # `nil` input cell is a *missing* value (not a parse failure) and always
1624
+ # becomes UNDEF, regardless of `on_error`.
1625
+ # @param x [Time, String, Integer, DateTime, Array, CArray] a literal, a
1626
+ # Ruby Array of literals, or a CArray of literals.
1627
+ # @param unit [Resolution, Symbol, String] target grid resolution.
1628
+ # @param format [String, nil] optional strptime format for String input.
1629
+ # @param on_error [:raise, :mask] parse-failure policy (default `:raise`).
1630
+ # @return [CATime] shape `[1]` for a literal, else `x`'s shape.
1631
+ # @raise [ArgumentError] on an unparseable value when `on_error: :raise`.
1632
+ #
1633
+ # @example
1634
+ # CArray.time("2024-06-15", unit: :D) # 1-element
1635
+ # CArray.time(%w[2024-01-01 2024-02-01], unit: :D) # Ruby Array
1636
+ # CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)
1637
+ def self.time(x, unit: :s, format: nil, on_error: :raise)
1638
+ res = CATime::Resolution.parse(unit)
1639
+ unless %i[raise mask].include?(on_error)
1640
+ raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
1641
+ end
1642
+ x = CA_OBJECT(x) if x.is_a?(Array) # Ruby Array of literals -> object CArray
1643
+ unless x.is_a?(CArray)
1644
+ return _time_cell(x, res, format, on_error)
1645
+ end
1646
+ raw = CArray.int64(*x.shape)
1647
+ x.each_index do |*idx|
1648
+ s = x[*idx]
1649
+ if s == UNDEF || s.nil?
1650
+ raw[*idx] = UNDEF # missing input -> missing output (no phantom epoch)
1651
+ next
1652
+ end
1653
+ begin
1654
+ raw[*idx] = _epoch_tick_index(s, res, format)
1655
+ rescue ArgumentError, TypeError
1656
+ raise if on_error == :raise
1657
+ raw[*idx] = UNDEF # opt-in parse-mask
1658
+ end
1659
+ end
1660
+ raw.time(unit: res)
1661
+ end
1662
+
1663
+ # Single-literal build for {.time}: a 1-element CATime, honouring
1664
+ # the on_error policy (raise, or a masked cell).
1665
+ def self._time_cell(literal, res, format, on_error)
1666
+ raw = CArray.int64(1)
1667
+ begin
1668
+ raw[0] = _epoch_tick_index(literal, res, format)
1669
+ rescue ArgumentError, TypeError
1670
+ raise if on_error == :raise
1671
+ raw[0] = UNDEF
28
1672
  end
1673
+ raw.time(unit: res)
29
1674
  end
30
1675
 
31
- def time_format (template = nil)
32
- if template
33
- return time_strftime(template)
1676
+ # @overload time(unit: :ns, origin: nil)
1677
+ # Returns `self` as a {CATime} on the `unit` grid. With `origin`
1678
+ # nil, `self`'s int64 values are taken as tick indices already anchored
1679
+ # to the Unix epoch (zero-copy Face wrap). With `origin` given, `self`'s
1680
+ # indices are relative to `origin` and are rebased to the epoch (a new
1681
+ # int64 array is built; the origin is not stored).
1682
+ # @param unit [Resolution, Symbol, String] grid resolution.
1683
+ # @param origin [Time, String, Integer, DateTime, nil] base instant that
1684
+ # `self`'s indices are counted from (default: the Unix epoch).
1685
+ # @return [CATime]
1686
+ # @note An `int64` receiver is wrapped zero-copy. A narrower integer type
1687
+ # is widened to `int64` first (a copy); a `Float` / non-integer type
1688
+ # raises (cast it explicitly if the truncation is intended).
1689
+ def time(unit: :ns, origin: nil)
1690
+ res = CATime::Resolution.parse(unit)
1691
+ src = _time_int64_storage
1692
+ if origin.nil?
1693
+ CATime.wrap(src, unit: res)
34
1694
  else
35
- return convert(&:to_s)
36
- end
1695
+ o = CArray._epoch_tick_index(origin, res)
1696
+ (src + o).time(unit: res)
1697
+ end
1698
+ end
1699
+
1700
+ # @overload timedelta(unit: :ns)
1701
+ # Returns `self` re-wrapped as a zero-copy {CATimedelta} view
1702
+ # with the given `unit`.
1703
+ # @param unit [Resolution, Symbol, String] duration resolution.
1704
+ # @return [CATimedelta]
1705
+ # @note An `int64` receiver is wrapped zero-copy; a narrower integer type
1706
+ # is widened to `int64` first (a copy); a `Float` / non-integer raises.
1707
+ def timedelta(unit: :ns)
1708
+ CATimedelta.wrap(_time_int64_storage, unit: unit)
1709
+ end
1710
+
1711
+ # Coerce the receiver to the int64 storage a time / timedelta Face needs:
1712
+ # int64 passes through (zero-copy), a narrower integer type widens losslessly
1713
+ # to int64 (a copy), and any non-integer (Float, boolean, object, ...) raises.
1714
+ def _time_int64_storage
1715
+ return self if data_type == CA_INT64
1716
+ unless integer?
1717
+ raise TypeError,
1718
+ "CATime / CATimedelta storage must be int64; a #{data_type} " \
1719
+ "array cannot be wrapped (cast it explicitly, e.g. " \
1720
+ "arr.int64.time(...), if a lossy conversion is intended)"
1721
+ end
1722
+ int64
1723
+ end
1724
+ private :_time_int64_storage
1725
+ end
1726
+
1727
+ # ============================================================================
1728
+ # timestep system (P1/P2 integer path)
1729
+ #
1730
+ # Projects an absolute time onto an integer "step index" -- the k-th
1731
+ # fixed-width bucket of `step` counted from `origin` -- and the inverse /
1732
+ # rounding / on-grid operators built on it. All storage-domain int64 and
1733
+ # vectorized; the crown jewel is that bucketing, matching, and positional
1734
+ # addressing across differently-scaled series all become integer arithmetic.
1735
+ #
1736
+ # Integer path covers: fixed-length step on fixed-length storage, and Y/M
1737
+ # step on Y/M storage (:M storage already encodes a linear month ordinal, so
1738
+ # it floor-divides identically). The calendar path (Y/M step on sub-day
1739
+ # storage, via civil-date algebra) is P3 and currently raises.
1740
+ # See devel/PROPOSAL_DATETIME64_STEP_SYSTEM.md.
1741
+ # ============================================================================
1742
+
1743
+ class CATime
1744
+ # A tick resolution: `count` ticks of a `base` unit. The human surface is
1745
+ # a String ("3 hours" / "1 month"); a bare Symbol (:h) is the count-1
1746
+ # shorthand over the base-unit vocabulary. It names both a storage tick
1747
+ # (= the grid a CATime is stored on) and a coarser bucket for the
1748
+ # floor / timesteps family. Value object: frozen, value-equal, hashable.
1749
+ class Resolution
1750
+ attr_reader :count, :base
1751
+
1752
+ # Long / abbreviated unit words -> base unit symbol. Calendar words
1753
+ # (ayear etc.) are intentionally dropped (standard calendar only).
1754
+ WORDS = {
1755
+ "year" => :Y, "years" => :Y, "yr" => :Y, "yrs" => :Y,
1756
+ "month" => :M, "months" => :M, "mon" => :M, "mons" => :M,
1757
+ "week" => :W, "weeks" => :W,
1758
+ "day" => :D, "days" => :D,
1759
+ "hour" => :h, "hours" => :h, "hr" => :h, "hrs" => :h,
1760
+ "minute" => :m, "minutes" => :m, "min" => :m, "mins" => :m,
1761
+ "second" => :s, "seconds" => :s, "sec" => :s, "secs" => :s,
1762
+ "millisecond" => :ms, "milliseconds" => :ms, "msec" => :ms, "msecs" => :ms,
1763
+ "microsecond" => :us, "microseconds" => :us, "usec" => :us, "usecs" => :us,
1764
+ "nanosecond" => :ns, "nanoseconds" => :ns,
1765
+ "picosecond" => :ps, "picoseconds" => :ps,
1766
+ "femtosecond" => :fs, "femtoseconds" => :fs,
1767
+ "attosecond" => :as, "attoseconds" => :as,
1768
+ }.freeze
1769
+
1770
+ # Single-letter / short symbols accepted as the count-1 shorthand. These
1771
+ # are the base-unit letters (case-sensitive: :m minute vs :M month).
1772
+ SYMBOLS = %i[Y M W D h m s ms us ns ps fs as].freeze
1773
+
1774
+ # Coerces `spec` into a {Resolution}: a Resolution passes through, a base
1775
+ # unit Symbol becomes count 1, and a String such as `"10 minutes"` is parsed.
1776
+ # @param spec [Resolution, Symbol, String]
1777
+ # @return [Resolution]
1778
+ # @raise [ArgumentError] when `spec` names no known unit.
1779
+ def self.parse(spec)
1780
+ case spec
1781
+ when Resolution then spec
1782
+ when Symbol
1783
+ unless SYMBOLS.include?(spec)
1784
+ raise ArgumentError, "invalid unit #{spec.inspect} " \
1785
+ "(one of #{SYMBOLS.map(&:inspect).join(', ')})"
1786
+ end
1787
+ new(1, spec)
1788
+ when String
1789
+ # Strict grammar: "<unit>" (count 1) or "<n> <unit>" (whitespace
1790
+ # required -- compact "3h" is rejected).
1791
+ m = spec.strip.match(/\A(?:(\d+)\s+)?([A-Za-z]+)\z/)
1792
+ unless m && WORDS.key?(m[2].downcase)
1793
+ raise ArgumentError, "invalid unit spec #{spec.inspect} " \
1794
+ "(use e.g. \"3 hours\" / \"1 month\", or a unit Symbol)"
1795
+ end
1796
+ new(m[1] ? Integer(m[1]) : 1, WORDS[m[2].downcase])
1797
+ else
1798
+ raise ArgumentError, "unit spec must be a String / Symbol / Resolution " \
1799
+ "(got #{spec.class})"
1800
+ end
1801
+ end
1802
+
1803
+ def initialize(count, base)
1804
+ raise ArgumentError, "unit count must be >= 1 (got #{count})" if count < 1
1805
+ # calendar (Y/M) bases are month-ordinal linear, so only integer
1806
+ # multiples are well-defined; a fractional multiple has no meaning.
1807
+ # (count is already Integer here; the guard documents the rule.)
1808
+ @count = count
1809
+ @base = base
1810
+ freeze
1811
+ end
1812
+
1813
+ # seconds- (fixed base) or months- (calendar base) per tick (Rational).
1814
+ def tick_ratio
1815
+ count * CATimeUnitAlgebra.base_ratio(base)
1816
+ end
1817
+
1818
+ # @return [Boolean] whether `other` is the same count and base unit.
1819
+ def ==(other) = other.is_a?(Resolution) && other.count == count && other.base == base
1820
+ alias eql? ==
1821
+ # @return [Integer] a hash consistent with #==.
1822
+ def hash = [count, base].hash
1823
+ # @return [String] e.g. `"h"` for a count of 1, `"10 minutes"` otherwise.
1824
+ def to_s = count == 1 ? base.to_s : "#{count} #{base}"
1825
+ # @return [String]
1826
+ def inspect = "#<CATime::Resolution #{count} #{base}>"
1827
+ end
1828
+
1829
+ # Base units that are day-or-finer (a period head is representable exactly;
1830
+ # the civil path targets these for a calendar bucket).
1831
+ SU_LE_DAY = %i[D h m s ms us ns ps fs as].freeze
1832
+
1833
+ # @overload timesteps(unit: self.unit, origin: nil)
1834
+ # Returns the integer timestep of every element: the k-th `unit`-wide
1835
+ # bucket counted from `origin` (floor toward the past, so pre-`origin`
1836
+ # elements get a negative index -- a normal value, not masked). The
1837
+ # result is an int64 CArray; the input mask propagates. With no `unit`
1838
+ # the bucket is the storage resolution itself, so the result is a copy of
1839
+ # the raw tick indices since the epoch (the same values as
1840
+ # {#ticks}, but a fresh array rather than the live storage).
1841
+ # @param unit [String, Symbol, Resolution] bucket resolution
1842
+ # (default: this array's own storage resolution).
1843
+ # @param origin [Time, String, CATime::Element, DateTime, nil] grid
1844
+ # phase (default: the Unix epoch, or ISO Monday for a week bucket).
1845
+ # @return [CArray] int64 timesteps.
1846
+ # @raise [ArgumentError] on a sub-resolution / unrepresentable (unit,
1847
+ # storage-resolution) pair or a lossy origin.
1848
+ def timesteps(unit: self.unit, origin: nil)
1849
+ g = _step_grid(unit, origin)
1850
+ return _civil(:index, g[1], origin) if g[0] == :civil
1851
+ su, mul, step_ticks, o = g
1852
+ _guard_overflow(su, mul, o)
1853
+ d = _num_ticks(mul) - o
1854
+ q = d / step_ticks
1855
+ q - (d - q * step_ticks).lt(0) # floor-div correction (%-independent)
37
1856
  end
38
1857
 
39
- def time_year
40
- return convert(:int, &:year)
1858
+ # @overload floor(unit:, origin: nil)
1859
+ # Returns each element floored to its bucket head (toward the past), as a
1860
+ # {CATime} in the same storage resolution.
1861
+ # @param unit [String, Symbol, Resolution] bucket resolution.
1862
+ # @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
1863
+ # @return [CATime]
1864
+ def floor(unit:, origin: nil)
1865
+ g = _step_grid(unit, origin)
1866
+ return _civil(:floor, g[1], origin) if g[0] == :civil
1867
+ su, mul, step_ticks, o = g
1868
+ _guard_overflow(su, mul, o, step_ticks, :floor)
1869
+ d = _num_ticks(mul) - o
1870
+ q = d / step_ticks
1871
+ q = q - (d - q * step_ticks).lt(0)
1872
+ _head_time(o + q * step_ticks, mul, su)
41
1873
  end
42
1874
 
43
- def time_month
44
- return convert(:int, &:month)
1875
+ # @overload ceil(unit:, origin: nil)
1876
+ # Returns each element raised to its bucket head at or after it (an
1877
+ # element already on a boundary maps to itself), as a {CATime}.
1878
+ # @return [CATime]
1879
+ def ceil(unit:, origin: nil)
1880
+ g = _step_grid(unit, origin)
1881
+ return _civil(:ceil, g[1], origin) if g[0] == :civil
1882
+ su, mul, step_ticks, o = g
1883
+ _guard_overflow(su, mul, o, step_ticks, :ceil)
1884
+ n = _num_ticks(mul)
1885
+ d = n - o
1886
+ q = d / step_ticks
1887
+ q = q - (d - q * step_ticks).lt(0)
1888
+ fl = o + q * step_ticks
1889
+ _head_time(fl + step_ticks * n.ne(fl), mul, su)
45
1890
  end
46
1891
 
47
- def time_day
48
- return convert(:int, &:day)
1892
+ # @overload round(unit:, origin: nil)
1893
+ # Returns each element rounded to its nearest bucket head (ties toward
1894
+ # the future, matching `snap` :round), as a {CATime}. Exact for
1895
+ # odd `step_ticks` (no half-tick loss). For a calendar bucket the nearest
1896
+ # head is by absolute tick distance (month lengths vary), ties toward the
1897
+ # future.
1898
+ # @return [CATime]
1899
+ def round(unit:, origin: nil)
1900
+ g = _step_grid(unit, origin)
1901
+ return _civil(:round, g[1], origin) if g[0] == :civil
1902
+ su, mul, step_ticks, o = g
1903
+ _guard_overflow(su, mul, o, step_ticks, :round)
1904
+ d = _num_ticks(mul) - o
1905
+ q = d / step_ticks
1906
+ q = q - (d - q * step_ticks).lt(0) # floor bucket
1907
+ r = d - q * step_ticks # 0 <= r < step_ticks
1908
+ q = q + r.ge((step_ticks + 1) / 2) # ties -> future; no 2*d / 2*r
1909
+ _head_time(o + q * step_ticks, mul, su)
49
1910
  end
50
1911
 
51
- def time_hour
52
- return convert(:int, &:hour)
1912
+ # @overload is_righttime(unit:, origin: nil)
1913
+ # Returns a boolean CArray flagging elements that land exactly on a bucket
1914
+ # head. Use as an assertion before matching to catch off-grid series (a
1915
+ # timesteps match is "same bucket", not "same instant").
1916
+ # @param unit [String, Symbol, Resolution] bucket resolution.
1917
+ # @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
1918
+ # @return [CArray] boolean.
1919
+ def is_righttime(unit:, origin: nil)
1920
+ g = _step_grid(unit, origin)
1921
+ return _civil(:on, g[1], origin) if g[0] == :civil
1922
+ su, mul, step_ticks, o = g
1923
+ _guard_overflow(su, mul, o)
1924
+ d = _num_ticks(mul) - o
1925
+ q = d / step_ticks
1926
+ (d - q * step_ticks).eq(0)
53
1927
  end
54
1928
 
55
- def time_minute
56
- return convert(:int, &:min)
1929
+ # @overload from_timesteps(k, unit:, origin: nil)
1930
+ # Inverse of {#timesteps}: returns the bucket-head time for timestep
1931
+ # `k`, stored on the `unit` grid. Use to relabel a `group_by(timesteps)`
1932
+ # result, generate a regular grid, or as a timesteps round-trip oracle.
1933
+ # A scalar `k` returns a {Element}; a CArray `k` returns a {CATime}.
1934
+ # @param k [Integer, CArray] timestep / timesteps.
1935
+ # @param unit [String, Symbol, Resolution] grid resolution of the result.
1936
+ # @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
1937
+ # @return [Element, CATime]
1938
+ def self.from_timesteps(k, unit:, origin: nil)
1939
+ res = Resolution.parse(unit)
1940
+ kk = k.is_a?(CArray) ? k.int64 : CArray.int64(1) { Integer(k) }
1941
+ _mul, step_ticks, o = _resolve_grid(res, res, origin) # step_ticks = 1 (same res)
1942
+ raw = o + kk * step_ticks
1943
+ k.is_a?(CArray) ? raw.time(unit: res) : Element.new(raw[0], res)
57
1944
  end
58
1945
 
59
- def time_second
60
- return convert(:double) {|d| d.second + d.second_fraction }
1946
+ private
1947
+
1948
+ # Resolve the grid for an instance: [storage_res, mul, step_ticks,
1949
+ # origin_ticks] for the integer path, or [:civil, bucket_res] for the
1950
+ # calendar path. Bucket arithmetic runs in the *numerator* grid -- the
1951
+ # finer of the storage tick and the bucket tick -- so `mul` (storage ticks
1952
+ # per numerator tick) is 1 for a bucket at or coarser than the storage
1953
+ # tick, and the widening factor for a finer bucket. `origin_ticks` is
1954
+ # likewise in numerator ticks.
1955
+ def _step_grid(step, origin)
1956
+ storage_res = unit
1957
+ step_res = Resolution.parse(step)
1958
+ scale = self.class.send(:_resolve_step_scale, step_res, storage_res)
1959
+ return [:civil, step_res] if scale == :civil
1960
+ mul, step_ticks = scale
1961
+ num_res = mul == 1 ? storage_res : step_res
1962
+ o = self.class.send(:_resolve_origin_ticks, origin, step_res, num_res)
1963
+ [storage_res, mul, step_ticks, o]
61
1964
  end
62
1965
 
63
- def time_jd
64
- return convert(:int, &:jd)
1966
+ # Storage ticks lifted into the numerator grid (see {#_step_grid}).
1967
+ def _num_ticks(mul)
1968
+ mul == 1 ? parent : CATimeUnitAlgebra.widen(parent, mul)
65
1969
  end
66
1970
 
67
- def time_ajd
68
- return convert(:double, &:ajd)
1971
+ # A bucket head computed in numerator ticks, back as a {CATime} in the
1972
+ # storage resolution. The division is exact: a numerator grid finer than
1973
+ # the storage tick only arises when every element already sits on it, so
1974
+ # each head is a whole number of storage ticks.
1975
+ def _head_time(head, mul, su)
1976
+ (mul == 1 ? head : head / mul).time(unit: su)
69
1977
  end
70
1978
 
71
- def time_is_leap
72
- return test(&:leap?)
1979
+ # Base units where int64 range is small enough that array-domain arithmetic
1980
+ # (parent - o, 2*d) can overflow for a realistic time span; coarser
1981
+ # bases cannot, so the range guard is skipped for them (zero cost on the
1982
+ # common :s / :h / :D / :M path).
1983
+ FINE_UNITS = %i[us ns ps fs as].freeze
1984
+
1985
+ # Raise (rather than silently wrap) if the integer-path arithmetic would
1986
+ # overflow int64. Checks `parent - o`, and -- when `kind` is a bucket op
1987
+ # (:floor / :ceil / :round) -- the reconstructed bucket head `o + q*step`,
1988
+ # which ceil / round can push up to one step beyond the input range. Both
1989
+ # extremes are computed in the Ruby (bignum) domain from the storage
1990
+ # min / max, so the check is O(1) beyond the two reduces.
1991
+ #
1992
+ # Gated to fine units, where the int64 span is genuinely tight, plus every
1993
+ # widened (`mul` > 1) grid, where a coarse storage unit is lifted into a
1994
+ # much finer one; a pathological raw value wrapped in a coarse-unit Face is
1995
+ # not guarded (the gate is a value-range heuristic, not a proof).
1996
+ def _guard_overflow(su, mul, o, step_ticks = nil, kind = nil)
1997
+ return unless mul > 1 or FINE_UNITS.include?(su.base)
1998
+ return if parent.elements == 0
1999
+ raw = parent.has_mask? ? parent.value : parent
2000
+ lo = Integer(raw.min) * mul
2001
+ hi = Integer(raw.max) * mul
2002
+ chk = ->(v, w) { self.class.send(:_chk64, v, w) }
2003
+ chk.(lo, "ticks in bucket resolution")
2004
+ chk.(hi, "ticks in bucket resolution")
2005
+ chk.(lo - o, "parent - origin")
2006
+ chk.(hi - o, "parent - origin")
2007
+ if kind
2008
+ chk.(_bucket_head(lo, o, step_ticks, kind), "bucket head")
2009
+ chk.(_bucket_head(hi, o, step_ticks, kind), "bucket head")
2010
+ end
73
2011
  end
74
2012
 
2013
+ # Ruby-domain (exact bignum) bucket head for a single storage value -- the
2014
+ # scalar mirror of the array floor / ceil / round, used only by the overflow
2015
+ # guard. Ruby Integer `/` and `%` floor, so `(v - o) / s` is the floor
2016
+ # bucket directly.
2017
+ def _bucket_head(v, o, s, kind)
2018
+ fq = (v - o) / s
2019
+ case kind
2020
+ when :floor then o + fq * s
2021
+ when :ceil then h = o + fq * s; v == h ? h : h + s
2022
+ when :round then r = (v - o) - fq * s; o + (fq + (r >= (s + 1) / 2 ? 1 : 0)) * s
2023
+ end
2024
+ end
2025
+
2026
+ # Days since the Unix epoch of the date each element represents (mask
2027
+ # propagates). Calendar units (:Y / :M) resolve to day 1 of their year /
2028
+ # month; a week is 7 days; day-or-finer units floor to the day.
2029
+ def _field_days
2030
+ res = unit
2031
+ case res.base
2032
+ when :Y
2033
+ ones = CArray.int64(*shape) { 1 }
2034
+ self.class.send(:_days_from_civil, parent * res.count + 1970, ones, ones)
2035
+ when :M
2036
+ mo = parent * res.count + 1970 * 12
2037
+ y = self.class.send(:_floordiv_i, mo, 12)
2038
+ m = mo - y * 12 + 1
2039
+ self.class.send(:_days_from_civil, y, m, CArray.int64(*shape) { 1 })
2040
+ when :W then parent * res.count * 7
2041
+ when :D then parent * res.count
2042
+ else # fixed sub-day storage
2043
+ day = CATime::Resolution.new(1, :D)
2044
+ r = CATimeUnitAlgebra.ratio(day, res) # storage ticks per day
2045
+ if r.denominator == 1
2046
+ self.class.send(:_floordiv_i, parent, r.numerator)
2047
+ else # non-day-aligned tick
2048
+ tr = res.tick_ratio # seconds / tick
2049
+ self.class.send(:_floordiv_i, parent * tr.numerator, 86400 * tr.denominator)
2050
+ end
2051
+ end
2052
+ end
2053
+
2054
+ # Clock field `fu` (:h / :m / :s): the field-of-its-parent value (hour of
2055
+ # day, minute of hour, second of minute). Computed as the epoch-relative
2056
+ # `fu` index of each element modulo its cycle, so a storage tick finer OR
2057
+ # coarser than `fu` both resolve exactly (a 10-minute grid gives minute 0 /
2058
+ # 10 / 20 / ...; a day grid collapses hour / minute / second to 0). A
2059
+ # calendar-storage element has no intra-day field, so it collapses to 0.
2060
+ def _clock_field(fu)
2061
+ r = CATimeUnitAlgebra.ratio(unit, CATime::Resolution.new(1, fu)) # fu ticks / storage tick
2062
+ return parent * 0 if r.nil? # calendar storage: no clock field
2063
+ fi = r.denominator == 1 ? parent * r.numerator # epoch-relative fu index
2064
+ : self.class.send(:_floordiv_i, parent * r.numerator, r.denominator)
2065
+ cycle = fu == :h ? 24 : 60 # hour 0..23; min/sec 0..59
2066
+ fi - self.class.send(:_floordiv_i, fi, cycle) * cycle # floor-mod (handles pre-epoch)
2067
+ end
2068
+
2069
+ # Calendar path (Y/M step on day-or-finer storage) via civil-date integer
2070
+ # algebra: floor toward the past by month ordinal, no boundary array / no
2071
+ # min-max, O(N) branch-free, origin-absolute k (negative pre-origin). The
2072
+ # origin's day / time is ignored (month ordinal only, per the design).
2073
+ # `kind`: :index / :floor / :ceil / :round / :on.
2074
+ def _civil(kind, st, origin)
2075
+ su = unit # storage Resolution
2076
+ count = st.count * (st.base == :Y ? 12 : 1) # bucket in months
2077
+ ym0 = self.class.send(:_origin_month_ordinal, origin)
2078
+ days = _field_days # count-folded day index
2079
+ y, m = self.class.send(:_civil_from_days, days)
2080
+ ym = y * 12 + (m - 1)
2081
+ k = self.class.send(:_floordiv_i, ym - ym0, count)
2082
+ return k if kind == :index
2083
+ head = self.class.send(:_civil_head_ticks, k, st, su, origin)
2084
+ return head.time(unit: su) if kind == :floor
2085
+ return parent.eq(head) if kind == :on
2086
+ head_next = self.class.send(:_civil_head_ticks, k + 1, st, su, origin)
2087
+ sel = # boolean 0/1: pick head_next?
2088
+ case kind
2089
+ when :ceil then parent.ne(head) # on-boundary keeps head
2090
+ when :round then (head_next - parent).le(parent - head) # nearest, ties -> future
2091
+ end
2092
+ (head + (head_next - head) * sel).time(unit: su)
2093
+ end
2094
+
2095
+ class << self
2096
+ private
2097
+
2098
+ INT64_MIN = -(2**63)
2099
+ INT64_MAX = 2**63 - 1
2100
+
2101
+ # Raise (rather than let an int64 CArray operand silently wrap) if a
2102
+ # Ruby-domain quantity is out of int64 range. Returns the value on success
2103
+ # so it composes inline.
2104
+ def _chk64(v, what)
2105
+ if v < INT64_MIN || v > INT64_MAX
2106
+ raise RangeError,
2107
+ "time step: #{what} = #{v} overflows int64 " \
2108
+ "(the storage unit is too fine for this step / origin / span; " \
2109
+ "use a coarser unit)"
2110
+ end
2111
+ v
2112
+ end
2113
+
2114
+ # Return [step_ticks, origin_ticks] (both Integer, in storage ticks) for
2115
+ # the integer path. The calendar path is handled by the caller.
2116
+ def _resolve_grid(step_res, storage_res, origin)
2117
+ mul, step_ticks = _resolve_step_scale(step_res, storage_res)
2118
+ [mul, step_ticks, _resolve_origin_ticks(origin, step_res,
2119
+ mul == 1 ? storage_res : step_res)]
2120
+ end
2121
+
2122
+ # Storage ticks per day of `storage_res` (must be a whole number, else the
2123
+ # storage grid does not tile a day -- a calendar bucket is unrepresentable).
2124
+ def _ticks_per_day(storage_res)
2125
+ r = CATimeUnitAlgebra.ratio(Resolution.new(1, :D), storage_res)
2126
+ unless r.denominator == 1
2127
+ raise ArgumentError,
2128
+ "cannot place a calendar bucket on storage resolution " \
2129
+ "#{storage_res} (its tick does not tile a day)"
2130
+ end
2131
+ _chk64(r.numerator, "ticks per day for #{storage_res}")
2132
+ end
2133
+
2134
+ # Storage ticks of the bucket head at timestep `k` (int64 CArray) for a
2135
+ # calendar bucket: ym0 + k*count months -> day 1 of that month -> ticks.
2136
+ def _civil_head_ticks(k, st, storage_res, origin)
2137
+ tpd = _ticks_per_day(storage_res)
2138
+ count = st.count * (st.base == :Y ? 12 : 1)
2139
+ ym0 = _origin_month_ordinal(origin)
2140
+ ymk = ym0 + k * count
2141
+ yy = _floordiv_i(ymk, 12)
2142
+ mm = ymk - yy * 12 + 1
2143
+ _days_from_civil(yy, mm, CArray.int64(*k.shape) { 1 }) * tpd
2144
+ end
2145
+
2146
+ # Month ordinal (year*12 + month-1) of origin; day / time ignored.
2147
+ # Default (nil) is the epoch month 1970-01.
2148
+ def _origin_month_ordinal(origin)
2149
+ return 1970 * 12 if origin.nil?
2150
+ y, m = _origin_year_month(origin)
2151
+ y * 12 + (m - 1)
2152
+ end
2153
+
2154
+ # Floored integer division of an int64 CArray by a positive Integer
2155
+ # (toward -inf; CArray `/` truncates toward zero).
2156
+ def _floordiv_i(a, b)
2157
+ q = a / b
2158
+ q - (a - q * b).lt(0)
2159
+ end
2160
+
2161
+ # Vectorized Howard Hinnant civil-date algebra (proleptic Gregorian,
2162
+ # negative days supported). days since the Unix epoch -> [year, month,
2163
+ # day] int64 CArrays. Truncating division is what the algorithm assumes;
2164
+ # the only negative operand (the 400-year era) is pre-adjusted so
2165
+ # truncation behaves like floor.
2166
+ def _civil_from_days(z)
2167
+ z = z + 719468
2168
+ era = (z - 146096 * z.lt(0)) / 146097
2169
+ doe = z - era * 146097
2170
+ yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
2171
+ y = yoe + era * 400
2172
+ doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
2173
+ mp = (5 * doy + 2) / 153
2174
+ d = doy - (153 * mp + 2) / 5 + 1 # day of month, 1..31
2175
+ m = mp + (3 - 12 * mp.ge(10)) # mp<10 ? +3 : -9
2176
+ [y + m.le(2), m, d]
2177
+ end
2178
+
2179
+ # [year, month, day] int64 CArrays -> days since the Unix epoch.
2180
+ def _days_from_civil(y, m, d)
2181
+ y = y - m.le(2)
2182
+ era = (y - 399 * y.lt(0)) / 400
2183
+ yoe = y - era * 400
2184
+ doy = (153 * (m + (9 - 12 * m.gt(2))) + 2) / 5 + (d - 1) # m>2 ? -3 : +9
2185
+ doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
2186
+ era * 146097 + doe - 719468
2187
+ end
2188
+
2189
+ # [mul, step_ticks] for the integer path, or :civil (calendar bucket on
2190
+ # day-or-finer storage), or raise. Bucket arithmetic runs in the finer of
2191
+ # the two grids, so exactly one of the pair is > 1:
2192
+ # - bucket at or coarser than the storage tick (a :h bucket on :s
2193
+ # storage) -> [1, N]: N storage ticks per bucket.
2194
+ # - bucket finer than the storage tick (a :h bucket on :D storage) ->
2195
+ # [N, 1]: one storage tick spans N buckets exactly, so a timestep is a
2196
+ # plain widening. This needs the storage tick to be a *whole* multiple
2197
+ # of the bucket tick; a partial multiple ("90 minutes" storage against
2198
+ # an :h bucket) has no integer timestep and raises.
2199
+ def _resolve_step_scale(step_res, storage_res)
2200
+ r = CATimeUnitAlgebra.ratio(step_res, storage_res) # storage ticks / step tick
2201
+ if r
2202
+ what = "bucket #{step_res} in ticks of #{storage_res}"
2203
+ return [1, _chk64(r.numerator, what)] if r.denominator == 1
2204
+ return [_chk64(r.denominator, what), 1] if r.numerator == 1
2205
+ end
2206
+ if CATimeUnitAlgebra::CALENDAR.key?(step_res.base) && SU_LE_DAY.include?(storage_res.base)
2207
+ return :civil
2208
+ end
2209
+ raise ArgumentError,
2210
+ "cannot express bucket #{step_res} on storage resolution #{storage_res} " \
2211
+ "(neither is a whole multiple of the other)"
2212
+ end
2213
+
2214
+ # origin -> integer tick count in storage ticks. Default (nil) is the
2215
+ # epoch, except a week bucket defaults to ISO Monday (1970-01-05). A lossy
2216
+ # conversion (origin not landing exactly on the storage grid) raises; a
2217
+ # bare Integer is rejected (epoch-dependent, ambiguous).
2218
+ def _resolve_origin_ticks(origin, step_res, storage_res)
2219
+ if origin.nil?
2220
+ if step_res.base == :W && SU_LE_DAY.include?(storage_res.base)
2221
+ return _chk64(_ticks_per_day(storage_res) * 4,
2222
+ "ISO-Monday week origin in ticks of #{storage_res}")
2223
+ end
2224
+ return 0
2225
+ end
2226
+ if CATimeUnitAlgebra::FIXED.key?(storage_res.base)
2227
+ secs = _origin_seconds_exact(origin)
2228
+ tick = secs / storage_res.tick_ratio # Rational
2229
+ unless tick.denominator == 1
2230
+ raise ArgumentError,
2231
+ "origin #{origin.inspect} is not representable losslessly " \
2232
+ "in storage resolution #{storage_res} (would truncate the grid phase)"
2233
+ end
2234
+ _chk64(tick.numerator, "origin #{origin.inspect} in ticks of #{storage_res}")
2235
+ else # :Y / :M storage
2236
+ y, m = _origin_year_month(origin) # day / time ignored
2237
+ ord = storage_res.base == :Y ? (y - 1970) : (y * 12 + (m - 1) - 1970 * 12)
2238
+ if storage_res.count > 1
2239
+ unless (ord % storage_res.count).zero?
2240
+ raise ArgumentError,
2241
+ "origin #{origin.inspect} is not on the #{storage_res} grid"
2242
+ end
2243
+ ord /= storage_res.count
2244
+ end
2245
+ ord
2246
+ end
2247
+ end
2248
+
2249
+ # Exact Rational seconds since the Unix epoch for a fixed-storage origin.
2250
+ # Time / String / DateTime go through the DateTime-independent parser.
2251
+ def _origin_seconds_exact(origin)
2252
+ case origin
2253
+ when CATime::Element
2254
+ unless CATimeUnitAlgebra::FIXED.key?(origin.unit.base)
2255
+ raise ArgumentError,
2256
+ "origin scalar in calendar unit #{origin.unit} has no exact " \
2257
+ "seconds; use a fixed-unit scalar / Time / String"
2258
+ end
2259
+ Rational(origin.value) * origin.unit.tick_ratio
2260
+ when Integer
2261
+ raise ArgumentError,
2262
+ "origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
2263
+ "Time / String / CATime scalar"
2264
+ else
2265
+ CArray._epoch_seconds_exact(origin) # Time / String / DateTime
2266
+ end
2267
+ end
2268
+
2269
+ # [year, month] of a calendar-storage origin (finer fields ignored).
2270
+ def _origin_year_month(origin)
2271
+ case origin
2272
+ when CATime::Element
2273
+ case origin.unit.base
2274
+ when :M then mo = origin.value * origin.unit.count + 1970 * 12; [mo / 12, mo % 12 + 1]
2275
+ when :Y then [origin.value * origin.unit.count + 1970, 1]
2276
+ else t = origin.to_time.utc; [t.year, t.month]
2277
+ end
2278
+ when Integer
2279
+ raise ArgumentError,
2280
+ "origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
2281
+ "Time / String / CATime scalar"
2282
+ else
2283
+ CArray._epoch_year_month(origin) # Time / String / DateTime
2284
+ end
2285
+ end
2286
+ end
75
2287
  end
76
2288
 
2289
+ # The view-creating method lift hook lives in the C layer (= subclass-
2290
+ # agnostic): each view-creating C method (rb_ca_transpose / rb_ca_reshape
2291
+ # / etc.) ends with `if (ca_is_face(ca)) obj = ca_face_lift(obj, self);`,
2292
+ # so a view of a Face re-lifts to the same Face class. See the relevant C
2293
+ # source (ext/ca_obj_transpose.c etc.) for details.