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