carray 3.0.3 → 3.0.4

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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
data/lib/carray/time.rb CHANGED
@@ -11,7 +11,9 @@
11
11
  # - Same pattern for CATimedelta
12
12
  #
13
13
  # Base epoch fixed to 1970-01-01 (Unix epoch).
14
- # NetCDF CF convention (= reference time epoch) is delegated to external gems (timesteps etc.).
14
+ # Counts from another reference time (a NetCDF CF units attribute) convert in
15
+ # core through CATime::Grid; only non-standard calendars are left to external
16
+ # gems.
15
17
 
16
18
  # Internal: unit-conversion algebra shared by CATime / CATimedelta
17
19
  # (used by to_comparable to align a search query to the reference unit).
@@ -25,6 +27,11 @@
25
27
  # Civil-date arithmetic on int64 CArrays: the calendar kernel every time
26
28
  # grid is built on (Howard Hinnant's days<->civil algorithms, vectorized).
27
29
  # Internal to the time surface: nothing here is part of the public API.
30
+ # Loaded with the file, not inside the methods: a require is a lock that
31
+ # Ractors running these methods at the same time can deadlock on.
32
+ require 'date'
33
+ require 'time'
34
+
28
35
  module CATimeCivil
29
36
  module_function
30
37
 
@@ -106,6 +113,40 @@ module CATimeUnitAlgebra
106
113
  end
107
114
  end
108
115
 
116
+ # A single value an operator meets an array with, as a one-cell array
117
+ # marked scalar (a CScalar) of its kind, or nil when it is not one: a
118
+ # duration -- a CATimedelta::Element, or a String that reads as one
119
+ # ("3 days") -- or an instant -- a CATime::Element, a Time, a Date, or any
120
+ # other String. An instant that lands on the +unit+ grid is given in that
121
+ # unit, so a day array minus "2024-01-02" answers in days.
122
+ def scalar_operand(x, unit)
123
+ case x
124
+ when CATimedelta::Element
125
+ CATimedelta.wrap(CScalar.int64 { x.value }, unit: x.unit)
126
+ when String
127
+ res = begin
128
+ CATime::Resolution.parse(x)
129
+ rescue ArgumentError
130
+ nil
131
+ end
132
+ return CATimedelta.wrap(CScalar.int64 { res.count }, unit: res.base) if res
133
+ scalar_instant(CATimeLiteral.exact_instant(x), unit)
134
+ when CATime::Element
135
+ CATime.wrap(CScalar.int64 { x.value }, unit: x.unit)
136
+ when Time, Date
137
+ scalar_instant(CATimeLiteral.exact_instant(x), unit)
138
+ end
139
+ end
140
+
141
+ def scalar_instant(t, unit)
142
+ v = begin
143
+ convert_instant!(t.parent, t.unit, unit)[0]
144
+ rescue ArgumentError
145
+ return CATime.wrap(CScalar.int64 { t.parent[0] }, unit: t.unit)
146
+ end
147
+ CATime.wrap(CScalar.int64 { v }, unit: unit)
148
+ end
149
+
109
150
  # seconds- (fixed) or months- (calendar) per base tick.
110
151
  def base_ratio(base)
111
152
  FIXED[base] || CALENDAR[base]
@@ -254,7 +295,8 @@ module CATimeUnitAlgebra
254
295
  # CATimedelta#to_unit and by dt +/- td, where the result keeps the time's
255
296
  # unit and a finer duration is truncated to it (a :D time + a 5 h duration
256
297
  # is + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
257
- # duration has no fixed ratio to a fixed unit).
298
+ # duration has no fixed ratio to a fixed unit; dt +/- a month moves the date
299
+ # through shift_months instead).
258
300
  def convert_scale_trunc(storage, from, to)
259
301
  a = res(from); b = res(to)
260
302
  return storage if a == b
@@ -269,6 +311,67 @@ module CATimeUnitAlgebra
269
311
  r.denominator == 1 ? scaled : trunc_divide(scaled, r.denominator)
270
312
  end
271
313
 
314
+ # A duration brought onto a time's grid for dt +/- td. A duration finer
315
+ # than the grid is truncated toward zero (a :D time + 5 h is + 0 days), but
316
+ # one whose tick is coarser and not a whole number of grid ticks ("1 day"
317
+ # on a "7 hours" grid, "15 minutes" on a "10 minutes" grid) must land on
318
+ # the grid exactly: cutting it would move the time by part of its own
319
+ # tick, which no rounding of the time asked for.
320
+ def duration_on_grid(storage, from, to)
321
+ r = ratio(res(from), res(to))
322
+ return convert_scale_trunc(storage, from, to) if r.nil? || r <= 1 || r.denominator == 1
323
+ scaled = widen(storage, r.numerator)
324
+ unless (scaled % r.denominator).eq(0).all
325
+ raise ArgumentError,
326
+ "a duration in #{res(from)} does not land on the #{res(to)} grid " \
327
+ "(it is not a whole number of #{res(to)} ticks); convert the time " \
328
+ "to a grid both share first"
329
+ end
330
+ scaled / r.denominator
331
+ end
332
+
333
+ # a + b and a - b on int64 storage (b a CArray or an Integer), raising
334
+ # instead of wrapping around when a result leaves int64. An overflow is a
335
+ # result whose sign differs from both terms of the sum (a and b for +, a
336
+ # and -b for -).
337
+ def checked_add(a, b)
338
+ check_term(b, "+")
339
+ r = a + b
340
+ overflow_raise("+") if ((a ^ r) & (b ^ r)).lt(0).any
341
+ r
342
+ end
343
+
344
+ def checked_sub(a, b)
345
+ check_term(b, "-")
346
+ r = a - b
347
+ overflow_raise("-") if ((a ^ b) & (a ^ r)).lt(0).any
348
+ r
349
+ end
350
+
351
+ def checked_mul(a, k)
352
+ check_term(k, "*")
353
+ lo = a.min
354
+ unless lo == UNDEF
355
+ [lo, a.max].each do |x|
356
+ overflow_raise("*") unless (INT64_MIN..INT64_MAX).cover?(Integer(x) * k)
357
+ end
358
+ end
359
+ a * k
360
+ end
361
+
362
+ def check_term(b, op)
363
+ overflow_raise(op) if b.is_a?(Integer) && !(INT64_MIN..INT64_MAX).cover?(b)
364
+ end
365
+
366
+ def overflow_raise(op)
367
+ raise RangeError,
368
+ "time arithmetic overflows int64 (#{op}): the result is outside " \
369
+ "the range of the storage unit; use a coarser unit"
370
+ end
371
+
372
+ INT64_MIN = -2**63
373
+ INT64_MAX = 2**63 - 1
374
+
272
375
  # Truncating integer division of an int64 CArray (toward zero). CArray
273
376
  # `/` floors toward the past, which is what an instant wants; a duration
274
377
  # is a magnitude and shrinks toward zero instead.
@@ -297,6 +400,40 @@ module CATimeUnitAlgebra
297
400
  end
298
401
  end
299
402
 
403
+ # A time on a fixed grid moved by whole months, as Ruby's Date#>> moves a
404
+ # date: the year and the month move, the day stays unless the new month is
405
+ # shorter (then it is that month's last day), and the time of day stays.
406
+ # The grid has to tile a day, so the result lands on it; on any other grid
407
+ # (:W, "3 days", "7 hours") a month moves a time off its grid, and that
408
+ # raises. +months+ is an Integer or an int64 CArray of the time's shape.
409
+ def shift_months(storage, from, months)
410
+ rd = ratio(CATime::Resolution.new(1, :D), from) # ticks of `from` per day
411
+ unless rd && rd.denominator == 1
412
+ raise ArgumentError,
413
+ "cannot move a time on the #{from} grid by months: a day is not a " \
414
+ "whole number of its ticks, so the result would be off the grid"
415
+ end
416
+ n = rd.numerator
417
+ days = storage / n # floors toward the past
418
+ rest = storage - days * n # the time of day, in ticks
419
+ y, m, d = CATimeCivil.civil_from_days(days)
420
+ ord = y * 12 + (m - 1) + months # month ordinal after the move
421
+ ny = ord / 12
422
+ nm = ord - ny * 12 + 1
423
+ ones = CArray.int64(*storage.shape) { 1 }
424
+ head = CATimeCivil.days_from_civil(ny, nm, ones)
425
+ ord1 = ord + 1
426
+ ny1 = ord1 / 12
427
+ last = CATimeCivil.days_from_civil(ny1, ord1 - ny1 * 12 + 1, ones) - head
428
+ widen(head + d.pmin(last) - 1, n) + rest
429
+ end
430
+
431
+ # Whole months in a calendar duration of +res+: its count of months or of
432
+ # years.
433
+ def months_per_tick(res)
434
+ (res.count * CALENDAR.fetch(res.base)).to_i
435
+ end
436
+
300
437
  # calendar time (Resolution `from`) -> days since the epoch (int64
301
438
  # CArray). Folds the resolution count (value = count-Y/M buckets).
302
439
  def calendar_days_since_epoch(storage, from)
@@ -325,25 +462,29 @@ module CATimeUnitAlgebra
325
462
  widen(calendar_days_since_epoch(storage, from), r.numerator)
326
463
  end
327
464
 
465
+ # Whole days since the epoch (floored) of fixed-length ticks, and the
466
+ # remainder that says whether each instant sits on a day boundary. A tick
467
+ # need not divide a day, nor be a whole number of days ("7 hours",
468
+ # "36 hours", "7 seconds"): the count is scaled to a grid both share.
469
+ def fixed_storage_days(storage, from)
470
+ rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
471
+ scaled = widen(storage, rd.denominator)
472
+ n = rd.numerator
473
+ { days: scaled / n, rest: scaled % n }
474
+ end
475
+
328
476
  # fixed-length grid -> calendar time: the coarsening half of
329
477
  # convert_instant!, exact-or-raise. Every instant must land on a day
330
478
  # boundary and then on the calendar boundary itself (the 1st, and
331
479
  # January too for :Y), since a mid-month instant has no :M value.
332
480
  def convert_instant_fixed_to_calendar(storage, from, to)
333
- rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
334
- days =
335
- if rd.denominator == 1
336
- n = rd.numerator
337
- unless (storage % n).eq(0).all
338
- raise ArgumentError,
339
- "cannot convert time #{from} to #{to} without loss " \
340
- "(instant is not on a day boundary)"
341
- end
342
- storage / n
343
- else # coarser than a day (:W)
344
- storage * (from.tick_ratio / 86400r).to_i
345
- end
346
- y, m, d = CATimeCivil.civil_from_days(days)
481
+ days = fixed_storage_days(storage, from)
482
+ unless (days[:rest]).eq(0).all
483
+ raise ArgumentError,
484
+ "cannot convert time #{from} to #{to} without loss " \
485
+ "(instant is not on a day boundary)"
486
+ end
487
+ y, m, d = CATimeCivil.civil_from_days(days[:days])
347
488
  on_boundary = d.eq(1)
348
489
  on_boundary &= m.eq(1) if to.base == :Y
349
490
  unless on_boundary.all
@@ -390,14 +531,7 @@ module CATimeUnitAlgebra
390
531
  # month, whatever day and time it carries. The flooring counterpart of
391
532
  # convert_instant_fixed_to_calendar.
392
533
  def convert_instant_fixed_to_calendar_floor(storage, from, to)
393
- rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
394
- days =
395
- if rd.denominator == 1
396
- storage / rd.numerator
397
- else # coarser than a day (:W)
398
- storage * (from.tick_ratio / 86400r).to_i
399
- end
400
- y, m, = CATimeCivil.civil_from_days(days)
534
+ y, m, = CATimeCivil.civil_from_days(fixed_storage_days(storage, from)[:days])
401
535
  ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
402
536
  to.count > 1 ? ord / to.count : ord
403
537
  end
@@ -443,8 +577,64 @@ module CATimeSurfaceFill
443
577
 
444
578
  end
445
579
 
580
+ # The part of to_comparable the two time Faces share: values written one by
581
+ # one (a Ruby Array) and an array that is not of this kind (an object column,
582
+ # a string column) are each read as a value of this Face by to_comparable, an
583
+ # UNDEF or a masked cell kept masked. Nil for any other operand, which the Face's own
584
+ # to_comparable decides.
585
+ module CATimeOperandValues
586
+
587
+ # The reference and the operand of a comparison (eq, <, count, search,
588
+ # linear_section, pmax ...) brought to one unit: the operand read in its
589
+ # own unit, then the reference's unit when the operand sits on its grid
590
+ # (the reference is not converted), else the finer unit the two share, as
591
+ # is_in and the set operations meet. A value that is not of this kind is
592
+ # refused as to_comparable refuses it. An assignment, which has to land on
593
+ # the reference's grid, reconciles with to_comparable instead.
594
+ # @api private
595
+ def comparable_pair (operand)
596
+ lifted = lift_operand(operand)
597
+ return [self, lifted] if lifted.unit == unit
598
+ narrowed = lifted.to_unit(unit)
599
+ if narrowed.to_unit(lifted.unit).parent.eq(lifted.parent).all
600
+ return [self, narrowed]
601
+ end
602
+ in_common_unit_with(lifted)
603
+ end
604
+
605
+ private
606
+
607
+ # The operand as an array of this kind in its own unit: one value read as
608
+ # it is written, an Array or an array of another kind read value by value
609
+ # in the unit they share, an UNDEF or a masked cell kept masked.
610
+ def lift_operand (operand)
611
+ return operand if operand.is_a?(self.class)
612
+ case operand
613
+ when Array
614
+ # A nested Array keeps its shape; reading it row by row would land
615
+ # each row as one value.
616
+ return lift_operand(CA_OBJECT(operand)) if operand.any?(Array)
617
+ lifted = operand.map { |v| UNDEF.equal?(v) ? nil : lift_operand(v) }
618
+ units = lifted.compact.map(&:unit)
619
+ u = units.empty? ? unit : units.reduce { |a, b| common_unit(a, b) }
620
+ self.class.wrap(CA_INT64(lifted.map { |x| x ? x.to_unit(u).parent[0] : UNDEF }),
621
+ unit: u)
622
+ when CArray
623
+ lift_operand(operand.to_a.flatten).reshape(*operand.shape)
624
+ else
625
+ lift_value(operand) || to_comparable(operand)
626
+ end
627
+ end
628
+
629
+ def reconcile_values (operand)
630
+ to_comparable(lift_operand(operand))
631
+ end
632
+
633
+ end
634
+
446
635
  class CATime
447
636
  include CATimeSurfaceFill
637
+ include CATimeOperandValues
448
638
  # @overload new(*shape, unit: :ns)
449
639
  # Allocates a new int64 storage CArray of the given `shape` and
450
640
  # wraps it as a CATime Face with the given `unit`. The
@@ -465,7 +655,7 @@ class CATime
465
655
  # @param unit [Resolution, Symbol, String] tick resolution.
466
656
  # @return [CATime]
467
657
  def self.wrap(raw, unit: :ns)
468
- __wrap__(raw, unit)
658
+ wrap_storage(raw, unit)
469
659
  end
470
660
 
471
661
  # Element-return class: a single time element carries both its
@@ -497,8 +687,6 @@ class CATime
497
687
  # @return [Time] the instant this scalar denotes (UTC). For a calendar
498
688
  # unit this is the first instant (midnight of day 1) of the granule.
499
689
  def to_time
500
- require 'time'
501
- require 'date'
502
690
  case unit.base
503
691
  when :Y then d = epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
504
692
  when :M then d = epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
@@ -510,7 +698,6 @@ class CATime
510
698
  # @return [Date] the date this scalar denotes (UTC). A sub-day unit is
511
699
  # floored to its day.
512
700
  def to_date
513
- require 'date'
514
701
  case unit.base
515
702
  when :Y then epoch_date.next_year(value * unit.count)
516
703
  when :M then epoch_date.next_month(value * unit.count)
@@ -559,6 +746,10 @@ class CATime
559
746
  to_time <=> other.getutc
560
747
  when (defined?(DateTime) ? DateTime : nil)
561
748
  to_time <=> other.to_time.getutc
749
+ when (defined?(Date) ? Date : nil)
750
+ # a Date names the midnight (UTC) that begins it, as it does when an
751
+ # array is compared with it
752
+ to_time.to_r <=> CATimeLiteral.epoch_seconds(other)
562
753
  end
563
754
  rescue ArgumentError
564
755
  nil
@@ -576,18 +767,27 @@ class CATime
576
767
  [value, unit].hash
577
768
  end
578
769
 
770
+ # Marshal carries the tick and its resolution.
771
+ def marshal_dump = [value, unit.count, unit.base]
772
+
773
+ def marshal_load(data)
774
+ v, count, base = data
775
+ initialize(v, CATime::Resolution.new(count, base))
776
+ end
777
+
579
778
  # @overload +(td)
580
779
  # Returns this instant advanced by a {CATimedelta::Element}. The time
581
780
  # is the anchor: the result keeps `self`'s unit and the duration is
582
781
  # converted into it, truncated toward zero when finer (a :D time + a
583
782
  # 5 h duration is + 0 days; + 30 h is + 1 day) -- the same rule as the
584
- # array {CATime#+}. Only a duration in the SAME group (both calendar
585
- # or both fixed) is accepted -- a cross-group step (a :s time + a :M
586
- # duration) is calendar arithmetic; use `to_date` +
587
- # `Date#next_month` / `#next_year` for that.
783
+ # array {CATime#+}. A month or year duration on a fixed grid moves
784
+ # the date as Ruby's `Date#>>` does (the day stays unless the month is
785
+ # shorter, the time of day stays); a fixed duration on a month or year
786
+ # grid raises, as does a month on a grid a day does not divide.
588
787
  # @param td [CATimedelta::Element]
589
788
  # @return [CATime::Element]
590
- # @raise [TypeError, ArgumentError] on a non-timedelta / cross-group operand.
789
+ # @raise [TypeError, ArgumentError] on a non-timedelta operand, or a
790
+ # pair that has no answer on self's grid.
591
791
  def +(td)
592
792
  shifted_by_duration(td, 1)
593
793
  end
@@ -603,9 +803,13 @@ class CATime
603
803
  case other
604
804
  when Element
605
805
  u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
606
- CATimedelta::Element.new(instant_in(u) - other.instant_in(u), u)
607
- when CATimedelta::Element
806
+ v = instant_in(u) - other.instant_in(u)
807
+ CATimeUnitAlgebra.check_term(v, "-")
808
+ CATimedelta::Element.new(v, u)
809
+ when CATimedelta::Element, CATimedelta
608
810
  shifted_by_duration(other, -1)
811
+ when CATime
812
+ -(other - self)
609
813
  else
610
814
  raise TypeError, "CATime::Element - #{other.class} is not allowed"
611
815
  end
@@ -624,22 +828,33 @@ class CATime
624
828
 
625
829
  # s +/- td: the time is the anchor, so the result keeps self's unit and
626
830
  # the duration is converted into it (truncated toward zero when finer; a
627
- # cross-group calendar duration raises). Mirrors the array CATime +/-.
831
+ # month or year duration on a fixed grid moves the date by months).
832
+ # Mirrors the array CATime +/-.
628
833
  def shifted_by_duration(td, sign)
834
+ # An array of durations gives an array of instants, anchored here.
835
+ return sign > 0 ? td + self : (-td) + self if td.is_a?(CATimedelta)
629
836
  unless td.is_a?(CATimedelta::Element)
630
837
  raise TypeError,
631
838
  "CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
632
839
  "allowed (add / subtract a CATimedelta)"
633
840
  end
634
- tv = CATimeUnitAlgebra.convert_scale_trunc(CA_INT64([td.value]), td.unit, unit)[0]
635
- Element.new(value + sign * tv, unit)
841
+ if CATimeUnitAlgebra::CALENDAR.key?(td.unit.base) &&
842
+ !CATimeUnitAlgebra::CALENDAR.key?(unit.base)
843
+ # months on a fixed grid move the date as Date#>> does, as the
844
+ # array does
845
+ months = sign * td.value * CATimeUnitAlgebra.months_per_tick(td.unit)
846
+ return Element.new(CATimeUnitAlgebra.shift_months(CA_INT64([value]), unit, months)[0], unit)
847
+ end
848
+ tv = CATimeUnitAlgebra.duration_on_grid(CA_INT64([td.value]), td.unit, unit)[0]
849
+ v = value + sign * tv
850
+ CATimeUnitAlgebra.check_term(v, sign > 0 ? "+" : "-")
851
+ Element.new(v, unit)
636
852
  end
637
853
 
638
854
  # Base epoch as a Date. next_month / next_year clamp end-of-month
639
855
  # (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
640
856
  # do not move the base off the first of the month without revisiting.
641
857
  def epoch_date
642
- require 'date'
643
858
  Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
644
859
  end
645
860
 
@@ -662,6 +877,7 @@ end
662
877
 
663
878
  class CATimedelta
664
879
  include CATimeSurfaceFill
880
+ include CATimeOperandValues
665
881
  # @overload new(*shape, unit: :ns)
666
882
  # Allocates a new int64 storage CArray of the given `shape` and
667
883
  # wraps it as a CATimedelta Face with the given `unit`.
@@ -679,7 +895,7 @@ class CATimedelta
679
895
  # @param unit [CATime::Resolution, Symbol, String] tick resolution.
680
896
  # @return [CATimedelta]
681
897
  def self.wrap(raw, unit: :ns)
682
- __wrap__(raw, unit)
898
+ wrap_storage(raw, unit)
683
899
  end
684
900
 
685
901
  # A single duration: an integer tick count plus the resolution those ticks
@@ -746,6 +962,14 @@ class CATimedelta
746
962
  [value, unit].hash
747
963
  end
748
964
 
965
+ # Marshal carries the tick and its resolution.
966
+ def marshal_dump = [value, unit.count, unit.base]
967
+
968
+ def marshal_load(data)
969
+ v, count, base = data
970
+ initialize(v, CATime::Resolution.new(count, base))
971
+ end
972
+
749
973
  # @overload +(other)
750
974
  # Returns the sum of two durations, promoting to the finer unit. A
751
975
  # cross-group operand (a :M + a :s duration) has no fixed relation and
@@ -766,7 +990,9 @@ class CATimedelta
766
990
  # @return [CATimedelta::Element]
767
991
  def *(n)
768
992
  raise TypeError, "CATimedelta::Element * #{n.class} is not allowed (Integer only)" unless n.is_a?(Integer)
769
- Element.new(value * n, unit)
993
+ v = value * n
994
+ CATimeUnitAlgebra.check_term(v, "*")
995
+ Element.new(v, unit)
770
996
  end
771
997
 
772
998
  # @overload /(other)
@@ -801,12 +1027,21 @@ class CATimedelta
801
1027
  private
802
1028
 
803
1029
  def combined_with_duration(other, sign)
1030
+ # An array gives an array: durations by durations, and instants for a
1031
+ # duration added to times (subtracting times from a duration is not
1032
+ # defined).
1033
+ case other
1034
+ when CATimedelta then return sign > 0 ? other + self : -(other - self)
1035
+ when CATime then return other + self if sign > 0
1036
+ end
804
1037
  unless other.is_a?(Element)
805
1038
  raise TypeError,
806
1039
  "CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
807
1040
  end
808
1041
  u = CATimeUnitAlgebra.finer(unit, other.unit)
809
- Element.new(duration_in(u) + sign * other.duration_in(u), u)
1042
+ v = duration_in(u) + sign * other.duration_in(u)
1043
+ CATimeUnitAlgebra.check_term(v, sign > 0 ? "+" : "-")
1044
+ Element.new(v, u)
810
1045
  end
811
1046
  end
812
1047
  end
@@ -873,22 +1108,36 @@ class CATime
873
1108
  # Returns `self + other` for a {CATimedelta}: the time is the
874
1109
  # anchor, so the result keeps `self`'s unit and the duration is converted
875
1110
  # into it (a duration finer than `self`'s unit is truncated to it; a
876
- # cross-group calendar duration raises). Adding two datetimes is
877
- # ill-defined.
878
- # @param other [CATimedelta]
1111
+ # coarser one that is not a whole number of `self`'s ticks raises). A
1112
+ # month or year duration on a fixed grid moves each date as Ruby's
1113
+ # `Date#>>` does: the day stays unless the new month is shorter (then it
1114
+ # is that month's last day), and the time of day stays, so
1115
+ # `2024-01-31 + 1 month` is `2024-02-29`. The grid has to divide a day
1116
+ # into whole ticks; on any other (`:W`, `"3 days"`) a month raises, as a
1117
+ # fixed duration on a month or year grid does. Adding two datetimes is
1118
+ # ill-defined. A single duration -- a {CATimedelta::Element} or a String
1119
+ # such as `"3 days"` -- is added to every cell.
1120
+ # @param other [CATimedelta, CATimedelta::Element, String]
879
1121
  # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
880
1122
  # duration array)
881
1123
  # @return [CATime]
882
1124
  # @raise [TypeError] on a non-timedelta operand.
1125
+ # @raise [ArgumentError] when the duration does not land on `self`'s grid.
1126
+ # @raise [RangeError] when a result leaves the range of int64 ticks.
883
1127
  def +(other)
884
1128
  given, other = other, CATimeUnitAlgebra.operand(other)
1129
+ other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
885
1130
  case other
886
1131
  when CATimedelta
887
- (parent + CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
1132
+ return shifted_by_months(other, 1) if moved_by_months?(other)
1133
+ d = CATimeUnitAlgebra.duration_on_grid(other.parent, other.unit, unit)
1134
+ d = d[0] if other.scalar?
1135
+ CATimeUnitAlgebra.checked_add(parent, d).time(unit: unit)
888
1136
  when CATime
889
1137
  raise TypeError, "CATime + CATime is ill-defined"
890
1138
  else
891
- raise TypeError, "CATime + #{given.class} is not allowed (use CATimedelta)"
1139
+ raise TypeError, "CATime + #{given.class} is not allowed: add a duration " \
1140
+ "(a CATimedelta, a CATimedelta::Element or a String such as \"3 days\")"
892
1141
  end
893
1142
  end
894
1143
 
@@ -896,27 +1145,91 @@ class CATime
896
1145
  # Subtracting a {CATimedelta} yields a {CATime} at `self`'s unit (the
897
1146
  # duration is converted into it, truncated when finer); subtracting
898
1147
  # another {CATime} yields a {CATimedelta} at the finer of the two
899
- # units (cross-group falls to the fixed unit).
900
- # @param other [CATimedelta, CATime]
1148
+ # units (cross-group falls to the fixed unit). A single duration (a
1149
+ # {CATimedelta::Element}, `"3 days"`) or time (a {CATime::Element}, a
1150
+ # `Time`, a `Date`, `"2024-01-02"`) is subtracted from every cell.
1151
+ # @param other [CATimedelta, CATime, CATimedelta::Element, CATime::Element, Time, Date, String]
901
1152
  # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
902
1153
  # duration or timestamp array)
903
1154
  # @return [CATime, CATimedelta]
904
1155
  # @raise [TypeError] on an unsupported operand.
905
1156
  def -(other)
906
1157
  given, other = other, CATimeUnitAlgebra.operand(other)
1158
+ other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
907
1159
  case other
908
1160
  when CATimedelta
909
- (parent - CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
1161
+ return shifted_by_months(other, -1) if moved_by_months?(other)
1162
+ d = CATimeUnitAlgebra.duration_on_grid(other.parent, other.unit, unit)
1163
+ d = d[0] if other.scalar?
1164
+ CATimeUnitAlgebra.checked_sub(parent, d).time(unit: unit)
910
1165
  when CATime
911
1166
  u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
912
1167
  a = CATimeUnitAlgebra.convert_instant!(parent, unit, u)
913
1168
  b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
914
- (a - b).timedelta(unit: u)
1169
+ b = b[0] if other.scalar?
1170
+ CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
915
1171
  else
916
- raise TypeError, "CATime - #{given.class} is not allowed"
1172
+ raise TypeError, "CATime - #{given.class} is not allowed: subtract a duration " \
1173
+ "(a CATimedelta, a CATimedelta::Element or a String such as \"3 days\") " \
1174
+ "or a time (a CATime, a CATime::Element, a Time, a Date or a String)"
917
1175
  end
918
1176
  end
919
1177
 
1178
+ # Whether a duration moves this time by calendar months: a month or year
1179
+ # duration on a time whose grid is fixed-length. A time on a month or year
1180
+ # grid adds it as months already, and a fixed duration on a calendar grid
1181
+ # is refused by the scale conversion.
1182
+ private def moved_by_months? (td)
1183
+ CATimeUnitAlgebra::CALENDAR.key?(td.unit.base) &&
1184
+ !CATimeUnitAlgebra::CALENDAR.key?(unit.base)
1185
+ end
1186
+
1187
+ # self moved by the months of +td+ (+sign+ 1 to add, -1 to subtract), as
1188
+ # Ruby's Date#>> moves a date: the day is kept unless the month is shorter.
1189
+ private def shifted_by_months (td, sign)
1190
+ months = td.parent.int64 * (sign * CATimeUnitAlgebra.months_per_tick(td.unit))
1191
+ months = months[0] if td.scalar?
1192
+ CATimeUnitAlgebra.shift_months(parent, unit, months).time(unit: unit)
1193
+ end
1194
+
1195
+ # @overload pmax(other)
1196
+ # The later of each cell and `other`, cell by cell: `other` is a time
1197
+ # array of the same shape, or one time (a {CATime::Element}, a `Time`, a
1198
+ # `Date`, `"2024-01-02"`) for every cell. The two meet as a comparison
1199
+ # does, so the result is in `self`'s unit when `other` sits on its grid
1200
+ # and in the finer unit the two share otherwise. A masked cell on either
1201
+ # side gives a masked cell.
1202
+ # @param other [CATime, CATime::Element, Time, Date, String]
1203
+ # @return [CATime]
1204
+ # @raise [TypeError] when `other` is not a time.
1205
+ def pmax (other)
1206
+ pick_by_order(other) { |a, b| a < b }
1207
+ end
1208
+
1209
+ # @overload pmin(other)
1210
+ # The earlier of each cell and `other`; see {#pmax}.
1211
+ # @param other [CATime, CATime::Element, Time, Date, String]
1212
+ # @return [CATime]
1213
+ # @raise [TypeError] when `other` is not a time.
1214
+ def pmin (other)
1215
+ pick_by_order(other) { |a, b| a > b }
1216
+ end
1217
+
1218
+ # A time has no NaN, so maximum / minimum (which let a NaN win) are pmax /
1219
+ # pmin, and fmax / fmin are their other names.
1220
+ alias_method :maximum, :pmax
1221
+ alias_method :minimum, :pmin
1222
+ alias_method :fmax, :pmax
1223
+ alias_method :fmin, :pmin
1224
+
1225
+ # `other` where the block says self's cell loses to it, else self's cell.
1226
+ private def pick_by_order (other)
1227
+ return copy.tap { |out| out[] = UNDEF } if UNDEF.equal?(other)
1228
+ a, b = comparable_pair(other)
1229
+ b = b[0] unless other.is_a?(CArray) || other.is_a?(Array)
1230
+ yield(a, b).then_else(b, a)
1231
+ end
1232
+
920
1233
  # Element-wise comparison (< <= > >= <=>) rides the core comparison Face
921
1234
  # gate (ca_face_reconcile_comparison in ext/ca_obj_face.c): the inherited
922
1235
  # CArray operators descend to storage and reconcile a CATime RHS in a
@@ -1088,8 +1401,21 @@ class CATime
1088
1401
  [to_unit(u), other.to_unit(u)]
1089
1402
  end
1090
1403
 
1404
+ private def common_unit (a, b)
1405
+ CATimeUnitAlgebra.diff_unit(a, b)
1406
+ end
1407
+
1408
+ # One instant as a length-1 time array in its own unit, or nil.
1409
+ private def lift_value (operand)
1410
+ case operand
1411
+ when CATime::Element
1412
+ CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
1413
+ when Time, String, Date
1414
+ CATimeLiteral.exact_instant(operand)
1415
+ end
1416
+ end
1417
+
1091
1418
  def to_comparable (operand)
1092
- require 'date' # Date is one of the operand kinds below
1093
1419
  case operand
1094
1420
  when CATime
1095
1421
  return operand if operand.unit == unit
@@ -1100,21 +1426,15 @@ class CATime
1100
1426
  to_comparable(lifted)
1101
1427
  when Time, String, Date
1102
1428
  to_comparable(CATimeLiteral.exact_instant(operand))
1103
- when Array
1104
- # A set of instants written one by one (is_in(["2024-01-02", ...])).
1105
- CATime.wrap(CA_INT64(operand.map { |v| to_comparable(v).parent[0] }),
1106
- unit: unit)
1107
- when CArray
1108
- # An array that is not a time array: each value read as an instant (an
1109
- # object column of Time values, a string column), a masked cell kept
1110
- # masked.
1111
- require "carray/methods/is_in"
1112
- send(:values_in_face, self, operand) ||
1113
- CArray.int64(*operand.shape).fill(UNDEF).time(unit: unit)
1429
+ when Array, CArray
1430
+ # A set of instants written one by one (is_in(["2024-01-02", ...])),
1431
+ # or an array that is not a time array (an object column of Time
1432
+ # values, a string column), read value by value.
1433
+ reconcile_values(operand)
1114
1434
  else
1115
1435
  raise TypeError,
1116
1436
  "CATime cannot reconcile #{operand.class} " \
1117
- "(use ca.parent to compare the raw int64 storage directly)"
1437
+ "(compare #ticks for the raw int64 storage)"
1118
1438
  end
1119
1439
  end
1120
1440
 
@@ -1201,7 +1521,6 @@ class CATime
1201
1521
  # CArray. Array-level counterpart to {Element#to_time}.
1202
1522
  # @return [CArray]
1203
1523
  def to_time
1204
- require 'time'
1205
1524
  if CATimeUnitAlgebra::FIXED.key?(unit.base)
1206
1525
  f = unit.tick_ratio # exact seconds / tick (Rational)
1207
1526
  parent.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
@@ -1216,7 +1535,6 @@ class CATime
1216
1535
  # {Element#to_date}.
1217
1536
  # @return [CArray]
1218
1537
  def to_date
1219
- require 'date'
1220
1538
  # 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
1221
1539
  days_since_epoch.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
1222
1540
  end
@@ -1226,7 +1544,6 @@ class CATime
1226
1544
  # as an object CArray.
1227
1545
  # @return [CArray]
1228
1546
  def to_datetime
1229
- require 'date'
1230
1547
  to_time.convert(:object) {|t| t.to_datetime}
1231
1548
  end
1232
1549
 
@@ -1279,6 +1596,54 @@ class CATime
1279
1596
  # @return [CArray] integer.
1280
1597
  def second; clock_field(:s); end
1281
1598
 
1599
+ # Quarter of the year, 1..4 (January to March is 1). For a fiscal year,
1600
+ # count quarters with `timesteps(unit: "3 months", origin:)`.
1601
+ # @return [CArray] integer.
1602
+ def quarter
1603
+ (month - 1) / 3 + 1
1604
+ end
1605
+
1606
+ # ISO 8601 week number, 1..53. A week runs Monday to Sunday and belongs
1607
+ # to the year its Thursday is in, so the first days of January can be in
1608
+ # the last week of the year before, and the last days of December in
1609
+ # week 1 of the next; {#iso_year} is that year.
1610
+ # @return [CArray] integer.
1611
+ def iso_week
1612
+ thursday, year = iso_thursday
1613
+ ones = CArray.int64(*shape) { 1 }
1614
+ (thursday - CATimeCivil.days_from_civil(year, ones, ones)) / 7 + 1
1615
+ end
1616
+
1617
+ # The ISO 8601 year of each time's week (see {#iso_week}): the calendar
1618
+ # year except in the few days around New Year whose week is counted in the
1619
+ # neighbouring year.
1620
+ # @return [CArray] integer.
1621
+ def iso_year
1622
+ iso_thursday[1]
1623
+ end
1624
+
1625
+ # The Thursday of each time's ISO week, in days since the epoch, and the
1626
+ # year it falls in.
1627
+ private def iso_thursday
1628
+ days = days_since_epoch
1629
+ iso_weekday = (days + 3) % 7 + 1 # Monday 1 .. Sunday 7; 1970-01-01 is a Thursday
1630
+ thursday = days - iso_weekday + 4
1631
+ [thursday, CATimeCivil.civil_from_days(thursday)[0]]
1632
+ end
1633
+
1634
+ # Number of days in the month, 28..31 (January's for `:Y` storage, which
1635
+ # does not resolve months). `t.day.eq(t.days_in_month)` is the last day of
1636
+ # the month.
1637
+ # @return [CArray] integer.
1638
+ def days_in_month
1639
+ y, m, = CATimeCivil.civil_from_days(days_since_epoch)
1640
+ ones = CArray.int64(*shape) { 1 }
1641
+ ord = y * 12 + m # the next month's ordinal (m is 1..12)
1642
+ ny = ord / 12
1643
+ CATimeCivil.days_from_civil(ny, ord - ny * 12 + 1, ones) -
1644
+ CATimeCivil.days_from_civil(y, m, ones)
1645
+ end
1646
+
1282
1647
  # Day of the week, Sunday = 0 .. Saturday = 6.
1283
1648
  # @return [CArray] integer.
1284
1649
  def weekday
@@ -1301,7 +1666,6 @@ class CATime
1301
1666
  # Returns the Julian Day Number for each element.
1302
1667
  # @return [CArray]
1303
1668
  def jd
1304
- require 'date'
1305
1669
  to_time.convert(:int) {|t| t.to_date.jd}
1306
1670
  end
1307
1671
 
@@ -1310,7 +1674,6 @@ class CATime
1310
1674
  # day) for each element.
1311
1675
  # @return [CArray]
1312
1676
  def ajd
1313
- require 'date'
1314
1677
  to_time.convert(:double) {|t| t.to_datetime.ajd.to_f}
1315
1678
  end
1316
1679
 
@@ -1454,14 +1817,18 @@ class CATimedelta
1454
1817
  # @raise [TypeError] on incompatible operands.
1455
1818
  def +(other)
1456
1819
  given, other = other, CATimeUnitAlgebra.operand(other)
1820
+ other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
1457
1821
  case other
1458
1822
  when CATimedelta
1459
1823
  u = common_duration_unit(other.unit)
1460
1824
  a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
1461
1825
  b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1462
- (a + b).timedelta(unit: u)
1826
+ b = b[0] if other.scalar?
1827
+ CATimeUnitAlgebra.checked_add(a, b).timedelta(unit: u)
1463
1828
  when CATime
1464
- other + self # commutative -> time's unit
1829
+ return other + self unless other.scalar? # commutative -> time's unit
1830
+ d = CATimeUnitAlgebra.duration_on_grid(parent, unit, other.unit)
1831
+ CATimeUnitAlgebra.checked_add(d, other.parent[0]).time(unit: other.unit)
1465
1832
  else
1466
1833
  raise TypeError, "CATimedelta + #{given.class} is not allowed"
1467
1834
  end
@@ -1477,12 +1844,14 @@ class CATimedelta
1477
1844
  # @raise [TypeError] on a non-timedelta operand.
1478
1845
  def -(other)
1479
1846
  given, other = other, CATimeUnitAlgebra.operand(other)
1847
+ other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
1480
1848
  case other
1481
1849
  when CATimedelta
1482
1850
  u = common_duration_unit(other.unit)
1483
1851
  a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
1484
1852
  b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1485
- (a - b).timedelta(unit: u)
1853
+ b = b[0] if other.scalar?
1854
+ CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
1486
1855
  else
1487
1856
  raise TypeError, "CATimedelta - #{given.class} is not allowed"
1488
1857
  end
@@ -1510,15 +1879,16 @@ class CATimedelta
1510
1879
  # @raise [TypeError] when `other` is not an Integer.
1511
1880
  def *(other)
1512
1881
  case other
1513
- when Integer then (parent * other).timedelta(unit: unit)
1882
+ when Integer then CATimeUnitAlgebra.checked_mul(parent, other).timedelta(unit: unit)
1514
1883
  else raise TypeError, "CATimedelta * #{other.class} is not allowed (Integer only)"
1515
1884
  end
1516
1885
  end
1517
1886
 
1518
1887
  # @overload /(other)
1519
1888
  # Returns element-wise division: by Integer yields a
1520
- # {CATimedelta}, by another {CATimedelta} with matching `unit`
1521
- # yields a dimensionless CArray.
1889
+ # {CATimedelta} (truncated toward zero to the unit), by another
1890
+ # {CATimedelta} their ratio as a float64 CArray (90 s / 60 s is 1.5),
1891
+ # the two brought to a common unit first.
1522
1892
  # @param other [Integer, CATimedelta]
1523
1893
  # (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
1524
1894
  # duration array)
@@ -1530,10 +1900,12 @@ class CATimedelta
1530
1900
  when Integer
1531
1901
  CATimeUnitAlgebra.trunc_divide(parent, other).timedelta(unit: unit)
1532
1902
  when CATimedelta
1903
+ # A ratio of durations is a number, not a count of whole ticks: 90 s /
1904
+ # 60 s is 1.5. Both are brought to one unit first, so the ratio does
1905
+ # not depend on the units they are held in.
1533
1906
  u = common_duration_unit(other.unit)
1534
- CATimeUnitAlgebra.trunc_divide(
1535
- CATimeUnitAlgebra.convert_scale!(parent, unit, u),
1536
- CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u))
1907
+ CATimeUnitAlgebra.convert_scale!(parent, unit, u).to_type(:float64) /
1908
+ CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
1537
1909
  else raise TypeError, "CATimedelta / #{given.class} is not allowed"
1538
1910
  end
1539
1911
  end
@@ -1547,15 +1919,40 @@ class CATimedelta
1547
1919
 
1548
1920
  # @overload sum(*args, **opts)
1549
1921
  # Returns the sum of durations as a {Element} for full reduction
1550
- # or as a {CATimedelta} view for per-axis reduction.
1922
+ # or as a {CATimedelta} view for per-axis reduction. The sum is exact:
1923
+ # the unit counts are added in int64, not in float64.
1551
1924
  # @return [Element, CATimedelta]
1925
+ # @raise [RangeError] if a sum does not fit int64 (for a per-axis
1926
+ # reduction, if any of its cells does not).
1552
1927
  def sum(*args, **opts)
1553
- with_surface_fill(opts) { |o| round_and_relift(parent.sum(*args, **o)) }
1928
+ with_surface_fill(opts) { |o|
1929
+ exact_sum_relift(parent.accumulate(*args, **o), parent.sum(*args, **o))
1930
+ }
1554
1931
  end
1555
1932
 
1933
+ # int64 addition is exact modulo 2**64, so the int64 sum is the true sum
1934
+ # whenever the true sum fits int64, whatever order the cells were added
1935
+ # in. Whether it fits is read from the float64 sum, whose error is far
1936
+ # inside the factor of 2 between 2**62 and 2**63.
1937
+ def exact_sum_relift (exact, approx)
1938
+ past = case approx
1939
+ when Numeric then approx.abs > 2.0**62
1940
+ when CArray then (approx.abs > 2.0**62).any
1941
+ else false
1942
+ end
1943
+ raise RangeError, "sum of durations does not fit int64 (in #{unit})" if past
1944
+ case exact
1945
+ when Integer then Element.new(exact, unit)
1946
+ when CArray then exact.timedelta(unit: unit)
1947
+ else exact
1948
+ end
1949
+ end
1950
+ private :exact_sum_relift
1951
+
1556
1952
  # @overload accumulate(*args, **opts)
1557
- # Returns the sum of durations in the storage's own integer type, as
1558
- # {#sum} does in float64.
1953
+ # Returns the sum of durations in the storage's integer type (int64),
1954
+ # added without a range check: a sum past int64 wraps. {#sum} gives
1955
+ # the same answer when it fits and raises RangeError when it does not.
1559
1956
  # @return [Element, CATimedelta]
1560
1957
  def accumulate(*args, **opts)
1561
1958
  with_surface_fill(opts) { |o| round_and_relift(parent.accumulate(*args, **o)) }
@@ -1564,9 +1961,13 @@ class CATimedelta
1564
1961
  # @overload cumsum(axis: nil)
1565
1962
  # Returns the running sum of durations as a {CATimedelta} on `self`'s
1566
1963
  # unit. A masked cell holds the running sum, as {CArray#cumsum} does.
1964
+ # The running sum is exact: the unit counts are added in int64, as
1965
+ # {#sum} adds them.
1567
1966
  # @return [CATimedelta]
1967
+ # @raise [RangeError] if any cell of the running sum does not fit int64.
1568
1968
  def cumsum(*args, **opts)
1569
- round_and_relift(parent.cumsum(*args, **opts))
1969
+ exact_sum_relift(parent.send(:cumsum_in_own_type, *args, **opts),
1970
+ parent.cumsum(*args, **opts))
1570
1971
  end
1571
1972
 
1572
1973
  # @overload prod(*)
@@ -1699,6 +2100,17 @@ class CATimedelta
1699
2100
  [to_unit(u), other.to_unit(u)]
1700
2101
  end
1701
2102
 
2103
+ private def common_unit (a, b)
2104
+ CATimeUnitAlgebra.finer(a, b)
2105
+ end
2106
+
2107
+ # One duration as a length-1 duration array in its own unit, or nil.
2108
+ private def lift_value (operand)
2109
+ if operand.is_a?(CATimedelta::Element)
2110
+ CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
2111
+ end
2112
+ end
2113
+
1702
2114
  def to_comparable (operand)
1703
2115
  case operand
1704
2116
  when CATimedelta
@@ -1708,10 +2120,14 @@ class CATimedelta
1708
2120
  when CATimedelta::Element
1709
2121
  lifted = CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
1710
2122
  to_comparable(lifted)
2123
+ when Array, CArray
2124
+ # Durations written one by one, or an array that is not a duration
2125
+ # array (an object column of elements), read value by value.
2126
+ reconcile_values(operand)
1711
2127
  else
1712
2128
  raise TypeError,
1713
2129
  "CATimedelta cannot reconcile #{operand.class} " \
1714
- "(use ca.parent to compare the raw int64 storage directly)"
2130
+ "(compare #ticks for the raw int64 storage)"
1715
2131
  end
1716
2132
  end
1717
2133
 
@@ -1808,14 +2224,25 @@ module CATimeLiteral
1808
2224
  # "201909" is a valid YYMMDD to Ruby (2020-19-09, which landed on 2021-07),
1809
2225
  # and "2019-02-31" landed on 2019-03-03. A missing finer field is not an
1810
2226
  # error -- a year or a year-month names the head of that period.
2227
+ # %s and %Q give the seconds since the epoch, not the date's fields;
2228
+ # they are spelled out as the UTC date and time they name.
2229
+ def epoch_fields(h)
2230
+ return h unless h && h[:seconds] && !h[:year]
2231
+ t = Time.at(h[:seconds]).utc
2232
+ frac = h[:seconds] - h[:seconds].floor
2233
+ { year: t.year, mon: t.month, mday: t.day, hour: t.hour, min: t.min,
2234
+ sec: t.sec, sec_fraction: frac.zero? ? nil : Rational(frac), leftover: h[:leftover] }.compact
2235
+ end
2236
+
1811
2237
  def parse_date_fields(spec, format)
1812
2238
  h =
1813
- if format then Date._strptime(spec, format)
2239
+ if format then epoch_fields(Date._strptime(spec, format))
1814
2240
  elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
1815
2241
  elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
1816
2242
  elsif (hour = hour_only_fields(spec)) then hour
1817
2243
  else Date._parse(spec)
1818
2244
  end
2245
+ h = resolve_day_of_year(h, spec) if h
1819
2246
  unless h && h[:year]
1820
2247
  raise ArgumentError, "cannot parse time #{spec.inspect}"
1821
2248
  end
@@ -1840,10 +2267,63 @@ module CATimeLiteral
1840
2267
  raise ArgumentError,
1841
2268
  "cannot parse time #{spec.inspect}: hour 24 is only 24:00:00"
1842
2269
  end
2270
+ # A clock field out of range is refused rather than carried into the
2271
+ # next one ("10:75" is not 11:15). Second 60 is the leap second.
2272
+ { hour: 24, min: 59, sec: 60 }.each do |key, top|
2273
+ v = h[key]
2274
+ next if v.nil? || (0..top).cover?(v)
2275
+ raise ArgumentError,
2276
+ "cannot parse time #{spec.inspect}: #{key} #{v} is out of range"
2277
+ end
1843
2278
  h
1844
2279
  end
1845
2280
  private_class_method :parse_date_fields
1846
2281
 
2282
+ # The month and day of a date written by its day of the year ("2024-015",
2283
+ # %j), its ISO week ("2024-W03-1", %G-W%V-%u) or its week of the year
2284
+ # (%U / %W with a weekday). Date._parse and Date._strptime return those
2285
+ # fields as they are written; left alone, the date would fall back to
2286
+ # January 1.
2287
+ def resolve_day_of_year(h, spec)
2288
+ return h if h[:mon] || h[:mday]
2289
+ date =
2290
+ if h[:yday] && h[:year]
2291
+ unless Date.valid_ordinal?(h[:year], h[:yday], Date::GREGORIAN)
2292
+ raise ArgumentError,
2293
+ "cannot parse time #{spec.inspect}: year #{h[:year]} has no day #{h[:yday]}"
2294
+ end
2295
+ Date.ordinal(h[:year], h[:yday], Date::GREGORIAN)
2296
+ elsif h[:cwyear] && h[:cweek]
2297
+ wd = h[:cwday] || 1
2298
+ unless Date.valid_commercial?(h[:cwyear], h[:cweek], wd, Date::GREGORIAN)
2299
+ raise ArgumentError,
2300
+ "cannot parse time #{spec.inspect}: ISO year #{h[:cwyear]} " \
2301
+ "has no week #{h[:cweek]}, day #{wd}"
2302
+ end
2303
+ Date.commercial(h[:cwyear], h[:cweek], wd, Date::GREGORIAN)
2304
+ elsif h[:year] && (h[:wnum0] || h[:wnum1])
2305
+ jan1 = Date.new(h[:year], 1, 1, Date::GREGORIAN)
2306
+ wday = h[:wday] || (h[:cwday] && h[:cwday] % 7)
2307
+ # Week 1 begins on the first Sunday (%U) or Monday (%W) of the year;
2308
+ # the days before it are week 0.
2309
+ if h[:wnum0]
2310
+ first = jan1 + (7 - jan1.wday) % 7
2311
+ d = first + (h[:wnum0] - 1) * 7 + (wday || 0)
2312
+ else
2313
+ first = jan1 + (8 - jan1.wday) % 7
2314
+ d = first + (h[:wnum1] - 1) * 7 + ((wday || 1) + 6) % 7
2315
+ end
2316
+ unless d.year == h[:year]
2317
+ raise ArgumentError,
2318
+ "cannot parse time #{spec.inspect}: the week and weekday fall outside #{h[:year]}"
2319
+ end
2320
+ d
2321
+ end
2322
+ return h unless date
2323
+ h.merge(year: date.year, mon: date.month, mday: date.day)
2324
+ end
2325
+ private_class_method :resolve_day_of_year
2326
+
1847
2327
  # "YYYY-MM-DDThh" with an optional zone ("Z", "+hh", "+hhmm", "+hh:mm").
1848
2328
  def hour_only_fields(spec)
1849
2329
  m = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2})(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?\z/.match(spec)
@@ -1879,8 +2359,6 @@ module CATimeLiteral
1879
2359
  # Exact Rational seconds since the Unix epoch for a start literal (Time /
1880
2360
  # Date / DateTime / Integer unix-seconds / String). UTC default.
1881
2361
  def epoch_seconds(spec, format = nil)
1882
- require 'date'
1883
- require 'time'
1884
2362
  case spec
1885
2363
  when Time then spec.to_r
1886
2364
  when Integer then Rational(spec)
@@ -1917,8 +2395,6 @@ module CATimeLiteral
1917
2395
 
1918
2396
  # [year, month] (UTC) of a start literal, for a calendar-resolution grid.
1919
2397
  def year_month(spec, format = nil)
1920
- require 'date'
1921
- require 'time'
1922
2398
  case spec
1923
2399
  when Time then t = spec.utc; [t.year, t.month]
1924
2400
  when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
@@ -1963,12 +2439,14 @@ module CATimeLiteral
1963
2439
 
1964
2440
  INFER_DATES = %w[%d/%m/%Y %m/%d/%Y %d-%m-%Y %m-%d-%Y %d.%m.%Y %d/%m/%y %m/%d/%y] +
1965
2441
  ["%d %b %Y", "%d-%b-%Y", "%d-%b-%y", "%b %d, %Y", "%b %d %Y", "%Y%m%d"]
2442
+ Ractor.make_shareable(INFER_DATES)
1966
2443
 
1967
2444
  # A space in a format matches any run of spaces, none included.
1968
2445
  INFER_CLOCKS = [
1969
2446
  ["%H:%M", :clock], ["%H:%M:%S", :clock], ["%H:%M:%S.%N", :fraction],
1970
2447
  ["%I:%M %p", :clock], ["%I:%M:%S %p", :clock],
1971
2448
  ].freeze
2449
+ Ractor.make_shareable(INFER_CLOCKS)
1972
2450
 
1973
2451
  # A time after a date: none, or a clock with or without a zone. %z takes
1974
2452
  # an offset, Z, UTC or GMT, not a zone named for a place (some of those
@@ -1976,16 +2454,19 @@ module CATimeLiteral
1976
2454
  INFER_TIMES = ([["", :date]] +
1977
2455
  INFER_CLOCKS.flat_map { |clock, kind| [[" #{clock}", kind], [" #{clock} %z", kind]] }
1978
2456
  ).freeze
2457
+ Ractor.make_shareable(INFER_TIMES)
1979
2458
 
1980
2459
  # A date written in Japanese, year first ("2024<year>1<month>2<day>"),
1981
2460
  # takes a time in the same markers ("3<hour>4<minute>", "...5<second>")
1982
2461
  # or with colons.
1983
2462
  JA_YEAR, JA_MONTH, JA_DAY = "年", "月", "日"
1984
2463
  JA_HOUR, JA_MINUTE, JA_SECOND = "時", "分", "秒"
2464
+ [JA_YEAR, JA_MONTH, JA_DAY, JA_HOUR, JA_MINUTE, JA_SECOND].each { |s| Ractor.make_shareable(s) }
1985
2465
  INFER_JA_TIMES = ([["", :date]] +
1986
2466
  ([["%H#{JA_HOUR}%M#{JA_MINUTE}", :clock],
1987
2467
  ["%H#{JA_HOUR}%M#{JA_MINUTE}%S#{JA_SECOND}", :clock]] + INFER_CLOCKS)
1988
2468
  .map { |clock, kind| [" #{clock}", kind] }).freeze
2469
+ Ractor.make_shareable(INFER_JA_TIMES)
1989
2470
 
1990
2471
  INFER_FORMATS = (
1991
2472
  INFER_DATES.product(INFER_TIMES).map { |date, (time, kind)|
@@ -1995,7 +2476,9 @@ module CATimeLiteral
1995
2476
  TimeFormat.new("%Y#{JA_YEAR}%m#{JA_MONTH}%d#{JA_DAY}#{time}", kind)
1996
2477
  }
1997
2478
  ).freeze
2479
+ Ractor.make_shareable(INFER_FORMATS)
1998
2480
  INFER_FORMAT_STRINGS = INFER_FORMATS.map(&:format).freeze
2481
+ Ractor.make_shareable(INFER_FORMAT_STRINGS)
1999
2482
 
2000
2483
  # The one format of INFER_FORMATS that the text of object array `x` is
2001
2484
  # written in, `first` being its first present cell, as
@@ -2061,8 +2544,7 @@ module CATimeLiteral
2061
2544
  # otherwise each String is parsed into fields here. Either way the fields
2062
2545
  # become ticks for the whole array at once, and a cell that is not a
2063
2546
  # String goes through tick_index.
2064
- def ticks_from_cells(x, res, format, on_error)
2065
- require 'date'
2547
+ def ticks_from_cells(x, res, format, on_error, seconds: false)
2066
2548
  kind, per = tick_kind(res)
2067
2549
  return nil unless kind
2068
2550
  if format.is_a?(String)
@@ -2092,6 +2574,10 @@ module CATimeLiteral
2092
2574
  frac[i] = (h[:sec_fraction] * 1_000_000_000).floor if h[:sec_fraction]
2093
2575
  off[i] = h[:offset] if h[:offset]
2094
2576
  present[i] = 1
2577
+ elsif s.is_a?(Integer) && format.nil?
2578
+ # an Integer counts ticks of the unit given, or seconds when the
2579
+ # unit is chosen from the values
2580
+ direct[i] = seconds ? tick_index(s, res) : s
2095
2581
  else
2096
2582
  direct[i] = tick_index(s, res, format)
2097
2583
  end
@@ -2139,6 +2625,80 @@ module CATimeLiteral
2139
2625
  raise RangeError, "time #{cells[i].inspect} does not fit int64 ticks of #{res}"
2140
2626
  end
2141
2627
 
2628
+ # The unit CArray.time reads +x+ in when none is given: the finest any of
2629
+ # its values shows (see the unit: of CArray.time), :s when none shows one.
2630
+ def unit_shown(x, format)
2631
+ cells = x.is_a?(CArray) ? x.flatten.to_a : [x]
2632
+ text = cells.select { |v| v.is_a?(String) }
2633
+ units = []
2634
+ unless text.empty?
2635
+ if format
2636
+ units << format_unit(format)
2637
+ else
2638
+ unreadable = []
2639
+ _, read = CA_OBJECT(text).__parse_time_text__(nil, unreadable, true)
2640
+ units << read if read
2641
+ unreadable.each do |i|
2642
+ u = text_unit(text[i])
2643
+ units << u if u
2644
+ end
2645
+ end
2646
+ end
2647
+ cells.each do |v|
2648
+ case v
2649
+ when Integer then units << :s
2650
+ when CATime::Element then units << v.unit
2651
+ when Time then units << CATimeLiteral.exact_instant(v).unit
2652
+ when Date
2653
+ units << (defined?(DateTime) && v.is_a?(DateTime) ? CATimeLiteral.exact_instant(v.to_time).unit : :D)
2654
+ end
2655
+ end
2656
+ # A month beside a day is read on the fixed grid both instants reach,
2657
+ # as two elements are compared (diff_unit); finer covers one group only.
2658
+ units.map { |u| CATime::Resolution.parse(u) }
2659
+ .reduce { |a, b| CATimeUnitAlgebra.diff_unit(a, b) } || CATime::Resolution.parse(:s)
2660
+ end
2661
+
2662
+ # The unit a String read with Date._parse shows, or nil when it does not
2663
+ # read (the reading raises or masks it later).
2664
+ def text_unit(str)
2665
+ h = Date._parse(str)
2666
+ return nil unless h && h[:year]
2667
+ return fraction_unit(h[:sec_fraction].denominator) if h[:sec_fraction]
2668
+ (h[:hour] || h[:min] || h[:sec]) ? :s : :D
2669
+ end
2670
+
2671
+ # The finest field a strptime format writes.
2672
+ def format_unit(format)
2673
+ if (m = format.match(/%(\d*)([LNQ])/))
2674
+ return :ms if m[2] == "L" || m[2] == "Q"
2675
+ digits = m[1].empty? ? 9 : m[1].to_i
2676
+ return fraction_unit(10 ** digits)
2677
+ end
2678
+ format.match?(/%[-_0^#]*[sSTHIklMRrXcp]/) ? :s : :D
2679
+ end
2680
+
2681
+ def fraction_unit(denominator)
2682
+ digits = Math.log10(denominator).ceil
2683
+ digits <= 3 ? :ms : digits <= 6 ? :us : :ns
2684
+ end
2685
+
2686
+ # +x+ with each Integer replaced by its decimal digits as a String, for a
2687
+ # format to read; a masked cell stays masked.
2688
+ def integers_as_text(x)
2689
+ case x
2690
+ when Integer then x.to_s
2691
+ when CArray
2692
+ return x if x.face? || !(x.integer? || x.data_type == CA_OBJECT)
2693
+ text = CArray.object(*x.shape)
2694
+ cells = x.flatten.to_a
2695
+ cells.each_with_index { |v, i| text[i] = v.is_a?(Integer) ? v.to_s : v }
2696
+ text = text.reshape(*x.shape) if x.ndim > 1
2697
+ text
2698
+ else x
2699
+ end
2700
+ end
2701
+
2142
2702
  # Single-literal build for {.time}: a 1-element CATime, honouring
2143
2703
  # the on_error policy (raise, or a masked cell).
2144
2704
  def to_time_array(literal, res, format, on_error)
@@ -2218,10 +2778,13 @@ class CArray
2218
2778
 
2219
2779
  # @overload time(x, unit: :s, format: nil, on_error: :raise)
2220
2780
  # Builds a {CATime} on the `unit` grid from time value(s). `x`
2221
- # is either a single literal (Time / ISO 8601 String / Unix-seconds
2222
- # Integer / DateTime) — giving a 1-element result — or a CArray of such
2781
+ # is either a single literal (Time / ISO 8601 String / Integer /
2782
+ # DateTime) — giving a 1-element result — or a CArray of such
2223
2783
  # literals — giving a same-shape result parsed per cell. Parsing is UTC
2224
- # and DateTime-independent.
2784
+ # and DateTime-independent. An Integer counts ticks of `unit` from
2785
+ # the epoch, so it is Unix seconds when `unit` is not given (also among
2786
+ # values that make the chosen unit finer):
2787
+ # `CArray.time(CA_INT64([0, 10]), unit: :m)` is 00:00 and 00:10.
2225
2788
  #
2226
2789
  # A value that cannot be parsed raises by default (`on_error: :raise`);
2227
2790
  # pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
@@ -2229,8 +2792,16 @@ class CArray
2229
2792
  # becomes UNDEF, regardless of `on_error`.
2230
2793
  # @param x [Time, String, Integer, DateTime, CATime::Element, Array, CArray] a literal, a
2231
2794
  # Ruby Array of literals, or a CArray of literals.
2232
- # @param unit [Resolution, Symbol, String] target grid resolution.
2795
+ # @param unit [Resolution, Symbol, String, nil] target grid resolution;
2796
+ # by default the finest the values show, so nothing they hold is cut
2797
+ # off: text written to the day is `:D`, to the second `:s`, with a
2798
+ # fraction `:ms` / `:us` / `:ns` by its digits (as
2799
+ # `CAFrame#parse_to_time` reads text); with a format, the finest field
2800
+ # the format writes; an Integer `:s`; a Time the finest
2801
+ # its value needs; a Date `:D`; an element its own unit.
2233
2802
  # @param format [String, nil] optional strptime format for String input.
2803
+ # With a format, an Integer is read as its decimal digits
2804
+ # (`CArray.time(20240101, format: "%Y%m%d")`), not as a count of ticks.
2234
2805
  # @param on_error [:raise, :mask] parse-failure policy (default `:raise`).
2235
2806
  # @return [CATime] shape `[1]` for a literal, else `x`'s shape.
2236
2807
  # @raise [ArgumentError] on an unparseable value when `on_error: :raise`.
@@ -2239,16 +2810,25 @@ class CArray
2239
2810
  # CArray.time("2024-06-15", unit: :D) # 1-element
2240
2811
  # CArray.time(%w[2024-01-01 2024-02-01], unit: :D) # Ruby Array
2241
2812
  # CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)
2242
- def self.time(x, unit: :s, format: nil, on_error: :raise)
2243
- res = CATime::Resolution.parse(unit)
2813
+ def self.time(x, unit: nil, format: nil, on_error: :raise)
2244
2814
  unless %i[raise mask].include?(on_error)
2245
2815
  raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
2246
2816
  end
2247
2817
  x = CA_OBJECT(x) if x.is_a?(Array) # Ruby Array of literals -> object CArray
2818
+ # With a format, an Integer is the digits it is written with (20240101
2819
+ # read as "%Y%m%d"), not a count of ticks: a format describes text.
2820
+ x = CATimeLiteral.integers_as_text(x) if format
2821
+ res = unit ? CATime::Resolution.parse(unit) : CATimeLiteral.unit_shown(x, format)
2822
+ # Without a format, an Integer counts ticks of the unit from the epoch:
2823
+ # seconds by default, minutes with unit: :m.
2824
+ if format.nil?
2825
+ return CA_INT64([x]).time(unit: res) if x.is_a?(Integer)
2826
+ return (x.data_type == CA_INT64 ? x.copy : x).time(unit: res) if x.is_a?(CArray) && !x.face? && x.integer?
2827
+ end
2248
2828
  unless x.is_a?(CArray)
2249
2829
  return CATimeLiteral.to_time_array(x, res, format, on_error)
2250
2830
  end
2251
- raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error)
2831
+ raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error, seconds: unit.nil?)
2252
2832
  return raw.time(unit: res) if raw
2253
2833
  raw = CArray.int64(*x.shape)
2254
2834
  x.each_index do |*idx|
@@ -2258,7 +2838,7 @@ class CArray
2258
2838
  next
2259
2839
  end
2260
2840
  begin
2261
- raw[*idx] = CATimeLiteral.tick_index(s, res, format)
2841
+ raw[*idx] = s.is_a?(Integer) && format.nil? && unit ? s : CATimeLiteral.tick_index(s, res, format)
2262
2842
  rescue ArgumentError, TypeError
2263
2843
  raise if on_error == :raise
2264
2844
  raw[*idx] = UNDEF # opt-in parse-mask
@@ -2275,8 +2855,10 @@ class CArray
2275
2855
  # int64 array is built; the origin is not stored).
2276
2856
  # @param unit [Resolution, Symbol, String] grid resolution.
2277
2857
  # @param origin [Time, String, Integer, DateTime, CATime::Element, nil] base instant that
2278
- # `self`'s indices are counted from (default: the Unix epoch).
2858
+ # `self`'s indices are counted from (default: the Unix epoch); an
2859
+ # Integer is Unix seconds. It has to be a point of the `unit` grid.
2279
2860
  # @return [CATime]
2861
+ # @raise [ArgumentError] when `origin` falls between two ticks of `unit`.
2280
2862
  # @note An `int64` receiver is wrapped zero-copy. A narrower integer type
2281
2863
  # is widened to `int64` first (a copy); a `Float` / non-integer type
2282
2864
  # raises (cast it explicitly if the truncation is intended).
@@ -2286,8 +2868,11 @@ class CArray
2286
2868
  if origin.nil?
2287
2869
  CATime.wrap(src, unit: res)
2288
2870
  else
2289
- o = CATimeLiteral.tick_index(origin, res)
2290
- (src + o).time(unit: res)
2871
+ # The origin is a point of the grid: one between two ticks has no
2872
+ # index, so it raises rather than moving to the tick before it.
2873
+ origin = Time.at(origin, in: "UTC") if origin.is_a?(Integer)
2874
+ o = CATimeGrid.resolve_origin_ticks(origin, res, res)
2875
+ CATimeUnitAlgebra.checked_add(src, o).time(unit: res)
2291
2876
  end
2292
2877
  end
2293
2878
 
@@ -2313,6 +2898,13 @@ class CArray
2313
2898
  "array cannot be wrapped (cast it explicitly, e.g. " \
2314
2899
  "arr.int64.time(...), if a lossy conversion is intended)"
2315
2900
  end
2901
+ if data_type == CA_UINT64
2902
+ hi = max
2903
+ if hi != UNDEF && hi > 2**63 - 1
2904
+ raise RangeError,
2905
+ "CATime / CATimedelta storage is int64; #{hi} does not fit"
2906
+ end
2907
+ end
2316
2908
  int64
2317
2909
  end
2318
2910
  private :as_int64_time_storage
@@ -2646,6 +3238,7 @@ class CATime
2646
3238
  m: "minutes", s: "seconds", ms: "milliseconds",
2647
3239
  us: "microseconds", ns: "nanoseconds", ps: "picoseconds",
2648
3240
  fs: "femtoseconds", as: "attoseconds" }.freeze
3241
+ Ractor.make_shareable(WORD)
2649
3242
 
2650
3243
  # Storage resolutions to fall back on when the unit's own tick cannot
2651
3244
  # hold the origin exactly (a day grid anchored at 12:00 needs seconds).
@@ -2682,7 +3275,8 @@ class CATime
2682
3275
  @unit = Resolution.parse(unit)
2683
3276
  @origin = origin
2684
3277
  @storage = resolve_storage
2685
- @origin = CArray.time(origin, unit: @storage)[0] if origin
3278
+ # An Integer origin is an instant in Unix seconds, whatever the unit.
3279
+ @origin = CATimeLiteral.to_time_array(origin, @storage, nil, :raise)[0] if origin
2686
3280
  freeze
2687
3281
  end
2688
3282
 
@@ -2884,6 +3478,34 @@ class CATime
2884
3478
  snap(grid, unit: unit, origin: origin, direction: :floor)
2885
3479
  end
2886
3480
 
3481
+ # @overload end_of(grid = nil, unit:, origin: nil)
3482
+ # Each element at the last tick of its bucket, on the array's own grid:
3483
+ # the bucket {#floor} gives the head of, one tick before the next head.
3484
+ # A day array's month ends on the month's last day, a second array's at
3485
+ # 23:59:59 of it:
3486
+ #
3487
+ # CArray.time(%w[2024-02-10 2024-03-31], unit: :D).end_of(unit: :M)
3488
+ # # => 2024-02-29, 2024-03-31
3489
+ #
3490
+ # The bucket has to be a whole number of the array's ticks, since its
3491
+ # end is counted on that grid.
3492
+ # @return [CATime]
3493
+ # @raise [ArgumentError] when the bucket is finer than the grid or not a
3494
+ # whole number of its ticks.
3495
+ def end_of(grid = nil, unit: nil, origin: nil)
3496
+ step, origin = Grid.resolve(grid, unit, origin)
3497
+ step = CATime::Resolution.parse(step)
3498
+ per = CATimeUnitAlgebra.ratio(step, self.unit) # ticks of the grid per bucket
3499
+ if per && !(per.denominator == 1 && per >= 1)
3500
+ raise ArgumentError,
3501
+ "end_of: a bucket of #{step} is not a whole number of the #{self.unit} " \
3502
+ "grid's ticks, so its last tick is not on the grid"
3503
+ end
3504
+ head = floor(unit: step, origin: origin)
3505
+ head + CATimedelta::Element.new(step.count, step.base) -
3506
+ CATimedelta::Element.new(self.unit.count, self.unit.base)
3507
+ end
3508
+
2887
3509
  # @overload ceil(grid = nil, unit:, origin: nil)
2888
3510
  # {#snap} with `direction: :ceil` -- each element at the bucket head at
2889
3511
  # or after it (an element already on a boundary maps to itself).