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