carray 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +5 -25
- data/CHANGELOG.md +16 -0
- data/LICENSE +1 -1
- data/NEWS.md +3 -0
- data/README.md +128 -44
- data/carray.gemspec +22 -24
- data/ext/ca_array_pool.c +91 -0
- data/ext/ca_axis_descriptor.h +186 -0
- data/ext/ca_axis_dispatch.c +924 -0
- data/ext/ca_axis_group.c +1208 -0
- data/ext/ca_bincmp_dispatch.c +76 -0
- data/ext/ca_bincmp_dispatch.h +85 -0
- data/ext/ca_binop_dispatch.c +125 -0
- data/ext/ca_binop_dispatch.h +159 -0
- data/ext/ca_categorical_iterator.c +1375 -0
- data/ext/ca_compare.c +94 -0
- data/ext/ca_compare.h +26 -0
- data/ext/ca_composite_dispatch.c +414 -0
- data/ext/ca_composite_dispatch.h +116 -0
- data/ext/ca_for_buffer.h +96 -0
- data/ext/ca_for_each_element.h +241 -0
- data/ext/ca_group_iter.c +304 -0
- data/ext/ca_iter_substrate.h +325 -0
- data/ext/ca_kernel_iterator.c +4321 -0
- data/ext/ca_kernel_iterator.h +2603 -0
- data/ext/ca_moncmp_dispatch.c +37 -0
- data/ext/ca_moncmp_dispatch.h +62 -0
- data/ext/ca_monop_dispatch.c +200 -0
- data/ext/ca_monop_dispatch.h +235 -0
- data/ext/ca_obj_array.c +355 -359
- data/ext/ca_obj_bincmp.c +809 -0
- data/ext/ca_obj_binop.c +892 -0
- data/ext/ca_obj_bitarray.c +369 -164
- data/ext/ca_obj_bitfield.c +294 -234
- data/ext/ca_obj_block.c +189 -711
- data/ext/ca_obj_byte_swap.c +766 -0
- data/ext/ca_obj_const_string.c +965 -0
- data/ext/ca_obj_face.c +670 -0
- data/ext/ca_obj_face.h +247 -0
- data/ext/ca_obj_fake.c +228 -100
- data/ext/ca_obj_farray.c +54 -441
- data/ext/ca_obj_field.c +82 -529
- data/ext/ca_obj_fixlen_string.c +306 -0
- data/ext/ca_obj_grid.c +858 -440
- data/ext/ca_obj_meld.c +1034 -0
- data/ext/ca_obj_moncmp.c +569 -0
- data/ext/ca_obj_monop.c +1111 -0
- data/ext/ca_obj_object.c +774 -298
- data/ext/ca_obj_record.c +468 -0
- data/ext/ca_obj_reduce.c +97 -82
- data/ext/ca_obj_refer.c +569 -459
- data/ext/ca_obj_remap.c +475 -0
- data/ext/ca_obj_repeat.c +92 -477
- data/ext/ca_obj_roll.c +616 -0
- data/ext/ca_obj_select.c +344 -296
- data/ext/ca_obj_select_axis.c +1296 -0
- data/ext/ca_obj_shift.c +230 -792
- data/ext/ca_obj_source.c +78 -0
- data/ext/ca_obj_stack.c +1173 -0
- data/ext/ca_obj_stride.c +2501 -0
- data/ext/ca_obj_string.c +268 -0
- data/ext/ca_obj_tile.c +614 -0
- data/ext/ca_obj_time.c +546 -0
- data/ext/ca_obj_timedelta.c +435 -0
- data/ext/ca_obj_transpose.c +62 -516
- data/ext/ca_obj_triop.c +746 -0
- data/ext/ca_obj_unbound_repeat.c +208 -241
- data/ext/ca_obj_window.c +1131 -563
- data/ext/ca_op_byte_swap.c +175 -0
- data/ext/ca_op_ipower.c +319 -0
- data/ext/ca_op_powi.h +88 -0
- data/ext/ca_sort_kernels.h +132 -0
- data/ext/ca_sweep_engine.c +430 -0
- data/ext/ca_sweep_engine.h +157 -0
- data/ext/ca_transform_common.c +228 -0
- data/ext/ca_triop_dispatch.c +55 -0
- data/ext/ca_triop_dispatch.h +62 -0
- data/ext/carray.h +795 -402
- data/ext/carray_access.c +831 -711
- data/ext/carray_attribute.c +98 -330
- data/ext/carray_bincount.c +255 -0
- data/ext/carray_broadcast.c +283 -0
- data/ext/carray_call_cfunc.c +1360 -828
- data/ext/carray_call_cfunc.h +160 -0
- data/ext/carray_cast.c +1212 -301
- data/ext/carray_cast_func.rb +81 -40
- data/ext/carray_class.c +53 -63
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +350 -346
- data/ext/carray_copy.c +156 -268
- data/ext/carray_core.c +1342 -199
- data/ext/carray_count.c +312 -0
- data/ext/carray_data_type.c +43 -19
- data/ext/carray_element.c +585 -213
- data/ext/carray_factorize.c +2542 -0
- data/ext/carray_generate.c +230 -559
- data/ext/carray_histogram.c +490 -0
- data/ext/carray_hold.c +228 -0
- data/ext/carray_index_classifier.c +1035 -0
- data/ext/carray_index_classifier.h +27 -0
- data/ext/carray_internal.h +120 -0
- data/ext/carray_kernels_bincmp.c +4445 -0
- data/ext/carray_kernels_binop.c +10979 -0
- data/ext/carray_kernels_init.c +36 -0
- data/ext/carray_kernels_map.c +3466 -0
- data/ext/carray_kernels_moncmp.c +2096 -0
- data/ext/carray_kernels_monop.c +18312 -0
- data/ext/carray_kernels_reduce_aggregate.c +25836 -0
- data/ext/carray_kernels_reduce_boolean.c +329 -0
- data/ext/carray_kernels_reduce_cumulative.c +14592 -0
- data/ext/carray_kernels_reduce_extreme.c +16947 -0
- data/ext/carray_kernels_reduce_variance.c +3909 -0
- data/ext/carray_kernels_scan.c +3692 -0
- data/ext/carray_kernels_search.c +32137 -0
- data/ext/carray_kernels_sort.c +10625 -0
- data/ext/carray_kernels_triop.c +1391 -0
- data/ext/carray_lazy.c +567 -0
- data/ext/carray_loop.c +88 -200
- data/ext/carray_mask.c +848 -154
- data/ext/carray_math_kernel.h +120 -0
- data/ext/carray_mathfunc.c +10 -241
- data/ext/carray_median_percentile.c +1257 -0
- data/ext/carray_memory_view.c +1625 -0
- data/ext/carray_operator.c +1526 -318
- data/ext/carray_order.c +664 -1394
- data/ext/carray_partition.c +416 -0
- data/ext/carray_random.c +518 -0
- data/ext/carray_scatter.c +357 -0
- data/ext/carray_slab.c +1219 -0
- data/ext/carray_slab.h +84 -0
- data/ext/carray_sort.c +829 -0
- data/ext/carray_sort_kernel.c +620 -0
- data/ext/carray_struct.c +695 -0
- data/ext/carray_test.c +343 -229
- data/ext/carray_undef.c +34 -17
- data/ext/carray_utils.c +175 -74
- data/ext/extconf.rb +216 -55
- data/ext/mk_call_cfunc.rb +480 -0
- data/ext/mkkernel.rb +8842 -0
- data/ext/ruby_carray.c +202 -101
- data/ext/version.h +4 -14
- data/ext/version.rb +5 -13
- data/lib/carray/arrow_tensor.rb +401 -0
- data/lib/carray/attribute.rb +166 -0
- data/lib/carray/autoload_carray.rb +220 -0
- data/lib/carray/autoload_method_extension.rb +44 -0
- data/lib/carray/axis_group.rb +711 -0
- data/lib/carray/basics.rb +481 -0
- data/lib/carray/bincount_nd.rb +358 -0
- data/lib/carray/block_iterator.rb +604 -0
- data/lib/carray/boolean_reduce.rb +109 -0
- data/lib/carray/categorical.rb +561 -0
- data/lib/carray/categorical_iterator.rb +1062 -0
- data/lib/carray/complex.rb +150 -0
- data/lib/carray/conditional.rb +216 -0
- data/lib/carray/const_string.rb +228 -0
- data/lib/carray/construct.rb +139 -328
- data/lib/carray/core_extensions.rb +240 -0
- data/lib/carray/data_type_extension.rb +233 -0
- data/lib/carray/fixlen_string.rb +95 -0
- data/lib/carray/frame/concat.rb +132 -0
- data/lib/carray/frame/convert.rb +95 -0
- data/lib/carray/frame/csv_parser.rb +211 -0
- data/lib/carray/frame/frame.rb +649 -0
- data/lib/carray/frame/group.rb +186 -0
- data/lib/carray/frame/io.rb +164 -0
- data/lib/carray/frame/join.rb +248 -0
- data/lib/carray/frame/records.rb +99 -0
- data/lib/carray/frame/sort.rb +113 -0
- data/lib/carray/frame/verbs.rb +299 -0
- data/lib/carray/frame.rb +16 -0
- data/lib/carray/histogram.rb +512 -0
- data/lib/carray/inspect.rb +37 -20
- data/lib/carray/iterator.rb +57 -349
- data/lib/carray/lazy.rb +889 -0
- data/lib/carray/mask_gap_fill.rb +200 -0
- data/lib/carray/math.rb +78 -342
- data/lib/carray/meld_reduce.rb +289 -0
- data/lib/carray/methods/align_addr.rb +116 -0
- data/lib/carray/methods/bin.rb +128 -0
- data/lib/carray/methods/bincount.rb +87 -0
- data/lib/carray/methods/bit_string.rb +92 -0
- data/lib/carray/methods/broadcast.rb +63 -0
- data/lib/carray/methods/choose.rb +39 -0
- data/lib/carray/methods/composition.rb +280 -0
- data/lib/carray/methods/gather_nd.rb +206 -0
- data/lib/carray/methods/index.rb +39 -0
- data/lib/carray/methods/insert_block.rb +99 -0
- data/lib/carray/methods/is_in.rb +141 -0
- data/lib/carray/methods/join.rb +90 -0
- data/lib/carray/methods/locate_addr.rb +47 -0
- data/lib/carray/methods/mask_duplicates.rb +41 -0
- data/lib/carray/methods/meshgrid.rb +91 -0
- data/lib/carray/methods/mode.rb +126 -0
- data/lib/carray/methods/nunique.rb +46 -0
- data/lib/carray/methods/resize.rb +56 -0
- data/lib/carray/methods/snap.rb +156 -0
- data/lib/carray/methods/string_format.rb +57 -0
- data/lib/carray/methods/unique.rb +47 -0
- data/lib/carray/methods/value_counts.rb +71 -0
- data/lib/carray/mkmf.rb +124 -101
- data/lib/carray/runtime.rb +108 -0
- data/lib/carray/serialize.rb +478 -167
- data/lib/carray/slab_iterator.rb +292 -0
- data/lib/carray/stack.rb +291 -0
- data/lib/carray/string.rb +56 -180
- data/lib/carray/string_operation_extension.rb +289 -0
- data/lib/carray/struct.rb +335 -323
- data/lib/carray/struct_builder.rb +697 -0
- data/lib/carray/table.rb +41 -2
- data/lib/carray/time.rb +2255 -38
- data/lib/carray/window_iterator.rb +655 -0
- data/lib/carray.rb +55 -57
- metadata +163 -130
- data/Rakefile +0 -51
- data/TODO.md +0 -18
- data/ext/ca_iter_block.c +0 -257
- data/ext/ca_iter_dimension.c +0 -299
- data/ext/ca_iter_window.c +0 -214
- data/ext/ca_obj_mapping.c +0 -644
- data/ext/carray_iterator.c +0 -641
- data/ext/carray_math.rb +0 -850
- data/ext/carray_numeric.c +0 -259
- data/ext/carray_sort_addr.c +0 -254
- data/ext/carray_stat.c +0 -2100
- data/ext/carray_stat_proc.rb +0 -1999
- data/ext/mkmath.rb +0 -741
- data/ext/ruby_ccomplex.c +0 -509
- data/ext/ruby_float_func.c +0 -86
- data/lib/carray/array.rb +0 -8
- data/lib/carray/autoload/autoload_base.rb +0 -19
- data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
- data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
- data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
- data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
- data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
- data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
- data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
- data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
- data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
- data/lib/carray/autoload/autoload_gem_random.rb +0 -8
- data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
- data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
- data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
- data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
- data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
- data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
- data/lib/carray/autoload/autoload_object_link.rb +0 -1
- data/lib/carray/autoload/autoload_object_pack.rb +0 -2
- data/lib/carray/autoload.rb +0 -141
- data/lib/carray/basic.rb +0 -191
- data/lib/carray/broadcast.rb +0 -101
- data/lib/carray/compose.rb +0 -315
- data/lib/carray/convert.rb +0 -115
- data/lib/carray/info.rb +0 -110
- data/lib/carray/io/imagemagick.rb +0 -235
- data/lib/carray/mask.rb +0 -102
- data/lib/carray/math/histogram.rb +0 -177
- data/lib/carray/math/recurrence.rb +0 -93
- data/lib/carray/object/ca_obj_iterator.rb +0 -50
- data/lib/carray/object/ca_obj_link.rb +0 -50
- data/lib/carray/object/ca_obj_pack.rb +0 -99
- data/lib/carray/obsolete.rb +0 -256
- data/lib/carray/ordering.rb +0 -181
- data/lib/carray/testing.rb +0 -51
- data/lib/carray/transform.rb +0 -109
- data/misc/Methods.ja.md +0 -182
- data/misc/NOTE +0 -51
- data/spec/Classes/CABitfield_spec.rb +0 -58
- data/spec/Classes/CABlockIterator_spec.rb +0 -114
- data/spec/Classes/CABlock_spec.rb +0 -205
- data/spec/Classes/CAField_spec.rb +0 -39
- data/spec/Classes/CAGrid_spec.rb +0 -75
- data/spec/Classes/CAMap_spec.rb +0 -0
- data/spec/Classes/CAMapping_spec.rb +0 -105
- data/spec/Classes/CAObject_attribute_spec.rb +0 -33
- data/spec/Classes/CAObject_spec.rb +0 -33
- data/spec/Classes/CARefer_spec.rb +0 -93
- data/spec/Classes/CARepeat_spec.rb +0 -65
- data/spec/Classes/CASelect_spec.rb +0 -22
- data/spec/Classes/CAShift_spec.rb +0 -16
- data/spec/Classes/CAStruct_spec.rb +0 -71
- data/spec/Classes/CATranspose_spec.rb +0 -60
- data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
- data/spec/Classes/CAWindow_spec.rb +0 -54
- data/spec/Classes/CAWrap_spec.rb +0 -8
- data/spec/Classes/CArray_spec.rb +0 -184
- data/spec/Classes/CScalar_spec.rb +0 -55
- data/spec/Classes/ex1.rb +0 -46
- data/spec/Features/feature_130_spec.rb +0 -19
- data/spec/Features/feature_attributes_spec.rb +0 -280
- data/spec/Features/feature_boolean_spec.rb +0 -98
- data/spec/Features/feature_broadcast.rb +0 -116
- data/spec/Features/feature_cast_function.rb +0 -19
- data/spec/Features/feature_cast_spec.rb +0 -33
- data/spec/Features/feature_class_spec.rb +0 -84
- data/spec/Features/feature_complex_spec.rb +0 -42
- data/spec/Features/feature_composite_spec.rb +0 -124
- data/spec/Features/feature_convert_spec.rb +0 -46
- data/spec/Features/feature_copy_spec.rb +0 -123
- data/spec/Features/feature_creation_spec.rb +0 -84
- data/spec/Features/feature_element_spec.rb +0 -144
- data/spec/Features/feature_extream_spec.rb +0 -54
- data/spec/Features/feature_generate_spec.rb +0 -74
- data/spec/Features/feature_index_spec.rb +0 -69
- data/spec/Features/feature_mask_spec.rb +0 -580
- data/spec/Features/feature_math_spec.rb +0 -97
- data/spec/Features/feature_order_spec.rb +0 -146
- data/spec/Features/feature_ref_store_spec.rb +0 -209
- data/spec/Features/feature_serialization_spec.rb +0 -125
- data/spec/Features/feature_stat_spec.rb +0 -397
- data/spec/Features/feature_virtual_spec.rb +0 -48
- data/spec/Features/method_eq_spec.rb +0 -81
- data/spec/Features/method_is_nan_spec.rb +0 -12
- data/spec/Features/method_map_spec.rb +0 -54
- data/spec/Features/method_max_with.rb +0 -20
- data/spec/Features/method_min_with.rb +0 -19
- data/spec/Features/method_ne_spec.rb +0 -18
- data/spec/Features/method_project_spec.rb +0 -188
- data/spec/Features/method_ref_spec.rb +0 -27
- data/spec/Features/method_round_spec.rb +0 -11
- data/spec/Features/method_s_linspace_spec.rb +0 -48
- data/spec/Features/method_s_span_spec.rb +0 -14
- data/spec/Features/method_seq_spec.rb +0 -47
- data/spec/Features/method_sort_with.rb +0 -43
- data/spec/Features/method_sorted_with.rb +0 -29
- data/spec/Features/method_span_spec.rb +0 -42
- data/spec/Features/method_wrap_readonly_spec.rb +0 -43
- data/spec/UnitTest/test_CAVirtual.rb +0 -214
- data/spec/spec_all.rb +0 -10
- data/utils/ca_ase.rb +0 -21
- data/utils/ca_methods.rb +0 -15
- data/utils/cast_checker.rb +0 -30
- data/utils/convert_test.rb +0 -73
- data/utils/extract_yard.rb +0 -22
- data/utils/guess_shape.rb +0 -76
- data/utils/monkey_patch_methods.rb +0 -62
- data/utils/remove_resource_fork.sh +0 -5
data/lib/carray/time.rb
CHANGED
|
@@ -1,76 +1,2293 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lib/carray/time.rb
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# Ruby surface for the CATime / CATimedelta Faces: construction
|
|
4
|
+
# entry points, arithmetic / comparison operators, reductions, and the
|
|
5
|
+
# pandas-style field accessors.
|
|
4
6
|
#
|
|
5
|
-
#
|
|
7
|
+
# CATime / CATimedelta construction entry points:
|
|
8
|
+
# - `CATime.new(*shape, unit: :ns)` — new allocate + Face wrap
|
|
9
|
+
# - `CATime.wrap(raw_int64, unit: :ns)` — zero-copy mask over existing entity (C side)
|
|
10
|
+
# - `CArray#time(unit: :ns)` — instance-method Face-conversion sugar (= sugar over `.wrap`)
|
|
11
|
+
# - Same pattern for CATimedelta
|
|
6
12
|
#
|
|
7
|
-
#
|
|
13
|
+
# Base epoch fixed to 1970-01-01 (Unix epoch).
|
|
14
|
+
# NetCDF CF convention (= reference time epoch) is delegated to external gems (timesteps etc.).
|
|
15
|
+
|
|
16
|
+
# Internal: unit-conversion algebra shared by CATime / CATimedelta
|
|
17
|
+
# (used by to_comparable to align a search query to the reference unit).
|
|
8
18
|
#
|
|
9
|
-
#
|
|
19
|
+
# The units split into two groups that are NOT inter-convertible by a fixed
|
|
20
|
+
# ratio: fixed-length (W/D/h/m/s/ms/.../as, ratios in seconds) and calendar
|
|
21
|
+
# (Y/M, ratio in months -- W and below are calendar-dependent in days).
|
|
22
|
+
# Within a group any pair is an exact integer ratio (coarse = fine * N), so a
|
|
23
|
+
# coarse->fine cast is lossless (multiply), and a fine->coarse cast is lossless
|
|
24
|
+
# only when every value is divisible by the divisor.
|
|
25
|
+
module CATimeUnitAlgebra
|
|
26
|
+
# seconds per base unit (Rational)
|
|
27
|
+
FIXED = {
|
|
28
|
+
W: 604800r, D: 86400r, h: 3600r, m: 60r, s: 1r,
|
|
29
|
+
ms: Rational(1, 10**3), us: Rational(1, 10**6), ns: Rational(1, 10**9),
|
|
30
|
+
ps: Rational(1, 10**12), fs: Rational(1, 10**15), as: Rational(1, 10**18),
|
|
31
|
+
}.freeze
|
|
32
|
+
# months per base unit (Rational)
|
|
33
|
+
CALENDAR = { Y: 12r, M: 1r }.freeze
|
|
34
|
+
|
|
35
|
+
# Base units from finest to coarsest granularity. Every fixed-length unit is
|
|
36
|
+
# finer than every calendar unit (a week < a month), so this is a total
|
|
37
|
+
# order used to pick the base that two operands both convert into exactly.
|
|
38
|
+
GRANULARITY = %i[as fs ps ns us ms s m h D W M Y].freeze
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# seconds- (fixed) or months- (calendar) per base tick.
|
|
43
|
+
def base_ratio(base)
|
|
44
|
+
FIXED[base] || CALENDAR[base]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Normalize a unit spec (Resolution / Symbol / String) to a Resolution.
|
|
48
|
+
# A bare Symbol / String routes through Resolution.parse (count-1 base).
|
|
49
|
+
def res(u)
|
|
50
|
+
u.is_a?(CATime::Resolution) ? u : CATime::Resolution.parse(u)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Greatest common divisor of two positive Rationals (both in lowest terms).
|
|
54
|
+
def rgcd(a, b)
|
|
55
|
+
Rational(a.numerator.gcd(b.numerator), a.denominator.lcm(b.denominator))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether two units are in the same group (both calendar or both fixed).
|
|
59
|
+
def same_group?(u1, u2)
|
|
60
|
+
CALENDAR.key?(res(u1).base) == CALENDAR.key?(res(u2).base)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The common grid two same-group resolutions both convert into exactly: the
|
|
64
|
+
# resolution whose tick is the gcd of the two ticks (finest common base +
|
|
65
|
+
# the whole multiplier). For equal resolutions this is the resolution
|
|
66
|
+
# itself; for `(1,:D)` & `(1,:h)` it is `(1,:h)`; for `(5,:m)` & `(2,:m)` it
|
|
67
|
+
# is `(1,:m)`. Both operands are same-group (checked by the caller).
|
|
68
|
+
def common(u1, u2)
|
|
69
|
+
a = res(u1); b = res(u2)
|
|
70
|
+
fb = GRANULARITY.index(a.base) <= GRANULARITY.index(b.base) ? a.base : b.base
|
|
71
|
+
g = rgcd(a.tick_ratio, b.tick_ratio)
|
|
72
|
+
CATime::Resolution.new(Integer(g / base_ratio(fb)), fb)
|
|
73
|
+
end
|
|
74
|
+
alias_method :finer, :common
|
|
75
|
+
module_function :finer
|
|
76
|
+
|
|
77
|
+
# Common resolution for a time difference (E): same group -> the common
|
|
78
|
+
# grid; cross-group -> the fixed-group resolution (a :M/:Y difference only
|
|
79
|
+
# arises from two calendar operands), coarsened to `(1,:D)` when the fixed
|
|
80
|
+
# side is a week (a week is not calendar-alignable, but both sides convert
|
|
81
|
+
# into days exactly).
|
|
82
|
+
def diff_unit(u1, u2)
|
|
83
|
+
a = res(u1); b = res(u2)
|
|
84
|
+
return common(a, b) if same_group?(a, b)
|
|
85
|
+
fx = CALENDAR.key?(a.base) ? b : a
|
|
86
|
+
fx.base == :W ? CATime::Resolution.new(1, :D) : fx
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Tick ratio from `from` to `to` (how many `to` ticks per `from` tick), or
|
|
90
|
+
# nil if they are in different groups (not inter-convertible by a fixed
|
|
91
|
+
# ratio). Folds each resolution's count.
|
|
92
|
+
def ratio(from, to)
|
|
93
|
+
a = res(from); b = res(to)
|
|
94
|
+
return nil unless same_group?(a, b)
|
|
95
|
+
a.tick_ratio / b.tick_ratio
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Target ticks per source tick for the strict unit-change surface
|
|
99
|
+
# (CATime#to_unit / CATimedelta#to_unit): accepted only when the source tick
|
|
100
|
+
# is a whole multiple of the target tick, so every value re-expresses
|
|
101
|
+
# exactly on the finer grid. A coarser target (which would round) and a
|
|
102
|
+
# cross-group pair (no fixed ratio at all) both raise -- unlike
|
|
103
|
+
# convert_scale! / convert_instant!, which coarsen when the values happen to
|
|
104
|
+
# allow it, this decides on the units alone.
|
|
105
|
+
def multiple_factor(from, to)
|
|
106
|
+
a = res(from); b = res(to)
|
|
107
|
+
return 1 if a == b
|
|
108
|
+
r = ratio(a, b)
|
|
109
|
+
if r.nil?
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"cannot express #{a} in #{b}: calendar units (:Y/:M) and " \
|
|
112
|
+
"fixed-length units (:W/:D/:h/:s/...) have no fixed ratio"
|
|
113
|
+
end
|
|
114
|
+
unless r.denominator == 1
|
|
115
|
+
raise ArgumentError,
|
|
116
|
+
"cannot express #{a} in whole #{b} ticks " \
|
|
117
|
+
"(a #{a} tick is not a whole multiple of a #{b} tick)"
|
|
118
|
+
end
|
|
119
|
+
r.numerator
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `storage * factor` with a loud overflow guard: widening a wide time range
|
|
123
|
+
# into a fine unit can exceed int64, and a silent wrap would give a wrong
|
|
124
|
+
# instant / duration. Checks the extremes (they bound every element), then
|
|
125
|
+
# multiplies. Shared by every coarse->fine conversion (arithmetic,
|
|
126
|
+
# comparison, search).
|
|
127
|
+
def widen(storage, factor)
|
|
128
|
+
return storage if factor == 1
|
|
129
|
+
if storage.elements > 0
|
|
130
|
+
raw = storage.has_mask? ? storage.value : storage
|
|
131
|
+
lo = raw.min
|
|
132
|
+
unless lo == UNDEF
|
|
133
|
+
lim = 2**63 - 1
|
|
134
|
+
[lo, raw.max].each do |x|
|
|
135
|
+
next if (Integer(x) * factor).abs <= lim
|
|
136
|
+
raise RangeError,
|
|
137
|
+
"time unit conversion overflows int64: the time range is " \
|
|
138
|
+
"too wide to widen into this resolution (x#{factor})"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
storage * factor
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# SCALE conversion (durations / timedelta): convert an int64 storage CArray
|
|
146
|
+
# from `from` unit to `to` unit by the fixed ratio. coarse->fine multiplies;
|
|
147
|
+
# fine->coarse divides only when every value is exact; cross-group ALWAYS
|
|
148
|
+
# raises -- a :M / :Y duration has no fixed ratio to days (a month is
|
|
149
|
+
# calendar-variable), so it genuinely cannot scale to seconds.
|
|
150
|
+
def convert_scale!(storage, from, to)
|
|
151
|
+
a = res(from); b = res(to)
|
|
152
|
+
return storage if a == b
|
|
153
|
+
r = ratio(a, b)
|
|
154
|
+
if r.nil?
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
"cannot scale duration #{a} to #{b}: calendar units " \
|
|
157
|
+
"(:Y/:M) and fixed-length units (:W/:D/:h/:s/...) have no fixed " \
|
|
158
|
+
"ratio (a month / year is calendar-variable)"
|
|
159
|
+
end
|
|
160
|
+
if r.denominator == 1
|
|
161
|
+
widen(storage, r.numerator) # coarse -> fine: lossless multiply
|
|
162
|
+
else
|
|
163
|
+
divisor = r.denominator # fine -> coarse: exact only
|
|
164
|
+
unless (storage % divisor).eq(0).all
|
|
165
|
+
raise ArgumentError,
|
|
166
|
+
"cannot scale duration #{a} to #{b} without loss: " \
|
|
167
|
+
"some values are not a whole multiple of #{b} " \
|
|
168
|
+
"(finer resolution would be truncated)"
|
|
169
|
+
end
|
|
170
|
+
storage / divisor
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# SCALE conversion with truncation: like convert_scale! but a fine->coarse
|
|
175
|
+
# conversion drops the sub-`to` remainder (truncating toward zero) instead of
|
|
176
|
+
# raising. Used for dt +/- td, where the result keeps the time's unit
|
|
177
|
+
# and a finer duration is truncated to it (a :D time + a 5 h duration is
|
|
178
|
+
# + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
|
|
179
|
+
# duration has no fixed ratio to a fixed unit).
|
|
180
|
+
def convert_scale_trunc(storage, from, to)
|
|
181
|
+
a = res(from); b = res(to)
|
|
182
|
+
return storage if a == b
|
|
183
|
+
r = ratio(a, b)
|
|
184
|
+
if r.nil?
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"cannot combine a #{a} duration with a #{b} time across the " \
|
|
187
|
+
"calendar/fixed boundary (a calendar duration has no fixed ratio)"
|
|
188
|
+
end
|
|
189
|
+
r.denominator == 1 ? widen(storage, r.numerator) : storage / r.denominator
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# INSTANT conversion (absolute datetimes): unlike a duration, a time :M
|
|
193
|
+
# value HAS a well-defined instant (the month's first midnight), so a
|
|
194
|
+
# cross-group cast is possible via civil-date algebra even though no fixed
|
|
195
|
+
# ratio exists (a :M time casts to :s, a :M duration cannot). Same-group
|
|
196
|
+
# falls back to the ratio.
|
|
197
|
+
# - calendar (:M/:Y) -> fixed (<= :D): always exact (widen to the finer
|
|
198
|
+
# grid). :W is rejected (month / year starts are not week-aligned).
|
|
199
|
+
# - fixed -> calendar: exact only when the instant lands on the calendar
|
|
200
|
+
# boundary (midnight of the 1st), else raises.
|
|
201
|
+
def convert_instant!(storage, from, to)
|
|
202
|
+
a = res(from); b = res(to)
|
|
203
|
+
return storage if a == b
|
|
204
|
+
return convert_scale!(storage, a, b) if ratio(a, b) # same group
|
|
205
|
+
if CALENDAR.key?(a.base)
|
|
206
|
+
_instant_cal_to_fixed(storage, a, b) # widen
|
|
207
|
+
else
|
|
208
|
+
_instant_fixed_to_cal(storage, a, b) # coarsen (exact-or-raise)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# calendar time (Resolution `from`) -> days since the epoch (int64
|
|
213
|
+
# CArray). Folds the resolution count (value = count-Y/M buckets).
|
|
214
|
+
def _cal_days(storage, from)
|
|
215
|
+
ones = CArray.int64(*storage.shape) { 1 }
|
|
216
|
+
if from.base == :M
|
|
217
|
+
abs = storage * from.count + 1970 * 12 # absolute month ordinal
|
|
218
|
+
y = CATime.send(:_floordiv_i, abs, 12)
|
|
219
|
+
m = abs - y * 12 + 1
|
|
220
|
+
CATime.send(:_days_from_civil, y, m, ones)
|
|
221
|
+
else # :Y
|
|
222
|
+
CATime.send(:_days_from_civil, storage * from.count + 1970, ones, ones)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# calendar time -> fixed-length grid: the widening half of
|
|
227
|
+
# convert_instant!. Goes through the day count, so the target grid has
|
|
228
|
+
# to tile a day exactly (:W is rejected -- month starts are not
|
|
229
|
+
# week-aligned). Always exact once that holds.
|
|
230
|
+
def _instant_cal_to_fixed(storage, from, to)
|
|
231
|
+
r = ratio(CATime::Resolution.new(1, :D), to) # ticks of `to` per day
|
|
232
|
+
unless r.denominator == 1
|
|
233
|
+
raise ArgumentError,
|
|
234
|
+
"cannot convert calendar time #{from} to #{to} " \
|
|
235
|
+
"(a day boundary is not aligned to the #{to} grid)"
|
|
236
|
+
end
|
|
237
|
+
widen(_cal_days(storage, from), r.numerator)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# fixed-length grid -> calendar time: the coarsening half of
|
|
241
|
+
# convert_instant!, exact-or-raise. Every instant must land on a day
|
|
242
|
+
# boundary and then on the calendar boundary itself (the 1st, and
|
|
243
|
+
# January too for :Y), since a mid-month instant has no :M value.
|
|
244
|
+
def _instant_fixed_to_cal(storage, from, to)
|
|
245
|
+
rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
|
|
246
|
+
days =
|
|
247
|
+
if rd.denominator == 1
|
|
248
|
+
n = rd.numerator
|
|
249
|
+
unless (storage % n).eq(0).all
|
|
250
|
+
raise ArgumentError,
|
|
251
|
+
"cannot convert time #{from} to #{to} without loss " \
|
|
252
|
+
"(instant is not on a day boundary)"
|
|
253
|
+
end
|
|
254
|
+
storage / n
|
|
255
|
+
else # coarser than a day (:W)
|
|
256
|
+
storage * (from.tick_ratio / 86400r).to_i
|
|
257
|
+
end
|
|
258
|
+
y, m, d = CATime.send(:_civil_from_days, days)
|
|
259
|
+
on_boundary = d.eq(1)
|
|
260
|
+
on_boundary &= m.eq(1) if to.base == :Y
|
|
261
|
+
unless on_boundary.all
|
|
262
|
+
raise ArgumentError,
|
|
263
|
+
"cannot convert time #{from} to #{to} without loss " \
|
|
264
|
+
"(instant is not on a #{to} boundary)"
|
|
265
|
+
end
|
|
266
|
+
ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
|
|
267
|
+
if to.count > 1
|
|
268
|
+
unless (ord % to.count).eq(0).all
|
|
269
|
+
raise ArgumentError,
|
|
270
|
+
"cannot convert time #{from} to #{to} without loss " \
|
|
271
|
+
"(instant is not on a #{to} boundary)"
|
|
272
|
+
end
|
|
273
|
+
ord = CATime.send(:_floordiv_i, ord, to.count)
|
|
274
|
+
end
|
|
275
|
+
ord
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
class CATime
|
|
280
|
+
# @overload new(*shape, unit: :ns)
|
|
281
|
+
# Allocates a new int64 storage CArray of the given `shape` and
|
|
282
|
+
# wraps it as a CATime Face with the given `unit`. The
|
|
283
|
+
# reference epoch is the Unix epoch (1970-01-01 UTC).
|
|
284
|
+
# @param shape [Array<Integer>] shape of the new CATime.
|
|
285
|
+
# @param unit [Symbol] resolution unit (`:Y`, `:M`, `:W`, `:D`,
|
|
286
|
+
# `:h`, `:m`, `:s`, `:ms`, `:us`, `:ns`, ...).
|
|
287
|
+
# @return [CATime]
|
|
288
|
+
def self.new(*shape, unit: :ns)
|
|
289
|
+
raw = CArray.int64(*shape)
|
|
290
|
+
wrap(raw, unit: unit)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @overload wrap(raw, unit: :ns)
|
|
294
|
+
# Zero-copy Face wrap of an existing int64 CArray. `unit` is a
|
|
295
|
+
# {Resolution} (or a Symbol / String it parses from).
|
|
296
|
+
# @param raw [CArray] int64 storage.
|
|
297
|
+
# @param unit [Resolution, Symbol, String] tick resolution.
|
|
298
|
+
# @return [CATime]
|
|
299
|
+
def self.wrap(raw, unit: :ns)
|
|
300
|
+
__wrap__(raw, unit)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Element-return class: a single time element carries both its
|
|
304
|
+
# int64 count and its resolution, so scalar access (ca[i,j]) round-trips
|
|
305
|
+
# through this rather than a bare Integer.
|
|
306
|
+
class Element
|
|
307
|
+
include Comparable
|
|
308
|
+
|
|
309
|
+
# @!visibility private
|
|
310
|
+
UNIT_TO_SECONDS = {
|
|
311
|
+
Y: 365.25 * 86400, M: 30.5 * 86400, W: 7 * 86400, D: 86400,
|
|
312
|
+
h: 3600, m: 60, s: 1,
|
|
313
|
+
ms: 1.0e-3, us: 1.0e-6, ns: 1.0e-9,
|
|
314
|
+
ps: 1.0e-12, fs: 1.0e-15, as: 1.0e-18
|
|
315
|
+
}.freeze
|
|
316
|
+
|
|
317
|
+
# storage + value/unit accessors + initialize are C-implemented in
|
|
318
|
+
# ext/ca_obj_datetime.c (= TypedData_Make_Struct fast path).
|
|
319
|
+
# Methods below are Ruby-side; they call value/unit which are C accessors.
|
|
320
|
+
#
|
|
321
|
+
# Calendar breakdown is delegated to Ruby Date / Time (the scalar pays one
|
|
322
|
+
# object's cost, unlike the vectorized array accessors), but exactly: a
|
|
323
|
+
# :M / :Y value decodes through Date#next_month / #next_year (NOT the
|
|
324
|
+
# 30.5-day UNIT_TO_SECONDS approximation, which drifts), and a fixed-unit
|
|
325
|
+
# value through an exact Rational-second Time.at. All UTC.
|
|
326
|
+
|
|
327
|
+
EPOCH_JD = 2440588 # Julian Day Number of 1970-01-01
|
|
328
|
+
|
|
329
|
+
# @return [Time] the instant this scalar denotes (UTC). For a calendar
|
|
330
|
+
# unit this is the first instant (midnight of day 1) of the granule.
|
|
331
|
+
def to_time
|
|
332
|
+
require 'time'
|
|
333
|
+
require 'date'
|
|
334
|
+
case unit.base
|
|
335
|
+
when :Y then d = _epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
336
|
+
when :M then d = _epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
337
|
+
else
|
|
338
|
+
Time.at(Rational(value) * unit.tick_ratio, in: 'UTC') # exact seconds
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @return [Date] the date this scalar denotes (UTC). A sub-day unit is
|
|
343
|
+
# floored to its day.
|
|
344
|
+
def to_date
|
|
345
|
+
require 'date'
|
|
346
|
+
case unit.base
|
|
347
|
+
when :Y then _epoch_date.next_year(value * unit.count)
|
|
348
|
+
when :M then _epoch_date.next_month(value * unit.count)
|
|
349
|
+
when :W then Date.jd(EPOCH_JD + value * unit.count * 7, Date::GREGORIAN)
|
|
350
|
+
when :D then Date.jd(EPOCH_JD + value * unit.count, Date::GREGORIAN)
|
|
351
|
+
else Date.jd(EPOCH_JD + _floor_days, Date::GREGORIAN) # sub-day: floor to day
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# @return [DateTime] the instant this scalar denotes (UTC).
|
|
356
|
+
def to_datetime
|
|
357
|
+
to_time.to_datetime
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Unit-aware string: coarse units print at their own granularity so the
|
|
361
|
+
# `(value, unit)` pair stays recoverable (a :M scalar is "2024-03", not
|
|
362
|
+
# "2024-03-01T00:00:00Z", which is indistinguishable from a :D).
|
|
363
|
+
# @return [String]
|
|
364
|
+
def to_s
|
|
365
|
+
case unit.base
|
|
366
|
+
when :Y then format("%04d", to_date.year)
|
|
367
|
+
when :M then to_date.strftime("%Y-%m")
|
|
368
|
+
when :W, :D then to_date.strftime("%Y-%m-%d")
|
|
369
|
+
else to_time.iso8601(_precision_digits) # :h .. :as (time shown)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# @return [String] the instant plus its storage unit.
|
|
374
|
+
def inspect
|
|
375
|
+
tag = unit.count == 1 ? "#{value}#{unit.base}" : "#{value} @ #{unit}"
|
|
376
|
+
"#<CATime::Element #{to_s} (#{tag})>"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Three-way compare by INSTANT, reconciling a different unit (both are
|
|
380
|
+
# brought to a common unit both reach exactly, via `diff_unit`), so two
|
|
381
|
+
# datetimes are always ordered regardless of unit. Returns nil for a
|
|
382
|
+
# non-time operand (Time / DateTime are accepted); per the Comparable
|
|
383
|
+
# contract that makes `==` false and `<` raise, so no explicit `==` is
|
|
384
|
+
# defined (Comparable derives it: same instant -> equal, even cross-unit).
|
|
385
|
+
def <=>(other)
|
|
386
|
+
case other
|
|
387
|
+
when Element
|
|
388
|
+
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
389
|
+
_instant_in(u) <=> other._instant_in(u)
|
|
390
|
+
when Time
|
|
391
|
+
to_time <=> other.getutc
|
|
392
|
+
when (defined?(DateTime) ? DateTime : nil)
|
|
393
|
+
to_time <=> other.to_time.getutc
|
|
394
|
+
end
|
|
395
|
+
rescue ArgumentError
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Hash-key identity is unit-strict (mirrors Ruby: 1 == 1.0 but not
|
|
400
|
+
# 1.eql?(1.0)), so a :s and a :ms scalar at the same instant compare `==`
|
|
401
|
+
# yet key a Hash separately.
|
|
402
|
+
def eql?(other)
|
|
403
|
+
other.is_a?(Element) && other.unit == unit && other.value == value
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# @return [Integer] a hash consistent with #eql? (unit-strict).
|
|
407
|
+
def hash
|
|
408
|
+
[value, unit].hash
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# @overload +(td)
|
|
412
|
+
# Returns this instant advanced by a {CATimedelta::Element}. The unit
|
|
413
|
+
# promotes to the finer of the two (numpy-style: :D + :h -> :h). Only a
|
|
414
|
+
# duration in the SAME group (both calendar or both fixed) is accepted --
|
|
415
|
+
# a cross-group step (a :s time + a :M duration) is calendar
|
|
416
|
+
# arithmetic; use `to_date` + `Date#next_month` / `#next_year` for that.
|
|
417
|
+
# @param td [CATimedelta::Element]
|
|
418
|
+
# @return [CATime::Element]
|
|
419
|
+
# @raise [TypeError, ArgumentError] on a non-timedelta / cross-group operand.
|
|
420
|
+
def +(td)
|
|
421
|
+
_combine(td, 1)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# @overload -(other)
|
|
425
|
+
# Subtracting a {CATimedelta::Element} yields a {CATime::Element}
|
|
426
|
+
# (same rule as `+`); subtracting another {CATime::Element} yields
|
|
427
|
+
# the elapsed duration as a {CATimedelta::Element}, in the `diff_unit`
|
|
428
|
+
# of the two (same group -> finer unit; cross-group -> the fixed unit).
|
|
429
|
+
# @param other [CATimedelta::Element, CATime::Element]
|
|
430
|
+
# @return [CATime::Element, CATimedelta::Element]
|
|
431
|
+
def -(other)
|
|
432
|
+
case other
|
|
433
|
+
when Element
|
|
434
|
+
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
435
|
+
CATimedelta::Element.new(_instant_in(u) - other.send(:_instant_in, u), u)
|
|
436
|
+
when CATimedelta::Element
|
|
437
|
+
_combine(other, -1)
|
|
438
|
+
else
|
|
439
|
+
raise TypeError, "CATime::Element - #{other.class} is not allowed"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
protected
|
|
444
|
+
|
|
445
|
+
# This instant's storage value expressed in `to` unit (exact; raises if the
|
|
446
|
+
# instant does not land on `to`'s grid).
|
|
447
|
+
def _instant_in(to)
|
|
448
|
+
return value if to == unit
|
|
449
|
+
CATimeUnitAlgebra.convert_instant!(CA_INT64([value]), unit, to)[0]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
|
|
454
|
+
# s +/- td: the time is the anchor, so the result keeps self's unit and
|
|
455
|
+
# the duration is converted into it (truncated toward zero when finer; a
|
|
456
|
+
# cross-group calendar duration raises). Mirrors the array CATime +/-.
|
|
457
|
+
def _combine(td, sign)
|
|
458
|
+
unless td.is_a?(CATimedelta::Element)
|
|
459
|
+
raise TypeError,
|
|
460
|
+
"CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
|
|
461
|
+
"allowed (add / subtract a CATimedelta)"
|
|
462
|
+
end
|
|
463
|
+
tv = CATimeUnitAlgebra.convert_scale_trunc(CA_INT64([td.value]), td.unit, unit)[0]
|
|
464
|
+
Element.new(value + sign * tv, unit)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Base epoch as a Date. next_month / next_year clamp end-of-month
|
|
468
|
+
# (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
|
|
469
|
+
# do not move the base off the first of the month without revisiting.
|
|
470
|
+
def _epoch_date
|
|
471
|
+
require 'date'
|
|
472
|
+
Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Days since the epoch for a fixed sub-day unit (floored toward the past).
|
|
476
|
+
def _floor_days
|
|
477
|
+
sec = Rational(value) * unit.tick_ratio # exact seconds
|
|
478
|
+
(sec / 86400).floor
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def _precision_digits
|
|
482
|
+
case unit.base
|
|
483
|
+
when :ms then 3
|
|
484
|
+
when :us then 6
|
|
485
|
+
when :ns, :ps, :fs, :as then 9 # Time caps at nanoseconds; finer is not shown
|
|
486
|
+
else 0
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
class CATimedelta
|
|
493
|
+
# @overload new(*shape, unit: :ns)
|
|
494
|
+
# Allocates a new int64 storage CArray of the given `shape` and
|
|
495
|
+
# wraps it as a CATimedelta Face with the given `unit`.
|
|
496
|
+
# @param shape [Array<Integer>] shape of the new CATimedelta.
|
|
497
|
+
# @param unit [Symbol] duration unit.
|
|
498
|
+
# @return [CATimedelta]
|
|
499
|
+
def self.new(*shape, unit: :ns)
|
|
500
|
+
raw = CArray.int64(*shape)
|
|
501
|
+
wrap(raw, unit: unit)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# @overload wrap(raw, unit: :ns)
|
|
505
|
+
# Zero-copy Face wrap of an existing int64 CArray as a duration.
|
|
506
|
+
# @param raw [CArray] int64 storage.
|
|
507
|
+
# @param unit [CATime::Resolution, Symbol, String] tick resolution.
|
|
508
|
+
# @return [CATimedelta]
|
|
509
|
+
def self.wrap(raw, unit: :ns)
|
|
510
|
+
__wrap__(raw, unit)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# A single duration: an integer tick count plus the resolution those ticks
|
|
514
|
+
# are counted in. What a scalar read of a {CATimedelta} returns.
|
|
515
|
+
class Element
|
|
516
|
+
include Comparable
|
|
517
|
+
# @!visibility private
|
|
518
|
+
UNIT_TO_SECONDS = CATime::Element::UNIT_TO_SECONDS
|
|
519
|
+
|
|
520
|
+
# storage + value/unit accessors + initialize are C-implemented in
|
|
521
|
+
# ext/ca_obj_timedelta.c (= TypedData_Make_Struct fast path).
|
|
522
|
+
# Methods below are Ruby-side; they call value/unit which are C accessors.
|
|
523
|
+
|
|
524
|
+
# @return [Rational] the exact duration in seconds. A :Y / :M duration
|
|
525
|
+
# has no exact second count (a month / year is calendar-variable), so it
|
|
526
|
+
# raises rather than silently handing back a 30.5-day approximation --
|
|
527
|
+
# use {#to_seconds_approx} when an estimate is acceptable.
|
|
528
|
+
# @raise [ArgumentError] for a :Y / :M unit.
|
|
529
|
+
def to_seconds
|
|
530
|
+
unless CATimeUnitAlgebra::FIXED.key?(unit.base)
|
|
531
|
+
raise ArgumentError,
|
|
532
|
+
"CATimedelta #{unit} has no exact seconds (a :#{unit.base} is " \
|
|
533
|
+
"calendar-variable); use to_seconds_approx for an estimate"
|
|
534
|
+
end
|
|
535
|
+
Rational(value) * unit.tick_ratio # exact seconds
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# @return [Float] an approximate duration in seconds, using nominal
|
|
539
|
+
# lengths (a month = 30.5 days, a year = 365.25 days). The name makes
|
|
540
|
+
# the approximation explicit at the call site.
|
|
541
|
+
def to_seconds_approx
|
|
542
|
+
value * unit.count * (UNIT_TO_SECONDS[unit.base] || 1)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# @return [String] the value followed by its unit.
|
|
546
|
+
def to_s
|
|
547
|
+
unit.count == 1 ? "#{value}#{unit.base}" : "#{value} @ #{unit}"
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# @return [String]
|
|
551
|
+
def inspect
|
|
552
|
+
"#<CATimedelta::Element #{to_s}>"
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Three-way compare, reconciling a different unit by the fixed ratio. A
|
|
556
|
+
# cross-group pair (a :M vs a :s duration) has no order -- there is no fixed
|
|
557
|
+
# ratio -- so it returns nil (Comparable then makes `<` raise and `==`
|
|
558
|
+
# false). No explicit `==` is defined; Comparable derives it.
|
|
559
|
+
def <=>(other)
|
|
560
|
+
return nil unless other.is_a?(Element)
|
|
561
|
+
return nil unless CATimeUnitAlgebra.same_group?(unit, other.unit)
|
|
562
|
+
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
563
|
+
_scale_in(u) <=> other._scale_in(u)
|
|
564
|
+
rescue ArgumentError
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def eql?(other)
|
|
569
|
+
other.is_a?(Element) && other.unit == unit && other.value == value
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# @return [Integer] a hash consistent with #eql? (unit-strict).
|
|
573
|
+
def hash
|
|
574
|
+
[value, unit].hash
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# @overload +(other)
|
|
578
|
+
# Returns the sum of two durations, promoting to the finer unit. A
|
|
579
|
+
# cross-group operand (a :M + a :s duration) has no fixed relation and
|
|
580
|
+
# raises.
|
|
581
|
+
# @param other [CATimedelta::Element]
|
|
582
|
+
# @return [CATimedelta::Element]
|
|
583
|
+
def +(other) = _combine(other, 1)
|
|
584
|
+
|
|
585
|
+
# @overload -(other)
|
|
586
|
+
# Returns the difference of two durations (finer unit; cross-group raises).
|
|
587
|
+
# @param other [CATimedelta::Element]
|
|
588
|
+
# @return [CATimedelta::Element]
|
|
589
|
+
def -(other) = _combine(other, -1)
|
|
590
|
+
|
|
591
|
+
# @overload *(n)
|
|
592
|
+
# Returns this duration scaled by an Integer.
|
|
593
|
+
# @param n [Integer]
|
|
594
|
+
# @return [CATimedelta::Element]
|
|
595
|
+
def *(n)
|
|
596
|
+
raise TypeError, "CATimedelta::Element * #{n.class} is not allowed (Integer only)" unless n.is_a?(Integer)
|
|
597
|
+
Element.new(value * n, unit)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# @overload /(other)
|
|
601
|
+
# By an Integer -> a scaled {CATimedelta::Element}; by another
|
|
602
|
+
# {CATimedelta::Element} -> their dimensionless ratio as a `Rational`
|
|
603
|
+
# (both brought to the finer unit; cross-group raises).
|
|
604
|
+
# @param other [Integer, CATimedelta::Element]
|
|
605
|
+
# @return [CATimedelta::Element, Rational]
|
|
606
|
+
def /(other)
|
|
607
|
+
case other
|
|
608
|
+
when Integer then Element.new(value / other, unit)
|
|
609
|
+
when Element
|
|
610
|
+
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
611
|
+
Rational(_scale_in(u), other.send(:_scale_in, u))
|
|
612
|
+
else
|
|
613
|
+
raise TypeError, "CATimedelta::Element / #{other.class} is not allowed"
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
protected
|
|
618
|
+
|
|
619
|
+
# This duration's value expressed in `to` unit (exact scale; raises on a
|
|
620
|
+
# cross-group or non-whole conversion).
|
|
621
|
+
def _scale_in(to)
|
|
622
|
+
return value if to == unit
|
|
623
|
+
CATimeUnitAlgebra.convert_scale!(CA_INT64([value]), unit, to)[0]
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
private
|
|
627
|
+
|
|
628
|
+
def _combine(other, sign)
|
|
629
|
+
unless other.is_a?(Element)
|
|
630
|
+
raise TypeError,
|
|
631
|
+
"CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
|
|
632
|
+
end
|
|
633
|
+
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
634
|
+
Element.new(_scale_in(u) + sign * other.send(:_scale_in, u), u)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# ============================================================================
|
|
640
|
+
# Ruby surface operators (CATime)
|
|
641
|
+
# ============================================================================
|
|
642
|
+
|
|
643
|
+
class CATime
|
|
644
|
+
# NOTE: for internal ops accessing the storage CArray (= int64), use `parent`
|
|
645
|
+
# directly. `value` is a user-facing canonical method (= mask-strip CARefer
|
|
646
|
+
# view) with a lift hook deployed in the C layer; do not shadow.
|
|
647
|
+
|
|
648
|
+
# Sanctioned external accessor that pairs with the existing `unit` reader.
|
|
649
|
+
# An interop bridge (carray-xarray, carray-pycall consumers, etc.) can
|
|
650
|
+
# read out the int64 count + unit without reaching into `parent` (= an
|
|
651
|
+
# internal-contract accessor). The reference epoch is the implicit Unix
|
|
652
|
+
# epoch (1970-01-01 UTC) baked into the convention at the top of this
|
|
653
|
+
# file and into the C layer's Time.at-based decoding; it is not a
|
|
654
|
+
# per-instance state and therefore is not exposed as an accessor.
|
|
655
|
+
#
|
|
656
|
+
# ca.ticks # => CArray (int64), tick counts since 1970-01-01 UTC
|
|
657
|
+
# ca.unit # => Symbol (:Y :M :W :D :h :m :s :ms :us :ns ...)
|
|
658
|
+
# @overload ticks
|
|
659
|
+
# Returns the underlying int64 CArray of tick counts since the
|
|
660
|
+
# Unix epoch (1970-01-01 UTC) — the k-th tick of this array's
|
|
661
|
+
# resolution (see §4 of docs/CATime.md).
|
|
662
|
+
# @return [CArray]
|
|
663
|
+
def ticks
|
|
664
|
+
parent
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# @overload to_unit(unit)
|
|
668
|
+
# Returns the same instants re-expressed on a finer grid: a new
|
|
669
|
+
# {CATime} whose storage is `self`'s ticks widened into `unit`.
|
|
670
|
+
# Accepted only when `self`'s tick is a whole multiple of `unit`'s, so
|
|
671
|
+
# every element lands exactly on the new grid and no instant moves
|
|
672
|
+
# (`:D` -> `:h`, `"1 hour"` -> `"10 minutes"`, `:Y` -> `:M`). A coarser
|
|
673
|
+
# or partially-overlapping target raises rather than rounding silently;
|
|
674
|
+
# use {#floor} / {#ceil} / {#round} to move to a coarser grid explicitly.
|
|
675
|
+
# @param unit [Resolution, Symbol, String] target resolution.
|
|
676
|
+
# @return [CATime]
|
|
677
|
+
# @raise [ArgumentError] when `self`'s tick is not a whole multiple of
|
|
678
|
+
# `unit`'s (including any calendar / fixed-length pair, where no fixed
|
|
679
|
+
# ratio exists).
|
|
680
|
+
# @raise [RangeError] when the widened ticks overflow int64.
|
|
681
|
+
def to_unit(unit)
|
|
682
|
+
to = CATime::Resolution.parse(unit)
|
|
683
|
+
CATimeUnitAlgebra.widen(parent,
|
|
684
|
+
CATimeUnitAlgebra.multiple_factor(self.unit, to)).time(unit: to)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# @overload +(other)
|
|
688
|
+
# Returns `self + other` for a {CATimedelta}: the time is the
|
|
689
|
+
# anchor, so the result keeps `self`'s unit and the duration is converted
|
|
690
|
+
# into it (a duration finer than `self`'s unit is truncated to it; a
|
|
691
|
+
# cross-group calendar duration raises). Adding two datetimes is
|
|
692
|
+
# ill-defined.
|
|
693
|
+
# @param other [CATimedelta]
|
|
694
|
+
# @return [CATime]
|
|
695
|
+
# @raise [TypeError] on a non-timedelta operand.
|
|
696
|
+
def +(other)
|
|
697
|
+
case other
|
|
698
|
+
when CATimedelta
|
|
699
|
+
(parent + CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
|
|
700
|
+
when CATime
|
|
701
|
+
raise TypeError, "CATime + CATime is ill-defined"
|
|
702
|
+
else
|
|
703
|
+
raise TypeError, "CATime + #{other.class} is not allowed (use CATimedelta)"
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# @overload -(other)
|
|
708
|
+
# Subtracting a {CATimedelta} yields a {CATime} at `self`'s unit (the
|
|
709
|
+
# duration is converted into it, truncated when finer); subtracting
|
|
710
|
+
# another {CATime} yields a {CATimedelta} at the finer of the two
|
|
711
|
+
# units (cross-group falls to the fixed unit).
|
|
712
|
+
# @param other [CATimedelta, CATime]
|
|
713
|
+
# @return [CATime, CATimedelta]
|
|
714
|
+
# @raise [TypeError] on an unsupported operand.
|
|
715
|
+
def -(other)
|
|
716
|
+
case other
|
|
717
|
+
when CATimedelta
|
|
718
|
+
(parent - CATimeUnitAlgebra.convert_scale_trunc(other.parent, other.unit, unit)).time(unit: unit)
|
|
719
|
+
when CATime
|
|
720
|
+
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
721
|
+
a = CATimeUnitAlgebra.convert_instant!(parent, unit, u)
|
|
722
|
+
b = CATimeUnitAlgebra.convert_instant!(other.parent, other.unit, u)
|
|
723
|
+
(a - b).timedelta(unit: u)
|
|
724
|
+
else
|
|
725
|
+
raise TypeError, "CATime - #{other.class} is not allowed"
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Element-wise comparison (< <= > >= <=>) rides the core comparison Face
|
|
730
|
+
# gate (ca_face_reconcile_comparison in ext/ca_obj_face.c): the inherited
|
|
731
|
+
# CArray operators descend to storage and reconcile a CATime RHS in a
|
|
732
|
+
# different unit via to_comparable, returning a boolean CArray. <=> is the
|
|
733
|
+
# base CArray#<=> composed from the (gated) > and <.
|
|
734
|
+
|
|
735
|
+
# Element decode convention via the C-layer macro
|
|
736
|
+
# `CA_FACE_STORAGE_TO_SCALAR_IF_FACE`: no Ruby `[]` override is needed, a
|
|
737
|
+
# scalar auto-decodes on all scalar-return paths such as `ca[i,j,k]` /
|
|
738
|
+
# `ca[k]`. storage_to_scalar itself lives in C (ext/ca_obj_datetime.c)
|
|
739
|
+
# so the hot path stays off Ruby method dispatch. Its write counterpart
|
|
740
|
+
# is scalar_to_storage (below, surface -> storage on store).
|
|
741
|
+
|
|
742
|
+
# @!group Reductions
|
|
743
|
+
|
|
744
|
+
# min / max are order-structure reductions: they ride the core reduce
|
|
745
|
+
# Face gate (ORDERABLE storage descent + output re-lift, see
|
|
746
|
+
# ext/mkkernel.rb face_gate: :relift), so no Ruby override is needed --
|
|
747
|
+
# the inherited CArray#min / #max handle the Element / CATime return
|
|
748
|
+
# shapes directly. minmax is the paired form; it has no core gate, so
|
|
749
|
+
# re-lift both extremes here. They are actual elements, so they keep the
|
|
750
|
+
# storage unit (no rounding, unlike mean).
|
|
751
|
+
|
|
752
|
+
# @overload minmax(*axes, **opts)
|
|
753
|
+
# Returns the earliest and latest time as a `[min, max]` pair (§8).
|
|
754
|
+
# @return [Array(Element, Element), Array(CATime, CATime)]
|
|
755
|
+
def minmax(*args, **opts)
|
|
756
|
+
lo, hi = parent.minmax(*args, **opts)
|
|
757
|
+
[_lift_extremum(lo), _lift_extremum(hi)]
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def _lift_extremum(r)
|
|
761
|
+
case r
|
|
762
|
+
when Integer then Element.new(r, unit)
|
|
763
|
+
when CArray then r.time(unit: unit)
|
|
764
|
+
else r
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
private :_lift_extremum
|
|
768
|
+
|
|
769
|
+
# The centroid / spread / order reductions report on the array's own grid:
|
|
770
|
+
# they run on the storage ticks and round the result back to the nearest
|
|
771
|
+
# tick of `unit`. A caller who needs a finer answer declares the grid
|
|
772
|
+
# first (`t.to_unit(:ms).mean`) -- the same rule linear_fetch follows.
|
|
773
|
+
# Rounding is to nearest, not toward zero: a truncating round would flip
|
|
774
|
+
# the direction of the error at the epoch.
|
|
775
|
+
|
|
776
|
+
# @overload mean(axis: nil, **opts)
|
|
777
|
+
# Returns the centroid time on `self`'s unit (§8), rounded to the
|
|
778
|
+
# nearest tick.
|
|
779
|
+
# @return [Element, CATime]
|
|
780
|
+
def mean(*args, **opts)
|
|
781
|
+
_reduce_dt(:mean, :time, args, opts)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# @overload median(axis: nil, **opts)
|
|
785
|
+
# Returns the median time on `self`'s unit (§8). An odd-count full
|
|
786
|
+
# reduction is an actual element (exact); the even-count / per-axis
|
|
787
|
+
# cases interpolate and round to the nearest tick.
|
|
788
|
+
# @return [Element, CATime]
|
|
789
|
+
def median(*args, **opts)
|
|
790
|
+
_reduce_dt(:median, :time, args, opts)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# @overload percentile(*p, axis: nil, **opts)
|
|
794
|
+
# Returns the percentile instants on `self`'s unit, in the shapes the
|
|
795
|
+
# plain {CArray#percentile} uses: one `p` reduces to a single value
|
|
796
|
+
# (an {Element}, or a {CATime} with `axis:`), two or more `p` give an
|
|
797
|
+
# Array of those.
|
|
798
|
+
# @return [Element, CATime, Array<Element>, Array<CATime>]
|
|
799
|
+
def percentile(*args, **opts)
|
|
800
|
+
_reduce_dt(:percentile, :time, args, opts)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# @overload quantile(axis: nil, **opts)
|
|
804
|
+
# Returns the five quartile instants `[p0, p25, p50, p75, p100]` on
|
|
805
|
+
# `self`'s unit (shorthand for `percentile(0, 25, 50, 75, 100)`).
|
|
806
|
+
# @return [Array<Element>, Array<CATime>]
|
|
807
|
+
def quantile(*args, **opts)
|
|
808
|
+
_reduce_dt(:quantile, :time, args, opts)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# @overload stddev(axis: nil, **opts)
|
|
812
|
+
# Returns the spread of the instants as a {CATimedelta} (a duration) on
|
|
813
|
+
# `self`'s unit. A spread is not a lattice point, so the rounding costs
|
|
814
|
+
# more here than for a centroid: on a coarse unit use
|
|
815
|
+
# `t.to_unit(:h).stddev` when the precision matters (§8).
|
|
816
|
+
# @return [CATimedelta::Element, CATimedelta]
|
|
817
|
+
def stddev(*args, **opts)
|
|
818
|
+
_reduce_dt(:stddev, :timedelta, args, opts)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# @overload stddevp(axis: nil, **opts)
|
|
822
|
+
# Returns the population spread as a {CATimedelta}, in the same shapes
|
|
823
|
+
# as {#stddev}.
|
|
824
|
+
# @return [CATimedelta::Element, CATimedelta]
|
|
825
|
+
def stddevp(*args, **opts)
|
|
826
|
+
_reduce_dt(:stddevp, :timedelta, args, opts)
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# @overload sum(*)
|
|
830
|
+
# Not supported; use {#mean} for a centroid.
|
|
831
|
+
# @raise [TypeError] always.
|
|
832
|
+
def sum(*)
|
|
833
|
+
raise TypeError, "CATime#sum is ill-defined; use mean for centroid"
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# @overload variance(*)
|
|
837
|
+
# Not supported: the variance of instants has squared-time units, which
|
|
838
|
+
# no type represents (ill-defined, like {#sum}). Use {#stddev} for the
|
|
839
|
+
# spread as a duration.
|
|
840
|
+
# @raise [TypeError] always.
|
|
841
|
+
def variance(*); raise TypeError, "CATime#variance is ill-defined (squared-time units); use stddev"; end
|
|
842
|
+
|
|
843
|
+
# @overload variancep(*)
|
|
844
|
+
# Not supported, for the same reason as {#variance}. Use {#stddevp}.
|
|
845
|
+
# @raise [TypeError] always.
|
|
846
|
+
def variancep(*); raise TypeError, "CATime#variancep is ill-defined (squared-time units); use stddevp"; end
|
|
847
|
+
# @!endgroup
|
|
848
|
+
|
|
849
|
+
# sort / partition ride the core sort Face gate (ORDERABLE storage
|
|
850
|
+
# descent + output re-lift): the inherited CArray#sort / #partition
|
|
851
|
+
# return a sorted CATime with the same unit, so no override is
|
|
852
|
+
# needed.
|
|
853
|
+
|
|
854
|
+
# @overload to_comparable(operand)
|
|
855
|
+
# Brings `operand` into `self`'s unit space for a direct storage
|
|
856
|
+
# comparison (comparison operators / search family / linear_section).
|
|
857
|
+
# `self` is the reference Face -- always one of our classes -- so it
|
|
858
|
+
# class-dispatches the operand rather than requiring every operand type
|
|
859
|
+
# to know every Face (which a core class like Time could not).
|
|
860
|
+
# CATime is ORDERABLE but not COMPARABLE (an operand may carry a
|
|
861
|
+
# different unit or shape), so the gate routes the operand through here.
|
|
862
|
+
#
|
|
863
|
+
# Accepted operands: another {CATime} (unit-rescaled to self),
|
|
864
|
+
# a {Element} (lifted to a length-1 CATime), a Ruby `Time`, and a
|
|
865
|
+
# Ruby `DateTime` (both absolute instants converted to self's unit,
|
|
866
|
+
# Unix epoch, UTC). A `String` is out of scope (parsing is a separate,
|
|
867
|
+
# opposite-direction mechanism). A bare Integer / other type raises;
|
|
868
|
+
# descend to `ca.parent` to compare the hidden storage directly.
|
|
869
|
+
#
|
|
870
|
+
# The rescale is an INSTANT conversion (convert_instant!), lossless: a
|
|
871
|
+
# coarser->finer unit always converts; a finer->coarser unit converts only
|
|
872
|
+
# when every value lands exactly on the coarser grid, else raises. Unlike
|
|
873
|
+
# a duration, a cross-group time cast IS possible via civil-date
|
|
874
|
+
# algebra (a :M value has a well-defined instant): :M/:Y widen exactly to
|
|
875
|
+
# :D and finer, and a fixed operand coarsens to :M/:Y only when it sits on
|
|
876
|
+
# the calendar boundary (:W is the one exception -- month/year starts are
|
|
877
|
+
# not week-aligned, so :Y/:M <-> :W raises).
|
|
878
|
+
# @param operand [CATime, CATime::Element, Time, DateTime]
|
|
879
|
+
# @return [CATime] in self's unit.
|
|
880
|
+
# @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
|
|
881
|
+
def to_comparable (operand)
|
|
882
|
+
case operand
|
|
883
|
+
when CATime
|
|
884
|
+
return operand if operand.unit == unit
|
|
885
|
+
CATimeUnitAlgebra.convert_instant!(operand.parent, operand.unit, unit)
|
|
886
|
+
.time(unit: unit)
|
|
887
|
+
when CATime::Element
|
|
888
|
+
lifted = CATime.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
889
|
+
to_comparable(lifted)
|
|
890
|
+
when Time
|
|
891
|
+
# Reuse the single-literal builder; it yields a length-1 CATime
|
|
892
|
+
# in the requested (= self's) resolution.
|
|
893
|
+
CArray.time(operand, unit: unit)
|
|
894
|
+
when defined?(DateTime) && DateTime
|
|
895
|
+
to_comparable(operand.to_time.utc)
|
|
896
|
+
else
|
|
897
|
+
raise TypeError,
|
|
898
|
+
"CATime cannot reconcile #{operand.class} " \
|
|
899
|
+
"(use ca.parent to compare the raw int64 storage directly)"
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# @overload scalar_to_storage(surface)
|
|
904
|
+
# Write-direction counterpart of storage_to_scalar (the store hook fired
|
|
905
|
+
# from rb_ca_obj2ptr): brings a surface value object into this Face's
|
|
906
|
+
# int64 storage (count in self's unit since the Unix epoch) so a scalar
|
|
907
|
+
# store round-trips with a fetch. A {Element} / `Time` / `DateTime` is
|
|
908
|
+
# reconciled to self's unit via {#to_comparable} (same lossless
|
|
909
|
+
# discipline: a cross-group unit or a non-exact finer->coarser cast
|
|
910
|
+
# raises). A bare Integer (the documented `.parent` raw-storage escape)
|
|
911
|
+
# and a String (parsing is a separate, opposite-direction mechanism)
|
|
912
|
+
# pass through unchanged to the storage cast.
|
|
913
|
+
# @param surface [Element, Time, DateTime, Integer, String]
|
|
914
|
+
# @return [Integer, Object] the storage-domain value, or `surface`
|
|
915
|
+
# unchanged for a pass-through type.
|
|
916
|
+
# @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
|
|
917
|
+
def scalar_to_storage (surface)
|
|
918
|
+
case surface
|
|
919
|
+
when Integer, String
|
|
920
|
+
surface
|
|
921
|
+
else
|
|
922
|
+
to_comparable(surface).parent[0]
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# NOTE: search (bsearch / search / bsearch_addr) and linear_section need NO
|
|
927
|
+
# per-method override. The C kernel gate descends an ORDERABLE Face
|
|
928
|
+
# reference to storage and, for a Face query, calls to_comparable itself to
|
|
929
|
+
# reconcile it -- so declaring ORDERABLE (init flag) and defining
|
|
930
|
+
# to_comparable (above) is the whole contract. linear_fetch is the one
|
|
931
|
+
# exception: it returns a value rather than a position, so it re-lifts
|
|
932
|
+
# (below).
|
|
933
|
+
|
|
934
|
+
# @overload linear_fetch(addr, axis: nil)
|
|
935
|
+
# Returns the time at the fractional position `addr` on this array's
|
|
936
|
+
# grid, interpolating between the two bracketing instants. The inverse
|
|
937
|
+
# of `linear_section`, and the reason for the override: `linear_fetch`
|
|
938
|
+
# returns a *value*, so the result is a {CATime} again, whereas
|
|
939
|
+
# `linear_section` returns a position and stays a plain index.
|
|
940
|
+
#
|
|
941
|
+
# The result keeps `self`'s unit -- the array's grid is the output grid,
|
|
942
|
+
# so an instant that lands between two ticks is rounded to the nearest
|
|
943
|
+
# one. Widen the grid first when the interpolation needs finer
|
|
944
|
+
# resolution: `t.to_unit(:ms).linear_fetch(addr)` interpolates on the
|
|
945
|
+
# millisecond grid. (Unlike {#mean} / {#median}, which collapse the axis
|
|
946
|
+
# and therefore have no output grid to preserve, so they refine the
|
|
947
|
+
# resolution instead.)
|
|
948
|
+
#
|
|
949
|
+
# An out-of-range `addr` yields UNDEF rather than the NaN that a plain
|
|
950
|
+
# float axis returns -- int64 storage has no NaN to carry a sentinel. A
|
|
951
|
+
# masked `addr` cell stays UNDEF (the kernel's own rule: an undetermined
|
|
952
|
+
# query gets an undetermined answer).
|
|
953
|
+
# @param addr [Float, CArray] fractional position(s) into `self`.
|
|
954
|
+
# @param axis [Integer, nil]
|
|
955
|
+
# @return [Element, CATime] an {Element} for a scalar `addr`, `nil` when
|
|
956
|
+
# that single answer is undetermined.
|
|
957
|
+
def linear_fetch (addr, **opts)
|
|
958
|
+
r = parent.float64.linear_fetch(addr, **opts)
|
|
959
|
+
case r
|
|
960
|
+
when CArray
|
|
961
|
+
# mask_invalid before the int64 cast: the cast turns a NaN into 0, which
|
|
962
|
+
# would read as the epoch instead of "no answer".
|
|
963
|
+
r.mask_invalid.round.int64.time(unit: unit)
|
|
964
|
+
when Numeric
|
|
965
|
+
# Scalar query. Out of range -> nil, joining the nil the kernel already
|
|
966
|
+
# returns for a masked scalar query: one "no answer" on this path.
|
|
967
|
+
r.to_f.nan? ? nil : Element.new(r.round, unit)
|
|
968
|
+
else
|
|
969
|
+
r # nil (masked scalar query)
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# ====================================================================
|
|
974
|
+
# pandas .dt.* style field accessors
|
|
975
|
+
# ====================================================================
|
|
976
|
+
#
|
|
977
|
+
# Face-strip semantic: each accessor returns a CArray of Integer / bool
|
|
978
|
+
# (= year / month / etc. are not time, they descend to the storage
|
|
979
|
+
# type), computed directly from the storage int64.
|
|
980
|
+
# ====================================================================
|
|
981
|
+
|
|
982
|
+
# @overload to_time
|
|
983
|
+
# Converts every element to a Ruby `Time` (UTC), returned as an object
|
|
984
|
+
# CArray. Array-level counterpart to {Element#to_time}.
|
|
985
|
+
# @return [CArray]
|
|
986
|
+
def to_time
|
|
987
|
+
require 'time'
|
|
988
|
+
if CATimeUnitAlgebra::FIXED.key?(unit.base)
|
|
989
|
+
f = unit.tick_ratio # exact seconds / tick (Rational)
|
|
990
|
+
parent.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
|
|
991
|
+
else # calendar: exact granule midnight
|
|
992
|
+
(_field_days * 86400).convert(:object) {|v| Time.at(v, in: 'UTC')}
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# @overload to_date
|
|
997
|
+
# Converts every element to a Ruby `Date` (UTC), returned as an object
|
|
998
|
+
# CArray. A sub-day unit floors to its day. Array-level counterpart to
|
|
999
|
+
# {Element#to_date}.
|
|
1000
|
+
# @return [CArray]
|
|
1001
|
+
def to_date
|
|
1002
|
+
require 'date'
|
|
1003
|
+
# 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
|
|
1004
|
+
_field_days.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
# @overload to_datetime
|
|
1008
|
+
# Converts every element to a Ruby `DateTime` (UTC offset 0), returned
|
|
1009
|
+
# as an object CArray.
|
|
1010
|
+
# @return [CArray]
|
|
1011
|
+
def to_datetime
|
|
1012
|
+
require 'date'
|
|
1013
|
+
to_time.convert(:object) {|t| t.to_datetime}
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# @!group Field accessors
|
|
1017
|
+
# Each accessor returns an integer CArray with the requested calendar /
|
|
1018
|
+
# clock field extracted from every element (UTC), computed by vectorized
|
|
1019
|
+
# civil-date algebra directly on the int64 storage -- no per-cell Time.
|
|
1020
|
+
# Exact for every unit, including :M / :Y (where the old Time.at path drifted
|
|
1021
|
+
# by using 30.5-day / 365.25-day approximations). Fields finer than the
|
|
1022
|
+
# storage unit collapse to their zero point, and the input mask propagates.
|
|
1023
|
+
# @return [CArray]
|
|
1024
|
+
def year
|
|
1025
|
+
case unit.base
|
|
1026
|
+
when :Y then parent * unit.count + 1970
|
|
1027
|
+
when :M then self.class.send(:_floordiv_i, parent * unit.count + 1970 * 12, 12)
|
|
1028
|
+
else self.class.send(:_civil_from_days, _field_days)[0]
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
# Calendar month, 1..12. Every cell is 1 for `:Y` storage, which does not
|
|
1033
|
+
# resolve months.
|
|
1034
|
+
# @return [CArray] integer.
|
|
1035
|
+
def month
|
|
1036
|
+
case unit.base
|
|
1037
|
+
when :Y then parent * 0 + 1
|
|
1038
|
+
when :M
|
|
1039
|
+
mo = parent * unit.count + 1970 * 12
|
|
1040
|
+
mo - self.class.send(:_floordiv_i, mo, 12) * 12 + 1
|
|
1041
|
+
else self.class.send(:_civil_from_days, _field_days)[1]
|
|
1042
|
+
end
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
# Day of the month, 1..31. Every cell is 1 for `:Y` / `:M` storage, which
|
|
1046
|
+
# does not resolve days.
|
|
1047
|
+
# @return [CArray] integer.
|
|
1048
|
+
def day
|
|
1049
|
+
case unit.base
|
|
1050
|
+
when :Y, :M then parent * 0 + 1
|
|
1051
|
+
else self.class.send(:_civil_from_days, _field_days)[2]
|
|
1052
|
+
end
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
# Hour of the day, 0..23; 0 when the storage unit is coarser than an hour.
|
|
1056
|
+
# @return [CArray] integer.
|
|
1057
|
+
def hour; _clock_field(:h); end
|
|
1058
|
+
# Minute of the hour, 0..59; 0 when the storage unit is coarser than a minute.
|
|
1059
|
+
# @return [CArray] integer.
|
|
1060
|
+
def minute; _clock_field(:m); end
|
|
1061
|
+
# Second of the minute, 0..59; 0 when the storage unit is coarser than a second.
|
|
1062
|
+
# @return [CArray] integer.
|
|
1063
|
+
def second; _clock_field(:s); end
|
|
1064
|
+
|
|
1065
|
+
# Day of the week, Sunday = 0 .. Saturday = 6.
|
|
1066
|
+
# @return [CArray] integer.
|
|
1067
|
+
def weekday
|
|
1068
|
+
# 1970-01-01 is a Thursday (wday 4); Sun=0..Sat=6.
|
|
1069
|
+
n = _field_days + 4
|
|
1070
|
+
n - self.class.send(:_floordiv_i, n, 7) * 7 # floor-mod 7
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Day of the year, 1..366.
|
|
1074
|
+
# @return [CArray] integer.
|
|
1075
|
+
def yday
|
|
1076
|
+
d = _field_days
|
|
1077
|
+
y = self.class.send(:_civil_from_days, d)[0]
|
|
1078
|
+
ones = CArray.int64(*shape) { 1 }
|
|
1079
|
+
d - self.class.send(:_days_from_civil, y, ones, ones) + 1
|
|
1080
|
+
end
|
|
1081
|
+
# @!endgroup
|
|
1082
|
+
|
|
1083
|
+
# @overload jd
|
|
1084
|
+
# Returns the Julian Day Number for each element.
|
|
1085
|
+
# @return [CArray]
|
|
1086
|
+
def jd
|
|
1087
|
+
require 'date'
|
|
1088
|
+
to_time.convert(:int) {|t| t.to_date.jd}
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# @overload ajd
|
|
1092
|
+
# Returns the Astronomical Julian Day (float, offset by half a
|
|
1093
|
+
# day) for each element.
|
|
1094
|
+
# @return [CArray]
|
|
1095
|
+
def ajd
|
|
1096
|
+
require 'date'
|
|
1097
|
+
to_time.convert(:double) {|t| t.to_datetime.ajd.to_f}
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# @overload is_leap
|
|
1101
|
+
# Returns a boolean CArray flagging elements that fall in a
|
|
1102
|
+
# leap year (UTC).
|
|
1103
|
+
# @return [CArray]
|
|
1104
|
+
def is_leap
|
|
1105
|
+
y = year
|
|
1106
|
+
((y % 4).eq(0) & (y % 100).ne(0)) | (y % 400).eq(0)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
# @overload strftime(fmt)
|
|
1110
|
+
# Returns a {CAString} whose elements are the per-cell
|
|
1111
|
+
# `Time#strftime(fmt)` result (UTC). The input mask propagates.
|
|
1112
|
+
# @param fmt [String] strftime format string.
|
|
1113
|
+
# @return [CAString]
|
|
1114
|
+
def strftime(fmt)
|
|
1115
|
+
CAString.wrap(to_time.convert(:object) {|t| t.strftime(fmt)})
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
private
|
|
1119
|
+
|
|
1120
|
+
# Reduce to `kind` (:time -> CATime, :timedelta -> CATimedelta) on the
|
|
1121
|
+
# array's own grid. The reduction runs on the storage ticks -- for a
|
|
1122
|
+
# calendar unit that means the month / year ordinal, so a :M centroid is
|
|
1123
|
+
# the centroid of month numbers (a caller wanting the day-space answer
|
|
1124
|
+
# writes to_unit(:D) first) -- and the result is rounded back to a tick.
|
|
1125
|
+
def _reduce_dt(op, kind, args, opts)
|
|
1126
|
+
_lift_reduced(parent.send(op, *args, **opts), kind)
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
# Round-to-nearest onto the storage grid, then wear the Face again. An
|
|
1130
|
+
# Array arrives from the multi-p percentile / quantile shapes; UNDEF and
|
|
1131
|
+
# nil (empty or all-masked) pass through untouched.
|
|
1132
|
+
def _lift_reduced(r, kind)
|
|
1133
|
+
case r
|
|
1134
|
+
when Numeric
|
|
1135
|
+
v = r.round
|
|
1136
|
+
kind == :timedelta ? CATimedelta::Element.new(v, unit) : Element.new(v, unit)
|
|
1137
|
+
when CArray
|
|
1138
|
+
# A cell with no answer arrives masked and the mask survives the cast.
|
|
1139
|
+
# NaN does not: it has no int64 image, so a `fill_value: Float::NAN`
|
|
1140
|
+
# cell would cast to garbage bits. mask_invalid turns it into UNDEF,
|
|
1141
|
+
# which is what "no answer" means here.
|
|
1142
|
+
iv = r.round.mask_invalid.int64
|
|
1143
|
+
kind == :timedelta ? iv.timedelta(unit: unit) : iv.time(unit: unit)
|
|
1144
|
+
when Array
|
|
1145
|
+
r.map {|x| _lift_reduced(x, kind)}
|
|
1146
|
+
else
|
|
1147
|
+
r # UNDEF / nil passthrough (empty or all-masked reduction)
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# ============================================================================
|
|
1154
|
+
# Ruby surface operators (CATimedelta)
|
|
1155
|
+
# ============================================================================
|
|
1156
|
+
|
|
1157
|
+
class CATimedelta
|
|
1158
|
+
# NOTE: internal ops use `parent` directly. `value` is a C-layer canonical
|
|
1159
|
+
# method (mask-strip) with a lift hook deployed in the C layer; do not
|
|
1160
|
+
# shadow.
|
|
1161
|
+
|
|
1162
|
+
# @overload ticks
|
|
1163
|
+
# Returns the underlying int64 CArray of tick counts — the duration
|
|
1164
|
+
# measured in this array's resolution (see {CATime#ticks}).
|
|
1165
|
+
# @return [CArray]
|
|
1166
|
+
def ticks
|
|
1167
|
+
parent
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# @overload to_unit(unit)
|
|
1171
|
+
# Returns the same durations re-expressed on a finer grid: a new
|
|
1172
|
+
# {CATimedelta} whose storage is `self`'s ticks widened into `unit`.
|
|
1173
|
+
# Accepted only when `self`'s tick is a whole multiple of `unit`'s
|
|
1174
|
+
# (`:D` -> `:h`, `"1 hour"` -> `"10 minutes"`), so no duration changes.
|
|
1175
|
+
# A coarser target raises rather than truncating silently; a calendar /
|
|
1176
|
+
# fixed-length pair raises because a month has no fixed length.
|
|
1177
|
+
# @param unit [Resolution, Symbol, String] target resolution.
|
|
1178
|
+
# @return [CATimedelta]
|
|
1179
|
+
# @raise [ArgumentError] when `self`'s tick is not a whole multiple of
|
|
1180
|
+
# `unit`'s.
|
|
1181
|
+
# @raise [RangeError] when the widened ticks overflow int64.
|
|
1182
|
+
def to_unit(unit)
|
|
1183
|
+
to = CATime::Resolution.parse(unit)
|
|
1184
|
+
CATimeUnitAlgebra.widen(parent,
|
|
1185
|
+
CATimeUnitAlgebra.multiple_factor(self.unit, to)).timedelta(unit: to)
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
# @overload +(other)
|
|
1189
|
+
# Returns `self + other`. A {CATimedelta} operand yields a
|
|
1190
|
+
# {CATimedelta}; a {CATime} operand delegates to
|
|
1191
|
+
# {CATime#+} (commutative).
|
|
1192
|
+
# @param other [CATimedelta, CATime]
|
|
1193
|
+
# @return [CATimedelta, CATime]
|
|
1194
|
+
# @raise [TypeError] on incompatible operands.
|
|
1195
|
+
def +(other)
|
|
1196
|
+
case other
|
|
1197
|
+
when CATimedelta
|
|
1198
|
+
u = _finer_duration(other.unit)
|
|
1199
|
+
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1200
|
+
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1201
|
+
(a + b).timedelta(unit: u)
|
|
1202
|
+
when CATime
|
|
1203
|
+
other + self # commutative -> time's unit
|
|
1204
|
+
else
|
|
1205
|
+
raise TypeError, "CATimedelta + #{other.class} is not allowed"
|
|
1206
|
+
end
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
# @overload -(other)
|
|
1210
|
+
# Returns the difference of two {CATimedelta} at the finer of the two
|
|
1211
|
+
# units (a cross-group pair raises).
|
|
1212
|
+
# @param other [CATimedelta]
|
|
1213
|
+
# @return [CATimedelta]
|
|
1214
|
+
# @raise [TypeError] on a non-timedelta operand.
|
|
1215
|
+
def -(other)
|
|
1216
|
+
case other
|
|
1217
|
+
when CATimedelta
|
|
1218
|
+
u = _finer_duration(other.unit)
|
|
1219
|
+
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1220
|
+
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1221
|
+
(a - b).timedelta(unit: u)
|
|
1222
|
+
else
|
|
1223
|
+
raise TypeError, "CATimedelta - #{other.class} is not allowed"
|
|
1224
|
+
end
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
# @overload abs
|
|
1228
|
+
# Returns the magnitude of each duration as a {CATimedelta} on the same
|
|
1229
|
+
# unit.
|
|
1230
|
+
# @return [CATimedelta]
|
|
1231
|
+
def abs
|
|
1232
|
+
parent.abs.timedelta(unit: unit)
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
# @overload -@
|
|
1236
|
+
# Returns each duration with its sign reversed, on the same unit.
|
|
1237
|
+
# @return [CATimedelta]
|
|
1238
|
+
def -@
|
|
1239
|
+
(-parent).timedelta(unit: unit)
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
# @overload *(other)
|
|
1243
|
+
# Returns `self` scaled by an Integer.
|
|
1244
|
+
# @param other [Integer]
|
|
1245
|
+
# @return [CATimedelta]
|
|
1246
|
+
# @raise [TypeError] when `other` is not an Integer.
|
|
1247
|
+
def *(other)
|
|
1248
|
+
case other
|
|
1249
|
+
when Integer then (parent * other).timedelta(unit: unit)
|
|
1250
|
+
else raise TypeError, "CATimedelta * #{other.class} is not allowed (Integer only)"
|
|
1251
|
+
end
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
# @overload /(other)
|
|
1255
|
+
# Returns element-wise division: by Integer yields a
|
|
1256
|
+
# {CATimedelta}, by another {CATimedelta} with matching `unit`
|
|
1257
|
+
# yields a dimensionless CArray.
|
|
1258
|
+
# @param other [Integer, CATimedelta]
|
|
1259
|
+
# @return [CATimedelta, CArray]
|
|
1260
|
+
# @raise [TypeError] on incompatible operands.
|
|
1261
|
+
def /(other)
|
|
1262
|
+
case other
|
|
1263
|
+
when Integer then (parent / other).timedelta(unit: unit)
|
|
1264
|
+
when CATimedelta
|
|
1265
|
+
u = _finer_duration(other.unit)
|
|
1266
|
+
CATimeUnitAlgebra.convert_scale!(parent, unit, u) /
|
|
1267
|
+
CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1268
|
+
else raise TypeError, "CATimedelta / #{other.class} is not allowed"
|
|
1269
|
+
end
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
# Element-wise comparison (< <= > >= <=>) rides the core comparison Face
|
|
1273
|
+
# gate (ca_face_reconcile_comparison): the inherited CArray operators
|
|
1274
|
+
# descend to storage and reconcile a CATimedelta RHS in a different unit
|
|
1275
|
+
# via to_comparable, returning a boolean CArray.
|
|
1276
|
+
|
|
1277
|
+
# @!group Reductions
|
|
1278
|
+
|
|
1279
|
+
# @overload sum(*args, **opts)
|
|
1280
|
+
# Returns the sum of durations as a {Element} for full reduction
|
|
1281
|
+
# or as a {CATimedelta} view for per-axis reduction.
|
|
1282
|
+
# @return [Element, CATimedelta]
|
|
1283
|
+
def sum(*args, **opts)
|
|
1284
|
+
_lift_reduced(parent.sum(*args, **opts))
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
# @overload mean(axis: nil, **opts)
|
|
1288
|
+
# Returns the mean duration rounded to the nearest unit count.
|
|
1289
|
+
# @return [Element, CATimedelta]
|
|
1290
|
+
def mean(*args, **opts)
|
|
1291
|
+
_lift_reduced(parent.mean(*args, **opts))
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
# median / percentile / quantile / stddev / stddevp report on this array's
|
|
1295
|
+
# own unit, rounded to the nearest tick -- the same rule {CATime} follows.
|
|
1296
|
+
# A caller who needs a finer answer declares the grid first
|
|
1297
|
+
# (`td.to_unit(:ms).stddev`).
|
|
1298
|
+
|
|
1299
|
+
# @overload median(axis: nil, **opts)
|
|
1300
|
+
# Returns the median duration on `self`'s unit.
|
|
1301
|
+
# @return [Element, CATimedelta]
|
|
1302
|
+
def median(*args, **opts)
|
|
1303
|
+
_lift_reduced(parent.median(*args, **opts))
|
|
1304
|
+
end
|
|
1305
|
+
|
|
1306
|
+
# @overload percentile(*p, axis: nil, **opts)
|
|
1307
|
+
# Returns the percentile durations on `self`'s unit, in the shapes the
|
|
1308
|
+
# plain {CArray#percentile} uses (one `p` reduces to a single value,
|
|
1309
|
+
# two or more give an Array).
|
|
1310
|
+
# @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
|
|
1311
|
+
def percentile(*args, **opts)
|
|
1312
|
+
_lift_reduced(parent.percentile(*args, **opts))
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# @overload quantile(axis: nil, **opts)
|
|
1316
|
+
# Returns the five quartile durations `[p0, p25, p50, p75, p100]` on
|
|
1317
|
+
# `self`'s unit.
|
|
1318
|
+
# @return [Array<Element>, Array<CATimedelta>]
|
|
1319
|
+
def quantile(*args, **opts)
|
|
1320
|
+
_lift_reduced(parent.quantile(*args, **opts))
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
# @overload stddev(axis: nil, **opts)
|
|
1324
|
+
# Returns the spread of the durations on `self`'s unit. A spread is not
|
|
1325
|
+
# a lattice point, so on a coarse unit use `td.to_unit(:h).stddev` when
|
|
1326
|
+
# the precision matters.
|
|
1327
|
+
# @return [Element, CATimedelta]
|
|
1328
|
+
def stddev(*args, **opts)
|
|
1329
|
+
_lift_reduced(parent.stddev(*args, **opts))
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
# @overload stddevp(axis: nil, **opts)
|
|
1333
|
+
# Returns the population spread on `self`'s unit, in the same shapes as
|
|
1334
|
+
# {#stddev}.
|
|
1335
|
+
# @return [Element, CATimedelta]
|
|
1336
|
+
def stddevp(*args, **opts)
|
|
1337
|
+
_lift_reduced(parent.stddevp(*args, **opts))
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
# variance / variancep are the one pair in this family that cannot carry a
|
|
1341
|
+
# unit: their value is in squared ticks, and no Face represents squared
|
|
1342
|
+
# time. They are left as the inherited plain Float (unlike
|
|
1343
|
+
# {CATime#variance}, which raises -- a squared instant is meaningless,
|
|
1344
|
+
# while a squared duration is a real quantity, s^2). Read the result as
|
|
1345
|
+
# "in units of `self.unit` squared", or take {#stddev} for a typed answer.
|
|
1346
|
+
|
|
1347
|
+
def _lift_reduced(r)
|
|
1348
|
+
case r
|
|
1349
|
+
when Numeric then Element.new(r.round, unit)
|
|
1350
|
+
when CArray then r.round.mask_invalid.int64.timedelta(unit: unit)
|
|
1351
|
+
when Array then r.map {|x| _lift_reduced(x)}
|
|
1352
|
+
else r
|
|
1353
|
+
end
|
|
1354
|
+
end
|
|
1355
|
+
private :_lift_reduced
|
|
1356
|
+
|
|
1357
|
+
# min / max ride the core reduce Face gate (ORDERABLE storage descent +
|
|
1358
|
+
# output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
|
|
1359
|
+
# CArray#min / #max return the Element / CATimedelta shapes directly.
|
|
1360
|
+
# @overload variance(*)
|
|
1361
|
+
# Not supported: the variance of durations has squared-time units, which
|
|
1362
|
+
# no type represents -- the same reason {CATime#variance} refuses. Use
|
|
1363
|
+
# {#stddev} for the spread as a duration.
|
|
1364
|
+
# @raise [TypeError] always.
|
|
1365
|
+
def variance(*)
|
|
1366
|
+
raise TypeError, "CATimedelta#variance is ill-defined (squared-time units); use stddev"
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
# @overload variancep(*)
|
|
1370
|
+
# Not supported, for the same reason as {#variance}. Use {#stddevp}.
|
|
1371
|
+
# @raise [TypeError] always.
|
|
1372
|
+
def variancep(*)
|
|
1373
|
+
raise TypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
# minmax has no core gate, so re-lift both extremes here.
|
|
1377
|
+
|
|
1378
|
+
# @overload minmax(*axes, **opts)
|
|
1379
|
+
# Returns the shortest and longest duration as a `[min, max]` pair.
|
|
1380
|
+
# @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
|
|
1381
|
+
def minmax(*args, **opts)
|
|
1382
|
+
lo, hi = parent.minmax(*args, **opts)
|
|
1383
|
+
[_lift_extremum(lo), _lift_extremum(hi)]
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
def _lift_extremum(r)
|
|
1387
|
+
case r
|
|
1388
|
+
when Integer then Element.new(r, unit)
|
|
1389
|
+
when CArray then r.timedelta(unit: unit)
|
|
1390
|
+
else r
|
|
1391
|
+
end
|
|
1392
|
+
end
|
|
1393
|
+
private :_lift_extremum
|
|
1394
|
+
# @!endgroup
|
|
1395
|
+
|
|
1396
|
+
# sort / partition ride the core sort Face gate (ORDERABLE storage
|
|
1397
|
+
# descent + output re-lift); the inherited CArray#sort / #partition
|
|
1398
|
+
# return a sorted CATimedelta with the same unit.
|
|
1399
|
+
|
|
1400
|
+
# @overload to_comparable(operand)
|
|
1401
|
+
# Brings `operand` into `self`'s unit space for a direct storage
|
|
1402
|
+
# comparison (see {CATime#to_comparable} for the reference-side
|
|
1403
|
+
# contract). `self` is the reference Face and class-dispatches the
|
|
1404
|
+
# operand. Coverage is deliberately narrower than CATime: only
|
|
1405
|
+
# another {CATimedelta} (unit-rescaled to self) and a {Element} (lifted
|
|
1406
|
+
# to a length-1 CATimedelta) are accepted. `Time` / `DateTime` are
|
|
1407
|
+
# absolute instants, not durations, so they raise -- a Face owns its own
|
|
1408
|
+
# coverage. Auto-casts to self's unit when lossless (coarser->finer
|
|
1409
|
+
# always; finer->coarser only when every value is exact), otherwise
|
|
1410
|
+
# raises.
|
|
1411
|
+
# @param operand [CATimedelta, CATimedelta::Element]
|
|
1412
|
+
# @return [CATimedelta] in self's unit.
|
|
1413
|
+
# @raise [TypeError, ArgumentError] on an unreconcilable operand / unit.
|
|
1414
|
+
def to_comparable (operand)
|
|
1415
|
+
case operand
|
|
1416
|
+
when CATimedelta
|
|
1417
|
+
return operand if operand.unit == unit
|
|
1418
|
+
CATimeUnitAlgebra.convert_scale!(operand.parent, operand.unit, unit)
|
|
1419
|
+
.timedelta(unit: unit)
|
|
1420
|
+
when CATimedelta::Element
|
|
1421
|
+
lifted = CATimedelta.wrap(CA_INT64([operand.value]), unit: operand.unit)
|
|
1422
|
+
to_comparable(lifted)
|
|
1423
|
+
else
|
|
1424
|
+
raise TypeError,
|
|
1425
|
+
"CATimedelta cannot reconcile #{operand.class} " \
|
|
1426
|
+
"(use ca.parent to compare the raw int64 storage directly)"
|
|
1427
|
+
end
|
|
1428
|
+
end
|
|
10
1429
|
|
|
11
|
-
|
|
1430
|
+
# @overload scalar_to_storage(surface)
|
|
1431
|
+
# Write-direction counterpart of storage_to_scalar: brings a surface
|
|
1432
|
+
# value object into this Face's int64 storage (count in self's unit) so a
|
|
1433
|
+
# scalar store round-trips with a fetch. A {Element} is reconciled to
|
|
1434
|
+
# self's unit via {#to_comparable} (lossless discipline; a `Time` /
|
|
1435
|
+
# `DateTime` is an absolute instant, not a duration, so it raises there).
|
|
1436
|
+
# A bare Integer (the `.parent` raw-storage escape) and a String pass
|
|
1437
|
+
# through unchanged.
|
|
1438
|
+
# @param surface [Element, Integer, String]
|
|
1439
|
+
# @return [Integer, Object] the storage-domain value, or `surface`
|
|
1440
|
+
# unchanged for a pass-through type.
|
|
1441
|
+
# @raise [TypeError, ArgumentError] on an unreconcilable surface / unit.
|
|
1442
|
+
def scalar_to_storage (surface)
|
|
1443
|
+
case surface
|
|
1444
|
+
when Integer, String
|
|
1445
|
+
surface
|
|
1446
|
+
else
|
|
1447
|
+
to_comparable(surface).parent[0]
|
|
1448
|
+
end
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
# Search / linear_section need no per-method override: the C kernel gate
|
|
1452
|
+
# descends an ORDERABLE Face reference and reconciles a Face query via
|
|
1453
|
+
# to_comparable. See {CATime}.
|
|
1454
|
+
|
|
1455
|
+
# @overload linear_fetch(addr, axis: nil)
|
|
1456
|
+
# Returns the duration at the fractional position `addr` on this array's
|
|
1457
|
+
# grid, interpolating between the two bracketing durations. Keeps
|
|
1458
|
+
# `self`'s unit and rounds to it (widen the grid with {#to_unit} first
|
|
1459
|
+
# when the interpolation needs finer resolution), which matches {#mean} /
|
|
1460
|
+
# {#sum} -- a duration reduction stays on the grid too. An out-of-range
|
|
1461
|
+
# `addr` yields UNDEF; see {CATime#linear_fetch} for the full contract.
|
|
1462
|
+
# @param addr [Float, CArray] fractional position(s) into `self`.
|
|
1463
|
+
# @param axis [Integer, nil]
|
|
1464
|
+
# @return [Element, CATimedelta] an {Element} for a scalar `addr`, `nil`
|
|
1465
|
+
# when that single answer is undetermined.
|
|
1466
|
+
def linear_fetch (addr, **opts)
|
|
1467
|
+
r = parent.float64.linear_fetch(addr, **opts)
|
|
1468
|
+
case r
|
|
1469
|
+
when CArray then r.mask_invalid.round.int64.timedelta(unit: unit)
|
|
1470
|
+
when Numeric then r.to_f.nan? ? nil : Element.new(r.round, unit)
|
|
1471
|
+
else r
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
private
|
|
1476
|
+
|
|
1477
|
+
# The finer of `self`'s unit and `ou` for a duration + duration / ratio. A
|
|
1478
|
+
# cross-group pair (a month vs a second) has no common duration grid, so it
|
|
1479
|
+
# raises rather than mixing calendar and fixed durations.
|
|
1480
|
+
def _finer_duration(ou)
|
|
1481
|
+
unless CATimeUnitAlgebra.same_group?(unit, ou)
|
|
1482
|
+
raise ArgumentError,
|
|
1483
|
+
"cannot combine a #{unit} duration with a #{ou} duration " \
|
|
1484
|
+
"across the calendar/fixed boundary"
|
|
1485
|
+
end
|
|
1486
|
+
CATimeUnitAlgebra.finer(unit, ou)
|
|
1487
|
+
end
|
|
1488
|
+
end
|
|
12
1489
|
|
|
13
1490
|
class CArray
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1491
|
+
# Time is stored as an int64 tick index on a grid whose resolution is
|
|
1492
|
+
# `unit` (= a {CATime::Resolution}, tick = count * base). The value
|
|
1493
|
+
# is the k-th tick since the Unix epoch (1970-01-01 UTC): e.g. unit
|
|
1494
|
+
# `"10 minutes"` value 3 = 1970-01-01T00:30:00Z. All parsing is UTC (an
|
|
1495
|
+
# explicit offset is honoured; otherwise UTC), DateTime-independent
|
|
1496
|
+
# (Date._parse + civil kernel).
|
|
1497
|
+
|
|
1498
|
+
# Exact Rational seconds since the Unix epoch for a start literal (Time /
|
|
1499
|
+
# DateTime / Integer unix-seconds / String). UTC default.
|
|
1500
|
+
def self._epoch_seconds_exact(spec, format = nil)
|
|
1501
|
+
require 'date'
|
|
1502
|
+
require 'time'
|
|
1503
|
+
case spec
|
|
1504
|
+
when Time then spec.to_r
|
|
1505
|
+
when Integer then Rational(spec)
|
|
1506
|
+
when String
|
|
1507
|
+
h = format ? Date._strptime(spec, format) : Date._parse(spec)
|
|
1508
|
+
unless h && h[:year] && h[:mon] && h[:mday]
|
|
1509
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}"
|
|
1510
|
+
end
|
|
1511
|
+
days = CATime.send(:_days_from_civil,
|
|
1512
|
+
CA_INT64([h[:year]]), CA_INT64([h[:mon]]),
|
|
1513
|
+
CA_INT64([h[:mday]]))[0]
|
|
1514
|
+
sec = Rational(days * 86400 + (h[:hour] || 0) * 3600 +
|
|
1515
|
+
(h[:min] || 0) * 60 + (h[:sec] || 0))
|
|
1516
|
+
sec += h[:sec_fraction] if h[:sec_fraction]
|
|
1517
|
+
sec -= h[:offset] if h[:offset] # east-of-UTC offset -> UTC
|
|
1518
|
+
sec
|
|
1519
|
+
else
|
|
1520
|
+
if defined?(DateTime) && spec.is_a?(DateTime)
|
|
1521
|
+
spec.to_time.to_r
|
|
1522
|
+
else
|
|
1523
|
+
raise ArgumentError, "cannot parse time #{spec.class}"
|
|
1524
|
+
end
|
|
1525
|
+
end
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
# [year, month] (UTC) of a start literal, for a calendar-resolution grid.
|
|
1529
|
+
def self._epoch_year_month(spec, format = nil)
|
|
1530
|
+
require 'date'
|
|
1531
|
+
require 'time'
|
|
1532
|
+
case spec
|
|
1533
|
+
when Time then t = spec.utc; [t.year, t.month]
|
|
1534
|
+
when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
|
|
1535
|
+
when String
|
|
1536
|
+
h = format ? Date._strptime(spec, format) : Date._parse(spec)
|
|
1537
|
+
unless h && h[:year]
|
|
1538
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}"
|
|
1539
|
+
end
|
|
1540
|
+
[h[:year], h[:mon] || 1]
|
|
18
1541
|
else
|
|
19
|
-
|
|
1542
|
+
if defined?(DateTime) && spec.is_a?(DateTime)
|
|
1543
|
+
t = spec.to_time.utc; [t.year, t.month]
|
|
1544
|
+
else
|
|
1545
|
+
raise ArgumentError, "cannot parse time #{spec.class}"
|
|
1546
|
+
end
|
|
20
1547
|
end
|
|
21
1548
|
end
|
|
22
1549
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1550
|
+
# Tick index of `spec`'s instant on the `res` grid (floor toward the past).
|
|
1551
|
+
def self._epoch_tick_index(spec, res, format = nil)
|
|
1552
|
+
if CATimeUnitAlgebra::FIXED.key?(res.base)
|
|
1553
|
+
(_epoch_seconds_exact(spec, format) / res.tick_ratio).floor
|
|
26
1554
|
else
|
|
27
|
-
|
|
1555
|
+
y, m = _epoch_year_month(spec, format)
|
|
1556
|
+
months = (y - 1970) * 12 + (m - 1)
|
|
1557
|
+
(Rational(months) / res.tick_ratio).floor
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# @overload time_range(start, last, unit:, step: nil, format: nil)
|
|
1562
|
+
# Returns a {CATime} from `start` to `last` inclusive on the `unit`
|
|
1563
|
+
# grid, spaced `step` apart. `unit` is the resolution the result is
|
|
1564
|
+
# stored on and `step` is the spacing, so an hourly grid sampled once a
|
|
1565
|
+
# day is `unit: :h, step: "1 day"`. With no `step` the spacing is one
|
|
1566
|
+
# `unit` tick (consecutive ticks). Off-grid endpoints floor to their
|
|
1567
|
+
# bucket head (toward the past); the phase is anchored at `start`, and
|
|
1568
|
+
# `last` is a bound rather than a member -- the series stops at the last
|
|
1569
|
+
# step at or before it.
|
|
1570
|
+
# @param start [Time, String, Integer, DateTime] first instant.
|
|
1571
|
+
# @param last [Time, String, Integer, DateTime] last instant (inclusive).
|
|
1572
|
+
# @param unit [Resolution, Symbol, String] grid resolution (tick).
|
|
1573
|
+
# @param step [Resolution, Symbol, String, nil] spacing between elements
|
|
1574
|
+
# (default: one `unit` tick). Must be a whole multiple of `unit`.
|
|
1575
|
+
# @param format [String, nil] optional strptime format for String inputs.
|
|
1576
|
+
# @return [CATime]
|
|
1577
|
+
# @raise [ArgumentError] when `step` is not a whole multiple of `unit`
|
|
1578
|
+
# (including a calendar `step` on a fixed-length `unit`, e.g. a month
|
|
1579
|
+
# step on an hour grid -- a month is not a fixed number of hours).
|
|
1580
|
+
def self.time_range(start, last, unit:, step: nil, format: nil)
|
|
1581
|
+
res = CATime::Resolution.parse(unit)
|
|
1582
|
+
stride = step.nil? ? 1 :
|
|
1583
|
+
CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
|
|
1584
|
+
s = _epoch_tick_index(start, res, format)
|
|
1585
|
+
e = _epoch_tick_index(last, res, format)
|
|
1586
|
+
n = e < s ? 0 : (e - s) / stride + 1
|
|
1587
|
+
CArray.int64(n) {|i| s + i * stride }.time(unit: res)
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
# @overload time_series(start, count:, unit:, step: nil, format: nil)
|
|
1591
|
+
# Returns a {CATime} of `count` instants starting at `start` on the
|
|
1592
|
+
# `unit` grid, spaced `step` apart. `unit` is the resolution the result
|
|
1593
|
+
# is stored on and `step` is the spacing, so an hourly grid sampled once
|
|
1594
|
+
# a day is `unit: :h, step: "1 day"`. With no `step` the spacing is one
|
|
1595
|
+
# `unit` tick (consecutive ticks, as before).
|
|
1596
|
+
# @param start [Time, String, Integer, DateTime] first instant.
|
|
1597
|
+
# @param count [Integer] number of elements.
|
|
1598
|
+
# @param unit [Resolution, Symbol, String] grid resolution (tick).
|
|
1599
|
+
# @param step [Resolution, Symbol, String, nil] spacing between elements
|
|
1600
|
+
# (default: one `unit` tick). Must be a whole multiple of `unit`.
|
|
1601
|
+
# @param format [String, nil] optional strptime format for String inputs.
|
|
1602
|
+
# @return [CATime]
|
|
1603
|
+
# @raise [ArgumentError] when `step` is not a whole multiple of `unit`
|
|
1604
|
+
# (including a calendar `step` on a fixed-length `unit`, e.g. a month
|
|
1605
|
+
# step on an hour grid -- a month is not a fixed number of hours).
|
|
1606
|
+
def self.time_series(start, count:, unit:, step: nil, format: nil)
|
|
1607
|
+
res = CATime::Resolution.parse(unit)
|
|
1608
|
+
stride = step.nil? ? 1 :
|
|
1609
|
+
CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
|
|
1610
|
+
s = _epoch_tick_index(start, res, format)
|
|
1611
|
+
CArray.int64(count) {|i| s + i * stride }.time(unit: res)
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
# @overload time(x, unit: :s, format: nil, on_error: :raise)
|
|
1615
|
+
# Builds a {CATime} on the `unit` grid from time value(s). `x`
|
|
1616
|
+
# is either a single literal (Time / ISO 8601 String / Unix-seconds
|
|
1617
|
+
# Integer / DateTime) — giving a 1-element result — or a CArray of such
|
|
1618
|
+
# literals — giving a same-shape result parsed per cell. Parsing is UTC
|
|
1619
|
+
# and DateTime-independent.
|
|
1620
|
+
#
|
|
1621
|
+
# A value that cannot be parsed raises by default (`on_error: :raise`);
|
|
1622
|
+
# pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
|
|
1623
|
+
# `nil` input cell is a *missing* value (not a parse failure) and always
|
|
1624
|
+
# becomes UNDEF, regardless of `on_error`.
|
|
1625
|
+
# @param x [Time, String, Integer, DateTime, Array, CArray] a literal, a
|
|
1626
|
+
# Ruby Array of literals, or a CArray of literals.
|
|
1627
|
+
# @param unit [Resolution, Symbol, String] target grid resolution.
|
|
1628
|
+
# @param format [String, nil] optional strptime format for String input.
|
|
1629
|
+
# @param on_error [:raise, :mask] parse-failure policy (default `:raise`).
|
|
1630
|
+
# @return [CATime] shape `[1]` for a literal, else `x`'s shape.
|
|
1631
|
+
# @raise [ArgumentError] on an unparseable value when `on_error: :raise`.
|
|
1632
|
+
#
|
|
1633
|
+
# @example
|
|
1634
|
+
# CArray.time("2024-06-15", unit: :D) # 1-element
|
|
1635
|
+
# CArray.time(%w[2024-01-01 2024-02-01], unit: :D) # Ruby Array
|
|
1636
|
+
# CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)
|
|
1637
|
+
def self.time(x, unit: :s, format: nil, on_error: :raise)
|
|
1638
|
+
res = CATime::Resolution.parse(unit)
|
|
1639
|
+
unless %i[raise mask].include?(on_error)
|
|
1640
|
+
raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
|
|
1641
|
+
end
|
|
1642
|
+
x = CA_OBJECT(x) if x.is_a?(Array) # Ruby Array of literals -> object CArray
|
|
1643
|
+
unless x.is_a?(CArray)
|
|
1644
|
+
return _time_cell(x, res, format, on_error)
|
|
1645
|
+
end
|
|
1646
|
+
raw = CArray.int64(*x.shape)
|
|
1647
|
+
x.each_index do |*idx|
|
|
1648
|
+
s = x[*idx]
|
|
1649
|
+
if s == UNDEF || s.nil?
|
|
1650
|
+
raw[*idx] = UNDEF # missing input -> missing output (no phantom epoch)
|
|
1651
|
+
next
|
|
1652
|
+
end
|
|
1653
|
+
begin
|
|
1654
|
+
raw[*idx] = _epoch_tick_index(s, res, format)
|
|
1655
|
+
rescue ArgumentError, TypeError
|
|
1656
|
+
raise if on_error == :raise
|
|
1657
|
+
raw[*idx] = UNDEF # opt-in parse-mask
|
|
1658
|
+
end
|
|
1659
|
+
end
|
|
1660
|
+
raw.time(unit: res)
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
# Single-literal build for {.time}: a 1-element CATime, honouring
|
|
1664
|
+
# the on_error policy (raise, or a masked cell).
|
|
1665
|
+
def self._time_cell(literal, res, format, on_error)
|
|
1666
|
+
raw = CArray.int64(1)
|
|
1667
|
+
begin
|
|
1668
|
+
raw[0] = _epoch_tick_index(literal, res, format)
|
|
1669
|
+
rescue ArgumentError, TypeError
|
|
1670
|
+
raise if on_error == :raise
|
|
1671
|
+
raw[0] = UNDEF
|
|
28
1672
|
end
|
|
1673
|
+
raw.time(unit: res)
|
|
29
1674
|
end
|
|
30
1675
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1676
|
+
# @overload time(unit: :ns, origin: nil)
|
|
1677
|
+
# Returns `self` as a {CATime} on the `unit` grid. With `origin`
|
|
1678
|
+
# nil, `self`'s int64 values are taken as tick indices already anchored
|
|
1679
|
+
# to the Unix epoch (zero-copy Face wrap). With `origin` given, `self`'s
|
|
1680
|
+
# indices are relative to `origin` and are rebased to the epoch (a new
|
|
1681
|
+
# int64 array is built; the origin is not stored).
|
|
1682
|
+
# @param unit [Resolution, Symbol, String] grid resolution.
|
|
1683
|
+
# @param origin [Time, String, Integer, DateTime, nil] base instant that
|
|
1684
|
+
# `self`'s indices are counted from (default: the Unix epoch).
|
|
1685
|
+
# @return [CATime]
|
|
1686
|
+
# @note An `int64` receiver is wrapped zero-copy. A narrower integer type
|
|
1687
|
+
# is widened to `int64` first (a copy); a `Float` / non-integer type
|
|
1688
|
+
# raises (cast it explicitly if the truncation is intended).
|
|
1689
|
+
def time(unit: :ns, origin: nil)
|
|
1690
|
+
res = CATime::Resolution.parse(unit)
|
|
1691
|
+
src = _time_int64_storage
|
|
1692
|
+
if origin.nil?
|
|
1693
|
+
CATime.wrap(src, unit: res)
|
|
34
1694
|
else
|
|
35
|
-
|
|
36
|
-
|
|
1695
|
+
o = CArray._epoch_tick_index(origin, res)
|
|
1696
|
+
(src + o).time(unit: res)
|
|
1697
|
+
end
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
# @overload timedelta(unit: :ns)
|
|
1701
|
+
# Returns `self` re-wrapped as a zero-copy {CATimedelta} view
|
|
1702
|
+
# with the given `unit`.
|
|
1703
|
+
# @param unit [Resolution, Symbol, String] duration resolution.
|
|
1704
|
+
# @return [CATimedelta]
|
|
1705
|
+
# @note An `int64` receiver is wrapped zero-copy; a narrower integer type
|
|
1706
|
+
# is widened to `int64` first (a copy); a `Float` / non-integer raises.
|
|
1707
|
+
def timedelta(unit: :ns)
|
|
1708
|
+
CATimedelta.wrap(_time_int64_storage, unit: unit)
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
# Coerce the receiver to the int64 storage a time / timedelta Face needs:
|
|
1712
|
+
# int64 passes through (zero-copy), a narrower integer type widens losslessly
|
|
1713
|
+
# to int64 (a copy), and any non-integer (Float, boolean, object, ...) raises.
|
|
1714
|
+
def _time_int64_storage
|
|
1715
|
+
return self if data_type == CA_INT64
|
|
1716
|
+
unless integer?
|
|
1717
|
+
raise TypeError,
|
|
1718
|
+
"CATime / CATimedelta storage must be int64; a #{data_type} " \
|
|
1719
|
+
"array cannot be wrapped (cast it explicitly, e.g. " \
|
|
1720
|
+
"arr.int64.time(...), if a lossy conversion is intended)"
|
|
1721
|
+
end
|
|
1722
|
+
int64
|
|
1723
|
+
end
|
|
1724
|
+
private :_time_int64_storage
|
|
1725
|
+
end
|
|
1726
|
+
|
|
1727
|
+
# ============================================================================
|
|
1728
|
+
# timestep system (P1/P2 integer path)
|
|
1729
|
+
#
|
|
1730
|
+
# Projects an absolute time onto an integer "step index" -- the k-th
|
|
1731
|
+
# fixed-width bucket of `step` counted from `origin` -- and the inverse /
|
|
1732
|
+
# rounding / on-grid operators built on it. All storage-domain int64 and
|
|
1733
|
+
# vectorized; the crown jewel is that bucketing, matching, and positional
|
|
1734
|
+
# addressing across differently-scaled series all become integer arithmetic.
|
|
1735
|
+
#
|
|
1736
|
+
# Integer path covers: fixed-length step on fixed-length storage, and Y/M
|
|
1737
|
+
# step on Y/M storage (:M storage already encodes a linear month ordinal, so
|
|
1738
|
+
# it floor-divides identically). The calendar path (Y/M step on sub-day
|
|
1739
|
+
# storage, via civil-date algebra) is P3 and currently raises.
|
|
1740
|
+
# See devel/PROPOSAL_DATETIME64_STEP_SYSTEM.md.
|
|
1741
|
+
# ============================================================================
|
|
1742
|
+
|
|
1743
|
+
class CATime
|
|
1744
|
+
# A tick resolution: `count` ticks of a `base` unit. The human surface is
|
|
1745
|
+
# a String ("3 hours" / "1 month"); a bare Symbol (:h) is the count-1
|
|
1746
|
+
# shorthand over the base-unit vocabulary. It names both a storage tick
|
|
1747
|
+
# (= the grid a CATime is stored on) and a coarser bucket for the
|
|
1748
|
+
# floor / timesteps family. Value object: frozen, value-equal, hashable.
|
|
1749
|
+
class Resolution
|
|
1750
|
+
attr_reader :count, :base
|
|
1751
|
+
|
|
1752
|
+
# Long / abbreviated unit words -> base unit symbol. Calendar words
|
|
1753
|
+
# (ayear etc.) are intentionally dropped (standard calendar only).
|
|
1754
|
+
WORDS = {
|
|
1755
|
+
"year" => :Y, "years" => :Y, "yr" => :Y, "yrs" => :Y,
|
|
1756
|
+
"month" => :M, "months" => :M, "mon" => :M, "mons" => :M,
|
|
1757
|
+
"week" => :W, "weeks" => :W,
|
|
1758
|
+
"day" => :D, "days" => :D,
|
|
1759
|
+
"hour" => :h, "hours" => :h, "hr" => :h, "hrs" => :h,
|
|
1760
|
+
"minute" => :m, "minutes" => :m, "min" => :m, "mins" => :m,
|
|
1761
|
+
"second" => :s, "seconds" => :s, "sec" => :s, "secs" => :s,
|
|
1762
|
+
"millisecond" => :ms, "milliseconds" => :ms, "msec" => :ms, "msecs" => :ms,
|
|
1763
|
+
"microsecond" => :us, "microseconds" => :us, "usec" => :us, "usecs" => :us,
|
|
1764
|
+
"nanosecond" => :ns, "nanoseconds" => :ns,
|
|
1765
|
+
"picosecond" => :ps, "picoseconds" => :ps,
|
|
1766
|
+
"femtosecond" => :fs, "femtoseconds" => :fs,
|
|
1767
|
+
"attosecond" => :as, "attoseconds" => :as,
|
|
1768
|
+
}.freeze
|
|
1769
|
+
|
|
1770
|
+
# Single-letter / short symbols accepted as the count-1 shorthand. These
|
|
1771
|
+
# are the base-unit letters (case-sensitive: :m minute vs :M month).
|
|
1772
|
+
SYMBOLS = %i[Y M W D h m s ms us ns ps fs as].freeze
|
|
1773
|
+
|
|
1774
|
+
# Coerces `spec` into a {Resolution}: a Resolution passes through, a base
|
|
1775
|
+
# unit Symbol becomes count 1, and a String such as `"10 minutes"` is parsed.
|
|
1776
|
+
# @param spec [Resolution, Symbol, String]
|
|
1777
|
+
# @return [Resolution]
|
|
1778
|
+
# @raise [ArgumentError] when `spec` names no known unit.
|
|
1779
|
+
def self.parse(spec)
|
|
1780
|
+
case spec
|
|
1781
|
+
when Resolution then spec
|
|
1782
|
+
when Symbol
|
|
1783
|
+
unless SYMBOLS.include?(spec)
|
|
1784
|
+
raise ArgumentError, "invalid unit #{spec.inspect} " \
|
|
1785
|
+
"(one of #{SYMBOLS.map(&:inspect).join(', ')})"
|
|
1786
|
+
end
|
|
1787
|
+
new(1, spec)
|
|
1788
|
+
when String
|
|
1789
|
+
# Strict grammar: "<unit>" (count 1) or "<n> <unit>" (whitespace
|
|
1790
|
+
# required -- compact "3h" is rejected).
|
|
1791
|
+
m = spec.strip.match(/\A(?:(\d+)\s+)?([A-Za-z]+)\z/)
|
|
1792
|
+
unless m && WORDS.key?(m[2].downcase)
|
|
1793
|
+
raise ArgumentError, "invalid unit spec #{spec.inspect} " \
|
|
1794
|
+
"(use e.g. \"3 hours\" / \"1 month\", or a unit Symbol)"
|
|
1795
|
+
end
|
|
1796
|
+
new(m[1] ? Integer(m[1]) : 1, WORDS[m[2].downcase])
|
|
1797
|
+
else
|
|
1798
|
+
raise ArgumentError, "unit spec must be a String / Symbol / Resolution " \
|
|
1799
|
+
"(got #{spec.class})"
|
|
1800
|
+
end
|
|
1801
|
+
end
|
|
1802
|
+
|
|
1803
|
+
def initialize(count, base)
|
|
1804
|
+
raise ArgumentError, "unit count must be >= 1 (got #{count})" if count < 1
|
|
1805
|
+
# calendar (Y/M) bases are month-ordinal linear, so only integer
|
|
1806
|
+
# multiples are well-defined; a fractional multiple has no meaning.
|
|
1807
|
+
# (count is already Integer here; the guard documents the rule.)
|
|
1808
|
+
@count = count
|
|
1809
|
+
@base = base
|
|
1810
|
+
freeze
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
# seconds- (fixed base) or months- (calendar base) per tick (Rational).
|
|
1814
|
+
def tick_ratio
|
|
1815
|
+
count * CATimeUnitAlgebra.base_ratio(base)
|
|
1816
|
+
end
|
|
1817
|
+
|
|
1818
|
+
# @return [Boolean] whether `other` is the same count and base unit.
|
|
1819
|
+
def ==(other) = other.is_a?(Resolution) && other.count == count && other.base == base
|
|
1820
|
+
alias eql? ==
|
|
1821
|
+
# @return [Integer] a hash consistent with #==.
|
|
1822
|
+
def hash = [count, base].hash
|
|
1823
|
+
# @return [String] e.g. `"h"` for a count of 1, `"10 minutes"` otherwise.
|
|
1824
|
+
def to_s = count == 1 ? base.to_s : "#{count} #{base}"
|
|
1825
|
+
# @return [String]
|
|
1826
|
+
def inspect = "#<CATime::Resolution #{count} #{base}>"
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
# Base units that are day-or-finer (a period head is representable exactly;
|
|
1830
|
+
# the civil path targets these for a calendar bucket).
|
|
1831
|
+
SU_LE_DAY = %i[D h m s ms us ns ps fs as].freeze
|
|
1832
|
+
|
|
1833
|
+
# @overload timesteps(unit: self.unit, origin: nil)
|
|
1834
|
+
# Returns the integer timestep of every element: the k-th `unit`-wide
|
|
1835
|
+
# bucket counted from `origin` (floor toward the past, so pre-`origin`
|
|
1836
|
+
# elements get a negative index -- a normal value, not masked). The
|
|
1837
|
+
# result is an int64 CArray; the input mask propagates. With no `unit`
|
|
1838
|
+
# the bucket is the storage resolution itself, so the result is a copy of
|
|
1839
|
+
# the raw tick indices since the epoch (the same values as
|
|
1840
|
+
# {#ticks}, but a fresh array rather than the live storage).
|
|
1841
|
+
# @param unit [String, Symbol, Resolution] bucket resolution
|
|
1842
|
+
# (default: this array's own storage resolution).
|
|
1843
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] grid
|
|
1844
|
+
# phase (default: the Unix epoch, or ISO Monday for a week bucket).
|
|
1845
|
+
# @return [CArray] int64 timesteps.
|
|
1846
|
+
# @raise [ArgumentError] on a sub-resolution / unrepresentable (unit,
|
|
1847
|
+
# storage-resolution) pair or a lossy origin.
|
|
1848
|
+
def timesteps(unit: self.unit, origin: nil)
|
|
1849
|
+
g = _step_grid(unit, origin)
|
|
1850
|
+
return _civil(:index, g[1], origin) if g[0] == :civil
|
|
1851
|
+
su, mul, step_ticks, o = g
|
|
1852
|
+
_guard_overflow(su, mul, o)
|
|
1853
|
+
d = _num_ticks(mul) - o
|
|
1854
|
+
q = d / step_ticks
|
|
1855
|
+
q - (d - q * step_ticks).lt(0) # floor-div correction (%-independent)
|
|
37
1856
|
end
|
|
38
1857
|
|
|
39
|
-
|
|
40
|
-
|
|
1858
|
+
# @overload floor(unit:, origin: nil)
|
|
1859
|
+
# Returns each element floored to its bucket head (toward the past), as a
|
|
1860
|
+
# {CATime} in the same storage resolution.
|
|
1861
|
+
# @param unit [String, Symbol, Resolution] bucket resolution.
|
|
1862
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
|
|
1863
|
+
# @return [CATime]
|
|
1864
|
+
def floor(unit:, origin: nil)
|
|
1865
|
+
g = _step_grid(unit, origin)
|
|
1866
|
+
return _civil(:floor, g[1], origin) if g[0] == :civil
|
|
1867
|
+
su, mul, step_ticks, o = g
|
|
1868
|
+
_guard_overflow(su, mul, o, step_ticks, :floor)
|
|
1869
|
+
d = _num_ticks(mul) - o
|
|
1870
|
+
q = d / step_ticks
|
|
1871
|
+
q = q - (d - q * step_ticks).lt(0)
|
|
1872
|
+
_head_time(o + q * step_ticks, mul, su)
|
|
41
1873
|
end
|
|
42
1874
|
|
|
43
|
-
|
|
44
|
-
|
|
1875
|
+
# @overload ceil(unit:, origin: nil)
|
|
1876
|
+
# Returns each element raised to its bucket head at or after it (an
|
|
1877
|
+
# element already on a boundary maps to itself), as a {CATime}.
|
|
1878
|
+
# @return [CATime]
|
|
1879
|
+
def ceil(unit:, origin: nil)
|
|
1880
|
+
g = _step_grid(unit, origin)
|
|
1881
|
+
return _civil(:ceil, g[1], origin) if g[0] == :civil
|
|
1882
|
+
su, mul, step_ticks, o = g
|
|
1883
|
+
_guard_overflow(su, mul, o, step_ticks, :ceil)
|
|
1884
|
+
n = _num_ticks(mul)
|
|
1885
|
+
d = n - o
|
|
1886
|
+
q = d / step_ticks
|
|
1887
|
+
q = q - (d - q * step_ticks).lt(0)
|
|
1888
|
+
fl = o + q * step_ticks
|
|
1889
|
+
_head_time(fl + step_ticks * n.ne(fl), mul, su)
|
|
45
1890
|
end
|
|
46
1891
|
|
|
47
|
-
|
|
48
|
-
|
|
1892
|
+
# @overload round(unit:, origin: nil)
|
|
1893
|
+
# Returns each element rounded to its nearest bucket head (ties toward
|
|
1894
|
+
# the future, matching `snap` :round), as a {CATime}. Exact for
|
|
1895
|
+
# odd `step_ticks` (no half-tick loss). For a calendar bucket the nearest
|
|
1896
|
+
# head is by absolute tick distance (month lengths vary), ties toward the
|
|
1897
|
+
# future.
|
|
1898
|
+
# @return [CATime]
|
|
1899
|
+
def round(unit:, origin: nil)
|
|
1900
|
+
g = _step_grid(unit, origin)
|
|
1901
|
+
return _civil(:round, g[1], origin) if g[0] == :civil
|
|
1902
|
+
su, mul, step_ticks, o = g
|
|
1903
|
+
_guard_overflow(su, mul, o, step_ticks, :round)
|
|
1904
|
+
d = _num_ticks(mul) - o
|
|
1905
|
+
q = d / step_ticks
|
|
1906
|
+
q = q - (d - q * step_ticks).lt(0) # floor bucket
|
|
1907
|
+
r = d - q * step_ticks # 0 <= r < step_ticks
|
|
1908
|
+
q = q + r.ge((step_ticks + 1) / 2) # ties -> future; no 2*d / 2*r
|
|
1909
|
+
_head_time(o + q * step_ticks, mul, su)
|
|
49
1910
|
end
|
|
50
1911
|
|
|
51
|
-
|
|
52
|
-
|
|
1912
|
+
# @overload is_righttime(unit:, origin: nil)
|
|
1913
|
+
# Returns a boolean CArray flagging elements that land exactly on a bucket
|
|
1914
|
+
# head. Use as an assertion before matching to catch off-grid series (a
|
|
1915
|
+
# timesteps match is "same bucket", not "same instant").
|
|
1916
|
+
# @param unit [String, Symbol, Resolution] bucket resolution.
|
|
1917
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
|
|
1918
|
+
# @return [CArray] boolean.
|
|
1919
|
+
def is_righttime(unit:, origin: nil)
|
|
1920
|
+
g = _step_grid(unit, origin)
|
|
1921
|
+
return _civil(:on, g[1], origin) if g[0] == :civil
|
|
1922
|
+
su, mul, step_ticks, o = g
|
|
1923
|
+
_guard_overflow(su, mul, o)
|
|
1924
|
+
d = _num_ticks(mul) - o
|
|
1925
|
+
q = d / step_ticks
|
|
1926
|
+
(d - q * step_ticks).eq(0)
|
|
53
1927
|
end
|
|
54
1928
|
|
|
55
|
-
|
|
56
|
-
|
|
1929
|
+
# @overload from_timesteps(k, unit:, origin: nil)
|
|
1930
|
+
# Inverse of {#timesteps}: returns the bucket-head time for timestep
|
|
1931
|
+
# `k`, stored on the `unit` grid. Use to relabel a `group_by(timesteps)`
|
|
1932
|
+
# result, generate a regular grid, or as a timesteps round-trip oracle.
|
|
1933
|
+
# A scalar `k` returns a {Element}; a CArray `k` returns a {CATime}.
|
|
1934
|
+
# @param k [Integer, CArray] timestep / timesteps.
|
|
1935
|
+
# @param unit [String, Symbol, Resolution] grid resolution of the result.
|
|
1936
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] grid phase.
|
|
1937
|
+
# @return [Element, CATime]
|
|
1938
|
+
def self.from_timesteps(k, unit:, origin: nil)
|
|
1939
|
+
res = Resolution.parse(unit)
|
|
1940
|
+
kk = k.is_a?(CArray) ? k.int64 : CArray.int64(1) { Integer(k) }
|
|
1941
|
+
_mul, step_ticks, o = _resolve_grid(res, res, origin) # step_ticks = 1 (same res)
|
|
1942
|
+
raw = o + kk * step_ticks
|
|
1943
|
+
k.is_a?(CArray) ? raw.time(unit: res) : Element.new(raw[0], res)
|
|
57
1944
|
end
|
|
58
1945
|
|
|
59
|
-
|
|
60
|
-
|
|
1946
|
+
private
|
|
1947
|
+
|
|
1948
|
+
# Resolve the grid for an instance: [storage_res, mul, step_ticks,
|
|
1949
|
+
# origin_ticks] for the integer path, or [:civil, bucket_res] for the
|
|
1950
|
+
# calendar path. Bucket arithmetic runs in the *numerator* grid -- the
|
|
1951
|
+
# finer of the storage tick and the bucket tick -- so `mul` (storage ticks
|
|
1952
|
+
# per numerator tick) is 1 for a bucket at or coarser than the storage
|
|
1953
|
+
# tick, and the widening factor for a finer bucket. `origin_ticks` is
|
|
1954
|
+
# likewise in numerator ticks.
|
|
1955
|
+
def _step_grid(step, origin)
|
|
1956
|
+
storage_res = unit
|
|
1957
|
+
step_res = Resolution.parse(step)
|
|
1958
|
+
scale = self.class.send(:_resolve_step_scale, step_res, storage_res)
|
|
1959
|
+
return [:civil, step_res] if scale == :civil
|
|
1960
|
+
mul, step_ticks = scale
|
|
1961
|
+
num_res = mul == 1 ? storage_res : step_res
|
|
1962
|
+
o = self.class.send(:_resolve_origin_ticks, origin, step_res, num_res)
|
|
1963
|
+
[storage_res, mul, step_ticks, o]
|
|
61
1964
|
end
|
|
62
1965
|
|
|
63
|
-
|
|
64
|
-
|
|
1966
|
+
# Storage ticks lifted into the numerator grid (see {#_step_grid}).
|
|
1967
|
+
def _num_ticks(mul)
|
|
1968
|
+
mul == 1 ? parent : CATimeUnitAlgebra.widen(parent, mul)
|
|
65
1969
|
end
|
|
66
1970
|
|
|
67
|
-
|
|
68
|
-
|
|
1971
|
+
# A bucket head computed in numerator ticks, back as a {CATime} in the
|
|
1972
|
+
# storage resolution. The division is exact: a numerator grid finer than
|
|
1973
|
+
# the storage tick only arises when every element already sits on it, so
|
|
1974
|
+
# each head is a whole number of storage ticks.
|
|
1975
|
+
def _head_time(head, mul, su)
|
|
1976
|
+
(mul == 1 ? head : head / mul).time(unit: su)
|
|
69
1977
|
end
|
|
70
1978
|
|
|
71
|
-
|
|
72
|
-
|
|
1979
|
+
# Base units where int64 range is small enough that array-domain arithmetic
|
|
1980
|
+
# (parent - o, 2*d) can overflow for a realistic time span; coarser
|
|
1981
|
+
# bases cannot, so the range guard is skipped for them (zero cost on the
|
|
1982
|
+
# common :s / :h / :D / :M path).
|
|
1983
|
+
FINE_UNITS = %i[us ns ps fs as].freeze
|
|
1984
|
+
|
|
1985
|
+
# Raise (rather than silently wrap) if the integer-path arithmetic would
|
|
1986
|
+
# overflow int64. Checks `parent - o`, and -- when `kind` is a bucket op
|
|
1987
|
+
# (:floor / :ceil / :round) -- the reconstructed bucket head `o + q*step`,
|
|
1988
|
+
# which ceil / round can push up to one step beyond the input range. Both
|
|
1989
|
+
# extremes are computed in the Ruby (bignum) domain from the storage
|
|
1990
|
+
# min / max, so the check is O(1) beyond the two reduces.
|
|
1991
|
+
#
|
|
1992
|
+
# Gated to fine units, where the int64 span is genuinely tight, plus every
|
|
1993
|
+
# widened (`mul` > 1) grid, where a coarse storage unit is lifted into a
|
|
1994
|
+
# much finer one; a pathological raw value wrapped in a coarse-unit Face is
|
|
1995
|
+
# not guarded (the gate is a value-range heuristic, not a proof).
|
|
1996
|
+
def _guard_overflow(su, mul, o, step_ticks = nil, kind = nil)
|
|
1997
|
+
return unless mul > 1 or FINE_UNITS.include?(su.base)
|
|
1998
|
+
return if parent.elements == 0
|
|
1999
|
+
raw = parent.has_mask? ? parent.value : parent
|
|
2000
|
+
lo = Integer(raw.min) * mul
|
|
2001
|
+
hi = Integer(raw.max) * mul
|
|
2002
|
+
chk = ->(v, w) { self.class.send(:_chk64, v, w) }
|
|
2003
|
+
chk.(lo, "ticks in bucket resolution")
|
|
2004
|
+
chk.(hi, "ticks in bucket resolution")
|
|
2005
|
+
chk.(lo - o, "parent - origin")
|
|
2006
|
+
chk.(hi - o, "parent - origin")
|
|
2007
|
+
if kind
|
|
2008
|
+
chk.(_bucket_head(lo, o, step_ticks, kind), "bucket head")
|
|
2009
|
+
chk.(_bucket_head(hi, o, step_ticks, kind), "bucket head")
|
|
2010
|
+
end
|
|
73
2011
|
end
|
|
74
2012
|
|
|
2013
|
+
# Ruby-domain (exact bignum) bucket head for a single storage value -- the
|
|
2014
|
+
# scalar mirror of the array floor / ceil / round, used only by the overflow
|
|
2015
|
+
# guard. Ruby Integer `/` and `%` floor, so `(v - o) / s` is the floor
|
|
2016
|
+
# bucket directly.
|
|
2017
|
+
def _bucket_head(v, o, s, kind)
|
|
2018
|
+
fq = (v - o) / s
|
|
2019
|
+
case kind
|
|
2020
|
+
when :floor then o + fq * s
|
|
2021
|
+
when :ceil then h = o + fq * s; v == h ? h : h + s
|
|
2022
|
+
when :round then r = (v - o) - fq * s; o + (fq + (r >= (s + 1) / 2 ? 1 : 0)) * s
|
|
2023
|
+
end
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
# Days since the Unix epoch of the date each element represents (mask
|
|
2027
|
+
# propagates). Calendar units (:Y / :M) resolve to day 1 of their year /
|
|
2028
|
+
# month; a week is 7 days; day-or-finer units floor to the day.
|
|
2029
|
+
def _field_days
|
|
2030
|
+
res = unit
|
|
2031
|
+
case res.base
|
|
2032
|
+
when :Y
|
|
2033
|
+
ones = CArray.int64(*shape) { 1 }
|
|
2034
|
+
self.class.send(:_days_from_civil, parent * res.count + 1970, ones, ones)
|
|
2035
|
+
when :M
|
|
2036
|
+
mo = parent * res.count + 1970 * 12
|
|
2037
|
+
y = self.class.send(:_floordiv_i, mo, 12)
|
|
2038
|
+
m = mo - y * 12 + 1
|
|
2039
|
+
self.class.send(:_days_from_civil, y, m, CArray.int64(*shape) { 1 })
|
|
2040
|
+
when :W then parent * res.count * 7
|
|
2041
|
+
when :D then parent * res.count
|
|
2042
|
+
else # fixed sub-day storage
|
|
2043
|
+
day = CATime::Resolution.new(1, :D)
|
|
2044
|
+
r = CATimeUnitAlgebra.ratio(day, res) # storage ticks per day
|
|
2045
|
+
if r.denominator == 1
|
|
2046
|
+
self.class.send(:_floordiv_i, parent, r.numerator)
|
|
2047
|
+
else # non-day-aligned tick
|
|
2048
|
+
tr = res.tick_ratio # seconds / tick
|
|
2049
|
+
self.class.send(:_floordiv_i, parent * tr.numerator, 86400 * tr.denominator)
|
|
2050
|
+
end
|
|
2051
|
+
end
|
|
2052
|
+
end
|
|
2053
|
+
|
|
2054
|
+
# Clock field `fu` (:h / :m / :s): the field-of-its-parent value (hour of
|
|
2055
|
+
# day, minute of hour, second of minute). Computed as the epoch-relative
|
|
2056
|
+
# `fu` index of each element modulo its cycle, so a storage tick finer OR
|
|
2057
|
+
# coarser than `fu` both resolve exactly (a 10-minute grid gives minute 0 /
|
|
2058
|
+
# 10 / 20 / ...; a day grid collapses hour / minute / second to 0). A
|
|
2059
|
+
# calendar-storage element has no intra-day field, so it collapses to 0.
|
|
2060
|
+
def _clock_field(fu)
|
|
2061
|
+
r = CATimeUnitAlgebra.ratio(unit, CATime::Resolution.new(1, fu)) # fu ticks / storage tick
|
|
2062
|
+
return parent * 0 if r.nil? # calendar storage: no clock field
|
|
2063
|
+
fi = r.denominator == 1 ? parent * r.numerator # epoch-relative fu index
|
|
2064
|
+
: self.class.send(:_floordiv_i, parent * r.numerator, r.denominator)
|
|
2065
|
+
cycle = fu == :h ? 24 : 60 # hour 0..23; min/sec 0..59
|
|
2066
|
+
fi - self.class.send(:_floordiv_i, fi, cycle) * cycle # floor-mod (handles pre-epoch)
|
|
2067
|
+
end
|
|
2068
|
+
|
|
2069
|
+
# Calendar path (Y/M step on day-or-finer storage) via civil-date integer
|
|
2070
|
+
# algebra: floor toward the past by month ordinal, no boundary array / no
|
|
2071
|
+
# min-max, O(N) branch-free, origin-absolute k (negative pre-origin). The
|
|
2072
|
+
# origin's day / time is ignored (month ordinal only, per the design).
|
|
2073
|
+
# `kind`: :index / :floor / :ceil / :round / :on.
|
|
2074
|
+
def _civil(kind, st, origin)
|
|
2075
|
+
su = unit # storage Resolution
|
|
2076
|
+
count = st.count * (st.base == :Y ? 12 : 1) # bucket in months
|
|
2077
|
+
ym0 = self.class.send(:_origin_month_ordinal, origin)
|
|
2078
|
+
days = _field_days # count-folded day index
|
|
2079
|
+
y, m = self.class.send(:_civil_from_days, days)
|
|
2080
|
+
ym = y * 12 + (m - 1)
|
|
2081
|
+
k = self.class.send(:_floordiv_i, ym - ym0, count)
|
|
2082
|
+
return k if kind == :index
|
|
2083
|
+
head = self.class.send(:_civil_head_ticks, k, st, su, origin)
|
|
2084
|
+
return head.time(unit: su) if kind == :floor
|
|
2085
|
+
return parent.eq(head) if kind == :on
|
|
2086
|
+
head_next = self.class.send(:_civil_head_ticks, k + 1, st, su, origin)
|
|
2087
|
+
sel = # boolean 0/1: pick head_next?
|
|
2088
|
+
case kind
|
|
2089
|
+
when :ceil then parent.ne(head) # on-boundary keeps head
|
|
2090
|
+
when :round then (head_next - parent).le(parent - head) # nearest, ties -> future
|
|
2091
|
+
end
|
|
2092
|
+
(head + (head_next - head) * sel).time(unit: su)
|
|
2093
|
+
end
|
|
2094
|
+
|
|
2095
|
+
class << self
|
|
2096
|
+
private
|
|
2097
|
+
|
|
2098
|
+
INT64_MIN = -(2**63)
|
|
2099
|
+
INT64_MAX = 2**63 - 1
|
|
2100
|
+
|
|
2101
|
+
# Raise (rather than let an int64 CArray operand silently wrap) if a
|
|
2102
|
+
# Ruby-domain quantity is out of int64 range. Returns the value on success
|
|
2103
|
+
# so it composes inline.
|
|
2104
|
+
def _chk64(v, what)
|
|
2105
|
+
if v < INT64_MIN || v > INT64_MAX
|
|
2106
|
+
raise RangeError,
|
|
2107
|
+
"time step: #{what} = #{v} overflows int64 " \
|
|
2108
|
+
"(the storage unit is too fine for this step / origin / span; " \
|
|
2109
|
+
"use a coarser unit)"
|
|
2110
|
+
end
|
|
2111
|
+
v
|
|
2112
|
+
end
|
|
2113
|
+
|
|
2114
|
+
# Return [step_ticks, origin_ticks] (both Integer, in storage ticks) for
|
|
2115
|
+
# the integer path. The calendar path is handled by the caller.
|
|
2116
|
+
def _resolve_grid(step_res, storage_res, origin)
|
|
2117
|
+
mul, step_ticks = _resolve_step_scale(step_res, storage_res)
|
|
2118
|
+
[mul, step_ticks, _resolve_origin_ticks(origin, step_res,
|
|
2119
|
+
mul == 1 ? storage_res : step_res)]
|
|
2120
|
+
end
|
|
2121
|
+
|
|
2122
|
+
# Storage ticks per day of `storage_res` (must be a whole number, else the
|
|
2123
|
+
# storage grid does not tile a day -- a calendar bucket is unrepresentable).
|
|
2124
|
+
def _ticks_per_day(storage_res)
|
|
2125
|
+
r = CATimeUnitAlgebra.ratio(Resolution.new(1, :D), storage_res)
|
|
2126
|
+
unless r.denominator == 1
|
|
2127
|
+
raise ArgumentError,
|
|
2128
|
+
"cannot place a calendar bucket on storage resolution " \
|
|
2129
|
+
"#{storage_res} (its tick does not tile a day)"
|
|
2130
|
+
end
|
|
2131
|
+
_chk64(r.numerator, "ticks per day for #{storage_res}")
|
|
2132
|
+
end
|
|
2133
|
+
|
|
2134
|
+
# Storage ticks of the bucket head at timestep `k` (int64 CArray) for a
|
|
2135
|
+
# calendar bucket: ym0 + k*count months -> day 1 of that month -> ticks.
|
|
2136
|
+
def _civil_head_ticks(k, st, storage_res, origin)
|
|
2137
|
+
tpd = _ticks_per_day(storage_res)
|
|
2138
|
+
count = st.count * (st.base == :Y ? 12 : 1)
|
|
2139
|
+
ym0 = _origin_month_ordinal(origin)
|
|
2140
|
+
ymk = ym0 + k * count
|
|
2141
|
+
yy = _floordiv_i(ymk, 12)
|
|
2142
|
+
mm = ymk - yy * 12 + 1
|
|
2143
|
+
_days_from_civil(yy, mm, CArray.int64(*k.shape) { 1 }) * tpd
|
|
2144
|
+
end
|
|
2145
|
+
|
|
2146
|
+
# Month ordinal (year*12 + month-1) of origin; day / time ignored.
|
|
2147
|
+
# Default (nil) is the epoch month 1970-01.
|
|
2148
|
+
def _origin_month_ordinal(origin)
|
|
2149
|
+
return 1970 * 12 if origin.nil?
|
|
2150
|
+
y, m = _origin_year_month(origin)
|
|
2151
|
+
y * 12 + (m - 1)
|
|
2152
|
+
end
|
|
2153
|
+
|
|
2154
|
+
# Floored integer division of an int64 CArray by a positive Integer
|
|
2155
|
+
# (toward -inf; CArray `/` truncates toward zero).
|
|
2156
|
+
def _floordiv_i(a, b)
|
|
2157
|
+
q = a / b
|
|
2158
|
+
q - (a - q * b).lt(0)
|
|
2159
|
+
end
|
|
2160
|
+
|
|
2161
|
+
# Vectorized Howard Hinnant civil-date algebra (proleptic Gregorian,
|
|
2162
|
+
# negative days supported). days since the Unix epoch -> [year, month,
|
|
2163
|
+
# day] int64 CArrays. Truncating division is what the algorithm assumes;
|
|
2164
|
+
# the only negative operand (the 400-year era) is pre-adjusted so
|
|
2165
|
+
# truncation behaves like floor.
|
|
2166
|
+
def _civil_from_days(z)
|
|
2167
|
+
z = z + 719468
|
|
2168
|
+
era = (z - 146096 * z.lt(0)) / 146097
|
|
2169
|
+
doe = z - era * 146097
|
|
2170
|
+
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
|
|
2171
|
+
y = yoe + era * 400
|
|
2172
|
+
doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
|
|
2173
|
+
mp = (5 * doy + 2) / 153
|
|
2174
|
+
d = doy - (153 * mp + 2) / 5 + 1 # day of month, 1..31
|
|
2175
|
+
m = mp + (3 - 12 * mp.ge(10)) # mp<10 ? +3 : -9
|
|
2176
|
+
[y + m.le(2), m, d]
|
|
2177
|
+
end
|
|
2178
|
+
|
|
2179
|
+
# [year, month, day] int64 CArrays -> days since the Unix epoch.
|
|
2180
|
+
def _days_from_civil(y, m, d)
|
|
2181
|
+
y = y - m.le(2)
|
|
2182
|
+
era = (y - 399 * y.lt(0)) / 400
|
|
2183
|
+
yoe = y - era * 400
|
|
2184
|
+
doy = (153 * (m + (9 - 12 * m.gt(2))) + 2) / 5 + (d - 1) # m>2 ? -3 : +9
|
|
2185
|
+
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
|
|
2186
|
+
era * 146097 + doe - 719468
|
|
2187
|
+
end
|
|
2188
|
+
|
|
2189
|
+
# [mul, step_ticks] for the integer path, or :civil (calendar bucket on
|
|
2190
|
+
# day-or-finer storage), or raise. Bucket arithmetic runs in the finer of
|
|
2191
|
+
# the two grids, so exactly one of the pair is > 1:
|
|
2192
|
+
# - bucket at or coarser than the storage tick (a :h bucket on :s
|
|
2193
|
+
# storage) -> [1, N]: N storage ticks per bucket.
|
|
2194
|
+
# - bucket finer than the storage tick (a :h bucket on :D storage) ->
|
|
2195
|
+
# [N, 1]: one storage tick spans N buckets exactly, so a timestep is a
|
|
2196
|
+
# plain widening. This needs the storage tick to be a *whole* multiple
|
|
2197
|
+
# of the bucket tick; a partial multiple ("90 minutes" storage against
|
|
2198
|
+
# an :h bucket) has no integer timestep and raises.
|
|
2199
|
+
def _resolve_step_scale(step_res, storage_res)
|
|
2200
|
+
r = CATimeUnitAlgebra.ratio(step_res, storage_res) # storage ticks / step tick
|
|
2201
|
+
if r
|
|
2202
|
+
what = "bucket #{step_res} in ticks of #{storage_res}"
|
|
2203
|
+
return [1, _chk64(r.numerator, what)] if r.denominator == 1
|
|
2204
|
+
return [_chk64(r.denominator, what), 1] if r.numerator == 1
|
|
2205
|
+
end
|
|
2206
|
+
if CATimeUnitAlgebra::CALENDAR.key?(step_res.base) && SU_LE_DAY.include?(storage_res.base)
|
|
2207
|
+
return :civil
|
|
2208
|
+
end
|
|
2209
|
+
raise ArgumentError,
|
|
2210
|
+
"cannot express bucket #{step_res} on storage resolution #{storage_res} " \
|
|
2211
|
+
"(neither is a whole multiple of the other)"
|
|
2212
|
+
end
|
|
2213
|
+
|
|
2214
|
+
# origin -> integer tick count in storage ticks. Default (nil) is the
|
|
2215
|
+
# epoch, except a week bucket defaults to ISO Monday (1970-01-05). A lossy
|
|
2216
|
+
# conversion (origin not landing exactly on the storage grid) raises; a
|
|
2217
|
+
# bare Integer is rejected (epoch-dependent, ambiguous).
|
|
2218
|
+
def _resolve_origin_ticks(origin, step_res, storage_res)
|
|
2219
|
+
if origin.nil?
|
|
2220
|
+
if step_res.base == :W && SU_LE_DAY.include?(storage_res.base)
|
|
2221
|
+
return _chk64(_ticks_per_day(storage_res) * 4,
|
|
2222
|
+
"ISO-Monday week origin in ticks of #{storage_res}")
|
|
2223
|
+
end
|
|
2224
|
+
return 0
|
|
2225
|
+
end
|
|
2226
|
+
if CATimeUnitAlgebra::FIXED.key?(storage_res.base)
|
|
2227
|
+
secs = _origin_seconds_exact(origin)
|
|
2228
|
+
tick = secs / storage_res.tick_ratio # Rational
|
|
2229
|
+
unless tick.denominator == 1
|
|
2230
|
+
raise ArgumentError,
|
|
2231
|
+
"origin #{origin.inspect} is not representable losslessly " \
|
|
2232
|
+
"in storage resolution #{storage_res} (would truncate the grid phase)"
|
|
2233
|
+
end
|
|
2234
|
+
_chk64(tick.numerator, "origin #{origin.inspect} in ticks of #{storage_res}")
|
|
2235
|
+
else # :Y / :M storage
|
|
2236
|
+
y, m = _origin_year_month(origin) # day / time ignored
|
|
2237
|
+
ord = storage_res.base == :Y ? (y - 1970) : (y * 12 + (m - 1) - 1970 * 12)
|
|
2238
|
+
if storage_res.count > 1
|
|
2239
|
+
unless (ord % storage_res.count).zero?
|
|
2240
|
+
raise ArgumentError,
|
|
2241
|
+
"origin #{origin.inspect} is not on the #{storage_res} grid"
|
|
2242
|
+
end
|
|
2243
|
+
ord /= storage_res.count
|
|
2244
|
+
end
|
|
2245
|
+
ord
|
|
2246
|
+
end
|
|
2247
|
+
end
|
|
2248
|
+
|
|
2249
|
+
# Exact Rational seconds since the Unix epoch for a fixed-storage origin.
|
|
2250
|
+
# Time / String / DateTime go through the DateTime-independent parser.
|
|
2251
|
+
def _origin_seconds_exact(origin)
|
|
2252
|
+
case origin
|
|
2253
|
+
when CATime::Element
|
|
2254
|
+
unless CATimeUnitAlgebra::FIXED.key?(origin.unit.base)
|
|
2255
|
+
raise ArgumentError,
|
|
2256
|
+
"origin scalar in calendar unit #{origin.unit} has no exact " \
|
|
2257
|
+
"seconds; use a fixed-unit scalar / Time / String"
|
|
2258
|
+
end
|
|
2259
|
+
Rational(origin.value) * origin.unit.tick_ratio
|
|
2260
|
+
when Integer
|
|
2261
|
+
raise ArgumentError,
|
|
2262
|
+
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2263
|
+
"Time / String / CATime scalar"
|
|
2264
|
+
else
|
|
2265
|
+
CArray._epoch_seconds_exact(origin) # Time / String / DateTime
|
|
2266
|
+
end
|
|
2267
|
+
end
|
|
2268
|
+
|
|
2269
|
+
# [year, month] of a calendar-storage origin (finer fields ignored).
|
|
2270
|
+
def _origin_year_month(origin)
|
|
2271
|
+
case origin
|
|
2272
|
+
when CATime::Element
|
|
2273
|
+
case origin.unit.base
|
|
2274
|
+
when :M then mo = origin.value * origin.unit.count + 1970 * 12; [mo / 12, mo % 12 + 1]
|
|
2275
|
+
when :Y then [origin.value * origin.unit.count + 1970, 1]
|
|
2276
|
+
else t = origin.to_time.utc; [t.year, t.month]
|
|
2277
|
+
end
|
|
2278
|
+
when Integer
|
|
2279
|
+
raise ArgumentError,
|
|
2280
|
+
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2281
|
+
"Time / String / CATime scalar"
|
|
2282
|
+
else
|
|
2283
|
+
CArray._epoch_year_month(origin) # Time / String / DateTime
|
|
2284
|
+
end
|
|
2285
|
+
end
|
|
2286
|
+
end
|
|
75
2287
|
end
|
|
76
2288
|
|
|
2289
|
+
# The view-creating method lift hook lives in the C layer (= subclass-
|
|
2290
|
+
# agnostic): each view-creating C method (rb_ca_transpose / rb_ca_reshape
|
|
2291
|
+
# / etc.) ends with `if (ca_is_face(ca)) obj = ca_face_lift(obj, self);`,
|
|
2292
|
+
# so a view of a Face re-lifts to the same Face class. See the relevant C
|
|
2293
|
+
# source (ext/ca_obj_transpose.c etc.) for details.
|