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
@@ -0,0 +1,1257 @@
1
+ /* ---------------------------------------------------------------------------
2
+
3
+ C-native median / percentile / quantile. Written as a C extension
4
+ (not a transliteration of the Ruby script): the readability goal is
5
+ that the compute logic reads as C, not as Ruby executed from C.
6
+
7
+ Three lanes, dispatched on data_type:
8
+
9
+ - numeric (CA_INT8..CA_FLOAT64): full C compute. The kth selection /
10
+ per-fiber sort already produces a fresh *contiguous* entity
11
+ (rb_ca_partition_copy_c, sort_copy), so all post-selection extraction
12
+ and arithmetic is a plain loop-interchange (OUTER/M/INNER) over
13
+ native-typed buffers -> double, written into an rb_ca_new_reduced
14
+ CA_FLOAT64 output. Inner loops are instantiated per native type
15
+ (X-macro, dispatched once) so the per-element read is a typed pointer
16
+ load, not a switch; the even-median upper-region min is fmin-based so
17
+ it vectorises. No Ruby-surface funcall on this lane.
18
+
19
+ - object (CA_OBJECT): arithmetic on arbitrary Ruby objects is
20
+ irreducibly rb_funcall, so this lane drives the same CArray-level
21
+ operations (partition_copy / slice / min / max / `+` / `/` / `*`) as
22
+ per-array funcalls. Matches production exactly: flat -> bare scalar,
23
+ axis -> reduced CArray, mask+axis -> partition_copy raises.
24
+
25
+ - fixlen: rejected (no numeric midpoint; ordering-pick methods alone
26
+ are a partial surface, so fixlen median/percentile is not offered).
27
+
28
+ Each function below is tagged with the lane it belongs to:
29
+
30
+ [numeric] - numeric lane (CA_INT8..CA_FLOAT64); pure C double math,
31
+ no Ruby-surface funcall.
32
+ [object] - CA_OBJECT lane; CArray-level / stored-object funcall.
33
+ [shared] - used by both lanes (or by the entry dispatch).
34
+ [entry] - Ruby method entry: validates args, dispatches by data_type.
35
+
36
+ NOTE the two `pct_compute*` siblings: pct_compute is [numeric] (double
37
+ in/out); pct_compute_object is [object] (funcall) and is reached ONLY by
38
+ CA_OBJECT percentile/quantile -- never by numeric, never by median.
39
+
40
+ ---------------------------------------------------------------------------- */
41
+
42
+ #include "ruby.h"
43
+ #include "carray.h"
44
+ #include <math.h>
45
+
46
+ /* Externals (link-time; kept out of carray.h to keep the header lean). */
47
+ extern VALUE rb_ca_partition_copy_c (VALUE self, VALUE vkth, VALUE vaxis); /* carray_partition.c */
48
+ extern VALUE rb_ca_insert_axis (int argc, VALUE *argv, VALUE self); /* ca_obj_refer.c */
49
+
50
+ static ID id_axis, id_sort_copy, id_is_not_masked;
51
+ /* object lane (arbitrary Ruby objects -> arithmetic is irreducibly funcall) */
52
+ static ID id_plus, id_div, id_mul, id_min, id_max, id_sort, id_copy;
53
+ static ID id_aref, id_aset;
54
+ static VALUE sym_linear, sym_lower, sym_higher, sym_nearest, sym_midpoint;
55
+
56
+ /* [shared] data_type lane selector: true for CA_INT8..CA_FLOAT64. */
57
+ static inline int
58
+ mp_is_numeric (int8_t dt)
59
+ {
60
+ return ( dt >= CA_INT8 && dt <= CA_FLOAT64 );
61
+ }
62
+
63
+ /* [numeric] X-macro over the numeric data_types. All hot inner loops
64
+ are instantiated per native type (dispatched ONCE on data_type) so
65
+ the per-element load is a typed pointer read, not a switch: a
66
+ generic per-element dispatch defeats vectorisation. */
67
+ #define MP_TYPES(_) \
68
+ _(CA_INT8, int8_t) _(CA_UINT8, uint8_t) \
69
+ _(CA_INT16, int16_t) _(CA_UINT16, uint16_t) \
70
+ _(CA_INT32, int32_t) _(CA_UINT32, uint32_t) \
71
+ _(CA_INT64, int64_t) _(CA_UINT64, uint64_t) \
72
+ _(CA_FLOAT32, float32_t) _(CA_FLOAT64, double)
73
+
74
+ /* [numeric] min over the upper region [k+1, n-1] of a partitioned fiber,
75
+ skipping NaN (= matches production .min(axis:): NaN sorts to the tail
76
+ and is ignored). stride = element stride along the reduce axis.
77
+ fmin ignores NaN (fmin(x,NaN)==x), so a +INFINITY seed naturally skips
78
+ NaN and stays branch-free -> the compiler vectorises this loop, matching
79
+ the production SIMD min(axis:). An all-NaN (or empty) upper region
80
+ leaves hi == +INFINITY; fall back to the kth value (degenerate). */
81
+ #define MP_GEN_MINUP(CT, T) \
82
+ static double \
83
+ mp_minup_##T (const T *b, ca_size_t f0, long k, long n, long stride) { \
84
+ double hi = (double) INFINITY; \
85
+ for ( long ai = k + 1; ai < n; ai++ ) { \
86
+ double v = (double) b[f0 + (ca_size_t)(ai * stride)]; \
87
+ hi = fmin(hi, v); \
88
+ } \
89
+ return ( hi == (double) INFINITY ) \
90
+ ? (double) b[f0 + (ca_size_t)(k * stride)] : hi; \
91
+ }
92
+ MP_TYPES(MP_GEN_MINUP)
93
+
94
+ /* ---- geometry helper ------------------------------------------------- */
95
+
96
+ /* [numeric] split self.shape around `axis` into the contiguous-entity
97
+ walk dims: OUTER (axes before) x M (= dim[axis]) x INNER (axes after). */
98
+ static void
99
+ mp_geometry (CArray *ca, long axis, long *OUTER, long *M, long *INNER)
100
+ {
101
+ long inner = 1, outer = 1;
102
+ for ( int d = (int)axis + 1; d < ca->ndim; d++ ) inner *= ca->dim[d];
103
+ for ( int d = 0; d < (int)axis; d++ ) outer *= ca->dim[d];
104
+ *INNER = inner;
105
+ *OUTER = outer;
106
+ *M = ca->dim[axis];
107
+ }
108
+
109
+ /* [numeric] keep_axis: [1,1,...,1] CA_FLOAT64 entity (original ndim)
110
+ holding a scalar (or a single masked cell when is_undef). */
111
+ static VALUE
112
+ mp_keep_axis_full_f64 (VALUE self, double v, int is_undef)
113
+ {
114
+ CArray *ca;
115
+ GetCArray(self, ca);
116
+ int nd = ca->ndim;
117
+ ca_size_t *dim = ALLOCA_N(ca_size_t, nd);
118
+ for ( int i = 0; i < nd; i++ ) dim[i] = 1;
119
+ VALUE out = rb_carray_new(CA_FLOAT64, (int8_t) nd, dim, 0, NULL);
120
+ CArray *co;
121
+ GetCArray(out, co);
122
+ if ( is_undef ) {
123
+ ca_create_mask(co);
124
+ ((boolean8_t *) co->mask->ptr)[0] = 1;
125
+ } else {
126
+ ((double *) co->ptr)[0] = v;
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /* [shared] all-UNDEF reduced CA_FLOAT64 output for a zero-length reduction
132
+ axis. An order statistic of no elements has no value, so every reduced
133
+ cell is masked -- matching mean / min (which return UNDEF cells), not a
134
+ raise. keep_axis controls whether the reduced axis is dropped or kept
135
+ as a length-1 axis. */
136
+ static VALUE
137
+ mp_axis_all_masked (VALUE self, long axis, int keep_axis)
138
+ {
139
+ int8_t ax = (int8_t) axis;
140
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_FLOAT64, keep_axis);
141
+ CArray *co;
142
+ GetCArray(out, co);
143
+ ca_create_mask(co);
144
+ boolean8_t *m = (boolean8_t *) co->mask->ptr;
145
+ for ( ca_size_t i = 0; i < co->elements; i++ ) m[i] = 1;
146
+ return out;
147
+ }
148
+
149
+ /* ---- 5-method percentile picker -------------------------------------- */
150
+
151
+ typedef enum { PCT_LOWER_ONLY, PCT_UPPER_ONLY, PCT_BOTH } pct_need_t;
152
+
153
+ /* [shared] which of lower (sorted[k]) / upper (sorted[k+1]) the method
154
+ needs. Lets the caller skip the fetch it won't use. Used by both
155
+ lanes (the need classification is dtype-independent). */
156
+ static pct_need_t
157
+ pct_need (VALUE method, long k, double r, long n)
158
+ {
159
+ if ( method == sym_lower ) return PCT_LOWER_ONLY;
160
+ if ( method == sym_higher ) return (r == 0.0) ? PCT_LOWER_ONLY : PCT_UPPER_ONLY;
161
+ if ( method == sym_nearest ) {
162
+ int use_k = (r < 0.5) || (r == 0.5 && (k % 2 == 0));
163
+ return use_k ? PCT_LOWER_ONLY : PCT_UPPER_ONLY;
164
+ }
165
+ if ( method == sym_linear ) {
166
+ if ( r == 0.0 || k + 1 >= n ) return PCT_LOWER_ONLY;
167
+ return PCT_BOTH;
168
+ }
169
+ if ( method == sym_midpoint ) {
170
+ if ( k + 1 >= n ) return PCT_LOWER_ONLY;
171
+ return PCT_BOTH;
172
+ }
173
+ rb_raise(rb_eArgError, "percentile: invalid method (BUG)");
174
+ }
175
+
176
+ /* [numeric] 5-method picker, double in/out. (The [object] twin is
177
+ pct_compute_object, far below in the object lane.) */
178
+ static double
179
+ pct_compute (VALUE method, long k, double r, long n, double lo, double hi)
180
+ {
181
+ if ( method == sym_lower ) return lo;
182
+ if ( method == sym_higher ) return (r == 0.0) ? lo : hi;
183
+ if ( method == sym_nearest ) {
184
+ int use_k = (r < 0.5) || (r == 0.5 && (k % 2 == 0));
185
+ return use_k ? lo : hi;
186
+ }
187
+ if ( method == sym_linear ) {
188
+ if ( r == 0.0 || k + 1 >= n ) return lo;
189
+ return lo * (1.0 - r) + hi * r;
190
+ }
191
+ if ( method == sym_midpoint ) {
192
+ if ( k + 1 >= n ) return lo;
193
+ return (lo + hi) / 2.0;
194
+ }
195
+ rb_raise(rb_eArgError, "percentile: invalid method (BUG)");
196
+ }
197
+
198
+ /* [shared] reject any method symbol outside the supported 5. */
199
+ static void
200
+ pct_validate_method (VALUE method)
201
+ {
202
+ if ( method != sym_linear && method != sym_lower && method != sym_higher
203
+ && method != sym_nearest && method != sym_midpoint ) {
204
+ rb_raise(rb_eArgError,
205
+ "percentile: method %"PRIsVALUE" not supported "
206
+ "(use :linear / :lower / :higher / :nearest / :midpoint)",
207
+ rb_inspect(method));
208
+ }
209
+ }
210
+
211
+ /* =====================================================================
212
+ median -- [numeric] lane (CA_OBJECT median lives in the object lane)
213
+ ===================================================================== */
214
+
215
+ /* [numeric] fill a reduced CA_FLOAT64 output from a partitioned entity
216
+ (typed; one instantiation per native type). */
217
+ #define MP_GEN_MEDFILL(CT, T) \
218
+ static void \
219
+ mp_medfill_##T (double *op, const T *b, long OUTER, long M, \
220
+ long INNER, long n, long k, int odd) { \
221
+ for ( long o = 0; o < OUTER; o++ ) { \
222
+ for ( long in = 0; in < INNER; in++ ) { \
223
+ ca_size_t f0 = (ca_size_t)((o * M) * INNER + in); \
224
+ double lo = (double) b[f0 + (ca_size_t)(k * INNER)]; \
225
+ op[o * INNER + in] = odd ? lo \
226
+ : (lo + mp_minup_##T(b, f0, k, n, INNER)) / 2.0; \
227
+ } \
228
+ } \
229
+ }
230
+ MP_TYPES(MP_GEN_MEDFILL)
231
+
232
+ /* [numeric] dispatch median_fill once on data_type to the typed body. */
233
+ static void
234
+ median_fill (double *op, int8_t dt, const char *base,
235
+ long OUTER, long M, long INNER, long n, long k, int odd)
236
+ {
237
+ switch ( dt ) {
238
+ #define MP_CASE(CT, T) \
239
+ case CT: mp_medfill_##T(op, (const T *) base, OUTER, M, INNER, n, k, odd); break;
240
+ MP_TYPES(MP_CASE)
241
+ #undef MP_CASE
242
+ default:
243
+ rb_raise(rb_eCADataTypeError, "median: unsupported data_type %d", dt);
244
+ }
245
+ }
246
+
247
+ /* [numeric] per-axis median -> reduced CA_FLOAT64 CArray. */
248
+ static VALUE
249
+ median_axis (VALUE self, long axis, long n, int keep_axis)
250
+ {
251
+ CArray *ca;
252
+ GetCArray(self, ca);
253
+ int8_t dt = ca->data_type;
254
+ long k = (n % 2 == 0) ? (n / 2 - 1) : ((n - 1) / 2);
255
+
256
+ VALUE pp = rb_ca_partition_copy_c(self, LONG2NUM(k), LONG2NUM(axis));
257
+ CArray *cp;
258
+ GetCArray(pp, cp);
259
+
260
+ int8_t ax = (int8_t) axis;
261
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_FLOAT64, keep_axis);
262
+ CArray *co;
263
+ GetCArray(out, co);
264
+
265
+ long OUTER, M, INNER;
266
+ mp_geometry(ca, axis, &OUTER, &M, &INNER);
267
+ median_fill((double *) co->ptr, dt, (const char *) cp->ptr,
268
+ OUTER, M, INNER, n, k, (n % 2 != 0));
269
+ RB_GC_GUARD(pp);
270
+ return out;
271
+ }
272
+
273
+ /* [numeric] flat median scalar (double) of a 1-D numeric entity/view. */
274
+ static double
275
+ median_scalar_1d (VALUE src1d, long n)
276
+ {
277
+ CArray *ca;
278
+ GetCArray(src1d, ca);
279
+ long k = (n % 2 == 0) ? (n / 2 - 1) : ((n - 1) / 2);
280
+ VALUE pp = rb_ca_partition_copy_c(src1d, LONG2NUM(k), INT2FIX(0));
281
+ CArray *cp;
282
+ GetCArray(pp, cp);
283
+ double res;
284
+ median_fill(&res, ca->data_type, (const char *) cp->ptr,
285
+ 1, n, 1, n, k, (n % 2 != 0)); /* OUTER=1, M=n, INNER=1 */
286
+ RB_GC_GUARD(pp);
287
+ return res;
288
+ }
289
+
290
+ /* [numeric] flat median: strip mask / honour min_count, flatten, then
291
+ reduce on axis 0 to a bare Float (or keep_axis [1..1] entity). */
292
+ static VALUE
293
+ median_flat (VALUE self, long min_count, VALUE fill_value, int keep_axis)
294
+ {
295
+ VALUE src = self;
296
+ int filled = 0; /* masked-out or empty -> use fill_value / UNDEF */
297
+
298
+ if ( RTEST(rb_ca_has_mask(self)) ) {
299
+ CArray *mc; GetCArray(self, mc);
300
+ long cnt = (long) ca_count_not_masked(mc); /* = elements - count_masked */
301
+ if ( cnt < min_count ) filled = 1;
302
+ else src = rb_ca_fetch(self, ID2SYM(id_is_not_masked));
303
+ }
304
+ if ( !filled ) {
305
+ CArray *sc;
306
+ GetCArray(src, sc);
307
+ if ( sc->elements == 0 ) filled = 1;
308
+ }
309
+
310
+ if ( filled ) {
311
+ int is_undef = NIL_P(fill_value);
312
+ double fillv = is_undef ? 0.0 : NUM2DBL(fill_value);
313
+ if ( keep_axis ) return mp_keep_axis_full_f64(self, fillv, is_undef);
314
+ return is_undef ? CA_UNDEF : fill_value;
315
+ }
316
+
317
+ VALUE flat = rb_ca_flatten(src);
318
+ CArray *fc;
319
+ GetCArray(flat, fc);
320
+ double scalar = median_scalar_1d(flat, (long) fc->elements);
321
+ RB_GC_GUARD(flat);
322
+ if ( keep_axis ) return mp_keep_axis_full_f64(self, scalar, 0);
323
+ return rb_float_new(scalar);
324
+ }
325
+
326
+ /* =====================================================================
327
+ object lane (CA_OBJECT)
328
+
329
+ Arithmetic on arbitrary Ruby objects is irreducibly rb_funcall, so
330
+ this lane drives the *same* CArray-level operations the numeric lane
331
+ expresses inline in C: partition_copy / slice / min / max / `+` / `/`
332
+ / `*`. Combines are per-array funcalls (not per-scalar loops). This
333
+ matches production behaviour exactly: flat -> bare scalar, axis ->
334
+ reduced CArray, mask+axis -> partition_copy raises.
335
+
336
+ Every function in this block (down to the median entry) is [object].
337
+ ===================================================================== */
338
+
339
+ /* [object] recv.mid(axis: raxis) -- e.g. min/max/sort along an axis. */
340
+ static VALUE
341
+ obj_call_axis (VALUE recv, ID mid, VALUE raxis)
342
+ {
343
+ VALUE kw = rb_hash_new();
344
+ rb_hash_aset(kw, ID2SYM(id_axis), raxis);
345
+ VALUE argv[1] = { kw };
346
+ return rb_funcallv_kw(recv, mid, 1, argv, RB_PASS_KEYWORDS);
347
+ }
348
+
349
+ /* [object] arr[*spec] with spec[axis]=idx, rest nil (= drop one axis). */
350
+ static VALUE
351
+ obj_slice (VALUE arr, long axis, long idx)
352
+ {
353
+ CArray *ca;
354
+ GetCArray(arr, ca);
355
+ int ndim = ca->ndim;
356
+ VALUE *spec = ALLOCA_N(VALUE, ndim);
357
+ for ( int i = 0; i < ndim; i++ ) spec[i] = Qnil;
358
+ spec[axis] = LONG2NUM(idx);
359
+ return rb_funcallv(arr, id_aref, ndim, spec);
360
+ }
361
+
362
+ /* [object] min over pp[*spec] with spec[axis] = (k+1)..(n-1) along axis. */
363
+ static VALUE
364
+ obj_min_upper (VALUE pp, long axis, long k, long n)
365
+ {
366
+ CArray *ca;
367
+ GetCArray(pp, ca);
368
+ int ndim = ca->ndim;
369
+ VALUE *spec = ALLOCA_N(VALUE, ndim);
370
+ for ( int i = 0; i < ndim; i++ ) spec[i] = Qnil;
371
+ spec[axis] = rb_range_new(LONG2NUM(k + 1), LONG2NUM(n - 1), 0);
372
+ VALUE sliced = rb_funcallv(pp, id_aref, ndim, spec);
373
+ return obj_call_axis(sliced, id_min, LONG2NUM(axis));
374
+ }
375
+
376
+ /* [object] sorted[k] along axis via partition_copy. */
377
+ static VALUE
378
+ obj_kth_one (VALUE self, long axis, long k)
379
+ {
380
+ VALUE pp = rb_ca_partition_copy_c(self, LONG2NUM(k), LONG2NUM(axis));
381
+ return obj_slice(pp, axis, k);
382
+ }
383
+
384
+ /* [object] sorted[k] and sorted[k+1] from a single partition_copy(k). */
385
+ static void
386
+ obj_kth_pair (VALUE self, long axis, long k, long n, VALUE *lo, VALUE *hi)
387
+ {
388
+ VALUE pp = rb_ca_partition_copy_c(self, LONG2NUM(k), LONG2NUM(axis));
389
+ *lo = obj_slice(pp, axis, k);
390
+ *hi = obj_min_upper(pp, axis, k, n);
391
+ }
392
+
393
+ /* [object] keep_axis flat: [1,...,1] entity holding val (UNDEF assigns
394
+ masked). Uses []= so UNDEF / arbitrary objects store correctly. */
395
+ static VALUE
396
+ obj_keep_axis_full (VALUE self, VALUE val)
397
+ {
398
+ CArray *ca;
399
+ GetCArray(self, ca);
400
+ int ndim = ca->ndim;
401
+ ca_size_t *dim = ALLOCA_N(ca_size_t, ndim);
402
+ for ( int i = 0; i < ndim; i++ ) dim[i] = 1;
403
+ int8_t ot = (ca->data_type == CA_OBJECT) ? CA_OBJECT : CA_FLOAT64;
404
+ VALUE out = rb_carray_new(ot, (int8_t) ndim, dim, 0, NULL);
405
+ VALUE *av = ALLOCA_N(VALUE, ndim + 1);
406
+ for ( int i = 0; i < ndim; i++ ) av[i] = INT2FIX(0);
407
+ av[ndim] = val;
408
+ rb_funcallv(out, id_aset, ndim + 1, av);
409
+ return out;
410
+ }
411
+
412
+ /* [object] per-axis median (= reduced CArray; 1-D self -> bare scalar).
413
+ even-n combine and the *1.0 float-promote are stored-object funcalls. */
414
+ static VALUE
415
+ median_object_axis (VALUE self, long axis, long n, int keep_axis)
416
+ {
417
+ VALUE result;
418
+ if ( n % 2 == 0 ) {
419
+ long k = n / 2 - 1;
420
+ VALUE lo, hi;
421
+ obj_kth_pair(self, axis, k, n, &lo, &hi);
422
+ result = rb_funcall(rb_funcall(lo, id_plus, 1, hi), id_div, 1, DBL2NUM(2.0));
423
+ } else {
424
+ VALUE kv = obj_kth_one(self, axis, (n - 1) / 2);
425
+ result = rb_funcall(kv, id_mul, 1, DBL2NUM(1.0)); /* force float promote */
426
+ }
427
+ if ( keep_axis ) {
428
+ VALUE ia[1] = { LONG2NUM(axis) };
429
+ result = rb_ca_insert_axis(1, ia, result);
430
+ }
431
+ return result;
432
+ }
433
+
434
+ /* [object] flat median: mask-strip / min_count, flatten, axis-0 reduce. */
435
+ static VALUE
436
+ median_object_flat (VALUE self, long min_count, VALUE fill_value, int keep_axis)
437
+ {
438
+ VALUE src = self, result;
439
+ int filled = 0;
440
+ if ( RTEST(rb_ca_has_mask(self)) ) {
441
+ CArray *mc; GetCArray(self, mc);
442
+ long cnt = (long) ca_count_not_masked(mc); /* = elements - count_masked */
443
+ if ( cnt < min_count ) filled = 1;
444
+ else src = rb_ca_fetch(self, ID2SYM(id_is_not_masked));
445
+ }
446
+ if ( !filled ) {
447
+ CArray *sc;
448
+ GetCArray(src, sc);
449
+ if ( sc->elements == 0 ) filled = 1;
450
+ }
451
+ if ( filled ) {
452
+ result = NIL_P(fill_value) ? CA_UNDEF : fill_value;
453
+ } else {
454
+ VALUE flat = rb_ca_flatten(src);
455
+ CArray *fc;
456
+ GetCArray(flat, fc);
457
+ result = median_object_axis(flat, 0, (long) fc->elements, 0);
458
+ RB_GC_GUARD(flat);
459
+ }
460
+ return keep_axis ? obj_keep_axis_full(self, result) : result;
461
+ }
462
+
463
+ /* [object] 5-method picker on stored objects (funcall arithmetic; lo/hi
464
+ pre-fetched). Reached ONLY by CA_OBJECT percentile/quantile -- never
465
+ by numeric (that path uses pct_compute) and never by median.
466
+ The :linear / :midpoint interpolation funcalls are irreducible for
467
+ arbitrary objects; the *1.0 in the pick methods is float-promotion to
468
+ match the numeric "always Float" output contract. */
469
+ static VALUE
470
+ pct_compute_object (VALUE method, long k, double r, long n, VALUE lo, VALUE hi)
471
+ {
472
+ if ( method == sym_lower ) return rb_funcall(lo, id_mul, 1, DBL2NUM(1.0));
473
+ if ( method == sym_higher ) {
474
+ VALUE v = (r == 0.0) ? lo : hi;
475
+ return rb_funcall(v, id_mul, 1, DBL2NUM(1.0));
476
+ }
477
+ if ( method == sym_nearest ) {
478
+ int use_k = (r < 0.5) || (r == 0.5 && (k % 2 == 0));
479
+ return rb_funcall(use_k ? lo : hi, id_mul, 1, DBL2NUM(1.0));
480
+ }
481
+ if ( method == sym_linear ) {
482
+ if ( r == 0.0 || k + 1 >= n ) return rb_funcall(lo, id_mul, 1, DBL2NUM(1.0));
483
+ VALUE a = rb_funcall(lo, id_mul, 1, DBL2NUM(1.0 - r));
484
+ VALUE b = rb_funcall(hi, id_mul, 1, DBL2NUM(r));
485
+ return rb_funcall(a, id_plus, 1, b);
486
+ }
487
+ if ( method == sym_midpoint ) {
488
+ if ( k + 1 >= n ) return rb_funcall(lo, id_mul, 1, DBL2NUM(1.0));
489
+ return rb_funcall(rb_funcall(lo, id_plus, 1, hi), id_div, 1, DBL2NUM(2.0));
490
+ }
491
+ rb_raise(rb_eArgError, "percentile: invalid method (BUG)");
492
+ }
493
+
494
+ /* [object] single p via one partition_copy. */
495
+ static VALUE
496
+ pct_object_one_partition (VALUE self, long axis, long n, double p, VALUE method)
497
+ {
498
+ if ( p == 100.0 )
499
+ return rb_funcall(obj_call_axis(self, id_max, LONG2NUM(axis)),
500
+ id_mul, 1, DBL2NUM(1.0));
501
+ if ( n == 1 )
502
+ return rb_funcall(obj_slice(self, axis, 0), id_mul, 1, DBL2NUM(1.0));
503
+ double f = (n - 1) * p / 100.0;
504
+ long k = (long) floor(f);
505
+ double r = f - k;
506
+ pct_need_t need = pct_need(method, k, r, n);
507
+ VALUE lo = Qnil, hi = Qnil;
508
+ if ( need == PCT_LOWER_ONLY ) lo = obj_kth_one(self, axis, k);
509
+ else obj_kth_pair(self, axis, k, n, &lo, &hi);
510
+ return pct_compute_object(method, k, r, n, lo, hi);
511
+ }
512
+
513
+ /* [object] single p from a fully sorted entity (multi-p shared sort). */
514
+ static VALUE
515
+ pct_object_one_sorted (VALUE sorted, long axis, long n, double p, VALUE method)
516
+ {
517
+ if ( p == 100.0 )
518
+ return rb_funcall(obj_slice(sorted, axis, n - 1), id_mul, 1, DBL2NUM(1.0));
519
+ if ( n == 1 )
520
+ return rb_funcall(obj_slice(sorted, axis, 0), id_mul, 1, DBL2NUM(1.0));
521
+ double f = (n - 1) * p / 100.0;
522
+ long k = (long) floor(f);
523
+ double r = f - k;
524
+ pct_need_t need = pct_need(method, k, r, n);
525
+ long kup = (k + 1 < n) ? (k + 1) : (n - 1);
526
+ VALUE lo = Qnil, hi = Qnil;
527
+ if ( need != PCT_UPPER_ONLY ) lo = obj_slice(sorted, axis, k);
528
+ if ( need != PCT_LOWER_ONLY ) hi = obj_slice(sorted, axis, kup);
529
+ return pct_compute_object(method, k, r, n, lo, hi);
530
+ }
531
+
532
+ /* [object] per-axis percentile -> array of (reduced CArray | scalar),
533
+ one entry per requested p (keep_axis wrapping done by the entry). */
534
+ static VALUE
535
+ percentile_object_axis (VALUE self, VALUE pers, long axis, VALUE method)
536
+ {
537
+ CArray *ca;
538
+ GetCArray(self, ca);
539
+ long n = (long) ca->dim[axis];
540
+ long npers = RARRAY_LEN(pers);
541
+
542
+ if ( n == 0 ) {
543
+ /* zero-length axis: all-UNDEF reduced cell per p. keep_axis wrapping
544
+ is applied by the entry (rb_ca_percentile_m), so build reduced. */
545
+ VALUE res = rb_ary_new_capa(npers);
546
+ for ( long i = 0; i < npers; i++ )
547
+ rb_ary_push(res, mp_axis_all_masked(self, axis, 0));
548
+ return res;
549
+ }
550
+
551
+ if ( npers == 1 ) {
552
+ double p = NUM2DBL(rb_ary_entry(pers, 0));
553
+ return rb_ary_new_from_args(1,
554
+ pct_object_one_partition(self, axis, n, p, method));
555
+ }
556
+ /* multi-p: one shared sort (CA_OBJECT sort trampolines to the focused
557
+ Ruby per-slab helper), materialised once, then C-driven slicing. */
558
+ VALUE sorted = rb_funcall(obj_call_axis(self, id_sort, LONG2NUM(axis)), id_copy, 0);
559
+ VALUE result = rb_ary_new_capa(npers);
560
+ for ( long i = 0; i < npers; i++ ) {
561
+ double p = NUM2DBL(rb_ary_entry(pers, i));
562
+ rb_ary_push(result, pct_object_one_sorted(sorted, axis, n, p, method));
563
+ }
564
+ RB_GC_GUARD(sorted);
565
+ return result;
566
+ }
567
+
568
+ /* [object] masked per-axis percentile, one p. `sorted` is a materialised
569
+ sort(axis:) entity (present objects front, UNDEF tail per fiber); the
570
+ stored VALUEs are read straight from its buffer, so the per-fiber select
571
+ is C control flow driving only the interpolation funcalls. n_present ==
572
+ 0 (or < min_count) yields an UNDEF cell (fill_value when given); the mask
573
+ is created lazily, matching the numeric lane. */
574
+ static VALUE
575
+ pct_object_axis_masked_one (VALUE self, VALUE sorted, long axis, double p,
576
+ VALUE method, long OUTER, long M, long INNER,
577
+ const boolean8_t *sm, long min_count,
578
+ VALUE fill_value, int keep_axis)
579
+ {
580
+ int8_t ax = (int8_t) axis;
581
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_OBJECT, keep_axis);
582
+ CArray *co;
583
+ GetCArray(out, co);
584
+ CArray *cs;
585
+ GetCArray(sorted, cs);
586
+ VALUE *sb = (VALUE *) cs->ptr;
587
+ VALUE *op = (VALUE *) co->ptr;
588
+ int is_undef = NIL_P(fill_value);
589
+
590
+ for ( long o = 0; o < OUTER; o++ ) {
591
+ for ( long in = 0; in < INNER; in++ ) {
592
+ ca_size_t f0 = (ca_size_t)((o * M) * INNER + in);
593
+ long oc = o * INNER + in;
594
+ long np = M;
595
+ if ( sm ) {
596
+ np = 0;
597
+ for ( long j = 0; j < M; j++ )
598
+ if ( !sm[f0 + (ca_size_t)(j * INNER)] ) np++;
599
+ }
600
+ if ( np == 0 || np < min_count ) {
601
+ if ( is_undef ) {
602
+ if ( !co->mask ) ca_create_mask(co);
603
+ ((boolean8_t *) co->mask->ptr)[oc] = 1;
604
+ } else {
605
+ op[oc] = fill_value;
606
+ }
607
+ continue;
608
+ }
609
+ long k; double r;
610
+ if ( p == 100.0 ) { k = np - 1; r = 0.0; }
611
+ else if ( np == 1 ) { k = 0; r = 0.0; }
612
+ else { double f = (np - 1) * p / 100.0; k = (long) floor(f); r = f - k; }
613
+ pct_need_t need = pct_need(method, k, r, np);
614
+ long kup = (k + 1 < np) ? (k + 1) : (np - 1);
615
+ VALUE lo = Qnil, hi = Qnil;
616
+ if ( need != PCT_UPPER_ONLY ) lo = sb[f0 + (ca_size_t)(k * INNER)];
617
+ if ( need != PCT_LOWER_ONLY ) hi = sb[f0 + (ca_size_t)(kup * INNER)];
618
+ op[oc] = pct_compute_object(method, k, r, np, lo, hi);
619
+ }
620
+ }
621
+ return out;
622
+ }
623
+
624
+ /* [object] masked per-axis percentile: one sort(axis:) supplies the sorted
625
+ material, then each p is a per-fiber select. Returns an Array of reduced
626
+ CA_OBJECT CArrays, one per p. */
627
+ static VALUE
628
+ percentile_object_axis_masked (VALUE self, VALUE pers, long axis, VALUE method,
629
+ long min_count, VALUE fill_value, int keep_axis)
630
+ {
631
+ CArray *ca;
632
+ GetCArray(self, ca);
633
+ long npers = RARRAY_LEN(pers);
634
+ long M = (long) ca->dim[axis];
635
+
636
+ if ( M == 0 ) {
637
+ VALUE res = rb_ary_new_capa(npers);
638
+ for ( long i = 0; i < npers; i++ )
639
+ rb_ary_push(res, mp_axis_all_masked(self, axis, keep_axis));
640
+ return res;
641
+ }
642
+
643
+ VALUE sorted = rb_funcall(obj_call_axis(self, id_sort, LONG2NUM(axis)), id_copy, 0);
644
+ CArray *cs;
645
+ GetCArray(sorted, cs);
646
+ const boolean8_t *sm = cs->mask ? (const boolean8_t *) cs->mask->ptr : NULL;
647
+
648
+ long OUTER, MM, INNER;
649
+ mp_geometry(ca, axis, &OUTER, &MM, &INNER);
650
+
651
+ VALUE result = rb_ary_new_capa(npers);
652
+ for ( long i = 0; i < npers; i++ ) {
653
+ double p = NUM2DBL(rb_ary_entry(pers, i));
654
+ rb_ary_push(result,
655
+ pct_object_axis_masked_one(self, sorted, axis, p, method,
656
+ OUTER, MM, INNER, sm, min_count,
657
+ fill_value, keep_axis));
658
+ }
659
+ RB_GC_GUARD(sorted);
660
+ return result;
661
+ }
662
+
663
+ /* [object] per-axis median with mask / min_count / fill_value (= object
664
+ percentile(50, :linear); see the numeric median_axis_masked note). */
665
+ static VALUE
666
+ median_object_axis_masked (VALUE self, long axis, long min_count,
667
+ VALUE fill_value, int keep_axis)
668
+ {
669
+ VALUE pers = rb_ary_new_from_args(1, DBL2NUM(50.0));
670
+ VALUE res = percentile_object_axis_masked(self, pers, axis, sym_linear,
671
+ min_count, fill_value, keep_axis);
672
+ return rb_ary_entry(res, 0);
673
+ }
674
+
675
+ /* [object] flat percentile: mask-strip / min_count, flatten, axis-0. */
676
+ static VALUE
677
+ percentile_object_flat (VALUE self, VALUE pers, long min_count, VALUE fill_value,
678
+ VALUE method)
679
+ {
680
+ long npers = RARRAY_LEN(pers);
681
+ VALUE src = self;
682
+ int filled = 0;
683
+ if ( RTEST(rb_ca_has_mask(self)) ) {
684
+ CArray *mc; GetCArray(self, mc);
685
+ long cnt = (long) ca_count_not_masked(mc); /* = elements - count_masked */
686
+ if ( cnt < min_count ) filled = 1;
687
+ else src = rb_ca_fetch(self, ID2SYM(id_is_not_masked));
688
+ }
689
+ if ( !filled ) {
690
+ CArray *sc;
691
+ GetCArray(src, sc);
692
+ if ( sc->elements == 0 ) filled = 1;
693
+ }
694
+ if ( filled ) {
695
+ VALUE fill = NIL_P(fill_value) ? CA_UNDEF : fill_value;
696
+ VALUE result = rb_ary_new_capa(npers);
697
+ for ( long i = 0; i < npers; i++ ) rb_ary_push(result, fill);
698
+ return result;
699
+ }
700
+ VALUE flat = rb_ca_flatten(src);
701
+ VALUE r = percentile_object_axis(flat, pers, 0, method);
702
+ RB_GC_GUARD(flat);
703
+ return r;
704
+ }
705
+
706
+ /* numeric masked per-axis driver (defined in the percentile lane below). */
707
+ static VALUE median_axis_masked (VALUE self, long axis, long min_count,
708
+ VALUE fill_value, int keep_axis);
709
+
710
+ /* [entry] median(axis:, min_count:, fill_value:, keep_axis:). Dispatches
711
+ fixlen -> reject, object -> object lane, numeric -> numeric lane. */
712
+ static VALUE
713
+ rb_ca_median_m (int argc, VALUE *argv, VALUE self)
714
+ {
715
+ VALUE ropt = rb_pop_options(&argc, &argv);
716
+ VALUE raxis = Qnil, rmin_count = INT2FIX(0), fill_value = Qnil, rkeep = Qfalse;
717
+ rb_scan_options(ropt, "axis,min_count,fill_value,keep_axis",
718
+ &raxis, &rmin_count, &fill_value, &rkeep);
719
+ if ( argc != 0 )
720
+ rb_raise(rb_eArgError, "median: no positional args accepted (got %d)", argc);
721
+
722
+ CArray *ca;
723
+ GetCArray(self, ca);
724
+ /* Boolean rides the f64 lane: 0/1 -> 0.0/1.0, so median interpolates and
725
+ returns a float (a 2-element median averages to 0.5), matching how the
726
+ numeric lane treats integer input. */
727
+ if ( ca->data_type == CA_BOOLEAN ) {
728
+ self = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));
729
+ GetCArray(self, ca);
730
+ }
731
+ int8_t dt = ca->data_type;
732
+ if ( ca_is_fixlen_type(ca) )
733
+ rb_raise(rb_eCADataTypeError,
734
+ "median: not defined for fixlen (no numeric midpoint); "
735
+ "use a numeric or object array");
736
+ int is_obj = (dt == CA_OBJECT);
737
+ if ( !is_obj && !mp_is_numeric(dt) )
738
+ rb_raise(rb_eCADataTypeError, "median: unsupported data_type %d", dt);
739
+
740
+ long min_count = NUM2LONG(rmin_count);
741
+ if ( min_count < 0 )
742
+ rb_raise(rb_eArgError, "min_count must be non-negative; got %ld", min_count);
743
+ int keep_axis = RTEST(rkeep);
744
+
745
+ if ( !NIL_P(raxis) ) {
746
+ long axis = rb_ca_normalize_axis_value(self, raxis, "median");
747
+ long n = (long) ca->dim[axis];
748
+ if ( n == 0 ) return mp_axis_all_masked(self, axis, keep_axis);
749
+ /* mask / min_count / fill_value require the per-fiber select (each fiber
750
+ has its own n_present); the plain partition path assumes a uniform n. */
751
+ if ( RTEST(rb_ca_has_mask(self)) || min_count > 0 || !NIL_P(fill_value) )
752
+ return is_obj
753
+ ? median_object_axis_masked(self, axis, min_count, fill_value, keep_axis)
754
+ : median_axis_masked(self, axis, min_count, fill_value, keep_axis);
755
+ return is_obj ? median_object_axis(self, axis, n, keep_axis)
756
+ : median_axis(self, axis, n, keep_axis);
757
+ }
758
+ return is_obj ? median_object_flat(self, min_count, fill_value, keep_axis)
759
+ : median_flat(self, min_count, fill_value, keep_axis);
760
+ }
761
+
762
+ /* =====================================================================
763
+ percentile / quantile -- [numeric] lane below; the [object] percentile
764
+ functions live up in the object lane. The entries (rb_ca_percentile_m
765
+ / rb_ca_quantile_m) at the bottom dispatch between them.
766
+ ===================================================================== */
767
+
768
+ /* [numeric] fill a reduced CA_FLOAT64 output for one p, from either a
769
+ partitioned entity (from_sorted == 0; upper via mp_minup) or a fully
770
+ sorted entity (from_sorted == 1; upper = base[kup]). */
771
+ #define MP_GEN_PCTFILL(CT, T) \
772
+ static void \
773
+ mp_pctfill_##T (double *op, const T *b, int from_sorted, \
774
+ long OUTER, long M, long INNER, long n, \
775
+ long k, double r, VALUE method, pct_need_t need) { \
776
+ long kup = (k + 1 < n) ? (k + 1) : (n - 1); \
777
+ for ( long o = 0; o < OUTER; o++ ) { \
778
+ for ( long in = 0; in < INNER; in++ ) { \
779
+ ca_size_t f0 = (ca_size_t)((o * M) * INNER + in); \
780
+ double lo = 0.0, hi = 0.0; \
781
+ if ( need != PCT_UPPER_ONLY ) \
782
+ lo = (double) b[f0 + (ca_size_t)(k * INNER)]; \
783
+ if ( need != PCT_LOWER_ONLY ) \
784
+ hi = from_sorted ? (double) b[f0 + (ca_size_t)(kup * INNER)] \
785
+ : mp_minup_##T(b, f0, k, n, INNER); \
786
+ if ( need == PCT_UPPER_ONLY ) lo = hi; \
787
+ op[o * INNER + in] = pct_compute(method, k, r, n, lo, hi); \
788
+ } \
789
+ } \
790
+ }
791
+ MP_TYPES(MP_GEN_PCTFILL)
792
+
793
+ /* [numeric] dispatch pct_fill once on data_type to the typed body. */
794
+ static void
795
+ pct_fill (double *op, int8_t dt, const char *base, int from_sorted,
796
+ long OUTER, long M, long INNER, long n,
797
+ long k, double r, VALUE method, pct_need_t need)
798
+ {
799
+ switch ( dt ) {
800
+ #define MP_CASE(CT, T) \
801
+ case CT: mp_pctfill_##T(op, (const T *) base, from_sorted, \
802
+ OUTER, M, INNER, n, k, r, method, need); break;
803
+ MP_TYPES(MP_CASE)
804
+ #undef MP_CASE
805
+ default:
806
+ rb_raise(rb_eCADataTypeError, "percentile: unsupported data_type %d", dt);
807
+ }
808
+ }
809
+
810
+ /* [numeric] masked per-axis percentile fill. `b` / `sm` are the sorted-copy
811
+ entity's data and mask (present values front, UNDEF tail per fiber -- the
812
+ invariant sort_copy(axis:) guarantees for masked input), so n_present per
813
+ fiber = the count of non-masked cells (its leading run). A fiber with
814
+ n_present == 0 (or < min_count) yields an UNDEF cell, or fill_value when
815
+ given. The output mask is created lazily on the first UNDEF cell so a run
816
+ with no empty fiber keeps has_mask == false, matching mean(axis:). */
817
+ #define MP_GEN_PCTFILL_MASKED(CT, T) \
818
+ static void \
819
+ mp_pctfill_masked_##T (CArray *co, const T *b, const boolean8_t *sm, \
820
+ long OUTER, long M, long INNER, double p, \
821
+ VALUE method, long min_count, \
822
+ int is_undef, double fillv) { \
823
+ double *op = (double *) co->ptr; \
824
+ for ( long o = 0; o < OUTER; o++ ) { \
825
+ for ( long in = 0; in < INNER; in++ ) { \
826
+ ca_size_t f0 = (ca_size_t)((o * M) * INNER + in); \
827
+ long oc = o * INNER + in; \
828
+ long np = M; \
829
+ if ( sm ) { \
830
+ np = 0; \
831
+ for ( long j = 0; j < M; j++ ) \
832
+ if ( !sm[f0 + (ca_size_t)(j * INNER)] ) np++; \
833
+ } \
834
+ if ( np == 0 || np < min_count ) { \
835
+ if ( is_undef ) { \
836
+ if ( !co->mask ) ca_create_mask(co); \
837
+ ((boolean8_t *) co->mask->ptr)[oc] = 1; \
838
+ op[oc] = 0.0; \
839
+ } else { \
840
+ op[oc] = fillv; \
841
+ } \
842
+ continue; \
843
+ } \
844
+ long k; double r; \
845
+ if ( p == 100.0 ) { k = np - 1; r = 0.0; } \
846
+ else if ( np == 1 ) { k = 0; r = 0.0; } \
847
+ else { double f = (np - 1) * p / 100.0; k = (long) floor(f); r = f - k; } \
848
+ pct_need_t need = pct_need(method, k, r, np); \
849
+ long kup = (k + 1 < np) ? (k + 1) : (np - 1); \
850
+ double lo = 0.0, hi = 0.0; \
851
+ if ( need != PCT_UPPER_ONLY ) \
852
+ lo = (double) b[f0 + (ca_size_t)(k * INNER)]; \
853
+ if ( need != PCT_LOWER_ONLY ) \
854
+ hi = (double) b[f0 + (ca_size_t)(kup * INNER)]; \
855
+ if ( need == PCT_UPPER_ONLY ) lo = hi; \
856
+ op[oc] = pct_compute(method, k, r, np, lo, hi); \
857
+ } \
858
+ } \
859
+ }
860
+ MP_TYPES(MP_GEN_PCTFILL_MASKED)
861
+
862
+ /* [numeric] dispatch the masked fill once on data_type to the typed body. */
863
+ static void
864
+ pct_fill_masked (CArray *co, int8_t dt, const char *base, const boolean8_t *sm,
865
+ long OUTER, long M, long INNER, double p, VALUE method,
866
+ long min_count, int is_undef, double fillv)
867
+ {
868
+ switch ( dt ) {
869
+ #define MP_CASE(CT, T) \
870
+ case CT: mp_pctfill_masked_##T(co, (const T *) base, sm, OUTER, M, INNER, \
871
+ p, method, min_count, is_undef, fillv); break;
872
+ MP_TYPES(MP_CASE)
873
+ #undef MP_CASE
874
+ default:
875
+ rb_raise(rb_eCADataTypeError, "percentile: unsupported data_type %d", dt);
876
+ }
877
+ }
878
+
879
+ /* [numeric] single p via one partition_copy -> reduced CA_FLOAT64. */
880
+ static VALUE
881
+ pct_axis_one_partition (VALUE self, long axis, long n, double p,
882
+ VALUE method, int keep_axis)
883
+ {
884
+ CArray *ca;
885
+ GetCArray(self, ca);
886
+ int8_t dt = ca->data_type;
887
+
888
+ long k;
889
+ double r;
890
+ if ( p == 100.0 ) { k = n - 1; r = 0.0; }
891
+ else if ( n == 1 ) { k = 0; r = 0.0; }
892
+ else { double f = (n - 1) * p / 100.0; k = (long) floor(f); r = f - k; }
893
+
894
+ pct_need_t need = pct_need(method, k, r, n);
895
+ /* For higher/nearest that pick the upper element, we still partition at
896
+ k and read min-upper; but UPPER_ONLY means we need element k+1. To
897
+ keep one partition correct, partition at the index we actually read:
898
+ if UPPER_ONLY we need sorted[k+1] -> partition at k, min-upper gives
899
+ it. pct_fill handles that via from_sorted==0. */
900
+ VALUE pp = rb_ca_partition_copy_c(self, LONG2NUM(k), LONG2NUM(axis));
901
+ CArray *cp;
902
+ GetCArray(pp, cp);
903
+
904
+ int8_t ax = (int8_t) axis;
905
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_FLOAT64, keep_axis);
906
+ CArray *co;
907
+ GetCArray(out, co);
908
+ long OUTER, M, INNER;
909
+ mp_geometry(ca, axis, &OUTER, &M, &INNER);
910
+ pct_fill((double *) co->ptr, dt, (const char *) cp->ptr, 0,
911
+ OUTER, M, INNER, n, k, r, method, need);
912
+ RB_GC_GUARD(pp);
913
+ return out;
914
+ }
915
+
916
+ /* [numeric] multi p: one shared sort_copy + per-p C slice. (The single
917
+ sort_copy funcall is the only Ruby-surface call left on this lane; it
918
+ could be replaced by a C-callable sort_copy twin like partition_copy_c.) */
919
+ static VALUE
920
+ pct_axis_multi (VALUE self, VALUE pers_ary, long axis, long n,
921
+ VALUE method, int keep_axis)
922
+ {
923
+ CArray *ca;
924
+ GetCArray(self, ca);
925
+ int8_t dt = ca->data_type;
926
+ long npers = RARRAY_LEN(pers_ary);
927
+
928
+ /* one funcall to trigger the C per-fiber sort; result is a fresh
929
+ contiguous entity. Picking is full-C below. */
930
+ VALUE kw = rb_hash_new();
931
+ rb_hash_aset(kw, ID2SYM(id_axis), LONG2NUM(axis));
932
+ VALUE sc_argv[1] = { kw };
933
+ VALUE sorted = rb_funcallv_kw(self, id_sort_copy, 1, sc_argv, RB_PASS_KEYWORDS);
934
+ CArray *cs;
935
+ GetCArray(sorted, cs);
936
+
937
+ long OUTER, M, INNER;
938
+ mp_geometry(ca, axis, &OUTER, &M, &INNER);
939
+
940
+ VALUE result = rb_ary_new_capa(npers);
941
+ for ( long i = 0; i < npers; i++ ) {
942
+ double p = NUM2DBL(rb_ary_entry(pers_ary, i));
943
+ long k;
944
+ double r;
945
+ if ( p == 100.0 ) { k = n - 1; r = 0.0; }
946
+ else if ( n == 1 ) { k = 0; r = 0.0; }
947
+ else { double f = (n - 1) * p / 100.0; k = (long) floor(f); r = f - k; }
948
+ pct_need_t need = pct_need(method, k, r, n);
949
+
950
+ int8_t ax = (int8_t) axis;
951
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_FLOAT64, keep_axis);
952
+ CArray *co;
953
+ GetCArray(out, co);
954
+ pct_fill((double *) co->ptr, dt, (const char *) cs->ptr, 1,
955
+ OUTER, M, INNER, n, k, r, method, need);
956
+ rb_ary_push(result, out);
957
+ }
958
+ RB_GC_GUARD(sorted);
959
+ return result;
960
+ }
961
+
962
+ /* [numeric] per-axis percentile: 1 p -> partition, many p -> shared sort. */
963
+ static VALUE
964
+ pct_axis (VALUE self, VALUE pers_ary, long axis, VALUE method, int keep_axis)
965
+ {
966
+ CArray *ca;
967
+ GetCArray(self, ca);
968
+ long n = (long) ca->dim[axis];
969
+ long npers = RARRAY_LEN(pers_ary);
970
+
971
+ if ( n == 0 ) {
972
+ /* zero-length axis: every requested p reduces to an all-UNDEF cell. */
973
+ VALUE res = rb_ary_new_capa(npers);
974
+ for ( long i = 0; i < npers; i++ )
975
+ rb_ary_push(res, mp_axis_all_masked(self, axis, keep_axis));
976
+ return res;
977
+ }
978
+
979
+ if ( npers == 1 ) {
980
+ double p = NUM2DBL(rb_ary_entry(pers_ary, 0));
981
+ return rb_ary_new_from_args(1,
982
+ pct_axis_one_partition(self, axis, n, p, method, keep_axis));
983
+ }
984
+ return pct_axis_multi(self, pers_ary, axis, n, method, keep_axis);
985
+ }
986
+
987
+ /* [numeric] per-axis percentile with mask / min_count / fill_value: a single
988
+ masked sort_copy (present front, UNDEF tail per fiber) supplies the sorted
989
+ material, then each p is a per-fiber select honouring that fiber's own
990
+ n_present. Returns an Array of reduced CA_FLOAT64 CArrays, one per p. */
991
+ static VALUE
992
+ pct_axis_masked (VALUE self, VALUE pers_ary, long axis, VALUE method,
993
+ long min_count, VALUE fill_value, int keep_axis)
994
+ {
995
+ CArray *ca;
996
+ GetCArray(self, ca);
997
+ int8_t dt = ca->data_type;
998
+ long npers = RARRAY_LEN(pers_ary);
999
+ long M = (long) ca->dim[axis];
1000
+
1001
+ if ( M == 0 ) {
1002
+ VALUE res = rb_ary_new_capa(npers);
1003
+ for ( long i = 0; i < npers; i++ )
1004
+ rb_ary_push(res, mp_axis_all_masked(self, axis, keep_axis));
1005
+ return res;
1006
+ }
1007
+
1008
+ /* one masked sort_copy: present values front, UNDEF tail, per fiber. */
1009
+ VALUE kw = rb_hash_new();
1010
+ rb_hash_aset(kw, ID2SYM(id_axis), LONG2NUM(axis));
1011
+ VALUE sc_argv[1] = { kw };
1012
+ VALUE sorted = rb_funcallv_kw(self, id_sort_copy, 1, sc_argv, RB_PASS_KEYWORDS);
1013
+ CArray *cs;
1014
+ GetCArray(sorted, cs);
1015
+ const boolean8_t *sm = cs->mask ? (const boolean8_t *) cs->mask->ptr : NULL;
1016
+
1017
+ long OUTER, MM, INNER;
1018
+ mp_geometry(ca, axis, &OUTER, &MM, &INNER);
1019
+ int is_undef = NIL_P(fill_value);
1020
+ double fillv = is_undef ? 0.0 : NUM2DBL(fill_value);
1021
+
1022
+ VALUE result = rb_ary_new_capa(npers);
1023
+ for ( long i = 0; i < npers; i++ ) {
1024
+ double p = NUM2DBL(rb_ary_entry(pers_ary, i));
1025
+ int8_t ax = (int8_t) axis;
1026
+ VALUE out = rb_ca_new_reduced(self, &ax, 1, CA_FLOAT64, keep_axis);
1027
+ CArray *co;
1028
+ GetCArray(out, co);
1029
+ pct_fill_masked(co, dt, (const char *) cs->ptr, sm,
1030
+ OUTER, MM, INNER, p, method, min_count, is_undef, fillv);
1031
+ rb_ary_push(result, out);
1032
+ }
1033
+ RB_GC_GUARD(sorted);
1034
+ return result;
1035
+ }
1036
+
1037
+ /* [numeric] per-axis median with mask / min_count / fill_value. Median is
1038
+ percentile(50, :linear) (odd n -> middle, even n -> mean of the two middle
1039
+ values), so it rides the same per-fiber select. */
1040
+ static VALUE
1041
+ median_axis_masked (VALUE self, long axis, long min_count, VALUE fill_value,
1042
+ int keep_axis)
1043
+ {
1044
+ VALUE pers = rb_ary_new_from_args(1, DBL2NUM(50.0));
1045
+ VALUE res = pct_axis_masked(self, pers, axis, sym_linear,
1046
+ min_count, fill_value, keep_axis);
1047
+ return rb_ary_entry(res, 0);
1048
+ }
1049
+
1050
+ /* [numeric] flat percentile -> array of scalars (or keep_axis [1..1]
1051
+ entities), one per requested p. */
1052
+ static VALUE
1053
+ pct_flat (VALUE self, VALUE pers_ary, long min_count, VALUE fill_value,
1054
+ VALUE method, int keep_axis)
1055
+ {
1056
+ long npers = RARRAY_LEN(pers_ary);
1057
+ VALUE src = self;
1058
+ int masked_out = 0, is_undef = 0;
1059
+ double fillv = 0.0;
1060
+
1061
+ if ( RTEST(rb_ca_has_mask(self)) ) {
1062
+ CArray *mc; GetCArray(self, mc);
1063
+ long cnt = (long) ca_count_not_masked(mc); /* = elements - count_masked */
1064
+ if ( cnt < min_count ) {
1065
+ masked_out = 1; is_undef = NIL_P(fill_value);
1066
+ if ( !is_undef ) fillv = NUM2DBL(fill_value);
1067
+ } else {
1068
+ src = rb_ca_fetch(self, ID2SYM(id_is_not_masked));
1069
+ }
1070
+ }
1071
+ if ( !masked_out ) {
1072
+ CArray *sc;
1073
+ GetCArray(src, sc);
1074
+ if ( sc->elements == 0 ) {
1075
+ masked_out = 1; is_undef = NIL_P(fill_value);
1076
+ if ( !is_undef ) fillv = NUM2DBL(fill_value);
1077
+ }
1078
+ }
1079
+
1080
+ VALUE result = rb_ary_new_capa(npers);
1081
+
1082
+ if ( masked_out ) {
1083
+ for ( long i = 0; i < npers; i++ ) {
1084
+ VALUE v = keep_axis ? mp_keep_axis_full_f64(self, fillv, is_undef)
1085
+ : (is_undef ? CA_UNDEF
1086
+ : (NIL_P(fill_value) ? CA_UNDEF : fill_value));
1087
+ rb_ary_push(result, v);
1088
+ }
1089
+ return result;
1090
+ }
1091
+
1092
+ /* flatten -> 1-D, reduce on axis 0, extract op[0] per p. */
1093
+ VALUE flat = rb_ca_flatten(src);
1094
+ VALUE per_axis = pct_axis(flat, pers_ary, 0, method, 0); /* array of [1] CArrays */
1095
+ for ( long i = 0; i < npers; i++ ) {
1096
+ CArray *r1;
1097
+ GetCArray(rb_ary_entry(per_axis, i), r1);
1098
+ double v = ((double *) r1->ptr)[0];
1099
+ VALUE out = keep_axis ? mp_keep_axis_full_f64(self, v, 0) : rb_float_new(v);
1100
+ rb_ary_push(result, out);
1101
+ }
1102
+ RB_GC_GUARD(flat);
1103
+ return result;
1104
+ }
1105
+
1106
+ /* [shared] normalise + validate the p list: unwrap a single Array/CArray
1107
+ arg, reject empty, require each p be Numeric in [0,100]. */
1108
+ static VALUE
1109
+ pct_flatten_validate_pers (VALUE pers)
1110
+ {
1111
+ if ( RARRAY_LEN(pers) == 1 ) {
1112
+ VALUE first = rb_ary_entry(pers, 0);
1113
+ if ( RB_TYPE_P(first, T_ARRAY) ) pers = first;
1114
+ else if ( rb_obj_is_kind_of(first, rb_cCArray) )
1115
+ pers = rb_funcall(first, rb_intern("to_a"), 0);
1116
+ }
1117
+ long len = RARRAY_LEN(pers);
1118
+ if ( len == 0 )
1119
+ rb_raise(rb_eArgError, "percentile: at least one p value required");
1120
+ for ( long i = 0; i < len; i++ ) {
1121
+ VALUE p = rb_ary_entry(pers, i);
1122
+ if ( !rb_obj_is_kind_of(p, rb_cNumeric) || NUM2DBL(p) < 0.0 || NUM2DBL(p) > 100.0 )
1123
+ rb_raise(rb_eArgError,
1124
+ "percentile: p must be Numeric in [0,100] (got %"PRIsVALUE")",
1125
+ rb_inspect(p));
1126
+ }
1127
+ return pers;
1128
+ }
1129
+
1130
+ /* [entry] percentile(*pers, axis:, min_count:, fill_value:, method:,
1131
+ keep_axis:). Dispatches fixlen -> reject, object -> object lane,
1132
+ numeric -> numeric lane. */
1133
+ static VALUE
1134
+ rb_ca_percentile_m (int argc, VALUE *argv, VALUE self)
1135
+ {
1136
+ VALUE ropt = rb_pop_options(&argc, &argv);
1137
+ VALUE raxis = Qnil, rmin_count = INT2FIX(0), fill_value = Qnil,
1138
+ method = sym_linear, rkeep = Qfalse;
1139
+ rb_scan_options(ropt, "axis,min_count,fill_value,method,keep_axis",
1140
+ &raxis, &rmin_count, &fill_value, &method, &rkeep);
1141
+
1142
+ CArray *ca;
1143
+ GetCArray(self, ca);
1144
+ /* Boolean rides the f64 lane (0/1 -> 0.0/1.0), so percentile / quantile
1145
+ interpolate and return a float, matching the integer lane. */
1146
+ if ( ca->data_type == CA_BOOLEAN ) {
1147
+ self = rb_ca_wrap_readonly(self, INT2NUM(CA_FLOAT64));
1148
+ GetCArray(self, ca);
1149
+ }
1150
+ int8_t dt = ca->data_type;
1151
+ if ( ca_is_fixlen_type(ca) )
1152
+ rb_raise(rb_eCADataTypeError,
1153
+ "percentile: not defined for fixlen; use a numeric or object array");
1154
+ int is_obj = (dt == CA_OBJECT);
1155
+ if ( !is_obj && !mp_is_numeric(dt) )
1156
+ rb_raise(rb_eCADataTypeError, "percentile: unsupported data_type %d", dt);
1157
+
1158
+ long min_count = NUM2LONG(rmin_count);
1159
+ if ( min_count < 0 )
1160
+ rb_raise(rb_eArgError, "min_count must be non-negative; got %ld", min_count);
1161
+
1162
+ VALUE pers = pct_flatten_validate_pers(rb_ary_new_from_values(argc, argv));
1163
+ pct_validate_method(method);
1164
+ int keep_axis = RTEST(rkeep);
1165
+ int single_p = (RARRAY_LEN(pers) == 1);
1166
+
1167
+ /* numeric lane bakes keep_axis into the reduced output; object lane
1168
+ returns unwrapped results and is keep-wrapped here (insert_axis for
1169
+ the per-axis form, obj_keep_axis_full for the flat form) -- matching
1170
+ production's structure for arbitrary-object results.
1171
+
1172
+ Return shape: multi-p returns Array<Float> (flat) or Array<CArray>
1173
+ (per-axis); single-p unwraps the length-1 Array so the caller gets
1174
+ Float / CArray directly. A single Array or CArray p argument is
1175
+ flattened first and follows the same rule (length 1 unwraps). */
1176
+ VALUE result;
1177
+ if ( !NIL_P(raxis) ) {
1178
+ long axis = rb_ca_normalize_axis_value(self, raxis, "percentile");
1179
+ /* mask / min_count / fill_value require the per-fiber select (each fiber
1180
+ has its own n_present); the plain partition/sort path assumes a
1181
+ uniform n. These paths bake keep_axis into the reduced output. */
1182
+ int per_fiber = RTEST(rb_ca_has_mask(self)) || min_count > 0
1183
+ || !NIL_P(fill_value);
1184
+ if ( !is_obj ) {
1185
+ result = per_fiber
1186
+ ? pct_axis_masked(self, pers, axis, method, min_count, fill_value, keep_axis)
1187
+ : pct_axis(self, pers, axis, method, keep_axis);
1188
+ } else if ( per_fiber ) {
1189
+ result = percentile_object_axis_masked(self, pers, axis, method,
1190
+ min_count, fill_value, keep_axis);
1191
+ } else {
1192
+ result = percentile_object_axis(self, pers, axis, method);
1193
+ if ( keep_axis ) {
1194
+ long len = RARRAY_LEN(result);
1195
+ VALUE w = rb_ary_new_capa(len);
1196
+ VALUE ia[1] = { LONG2NUM(axis) };
1197
+ for ( long i = 0; i < len; i++ )
1198
+ rb_ary_push(w, rb_ca_insert_axis(1, ia, rb_ary_entry(result, i)));
1199
+ result = w;
1200
+ }
1201
+ }
1202
+ } else if ( !is_obj ) {
1203
+ result = pct_flat(self, pers, min_count, fill_value, method, keep_axis);
1204
+ } else {
1205
+ result = percentile_object_flat(self, pers, min_count, fill_value, method);
1206
+ if ( keep_axis ) {
1207
+ long len = RARRAY_LEN(result);
1208
+ VALUE w = rb_ary_new_capa(len);
1209
+ for ( long i = 0; i < len; i++ )
1210
+ rb_ary_push(w, obj_keep_axis_full(self, rb_ary_entry(result, i)));
1211
+ result = w;
1212
+ }
1213
+ }
1214
+
1215
+ if ( single_p ) return rb_ary_entry(result, 0);
1216
+ return result;
1217
+ }
1218
+
1219
+ /* [entry] quantile(axis:, keep_axis:) = percentile(0, 25, 50, 75, 100, ...).
1220
+ Return shape: Array<Float> len 5 (flat) or Array<CArray> len 5 (per-axis),
1221
+ matching percentile's multi-p wrapping since we pass 5 fixed p values. */
1222
+ static VALUE
1223
+ rb_ca_quantile_m (int argc, VALUE *argv, VALUE self)
1224
+ {
1225
+ VALUE ropt = rb_pop_options(&argc, &argv);
1226
+ if ( argc != 0 )
1227
+ rb_raise(rb_eArgError, "quantile: no positional args accepted (got %d)", argc);
1228
+ VALUE raxis = Qnil, rkeep = Qfalse;
1229
+ rb_scan_options(ropt, "axis,keep_axis", &raxis, &rkeep);
1230
+ VALUE opts = rb_hash_new();
1231
+ if ( !NIL_P(raxis) ) rb_hash_aset(opts, ID2SYM(rb_intern("axis")), raxis);
1232
+ rb_hash_aset(opts, ID2SYM(rb_intern("keep_axis")), rkeep);
1233
+ VALUE pa[6] = { INT2FIX(0), INT2FIX(25), INT2FIX(50), INT2FIX(75), INT2FIX(100),
1234
+ opts };
1235
+ return rb_ca_percentile_m(6, pa, self);
1236
+ }
1237
+
1238
+ void
1239
+ Init_carray_median_percentile (void)
1240
+ {
1241
+ id_axis = rb_intern("axis");
1242
+ id_sort_copy = rb_intern("sort_copy");
1243
+ id_is_not_masked = rb_intern("is_not_masked");
1244
+ id_plus = rb_intern("+"); id_div = rb_intern("/"); id_mul = rb_intern("*");
1245
+ id_min = rb_intern("min"); id_max = rb_intern("max");
1246
+ id_sort = rb_intern("sort"); id_copy = rb_intern("copy");
1247
+ id_aref = rb_intern("[]"); id_aset = rb_intern("[]=");
1248
+ sym_linear = ID2SYM(rb_intern("linear"));
1249
+ sym_lower = ID2SYM(rb_intern("lower"));
1250
+ sym_higher = ID2SYM(rb_intern("higher"));
1251
+ sym_nearest = ID2SYM(rb_intern("nearest"));
1252
+ sym_midpoint = ID2SYM(rb_intern("midpoint"));
1253
+
1254
+ rb_define_method(rb_cCArray, "median", rb_ca_median_m, -1);
1255
+ rb_define_method(rb_cCArray, "percentile", rb_ca_percentile_m, -1);
1256
+ rb_define_method(rb_cCArray, "quantile", rb_ca_quantile_m, -1);
1257
+ }