carray 3.0.3 → 3.0.5
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 +469 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_array_pool.c +25 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +14 -26
- data/ext/ca_axis_group.c +27 -27
- data/ext/ca_binop_dispatch.c +1 -29
- data/ext/ca_binop_dispatch.h +0 -1
- data/ext/ca_categorical_iterator.c +231 -153
- data/ext/ca_compare.c +9 -20
- data/ext/ca_composite_dispatch.c +9 -9
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1240 -2000
- data/ext/ca_kernel_iterator.h +502 -519
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +152 -45
- data/ext/ca_obj_bincmp.c +25 -21
- data/ext/ca_obj_binop.c +16 -26
- data/ext/ca_obj_bitarray.c +8 -3
- data/ext/ca_obj_bitfield.c +63 -49
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +7 -7
- data/ext/ca_obj_const_string.c +188 -22
- data/ext/ca_obj_face.c +280 -58
- data/ext/ca_obj_face.h +19 -0
- data/ext/ca_obj_fake.c +15 -29
- data/ext/ca_obj_farray.c +6 -3
- data/ext/ca_obj_field.c +30 -8
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +455 -134
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +113 -67
- data/ext/ca_obj_object.c +210 -52
- data/ext/ca_obj_record.c +15 -7
- data/ext/ca_obj_reduce.c +17 -8
- data/ext/ca_obj_refer.c +42 -35
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +31 -10
- data/ext/ca_obj_select.c +48 -14
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +41 -7
- data/ext/ca_obj_stack.c +180 -34
- data/ext/ca_obj_stride.c +190 -134
- data/ext/ca_obj_string.c +80 -4
- data/ext/ca_obj_tile.c +32 -11
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +13 -7
- data/ext/ca_obj_triop.c +26 -32
- data/ext/ca_obj_window.c +267 -74
- data/ext/ca_op_ipower.c +4 -4
- data/ext/ca_sort_kernels.h +6 -3
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +214 -75
- data/ext/carray_access.c +200 -99
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +113 -6
- data/ext/carray_bincount.c +10 -8
- data/ext/carray_broadcast.c +13 -8
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +108 -0
- data/ext/carray_cast.c +375 -96
- data/ext/carray_cast_func.rb +90 -20
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +18 -9
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +150 -55
- data/ext/carray_count.c +56 -20
- data/ext/carray_element.c +81 -33
- data/ext/carray_factorize.c +138 -84
- data/ext/carray_generate.c +199 -52
- data/ext/carray_histogram.c +63 -18
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +348 -29
- data/ext/carray_kernels_bincmp.c +4453 -0
- data/ext/carray_kernels_binop.c +11368 -0
- data/ext/carray_kernels_init.c +1734 -0
- data/ext/carray_kernels_moncmp.c +2104 -0
- data/ext/carray_kernels_monop.c +18369 -0
- data/ext/carray_kernels_reduce_aggregate.c +23818 -0
- data/ext/carray_kernels_reduce_boolean.c +406 -0
- data/ext/carray_kernels_reduce_cumulative.c +10086 -0
- data/ext/carray_kernels_reduce_extreme.c +17634 -0
- data/ext/carray_kernels_reduce_variance.c +3849 -0
- data/ext/carray_kernels_scan.c +4302 -0
- data/ext/carray_kernels_search.c +33829 -0
- data/ext/carray_kernels_sort.c +10897 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +220 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +337 -94
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +129 -52
- data/ext/carray_order.c +604 -77
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +85 -31
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +87 -45
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +123 -37
- data/ext/carray_sort.c +259 -51
- data/ext/carray_sort_kernel.c +5 -7
- data/ext/carray_struct.c +54 -61
- data/ext/carray_test.c +26 -5
- data/ext/carray_time_text.c +4 -2
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +309 -66
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +403 -350
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +139 -22
- data/lib/carray/autoload_carray.rb +8 -31
- data/lib/carray/autoload_method_extension.rb +14 -5
- data/lib/carray/axis_group.rb +255 -106
- data/lib/carray/basics.rb +105 -17
- data/lib/carray/bincount_nd.rb +12 -2
- data/lib/carray/block_iterator.rb +126 -23
- data/lib/carray/boolean_reduce.rb +17 -17
- data/lib/carray/categorical.rb +195 -59
- data/lib/carray/categorical_iterator.rb +132 -29
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +24 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +9 -1
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +48 -3
- data/lib/carray/frame/column_verbs.rb +71 -0
- data/lib/carray/frame/concat.rb +17 -3
- data/lib/carray/frame/convert.rb +69 -0
- data/lib/carray/frame/csv_parser.rb +111 -17
- data/lib/carray/frame/describe.rb +3 -2
- data/lib/carray/frame/frame.rb +252 -28
- data/lib/carray/frame/group.rb +603 -101
- data/lib/carray/frame/io.rb +117 -28
- data/lib/carray/frame/join.rb +312 -51
- data/lib/carray/frame/records.rb +83 -7
- data/lib/carray/frame/reshape.rb +42 -35
- data/lib/carray/frame/sort.rb +7 -19
- data/lib/carray/frame/verbs.rb +188 -48
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +42 -6
- data/lib/carray/fusion.rb +40 -16
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +31 -8
- data/lib/carray/iterator.rb +85 -7
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +217 -183
- data/lib/carray/mask_gap_fill.rb +63 -57
- data/lib/carray/meld_reduce.rb +12 -14
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +16 -0
- data/lib/carray/methods/bit_string.rb +1 -1
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +3 -1
- data/lib/carray/methods/gather_nd.rb +14 -5
- data/lib/carray/methods/insert_block.rb +2 -2
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +61 -37
- data/lib/carray/methods/join.rb +1 -6
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +47 -51
- data/lib/carray/methods/mode.rb +1 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/resize.rb +3 -3
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +3 -3
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +53 -8
- data/lib/carray/segment_iterator.rb +323 -88
- data/lib/carray/serialize.rb +45 -14
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/stack.rb +15 -12
- data/lib/carray/string.rb +113 -5
- data/lib/carray/string_operation_extension.rb +21 -6
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +24 -11
- data/lib/carray/time.rb +819 -158
- data/lib/carray/window_iterator.rb +122 -16
- data/lib/carray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +4 -0
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +2 -1
- data/yard-stubs/ca_obj_roll.rb +2 -2
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +5 -4
- data/yard-stubs/ca_obj_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +3 -6
- data/yard-stubs/carray_cast.rb +12 -0
- data/yard-stubs/carray_conversion.rb +7 -2
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +17 -10
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +32 -3
- data/yard-stubs/carray_math.rb +41 -5
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +129 -22
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +17 -10
- data/yard-stubs/carray_slab.rb +9 -2
- data/yard-stubs/carray_sort.rb +6 -5
- data/yard-stubs/carray_test.rb +5 -3
- metadata +25 -3
- 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
|
|
|
@@ -106,6 +113,61 @@ module CATimeUnitAlgebra
|
|
|
106
113
|
end
|
|
107
114
|
end
|
|
108
115
|
|
|
116
|
+
# How a refusal names an operand: a Face by its class, any other array as
|
|
117
|
+
# a CArray (a lazy expression's node classes are not the user's), anything
|
|
118
|
+
# else by its class.
|
|
119
|
+
def operand_name(x)
|
|
120
|
+
( x.is_a?(CArray) && !x.face? ) ? "CArray" : x.class.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A single value an operator meets an array with, as a one-cell array
|
|
124
|
+
# marked scalar (a CScalar) of its kind, or nil when it is not one: a
|
|
125
|
+
# duration -- a CATimedelta::Element, or a String that reads as one
|
|
126
|
+
# ("3 days") -- or an instant -- a CATime::Element, a Time, a Date, or any
|
|
127
|
+
# other String. An instant that lands on the +unit+ grid is given in that
|
|
128
|
+
# unit, so a day array minus "2024-01-02" answers in days.
|
|
129
|
+
def scalar_operand(x, unit)
|
|
130
|
+
case x
|
|
131
|
+
when CATimedelta::Element
|
|
132
|
+
CATimedelta.wrap(CScalar.int64 { x.value }, unit: x.unit)
|
|
133
|
+
when String
|
|
134
|
+
res = begin
|
|
135
|
+
CATime::Resolution.parse(x)
|
|
136
|
+
rescue ArgumentError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
return CATimedelta.wrap(CScalar.int64 { res.count }, unit: res.base) if res
|
|
140
|
+
scalar_instant(CATimeLiteral.exact_instant(x), unit)
|
|
141
|
+
when CATime::Element
|
|
142
|
+
CATime.wrap(CScalar.int64 { x.value }, unit: x.unit)
|
|
143
|
+
when Time, Date
|
|
144
|
+
scalar_instant(CATimeLiteral.exact_instant(x), unit)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A duration String added to or subtracted from a time, as one step of its
|
|
149
|
+
# own resolution: "15 minutes" is one 15-minute step, the duration
|
|
150
|
+
# CArray.int64(1).timedelta(unit: "15 minutes") is, so it lands on a
|
|
151
|
+
# "10 minutes" grid or is refused in the same way. nil for anything else.
|
|
152
|
+
def step_operand(x)
|
|
153
|
+
return nil unless x.is_a?(String)
|
|
154
|
+
res = begin
|
|
155
|
+
CATime::Resolution.parse(x)
|
|
156
|
+
rescue ArgumentError
|
|
157
|
+
return nil
|
|
158
|
+
end
|
|
159
|
+
CATimedelta.wrap(CScalar.int64 { 1 }, unit: res)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def scalar_instant(t, unit)
|
|
163
|
+
v = begin
|
|
164
|
+
convert_instant!(t.parent, t.unit, unit)[0]
|
|
165
|
+
rescue ArgumentError
|
|
166
|
+
return CATime.wrap(CScalar.int64 { t.parent[0] }, unit: t.unit)
|
|
167
|
+
end
|
|
168
|
+
CATime.wrap(CScalar.int64 { v }, unit: unit)
|
|
169
|
+
end
|
|
170
|
+
|
|
109
171
|
# seconds- (fixed) or months- (calendar) per base tick.
|
|
110
172
|
def base_ratio(base)
|
|
111
173
|
FIXED[base] || CALENDAR[base]
|
|
@@ -254,7 +316,8 @@ module CATimeUnitAlgebra
|
|
|
254
316
|
# CATimedelta#to_unit and by dt +/- td, where the result keeps the time's
|
|
255
317
|
# unit and a finer duration is truncated to it (a :D time + a 5 h duration
|
|
256
318
|
# is + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
|
|
257
|
-
# duration has no fixed ratio to a fixed unit
|
|
319
|
+
# duration has no fixed ratio to a fixed unit; dt +/- a month moves the date
|
|
320
|
+
# through shift_months instead).
|
|
258
321
|
def convert_scale_trunc(storage, from, to)
|
|
259
322
|
a = res(from); b = res(to)
|
|
260
323
|
return storage if a == b
|
|
@@ -269,6 +332,67 @@ module CATimeUnitAlgebra
|
|
|
269
332
|
r.denominator == 1 ? scaled : trunc_divide(scaled, r.denominator)
|
|
270
333
|
end
|
|
271
334
|
|
|
335
|
+
# A duration brought onto a time's grid for dt +/- td. A duration finer
|
|
336
|
+
# than the grid is truncated toward zero (a :D time + 5 h is + 0 days), but
|
|
337
|
+
# one whose tick is coarser and not a whole number of grid ticks ("1 day"
|
|
338
|
+
# on a "7 hours" grid, "15 minutes" on a "10 minutes" grid) must land on
|
|
339
|
+
# the grid exactly: cutting it would move the time by part of its own
|
|
340
|
+
# tick, which no rounding of the time asked for.
|
|
341
|
+
def duration_on_grid(storage, from, to)
|
|
342
|
+
r = ratio(res(from), res(to))
|
|
343
|
+
return convert_scale_trunc(storage, from, to) if r.nil? || r <= 1 || r.denominator == 1
|
|
344
|
+
scaled = widen(storage, r.numerator)
|
|
345
|
+
unless (scaled % r.denominator).eq(0).all
|
|
346
|
+
raise ArgumentError,
|
|
347
|
+
"a duration in #{res(from)} does not land on the #{res(to)} grid " \
|
|
348
|
+
"(it is not a whole number of #{res(to)} ticks); convert the time " \
|
|
349
|
+
"to a grid both share first"
|
|
350
|
+
end
|
|
351
|
+
scaled / r.denominator
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# a + b and a - b on int64 storage (b a CArray or an Integer), raising
|
|
355
|
+
# instead of wrapping around when a result leaves int64. An overflow is a
|
|
356
|
+
# result whose sign differs from both terms of the sum (a and b for +, a
|
|
357
|
+
# and -b for -).
|
|
358
|
+
def checked_add(a, b)
|
|
359
|
+
check_term(b, "+")
|
|
360
|
+
r = a + b
|
|
361
|
+
overflow_raise("+") if ((a ^ r) & (b ^ r)).lt(0).any
|
|
362
|
+
r
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def checked_sub(a, b)
|
|
366
|
+
check_term(b, "-")
|
|
367
|
+
r = a - b
|
|
368
|
+
overflow_raise("-") if ((a ^ b) & (a ^ r)).lt(0).any
|
|
369
|
+
r
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def checked_mul(a, k)
|
|
373
|
+
check_term(k, "*")
|
|
374
|
+
lo = a.min
|
|
375
|
+
unless lo == UNDEF
|
|
376
|
+
[lo, a.max].each do |x|
|
|
377
|
+
overflow_raise("*") unless (INT64_MIN..INT64_MAX).cover?(Integer(x) * k)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
a * k
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def check_term(b, op)
|
|
384
|
+
overflow_raise(op) if b.is_a?(Integer) && !(INT64_MIN..INT64_MAX).cover?(b)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def overflow_raise(op)
|
|
388
|
+
raise RangeError,
|
|
389
|
+
"time arithmetic overflows int64 (#{op}): the result is outside " \
|
|
390
|
+
"the range of the storage unit; use a coarser unit"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
INT64_MIN = -2**63
|
|
394
|
+
INT64_MAX = 2**63 - 1
|
|
395
|
+
|
|
272
396
|
# Truncating integer division of an int64 CArray (toward zero). CArray
|
|
273
397
|
# `/` floors toward the past, which is what an instant wants; a duration
|
|
274
398
|
# is a magnitude and shrinks toward zero instead.
|
|
@@ -297,6 +421,40 @@ module CATimeUnitAlgebra
|
|
|
297
421
|
end
|
|
298
422
|
end
|
|
299
423
|
|
|
424
|
+
# A time on a fixed grid moved by whole months, as Ruby's Date#>> moves a
|
|
425
|
+
# date: the year and the month move, the day stays unless the new month is
|
|
426
|
+
# shorter (then it is that month's last day), and the time of day stays.
|
|
427
|
+
# The grid has to tile a day, so the result lands on it; on any other grid
|
|
428
|
+
# (:W, "3 days", "7 hours") a month moves a time off its grid, and that
|
|
429
|
+
# raises. +months+ is an Integer or an int64 CArray of the time's shape.
|
|
430
|
+
def shift_months(storage, from, months)
|
|
431
|
+
rd = ratio(CATime::Resolution.new(1, :D), from) # ticks of `from` per day
|
|
432
|
+
unless rd && rd.denominator == 1
|
|
433
|
+
raise ArgumentError,
|
|
434
|
+
"cannot move a time on the #{from} grid by months: a day is not a " \
|
|
435
|
+
"whole number of its ticks, so the result would be off the grid"
|
|
436
|
+
end
|
|
437
|
+
n = rd.numerator
|
|
438
|
+
days = storage / n # floors toward the past
|
|
439
|
+
rest = storage - days * n # the time of day, in ticks
|
|
440
|
+
y, m, d = CATimeCivil.civil_from_days(days)
|
|
441
|
+
ord = y * 12 + (m - 1) + months # month ordinal after the move
|
|
442
|
+
ny = ord / 12
|
|
443
|
+
nm = ord - ny * 12 + 1
|
|
444
|
+
ones = CArray.int64(*storage.shape) { 1 }
|
|
445
|
+
head = CATimeCivil.days_from_civil(ny, nm, ones)
|
|
446
|
+
ord1 = ord + 1
|
|
447
|
+
ny1 = ord1 / 12
|
|
448
|
+
last = CATimeCivil.days_from_civil(ny1, ord1 - ny1 * 12 + 1, ones) - head
|
|
449
|
+
widen(head + d.pmin(last) - 1, n) + rest
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Whole months in a calendar duration of +res+: its count of months or of
|
|
453
|
+
# years.
|
|
454
|
+
def months_per_tick(res)
|
|
455
|
+
(res.count * CALENDAR.fetch(res.base)).to_i
|
|
456
|
+
end
|
|
457
|
+
|
|
300
458
|
# calendar time (Resolution `from`) -> days since the epoch (int64
|
|
301
459
|
# CArray). Folds the resolution count (value = count-Y/M buckets).
|
|
302
460
|
def calendar_days_since_epoch(storage, from)
|
|
@@ -325,25 +483,29 @@ module CATimeUnitAlgebra
|
|
|
325
483
|
widen(calendar_days_since_epoch(storage, from), r.numerator)
|
|
326
484
|
end
|
|
327
485
|
|
|
486
|
+
# Whole days since the epoch (floored) of fixed-length ticks, and the
|
|
487
|
+
# remainder that says whether each instant sits on a day boundary. A tick
|
|
488
|
+
# need not divide a day, nor be a whole number of days ("7 hours",
|
|
489
|
+
# "36 hours", "7 seconds"): the count is scaled to a grid both share.
|
|
490
|
+
def fixed_storage_days(storage, from)
|
|
491
|
+
rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
|
|
492
|
+
scaled = widen(storage, rd.denominator)
|
|
493
|
+
n = rd.numerator
|
|
494
|
+
{ days: scaled / n, rest: scaled % n }
|
|
495
|
+
end
|
|
496
|
+
|
|
328
497
|
# fixed-length grid -> calendar time: the coarsening half of
|
|
329
498
|
# convert_instant!, exact-or-raise. Every instant must land on a day
|
|
330
499
|
# boundary and then on the calendar boundary itself (the 1st, and
|
|
331
500
|
# January too for :Y), since a mid-month instant has no :M value.
|
|
332
501
|
def convert_instant_fixed_to_calendar(storage, from, to)
|
|
333
|
-
|
|
334
|
-
days
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"(instant is not on a day boundary)"
|
|
341
|
-
end
|
|
342
|
-
storage / n
|
|
343
|
-
else # coarser than a day (:W)
|
|
344
|
-
storage * (from.tick_ratio / 86400r).to_i
|
|
345
|
-
end
|
|
346
|
-
y, m, d = CATimeCivil.civil_from_days(days)
|
|
502
|
+
days = fixed_storage_days(storage, from)
|
|
503
|
+
unless (days[:rest]).eq(0).all
|
|
504
|
+
raise ArgumentError,
|
|
505
|
+
"cannot convert time #{from} to #{to} without loss " \
|
|
506
|
+
"(instant is not on a day boundary)"
|
|
507
|
+
end
|
|
508
|
+
y, m, d = CATimeCivil.civil_from_days(days[:days])
|
|
347
509
|
on_boundary = d.eq(1)
|
|
348
510
|
on_boundary &= m.eq(1) if to.base == :Y
|
|
349
511
|
unless on_boundary.all
|
|
@@ -390,14 +552,7 @@ module CATimeUnitAlgebra
|
|
|
390
552
|
# month, whatever day and time it carries. The flooring counterpart of
|
|
391
553
|
# convert_instant_fixed_to_calendar.
|
|
392
554
|
def convert_instant_fixed_to_calendar_floor(storage, from, to)
|
|
393
|
-
|
|
394
|
-
days =
|
|
395
|
-
if rd.denominator == 1
|
|
396
|
-
storage / rd.numerator
|
|
397
|
-
else # coarser than a day (:W)
|
|
398
|
-
storage * (from.tick_ratio / 86400r).to_i
|
|
399
|
-
end
|
|
400
|
-
y, m, = CATimeCivil.civil_from_days(days)
|
|
555
|
+
y, m, = CATimeCivil.civil_from_days(fixed_storage_days(storage, from)[:days])
|
|
401
556
|
ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
|
|
402
557
|
to.count > 1 ? ord / to.count : ord
|
|
403
558
|
end
|
|
@@ -443,8 +598,64 @@ module CATimeSurfaceFill
|
|
|
443
598
|
|
|
444
599
|
end
|
|
445
600
|
|
|
601
|
+
# The part of to_comparable the two time Faces share: values written one by
|
|
602
|
+
# one (a Ruby Array) and an array that is not of this kind (an object column,
|
|
603
|
+
# a string column) are each read as a value of this Face by to_comparable, an
|
|
604
|
+
# UNDEF or a masked cell kept masked. Nil for any other operand, which the Face's own
|
|
605
|
+
# to_comparable decides.
|
|
606
|
+
module CATimeOperandValues
|
|
607
|
+
|
|
608
|
+
# The reference and the operand of a comparison (eq, <, count, search,
|
|
609
|
+
# linear_section, pmax ...) brought to one unit: the operand read in its
|
|
610
|
+
# own unit, then the reference's unit when the operand sits on its grid
|
|
611
|
+
# (the reference is not converted), else the finer unit the two share, as
|
|
612
|
+
# is_in and the set operations meet. A value that is not of this kind is
|
|
613
|
+
# refused as to_comparable refuses it. An assignment, which has to land on
|
|
614
|
+
# the reference's grid, reconciles with to_comparable instead.
|
|
615
|
+
# @api private
|
|
616
|
+
def comparable_pair (operand)
|
|
617
|
+
lifted = lift_operand(operand)
|
|
618
|
+
return [self, lifted] if lifted.unit == unit
|
|
619
|
+
narrowed = lifted.to_unit(unit)
|
|
620
|
+
if narrowed.to_unit(lifted.unit).parent.eq(lifted.parent).all
|
|
621
|
+
return [self, narrowed]
|
|
622
|
+
end
|
|
623
|
+
in_common_unit_with(lifted)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
private
|
|
627
|
+
|
|
628
|
+
# The operand as an array of this kind in its own unit: one value read as
|
|
629
|
+
# it is written, an Array or an array of another kind read value by value
|
|
630
|
+
# in the unit they share, an UNDEF or a masked cell kept masked.
|
|
631
|
+
def lift_operand (operand)
|
|
632
|
+
return operand if operand.is_a?(self.class)
|
|
633
|
+
case operand
|
|
634
|
+
when Array
|
|
635
|
+
# A nested Array keeps its shape; reading it row by row would land
|
|
636
|
+
# each row as one value.
|
|
637
|
+
return lift_operand(CA_OBJECT(operand)) if operand.any?(Array)
|
|
638
|
+
lifted = operand.map { |v| UNDEF.equal?(v) ? nil : lift_operand(v) }
|
|
639
|
+
units = lifted.compact.map(&:unit)
|
|
640
|
+
u = units.empty? ? unit : units.reduce { |a, b| common_unit(a, b) }
|
|
641
|
+
self.class.wrap(CA_INT64(lifted.map { |x| x ? x.to_unit(u).parent[0] : UNDEF }),
|
|
642
|
+
unit: u)
|
|
643
|
+
when CArray
|
|
644
|
+
lift_operand(operand.to_a.flatten).reshape(*operand.shape)
|
|
645
|
+
else
|
|
646
|
+
lift_value(operand) || to_comparable(operand)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def reconcile_values (operand)
|
|
651
|
+
to_comparable(lift_operand(operand))
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
end
|
|
655
|
+
|
|
446
656
|
class CATime
|
|
447
657
|
include CATimeSurfaceFill
|
|
658
|
+
include CATimeOperandValues
|
|
448
659
|
# @overload new(*shape, unit: :ns)
|
|
449
660
|
# Allocates a new int64 storage CArray of the given `shape` and
|
|
450
661
|
# wraps it as a CATime Face with the given `unit`. The
|
|
@@ -465,7 +676,7 @@ class CATime
|
|
|
465
676
|
# @param unit [Resolution, Symbol, String] tick resolution.
|
|
466
677
|
# @return [CATime]
|
|
467
678
|
def self.wrap(raw, unit: :ns)
|
|
468
|
-
|
|
679
|
+
wrap_storage(raw, unit)
|
|
469
680
|
end
|
|
470
681
|
|
|
471
682
|
# Element-return class: a single time element carries both its
|
|
@@ -497,8 +708,6 @@ class CATime
|
|
|
497
708
|
# @return [Time] the instant this scalar denotes (UTC). For a calendar
|
|
498
709
|
# unit this is the first instant (midnight of day 1) of the granule.
|
|
499
710
|
def to_time
|
|
500
|
-
require 'time'
|
|
501
|
-
require 'date'
|
|
502
711
|
case unit.base
|
|
503
712
|
when :Y then d = epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
504
713
|
when :M then d = epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
@@ -510,7 +719,6 @@ class CATime
|
|
|
510
719
|
# @return [Date] the date this scalar denotes (UTC). A sub-day unit is
|
|
511
720
|
# floored to its day.
|
|
512
721
|
def to_date
|
|
513
|
-
require 'date'
|
|
514
722
|
case unit.base
|
|
515
723
|
when :Y then epoch_date.next_year(value * unit.count)
|
|
516
724
|
when :M then epoch_date.next_month(value * unit.count)
|
|
@@ -559,6 +767,10 @@ class CATime
|
|
|
559
767
|
to_time <=> other.getutc
|
|
560
768
|
when (defined?(DateTime) ? DateTime : nil)
|
|
561
769
|
to_time <=> other.to_time.getutc
|
|
770
|
+
when (defined?(Date) ? Date : nil)
|
|
771
|
+
# a Date names the midnight (UTC) that begins it, as it does when an
|
|
772
|
+
# array is compared with it
|
|
773
|
+
to_time.to_r <=> CATimeLiteral.epoch_seconds(other)
|
|
562
774
|
end
|
|
563
775
|
rescue ArgumentError
|
|
564
776
|
nil
|
|
@@ -576,18 +788,27 @@ class CATime
|
|
|
576
788
|
[value, unit].hash
|
|
577
789
|
end
|
|
578
790
|
|
|
791
|
+
# Marshal carries the tick and its resolution.
|
|
792
|
+
def marshal_dump = [value, unit.count, unit.base]
|
|
793
|
+
|
|
794
|
+
def marshal_load(data)
|
|
795
|
+
v, count, base = data
|
|
796
|
+
initialize(v, CATime::Resolution.new(count, base))
|
|
797
|
+
end
|
|
798
|
+
|
|
579
799
|
# @overload +(td)
|
|
580
800
|
# Returns this instant advanced by a {CATimedelta::Element}. The time
|
|
581
801
|
# is the anchor: the result keeps `self`'s unit and the duration is
|
|
582
802
|
# converted into it, truncated toward zero when finer (a :D time + a
|
|
583
803
|
# 5 h duration is + 0 days; + 30 h is + 1 day) -- the same rule as the
|
|
584
|
-
# array {CATime#+}.
|
|
585
|
-
#
|
|
586
|
-
#
|
|
587
|
-
#
|
|
804
|
+
# array {CATime#+}. A month or year duration on a fixed grid moves
|
|
805
|
+
# the date as Ruby's `Date#>>` does (the day stays unless the month is
|
|
806
|
+
# shorter, the time of day stays); a fixed duration on a month or year
|
|
807
|
+
# grid raises, as does a month on a grid a day does not divide.
|
|
588
808
|
# @param td [CATimedelta::Element]
|
|
589
809
|
# @return [CATime::Element]
|
|
590
|
-
# @raise [TypeError, ArgumentError] on a non-timedelta
|
|
810
|
+
# @raise [TypeError, ArgumentError] on a non-timedelta operand, or a
|
|
811
|
+
# pair that has no answer on self's grid.
|
|
591
812
|
def +(td)
|
|
592
813
|
shifted_by_duration(td, 1)
|
|
593
814
|
end
|
|
@@ -603,9 +824,13 @@ class CATime
|
|
|
603
824
|
case other
|
|
604
825
|
when Element
|
|
605
826
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
606
|
-
|
|
607
|
-
|
|
827
|
+
v = instant_in(u) - other.instant_in(u)
|
|
828
|
+
CATimeUnitAlgebra.check_term(v, "-")
|
|
829
|
+
CATimedelta::Element.new(v, u)
|
|
830
|
+
when CATimedelta::Element, CATimedelta
|
|
608
831
|
shifted_by_duration(other, -1)
|
|
832
|
+
when CATime
|
|
833
|
+
-(other - self)
|
|
609
834
|
else
|
|
610
835
|
raise TypeError, "CATime::Element - #{other.class} is not allowed"
|
|
611
836
|
end
|
|
@@ -624,22 +849,33 @@ class CATime
|
|
|
624
849
|
|
|
625
850
|
# s +/- td: the time is the anchor, so the result keeps self's unit and
|
|
626
851
|
# the duration is converted into it (truncated toward zero when finer; a
|
|
627
|
-
#
|
|
852
|
+
# month or year duration on a fixed grid moves the date by months).
|
|
853
|
+
# Mirrors the array CATime +/-.
|
|
628
854
|
def shifted_by_duration(td, sign)
|
|
855
|
+
# An array of durations gives an array of instants, anchored here.
|
|
856
|
+
return sign > 0 ? td + self : (-td) + self if td.is_a?(CATimedelta)
|
|
629
857
|
unless td.is_a?(CATimedelta::Element)
|
|
630
858
|
raise TypeError,
|
|
631
859
|
"CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
|
|
632
860
|
"allowed (add / subtract a CATimedelta)"
|
|
633
861
|
end
|
|
634
|
-
|
|
635
|
-
|
|
862
|
+
if CATimeUnitAlgebra::CALENDAR.key?(td.unit.base) &&
|
|
863
|
+
!CATimeUnitAlgebra::CALENDAR.key?(unit.base)
|
|
864
|
+
# months on a fixed grid move the date as Date#>> does, as the
|
|
865
|
+
# array does
|
|
866
|
+
months = sign * td.value * CATimeUnitAlgebra.months_per_tick(td.unit)
|
|
867
|
+
return Element.new(CATimeUnitAlgebra.shift_months(CA_INT64([value]), unit, months)[0], unit)
|
|
868
|
+
end
|
|
869
|
+
tv = CATimeUnitAlgebra.duration_on_grid(CA_INT64([td.value]), td.unit, unit)[0]
|
|
870
|
+
v = value + sign * tv
|
|
871
|
+
CATimeUnitAlgebra.check_term(v, sign > 0 ? "+" : "-")
|
|
872
|
+
Element.new(v, unit)
|
|
636
873
|
end
|
|
637
874
|
|
|
638
875
|
# Base epoch as a Date. next_month / next_year clamp end-of-month
|
|
639
876
|
# (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
|
|
640
877
|
# do not move the base off the first of the month without revisiting.
|
|
641
878
|
def epoch_date
|
|
642
|
-
require 'date'
|
|
643
879
|
Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
|
|
644
880
|
end
|
|
645
881
|
|
|
@@ -662,6 +898,7 @@ end
|
|
|
662
898
|
|
|
663
899
|
class CATimedelta
|
|
664
900
|
include CATimeSurfaceFill
|
|
901
|
+
include CATimeOperandValues
|
|
665
902
|
# @overload new(*shape, unit: :ns)
|
|
666
903
|
# Allocates a new int64 storage CArray of the given `shape` and
|
|
667
904
|
# wraps it as a CATimedelta Face with the given `unit`.
|
|
@@ -679,7 +916,7 @@ class CATimedelta
|
|
|
679
916
|
# @param unit [CATime::Resolution, Symbol, String] tick resolution.
|
|
680
917
|
# @return [CATimedelta]
|
|
681
918
|
def self.wrap(raw, unit: :ns)
|
|
682
|
-
|
|
919
|
+
wrap_storage(raw, unit)
|
|
683
920
|
end
|
|
684
921
|
|
|
685
922
|
# A single duration: an integer tick count plus the resolution those ticks
|
|
@@ -746,6 +983,14 @@ class CATimedelta
|
|
|
746
983
|
[value, unit].hash
|
|
747
984
|
end
|
|
748
985
|
|
|
986
|
+
# Marshal carries the tick and its resolution.
|
|
987
|
+
def marshal_dump = [value, unit.count, unit.base]
|
|
988
|
+
|
|
989
|
+
def marshal_load(data)
|
|
990
|
+
v, count, base = data
|
|
991
|
+
initialize(v, CATime::Resolution.new(count, base))
|
|
992
|
+
end
|
|
993
|
+
|
|
749
994
|
# @overload +(other)
|
|
750
995
|
# Returns the sum of two durations, promoting to the finer unit. A
|
|
751
996
|
# cross-group operand (a :M + a :s duration) has no fixed relation and
|
|
@@ -766,7 +1011,9 @@ class CATimedelta
|
|
|
766
1011
|
# @return [CATimedelta::Element]
|
|
767
1012
|
def *(n)
|
|
768
1013
|
raise TypeError, "CATimedelta::Element * #{n.class} is not allowed (Integer only)" unless n.is_a?(Integer)
|
|
769
|
-
|
|
1014
|
+
v = value * n
|
|
1015
|
+
CATimeUnitAlgebra.check_term(v, "*")
|
|
1016
|
+
Element.new(v, unit)
|
|
770
1017
|
end
|
|
771
1018
|
|
|
772
1019
|
# @overload /(other)
|
|
@@ -801,12 +1048,21 @@ class CATimedelta
|
|
|
801
1048
|
private
|
|
802
1049
|
|
|
803
1050
|
def combined_with_duration(other, sign)
|
|
1051
|
+
# An array gives an array: durations by durations, and instants for a
|
|
1052
|
+
# duration added to times (subtracting times from a duration is not
|
|
1053
|
+
# defined).
|
|
1054
|
+
case other
|
|
1055
|
+
when CATimedelta then return sign > 0 ? other + self : -(other - self)
|
|
1056
|
+
when CATime then return other + self if sign > 0
|
|
1057
|
+
end
|
|
804
1058
|
unless other.is_a?(Element)
|
|
805
1059
|
raise TypeError,
|
|
806
1060
|
"CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
|
|
807
1061
|
end
|
|
808
1062
|
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
809
|
-
|
|
1063
|
+
v = duration_in(u) + sign * other.duration_in(u)
|
|
1064
|
+
CATimeUnitAlgebra.check_term(v, sign > 0 ? "+" : "-")
|
|
1065
|
+
Element.new(v, u)
|
|
810
1066
|
end
|
|
811
1067
|
end
|
|
812
1068
|
end
|
|
@@ -836,7 +1092,7 @@ class CATime
|
|
|
836
1092
|
# resolution (see §4 of docs/CATime.md).
|
|
837
1093
|
# @return [CArray]
|
|
838
1094
|
def ticks
|
|
839
|
-
|
|
1095
|
+
face_storage
|
|
840
1096
|
end
|
|
841
1097
|
|
|
842
1098
|
# @overload to_unit(unit)
|
|
@@ -866,29 +1122,46 @@ class CATime
|
|
|
866
1122
|
# @raise [RangeError] when the widened ticks overflow int64.
|
|
867
1123
|
def to_unit(unit)
|
|
868
1124
|
to = CATime::Resolution.parse(unit)
|
|
869
|
-
CATimeUnitAlgebra.convert_instant_floor(
|
|
1125
|
+
CATimeUnitAlgebra.convert_instant_floor(face_storage, self.unit, to).time(unit: to)
|
|
870
1126
|
end
|
|
871
1127
|
|
|
872
1128
|
# @overload +(other)
|
|
873
1129
|
# Returns `self + other` for a {CATimedelta}: the time is the
|
|
874
1130
|
# anchor, so the result keeps `self`'s unit and the duration is converted
|
|
875
1131
|
# into it (a duration finer than `self`'s unit is truncated to it; a
|
|
876
|
-
#
|
|
877
|
-
#
|
|
878
|
-
#
|
|
1132
|
+
# coarser one that is not a whole number of `self`'s ticks raises). A
|
|
1133
|
+
# month or year duration on a fixed grid moves each date as Ruby's
|
|
1134
|
+
# `Date#>>` does: the day stays unless the new month is shorter (then it
|
|
1135
|
+
# is that month's last day), and the time of day stays, so
|
|
1136
|
+
# `2024-01-31 + 1 month` is `2024-02-29`. The grid has to divide a day
|
|
1137
|
+
# into whole ticks; on any other (`:W`, `"3 days"`) a month raises, as a
|
|
1138
|
+
# fixed duration on a month or year grid does. Adding two datetimes is
|
|
1139
|
+
# ill-defined. A single duration -- a {CATimedelta::Element} or a String
|
|
1140
|
+
# such as `"3 days"` -- is added to every cell; a String is one step of
|
|
1141
|
+
# its own resolution, so `"15 minutes"` on a `"10 minutes"` grid raises
|
|
1142
|
+
# as `CArray.int64(1).timedelta(unit: "15 minutes")` does.
|
|
1143
|
+
# @param other [CATimedelta, CATimedelta::Element, String]
|
|
879
1144
|
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
880
1145
|
# duration array)
|
|
881
1146
|
# @return [CATime]
|
|
882
1147
|
# @raise [TypeError] on a non-timedelta operand.
|
|
1148
|
+
# @raise [ArgumentError] when the duration does not land on `self`'s grid.
|
|
1149
|
+
# @raise [RangeError] when a result leaves the range of int64 ticks.
|
|
883
1150
|
def +(other)
|
|
884
1151
|
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1152
|
+
other = CATimeUnitAlgebra.step_operand(other) ||
|
|
1153
|
+
CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
885
1154
|
case other
|
|
886
1155
|
when CATimedelta
|
|
887
|
-
|
|
1156
|
+
return shifted_by_months(other, 1) if moved_by_months?(other)
|
|
1157
|
+
d = CATimeUnitAlgebra.duration_on_grid(other.parent, other.unit, unit)
|
|
1158
|
+
d = d[0] if other.scalar?
|
|
1159
|
+
CATimeUnitAlgebra.checked_add(face_storage, d).time(unit: unit)
|
|
888
1160
|
when CATime
|
|
889
1161
|
raise TypeError, "CATime + CATime is ill-defined"
|
|
890
1162
|
else
|
|
891
|
-
raise TypeError, "CATime + #{given
|
|
1163
|
+
raise TypeError, "CATime + #{CATimeUnitAlgebra.operand_name(given)} is not allowed: add a duration " \
|
|
1164
|
+
"(a CATimedelta, a CATimedelta::Element or a String such as \"3 days\")"
|
|
892
1165
|
end
|
|
893
1166
|
end
|
|
894
1167
|
|
|
@@ -896,27 +1169,92 @@ class CATime
|
|
|
896
1169
|
# Subtracting a {CATimedelta} yields a {CATime} at `self`'s unit (the
|
|
897
1170
|
# duration is converted into it, truncated when finer); subtracting
|
|
898
1171
|
# another {CATime} yields a {CATimedelta} at the finer of the two
|
|
899
|
-
# units (cross-group falls to the fixed unit).
|
|
900
|
-
#
|
|
1172
|
+
# units (cross-group falls to the fixed unit). A single duration (a
|
|
1173
|
+
# {CATimedelta::Element}, `"3 days"`) or time (a {CATime::Element}, a
|
|
1174
|
+
# `Time`, a `Date`, `"2024-01-02"`) is subtracted from every cell.
|
|
1175
|
+
# @param other [CATimedelta, CATime, CATimedelta::Element, CATime::Element, Time, Date, String]
|
|
901
1176
|
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
902
1177
|
# duration or timestamp array)
|
|
903
1178
|
# @return [CATime, CATimedelta]
|
|
904
1179
|
# @raise [TypeError] on an unsupported operand.
|
|
905
1180
|
def -(other)
|
|
906
1181
|
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1182
|
+
other = CATimeUnitAlgebra.step_operand(other) ||
|
|
1183
|
+
CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
907
1184
|
case other
|
|
908
1185
|
when CATimedelta
|
|
909
|
-
|
|
1186
|
+
return shifted_by_months(other, -1) if moved_by_months?(other)
|
|
1187
|
+
d = CATimeUnitAlgebra.duration_on_grid(other.parent, other.unit, unit)
|
|
1188
|
+
d = d[0] if other.scalar?
|
|
1189
|
+
CATimeUnitAlgebra.checked_sub(face_storage, d).time(unit: unit)
|
|
910
1190
|
when CATime
|
|
911
1191
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
912
|
-
a = CATimeUnitAlgebra.convert_instant!(
|
|
1192
|
+
a = CATimeUnitAlgebra.convert_instant!(face_storage, unit, u)
|
|
913
1193
|
b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
|
|
914
|
-
|
|
1194
|
+
b = b[0] if other.scalar?
|
|
1195
|
+
CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
|
|
915
1196
|
else
|
|
916
|
-
raise TypeError, "CATime - #{given
|
|
1197
|
+
raise TypeError, "CATime - #{CATimeUnitAlgebra.operand_name(given)} is not allowed: subtract a duration " \
|
|
1198
|
+
"(a CATimedelta, a CATimedelta::Element or a String such as \"3 days\") " \
|
|
1199
|
+
"or a time (a CATime, a CATime::Element, a Time, a Date or a String)"
|
|
917
1200
|
end
|
|
918
1201
|
end
|
|
919
1202
|
|
|
1203
|
+
# Whether a duration moves this time by calendar months: a month or year
|
|
1204
|
+
# duration on a time whose grid is fixed-length. A time on a month or year
|
|
1205
|
+
# grid adds it as months already, and a fixed duration on a calendar grid
|
|
1206
|
+
# is refused by the scale conversion.
|
|
1207
|
+
private def moved_by_months? (td)
|
|
1208
|
+
CATimeUnitAlgebra::CALENDAR.key?(td.unit.base) &&
|
|
1209
|
+
!CATimeUnitAlgebra::CALENDAR.key?(unit.base)
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
# self moved by the months of +td+ (+sign+ 1 to add, -1 to subtract), as
|
|
1213
|
+
# Ruby's Date#>> moves a date: the day is kept unless the month is shorter.
|
|
1214
|
+
private def shifted_by_months (td, sign)
|
|
1215
|
+
months = td.parent.int64 * (sign * CATimeUnitAlgebra.months_per_tick(td.unit))
|
|
1216
|
+
months = months[0] if td.scalar?
|
|
1217
|
+
CATimeUnitAlgebra.shift_months(face_storage, unit, months).time(unit: unit)
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
# @overload pmax(other)
|
|
1221
|
+
# The later of each cell and `other`, cell by cell: `other` is a time
|
|
1222
|
+
# array of the same shape, or one time (a {CATime::Element}, a `Time`, a
|
|
1223
|
+
# `Date`, `"2024-01-02"`) for every cell. The two meet as a comparison
|
|
1224
|
+
# does, so the result is in `self`'s unit when `other` sits on its grid
|
|
1225
|
+
# and in the finer unit the two share otherwise. A masked cell on either
|
|
1226
|
+
# side gives a masked cell.
|
|
1227
|
+
# @param other [CATime, CATime::Element, Time, Date, String]
|
|
1228
|
+
# @return [CATime]
|
|
1229
|
+
# @raise [TypeError] when `other` is not a time.
|
|
1230
|
+
def pmax (other)
|
|
1231
|
+
pick_by_order(other) { |a, b| a < b }
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
# @overload pmin(other)
|
|
1235
|
+
# The earlier of each cell and `other`; see {#pmax}.
|
|
1236
|
+
# @param other [CATime, CATime::Element, Time, Date, String]
|
|
1237
|
+
# @return [CATime]
|
|
1238
|
+
# @raise [TypeError] when `other` is not a time.
|
|
1239
|
+
def pmin (other)
|
|
1240
|
+
pick_by_order(other) { |a, b| a > b }
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
# A time has no NaN, so maximum / minimum (which let a NaN win) are pmax /
|
|
1244
|
+
# pmin, and fmax / fmin are their other names.
|
|
1245
|
+
alias_method :maximum, :pmax
|
|
1246
|
+
alias_method :minimum, :pmin
|
|
1247
|
+
alias_method :fmax, :pmax
|
|
1248
|
+
alias_method :fmin, :pmin
|
|
1249
|
+
|
|
1250
|
+
# `other` where the block says self's cell loses to it, else self's cell.
|
|
1251
|
+
private def pick_by_order (other)
|
|
1252
|
+
return copy.tap { |out| out[] = UNDEF } if UNDEF.equal?(other)
|
|
1253
|
+
a, b = comparable_pair(other)
|
|
1254
|
+
b = b[0] unless other.is_a?(CArray) || other.is_a?(Array)
|
|
1255
|
+
yield(a, b).then_else(b, a)
|
|
1256
|
+
end
|
|
1257
|
+
|
|
920
1258
|
# Element-wise comparison (< <= > >= <=>) rides the core comparison Face
|
|
921
1259
|
# gate (ca_face_reconcile_comparison in ext/ca_obj_face.c): the inherited
|
|
922
1260
|
# CArray operators descend to storage and reconcile a CATime RHS in a
|
|
@@ -942,8 +1280,9 @@ class CATime
|
|
|
942
1280
|
# they run on the storage ticks and round the result back to the nearest
|
|
943
1281
|
# tick of `unit`. A caller who needs a finer answer declares the grid
|
|
944
1282
|
# first (`t.to_unit(:ms).mean`) -- the same rule linear_fetch follows.
|
|
945
|
-
# Rounding is to nearest,
|
|
946
|
-
#
|
|
1283
|
+
# Rounding is to nearest, and a tie goes to the later tick: rounding half
|
|
1284
|
+
# away from zero would send a tie later after 1970 and earlier before it,
|
|
1285
|
+
# so moving the data by whole days would change the answer.
|
|
947
1286
|
|
|
948
1287
|
# @overload mean(axis: nil, **opts)
|
|
949
1288
|
# Returns the centroid time on `self`'s unit (§8), rounded to the
|
|
@@ -1088,8 +1427,21 @@ class CATime
|
|
|
1088
1427
|
[to_unit(u), other.to_unit(u)]
|
|
1089
1428
|
end
|
|
1090
1429
|
|
|
1430
|
+
private def common_unit (a, b)
|
|
1431
|
+
CATimeUnitAlgebra.diff_unit(a, b)
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
# One instant as a length-1 time array in its own unit, or nil.
|
|
1435
|
+
private def lift_value (operand)
|
|
1436
|
+
case operand
|
|
1437
|
+
when CATime::Element
|
|
1438
|
+
CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
1439
|
+
when Time, String, Date
|
|
1440
|
+
CATimeLiteral.exact_instant(operand)
|
|
1441
|
+
end
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1091
1444
|
def to_comparable (operand)
|
|
1092
|
-
require 'date' # Date is one of the operand kinds below
|
|
1093
1445
|
case operand
|
|
1094
1446
|
when CATime
|
|
1095
1447
|
return operand if operand.unit == unit
|
|
@@ -1100,21 +1452,15 @@ class CATime
|
|
|
1100
1452
|
to_comparable(lifted)
|
|
1101
1453
|
when Time, String, Date
|
|
1102
1454
|
to_comparable(CATimeLiteral.exact_instant(operand))
|
|
1103
|
-
when Array
|
|
1104
|
-
# A set of instants written one by one (is_in(["2024-01-02", ...]))
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
# An array that is not a time array: each value read as an instant (an
|
|
1109
|
-
# object column of Time values, a string column), a masked cell kept
|
|
1110
|
-
# masked.
|
|
1111
|
-
require "carray/methods/is_in"
|
|
1112
|
-
send(:values_in_face, self, operand) ||
|
|
1113
|
-
CArray.int64(*operand.shape).fill(UNDEF).time(unit: unit)
|
|
1455
|
+
when Array, CArray
|
|
1456
|
+
# A set of instants written one by one (is_in(["2024-01-02", ...])),
|
|
1457
|
+
# or an array that is not a time array (an object column of Time
|
|
1458
|
+
# values, a string column), read value by value.
|
|
1459
|
+
reconcile_values(operand)
|
|
1114
1460
|
else
|
|
1115
1461
|
raise TypeError,
|
|
1116
1462
|
"CATime cannot reconcile #{operand.class} " \
|
|
1117
|
-
"(
|
|
1463
|
+
"(compare #ticks for the raw int64 storage)"
|
|
1118
1464
|
end
|
|
1119
1465
|
end
|
|
1120
1466
|
|
|
@@ -1172,7 +1518,7 @@ class CATime
|
|
|
1172
1518
|
# @return [Element, CATime] an {Element} for a scalar `addr`, `nil` when
|
|
1173
1519
|
# that single answer is undetermined.
|
|
1174
1520
|
def linear_fetch (addr, **opts)
|
|
1175
|
-
r =
|
|
1521
|
+
r = face_storage.float64.linear_fetch(addr, **opts)
|
|
1176
1522
|
case r
|
|
1177
1523
|
when CArray
|
|
1178
1524
|
# mask_invalid before the int64 cast: the cast turns a NaN into 0, which
|
|
@@ -1201,10 +1547,9 @@ class CATime
|
|
|
1201
1547
|
# CArray. Array-level counterpart to {Element#to_time}.
|
|
1202
1548
|
# @return [CArray]
|
|
1203
1549
|
def to_time
|
|
1204
|
-
require 'time'
|
|
1205
1550
|
if CATimeUnitAlgebra::FIXED.key?(unit.base)
|
|
1206
1551
|
f = unit.tick_ratio # exact seconds / tick (Rational)
|
|
1207
|
-
|
|
1552
|
+
face_storage.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
|
|
1208
1553
|
else # calendar: exact granule midnight
|
|
1209
1554
|
(days_since_epoch * 86400).convert(:object) {|v| Time.at(v, in: 'UTC')}
|
|
1210
1555
|
end
|
|
@@ -1216,7 +1561,6 @@ class CATime
|
|
|
1216
1561
|
# {Element#to_date}.
|
|
1217
1562
|
# @return [CArray]
|
|
1218
1563
|
def to_date
|
|
1219
|
-
require 'date'
|
|
1220
1564
|
# 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
|
|
1221
1565
|
days_since_epoch.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
|
|
1222
1566
|
end
|
|
@@ -1226,7 +1570,6 @@ class CATime
|
|
|
1226
1570
|
# as an object CArray.
|
|
1227
1571
|
# @return [CArray]
|
|
1228
1572
|
def to_datetime
|
|
1229
|
-
require 'date'
|
|
1230
1573
|
to_time.convert(:object) {|t| t.to_datetime}
|
|
1231
1574
|
end
|
|
1232
1575
|
|
|
@@ -1240,8 +1583,8 @@ class CATime
|
|
|
1240
1583
|
# @return [CArray]
|
|
1241
1584
|
def year
|
|
1242
1585
|
case unit.base
|
|
1243
|
-
when :Y then
|
|
1244
|
-
when :M then (
|
|
1586
|
+
when :Y then face_storage * unit.count + 1970
|
|
1587
|
+
when :M then (face_storage * unit.count + 1970 * 12) / 12
|
|
1245
1588
|
else CATimeCivil.civil_from_days(days_since_epoch)[0]
|
|
1246
1589
|
end
|
|
1247
1590
|
end
|
|
@@ -1251,9 +1594,9 @@ class CATime
|
|
|
1251
1594
|
# @return [CArray] integer.
|
|
1252
1595
|
def month
|
|
1253
1596
|
case unit.base
|
|
1254
|
-
when :Y then
|
|
1597
|
+
when :Y then face_storage * 0 + 1
|
|
1255
1598
|
when :M
|
|
1256
|
-
mo =
|
|
1599
|
+
mo = face_storage * unit.count + 1970 * 12
|
|
1257
1600
|
mo % 12 + 1
|
|
1258
1601
|
else CATimeCivil.civil_from_days(days_since_epoch)[1]
|
|
1259
1602
|
end
|
|
@@ -1264,7 +1607,7 @@ class CATime
|
|
|
1264
1607
|
# @return [CArray] integer.
|
|
1265
1608
|
def day
|
|
1266
1609
|
case unit.base
|
|
1267
|
-
when :Y, :M then
|
|
1610
|
+
when :Y, :M then face_storage * 0 + 1
|
|
1268
1611
|
else CATimeCivil.civil_from_days(days_since_epoch)[2]
|
|
1269
1612
|
end
|
|
1270
1613
|
end
|
|
@@ -1279,6 +1622,54 @@ class CATime
|
|
|
1279
1622
|
# @return [CArray] integer.
|
|
1280
1623
|
def second; clock_field(:s); end
|
|
1281
1624
|
|
|
1625
|
+
# Quarter of the year, 1..4 (January to March is 1). For a fiscal year,
|
|
1626
|
+
# count quarters with `timesteps(unit: "3 months", origin:)`.
|
|
1627
|
+
# @return [CArray] integer.
|
|
1628
|
+
def quarter
|
|
1629
|
+
(month - 1) / 3 + 1
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
# ISO 8601 week number, 1..53. A week runs Monday to Sunday and belongs
|
|
1633
|
+
# to the year its Thursday is in, so the first days of January can be in
|
|
1634
|
+
# the last week of the year before, and the last days of December in
|
|
1635
|
+
# week 1 of the next; {#iso_year} is that year.
|
|
1636
|
+
# @return [CArray] integer.
|
|
1637
|
+
def iso_week
|
|
1638
|
+
thursday, year = iso_thursday
|
|
1639
|
+
ones = CArray.int64(*shape) { 1 }
|
|
1640
|
+
(thursday - CATimeCivil.days_from_civil(year, ones, ones)) / 7 + 1
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
# The ISO 8601 year of each time's week (see {#iso_week}): the calendar
|
|
1644
|
+
# year except in the few days around New Year whose week is counted in the
|
|
1645
|
+
# neighbouring year.
|
|
1646
|
+
# @return [CArray] integer.
|
|
1647
|
+
def iso_year
|
|
1648
|
+
iso_thursday[1]
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# The Thursday of each time's ISO week, in days since the epoch, and the
|
|
1652
|
+
# year it falls in.
|
|
1653
|
+
private def iso_thursday
|
|
1654
|
+
days = days_since_epoch
|
|
1655
|
+
iso_weekday = (days + 3) % 7 + 1 # Monday 1 .. Sunday 7; 1970-01-01 is a Thursday
|
|
1656
|
+
thursday = days - iso_weekday + 4
|
|
1657
|
+
[thursday, CATimeCivil.civil_from_days(thursday)[0]]
|
|
1658
|
+
end
|
|
1659
|
+
|
|
1660
|
+
# Number of days in the month, 28..31 (January's for `:Y` storage, which
|
|
1661
|
+
# does not resolve months). `t.day.eq(t.days_in_month)` is the last day of
|
|
1662
|
+
# the month.
|
|
1663
|
+
# @return [CArray] integer.
|
|
1664
|
+
def days_in_month
|
|
1665
|
+
y, m, = CATimeCivil.civil_from_days(days_since_epoch)
|
|
1666
|
+
ones = CArray.int64(*shape) { 1 }
|
|
1667
|
+
ord = y * 12 + m # the next month's ordinal (m is 1..12)
|
|
1668
|
+
ny = ord / 12
|
|
1669
|
+
CATimeCivil.days_from_civil(ny, ord - ny * 12 + 1, ones) -
|
|
1670
|
+
CATimeCivil.days_from_civil(y, m, ones)
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1282
1673
|
# Day of the week, Sunday = 0 .. Saturday = 6.
|
|
1283
1674
|
# @return [CArray] integer.
|
|
1284
1675
|
def weekday
|
|
@@ -1301,7 +1692,6 @@ class CATime
|
|
|
1301
1692
|
# Returns the Julian Day Number for each element.
|
|
1302
1693
|
# @return [CArray]
|
|
1303
1694
|
def jd
|
|
1304
|
-
require 'date'
|
|
1305
1695
|
to_time.convert(:int) {|t| t.to_date.jd}
|
|
1306
1696
|
end
|
|
1307
1697
|
|
|
@@ -1310,7 +1700,6 @@ class CATime
|
|
|
1310
1700
|
# day) for each element.
|
|
1311
1701
|
# @return [CArray]
|
|
1312
1702
|
def ajd
|
|
1313
|
-
require 'date'
|
|
1314
1703
|
to_time.convert(:double) {|t| t.to_datetime.ajd.to_f}
|
|
1315
1704
|
end
|
|
1316
1705
|
|
|
@@ -1341,7 +1730,7 @@ class CATime
|
|
|
1341
1730
|
# writes to_unit(:D) first) -- and the result is rounded back to a tick.
|
|
1342
1731
|
def reduce_on_own_grid(op, kind, args, opts)
|
|
1343
1732
|
with_surface_fill(opts) do |o|
|
|
1344
|
-
round_and_relift(
|
|
1733
|
+
round_and_relift(face_storage.public_send(op, *args, **o), kind)
|
|
1345
1734
|
end
|
|
1346
1735
|
end
|
|
1347
1736
|
|
|
@@ -1351,14 +1740,15 @@ class CATime
|
|
|
1351
1740
|
def round_and_relift(r, kind)
|
|
1352
1741
|
case r
|
|
1353
1742
|
when Numeric
|
|
1354
|
-
v = r.round
|
|
1743
|
+
v = kind == :timedelta ? r.round : (r + 0.5).floor
|
|
1355
1744
|
kind == :timedelta ? CATimedelta::Element.new(v, unit) : Element.new(v, unit)
|
|
1356
1745
|
when CArray
|
|
1357
1746
|
# A cell with no answer arrives masked and the mask survives the cast.
|
|
1358
1747
|
# NaN does not: it has no int64 image, so a `fill_value: Float::NAN`
|
|
1359
1748
|
# cell would cast to garbage bits. mask_invalid turns it into UNDEF,
|
|
1360
1749
|
# which is what "no answer" means here.
|
|
1361
|
-
|
|
1750
|
+
near = kind == :timedelta ? r.round : (r + 0.5).floor
|
|
1751
|
+
iv = near.mask_invalid.int64
|
|
1362
1752
|
kind == :timedelta ? iv.timedelta(unit: unit) : iv.time(unit: unit)
|
|
1363
1753
|
when Array
|
|
1364
1754
|
r.map {|x| round_and_relift(x, kind)}
|
|
@@ -1418,7 +1808,7 @@ class CATimedelta
|
|
|
1418
1808
|
# measured in this array's resolution (see {CATime#ticks}).
|
|
1419
1809
|
# @return [CArray]
|
|
1420
1810
|
def ticks
|
|
1421
|
-
|
|
1811
|
+
face_storage
|
|
1422
1812
|
end
|
|
1423
1813
|
|
|
1424
1814
|
# @overload to_unit(unit)
|
|
@@ -1440,7 +1830,7 @@ class CATimedelta
|
|
|
1440
1830
|
# @raise [RangeError] when the widened ticks overflow int64.
|
|
1441
1831
|
def to_unit(unit)
|
|
1442
1832
|
to = CATime::Resolution.parse(unit)
|
|
1443
|
-
CATimeUnitAlgebra.convert_scale_trunc(
|
|
1833
|
+
CATimeUnitAlgebra.convert_scale_trunc(face_storage, self.unit, to).timedelta(unit: to)
|
|
1444
1834
|
end
|
|
1445
1835
|
|
|
1446
1836
|
# @overload +(other)
|
|
@@ -1454,16 +1844,20 @@ class CATimedelta
|
|
|
1454
1844
|
# @raise [TypeError] on incompatible operands.
|
|
1455
1845
|
def +(other)
|
|
1456
1846
|
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1847
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
1457
1848
|
case other
|
|
1458
1849
|
when CATimedelta
|
|
1459
1850
|
u = common_duration_unit(other.unit)
|
|
1460
|
-
a = CATimeUnitAlgebra.convert_scale!(
|
|
1851
|
+
a = CATimeUnitAlgebra.convert_scale!(face_storage, unit, u)
|
|
1461
1852
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1462
|
-
|
|
1853
|
+
b = b[0] if other.scalar?
|
|
1854
|
+
CATimeUnitAlgebra.checked_add(a, b).timedelta(unit: u)
|
|
1463
1855
|
when CATime
|
|
1464
|
-
other + self
|
|
1856
|
+
return other + self unless other.scalar? # commutative -> time's unit
|
|
1857
|
+
d = CATimeUnitAlgebra.duration_on_grid(face_storage, unit, other.unit)
|
|
1858
|
+
CATimeUnitAlgebra.checked_add(d, other.parent[0]).time(unit: other.unit)
|
|
1465
1859
|
else
|
|
1466
|
-
raise TypeError, "CATimedelta + #{given
|
|
1860
|
+
raise TypeError, "CATimedelta + #{CATimeUnitAlgebra.operand_name(given)} is not allowed"
|
|
1467
1861
|
end
|
|
1468
1862
|
end
|
|
1469
1863
|
|
|
@@ -1477,14 +1871,16 @@ class CATimedelta
|
|
|
1477
1871
|
# @raise [TypeError] on a non-timedelta operand.
|
|
1478
1872
|
def -(other)
|
|
1479
1873
|
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1874
|
+
other = CATimeUnitAlgebra.scalar_operand(other, unit) || other
|
|
1480
1875
|
case other
|
|
1481
1876
|
when CATimedelta
|
|
1482
1877
|
u = common_duration_unit(other.unit)
|
|
1483
|
-
a = CATimeUnitAlgebra.convert_scale!(
|
|
1878
|
+
a = CATimeUnitAlgebra.convert_scale!(face_storage, unit, u)
|
|
1484
1879
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1485
|
-
|
|
1880
|
+
b = b[0] if other.scalar?
|
|
1881
|
+
CATimeUnitAlgebra.checked_sub(a, b).timedelta(unit: u)
|
|
1486
1882
|
else
|
|
1487
|
-
raise TypeError, "CATimedelta - #{given
|
|
1883
|
+
raise TypeError, "CATimedelta - #{CATimeUnitAlgebra.operand_name(given)} is not allowed"
|
|
1488
1884
|
end
|
|
1489
1885
|
end
|
|
1490
1886
|
|
|
@@ -1493,14 +1889,14 @@ class CATimedelta
|
|
|
1493
1889
|
# unit.
|
|
1494
1890
|
# @return [CATimedelta]
|
|
1495
1891
|
def abs
|
|
1496
|
-
|
|
1892
|
+
face_storage.abs.timedelta(unit: unit)
|
|
1497
1893
|
end
|
|
1498
1894
|
|
|
1499
1895
|
# @overload -@
|
|
1500
1896
|
# Returns each duration with its sign reversed, on the same unit.
|
|
1501
1897
|
# @return [CATimedelta]
|
|
1502
1898
|
def -@
|
|
1503
|
-
(-
|
|
1899
|
+
(-face_storage).timedelta(unit: unit)
|
|
1504
1900
|
end
|
|
1505
1901
|
|
|
1506
1902
|
# @overload *(other)
|
|
@@ -1510,15 +1906,16 @@ class CATimedelta
|
|
|
1510
1906
|
# @raise [TypeError] when `other` is not an Integer.
|
|
1511
1907
|
def *(other)
|
|
1512
1908
|
case other
|
|
1513
|
-
when Integer then (
|
|
1514
|
-
else raise TypeError, "CATimedelta * #{other
|
|
1909
|
+
when Integer then CATimeUnitAlgebra.checked_mul(face_storage, other).timedelta(unit: unit)
|
|
1910
|
+
else raise TypeError, "CATimedelta * #{CATimeUnitAlgebra.operand_name(other)} is not allowed (Integer only)"
|
|
1515
1911
|
end
|
|
1516
1912
|
end
|
|
1517
1913
|
|
|
1518
1914
|
# @overload /(other)
|
|
1519
1915
|
# Returns element-wise division: by Integer yields a
|
|
1520
|
-
# {CATimedelta}
|
|
1521
|
-
#
|
|
1916
|
+
# {CATimedelta} (truncated toward zero to the unit), by another
|
|
1917
|
+
# {CATimedelta} their ratio as a float64 CArray (90 s / 60 s is 1.5),
|
|
1918
|
+
# the two brought to a common unit first.
|
|
1522
1919
|
# @param other [Integer, CATimedelta]
|
|
1523
1920
|
# (or a MemoryView producer whose +to_ca+ gives one, such as an Arrow
|
|
1524
1921
|
# duration array)
|
|
@@ -1528,12 +1925,14 @@ class CATimedelta
|
|
|
1528
1925
|
given, other = other, CATimeUnitAlgebra.operand(other)
|
|
1529
1926
|
case other
|
|
1530
1927
|
when Integer
|
|
1531
|
-
CATimeUnitAlgebra.trunc_divide(
|
|
1928
|
+
CATimeUnitAlgebra.trunc_divide(face_storage, other).timedelta(unit: unit)
|
|
1532
1929
|
when CATimedelta
|
|
1930
|
+
# A ratio of durations is a number, not a count of whole ticks: 90 s /
|
|
1931
|
+
# 60 s is 1.5. Both are brought to one unit first, so the ratio does
|
|
1932
|
+
# not depend on the units they are held in.
|
|
1533
1933
|
u = common_duration_unit(other.unit)
|
|
1534
|
-
CATimeUnitAlgebra.
|
|
1535
|
-
CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1536
|
-
CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u))
|
|
1934
|
+
CATimeUnitAlgebra.convert_scale!(face_storage, unit, u).to_type(:float64) /
|
|
1935
|
+
CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1537
1936
|
else raise TypeError, "CATimedelta / #{given.class} is not allowed"
|
|
1538
1937
|
end
|
|
1539
1938
|
end
|
|
@@ -1547,26 +1946,55 @@ class CATimedelta
|
|
|
1547
1946
|
|
|
1548
1947
|
# @overload sum(*args, **opts)
|
|
1549
1948
|
# Returns the sum of durations as a {Element} for full reduction
|
|
1550
|
-
# or as a {CATimedelta} view for per-axis reduction.
|
|
1949
|
+
# or as a {CATimedelta} view for per-axis reduction. The sum is exact:
|
|
1950
|
+
# the unit counts are added in int64, not in float64.
|
|
1551
1951
|
# @return [Element, CATimedelta]
|
|
1952
|
+
# @raise [RangeError] if a sum does not fit int64 (for a per-axis
|
|
1953
|
+
# reduction, if any of its cells does not).
|
|
1552
1954
|
def sum(*args, **opts)
|
|
1553
|
-
with_surface_fill(opts) { |o|
|
|
1955
|
+
with_surface_fill(opts) { |o|
|
|
1956
|
+
exact_sum_relift(face_storage.accumulate(*args, **o), face_storage.sum(*args, **o))
|
|
1957
|
+
}
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1960
|
+
# int64 addition is exact modulo 2**64, so the int64 sum is the true sum
|
|
1961
|
+
# whenever the true sum fits int64, whatever order the cells were added
|
|
1962
|
+
# in. Whether it fits is read from the float64 sum, whose error is far
|
|
1963
|
+
# inside the factor of 2 between 2**62 and 2**63.
|
|
1964
|
+
def exact_sum_relift (exact, approx)
|
|
1965
|
+
past = case approx
|
|
1966
|
+
when Numeric then approx.abs > 2.0**62
|
|
1967
|
+
when CArray then (approx.abs > 2.0**62).any
|
|
1968
|
+
else false
|
|
1969
|
+
end
|
|
1970
|
+
raise RangeError, "sum of durations does not fit int64 (in #{unit})" if past
|
|
1971
|
+
case exact
|
|
1972
|
+
when Integer then Element.new(exact, unit)
|
|
1973
|
+
when CArray then exact.timedelta(unit: unit)
|
|
1974
|
+
else exact
|
|
1975
|
+
end
|
|
1554
1976
|
end
|
|
1977
|
+
private :exact_sum_relift
|
|
1555
1978
|
|
|
1556
1979
|
# @overload accumulate(*args, **opts)
|
|
1557
|
-
# Returns the sum of durations in the storage's
|
|
1558
|
-
# {#sum}
|
|
1980
|
+
# Returns the sum of durations in the storage's integer type (int64),
|
|
1981
|
+
# added without a range check: a sum past int64 wraps. {#sum} gives
|
|
1982
|
+
# the same answer when it fits and raises RangeError when it does not.
|
|
1559
1983
|
# @return [Element, CATimedelta]
|
|
1560
1984
|
def accumulate(*args, **opts)
|
|
1561
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
1985
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.accumulate(*args, **o)) }
|
|
1562
1986
|
end
|
|
1563
1987
|
|
|
1564
1988
|
# @overload cumsum(axis: nil)
|
|
1565
1989
|
# Returns the running sum of durations as a {CATimedelta} on `self`'s
|
|
1566
1990
|
# unit. A masked cell holds the running sum, as {CArray#cumsum} does.
|
|
1991
|
+
# The running sum is exact: the unit counts are added in int64, as
|
|
1992
|
+
# {#sum} adds them.
|
|
1567
1993
|
# @return [CATimedelta]
|
|
1994
|
+
# @raise [RangeError] if any cell of the running sum does not fit int64.
|
|
1568
1995
|
def cumsum(*args, **opts)
|
|
1569
|
-
|
|
1996
|
+
exact_sum_relift(face_storage.send(:cumsum_in_own_type, *args, **opts),
|
|
1997
|
+
face_storage.cumsum(*args, **opts))
|
|
1570
1998
|
end
|
|
1571
1999
|
|
|
1572
2000
|
# @overload prod(*)
|
|
@@ -1584,7 +2012,7 @@ class CATimedelta
|
|
|
1584
2012
|
# Returns the mean duration rounded to the nearest unit count.
|
|
1585
2013
|
# @return [Element, CATimedelta]
|
|
1586
2014
|
def mean(*args, **opts)
|
|
1587
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2015
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.mean(*args, **o)) }
|
|
1588
2016
|
end
|
|
1589
2017
|
|
|
1590
2018
|
# median / percentile / quantile / stddev / stddevp report on this array's
|
|
@@ -1596,7 +2024,7 @@ class CATimedelta
|
|
|
1596
2024
|
# Returns the median duration on `self`'s unit.
|
|
1597
2025
|
# @return [Element, CATimedelta]
|
|
1598
2026
|
def median(*args, **opts)
|
|
1599
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2027
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.median(*args, **o)) }
|
|
1600
2028
|
end
|
|
1601
2029
|
|
|
1602
2030
|
# @overload percentile(*p, axis: nil, **opts)
|
|
@@ -1605,7 +2033,7 @@ class CATimedelta
|
|
|
1605
2033
|
# two or more give an Array).
|
|
1606
2034
|
# @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
|
|
1607
2035
|
def percentile(*args, **opts)
|
|
1608
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2036
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.percentile(*args, **o)) }
|
|
1609
2037
|
end
|
|
1610
2038
|
|
|
1611
2039
|
# @overload quantile(axis: nil, **opts)
|
|
@@ -1613,7 +2041,7 @@ class CATimedelta
|
|
|
1613
2041
|
# `self`'s unit.
|
|
1614
2042
|
# @return [Array<Element>, Array<CATimedelta>]
|
|
1615
2043
|
def quantile(*args, **opts)
|
|
1616
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2044
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.quantile(*args, **o)) }
|
|
1617
2045
|
end
|
|
1618
2046
|
|
|
1619
2047
|
# @overload stddev(axis: nil, **opts)
|
|
@@ -1622,7 +2050,7 @@ class CATimedelta
|
|
|
1622
2050
|
# the precision matters.
|
|
1623
2051
|
# @return [Element, CATimedelta]
|
|
1624
2052
|
def stddev(*args, **opts)
|
|
1625
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2053
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.stddev(*args, **o)) }
|
|
1626
2054
|
end
|
|
1627
2055
|
|
|
1628
2056
|
# @overload stddevp(axis: nil, **opts)
|
|
@@ -1630,7 +2058,7 @@ class CATimedelta
|
|
|
1630
2058
|
# {#stddev}.
|
|
1631
2059
|
# @return [Element, CATimedelta]
|
|
1632
2060
|
def stddevp(*args, **opts)
|
|
1633
|
-
with_surface_fill(opts) { |o| round_and_relift(
|
|
2061
|
+
with_surface_fill(opts) { |o| round_and_relift(face_storage.stddevp(*args, **o)) }
|
|
1634
2062
|
end
|
|
1635
2063
|
|
|
1636
2064
|
def round_and_relift(r)
|
|
@@ -1699,6 +2127,17 @@ class CATimedelta
|
|
|
1699
2127
|
[to_unit(u), other.to_unit(u)]
|
|
1700
2128
|
end
|
|
1701
2129
|
|
|
2130
|
+
private def common_unit (a, b)
|
|
2131
|
+
CATimeUnitAlgebra.finer(a, b)
|
|
2132
|
+
end
|
|
2133
|
+
|
|
2134
|
+
# One duration as a length-1 duration array in its own unit, or nil.
|
|
2135
|
+
private def lift_value (operand)
|
|
2136
|
+
if operand.is_a?(CATimedelta::Element)
|
|
2137
|
+
CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
2138
|
+
end
|
|
2139
|
+
end
|
|
2140
|
+
|
|
1702
2141
|
def to_comparable (operand)
|
|
1703
2142
|
case operand
|
|
1704
2143
|
when CATimedelta
|
|
@@ -1708,10 +2147,14 @@ class CATimedelta
|
|
|
1708
2147
|
when CATimedelta::Element
|
|
1709
2148
|
lifted = CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
1710
2149
|
to_comparable(lifted)
|
|
2150
|
+
when Array, CArray
|
|
2151
|
+
# Durations written one by one, or an array that is not a duration
|
|
2152
|
+
# array (an object column of elements), read value by value.
|
|
2153
|
+
reconcile_values(operand)
|
|
1711
2154
|
else
|
|
1712
2155
|
raise TypeError,
|
|
1713
2156
|
"CATimedelta cannot reconcile #{operand.class} " \
|
|
1714
|
-
"(
|
|
2157
|
+
"(compare #ticks for the raw int64 storage)"
|
|
1715
2158
|
end
|
|
1716
2159
|
end
|
|
1717
2160
|
|
|
@@ -1752,7 +2195,7 @@ class CATimedelta
|
|
|
1752
2195
|
# @return [Element, CATimedelta] an {Element} for a scalar `addr`, `nil`
|
|
1753
2196
|
# when that single answer is undetermined.
|
|
1754
2197
|
def linear_fetch (addr, **opts)
|
|
1755
|
-
r =
|
|
2198
|
+
r = face_storage.float64.linear_fetch(addr, **opts)
|
|
1756
2199
|
case r
|
|
1757
2200
|
when CArray then r.mask_invalid.round.int64.timedelta(unit: unit)
|
|
1758
2201
|
when Numeric then r.to_f.nan? ? nil : Element.new(r.round, unit)
|
|
@@ -1808,14 +2251,25 @@ module CATimeLiteral
|
|
|
1808
2251
|
# "201909" is a valid YYMMDD to Ruby (2020-19-09, which landed on 2021-07),
|
|
1809
2252
|
# and "2019-02-31" landed on 2019-03-03. A missing finer field is not an
|
|
1810
2253
|
# error -- a year or a year-month names the head of that period.
|
|
2254
|
+
# %s and %Q give the seconds since the epoch, not the date's fields;
|
|
2255
|
+
# they are spelled out as the UTC date and time they name.
|
|
2256
|
+
def epoch_fields(h)
|
|
2257
|
+
return h unless h && h[:seconds] && !h[:year]
|
|
2258
|
+
t = Time.at(h[:seconds]).utc
|
|
2259
|
+
frac = h[:seconds] - h[:seconds].floor
|
|
2260
|
+
{ year: t.year, mon: t.month, mday: t.day, hour: t.hour, min: t.min,
|
|
2261
|
+
sec: t.sec, sec_fraction: frac.zero? ? nil : Rational(frac), leftover: h[:leftover] }.compact
|
|
2262
|
+
end
|
|
2263
|
+
|
|
1811
2264
|
def parse_date_fields(spec, format)
|
|
1812
2265
|
h =
|
|
1813
|
-
if format then Date._strptime(spec, format)
|
|
2266
|
+
if format then epoch_fields(Date._strptime(spec, format))
|
|
1814
2267
|
elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
|
|
1815
2268
|
elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
|
|
1816
2269
|
elsif (hour = hour_only_fields(spec)) then hour
|
|
1817
2270
|
else Date._parse(spec)
|
|
1818
2271
|
end
|
|
2272
|
+
h = resolve_day_of_year(h, spec) if h
|
|
1819
2273
|
unless h && h[:year]
|
|
1820
2274
|
raise ArgumentError, "cannot parse time #{spec.inspect}"
|
|
1821
2275
|
end
|
|
@@ -1840,10 +2294,63 @@ module CATimeLiteral
|
|
|
1840
2294
|
raise ArgumentError,
|
|
1841
2295
|
"cannot parse time #{spec.inspect}: hour 24 is only 24:00:00"
|
|
1842
2296
|
end
|
|
2297
|
+
# A clock field out of range is refused rather than carried into the
|
|
2298
|
+
# next one ("10:75" is not 11:15). Second 60 is the leap second.
|
|
2299
|
+
{ hour: 24, min: 59, sec: 60 }.each do |key, top|
|
|
2300
|
+
v = h[key]
|
|
2301
|
+
next if v.nil? || (0..top).cover?(v)
|
|
2302
|
+
raise ArgumentError,
|
|
2303
|
+
"cannot parse time #{spec.inspect}: #{key} #{v} is out of range"
|
|
2304
|
+
end
|
|
1843
2305
|
h
|
|
1844
2306
|
end
|
|
1845
2307
|
private_class_method :parse_date_fields
|
|
1846
2308
|
|
|
2309
|
+
# The month and day of a date written by its day of the year ("2024-015",
|
|
2310
|
+
# %j), its ISO week ("2024-W03-1", %G-W%V-%u) or its week of the year
|
|
2311
|
+
# (%U / %W with a weekday). Date._parse and Date._strptime return those
|
|
2312
|
+
# fields as they are written; left alone, the date would fall back to
|
|
2313
|
+
# January 1.
|
|
2314
|
+
def resolve_day_of_year(h, spec)
|
|
2315
|
+
return h if h[:mon] || h[:mday]
|
|
2316
|
+
date =
|
|
2317
|
+
if h[:yday] && h[:year]
|
|
2318
|
+
unless Date.valid_ordinal?(h[:year], h[:yday], Date::GREGORIAN)
|
|
2319
|
+
raise ArgumentError,
|
|
2320
|
+
"cannot parse time #{spec.inspect}: year #{h[:year]} has no day #{h[:yday]}"
|
|
2321
|
+
end
|
|
2322
|
+
Date.ordinal(h[:year], h[:yday], Date::GREGORIAN)
|
|
2323
|
+
elsif h[:cwyear] && h[:cweek]
|
|
2324
|
+
wd = h[:cwday] || 1
|
|
2325
|
+
unless Date.valid_commercial?(h[:cwyear], h[:cweek], wd, Date::GREGORIAN)
|
|
2326
|
+
raise ArgumentError,
|
|
2327
|
+
"cannot parse time #{spec.inspect}: ISO year #{h[:cwyear]} " \
|
|
2328
|
+
"has no week #{h[:cweek]}, day #{wd}"
|
|
2329
|
+
end
|
|
2330
|
+
Date.commercial(h[:cwyear], h[:cweek], wd, Date::GREGORIAN)
|
|
2331
|
+
elsif h[:year] && (h[:wnum0] || h[:wnum1])
|
|
2332
|
+
jan1 = Date.new(h[:year], 1, 1, Date::GREGORIAN)
|
|
2333
|
+
wday = h[:wday] || (h[:cwday] && h[:cwday] % 7)
|
|
2334
|
+
# Week 1 begins on the first Sunday (%U) or Monday (%W) of the year;
|
|
2335
|
+
# the days before it are week 0.
|
|
2336
|
+
if h[:wnum0]
|
|
2337
|
+
first = jan1 + (7 - jan1.wday) % 7
|
|
2338
|
+
d = first + (h[:wnum0] - 1) * 7 + (wday || 0)
|
|
2339
|
+
else
|
|
2340
|
+
first = jan1 + (8 - jan1.wday) % 7
|
|
2341
|
+
d = first + (h[:wnum1] - 1) * 7 + ((wday || 1) + 6) % 7
|
|
2342
|
+
end
|
|
2343
|
+
unless d.year == h[:year]
|
|
2344
|
+
raise ArgumentError,
|
|
2345
|
+
"cannot parse time #{spec.inspect}: the week and weekday fall outside #{h[:year]}"
|
|
2346
|
+
end
|
|
2347
|
+
d
|
|
2348
|
+
end
|
|
2349
|
+
return h unless date
|
|
2350
|
+
h.merge(year: date.year, mon: date.month, mday: date.day)
|
|
2351
|
+
end
|
|
2352
|
+
private_class_method :resolve_day_of_year
|
|
2353
|
+
|
|
1847
2354
|
# "YYYY-MM-DDThh" with an optional zone ("Z", "+hh", "+hhmm", "+hh:mm").
|
|
1848
2355
|
def hour_only_fields(spec)
|
|
1849
2356
|
m = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2})(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?\z/.match(spec)
|
|
@@ -1879,8 +2386,6 @@ module CATimeLiteral
|
|
|
1879
2386
|
# Exact Rational seconds since the Unix epoch for a start literal (Time /
|
|
1880
2387
|
# Date / DateTime / Integer unix-seconds / String). UTC default.
|
|
1881
2388
|
def epoch_seconds(spec, format = nil)
|
|
1882
|
-
require 'date'
|
|
1883
|
-
require 'time'
|
|
1884
2389
|
case spec
|
|
1885
2390
|
when Time then spec.to_r
|
|
1886
2391
|
when Integer then Rational(spec)
|
|
@@ -1917,8 +2422,6 @@ module CATimeLiteral
|
|
|
1917
2422
|
|
|
1918
2423
|
# [year, month] (UTC) of a start literal, for a calendar-resolution grid.
|
|
1919
2424
|
def year_month(spec, format = nil)
|
|
1920
|
-
require 'date'
|
|
1921
|
-
require 'time'
|
|
1922
2425
|
case spec
|
|
1923
2426
|
when Time then t = spec.utc; [t.year, t.month]
|
|
1924
2427
|
when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
|
|
@@ -1963,12 +2466,14 @@ module CATimeLiteral
|
|
|
1963
2466
|
|
|
1964
2467
|
INFER_DATES = %w[%d/%m/%Y %m/%d/%Y %d-%m-%Y %m-%d-%Y %d.%m.%Y %d/%m/%y %m/%d/%y] +
|
|
1965
2468
|
["%d %b %Y", "%d-%b-%Y", "%d-%b-%y", "%b %d, %Y", "%b %d %Y", "%Y%m%d"]
|
|
2469
|
+
Ractor.make_shareable(INFER_DATES)
|
|
1966
2470
|
|
|
1967
2471
|
# A space in a format matches any run of spaces, none included.
|
|
1968
2472
|
INFER_CLOCKS = [
|
|
1969
2473
|
["%H:%M", :clock], ["%H:%M:%S", :clock], ["%H:%M:%S.%N", :fraction],
|
|
1970
2474
|
["%I:%M %p", :clock], ["%I:%M:%S %p", :clock],
|
|
1971
2475
|
].freeze
|
|
2476
|
+
Ractor.make_shareable(INFER_CLOCKS)
|
|
1972
2477
|
|
|
1973
2478
|
# A time after a date: none, or a clock with or without a zone. %z takes
|
|
1974
2479
|
# an offset, Z, UTC or GMT, not a zone named for a place (some of those
|
|
@@ -1976,16 +2481,19 @@ module CATimeLiteral
|
|
|
1976
2481
|
INFER_TIMES = ([["", :date]] +
|
|
1977
2482
|
INFER_CLOCKS.flat_map { |clock, kind| [[" #{clock}", kind], [" #{clock} %z", kind]] }
|
|
1978
2483
|
).freeze
|
|
2484
|
+
Ractor.make_shareable(INFER_TIMES)
|
|
1979
2485
|
|
|
1980
2486
|
# A date written in Japanese, year first ("2024<year>1<month>2<day>"),
|
|
1981
2487
|
# takes a time in the same markers ("3<hour>4<minute>", "...5<second>")
|
|
1982
2488
|
# or with colons.
|
|
1983
2489
|
JA_YEAR, JA_MONTH, JA_DAY = "年", "月", "日"
|
|
1984
2490
|
JA_HOUR, JA_MINUTE, JA_SECOND = "時", "分", "秒"
|
|
2491
|
+
[JA_YEAR, JA_MONTH, JA_DAY, JA_HOUR, JA_MINUTE, JA_SECOND].each { |s| Ractor.make_shareable(s) }
|
|
1985
2492
|
INFER_JA_TIMES = ([["", :date]] +
|
|
1986
2493
|
([["%H#{JA_HOUR}%M#{JA_MINUTE}", :clock],
|
|
1987
2494
|
["%H#{JA_HOUR}%M#{JA_MINUTE}%S#{JA_SECOND}", :clock]] + INFER_CLOCKS)
|
|
1988
2495
|
.map { |clock, kind| [" #{clock}", kind] }).freeze
|
|
2496
|
+
Ractor.make_shareable(INFER_JA_TIMES)
|
|
1989
2497
|
|
|
1990
2498
|
INFER_FORMATS = (
|
|
1991
2499
|
INFER_DATES.product(INFER_TIMES).map { |date, (time, kind)|
|
|
@@ -1995,7 +2503,9 @@ module CATimeLiteral
|
|
|
1995
2503
|
TimeFormat.new("%Y#{JA_YEAR}%m#{JA_MONTH}%d#{JA_DAY}#{time}", kind)
|
|
1996
2504
|
}
|
|
1997
2505
|
).freeze
|
|
2506
|
+
Ractor.make_shareable(INFER_FORMATS)
|
|
1998
2507
|
INFER_FORMAT_STRINGS = INFER_FORMATS.map(&:format).freeze
|
|
2508
|
+
Ractor.make_shareable(INFER_FORMAT_STRINGS)
|
|
1999
2509
|
|
|
2000
2510
|
# The one format of INFER_FORMATS that the text of object array `x` is
|
|
2001
2511
|
# written in, `first` being its first present cell, as
|
|
@@ -2061,8 +2571,7 @@ module CATimeLiteral
|
|
|
2061
2571
|
# otherwise each String is parsed into fields here. Either way the fields
|
|
2062
2572
|
# become ticks for the whole array at once, and a cell that is not a
|
|
2063
2573
|
# String goes through tick_index.
|
|
2064
|
-
def ticks_from_cells(x, res, format, on_error)
|
|
2065
|
-
require 'date'
|
|
2574
|
+
def ticks_from_cells(x, res, format, on_error, seconds: false)
|
|
2066
2575
|
kind, per = tick_kind(res)
|
|
2067
2576
|
return nil unless kind
|
|
2068
2577
|
if format.is_a?(String)
|
|
@@ -2092,6 +2601,10 @@ module CATimeLiteral
|
|
|
2092
2601
|
frac[i] = (h[:sec_fraction] * 1_000_000_000).floor if h[:sec_fraction]
|
|
2093
2602
|
off[i] = h[:offset] if h[:offset]
|
|
2094
2603
|
present[i] = 1
|
|
2604
|
+
elsif s.is_a?(Integer) && format.nil?
|
|
2605
|
+
# an Integer counts ticks of the unit given, or seconds when the
|
|
2606
|
+
# unit is chosen from the values
|
|
2607
|
+
direct[i] = seconds ? tick_index(s, res) : s
|
|
2095
2608
|
else
|
|
2096
2609
|
direct[i] = tick_index(s, res, format)
|
|
2097
2610
|
end
|
|
@@ -2139,6 +2652,80 @@ module CATimeLiteral
|
|
|
2139
2652
|
raise RangeError, "time #{cells[i].inspect} does not fit int64 ticks of #{res}"
|
|
2140
2653
|
end
|
|
2141
2654
|
|
|
2655
|
+
# The unit CArray.time reads +x+ in when none is given: the finest any of
|
|
2656
|
+
# its values shows (see the unit: of CArray.time), :s when none shows one.
|
|
2657
|
+
def unit_shown(x, format)
|
|
2658
|
+
cells = x.is_a?(CArray) ? x.flatten.to_a : [x]
|
|
2659
|
+
text = cells.select { |v| v.is_a?(String) }
|
|
2660
|
+
units = []
|
|
2661
|
+
unless text.empty?
|
|
2662
|
+
if format
|
|
2663
|
+
units << format_unit(format)
|
|
2664
|
+
else
|
|
2665
|
+
unreadable = []
|
|
2666
|
+
_, read = CA_OBJECT(text).__parse_time_text__(nil, unreadable, true)
|
|
2667
|
+
units << read if read
|
|
2668
|
+
unreadable.each do |i|
|
|
2669
|
+
u = text_unit(text[i])
|
|
2670
|
+
units << u if u
|
|
2671
|
+
end
|
|
2672
|
+
end
|
|
2673
|
+
end
|
|
2674
|
+
cells.each do |v|
|
|
2675
|
+
case v
|
|
2676
|
+
when Integer then units << :s
|
|
2677
|
+
when CATime::Element then units << v.unit
|
|
2678
|
+
when Time then units << CATimeLiteral.exact_instant(v).unit
|
|
2679
|
+
when Date
|
|
2680
|
+
units << (defined?(DateTime) && v.is_a?(DateTime) ? CATimeLiteral.exact_instant(v.to_time).unit : :D)
|
|
2681
|
+
end
|
|
2682
|
+
end
|
|
2683
|
+
# A month beside a day is read on the fixed grid both instants reach,
|
|
2684
|
+
# as two elements are compared (diff_unit); finer covers one group only.
|
|
2685
|
+
units.map { |u| CATime::Resolution.parse(u) }
|
|
2686
|
+
.reduce { |a, b| CATimeUnitAlgebra.diff_unit(a, b) } || CATime::Resolution.parse(:s)
|
|
2687
|
+
end
|
|
2688
|
+
|
|
2689
|
+
# The unit a String read with Date._parse shows, or nil when it does not
|
|
2690
|
+
# read (the reading raises or masks it later).
|
|
2691
|
+
def text_unit(str)
|
|
2692
|
+
h = Date._parse(str)
|
|
2693
|
+
return nil unless h && h[:year]
|
|
2694
|
+
return fraction_unit(h[:sec_fraction].denominator) if h[:sec_fraction]
|
|
2695
|
+
(h[:hour] || h[:min] || h[:sec]) ? :s : :D
|
|
2696
|
+
end
|
|
2697
|
+
|
|
2698
|
+
# The finest field a strptime format writes.
|
|
2699
|
+
def format_unit(format)
|
|
2700
|
+
if (m = format.match(/%(\d*)([LNQ])/))
|
|
2701
|
+
return :ms if m[2] == "L" || m[2] == "Q"
|
|
2702
|
+
digits = m[1].empty? ? 9 : m[1].to_i
|
|
2703
|
+
return fraction_unit(10 ** digits)
|
|
2704
|
+
end
|
|
2705
|
+
format.match?(/%[-_0^#]*[sSTHIklMRrXcp]/) ? :s : :D
|
|
2706
|
+
end
|
|
2707
|
+
|
|
2708
|
+
def fraction_unit(denominator)
|
|
2709
|
+
digits = Math.log10(denominator).ceil
|
|
2710
|
+
digits <= 3 ? :ms : digits <= 6 ? :us : :ns
|
|
2711
|
+
end
|
|
2712
|
+
|
|
2713
|
+
# +x+ with each Integer replaced by its decimal digits as a String, for a
|
|
2714
|
+
# format to read; a masked cell stays masked.
|
|
2715
|
+
def integers_as_text(x)
|
|
2716
|
+
case x
|
|
2717
|
+
when Integer then x.to_s
|
|
2718
|
+
when CArray
|
|
2719
|
+
return x if x.face? || !(x.integer? || x.data_type == CA_OBJECT)
|
|
2720
|
+
text = CArray.object(*x.shape)
|
|
2721
|
+
cells = x.flatten.to_a
|
|
2722
|
+
cells.each_with_index { |v, i| text[i] = v.is_a?(Integer) ? v.to_s : v }
|
|
2723
|
+
text = text.reshape(*x.shape) if x.ndim > 1
|
|
2724
|
+
text
|
|
2725
|
+
else x
|
|
2726
|
+
end
|
|
2727
|
+
end
|
|
2728
|
+
|
|
2142
2729
|
# Single-literal build for {.time}: a 1-element CATime, honouring
|
|
2143
2730
|
# the on_error policy (raise, or a masked cell).
|
|
2144
2731
|
def to_time_array(literal, res, format, on_error)
|
|
@@ -2218,10 +2805,13 @@ class CArray
|
|
|
2218
2805
|
|
|
2219
2806
|
# @overload time(x, unit: :s, format: nil, on_error: :raise)
|
|
2220
2807
|
# Builds a {CATime} on the `unit` grid from time value(s). `x`
|
|
2221
|
-
# is either a single literal (Time / ISO 8601 String /
|
|
2222
|
-
#
|
|
2808
|
+
# is either a single literal (Time / ISO 8601 String / Integer /
|
|
2809
|
+
# DateTime) — giving a 1-element result — or a CArray of such
|
|
2223
2810
|
# literals — giving a same-shape result parsed per cell. Parsing is UTC
|
|
2224
|
-
# and DateTime-independent.
|
|
2811
|
+
# and DateTime-independent. An Integer counts ticks of `unit` from
|
|
2812
|
+
# the epoch, so it is Unix seconds when `unit` is not given (also among
|
|
2813
|
+
# values that make the chosen unit finer):
|
|
2814
|
+
# `CArray.time(CA_INT64([0, 10]), unit: :m)` is 00:00 and 00:10.
|
|
2225
2815
|
#
|
|
2226
2816
|
# A value that cannot be parsed raises by default (`on_error: :raise`);
|
|
2227
2817
|
# pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
|
|
@@ -2229,8 +2819,16 @@ class CArray
|
|
|
2229
2819
|
# becomes UNDEF, regardless of `on_error`.
|
|
2230
2820
|
# @param x [Time, String, Integer, DateTime, CATime::Element, Array, CArray] a literal, a
|
|
2231
2821
|
# Ruby Array of literals, or a CArray of literals.
|
|
2232
|
-
# @param unit [Resolution, Symbol, String] target grid resolution
|
|
2822
|
+
# @param unit [Resolution, Symbol, String, nil] target grid resolution;
|
|
2823
|
+
# by default the finest the values show, so nothing they hold is cut
|
|
2824
|
+
# off: text written to the day is `:D`, to the second `:s`, with a
|
|
2825
|
+
# fraction `:ms` / `:us` / `:ns` by its digits (as
|
|
2826
|
+
# `CAFrame#parse_to_time` reads text); with a format, the finest field
|
|
2827
|
+
# the format writes; an Integer `:s`; a Time the finest
|
|
2828
|
+
# its value needs; a Date `:D`; an element its own unit.
|
|
2233
2829
|
# @param format [String, nil] optional strptime format for String input.
|
|
2830
|
+
# With a format, an Integer is read as its decimal digits
|
|
2831
|
+
# (`CArray.time(20240101, format: "%Y%m%d")`), not as a count of ticks.
|
|
2234
2832
|
# @param on_error [:raise, :mask] parse-failure policy (default `:raise`).
|
|
2235
2833
|
# @return [CATime] shape `[1]` for a literal, else `x`'s shape.
|
|
2236
2834
|
# @raise [ArgumentError] on an unparseable value when `on_error: :raise`.
|
|
@@ -2239,16 +2837,25 @@ class CArray
|
|
|
2239
2837
|
# CArray.time("2024-06-15", unit: :D) # 1-element
|
|
2240
2838
|
# CArray.time(%w[2024-01-01 2024-02-01], unit: :D) # Ruby Array
|
|
2241
2839
|
# CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)
|
|
2242
|
-
def self.time(x, unit:
|
|
2243
|
-
res = CATime::Resolution.parse(unit)
|
|
2840
|
+
def self.time(x, unit: nil, format: nil, on_error: :raise)
|
|
2244
2841
|
unless %i[raise mask].include?(on_error)
|
|
2245
2842
|
raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
|
|
2246
2843
|
end
|
|
2247
2844
|
x = CA_OBJECT(x) if x.is_a?(Array) # Ruby Array of literals -> object CArray
|
|
2845
|
+
# With a format, an Integer is the digits it is written with (20240101
|
|
2846
|
+
# read as "%Y%m%d"), not a count of ticks: a format describes text.
|
|
2847
|
+
x = CATimeLiteral.integers_as_text(x) if format
|
|
2848
|
+
res = unit ? CATime::Resolution.parse(unit) : CATimeLiteral.unit_shown(x, format)
|
|
2849
|
+
# Without a format, an Integer counts ticks of the unit from the epoch:
|
|
2850
|
+
# seconds by default, minutes with unit: :m.
|
|
2851
|
+
if format.nil?
|
|
2852
|
+
return CA_INT64([x]).time(unit: res) if x.is_a?(Integer)
|
|
2853
|
+
return (x.data_type == CA_INT64 ? x.copy : x).time(unit: res) if x.is_a?(CArray) && !x.face? && x.integer?
|
|
2854
|
+
end
|
|
2248
2855
|
unless x.is_a?(CArray)
|
|
2249
2856
|
return CATimeLiteral.to_time_array(x, res, format, on_error)
|
|
2250
2857
|
end
|
|
2251
|
-
raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error)
|
|
2858
|
+
raw = CATimeLiteral.ticks_from_cells(x, res, format, on_error, seconds: unit.nil?)
|
|
2252
2859
|
return raw.time(unit: res) if raw
|
|
2253
2860
|
raw = CArray.int64(*x.shape)
|
|
2254
2861
|
x.each_index do |*idx|
|
|
@@ -2258,7 +2865,7 @@ class CArray
|
|
|
2258
2865
|
next
|
|
2259
2866
|
end
|
|
2260
2867
|
begin
|
|
2261
|
-
raw[*idx] = CATimeLiteral.tick_index(s, res, format)
|
|
2868
|
+
raw[*idx] = s.is_a?(Integer) && format.nil? && unit ? s : CATimeLiteral.tick_index(s, res, format)
|
|
2262
2869
|
rescue ArgumentError, TypeError
|
|
2263
2870
|
raise if on_error == :raise
|
|
2264
2871
|
raw[*idx] = UNDEF # opt-in parse-mask
|
|
@@ -2275,8 +2882,10 @@ class CArray
|
|
|
2275
2882
|
# int64 array is built; the origin is not stored).
|
|
2276
2883
|
# @param unit [Resolution, Symbol, String] grid resolution.
|
|
2277
2884
|
# @param origin [Time, String, Integer, DateTime, CATime::Element, nil] base instant that
|
|
2278
|
-
# `self`'s indices are counted from (default: the Unix epoch)
|
|
2885
|
+
# `self`'s indices are counted from (default: the Unix epoch); an
|
|
2886
|
+
# Integer is Unix seconds. It has to be a point of the `unit` grid.
|
|
2279
2887
|
# @return [CATime]
|
|
2888
|
+
# @raise [ArgumentError] when `origin` falls between two ticks of `unit`.
|
|
2280
2889
|
# @note An `int64` receiver is wrapped zero-copy. A narrower integer type
|
|
2281
2890
|
# is widened to `int64` first (a copy); a `Float` / non-integer type
|
|
2282
2891
|
# raises (cast it explicitly if the truncation is intended).
|
|
@@ -2286,8 +2895,11 @@ class CArray
|
|
|
2286
2895
|
if origin.nil?
|
|
2287
2896
|
CATime.wrap(src, unit: res)
|
|
2288
2897
|
else
|
|
2289
|
-
|
|
2290
|
-
|
|
2898
|
+
# The origin is a point of the grid: one between two ticks has no
|
|
2899
|
+
# index, so it raises rather than moving to the tick before it.
|
|
2900
|
+
origin = Time.at(origin, in: "UTC") if origin.is_a?(Integer)
|
|
2901
|
+
o = CATimeGrid.resolve_origin_ticks(origin, res, res)
|
|
2902
|
+
CATimeUnitAlgebra.checked_add(src, o).time(unit: res)
|
|
2291
2903
|
end
|
|
2292
2904
|
end
|
|
2293
2905
|
|
|
@@ -2313,6 +2925,13 @@ class CArray
|
|
|
2313
2925
|
"array cannot be wrapped (cast it explicitly, e.g. " \
|
|
2314
2926
|
"arr.int64.time(...), if a lossy conversion is intended)"
|
|
2315
2927
|
end
|
|
2928
|
+
if data_type == CA_UINT64
|
|
2929
|
+
hi = max
|
|
2930
|
+
if hi != UNDEF && hi > 2**63 - 1
|
|
2931
|
+
raise RangeError,
|
|
2932
|
+
"CATime / CATimedelta storage is int64; #{hi} does not fit"
|
|
2933
|
+
end
|
|
2934
|
+
end
|
|
2316
2935
|
int64
|
|
2317
2936
|
end
|
|
2318
2937
|
private :as_int64_time_storage
|
|
@@ -2646,6 +3265,7 @@ class CATime
|
|
|
2646
3265
|
m: "minutes", s: "seconds", ms: "milliseconds",
|
|
2647
3266
|
us: "microseconds", ns: "nanoseconds", ps: "picoseconds",
|
|
2648
3267
|
fs: "femtoseconds", as: "attoseconds" }.freeze
|
|
3268
|
+
Ractor.make_shareable(WORD)
|
|
2649
3269
|
|
|
2650
3270
|
# Storage resolutions to fall back on when the unit's own tick cannot
|
|
2651
3271
|
# hold the origin exactly (a day grid anchored at 12:00 needs seconds).
|
|
@@ -2682,7 +3302,8 @@ class CATime
|
|
|
2682
3302
|
@unit = Resolution.parse(unit)
|
|
2683
3303
|
@origin = origin
|
|
2684
3304
|
@storage = resolve_storage
|
|
2685
|
-
|
|
3305
|
+
# An Integer origin is an instant in Unix seconds, whatever the unit.
|
|
3306
|
+
@origin = CATimeLiteral.to_time_array(origin, @storage, nil, :raise)[0] if origin
|
|
2686
3307
|
freeze
|
|
2687
3308
|
end
|
|
2688
3309
|
|
|
@@ -2870,8 +3491,18 @@ class CATime
|
|
|
2870
3491
|
fl = o + q * step_ticks
|
|
2871
3492
|
return bucket_head_as_time(fl + step_ticks * n.ne(fl), mul, su)
|
|
2872
3493
|
when :round
|
|
2873
|
-
|
|
2874
|
-
|
|
3494
|
+
if mul == 1 and [:M, :Y].include?(su.base)
|
|
3495
|
+
# A month or year tick is the start of its month or year, so the
|
|
3496
|
+
# nearest head is by the instant, measured in days.
|
|
3497
|
+
fl = o + q * step_ticks
|
|
3498
|
+
here = days_since_epoch
|
|
3499
|
+
lo = fl.time(unit: su).send(:days_since_epoch)
|
|
3500
|
+
hi = (fl + step_ticks).time(unit: su).send(:days_since_epoch)
|
|
3501
|
+
q = q + (hi - here).le(here - lo) # ties -> future
|
|
3502
|
+
else
|
|
3503
|
+
r = d - q * step_ticks # 0 <= r < step_ticks
|
|
3504
|
+
q = q + r.ge((step_ticks + 1) / 2) # ties -> future; no 2*d / 2*r
|
|
3505
|
+
end
|
|
2875
3506
|
end
|
|
2876
3507
|
bucket_head_as_time(o + q * step_ticks, mul, su)
|
|
2877
3508
|
end
|
|
@@ -2884,6 +3515,34 @@ class CATime
|
|
|
2884
3515
|
snap(grid, unit: unit, origin: origin, direction: :floor)
|
|
2885
3516
|
end
|
|
2886
3517
|
|
|
3518
|
+
# @overload end_of(grid = nil, unit:, origin: nil)
|
|
3519
|
+
# Each element at the last tick of its bucket, on the array's own grid:
|
|
3520
|
+
# the bucket {#floor} gives the head of, one tick before the next head.
|
|
3521
|
+
# A day array's month ends on the month's last day, a second array's at
|
|
3522
|
+
# 23:59:59 of it:
|
|
3523
|
+
#
|
|
3524
|
+
# CArray.time(%w[2024-02-10 2024-03-31], unit: :D).end_of(unit: :M)
|
|
3525
|
+
# # => 2024-02-29, 2024-03-31
|
|
3526
|
+
#
|
|
3527
|
+
# The bucket has to be a whole number of the array's ticks, since its
|
|
3528
|
+
# end is counted on that grid.
|
|
3529
|
+
# @return [CATime]
|
|
3530
|
+
# @raise [ArgumentError] when the bucket is finer than the grid or not a
|
|
3531
|
+
# whole number of its ticks.
|
|
3532
|
+
def end_of(grid = nil, unit: nil, origin: nil)
|
|
3533
|
+
step, origin = Grid.resolve(grid, unit, origin)
|
|
3534
|
+
step = CATime::Resolution.parse(step)
|
|
3535
|
+
per = CATimeUnitAlgebra.ratio(step, self.unit) # ticks of the grid per bucket
|
|
3536
|
+
if per && !(per.denominator == 1 && per >= 1)
|
|
3537
|
+
raise ArgumentError,
|
|
3538
|
+
"end_of: a bucket of #{step} is not a whole number of the #{self.unit} " \
|
|
3539
|
+
"grid's ticks, so its last tick is not on the grid"
|
|
3540
|
+
end
|
|
3541
|
+
head = floor(unit: step, origin: origin)
|
|
3542
|
+
head + CATimedelta::Element.new(step.count, step.base) -
|
|
3543
|
+
CATimedelta::Element.new(self.unit.count, self.unit.base)
|
|
3544
|
+
end
|
|
3545
|
+
|
|
2887
3546
|
# @overload ceil(grid = nil, unit:, origin: nil)
|
|
2888
3547
|
# {#snap} with `direction: :ceil` -- each element at the bucket head at
|
|
2889
3548
|
# or after it (an element already on a boundary maps to itself).
|
|
@@ -2895,8 +3554,10 @@ class CATime
|
|
|
2895
3554
|
# @overload round(grid = nil, unit:, origin: nil)
|
|
2896
3555
|
# {#snap} with `direction: :round` -- each element at its nearest bucket
|
|
2897
3556
|
# head, ties toward the future. Exact for odd `step_ticks` (no half-tick
|
|
2898
|
-
# loss). For a calendar bucket the nearest head is by
|
|
2899
|
-
#
|
|
3557
|
+
# loss). For a calendar bucket the nearest head is by the instant (month
|
|
3558
|
+
# lengths vary), ties toward the future. A month or year storage tick
|
|
3559
|
+
# stands for the start of its month or year, so a `:M` array rounds as
|
|
3560
|
+
# the same dates on `:D` do: 1889-07 to the year is 1889.
|
|
2900
3561
|
# @return [CATime]
|
|
2901
3562
|
def round(grid = nil, unit: nil, origin: nil)
|
|
2902
3563
|
snap(grid, unit: unit, origin: origin, direction: :round)
|
|
@@ -2982,7 +3643,7 @@ class CATime
|
|
|
2982
3643
|
|
|
2983
3644
|
# Storage ticks lifted into the numerator grid (see {#resolve_timestep_grid}).
|
|
2984
3645
|
def storage_ticks_in_numerator_grid(mul)
|
|
2985
|
-
mul == 1 ?
|
|
3646
|
+
mul == 1 ? face_storage : CATimeUnitAlgebra.widen(face_storage, mul)
|
|
2986
3647
|
end
|
|
2987
3648
|
|
|
2988
3649
|
# A bucket head computed in numerator ticks, back as a {CATime} in the
|
|
@@ -3016,15 +3677,15 @@ class CATime
|
|
|
3016
3677
|
# an empty array, or one whose every cell is masked.
|
|
3017
3678
|
def raise_if_int64_overflow(su, mul, o, step_ticks = nil, kind = nil)
|
|
3018
3679
|
return unless mul > 1 or FINE_UNITS.include?(su.base)
|
|
3019
|
-
min =
|
|
3680
|
+
min = face_storage.min
|
|
3020
3681
|
return if min == UNDEF
|
|
3021
3682
|
lo = Integer(min) * mul
|
|
3022
|
-
hi = Integer(
|
|
3683
|
+
hi = Integer(face_storage.max) * mul
|
|
3023
3684
|
chk = ->(v, w) { CATimeGrid.check_int64_range(v, w) }
|
|
3024
3685
|
chk.(lo, "ticks in bucket resolution")
|
|
3025
3686
|
chk.(hi, "ticks in bucket resolution")
|
|
3026
|
-
chk.(lo - o, "
|
|
3027
|
-
chk.(hi - o, "
|
|
3687
|
+
chk.(lo - o, "face_storage - origin")
|
|
3688
|
+
chk.(hi - o, "face_storage - origin")
|
|
3028
3689
|
if kind
|
|
3029
3690
|
chk.(bucket_head_ticks(lo, o, step_ticks, kind), "bucket head")
|
|
3030
3691
|
chk.(bucket_head_ticks(hi, o, step_ticks, kind), "bucket head")
|
|
@@ -3052,22 +3713,22 @@ class CATime
|
|
|
3052
3713
|
case res.base
|
|
3053
3714
|
when :Y
|
|
3054
3715
|
ones = CArray.int64(*shape) { 1 }
|
|
3055
|
-
CATimeCivil.days_from_civil(
|
|
3716
|
+
CATimeCivil.days_from_civil(face_storage * res.count + 1970, ones, ones)
|
|
3056
3717
|
when :M
|
|
3057
|
-
mo =
|
|
3718
|
+
mo = face_storage * res.count + 1970 * 12
|
|
3058
3719
|
y = mo / 12
|
|
3059
3720
|
m = mo - y * 12 + 1
|
|
3060
3721
|
CATimeCivil.days_from_civil(y, m, CArray.int64(*shape) { 1 })
|
|
3061
|
-
when :W then
|
|
3062
|
-
when :D then
|
|
3722
|
+
when :W then face_storage * res.count * 7
|
|
3723
|
+
when :D then face_storage * res.count
|
|
3063
3724
|
else # fixed sub-day storage
|
|
3064
3725
|
day = CATime::Resolution.new(1, :D)
|
|
3065
3726
|
r = CATimeUnitAlgebra.ratio(day, res) # storage ticks per day
|
|
3066
3727
|
if r.denominator == 1
|
|
3067
|
-
|
|
3728
|
+
face_storage / r.numerator
|
|
3068
3729
|
else # non-day-aligned tick
|
|
3069
3730
|
tr = res.tick_ratio # seconds / tick
|
|
3070
|
-
|
|
3731
|
+
face_storage * tr.numerator / (86400 * tr.denominator)
|
|
3071
3732
|
end
|
|
3072
3733
|
end
|
|
3073
3734
|
end
|
|
@@ -3080,9 +3741,9 @@ class CATime
|
|
|
3080
3741
|
# calendar-storage element has no intra-day field, so it collapses to 0.
|
|
3081
3742
|
def clock_field(fu)
|
|
3082
3743
|
r = CATimeUnitAlgebra.ratio(unit, CATime::Resolution.new(1, fu)) # fu ticks / storage tick
|
|
3083
|
-
return
|
|
3084
|
-
fi = r.denominator == 1 ?
|
|
3085
|
-
:
|
|
3744
|
+
return face_storage * 0 if r.nil? # calendar storage: no clock field
|
|
3745
|
+
fi = r.denominator == 1 ? face_storage * r.numerator # epoch-relative fu index
|
|
3746
|
+
: face_storage * r.numerator / r.denominator
|
|
3086
3747
|
cycle = fu == :h ? 24 : 60 # hour 0..23; min/sec 0..59
|
|
3087
3748
|
fi % cycle
|
|
3088
3749
|
end
|
|
@@ -3103,12 +3764,12 @@ class CATime
|
|
|
3103
3764
|
return k if kind == :index
|
|
3104
3765
|
head = CATimeGrid.calendar_bucket_head_ticks(k, st, su, origin)
|
|
3105
3766
|
return head.time(unit: su) if kind == :floor
|
|
3106
|
-
return
|
|
3767
|
+
return face_storage.eq(head) if kind == :on
|
|
3107
3768
|
head_next = CATimeGrid.calendar_bucket_head_ticks(k + 1, st, su, origin)
|
|
3108
3769
|
sel = # boolean 0/1: pick head_next?
|
|
3109
3770
|
case kind
|
|
3110
|
-
when :ceil then
|
|
3111
|
-
when :round then (head_next -
|
|
3771
|
+
when :ceil then face_storage.ne(head) # on-boundary keeps head
|
|
3772
|
+
when :round then (head_next - face_storage).le(face_storage - head) # nearest, ties -> future
|
|
3112
3773
|
end
|
|
3113
3774
|
(head + (head_next - head) * sel).time(unit: su)
|
|
3114
3775
|
end
|