carray 3.0.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +568 -0
- data/README.md +6 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +220 -0
- data/ext/ca_axis_group.c +201 -123
- data/ext/ca_binop_dispatch.c +46 -52
- data/ext/ca_binop_dispatch.h +1 -1
- data/ext/ca_categorical_iterator.c +249 -183
- data/ext/ca_compare.c +7 -12
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +38 -23
- data/ext/ca_kernel_iterator.c +1765 -2076
- data/ext/ca_kernel_iterator.h +584 -566
- data/ext/ca_monop_dispatch.c +86 -63
- data/ext/ca_monop_dispatch.h +5 -0
- data/ext/ca_obj_array.c +223 -56
- data/ext/ca_obj_bincmp.c +172 -138
- data/ext/ca_obj_binop.c +116 -230
- data/ext/ca_obj_bitarray.c +40 -26
- data/ext/ca_obj_bitfield.c +100 -68
- data/ext/ca_obj_block.c +19 -9
- data/ext/ca_obj_byte_swap.c +45 -46
- data/ext/ca_obj_const_string.c +269 -44
- data/ext/ca_obj_face.c +612 -89
- data/ext/ca_obj_face.h +111 -47
- data/ext/ca_obj_fake.c +36 -79
- data/ext/ca_obj_farray.c +7 -3
- data/ext/ca_obj_field.c +33 -8
- data/ext/ca_obj_fixlen_string.c +23 -24
- data/ext/ca_obj_grid.c +541 -210
- data/ext/ca_obj_meld.c +331 -111
- data/ext/ca_obj_moncmp.c +80 -65
- data/ext/ca_obj_monop.c +177 -149
- data/ext/ca_obj_object.c +664 -248
- data/ext/ca_obj_record.c +63 -23
- data/ext/ca_obj_reduce.c +18 -16
- data/ext/ca_obj_refer.c +140 -58
- data/ext/ca_obj_remap.c +85 -24
- data/ext/ca_obj_repeat.c +24 -9
- data/ext/ca_obj_roll.c +121 -16
- data/ext/ca_obj_select.c +61 -58
- data/ext/ca_obj_select_axis.c +73 -107
- data/ext/ca_obj_shift.c +32 -102
- data/ext/ca_obj_stack.c +372 -122
- data/ext/ca_obj_stride.c +348 -140
- data/ext/ca_obj_string.c +88 -22
- data/ext/ca_obj_tile.c +38 -21
- data/ext/ca_obj_time.c +45 -31
- data/ext/ca_obj_timedelta.c +43 -29
- data/ext/ca_obj_transpose.c +17 -6
- data/ext/ca_obj_triop.c +125 -141
- data/ext/ca_obj_window.c +330 -44
- data/ext/ca_op_ipower.c +24 -11
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +104 -81
- data/ext/ca_sweep_engine.h +7 -20
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +542 -0
- data/ext/carray.h +241 -92
- data/ext/carray_access.c +608 -276
- data/ext/carray_address_basis.c +494 -97
- data/ext/carray_attribute.c +129 -4
- data/ext/carray_bincount.c +39 -7
- data/ext/carray_broadcast.c +56 -19
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_call_cfunc.c +108 -28
- data/ext/carray_cast.c +1576 -223
- data/ext/carray_cast_func.rb +77 -24
- data/ext/carray_class.c +4 -3
- data/ext/carray_conversion.c +208 -75
- data/ext/carray_copy.c +82 -24
- data/ext/carray_core.c +646 -162
- data/ext/carray_count.c +171 -88
- data/ext/carray_element.c +94 -60
- data/ext/carray_factorize.c +670 -324
- data/ext/carray_generate.c +260 -83
- data/ext/carray_histogram.c +209 -106
- data/ext/carray_hold.c +14 -16
- data/ext/carray_index_classifier.c +422 -243
- data/ext/carray_internal.h +692 -3
- data/ext/carray_kernels_bincmp.c +7 -0
- data/ext/carray_kernels_binop.c +487 -120
- data/ext/carray_kernels_init.c +653 -50
- data/ext/carray_kernels_moncmp.c +15 -8
- data/ext/carray_kernels_monop.c +99 -43
- data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
- data/ext/carray_kernels_reduce_boolean.c +112 -36
- data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
- data/ext/carray_kernels_reduce_extreme.c +3032 -2892
- data/ext/carray_kernels_reduce_variance.c +439 -500
- data/ext/carray_kernels_scan.c +1097 -488
- data/ext/carray_kernels_search.c +3787 -2176
- data/ext/carray_kernels_sort.c +1200 -920
- data/ext/carray_kernels_triop.c +479 -7
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +967 -153
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +910 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +299 -100
- data/ext/carray_memory_view.c +872 -758
- data/ext/carray_operator.c +1171 -364
- data/ext/carray_order.c +505 -133
- data/ext/carray_partition.c +112 -65
- data/ext/carray_random.c +327 -239
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +346 -78
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +249 -86
- data/ext/carray_sort.c +224 -227
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +118 -39
- data/ext/carray_test.c +193 -69
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +579 -113
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +19 -4
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1202 -1014
- data/ext/ruby_carray.c +17 -5
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +160 -17
- data/lib/carray/autoload_carray.rb +23 -27
- data/lib/carray/autoload_method_extension.rb +20 -5
- data/lib/carray/axis_group.rb +360 -63
- data/lib/carray/basics.rb +162 -28
- data/lib/carray/bincount_nd.rb +18 -2
- data/lib/carray/block_iterator.rb +231 -58
- data/lib/carray/boolean_reduce.rb +34 -42
- data/lib/carray/categorical.rb +242 -52
- data/lib/carray/categorical_iterator.rb +437 -827
- data/lib/carray/complex.rb +41 -34
- data/lib/carray/conditional.rb +150 -14
- data/lib/carray/const_string.rb +59 -4
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +82 -7
- data/lib/carray/frame/csv_parser.rb +688 -87
- data/lib/carray/frame/describe.rb +80 -0
- data/lib/carray/frame/frame.rb +369 -45
- data/lib/carray/frame/group.rb +631 -90
- data/lib/carray/frame/io.rb +506 -79
- data/lib/carray/frame/join.rb +411 -62
- data/lib/carray/frame/records.rb +116 -19
- data/lib/carray/frame/reshape.rb +455 -0
- data/lib/carray/frame/sort.rb +30 -18
- data/lib/carray/frame/verbs.rb +868 -61
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +4 -0
- data/lib/carray/fuse_source.rb +118 -2
- data/lib/carray/fusion.rb +257 -42
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +54 -13
- data/lib/carray/iterator.rb +99 -9
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +216 -193
- data/lib/carray/mask_gap_fill.rb +112 -92
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +52 -21
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +18 -2
- data/lib/carray/methods/bit_string.rb +15 -4
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +18 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +25 -5
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +140 -10
- data/lib/carray/methods/join.rb +2 -7
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +182 -45
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/repeat.rb +1 -20
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +11 -2
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +65 -17
- data/lib/carray/segment_iterator.rb +1221 -0
- data/lib/carray/serialize.rb +214 -30
- data/lib/carray/slab_iterator.rb +101 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +12 -4
- data/lib/carray/struct.rb +137 -58
- data/lib/carray/struct_builder.rb +124 -17
- data/lib/carray/time.rb +1222 -186
- data/lib/carray/window_iterator.rb +242 -81
- data/lib/carray.rb +9 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +5 -1
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +5 -4
- data/yard-stubs/ca_obj_roll.rb +2 -3
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +9 -5
- data/yard-stubs/carray_access.rb +11 -5
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +88 -43
- data/yard-stubs/carray_conversion.rb +15 -2
- data/yard-stubs/carray_copy.rb +12 -5
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +60 -12
- data/yard-stubs/carray_math.rb +52 -14
- data/yard-stubs/carray_median_percentile.rb +15 -8
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +61 -29
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +21 -9
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +10 -2
- data/yard-stubs/carray_sort.rb +49 -8
- data/yard-stubs/carray_test.rb +15 -3
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -5
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_kernels_map.c +0 -3467
- data/lib/carray/arrow_tensor.rb +0 -401
- 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
|
-
#
|
|
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
|
|
|
@@ -93,6 +100,53 @@ module CATimeUnitAlgebra
|
|
|
93
100
|
|
|
94
101
|
module_function
|
|
95
102
|
|
|
103
|
+
# An operand of a time or duration operator, taken through its to_ca when
|
|
104
|
+
# it exports MemoryView and defines one (an Apache Arrow duration array, given
|
|
105
|
+
# a bridge that adds to_ca), as the core binary operators take it; other
|
|
106
|
+
# objects are returned as they are, for the operator to accept or refuse.
|
|
107
|
+
def operand(other)
|
|
108
|
+
if !other.is_a?(CArray) && other.respond_to?(:to_ca) &&
|
|
109
|
+
CArray.memory_view_available?(other)
|
|
110
|
+
other.to_ca
|
|
111
|
+
else
|
|
112
|
+
other
|
|
113
|
+
end
|
|
114
|
+
end
|
|
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
|
+
|
|
96
150
|
# seconds- (fixed) or months- (calendar) per base tick.
|
|
97
151
|
def base_ratio(base)
|
|
98
152
|
FIXED[base] || CALENDAR[base]
|
|
@@ -208,7 +262,7 @@ module CATimeUnitAlgebra
|
|
|
208
262
|
# fine->coarse divides only when every value is exact; cross-group ALWAYS
|
|
209
263
|
# raises -- a :M / :Y duration has no fixed ratio to days (a month is
|
|
210
264
|
# calendar-variable), so it genuinely cannot scale to seconds.
|
|
211
|
-
def convert_scale!(storage, from, to)
|
|
265
|
+
def convert_scale!(storage, from, to, instant: false)
|
|
212
266
|
a = res(from); b = res(to)
|
|
213
267
|
return storage if a == b
|
|
214
268
|
r = ratio(a, b)
|
|
@@ -221,6 +275,11 @@ module CATimeUnitAlgebra
|
|
|
221
275
|
scaled = widen(storage, r.numerator) # coarse -> fine: lossless multiply
|
|
222
276
|
return scaled if r.denominator == 1
|
|
223
277
|
unless (scaled % r.denominator).eq(0).all
|
|
278
|
+
if instant
|
|
279
|
+
raise ArgumentError,
|
|
280
|
+
"cannot convert times in #{a} to #{b} without loss: " \
|
|
281
|
+
"some are not on the #{b} grid (they would be moved to it)"
|
|
282
|
+
end
|
|
224
283
|
raise ArgumentError,
|
|
225
284
|
"cannot scale duration #{a} to #{b} without loss: " \
|
|
226
285
|
"some values are not a whole multiple of #{b} " \
|
|
@@ -236,7 +295,8 @@ module CATimeUnitAlgebra
|
|
|
236
295
|
# CATimedelta#to_unit and by dt +/- td, where the result keeps the time's
|
|
237
296
|
# unit and a finer duration is truncated to it (a :D time + a 5 h duration
|
|
238
297
|
# is + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
|
|
239
|
-
# 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).
|
|
240
300
|
def convert_scale_trunc(storage, from, to)
|
|
241
301
|
a = res(from); b = res(to)
|
|
242
302
|
return storage if a == b
|
|
@@ -251,6 +311,67 @@ module CATimeUnitAlgebra
|
|
|
251
311
|
r.denominator == 1 ? scaled : trunc_divide(scaled, r.denominator)
|
|
252
312
|
end
|
|
253
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
|
+
|
|
254
375
|
# Truncating integer division of an int64 CArray (toward zero). CArray
|
|
255
376
|
# `/` floors toward the past, which is what an instant wants; a duration
|
|
256
377
|
# is a magnitude and shrinks toward zero instead.
|
|
@@ -271,7 +392,7 @@ module CATimeUnitAlgebra
|
|
|
271
392
|
def convert_instant!(storage, from, to)
|
|
272
393
|
a = res(from); b = res(to)
|
|
273
394
|
return storage if a == b
|
|
274
|
-
return convert_scale!(storage, a, b) if ratio(a, b) # same group
|
|
395
|
+
return convert_scale!(storage, a, b, instant: true) if ratio(a, b) # same group
|
|
275
396
|
if CALENDAR.key?(a.base)
|
|
276
397
|
convert_instant_calendar_to_fixed(storage, a, b) # widen
|
|
277
398
|
else
|
|
@@ -279,6 +400,40 @@ module CATimeUnitAlgebra
|
|
|
279
400
|
end
|
|
280
401
|
end
|
|
281
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
|
+
|
|
282
437
|
# calendar time (Resolution `from`) -> days since the epoch (int64
|
|
283
438
|
# CArray). Folds the resolution count (value = count-Y/M buckets).
|
|
284
439
|
def calendar_days_since_epoch(storage, from)
|
|
@@ -307,25 +462,29 @@ module CATimeUnitAlgebra
|
|
|
307
462
|
widen(calendar_days_since_epoch(storage, from), r.numerator)
|
|
308
463
|
end
|
|
309
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
|
+
|
|
310
476
|
# fixed-length grid -> calendar time: the coarsening half of
|
|
311
477
|
# convert_instant!, exact-or-raise. Every instant must land on a day
|
|
312
478
|
# boundary and then on the calendar boundary itself (the 1st, and
|
|
313
479
|
# January too for :Y), since a mid-month instant has no :M value.
|
|
314
480
|
def convert_instant_fixed_to_calendar(storage, from, to)
|
|
315
|
-
|
|
316
|
-
days
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"(instant is not on a day boundary)"
|
|
323
|
-
end
|
|
324
|
-
storage / n
|
|
325
|
-
else # coarser than a day (:W)
|
|
326
|
-
storage * (from.tick_ratio / 86400r).to_i
|
|
327
|
-
end
|
|
328
|
-
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])
|
|
329
488
|
on_boundary = d.eq(1)
|
|
330
489
|
on_boundary &= m.eq(1) if to.base == :Y
|
|
331
490
|
unless on_boundary.all
|
|
@@ -372,20 +531,110 @@ module CATimeUnitAlgebra
|
|
|
372
531
|
# month, whatever day and time it carries. The flooring counterpart of
|
|
373
532
|
# convert_instant_fixed_to_calendar.
|
|
374
533
|
def convert_instant_fixed_to_calendar_floor(storage, from, to)
|
|
375
|
-
|
|
376
|
-
days =
|
|
377
|
-
if rd.denominator == 1
|
|
378
|
-
storage / rd.numerator
|
|
379
|
-
else # coarser than a day (:W)
|
|
380
|
-
storage * (from.tick_ratio / 86400r).to_i
|
|
381
|
-
end
|
|
382
|
-
y, m, = CATimeCivil.civil_from_days(days)
|
|
534
|
+
y, m, = CATimeCivil.civil_from_days(fixed_storage_days(storage, from)[:days])
|
|
383
535
|
ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
|
|
384
536
|
to.count > 1 ? ord / to.count : ord
|
|
385
537
|
end
|
|
386
538
|
end
|
|
387
539
|
|
|
540
|
+
# A reduction of a time Face takes `fill_value:` as a value of the Face (an
|
|
541
|
+
# Element, or what an assignment to the Face accepts), the same as
|
|
542
|
+
# `strip_mask` and `[]=` read it. The reduction runs on the storage, so the
|
|
543
|
+
# fill goes in after the result is a Face again: a whole reduction keeps its
|
|
544
|
+
# axes for that, so the fill lands in a typed cell, and is then taken out.
|
|
545
|
+
module CATimeSurfaceFill
|
|
546
|
+
|
|
547
|
+
private
|
|
548
|
+
|
|
549
|
+
def with_surface_fill (opts)
|
|
550
|
+
fill = opts[:fill_value]
|
|
551
|
+
return yield(opts) if fill.nil? || fill.equal?(UNDEF)
|
|
552
|
+
o = opts.reject { |k, _| k == :fill_value }
|
|
553
|
+
o[:keep_axis] = true
|
|
554
|
+
r = yield(o)
|
|
555
|
+
kept = nil
|
|
556
|
+
unless opts[:keep_axis]
|
|
557
|
+
axes = opts[:axis].nil? ? (0...ndim).to_a :
|
|
558
|
+
Array(opts[:axis]).map { |a| a < 0 ? a + ndim : a }
|
|
559
|
+
kept = shape.each_with_index.reject { |_, i| axes.include?(i) }.map(&:first)
|
|
560
|
+
end
|
|
561
|
+
surface_fill(r, fill, kept)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# `kept` is the shape to return to, or nil to keep the reduced axes.
|
|
565
|
+
def surface_fill (r, fill, kept)
|
|
566
|
+
case r
|
|
567
|
+
when Array then r.map { |x| surface_fill(x, fill, kept) }
|
|
568
|
+
when CArray
|
|
569
|
+
r = r.strip_mask(fill) if r.has_mask?
|
|
570
|
+
if kept.nil? then r
|
|
571
|
+
elsif kept.empty? then r[*Array.new(r.ndim, 0)]
|
|
572
|
+
else r.reshape(*kept)
|
|
573
|
+
end
|
|
574
|
+
else r
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
end
|
|
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
|
+
|
|
388
635
|
class CATime
|
|
636
|
+
include CATimeSurfaceFill
|
|
637
|
+
include CATimeOperandValues
|
|
389
638
|
# @overload new(*shape, unit: :ns)
|
|
390
639
|
# Allocates a new int64 storage CArray of the given `shape` and
|
|
391
640
|
# wraps it as a CATime Face with the given `unit`. The
|
|
@@ -406,7 +655,7 @@ class CATime
|
|
|
406
655
|
# @param unit [Resolution, Symbol, String] tick resolution.
|
|
407
656
|
# @return [CATime]
|
|
408
657
|
def self.wrap(raw, unit: :ns)
|
|
409
|
-
|
|
658
|
+
wrap_storage(raw, unit)
|
|
410
659
|
end
|
|
411
660
|
|
|
412
661
|
# Element-return class: a single time element carries both its
|
|
@@ -438,8 +687,6 @@ class CATime
|
|
|
438
687
|
# @return [Time] the instant this scalar denotes (UTC). For a calendar
|
|
439
688
|
# unit this is the first instant (midnight of day 1) of the granule.
|
|
440
689
|
def to_time
|
|
441
|
-
require 'time'
|
|
442
|
-
require 'date'
|
|
443
690
|
case unit.base
|
|
444
691
|
when :Y then d = epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
445
692
|
when :M then d = epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
@@ -451,7 +698,6 @@ class CATime
|
|
|
451
698
|
# @return [Date] the date this scalar denotes (UTC). A sub-day unit is
|
|
452
699
|
# floored to its day.
|
|
453
700
|
def to_date
|
|
454
|
-
require 'date'
|
|
455
701
|
case unit.base
|
|
456
702
|
when :Y then epoch_date.next_year(value * unit.count)
|
|
457
703
|
when :M then epoch_date.next_month(value * unit.count)
|
|
@@ -472,7 +718,7 @@ class CATime
|
|
|
472
718
|
# @return [String]
|
|
473
719
|
def to_s
|
|
474
720
|
case unit.base
|
|
475
|
-
when :Y then
|
|
721
|
+
when :Y then to_date.strftime("%Y")
|
|
476
722
|
when :M then to_date.strftime("%Y-%m")
|
|
477
723
|
when :W, :D then to_date.strftime("%Y-%m-%d")
|
|
478
724
|
else to_time.iso8601(fractional_second_digits) # :h .. :as (time shown)
|
|
@@ -500,6 +746,10 @@ class CATime
|
|
|
500
746
|
to_time <=> other.getutc
|
|
501
747
|
when (defined?(DateTime) ? DateTime : nil)
|
|
502
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)
|
|
503
753
|
end
|
|
504
754
|
rescue ArgumentError
|
|
505
755
|
nil
|
|
@@ -517,18 +767,27 @@ class CATime
|
|
|
517
767
|
[value, unit].hash
|
|
518
768
|
end
|
|
519
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
|
+
|
|
520
778
|
# @overload +(td)
|
|
521
779
|
# Returns this instant advanced by a {CATimedelta::Element}. The time
|
|
522
780
|
# is the anchor: the result keeps `self`'s unit and the duration is
|
|
523
781
|
# converted into it, truncated toward zero when finer (a :D time + a
|
|
524
782
|
# 5 h duration is + 0 days; + 30 h is + 1 day) -- the same rule as the
|
|
525
|
-
# array {CATime#+}.
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
#
|
|
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.
|
|
529
787
|
# @param td [CATimedelta::Element]
|
|
530
788
|
# @return [CATime::Element]
|
|
531
|
-
# @raise [TypeError, ArgumentError] on a non-timedelta
|
|
789
|
+
# @raise [TypeError, ArgumentError] on a non-timedelta operand, or a
|
|
790
|
+
# pair that has no answer on self's grid.
|
|
532
791
|
def +(td)
|
|
533
792
|
shifted_by_duration(td, 1)
|
|
534
793
|
end
|
|
@@ -544,9 +803,13 @@ class CATime
|
|
|
544
803
|
case other
|
|
545
804
|
when Element
|
|
546
805
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
549
810
|
shifted_by_duration(other, -1)
|
|
811
|
+
when CATime
|
|
812
|
+
-(other - self)
|
|
550
813
|
else
|
|
551
814
|
raise TypeError, "CATime::Element - #{other.class} is not allowed"
|
|
552
815
|
end
|
|
@@ -565,22 +828,33 @@ class CATime
|
|
|
565
828
|
|
|
566
829
|
# s +/- td: the time is the anchor, so the result keeps self's unit and
|
|
567
830
|
# the duration is converted into it (truncated toward zero when finer; a
|
|
568
|
-
#
|
|
831
|
+
# month or year duration on a fixed grid moves the date by months).
|
|
832
|
+
# Mirrors the array CATime +/-.
|
|
569
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)
|
|
570
836
|
unless td.is_a?(CATimedelta::Element)
|
|
571
837
|
raise TypeError,
|
|
572
838
|
"CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
|
|
573
839
|
"allowed (add / subtract a CATimedelta)"
|
|
574
840
|
end
|
|
575
|
-
|
|
576
|
-
|
|
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)
|
|
577
852
|
end
|
|
578
853
|
|
|
579
854
|
# Base epoch as a Date. next_month / next_year clamp end-of-month
|
|
580
855
|
# (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
|
|
581
856
|
# do not move the base off the first of the month without revisiting.
|
|
582
857
|
def epoch_date
|
|
583
|
-
require 'date'
|
|
584
858
|
Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
|
|
585
859
|
end
|
|
586
860
|
|
|
@@ -602,6 +876,8 @@ class CATime
|
|
|
602
876
|
end
|
|
603
877
|
|
|
604
878
|
class CATimedelta
|
|
879
|
+
include CATimeSurfaceFill
|
|
880
|
+
include CATimeOperandValues
|
|
605
881
|
# @overload new(*shape, unit: :ns)
|
|
606
882
|
# Allocates a new int64 storage CArray of the given `shape` and
|
|
607
883
|
# wraps it as a CATimedelta Face with the given `unit`.
|
|
@@ -619,7 +895,7 @@ class CATimedelta
|
|
|
619
895
|
# @param unit [CATime::Resolution, Symbol, String] tick resolution.
|
|
620
896
|
# @return [CATimedelta]
|
|
621
897
|
def self.wrap(raw, unit: :ns)
|
|
622
|
-
|
|
898
|
+
wrap_storage(raw, unit)
|
|
623
899
|
end
|
|
624
900
|
|
|
625
901
|
# A single duration: an integer tick count plus the resolution those ticks
|
|
@@ -686,6 +962,14 @@ class CATimedelta
|
|
|
686
962
|
[value, unit].hash
|
|
687
963
|
end
|
|
688
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
|
+
|
|
689
973
|
# @overload +(other)
|
|
690
974
|
# Returns the sum of two durations, promoting to the finer unit. A
|
|
691
975
|
# cross-group operand (a :M + a :s duration) has no fixed relation and
|
|
@@ -706,7 +990,9 @@ class CATimedelta
|
|
|
706
990
|
# @return [CATimedelta::Element]
|
|
707
991
|
def *(n)
|
|
708
992
|
raise TypeError, "CATimedelta::Element * #{n.class} is not allowed (Integer only)" unless n.is_a?(Integer)
|
|
709
|
-
|
|
993
|
+
v = value * n
|
|
994
|
+
CATimeUnitAlgebra.check_term(v, "*")
|
|
995
|
+
Element.new(v, unit)
|
|
710
996
|
end
|
|
711
997
|
|
|
712
998
|
# @overload /(other)
|
|
@@ -741,12 +1027,21 @@ class CATimedelta
|
|
|
741
1027
|
private
|
|
742
1028
|
|
|
743
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
|
|
744
1037
|
unless other.is_a?(Element)
|
|
745
1038
|
raise TypeError,
|
|
746
1039
|
"CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
|
|
747
1040
|
end
|
|
748
1041
|
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
749
|
-
|
|
1042
|
+
v = duration_in(u) + sign * other.duration_in(u)
|
|
1043
|
+
CATimeUnitAlgebra.check_term(v, sign > 0 ? "+" : "-")
|
|
1044
|
+
Element.new(v, u)
|
|
750
1045
|
end
|
|
751
1046
|
end
|
|
752
1047
|
end
|
|
@@ -813,19 +1108,36 @@ class CATime
|
|
|
813
1108
|
# Returns `self + other` for a {CATimedelta}: the time is the
|
|
814
1109
|
# anchor, so the result keeps `self`'s unit and the duration is converted
|
|
815
1110
|
# into it (a duration finer than `self`'s unit is truncated to it; a
|
|
816
|
-
#
|
|
817
|
-
#
|
|
818
|
-
#
|
|
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]
|
|
1121
|
+
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1122
|
+
# duration array)
|
|
819
1123
|
# @return [CATime]
|
|
820
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.
|
|
821
1127
|
def +(other)
|
|
1128
|
+
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1129
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
822
1130
|
case other
|
|
823
1131
|
when CATimedelta
|
|
824
|
-
|
|
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)
|
|
825
1136
|
when CATime
|
|
826
1137
|
raise TypeError, "CATime + CATime is ill-defined"
|
|
827
1138
|
else
|
|
828
|
-
raise TypeError, "CATime + #{
|
|
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\")"
|
|
829
1141
|
end
|
|
830
1142
|
end
|
|
831
1143
|
|
|
@@ -833,24 +1145,91 @@ class CATime
|
|
|
833
1145
|
# Subtracting a {CATimedelta} yields a {CATime} at `self`'s unit (the
|
|
834
1146
|
# duration is converted into it, truncated when finer); subtracting
|
|
835
1147
|
# another {CATime} yields a {CATimedelta} at the finer of the two
|
|
836
|
-
# units (cross-group falls to the fixed unit).
|
|
837
|
-
#
|
|
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]
|
|
1152
|
+
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1153
|
+
# duration or timestamp array)
|
|
838
1154
|
# @return [CATime, CATimedelta]
|
|
839
1155
|
# @raise [TypeError] on an unsupported operand.
|
|
840
1156
|
def -(other)
|
|
1157
|
+
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1158
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
841
1159
|
case other
|
|
842
1160
|
when CATimedelta
|
|
843
|
-
|
|
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)
|
|
844
1165
|
when CATime
|
|
845
1166
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
846
1167
|
a = CATimeUnitAlgebra.convert_instant!(parent, unit, u)
|
|
847
1168
|
b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
|
|
848
|
-
|
|
1169
|
+
b = b[0] if other.scalar?
|
|
1170
|
+
CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
|
|
849
1171
|
else
|
|
850
|
-
raise TypeError, "CATime - #{
|
|
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)"
|
|
851
1175
|
end
|
|
852
1176
|
end
|
|
853
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
|
+
|
|
854
1233
|
# Element-wise comparison (< <= > >= <=>) rides the core comparison Face
|
|
855
1234
|
# gate (ca_face_reconcile_comparison in ext/ca_obj_face.c): the inherited
|
|
856
1235
|
# CArray operators descend to storage and reconcile a CATime RHS in a
|
|
@@ -858,38 +1237,19 @@ class CATime
|
|
|
858
1237
|
# base CArray#<=> composed from the (gated) > and <.
|
|
859
1238
|
|
|
860
1239
|
# Element decode convention via the C-layer macro
|
|
861
|
-
# `
|
|
1240
|
+
# `CA_FACE_STORAGE_TO_ELEMENT_IF_FACE`: no Ruby `[]` override is needed, a
|
|
862
1241
|
# scalar auto-decodes on all scalar-return paths such as `ca[i,j,k]` /
|
|
863
|
-
# `ca[k]`.
|
|
1242
|
+
# `ca[k]`. storage_to_element itself lives in C (ext/ca_obj_datetime.c)
|
|
864
1243
|
# so the hot path stays off Ruby method dispatch. Its write counterpart
|
|
865
|
-
# is
|
|
1244
|
+
# is element_to_storage (below, surface -> storage on store).
|
|
866
1245
|
|
|
867
1246
|
# @!group Reductions
|
|
868
1247
|
|
|
869
|
-
# min / max are order-structure reductions: they ride the core
|
|
870
|
-
# Face gate (ORDERABLE storage descent + output re-lift, see
|
|
871
|
-
# ext/mkkernel.rb face_gate: :relift), so
|
|
872
|
-
#
|
|
873
|
-
#
|
|
874
|
-
# re-lift both extremes here. They are actual elements, so they keep the
|
|
875
|
-
# storage unit (no rounding, unlike mean).
|
|
876
|
-
|
|
877
|
-
# @overload minmax(*axes, **opts)
|
|
878
|
-
# Returns the earliest and latest time as a `[min, max]` pair (§8).
|
|
879
|
-
# @return [Array(Element, Element), Array(CATime, CATime)]
|
|
880
|
-
def minmax(*args, **opts)
|
|
881
|
-
lo, hi = parent.minmax(*args, **opts)
|
|
882
|
-
[relift_extremum(lo), relift_extremum(hi)]
|
|
883
|
-
end
|
|
884
|
-
|
|
885
|
-
def relift_extremum(r)
|
|
886
|
-
case r
|
|
887
|
-
when Integer then Element.new(r, unit)
|
|
888
|
-
when CArray then r.time(unit: unit)
|
|
889
|
-
else r
|
|
890
|
-
end
|
|
891
|
-
end
|
|
892
|
-
private :relift_extremum
|
|
1248
|
+
# min / max / minmax are order-structure reductions: they ride the core
|
|
1249
|
+
# reduce Face gate (ORDERABLE storage descent + output re-lift, see
|
|
1250
|
+
# ext/mkkernel.rb face_gate: :relift), so the inherited methods return the
|
|
1251
|
+
# Element / CATime shapes directly. The extremes are actual elements, so
|
|
1252
|
+
# they keep the storage unit (no rounding, unlike mean).
|
|
893
1253
|
|
|
894
1254
|
# The centroid / spread / order reductions report on the array's own grid:
|
|
895
1255
|
# they run on the storage ticks and round the result back to the nearest
|
|
@@ -953,22 +1313,43 @@ class CATime
|
|
|
953
1313
|
|
|
954
1314
|
# @overload sum(*)
|
|
955
1315
|
# Not supported; use {#mean} for a centroid.
|
|
956
|
-
# @raise [
|
|
1316
|
+
# @raise [CArray::DataTypeError] always.
|
|
957
1317
|
def sum(*)
|
|
958
|
-
raise
|
|
1318
|
+
raise CArray::DataTypeError, "CATime#sum is ill-defined; use mean for centroid"
|
|
959
1319
|
end
|
|
960
1320
|
|
|
1321
|
+
# @overload accumulate(*)
|
|
1322
|
+
# Not supported, for the same reason as {#sum}.
|
|
1323
|
+
# @raise [CArray::DataTypeError] always.
|
|
1324
|
+
def accumulate(*); raise CArray::DataTypeError, "CATime#accumulate is ill-defined (a sum of instants)"; end
|
|
1325
|
+
|
|
1326
|
+
# @overload cumsum(*)
|
|
1327
|
+
# Not supported, for the same reason as {#sum}. The running position
|
|
1328
|
+
# relative to the first instant is `(t - t[0])` cumulated as durations.
|
|
1329
|
+
# @raise [CArray::DataTypeError] always.
|
|
1330
|
+
def cumsum(*); raise CArray::DataTypeError, "CATime#cumsum is ill-defined (a sum of instants)"; end
|
|
1331
|
+
|
|
1332
|
+
# @overload prod(*)
|
|
1333
|
+
# Not supported: a product of instants has no meaning.
|
|
1334
|
+
# @raise [CArray::DataTypeError] always.
|
|
1335
|
+
def prod(*); raise CArray::DataTypeError, "CATime#prod is ill-defined (a product of instants)"; end
|
|
1336
|
+
|
|
1337
|
+
# @overload cumprod(*)
|
|
1338
|
+
# Not supported, for the same reason as {#prod}.
|
|
1339
|
+
# @raise [CArray::DataTypeError] always.
|
|
1340
|
+
def cumprod(*); raise CArray::DataTypeError, "CATime#cumprod is ill-defined (a product of instants)"; end
|
|
1341
|
+
|
|
961
1342
|
# @overload variance(*)
|
|
962
1343
|
# Not supported: the variance of instants has squared-time units, which
|
|
963
1344
|
# no type represents (ill-defined, like {#sum}). Use {#stddev} for the
|
|
964
1345
|
# spread as a duration.
|
|
965
|
-
# @raise [
|
|
966
|
-
def variance(*); raise
|
|
1346
|
+
# @raise [CArray::DataTypeError] always.
|
|
1347
|
+
def variance(*); raise CArray::DataTypeError, "CATime#variance is ill-defined (squared-time units); use stddev"; end
|
|
967
1348
|
|
|
968
1349
|
# @overload variancep(*)
|
|
969
1350
|
# Not supported, for the same reason as {#variance}. Use {#stddevp}.
|
|
970
|
-
# @raise [
|
|
971
|
-
def variancep(*); raise
|
|
1351
|
+
# @raise [CArray::DataTypeError] always.
|
|
1352
|
+
def variancep(*); raise CArray::DataTypeError, "CATime#variancep is ill-defined (squared-time units); use stddevp"; end
|
|
972
1353
|
# @!endgroup
|
|
973
1354
|
|
|
974
1355
|
# sort / partition ride the core sort Face gate (ORDERABLE storage
|
|
@@ -986,11 +1367,15 @@ class CATime
|
|
|
986
1367
|
# different unit or shape), so the gate routes the operand through here.
|
|
987
1368
|
#
|
|
988
1369
|
# Accepted operands: another {CATime} (unit-rescaled to self),
|
|
989
|
-
# a {Element} (lifted to a length-1 CATime),
|
|
990
|
-
# Ruby `
|
|
991
|
-
#
|
|
992
|
-
#
|
|
993
|
-
#
|
|
1370
|
+
# a {Element} (lifted to a length-1 CATime), and an instant written
|
|
1371
|
+
# outside CArray -- a Ruby `Time`, `Date`, `DateTime`, or a `String` in
|
|
1372
|
+
# the grammar {CArray.time} reads (UTC unless it carries an offset).
|
|
1373
|
+
# Those are read at the precision they carry, not at self's unit, and
|
|
1374
|
+
# then go through the same conversion as a CATime: "2024-01-02 09:00"
|
|
1375
|
+
# against a day-unit array raises rather than matching 2024-01-02. A
|
|
1376
|
+
# bare Integer / other type raises; descend to `ca.parent` to compare
|
|
1377
|
+
# the hidden storage directly. An Array of any of these is a 1-D set
|
|
1378
|
+
# of instants (the value set of {#is_in}).
|
|
994
1379
|
#
|
|
995
1380
|
# The rescale is an INSTANT conversion (convert_instant!), lossless: a
|
|
996
1381
|
# coarser->finer unit always converts; a finer->coarser unit converts only
|
|
@@ -1000,9 +1385,36 @@ class CATime
|
|
|
1000
1385
|
# :D and finer, and a fixed operand coarsens to :M/:Y only when it sits on
|
|
1001
1386
|
# the calendar boundary (:W is the one exception -- month/year starts are
|
|
1002
1387
|
# not week-aligned, so :Y/:M <-> :W raises).
|
|
1003
|
-
# @param operand [CATime, CATime::Element, Time, DateTime]
|
|
1388
|
+
# @param operand [CATime, CATime::Element, Time, Date, DateTime, String, Array]
|
|
1004
1389
|
# @return [CATime] in self's unit.
|
|
1005
1390
|
# @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
|
|
1391
|
+
# Returns `[self, other]` in one unit that holds every value of both
|
|
1392
|
+
# exactly: the finer grid of two same-group units, or the fixed-length one
|
|
1393
|
+
# of a calendar / fixed pair (a month begins on a day). A lookup between
|
|
1394
|
+
# two arrays of different units (is_in, the set operations, locate_addr and
|
|
1395
|
+
# the joins on them) meets there, so it does not depend on which array is
|
|
1396
|
+
# the receiver.
|
|
1397
|
+
# @api private
|
|
1398
|
+
def in_common_unit_with (other)
|
|
1399
|
+
return [self, other] if other.unit == unit
|
|
1400
|
+
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
1401
|
+
[to_unit(u), other.to_unit(u)]
|
|
1402
|
+
end
|
|
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
|
+
|
|
1006
1418
|
def to_comparable (operand)
|
|
1007
1419
|
case operand
|
|
1008
1420
|
when CATime
|
|
@@ -1012,36 +1424,36 @@ class CATime
|
|
|
1012
1424
|
when CATime::Element
|
|
1013
1425
|
lifted = CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
1014
1426
|
to_comparable(lifted)
|
|
1015
|
-
when Time
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1427
|
+
when Time, String, Date
|
|
1428
|
+
to_comparable(CATimeLiteral.exact_instant(operand))
|
|
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)
|
|
1021
1434
|
else
|
|
1022
1435
|
raise TypeError,
|
|
1023
1436
|
"CATime cannot reconcile #{operand.class} " \
|
|
1024
|
-
"(
|
|
1437
|
+
"(compare #ticks for the raw int64 storage)"
|
|
1025
1438
|
end
|
|
1026
1439
|
end
|
|
1027
1440
|
|
|
1028
|
-
# @overload
|
|
1029
|
-
# Write-direction counterpart of
|
|
1441
|
+
# @overload element_to_storage(surface)
|
|
1442
|
+
# Write-direction counterpart of storage_to_element (the store hook fired
|
|
1030
1443
|
# from rb_ca_obj2ptr): brings a surface value object into this Face's
|
|
1031
1444
|
# int64 storage (count in self's unit since the Unix epoch) so a scalar
|
|
1032
|
-
# store round-trips with a fetch. A {Element} / `Time` / `
|
|
1033
|
-
# reconciled to self's unit via {#to_comparable}
|
|
1034
|
-
# discipline: a cross-group unit or a non-exact
|
|
1035
|
-
# raises). A bare Integer (the documented `.parent`
|
|
1036
|
-
#
|
|
1037
|
-
#
|
|
1038
|
-
# @param surface [Element, Time, DateTime, Integer, String]
|
|
1445
|
+
# store round-trips with a fetch. A {Element} / `Time` / `Date` /
|
|
1446
|
+
# `DateTime` / `String` is reconciled to self's unit via {#to_comparable}
|
|
1447
|
+
# (same lossless discipline: a cross-group unit or a non-exact
|
|
1448
|
+
# finer->coarser cast raises). A bare Integer (the documented `.parent`
|
|
1449
|
+
# raw-storage escape) passes through unchanged to the storage cast.
|
|
1450
|
+
# @param surface [Element, Time, Date, DateTime, Integer, String]
|
|
1039
1451
|
# @return [Integer, Object] the storage-domain value, or `surface`
|
|
1040
1452
|
# unchanged for a pass-through type.
|
|
1041
1453
|
# @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
|
|
1042
|
-
def
|
|
1454
|
+
def element_to_storage (surface)
|
|
1043
1455
|
case surface
|
|
1044
|
-
when Integer
|
|
1456
|
+
when Integer
|
|
1045
1457
|
surface
|
|
1046
1458
|
else
|
|
1047
1459
|
to_comparable(surface).parent[0]
|
|
@@ -1109,7 +1521,6 @@ class CATime
|
|
|
1109
1521
|
# CArray. Array-level counterpart to {Element#to_time}.
|
|
1110
1522
|
# @return [CArray]
|
|
1111
1523
|
def to_time
|
|
1112
|
-
require 'time'
|
|
1113
1524
|
if CATimeUnitAlgebra::FIXED.key?(unit.base)
|
|
1114
1525
|
f = unit.tick_ratio # exact seconds / tick (Rational)
|
|
1115
1526
|
parent.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
|
|
@@ -1124,7 +1535,6 @@ class CATime
|
|
|
1124
1535
|
# {Element#to_date}.
|
|
1125
1536
|
# @return [CArray]
|
|
1126
1537
|
def to_date
|
|
1127
|
-
require 'date'
|
|
1128
1538
|
# 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
|
|
1129
1539
|
days_since_epoch.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
|
|
1130
1540
|
end
|
|
@@ -1134,7 +1544,6 @@ class CATime
|
|
|
1134
1544
|
# as an object CArray.
|
|
1135
1545
|
# @return [CArray]
|
|
1136
1546
|
def to_datetime
|
|
1137
|
-
require 'date'
|
|
1138
1547
|
to_time.convert(:object) {|t| t.to_datetime}
|
|
1139
1548
|
end
|
|
1140
1549
|
|
|
@@ -1187,6 +1596,54 @@ class CATime
|
|
|
1187
1596
|
# @return [CArray] integer.
|
|
1188
1597
|
def second; clock_field(:s); end
|
|
1189
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
|
+
|
|
1190
1647
|
# Day of the week, Sunday = 0 .. Saturday = 6.
|
|
1191
1648
|
# @return [CArray] integer.
|
|
1192
1649
|
def weekday
|
|
@@ -1209,7 +1666,6 @@ class CATime
|
|
|
1209
1666
|
# Returns the Julian Day Number for each element.
|
|
1210
1667
|
# @return [CArray]
|
|
1211
1668
|
def jd
|
|
1212
|
-
require 'date'
|
|
1213
1669
|
to_time.convert(:int) {|t| t.to_date.jd}
|
|
1214
1670
|
end
|
|
1215
1671
|
|
|
@@ -1218,7 +1674,6 @@ class CATime
|
|
|
1218
1674
|
# day) for each element.
|
|
1219
1675
|
# @return [CArray]
|
|
1220
1676
|
def ajd
|
|
1221
|
-
require 'date'
|
|
1222
1677
|
to_time.convert(:double) {|t| t.to_datetime.ajd.to_f}
|
|
1223
1678
|
end
|
|
1224
1679
|
|
|
@@ -1248,7 +1703,9 @@ class CATime
|
|
|
1248
1703
|
# the centroid of month numbers (a caller wanting the day-space answer
|
|
1249
1704
|
# writes to_unit(:D) first) -- and the result is rounded back to a tick.
|
|
1250
1705
|
def reduce_on_own_grid(op, kind, args, opts)
|
|
1251
|
-
|
|
1706
|
+
with_surface_fill(opts) do |o|
|
|
1707
|
+
round_and_relift(parent.public_send(op, *args, **o), kind)
|
|
1708
|
+
end
|
|
1252
1709
|
end
|
|
1253
1710
|
|
|
1254
1711
|
# Round-to-nearest onto the storage grid, then wear the Face again. An
|
|
@@ -1273,6 +1730,41 @@ class CATime
|
|
|
1273
1730
|
end
|
|
1274
1731
|
end
|
|
1275
1732
|
|
|
1733
|
+
# inspect's name for the array: the class and the tick, which the storage
|
|
1734
|
+
# type name (fixlen[8]) does not tell.
|
|
1735
|
+
private def inspect_label
|
|
1736
|
+
"#{self.class}[#{unit}]"
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
# inspect's cell: the instant as Element#to_s writes it, but with the
|
|
1740
|
+
# digits of a second that some cell of the array needs, the same in every
|
|
1741
|
+
# cell so that the cells line up; the unit is in the label.
|
|
1742
|
+
private def inspect_cell_formatter
|
|
1743
|
+
digits = inspect_second_digits
|
|
1744
|
+
lambda do |e|
|
|
1745
|
+
case e.unit.base
|
|
1746
|
+
when :Y, :M, :W, :D then e.to_s
|
|
1747
|
+
else e.to_time.iso8601(digits)
|
|
1748
|
+
end
|
|
1749
|
+
end
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
# The digits of a second inspect writes: 0, 3, 6 or 9, the fewest that
|
|
1753
|
+
# show every present cell exactly (9 at most, as Time holds).
|
|
1754
|
+
private def inspect_second_digits
|
|
1755
|
+
base_digits = { ms: 3, us: 6, ns: 9, ps: 12, fs: 15, as: 18 }[unit.base]
|
|
1756
|
+
return 0 unless base_digits
|
|
1757
|
+
count = unit.count
|
|
1758
|
+
[0, 3, 6, 9].each do |d|
|
|
1759
|
+
break if d >= base_digits
|
|
1760
|
+
m = 10**(base_digits - d)
|
|
1761
|
+
# ticks * count is a multiple of m when ticks is one of m / gcd(m, count),
|
|
1762
|
+
# which does not multiply out of int64.
|
|
1763
|
+
return d if (ticks % (m / m.gcd(count))).eq(0).all
|
|
1764
|
+
end
|
|
1765
|
+
[base_digits, 9].min
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1276
1768
|
end
|
|
1277
1769
|
|
|
1278
1770
|
# ============================================================================
|
|
@@ -1319,19 +1811,26 @@ class CATimedelta
|
|
|
1319
1811
|
# {CATimedelta}; a {CATime} operand delegates to
|
|
1320
1812
|
# {CATime#+} (commutative).
|
|
1321
1813
|
# @param other [CATimedelta, CATime]
|
|
1814
|
+
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1815
|
+
# duration or timestamp array)
|
|
1322
1816
|
# @return [CATimedelta, CATime]
|
|
1323
1817
|
# @raise [TypeError] on incompatible operands.
|
|
1324
1818
|
def +(other)
|
|
1819
|
+
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1820
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
1325
1821
|
case other
|
|
1326
1822
|
when CATimedelta
|
|
1327
1823
|
u = common_duration_unit(other.unit)
|
|
1328
1824
|
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1329
1825
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1330
|
-
|
|
1826
|
+
b = b[0] if other.scalar?
|
|
1827
|
+
CATimeUnitAlgebra.checked_add(a, b).timedelta(unit: u)
|
|
1331
1828
|
when CATime
|
|
1332
|
-
other + self
|
|
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)
|
|
1333
1832
|
else
|
|
1334
|
-
raise TypeError, "CATimedelta + #{
|
|
1833
|
+
raise TypeError, "CATimedelta + #{given.class} is not allowed"
|
|
1335
1834
|
end
|
|
1336
1835
|
end
|
|
1337
1836
|
|
|
@@ -1339,17 +1838,22 @@ class CATimedelta
|
|
|
1339
1838
|
# Returns the difference of two {CATimedelta} at the finer of the two
|
|
1340
1839
|
# units (a cross-group pair raises).
|
|
1341
1840
|
# @param other [CATimedelta]
|
|
1841
|
+
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1842
|
+
# duration array)
|
|
1342
1843
|
# @return [CATimedelta]
|
|
1343
1844
|
# @raise [TypeError] on a non-timedelta operand.
|
|
1344
1845
|
def -(other)
|
|
1846
|
+
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1847
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
1345
1848
|
case other
|
|
1346
1849
|
when CATimedelta
|
|
1347
1850
|
u = common_duration_unit(other.unit)
|
|
1348
1851
|
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1349
1852
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1350
|
-
|
|
1853
|
+
b = b[0] if other.scalar?
|
|
1854
|
+
CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
|
|
1351
1855
|
else
|
|
1352
|
-
raise TypeError, "CATimedelta - #{
|
|
1856
|
+
raise TypeError, "CATimedelta - #{given.class} is not allowed"
|
|
1353
1857
|
end
|
|
1354
1858
|
end
|
|
1355
1859
|
|
|
@@ -1375,28 +1879,34 @@ class CATimedelta
|
|
|
1375
1879
|
# @raise [TypeError] when `other` is not an Integer.
|
|
1376
1880
|
def *(other)
|
|
1377
1881
|
case other
|
|
1378
|
-
when Integer then (parent
|
|
1882
|
+
when Integer then CATimeUnitAlgebra.checked_mul(parent, other).timedelta(unit: unit)
|
|
1379
1883
|
else raise TypeError, "CATimedelta * #{other.class} is not allowed (Integer only)"
|
|
1380
1884
|
end
|
|
1381
1885
|
end
|
|
1382
1886
|
|
|
1383
1887
|
# @overload /(other)
|
|
1384
1888
|
# Returns element-wise division: by Integer yields a
|
|
1385
|
-
# {CATimedelta}
|
|
1386
|
-
#
|
|
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.
|
|
1387
1892
|
# @param other [Integer, CATimedelta]
|
|
1893
|
+
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1894
|
+
# duration array)
|
|
1388
1895
|
# @return [CATimedelta, CArray]
|
|
1389
1896
|
# @raise [TypeError] on incompatible operands.
|
|
1390
1897
|
def /(other)
|
|
1898
|
+
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1391
1899
|
case other
|
|
1392
1900
|
when Integer
|
|
1393
1901
|
CATimeUnitAlgebra.trunc_divide(parent, other).timedelta(unit: unit)
|
|
1394
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.
|
|
1395
1906
|
u = common_duration_unit(other.unit)
|
|
1396
|
-
CATimeUnitAlgebra.
|
|
1397
|
-
CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1398
|
-
|
|
1399
|
-
else raise TypeError, "CATimedelta / #{other.class} is not allowed"
|
|
1907
|
+
CATimeUnitAlgebra.convert_scale!(parent, unit, u).to_type(:float64) /
|
|
1908
|
+
CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1909
|
+
else raise TypeError, "CATimedelta / #{given.class} is not allowed"
|
|
1400
1910
|
end
|
|
1401
1911
|
end
|
|
1402
1912
|
|
|
@@ -1409,17 +1919,73 @@ class CATimedelta
|
|
|
1409
1919
|
|
|
1410
1920
|
# @overload sum(*args, **opts)
|
|
1411
1921
|
# Returns the sum of durations as a {Element} for full reduction
|
|
1412
|
-
# 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.
|
|
1413
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).
|
|
1414
1927
|
def sum(*args, **opts)
|
|
1415
|
-
|
|
1928
|
+
with_surface_fill(opts) { |o|
|
|
1929
|
+
exact_sum_relift(parent.accumulate(*args, **o), parent.sum(*args, **o))
|
|
1930
|
+
}
|
|
1931
|
+
end
|
|
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
|
+
|
|
1952
|
+
# @overload accumulate(*args, **opts)
|
|
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.
|
|
1956
|
+
# @return [Element, CATimedelta]
|
|
1957
|
+
def accumulate(*args, **opts)
|
|
1958
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.accumulate(*args, **o)) }
|
|
1416
1959
|
end
|
|
1417
1960
|
|
|
1961
|
+
# @overload cumsum(axis: nil)
|
|
1962
|
+
# Returns the running sum of durations as a {CATimedelta} on `self`'s
|
|
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.
|
|
1966
|
+
# @return [CATimedelta]
|
|
1967
|
+
# @raise [RangeError] if any cell of the running sum does not fit int64.
|
|
1968
|
+
def cumsum(*args, **opts)
|
|
1969
|
+
exact_sum_relift(parent.send(:cumsum_in_own_type, *args, **opts),
|
|
1970
|
+
parent.cumsum(*args, **opts))
|
|
1971
|
+
end
|
|
1972
|
+
|
|
1973
|
+
# @overload prod(*)
|
|
1974
|
+
# Not supported: a product of durations has the units of a power of
|
|
1975
|
+
# time, which no type represents.
|
|
1976
|
+
# @raise [CArray::DataTypeError] always.
|
|
1977
|
+
def prod(*); raise CArray::DataTypeError, "CATimedelta#prod is ill-defined (powers of time units)"; end
|
|
1978
|
+
|
|
1979
|
+
# @overload cumprod(*)
|
|
1980
|
+
# Not supported, for the same reason as {#prod}.
|
|
1981
|
+
# @raise [CArray::DataTypeError] always.
|
|
1982
|
+
def cumprod(*); raise CArray::DataTypeError, "CATimedelta#cumprod is ill-defined (powers of time units)"; end
|
|
1983
|
+
|
|
1418
1984
|
# @overload mean(axis: nil, **opts)
|
|
1419
1985
|
# Returns the mean duration rounded to the nearest unit count.
|
|
1420
1986
|
# @return [Element, CATimedelta]
|
|
1421
1987
|
def mean(*args, **opts)
|
|
1422
|
-
round_and_relift(parent.mean(*args, **
|
|
1988
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.mean(*args, **o)) }
|
|
1423
1989
|
end
|
|
1424
1990
|
|
|
1425
1991
|
# median / percentile / quantile / stddev / stddevp report on this array's
|
|
@@ -1431,7 +1997,7 @@ class CATimedelta
|
|
|
1431
1997
|
# Returns the median duration on `self`'s unit.
|
|
1432
1998
|
# @return [Element, CATimedelta]
|
|
1433
1999
|
def median(*args, **opts)
|
|
1434
|
-
round_and_relift(parent.median(*args, **
|
|
2000
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.median(*args, **o)) }
|
|
1435
2001
|
end
|
|
1436
2002
|
|
|
1437
2003
|
# @overload percentile(*p, axis: nil, **opts)
|
|
@@ -1440,7 +2006,7 @@ class CATimedelta
|
|
|
1440
2006
|
# two or more give an Array).
|
|
1441
2007
|
# @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
|
|
1442
2008
|
def percentile(*args, **opts)
|
|
1443
|
-
round_and_relift(parent.percentile(*args, **
|
|
2009
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.percentile(*args, **o)) }
|
|
1444
2010
|
end
|
|
1445
2011
|
|
|
1446
2012
|
# @overload quantile(axis: nil, **opts)
|
|
@@ -1448,7 +2014,7 @@ class CATimedelta
|
|
|
1448
2014
|
# `self`'s unit.
|
|
1449
2015
|
# @return [Array<Element>, Array<CATimedelta>]
|
|
1450
2016
|
def quantile(*args, **opts)
|
|
1451
|
-
round_and_relift(parent.quantile(*args, **
|
|
2017
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.quantile(*args, **o)) }
|
|
1452
2018
|
end
|
|
1453
2019
|
|
|
1454
2020
|
# @overload stddev(axis: nil, **opts)
|
|
@@ -1457,7 +2023,7 @@ class CATimedelta
|
|
|
1457
2023
|
# the precision matters.
|
|
1458
2024
|
# @return [Element, CATimedelta]
|
|
1459
2025
|
def stddev(*args, **opts)
|
|
1460
|
-
round_and_relift(parent.stddev(*args, **
|
|
2026
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.stddev(*args, **o)) }
|
|
1461
2027
|
end
|
|
1462
2028
|
|
|
1463
2029
|
# @overload stddevp(axis: nil, **opts)
|
|
@@ -1465,7 +2031,7 @@ class CATimedelta
|
|
|
1465
2031
|
# {#stddev}.
|
|
1466
2032
|
# @return [Element, CATimedelta]
|
|
1467
2033
|
def stddevp(*args, **opts)
|
|
1468
|
-
round_and_relift(parent.stddevp(*args, **
|
|
2034
|
+
with_surface_fill(opts) { |o| round_and_relift(parent.stddevp(*args, **o)) }
|
|
1469
2035
|
end
|
|
1470
2036
|
|
|
1471
2037
|
def round_and_relift(r)
|
|
@@ -1482,39 +2048,21 @@ class CATimedelta
|
|
|
1482
2048
|
# Not supported: the variance of durations has squared-time units, which
|
|
1483
2049
|
# no type represents -- the same reason {CATime#variance} refuses. Use
|
|
1484
2050
|
# {#stddev} for the spread as a duration.
|
|
1485
|
-
# @raise [
|
|
2051
|
+
# @raise [CArray::DataTypeError] always.
|
|
1486
2052
|
def variance(*)
|
|
1487
|
-
raise
|
|
2053
|
+
raise CArray::DataTypeError, "CATimedelta#variance is ill-defined (squared-time units); use stddev"
|
|
1488
2054
|
end
|
|
1489
2055
|
|
|
1490
2056
|
# @overload variancep(*)
|
|
1491
2057
|
# Not supported, for the same reason as {#variance}. Use {#stddevp}.
|
|
1492
|
-
# @raise [
|
|
2058
|
+
# @raise [CArray::DataTypeError] always.
|
|
1493
2059
|
def variancep(*)
|
|
1494
|
-
raise
|
|
1495
|
-
end
|
|
1496
|
-
|
|
1497
|
-
# min / max ride the core reduce Face gate (ORDERABLE storage descent +
|
|
1498
|
-
# output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
|
|
1499
|
-
# CArray#min / #max return the Element / CATimedelta shapes directly.
|
|
1500
|
-
# minmax has no core gate, so re-lift both extremes here.
|
|
1501
|
-
|
|
1502
|
-
# @overload minmax(*axes, **opts)
|
|
1503
|
-
# Returns the shortest and longest duration as a `[min, max]` pair.
|
|
1504
|
-
# @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
|
|
1505
|
-
def minmax(*args, **opts)
|
|
1506
|
-
lo, hi = parent.minmax(*args, **opts)
|
|
1507
|
-
[relift_extremum(lo), relift_extremum(hi)]
|
|
2060
|
+
raise CArray::DataTypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
|
|
1508
2061
|
end
|
|
1509
2062
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
when CArray then r.timedelta(unit: unit)
|
|
1514
|
-
else r
|
|
1515
|
-
end
|
|
1516
|
-
end
|
|
1517
|
-
private :relift_extremum
|
|
2063
|
+
# min / max / minmax ride the core reduce Face gate (ORDERABLE storage
|
|
2064
|
+
# descent + output re-lift); the inherited methods return the Element /
|
|
2065
|
+
# CATimedelta shapes directly.
|
|
1518
2066
|
# @!endgroup
|
|
1519
2067
|
|
|
1520
2068
|
# sort / partition ride the core sort Face gate (ORDERABLE storage
|
|
@@ -1535,6 +2083,34 @@ class CATimedelta
|
|
|
1535
2083
|
# @param operand [CATimedelta, CATimedelta::Element]
|
|
1536
2084
|
# @return [CATimedelta] in self's unit.
|
|
1537
2085
|
# @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
|
|
2086
|
+
# Returns `[self, other]` in one unit, the finer grid of the two, which
|
|
2087
|
+
# holds every value of both exactly. A lookup between two duration
|
|
2088
|
+
# arrays of different units meets there, so it does not depend on which
|
|
2089
|
+
# array is the receiver. A calendar and a fixed-length duration have no
|
|
2090
|
+
# common unit and raise.
|
|
2091
|
+
# @api private
|
|
2092
|
+
def in_common_unit_with (other)
|
|
2093
|
+
return [self, other] if other.unit == unit
|
|
2094
|
+
unless CATimeUnitAlgebra.same_group?(unit, other.unit)
|
|
2095
|
+
raise ArgumentError,
|
|
2096
|
+
"cannot compare a #{unit} duration with a #{other.unit} duration: " \
|
|
2097
|
+
"a calendar length (months, years) has no fixed length in days"
|
|
2098
|
+
end
|
|
2099
|
+
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
2100
|
+
[to_unit(u), other.to_unit(u)]
|
|
2101
|
+
end
|
|
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
|
+
|
|
1538
2114
|
def to_comparable (operand)
|
|
1539
2115
|
case operand
|
|
1540
2116
|
when CATimedelta
|
|
@@ -1544,15 +2120,19 @@ class CATimedelta
|
|
|
1544
2120
|
when CATimedelta::Element
|
|
1545
2121
|
lifted = CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
1546
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)
|
|
1547
2127
|
else
|
|
1548
2128
|
raise TypeError,
|
|
1549
2129
|
"CATimedelta cannot reconcile #{operand.class} " \
|
|
1550
|
-
"(
|
|
2130
|
+
"(compare #ticks for the raw int64 storage)"
|
|
1551
2131
|
end
|
|
1552
2132
|
end
|
|
1553
2133
|
|
|
1554
|
-
# @overload
|
|
1555
|
-
# Write-direction counterpart of
|
|
2134
|
+
# @overload element_to_storage(surface)
|
|
2135
|
+
# Write-direction counterpart of storage_to_element: brings a surface
|
|
1556
2136
|
# value object into this Face's int64 storage (count in self's unit) so a
|
|
1557
2137
|
# scalar store round-trips with a fetch. A {Element} is reconciled to
|
|
1558
2138
|
# self's unit via {#to_comparable} (lossless discipline; a `Time` /
|
|
@@ -1563,7 +2143,7 @@ class CATimedelta
|
|
|
1563
2143
|
# @return [Integer, Object] the storage-domain value, or `surface`
|
|
1564
2144
|
# unchanged for a pass-through type.
|
|
1565
2145
|
# @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
|
|
1566
|
-
def
|
|
2146
|
+
def element_to_storage (surface)
|
|
1567
2147
|
case surface
|
|
1568
2148
|
when Integer, String
|
|
1569
2149
|
surface
|
|
@@ -1609,6 +2189,17 @@ class CATimedelta
|
|
|
1609
2189
|
end
|
|
1610
2190
|
CATimeUnitAlgebra.finer(unit, ou)
|
|
1611
2191
|
end
|
|
2192
|
+
|
|
2193
|
+
# inspect's name for the array: the class and the tick, which the storage
|
|
2194
|
+
# type name (fixlen[8]) does not tell.
|
|
2195
|
+
private def inspect_label
|
|
2196
|
+
"#{self.class}[#{unit}]"
|
|
2197
|
+
end
|
|
2198
|
+
|
|
2199
|
+
# inspect's cell: the duration in the base unit of the tick.
|
|
2200
|
+
private def inspect_cell_formatter
|
|
2201
|
+
lambda { |e| "#{e.value * e.unit.count}#{e.unit.base}" }
|
|
2202
|
+
end
|
|
1612
2203
|
end
|
|
1613
2204
|
|
|
1614
2205
|
# Reads a time literal (Time / DateTime / Integer unix-seconds / String) into
|
|
@@ -1621,39 +2212,153 @@ module CATimeLiteral
|
|
|
1621
2212
|
|
|
1622
2213
|
# Date fields of a String literal, UTC. Ruby's Date._parse does not read
|
|
1623
2214
|
# the calendar-grid forms CATime#to_s prints -- "2019-09" comes back as a
|
|
1624
|
-
# month of 20 with a zone, and a bare "2019" as a month and a day --
|
|
1625
|
-
#
|
|
2215
|
+
# month of 20 with a zone, and a bare "2019" as a month and a day -- nor an
|
|
2216
|
+
# hour alone after the date ("2024-01-01T05", ISO 8601 reduced to the
|
|
2217
|
+
# hour), so those are read here and everything else is left to Date._parse.
|
|
2218
|
+
# Date._parse passes over what it cannot read, so text with a time after
|
|
2219
|
+
# the date that comes back without an hour is refused rather than read as
|
|
2220
|
+
# midnight.
|
|
1626
2221
|
#
|
|
1627
2222
|
# What Date._parse returns is then checked, because an out-of-range field
|
|
1628
2223
|
# used to flow into the civil kernel and normalise into a different date:
|
|
1629
2224
|
# "201909" is a valid YYMMDD to Ruby (2020-19-09, which landed on 2021-07),
|
|
1630
2225
|
# and "2019-02-31" landed on 2019-03-03. A missing finer field is not an
|
|
1631
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
|
+
|
|
1632
2237
|
def parse_date_fields(spec, format)
|
|
1633
2238
|
h =
|
|
1634
|
-
if format then Date._strptime(spec, format)
|
|
2239
|
+
if format then epoch_fields(Date._strptime(spec, format))
|
|
1635
2240
|
elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
|
|
1636
2241
|
elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
|
|
2242
|
+
elsif (hour = hour_only_fields(spec)) then hour
|
|
1637
2243
|
else Date._parse(spec)
|
|
1638
2244
|
end
|
|
2245
|
+
h = resolve_day_of_year(h, spec) if h
|
|
1639
2246
|
unless h && h[:year]
|
|
1640
2247
|
raise ArgumentError, "cannot parse time #{spec.inspect}"
|
|
1641
2248
|
end
|
|
2249
|
+
if !format && !h[:hour] &&
|
|
2250
|
+
(spec =~ /\d\s*T\s*\d/ || spec =~ /\A\s*[+-]?\d{4,}[-\/]\d{1,2}[-\/]\d{1,2}\s+\d/)
|
|
2251
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}: the time after the date is not read"
|
|
2252
|
+
end
|
|
2253
|
+
# Text left over after the format is not part of a time in that format.
|
|
2254
|
+
if h[:leftover] && !h[:leftover].strip.empty?
|
|
2255
|
+
raise ArgumentError,
|
|
2256
|
+
"cannot parse time #{spec.inspect}: #{h[:leftover].inspect} is " \
|
|
2257
|
+
"left over after #{format.inspect}"
|
|
2258
|
+
end
|
|
1642
2259
|
y, m, d = h[:year], h[:mon] || 1, h[:mday] || 1
|
|
1643
2260
|
unless (1..12).cover?(m) && Date.valid_date?(y, m, d, Date::GREGORIAN)
|
|
1644
2261
|
raise ArgumentError,
|
|
1645
2262
|
"cannot parse time #{spec.inspect}: it reads as year #{y}, " \
|
|
1646
2263
|
"month #{m}, day #{d}, which is not a date"
|
|
1647
2264
|
end
|
|
2265
|
+
# Hour 24 is the end of the day, 24:00:00, and nothing past it.
|
|
2266
|
+
if h[:hour] == 24 && [h[:min], h[:sec], h[:sec_fraction]].any? { |v| v && v != 0 }
|
|
2267
|
+
raise ArgumentError,
|
|
2268
|
+
"cannot parse time #{spec.inspect}: hour 24 is only 24:00:00"
|
|
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
|
|
1648
2278
|
h
|
|
1649
2279
|
end
|
|
1650
2280
|
private_class_method :parse_date_fields
|
|
1651
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
|
+
|
|
2327
|
+
# "YYYY-MM-DDThh" with an optional zone ("Z", "+hh", "+hhmm", "+hh:mm").
|
|
2328
|
+
def hour_only_fields(spec)
|
|
2329
|
+
m = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2})(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?\z/.match(spec)
|
|
2330
|
+
return nil unless m
|
|
2331
|
+
h = { year: m[1].to_i, mon: m[2].to_i, mday: m[3].to_i, hour: m[4].to_i }
|
|
2332
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}: hour #{h[:hour]}" if h[:hour] > 23
|
|
2333
|
+
if m[6]
|
|
2334
|
+
oh, om = m[7].to_i, m[8].to_i
|
|
2335
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}: offset" if oh > 23 || om > 59
|
|
2336
|
+
h[:offset] = (oh * 3600 + om * 60) * (m[6] == "-" ? -1 : 1)
|
|
2337
|
+
elsif m[5]
|
|
2338
|
+
h[:offset] = 0
|
|
2339
|
+
end
|
|
2340
|
+
h
|
|
2341
|
+
end
|
|
2342
|
+
private_class_method :hour_only_fields
|
|
2343
|
+
|
|
2344
|
+
# An instant written outside CArray (Time / Date / DateTime / String) as a
|
|
2345
|
+
# length-1 CATime in the coarsest of :s / :ms / :us / :ns that holds it
|
|
2346
|
+
# exactly, so a comparison sees the instant it names and not that instant
|
|
2347
|
+
# floored onto some other grid. Finer than a nanosecond raises.
|
|
2348
|
+
def exact_instant(spec)
|
|
2349
|
+
sec = epoch_seconds(spec)
|
|
2350
|
+
[:s, :ms, :us, :ns].each_with_index do |u, k|
|
|
2351
|
+
ticks = sec * (10 ** (3 * k))
|
|
2352
|
+
if ticks.denominator == 1
|
|
2353
|
+
return CATime.wrap(CA_INT64([ticks.to_i]), unit: u)
|
|
2354
|
+
end
|
|
2355
|
+
end
|
|
2356
|
+
raise ArgumentError, "#{spec.inspect} is finer than a nanosecond"
|
|
2357
|
+
end
|
|
2358
|
+
|
|
1652
2359
|
# Exact Rational seconds since the Unix epoch for a start literal (Time /
|
|
1653
|
-
# DateTime / Integer unix-seconds / String). UTC default.
|
|
2360
|
+
# Date / DateTime / Integer unix-seconds / String). UTC default.
|
|
1654
2361
|
def epoch_seconds(spec, format = nil)
|
|
1655
|
-
require 'date'
|
|
1656
|
-
require 'time'
|
|
1657
2362
|
case spec
|
|
1658
2363
|
when Time then spec.to_r
|
|
1659
2364
|
when Integer then Rational(spec)
|
|
@@ -1680,6 +2385,8 @@ module CATimeLiteral
|
|
|
1680
2385
|
else
|
|
1681
2386
|
if defined?(DateTime) && spec.is_a?(DateTime)
|
|
1682
2387
|
spec.to_time.to_r
|
|
2388
|
+
elsif defined?(Date) && spec.is_a?(Date)
|
|
2389
|
+
Rational(CATimeCivil.days_from_civil_1(spec.year, spec.month, spec.day) * 86400)
|
|
1683
2390
|
else
|
|
1684
2391
|
raise ArgumentError, "cannot parse time #{spec.class}"
|
|
1685
2392
|
end
|
|
@@ -1688,8 +2395,6 @@ module CATimeLiteral
|
|
|
1688
2395
|
|
|
1689
2396
|
# [year, month] (UTC) of a start literal, for a calendar-resolution grid.
|
|
1690
2397
|
def year_month(spec, format = nil)
|
|
1691
|
-
require 'date'
|
|
1692
|
-
require 'time'
|
|
1693
2398
|
case spec
|
|
1694
2399
|
when Time then t = spec.utc; [t.year, t.month]
|
|
1695
2400
|
when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
|
|
@@ -1727,6 +2432,273 @@ module CATimeLiteral
|
|
|
1727
2432
|
end
|
|
1728
2433
|
end
|
|
1729
2434
|
|
|
2435
|
+
# A format :infer can choose: a strptime format, read strictly (see
|
|
2436
|
+
# __strptime_fields__), and what the text carries (:date, :clock, or
|
|
2437
|
+
# :fraction of a second).
|
|
2438
|
+
TimeFormat = Struct.new(:format, :kind)
|
|
2439
|
+
|
|
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] +
|
|
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)
|
|
2443
|
+
|
|
2444
|
+
# A space in a format matches any run of spaces, none included.
|
|
2445
|
+
INFER_CLOCKS = [
|
|
2446
|
+
["%H:%M", :clock], ["%H:%M:%S", :clock], ["%H:%M:%S.%N", :fraction],
|
|
2447
|
+
["%I:%M %p", :clock], ["%I:%M:%S %p", :clock],
|
|
2448
|
+
].freeze
|
|
2449
|
+
Ractor.make_shareable(INFER_CLOCKS)
|
|
2450
|
+
|
|
2451
|
+
# A time after a date: none, or a clock with or without a zone. %z takes
|
|
2452
|
+
# an offset, Z, UTC or GMT, not a zone named for a place (some of those
|
|
2453
|
+
# names mean different offsets in different places).
|
|
2454
|
+
INFER_TIMES = ([["", :date]] +
|
|
2455
|
+
INFER_CLOCKS.flat_map { |clock, kind| [[" #{clock}", kind], [" #{clock} %z", kind]] }
|
|
2456
|
+
).freeze
|
|
2457
|
+
Ractor.make_shareable(INFER_TIMES)
|
|
2458
|
+
|
|
2459
|
+
# A date written in Japanese, year first ("2024<year>1<month>2<day>"),
|
|
2460
|
+
# takes a time in the same markers ("3<hour>4<minute>", "...5<second>")
|
|
2461
|
+
# or with colons.
|
|
2462
|
+
JA_YEAR, JA_MONTH, JA_DAY = "年", "月", "日"
|
|
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) }
|
|
2465
|
+
INFER_JA_TIMES = ([["", :date]] +
|
|
2466
|
+
([["%H#{JA_HOUR}%M#{JA_MINUTE}", :clock],
|
|
2467
|
+
["%H#{JA_HOUR}%M#{JA_MINUTE}%S#{JA_SECOND}", :clock]] + INFER_CLOCKS)
|
|
2468
|
+
.map { |clock, kind| [" #{clock}", kind] }).freeze
|
|
2469
|
+
Ractor.make_shareable(INFER_JA_TIMES)
|
|
2470
|
+
|
|
2471
|
+
INFER_FORMATS = (
|
|
2472
|
+
INFER_DATES.product(INFER_TIMES).map { |date, (time, kind)|
|
|
2473
|
+
TimeFormat.new(date + time, kind)
|
|
2474
|
+
} +
|
|
2475
|
+
INFER_JA_TIMES.map { |time, kind|
|
|
2476
|
+
TimeFormat.new("%Y#{JA_YEAR}%m#{JA_MONTH}%d#{JA_DAY}#{time}", kind)
|
|
2477
|
+
}
|
|
2478
|
+
).freeze
|
|
2479
|
+
Ractor.make_shareable(INFER_FORMATS)
|
|
2480
|
+
INFER_FORMAT_STRINGS = INFER_FORMATS.map(&:format).freeze
|
|
2481
|
+
Ractor.make_shareable(INFER_FORMAT_STRINGS)
|
|
2482
|
+
|
|
2483
|
+
# The one format of INFER_FORMATS that the text of object array `x` is
|
|
2484
|
+
# written in, `first` being its first present cell, as
|
|
2485
|
+
# [format, fields, address]: the fields __strptime_fields__ read in it,
|
|
2486
|
+
# and the address of the first cell not in it, or nil.
|
|
2487
|
+
#
|
|
2488
|
+
# The first cell gives the candidates and each later cell drops those it
|
|
2489
|
+
# does not fit, until one is left. Each candidate is read over the whole
|
|
2490
|
+
# array once, and the candidate a later cell leaves last is the one left:
|
|
2491
|
+
# no candidate for the first cell or none left (the last ones dropped by
|
|
2492
|
+
# one cell) raises CAFrame::UnreadableColumn, and more than one left at the
|
|
2493
|
+
# end raises CAFrame::AmbiguousTimeFormat (CAFrame is its only caller). A cell
|
|
2494
|
+
# after the choice that is not in the format is returned for the caller
|
|
2495
|
+
# to report.
|
|
2496
|
+
def infer_time_format(x, first)
|
|
2497
|
+
candidates = CArray.__strptime_fits__(first, INFER_FORMAT_STRINGS)
|
|
2498
|
+
if candidates.empty?
|
|
2499
|
+
raise CAFrame::UnreadableColumn,
|
|
2500
|
+
"cannot infer a time format from #{first.inspect}; " \
|
|
2501
|
+
"pass a strptime format, or :mixed to guess at each cell"
|
|
2502
|
+
end
|
|
2503
|
+
runs = candidates.map do |i|
|
|
2504
|
+
fields = x.__strptime_fields__(INFER_FORMATS[i].format, true)
|
|
2505
|
+
[INFER_FORMATS[i], fields, fields[7].first || Float::INFINITY]
|
|
2506
|
+
end
|
|
2507
|
+
last = runs.map(&:last).max
|
|
2508
|
+
left = runs.select { |run| run[2] == last }
|
|
2509
|
+
if left.size > 1
|
|
2510
|
+
if last == Float::INFINITY
|
|
2511
|
+
formats = left.map { |run| run[0].format }
|
|
2512
|
+
raise CAFrame::AmbiguousTimeFormat.new(
|
|
2513
|
+
"#{first.inspect} could be read as " \
|
|
2514
|
+
"#{formats.map(&:inspect).join(' or ')}, " \
|
|
2515
|
+
"and no cell tells which; pass the format",
|
|
2516
|
+
formats: formats)
|
|
2517
|
+
end
|
|
2518
|
+
raise CAFrame::UnreadableColumn,
|
|
2519
|
+
"#{x.flatten[last].inspect} is not in the format of #{first.inspect}; " \
|
|
2520
|
+
"pass a strptime format, or :mixed to guess at each cell"
|
|
2521
|
+
end
|
|
2522
|
+
f, fields, bad = left.first
|
|
2523
|
+
[f, fields, bad == Float::INFINITY ? nil : bad]
|
|
2524
|
+
end
|
|
2525
|
+
|
|
2526
|
+
# [kind, per] for __time_ticks_from_fields__: ticks of `per` nanoseconds
|
|
2527
|
+
# (:fixed) or months (:calendar), or nil when `res` is finer than a
|
|
2528
|
+
# nanosecond.
|
|
2529
|
+
def tick_kind(res)
|
|
2530
|
+
if CATimeUnitAlgebra::FIXED.key?(res.base)
|
|
2531
|
+
per = res.tick_ratio * 1_000_000_000
|
|
2532
|
+
kind = :fixed
|
|
2533
|
+
else
|
|
2534
|
+
per = res.tick_ratio
|
|
2535
|
+
kind = :calendar
|
|
2536
|
+
end
|
|
2537
|
+
(per.denominator == 1 && per >= 1) ? [kind, per.to_i] : nil
|
|
2538
|
+
end
|
|
2539
|
+
|
|
2540
|
+
# The ticks of every cell of `x` on the `res` grid, as an int64 array of
|
|
2541
|
+
# x's shape (missing and, under on_error: :mask, unparseable cells
|
|
2542
|
+
# masked), or nil when `res` is finer than a nanosecond. A strptime
|
|
2543
|
+
# format that __strptime_fields__ reads is read for the whole array in C;
|
|
2544
|
+
# otherwise each String is parsed into fields here. Either way the fields
|
|
2545
|
+
# become ticks for the whole array at once, and a cell that is not a
|
|
2546
|
+
# String goes through tick_index.
|
|
2547
|
+
def ticks_from_cells(x, res, format, on_error, seconds: false)
|
|
2548
|
+
kind, per = tick_kind(res)
|
|
2549
|
+
return nil unless kind
|
|
2550
|
+
if format.is_a?(String)
|
|
2551
|
+
text = (x.data_type == CA_OBJECT && !x.face?) ? x : x.to_type(:object)
|
|
2552
|
+
fields = text.__strptime_fields__(format, false)
|
|
2553
|
+
return ticks_from_fields(fields, x, res, format, on_error) if fields
|
|
2554
|
+
end
|
|
2555
|
+
cells = x.flatten.to_a
|
|
2556
|
+
n = cells.size
|
|
2557
|
+
year = Array.new(n, 1970)
|
|
2558
|
+
mon = Array.new(n, 1)
|
|
2559
|
+
day = Array.new(n, 1)
|
|
2560
|
+
sod = Array.new(n, 0)
|
|
2561
|
+
frac = Array.new(n, 0)
|
|
2562
|
+
off = Array.new(n, 0)
|
|
2563
|
+
present = Array.new(n, 0)
|
|
2564
|
+
direct = {}
|
|
2565
|
+
cells.each_with_index do |s, i|
|
|
2566
|
+
next if s.nil? || UNDEF.equal?(s)
|
|
2567
|
+
begin
|
|
2568
|
+
if s.is_a?(String)
|
|
2569
|
+
h = parse_date_fields(s, format)
|
|
2570
|
+
year[i] = h[:year]
|
|
2571
|
+
mon[i] = h[:mon] || 1
|
|
2572
|
+
day[i] = h[:mday] || 1
|
|
2573
|
+
sod[i] = (h[:hour] || 0) * 3600 + (h[:min] || 0) * 60 + (h[:sec] || 0)
|
|
2574
|
+
frac[i] = (h[:sec_fraction] * 1_000_000_000).floor if h[:sec_fraction]
|
|
2575
|
+
off[i] = h[:offset] if h[:offset]
|
|
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
|
|
2581
|
+
else
|
|
2582
|
+
direct[i] = tick_index(s, res, format)
|
|
2583
|
+
end
|
|
2584
|
+
rescue ArgumentError, TypeError
|
|
2585
|
+
raise if on_error == :raise
|
|
2586
|
+
end
|
|
2587
|
+
end
|
|
2588
|
+
raw = CArray.__time_ticks_from_fields__(
|
|
2589
|
+
CA_INT64(year), CA_INT64(mon), CA_INT64(day), CA_INT64(sod),
|
|
2590
|
+
CA_INT64(frac), CA_INT64(off), CA_UINT8(present), kind, per)
|
|
2591
|
+
check_ticks_fit(raw, present, present.count(1), res) { cells }
|
|
2592
|
+
direct.each { |i, t| raw[i] = t }
|
|
2593
|
+
raw.reshape(*x.shape)
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
# Ticks from what __strptime_fields__ read; the cells it deferred are
|
|
2597
|
+
# read one by one through tick_index.
|
|
2598
|
+
def ticks_from_fields(fields, x, res, format, on_error)
|
|
2599
|
+
kind, per = tick_kind(res)
|
|
2600
|
+
year, mon, day, sod, frac, off, present, unreadable, deferred, = fields
|
|
2601
|
+
cells = nil
|
|
2602
|
+
if on_error == :raise && !unreadable.empty?
|
|
2603
|
+
cells = x.flatten.to_a
|
|
2604
|
+
raise ArgumentError, "cannot parse time #{cells[unreadable.first].inspect}"
|
|
2605
|
+
end
|
|
2606
|
+
raw = CArray.__time_ticks_from_fields__(year, mon, day, sod, frac, off,
|
|
2607
|
+
present, kind, per)
|
|
2608
|
+
check_ticks_fit(raw, present, present.sum, res) { cells ||= x.flatten.to_a }
|
|
2609
|
+
unless deferred.empty?
|
|
2610
|
+
cells ||= x.flatten.to_a
|
|
2611
|
+
deferred.each do |i|
|
|
2612
|
+
raw[i] = tick_index(cells[i], res, format)
|
|
2613
|
+
rescue ArgumentError, TypeError
|
|
2614
|
+
raise if on_error == :raise
|
|
2615
|
+
end
|
|
2616
|
+
end
|
|
2617
|
+
raw.reshape(*x.shape)
|
|
2618
|
+
end
|
|
2619
|
+
|
|
2620
|
+
# A tick that does not fit int64 is out of range, not unparseable.
|
|
2621
|
+
def check_ticks_fit(raw, present, present_count, res)
|
|
2622
|
+
return if raw.count_not_masked >= present_count
|
|
2623
|
+
cells = yield
|
|
2624
|
+
i = (0...raw.elements).find { |k| present[k] == 1 && UNDEF.equal?(raw[k]) }
|
|
2625
|
+
raise RangeError, "time #{cells[i].inspect} does not fit int64 ticks of #{res}"
|
|
2626
|
+
end
|
|
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
|
+
|
|
1730
2702
|
# Single-literal build for {.time}: a 1-element CATime, honouring
|
|
1731
2703
|
# the on_error policy (raise, or a masked cell).
|
|
1732
2704
|
def to_time_array(literal, res, format, on_error)
|
|
@@ -1806,10 +2778,13 @@ class CArray
|
|
|
1806
2778
|
|
|
1807
2779
|
# @overload time(x, unit: :s, format: nil, on_error: :raise)
|
|
1808
2780
|
# Builds a {CATime} on the `unit` grid from time value(s). `x`
|
|
1809
|
-
# is either a single literal (Time / ISO 8601 String /
|
|
1810
|
-
#
|
|
2781
|
+
# is either a single literal (Time / ISO 8601 String / Integer /
|
|
2782
|
+
# DateTime) — giving a 1-element result — or a CArray of such
|
|
1811
2783
|
# literals — giving a same-shape result parsed per cell. Parsing is UTC
|
|
1812
|
-
# 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.
|
|
1813
2788
|
#
|
|
1814
2789
|
# A value that cannot be parsed raises by default (`on_error: :raise`);
|
|
1815
2790
|
# pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
|
|
@@ -1817,8 +2792,16 @@ class CArray
|
|
|
1817
2792
|
# becomes UNDEF, regardless of `on_error`.
|
|
1818
2793
|
# @param x [Time, String, Integer, DateTime, CATime::Element, Array, CArray] a literal, a
|
|
1819
2794
|
# Ruby Array of literals, or a CArray of literals.
|
|
1820
|
-
# @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.
|
|
1821
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.
|
|
1822
2805
|
# @param on_error [:raise, :mask] parse-failure policy (default `:raise`).
|
|
1823
2806
|
# @return [CATime] shape `[1]` for a literal, else `x`'s shape.
|
|
1824
2807
|
# @raise [ArgumentError] on an unparseable value when `on_error: :raise`.
|
|
@@ -1827,15 +2810,26 @@ class CArray
|
|
|
1827
2810
|
# CArray.time("2024-06-15", unit: :D) # 1-element
|
|
1828
2811
|
# CArray.time(%w[2024-01-01 2024-02-01], unit: :D) # Ruby Array
|
|
1829
2812
|
# CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)
|
|
1830
|
-
def self.time(x, unit:
|
|
1831
|
-
res = CATime::Resolution.parse(unit)
|
|
2813
|
+
def self.time(x, unit: nil, format: nil, on_error: :raise)
|
|
1832
2814
|
unless %i[raise mask].include?(on_error)
|
|
1833
2815
|
raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
|
|
1834
2816
|
end
|
|
1835
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
|
|
1836
2828
|
unless x.is_a?(CArray)
|
|
1837
2829
|
return CATimeLiteral.to_time_array(x, res, format, on_error)
|
|
1838
2830
|
end
|
|
2831
|
+
raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error, seconds: unit.nil?)
|
|
2832
|
+
return raw.time(unit: res) if raw
|
|
1839
2833
|
raw = CArray.int64(*x.shape)
|
|
1840
2834
|
x.each_index do |*idx|
|
|
1841
2835
|
s = x[*idx]
|
|
@@ -1844,7 +2838,7 @@ class CArray
|
|
|
1844
2838
|
next
|
|
1845
2839
|
end
|
|
1846
2840
|
begin
|
|
1847
|
-
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)
|
|
1848
2842
|
rescue ArgumentError, TypeError
|
|
1849
2843
|
raise if on_error == :raise
|
|
1850
2844
|
raw[*idx] = UNDEF # opt-in parse-mask
|
|
@@ -1861,8 +2855,10 @@ class CArray
|
|
|
1861
2855
|
# int64 array is built; the origin is not stored).
|
|
1862
2856
|
# @param unit [Resolution, Symbol, String] grid resolution.
|
|
1863
2857
|
# @param origin [Time, String, Integer, DateTime, CATime::Element, nil] base instant that
|
|
1864
|
-
# `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.
|
|
1865
2860
|
# @return [CATime]
|
|
2861
|
+
# @raise [ArgumentError] when `origin` falls between two ticks of `unit`.
|
|
1866
2862
|
# @note An `int64` receiver is wrapped zero-copy. A narrower integer type
|
|
1867
2863
|
# is widened to `int64` first (a copy); a `Float` / non-integer type
|
|
1868
2864
|
# raises (cast it explicitly if the truncation is intended).
|
|
@@ -1872,8 +2868,11 @@ class CArray
|
|
|
1872
2868
|
if origin.nil?
|
|
1873
2869
|
CATime.wrap(src, unit: res)
|
|
1874
2870
|
else
|
|
1875
|
-
|
|
1876
|
-
|
|
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)
|
|
1877
2876
|
end
|
|
1878
2877
|
end
|
|
1879
2878
|
|
|
@@ -1899,6 +2898,13 @@ class CArray
|
|
|
1899
2898
|
"array cannot be wrapped (cast it explicitly, e.g. " \
|
|
1900
2899
|
"arr.int64.time(...), if a lossy conversion is intended)"
|
|
1901
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
|
|
1902
2908
|
int64
|
|
1903
2909
|
end
|
|
1904
2910
|
private :as_int64_time_storage
|
|
@@ -2232,6 +3238,7 @@ class CATime
|
|
|
2232
3238
|
m: "minutes", s: "seconds", ms: "milliseconds",
|
|
2233
3239
|
us: "microseconds", ns: "nanoseconds", ps: "picoseconds",
|
|
2234
3240
|
fs: "femtoseconds", as: "attoseconds" }.freeze
|
|
3241
|
+
Ractor.make_shareable(WORD)
|
|
2235
3242
|
|
|
2236
3243
|
# Storage resolutions to fall back on when the unit's own tick cannot
|
|
2237
3244
|
# hold the origin exactly (a day grid anchored at 12:00 needs seconds).
|
|
@@ -2268,7 +3275,8 @@ class CATime
|
|
|
2268
3275
|
@unit = Resolution.parse(unit)
|
|
2269
3276
|
@origin = origin
|
|
2270
3277
|
@storage = resolve_storage
|
|
2271
|
-
|
|
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
|
|
2272
3280
|
freeze
|
|
2273
3281
|
end
|
|
2274
3282
|
|
|
@@ -2470,6 +3478,34 @@ class CATime
|
|
|
2470
3478
|
snap(grid, unit: unit, origin: origin, direction: :floor)
|
|
2471
3479
|
end
|
|
2472
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
|
+
|
|
2473
3509
|
# @overload ceil(grid = nil, unit:, origin: nil)
|
|
2474
3510
|
# {#snap} with `direction: :ceil` -- each element at the bucket head at
|
|
2475
3511
|
# or after it (an element already on a boundary maps to itself).
|