carray 3.0.0 → 3.0.2
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 +2 -1
- data/CHANGELOG.md +845 -3
- data/{NEWS.md → CHANGELOG.v1.md} +1 -1
- data/README.md +33 -33
- data/carray.gemspec +12 -8
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +256 -114
- data/ext/ca_binop_dispatch.c +5 -6
- data/ext/ca_binop_dispatch.h +0 -7
- data/ext/ca_categorical_iterator.c +121 -67
- data/ext/ca_for_each_element.h +6 -8
- data/ext/ca_group_iter.c +1 -1
- data/ext/ca_kernel_iterator.c +386 -74
- data/ext/ca_kernel_iterator.h +145 -45
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_bincmp.c +57 -27
- data/ext/ca_obj_binop.c +86 -30
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +88 -27
- data/ext/ca_obj_face.c +104 -0
- data/ext/ca_obj_face.h +48 -1
- data/ext/ca_obj_fake.c +11 -0
- data/ext/ca_obj_fixlen_string.c +21 -6
- data/ext/ca_obj_grid.c +11 -3
- data/ext/ca_obj_meld.c +125 -22
- data/ext/ca_obj_moncmp.c +33 -14
- data/ext/ca_obj_monop.c +19 -7
- data/ext/ca_obj_object.c +107 -4
- data/ext/ca_obj_record.c +3 -1
- data/ext/ca_obj_refer.c +30 -6
- data/ext/ca_obj_roll.c +13 -5
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_select_axis.c +16 -6
- data/ext/ca_obj_shift.c +3 -3
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +178 -24
- data/ext/ca_obj_string.c +11 -5
- data/ext/ca_obj_tile.c +13 -5
- data/ext/ca_obj_time.c +3 -1
- data/ext/ca_obj_timedelta.c +3 -1
- data/ext/ca_obj_transpose.c +1 -1
- data/ext/ca_obj_triop.c +62 -23
- data/ext/ca_obj_window.c +94 -19
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +1 -5
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +5 -5
- data/ext/ca_sweep_engine.c +362 -155
- data/ext/ca_sweep_engine.h +39 -9
- data/ext/ca_transform_common.c +7 -0
- data/ext/carray.h +148 -132
- data/ext/carray_access.c +83 -49
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_bincount.c +8 -8
- data/ext/carray_broadcast.c +102 -9
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +2010 -335
- data/ext/carray_call_cfunc.h +153 -0
- data/ext/carray_cast.c +184 -72
- data/ext/carray_conversion.c +22 -22
- data/ext/carray_copy.c +67 -32
- data/ext/carray_core.c +140 -13
- data/ext/carray_count.c +10 -11
- data/ext/carray_element.c +7 -7
- data/ext/carray_factorize.c +72 -51
- data/ext/carray_hold.c +1 -1
- data/ext/carray_index_classifier.c +7 -21
- data/ext/carray_internal.h +36 -3
- data/ext/carray_kernels_bincmp.c +1 -0
- data/ext/carray_kernels_binop.c +320 -298
- data/ext/carray_kernels_init.c +1095 -0
- data/ext/carray_kernels_map.c +1 -0
- data/ext/carray_kernels_moncmp.c +1 -0
- data/ext/carray_kernels_monop.c +412 -411
- data/ext/carray_kernels_reduce_aggregate.c +256 -87
- data/ext/carray_kernels_reduce_boolean.c +1 -0
- data/ext/carray_kernels_reduce_cumulative.c +324 -54
- data/ext/carray_kernels_reduce_extreme.c +605 -58
- data/ext/carray_kernels_reduce_variance.c +1 -0
- data/ext/carray_kernels_scan.c +5 -4
- data/ext/carray_kernels_search.c +187 -106
- data/ext/carray_kernels_sort.c +1 -0
- data/ext/carray_kernels_triop.c +1 -0
- data/ext/carray_lazy.c +175 -5
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +29 -13
- data/ext/carray_median_percentile.c +56 -1
- data/ext/carray_memory_view.c +46 -21
- data/ext/carray_operator.c +50 -53
- data/ext/carray_order.c +1 -1
- data/ext/carray_partition.c +4 -4
- data/ext/carray_random.c +391 -47
- data/ext/carray_scatter.c +1 -1
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +22 -24
- data/ext/carray_sort_kernel.c +10 -10
- data/ext/carray_test.c +1 -1
- data/ext/extconf.rb +18 -0
- data/ext/mk_call_cfunc.rb +323 -145
- data/ext/mkkernel.rb +651 -129
- data/ext/ruby_carray.c +28 -17
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +14 -1
- data/lib/carray/axis_group.rb +84 -7
- data/lib/carray/basics.rb +63 -59
- data/lib/carray/bincount_nd.rb +12 -12
- data/lib/carray/block_iterator.rb +111 -25
- data/lib/carray/categorical.rb +154 -37
- data/lib/carray/categorical_iterator.rb +300 -151
- data/lib/carray/conditional.rb +14 -14
- data/lib/carray/const_string.rb +131 -27
- data/lib/carray/construct.rb +61 -0
- data/lib/carray/core_extensions.rb +62 -5
- data/lib/carray/data_type_extension.rb +21 -1
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +3 -3
- data/lib/carray/frame/convert.rb +1 -1
- data/lib/carray/frame/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +85 -21
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +224 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/sort.rb +1 -1
- data/lib/carray/frame/verbs.rb +32 -12
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +16 -16
- data/lib/carray/inspect.rb +43 -16
- data/lib/carray/iterator.rb +147 -3
- data/lib/carray/lazy.rb +88 -73
- data/lib/carray/mask_gap_fill.rb +3 -1
- data/lib/carray/meld_reduce.rb +2 -2
- data/lib/carray/methods/align_addr.rb +1 -1
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/is_in.rb +25 -14
- data/lib/carray/methods/locate_addr.rb +81 -3
- data/lib/carray/methods/mask_duplicates.rb +36 -2
- data/lib/carray/methods/meshgrid.rb +4 -5
- data/lib/carray/methods/mode.rb +2 -2
- data/lib/carray/methods/nunique.rb +23 -2
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/snap.rb +7 -2
- data/lib/carray/methods/unique.rb +44 -4
- data/lib/carray/methods/value_counts.rb +2 -2
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +0 -19
- data/lib/carray/slab_iterator.rb +77 -19
- data/lib/carray/string_operation_extension.rb +10 -6
- data/lib/carray/time.rb +906 -491
- data/lib/carray/window_iterator.rb +421 -27
- data/lib/carray.rb +7 -5
- 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 +68 -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 +57 -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 +34 -0
- data/yard-stubs/carray_count.rb +84 -0
- data/yard-stubs/carray_element.rb +108 -0
- data/yard-stubs/carray_generate.rb +66 -0
- data/yard-stubs/carray_lazy.rb +228 -0
- data/yard-stubs/carray_loop.rb +140 -0
- data/yard-stubs/carray_mask.rb +259 -0
- data/yard-stubs/carray_math.rb +1615 -0
- data/yard-stubs/carray_mathfunc.rb +45 -0
- data/yard-stubs/carray_median_percentile.rb +103 -0
- data/yard-stubs/carray_memory_view.rb +163 -0
- data/yard-stubs/carray_order.rb +252 -0
- data/yard-stubs/carray_random.rb +89 -0
- data/yard-stubs/carray_scatter.rb +106 -0
- data/yard-stubs/carray_slab.rb +59 -0
- data/yard-stubs/carray_sort.rb +165 -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 +67 -10
- data/ext/ca_obj_unbound_repeat.c +0 -496
data/lib/carray/time.rb
CHANGED
|
@@ -22,6 +22,60 @@
|
|
|
22
22
|
# Within a group any pair is an exact integer ratio (coarse = fine * N), so a
|
|
23
23
|
# coarse->fine cast is lossless (multiply), and a fine->coarse cast is lossless
|
|
24
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
|
+
|
|
59
|
+
# Same algebra for one date, on plain Integers. A literal is parsed one
|
|
60
|
+
# at a time, and routing each one through the vectorized form costs a
|
|
61
|
+
# one-cell CArray and a kernel call per literal -- which is where nearly
|
|
62
|
+
# all the time in a bulk column parse used to go (170k rows: 3.5 s, of
|
|
63
|
+
# which 3.1 s was this). Ruby's Integer `/` floors, as CArray's does,
|
|
64
|
+
# so the two forms read alike and agree everywhere.
|
|
65
|
+
def days_from_civil_1 (y, m, d)
|
|
66
|
+
y -= 1 if m <= 2
|
|
67
|
+
era = y / 400
|
|
68
|
+
yoe = y - era * 400
|
|
69
|
+
doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + (d - 1)
|
|
70
|
+
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
|
|
71
|
+
era * 146097 + doe - 719468
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Unit algebra for the time surface: how two units relate (same group, which
|
|
76
|
+
# is finer, what a difference is measured in) and how a tick count moves
|
|
77
|
+
# between them -- by scaling for a duration, on the calendar for an instant.
|
|
78
|
+
# Internal to the time surface: nothing here is part of the public API.
|
|
25
79
|
module CATimeUnitAlgebra
|
|
26
80
|
# seconds per base unit (Rational)
|
|
27
81
|
FIXED = {
|
|
@@ -71,6 +125,8 @@ module CATimeUnitAlgebra
|
|
|
71
125
|
g = rgcd(a.tick_ratio, b.tick_ratio)
|
|
72
126
|
CATime::Resolution.new(Integer(g / base_ratio(fb)), fb)
|
|
73
127
|
end
|
|
128
|
+
# Alias of {#common}, used where the caller reads the result as "the finer
|
|
129
|
+
# of the two grids" rather than "the grid they share".
|
|
74
130
|
alias_method :finer, :common
|
|
75
131
|
module_function :finer
|
|
76
132
|
|
|
@@ -78,7 +134,10 @@ module CATimeUnitAlgebra
|
|
|
78
134
|
# grid; cross-group -> the fixed-group resolution (a :M/:Y difference only
|
|
79
135
|
# arises from two calendar operands), coarsened to `(1,:D)` when the fixed
|
|
80
136
|
# side is a week (a week is not calendar-alignable, but both sides convert
|
|
81
|
-
# into days exactly).
|
|
137
|
+
# into days exactly). :W is the only such coarsening: a fixed resolution
|
|
138
|
+
# whose tick does not tile a day (e.g. "7 hours") is returned as-is, and
|
|
139
|
+
# the calendar side then fails to convert into it, so the subtraction
|
|
140
|
+
# raises downstream rather than being silently coarsened.
|
|
82
141
|
def diff_unit(u1, u2)
|
|
83
142
|
a = res(u1); b = res(u2)
|
|
84
143
|
return common(a, b) if same_group?(a, b)
|
|
@@ -124,19 +183,21 @@ module CATimeUnitAlgebra
|
|
|
124
183
|
# instant / duration. Checks the extremes (they bound every element), then
|
|
125
184
|
# multiplies. Shared by every coarse->fine conversion (arithmetic,
|
|
126
185
|
# comparison, search).
|
|
186
|
+
#
|
|
187
|
+
# min / max skip masked cells, so a masked cell does not decide the range:
|
|
188
|
+
# it carries no value to convert, and whatever bits sit under the mask are
|
|
189
|
+
# not a time. They answer UNDEF when there is nothing to bound (empty, or
|
|
190
|
+
# every cell masked), and then there is nothing to guard either.
|
|
127
191
|
def widen(storage, factor)
|
|
128
192
|
return storage if factor == 1
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"time unit conversion overflows int64: the time range is " \
|
|
138
|
-
"too wide to widen into this resolution (x#{factor})"
|
|
139
|
-
end
|
|
193
|
+
lo = storage.min
|
|
194
|
+
unless lo == UNDEF
|
|
195
|
+
lim = 2**63 - 1
|
|
196
|
+
[lo, storage.max].each do |x|
|
|
197
|
+
next if (Integer(x) * factor).abs <= lim
|
|
198
|
+
raise RangeError,
|
|
199
|
+
"time unit conversion overflows int64: the time range is " \
|
|
200
|
+
"too wide to widen into this resolution (x#{factor})"
|
|
140
201
|
end
|
|
141
202
|
end
|
|
142
203
|
storage * factor
|
|
@@ -157,25 +218,24 @@ module CATimeUnitAlgebra
|
|
|
157
218
|
"(:Y/:M) and fixed-length units (:W/:D/:h/:s/...) have no fixed " \
|
|
158
219
|
"ratio (a month / year is calendar-variable)"
|
|
159
220
|
end
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"some values are not a whole multiple of #{b} " \
|
|
168
|
-
"(finer resolution would be truncated)"
|
|
169
|
-
end
|
|
170
|
-
storage / divisor
|
|
221
|
+
scaled = widen(storage, r.numerator) # coarse -> fine: lossless multiply
|
|
222
|
+
return scaled if r.denominator == 1
|
|
223
|
+
unless (scaled % r.denominator).eq(0).all
|
|
224
|
+
raise ArgumentError,
|
|
225
|
+
"cannot scale duration #{a} to #{b} without loss: " \
|
|
226
|
+
"some values are not a whole multiple of #{b} " \
|
|
227
|
+
"(finer resolution would be truncated)"
|
|
171
228
|
end
|
|
229
|
+
scaled / r.denominator
|
|
172
230
|
end
|
|
173
231
|
|
|
174
232
|
# SCALE conversion with truncation: like convert_scale! but a fine->coarse
|
|
175
233
|
# conversion drops the sub-`to` remainder (truncating toward zero) instead of
|
|
176
|
-
# raising.
|
|
177
|
-
#
|
|
178
|
-
#
|
|
234
|
+
# raising. A duration is a magnitude, so it shrinks toward zero rather than
|
|
235
|
+
# flooring toward the past the way an instant does. Used by
|
|
236
|
+
# CATimedelta#to_unit and by dt +/- td, where the result keeps the time's
|
|
237
|
+
# unit and a finer duration is truncated to it (a :D time + a 5 h duration
|
|
238
|
+
# is + 0 days; + 30 h is + 1 day). Cross-group still raises (a calendar
|
|
179
239
|
# duration has no fixed ratio to a fixed unit).
|
|
180
240
|
def convert_scale_trunc(storage, from, to)
|
|
181
241
|
a = res(from); b = res(to)
|
|
@@ -183,10 +243,20 @@ module CATimeUnitAlgebra
|
|
|
183
243
|
r = ratio(a, b)
|
|
184
244
|
if r.nil?
|
|
185
245
|
raise ArgumentError,
|
|
186
|
-
"cannot
|
|
187
|
-
"
|
|
246
|
+
"cannot scale duration #{a} to #{b}: calendar units " \
|
|
247
|
+
"(:Y/:M) and fixed-length units (:W/:D/:h/:s/...) have no fixed " \
|
|
248
|
+
"ratio (a month / year is calendar-variable)"
|
|
188
249
|
end
|
|
189
|
-
|
|
250
|
+
scaled = widen(storage, r.numerator)
|
|
251
|
+
r.denominator == 1 ? scaled : trunc_divide(scaled, r.denominator)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Truncating integer division of an int64 CArray (toward zero). CArray
|
|
255
|
+
# `/` floors toward the past, which is what an instant wants; a duration
|
|
256
|
+
# is a magnitude and shrinks toward zero instead.
|
|
257
|
+
def trunc_divide(a, b)
|
|
258
|
+
q = a / b
|
|
259
|
+
q + (q.lt(0) & (a - q * b).ne(0))
|
|
190
260
|
end
|
|
191
261
|
|
|
192
262
|
# INSTANT conversion (absolute datetimes): unlike a duration, a time :M
|
|
@@ -203,23 +273,23 @@ module CATimeUnitAlgebra
|
|
|
203
273
|
return storage if a == b
|
|
204
274
|
return convert_scale!(storage, a, b) if ratio(a, b) # same group
|
|
205
275
|
if CALENDAR.key?(a.base)
|
|
206
|
-
|
|
276
|
+
convert_instant_calendar_to_fixed(storage, a, b) # widen
|
|
207
277
|
else
|
|
208
|
-
|
|
278
|
+
convert_instant_fixed_to_calendar(storage, a, b) # coarsen (exact-or-raise)
|
|
209
279
|
end
|
|
210
280
|
end
|
|
211
281
|
|
|
212
282
|
# calendar time (Resolution `from`) -> days since the epoch (int64
|
|
213
283
|
# CArray). Folds the resolution count (value = count-Y/M buckets).
|
|
214
|
-
def
|
|
284
|
+
def calendar_days_since_epoch(storage, from)
|
|
215
285
|
ones = CArray.int64(*storage.shape) { 1 }
|
|
216
286
|
if from.base == :M
|
|
217
287
|
abs = storage * from.count + 1970 * 12 # absolute month ordinal
|
|
218
|
-
y =
|
|
288
|
+
y = abs / 12
|
|
219
289
|
m = abs - y * 12 + 1
|
|
220
|
-
|
|
290
|
+
CATimeCivil.days_from_civil(y, m, ones)
|
|
221
291
|
else # :Y
|
|
222
|
-
|
|
292
|
+
CATimeCivil.days_from_civil(storage * from.count + 1970, ones, ones)
|
|
223
293
|
end
|
|
224
294
|
end
|
|
225
295
|
|
|
@@ -227,21 +297,21 @@ module CATimeUnitAlgebra
|
|
|
227
297
|
# convert_instant!. Goes through the day count, so the target grid has
|
|
228
298
|
# to tile a day exactly (:W is rejected -- month starts are not
|
|
229
299
|
# week-aligned). Always exact once that holds.
|
|
230
|
-
def
|
|
300
|
+
def convert_instant_calendar_to_fixed(storage, from, to)
|
|
231
301
|
r = ratio(CATime::Resolution.new(1, :D), to) # ticks of `to` per day
|
|
232
302
|
unless r.denominator == 1
|
|
233
303
|
raise ArgumentError,
|
|
234
304
|
"cannot convert calendar time #{from} to #{to} " \
|
|
235
305
|
"(a day boundary is not aligned to the #{to} grid)"
|
|
236
306
|
end
|
|
237
|
-
widen(
|
|
307
|
+
widen(calendar_days_since_epoch(storage, from), r.numerator)
|
|
238
308
|
end
|
|
239
309
|
|
|
240
310
|
# fixed-length grid -> calendar time: the coarsening half of
|
|
241
311
|
# convert_instant!, exact-or-raise. Every instant must land on a day
|
|
242
312
|
# boundary and then on the calendar boundary itself (the 1st, and
|
|
243
313
|
# January too for :Y), since a mid-month instant has no :M value.
|
|
244
|
-
def
|
|
314
|
+
def convert_instant_fixed_to_calendar(storage, from, to)
|
|
245
315
|
rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
|
|
246
316
|
days =
|
|
247
317
|
if rd.denominator == 1
|
|
@@ -255,7 +325,7 @@ module CATimeUnitAlgebra
|
|
|
255
325
|
else # coarser than a day (:W)
|
|
256
326
|
storage * (from.tick_ratio / 86400r).to_i
|
|
257
327
|
end
|
|
258
|
-
y, m, d =
|
|
328
|
+
y, m, d = CATimeCivil.civil_from_days(days)
|
|
259
329
|
on_boundary = d.eq(1)
|
|
260
330
|
on_boundary &= m.eq(1) if to.base == :Y
|
|
261
331
|
unless on_boundary.all
|
|
@@ -270,10 +340,49 @@ module CATimeUnitAlgebra
|
|
|
270
340
|
"cannot convert time #{from} to #{to} without loss " \
|
|
271
341
|
"(instant is not on a #{to} boundary)"
|
|
272
342
|
end
|
|
273
|
-
ord =
|
|
343
|
+
ord = ord / to.count
|
|
274
344
|
end
|
|
275
345
|
ord
|
|
276
346
|
end
|
|
347
|
+
|
|
348
|
+
# INSTANT conversion that floors instead of raising: the storage-resolution
|
|
349
|
+
# change behind CATime#to_unit. A coarser target keeps the bucket the
|
|
350
|
+
# instant falls in (floor toward the past, the direction construction and
|
|
351
|
+
# `floor` already use), so no element is rejected for sitting off the
|
|
352
|
+
# target grid -- a resolution change is a cast, not an assertion. The
|
|
353
|
+
# widening half is untouched and stays exact, and :Y/:M -> :W still raises
|
|
354
|
+
# (a month head is not week-aligned, so widening there would move the
|
|
355
|
+
# instant).
|
|
356
|
+
def convert_instant_floor(storage, from, to)
|
|
357
|
+
a = res(from); b = res(to)
|
|
358
|
+
return storage if a == b
|
|
359
|
+
r = ratio(a, b)
|
|
360
|
+
if r # same group
|
|
361
|
+
scaled = widen(storage, r.numerator)
|
|
362
|
+
r.denominator == 1 ? scaled
|
|
363
|
+
: scaled / r.denominator
|
|
364
|
+
elsif CALENDAR.key?(a.base)
|
|
365
|
+
convert_instant_calendar_to_fixed(storage, a, b) # widen, always exact
|
|
366
|
+
else
|
|
367
|
+
convert_instant_fixed_to_calendar_floor(storage, a, b)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# fixed-length grid -> calendar time, flooring: the instant's own year /
|
|
372
|
+
# month, whatever day and time it carries. The flooring counterpart of
|
|
373
|
+
# convert_instant_fixed_to_calendar.
|
|
374
|
+
def convert_instant_fixed_to_calendar_floor(storage, from, to)
|
|
375
|
+
rd = ratio(CATime::Resolution.new(1, :D), from) # `from` ticks per day
|
|
376
|
+
days =
|
|
377
|
+
if rd.denominator == 1
|
|
378
|
+
storage / rd.numerator
|
|
379
|
+
else # coarser than a day (:W)
|
|
380
|
+
storage * (from.tick_ratio / 86400r).to_i
|
|
381
|
+
end
|
|
382
|
+
y, m, = CATimeCivil.civil_from_days(days)
|
|
383
|
+
ord = to.base == :M ? (y * 12 + (m - 1) - 1970 * 12) : (y - 1970)
|
|
384
|
+
to.count > 1 ? ord / to.count : ord
|
|
385
|
+
end
|
|
277
386
|
end
|
|
278
387
|
|
|
279
388
|
class CATime
|
|
@@ -332,8 +441,8 @@ class CATime
|
|
|
332
441
|
require 'time'
|
|
333
442
|
require 'date'
|
|
334
443
|
case unit.base
|
|
335
|
-
when :Y then d =
|
|
336
|
-
when :M then d =
|
|
444
|
+
when :Y then d = epoch_date.next_year(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
445
|
+
when :M then d = epoch_date.next_month(value * unit.count); Time.utc(d.year, d.month, d.day)
|
|
337
446
|
else
|
|
338
447
|
Time.at(Rational(value) * unit.tick_ratio, in: 'UTC') # exact seconds
|
|
339
448
|
end
|
|
@@ -344,11 +453,11 @@ class CATime
|
|
|
344
453
|
def to_date
|
|
345
454
|
require 'date'
|
|
346
455
|
case unit.base
|
|
347
|
-
when :Y then
|
|
348
|
-
when :M then
|
|
456
|
+
when :Y then epoch_date.next_year(value * unit.count)
|
|
457
|
+
when :M then epoch_date.next_month(value * unit.count)
|
|
349
458
|
when :W then Date.jd(EPOCH_JD + value * unit.count * 7, Date::GREGORIAN)
|
|
350
459
|
when :D then Date.jd(EPOCH_JD + value * unit.count, Date::GREGORIAN)
|
|
351
|
-
else Date.jd(EPOCH_JD +
|
|
460
|
+
else Date.jd(EPOCH_JD + floor_to_days_since_epoch, Date::GREGORIAN) # sub-day: floor to day
|
|
352
461
|
end
|
|
353
462
|
end
|
|
354
463
|
|
|
@@ -366,7 +475,7 @@ class CATime
|
|
|
366
475
|
when :Y then format("%04d", to_date.year)
|
|
367
476
|
when :M then to_date.strftime("%Y-%m")
|
|
368
477
|
when :W, :D then to_date.strftime("%Y-%m-%d")
|
|
369
|
-
else to_time.iso8601(
|
|
478
|
+
else to_time.iso8601(fractional_second_digits) # :h .. :as (time shown)
|
|
370
479
|
end
|
|
371
480
|
end
|
|
372
481
|
|
|
@@ -386,7 +495,7 @@ class CATime
|
|
|
386
495
|
case other
|
|
387
496
|
when Element
|
|
388
497
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
389
|
-
|
|
498
|
+
instant_in(u) <=> other.instant_in(u)
|
|
390
499
|
when Time
|
|
391
500
|
to_time <=> other.getutc
|
|
392
501
|
when (defined?(DateTime) ? DateTime : nil)
|
|
@@ -409,16 +518,19 @@ class CATime
|
|
|
409
518
|
end
|
|
410
519
|
|
|
411
520
|
# @overload +(td)
|
|
412
|
-
# Returns this instant advanced by a {CATimedelta::Element}. The
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
521
|
+
# Returns this instant advanced by a {CATimedelta::Element}. The time
|
|
522
|
+
# is the anchor: the result keeps `self`'s unit and the duration is
|
|
523
|
+
# converted into it, truncated toward zero when finer (a :D time + a
|
|
524
|
+
# 5 h duration is + 0 days; + 30 h is + 1 day) -- the same rule as the
|
|
525
|
+
# array {CATime#+}. Only a duration in the SAME group (both calendar
|
|
526
|
+
# or both fixed) is accepted -- a cross-group step (a :s time + a :M
|
|
527
|
+
# duration) is calendar arithmetic; use `to_date` +
|
|
528
|
+
# `Date#next_month` / `#next_year` for that.
|
|
417
529
|
# @param td [CATimedelta::Element]
|
|
418
530
|
# @return [CATime::Element]
|
|
419
531
|
# @raise [TypeError, ArgumentError] on a non-timedelta / cross-group operand.
|
|
420
532
|
def +(td)
|
|
421
|
-
|
|
533
|
+
shifted_by_duration(td, 1)
|
|
422
534
|
end
|
|
423
535
|
|
|
424
536
|
# @overload -(other)
|
|
@@ -432,9 +544,9 @@ class CATime
|
|
|
432
544
|
case other
|
|
433
545
|
when Element
|
|
434
546
|
u = CATimeUnitAlgebra.diff_unit(unit, other.unit)
|
|
435
|
-
CATimedelta::Element.new(
|
|
547
|
+
CATimedelta::Element.new(instant_in(u) - other.instant_in(u), u)
|
|
436
548
|
when CATimedelta::Element
|
|
437
|
-
|
|
549
|
+
shifted_by_duration(other, -1)
|
|
438
550
|
else
|
|
439
551
|
raise TypeError, "CATime::Element - #{other.class} is not allowed"
|
|
440
552
|
end
|
|
@@ -444,7 +556,7 @@ class CATime
|
|
|
444
556
|
|
|
445
557
|
# This instant's storage value expressed in `to` unit (exact; raises if the
|
|
446
558
|
# instant does not land on `to`'s grid).
|
|
447
|
-
def
|
|
559
|
+
def instant_in(to)
|
|
448
560
|
return value if to == unit
|
|
449
561
|
CATimeUnitAlgebra.convert_instant!(CA_INT64([value]), unit, to)[0]
|
|
450
562
|
end
|
|
@@ -454,7 +566,7 @@ class CATime
|
|
|
454
566
|
# s +/- td: the time is the anchor, so the result keeps self's unit and
|
|
455
567
|
# the duration is converted into it (truncated toward zero when finer; a
|
|
456
568
|
# cross-group calendar duration raises). Mirrors the array CATime +/-.
|
|
457
|
-
def
|
|
569
|
+
def shifted_by_duration(td, sign)
|
|
458
570
|
unless td.is_a?(CATimedelta::Element)
|
|
459
571
|
raise TypeError,
|
|
460
572
|
"CATime::Element #{sign > 0 ? '+' : '-'} #{td.class} is not " \
|
|
@@ -467,18 +579,18 @@ class CATime
|
|
|
467
579
|
# Base epoch as a Date. next_month / next_year clamp end-of-month
|
|
468
580
|
# (Jan 31 -> Feb 28), but the base is day 1, so no clamping ever occurs --
|
|
469
581
|
# do not move the base off the first of the month without revisiting.
|
|
470
|
-
def
|
|
582
|
+
def epoch_date
|
|
471
583
|
require 'date'
|
|
472
584
|
Date.new(1970, 1, 1, Date::GREGORIAN) # proleptic Gregorian, matching to_time
|
|
473
585
|
end
|
|
474
586
|
|
|
475
587
|
# Days since the epoch for a fixed sub-day unit (floored toward the past).
|
|
476
|
-
def
|
|
588
|
+
def floor_to_days_since_epoch
|
|
477
589
|
sec = Rational(value) * unit.tick_ratio # exact seconds
|
|
478
590
|
(sec / 86400).floor
|
|
479
591
|
end
|
|
480
592
|
|
|
481
|
-
def
|
|
593
|
+
def fractional_second_digits
|
|
482
594
|
case unit.base
|
|
483
595
|
when :ms then 3
|
|
484
596
|
when :us then 6
|
|
@@ -560,7 +672,7 @@ class CATimedelta
|
|
|
560
672
|
return nil unless other.is_a?(Element)
|
|
561
673
|
return nil unless CATimeUnitAlgebra.same_group?(unit, other.unit)
|
|
562
674
|
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
563
|
-
|
|
675
|
+
duration_in(u) <=> other.duration_in(u)
|
|
564
676
|
rescue ArgumentError
|
|
565
677
|
nil
|
|
566
678
|
end
|
|
@@ -580,13 +692,13 @@ class CATimedelta
|
|
|
580
692
|
# raises.
|
|
581
693
|
# @param other [CATimedelta::Element]
|
|
582
694
|
# @return [CATimedelta::Element]
|
|
583
|
-
def +(other) =
|
|
695
|
+
def +(other) = combined_with_duration(other, 1)
|
|
584
696
|
|
|
585
697
|
# @overload -(other)
|
|
586
698
|
# Returns the difference of two durations (finer unit; cross-group raises).
|
|
587
699
|
# @param other [CATimedelta::Element]
|
|
588
700
|
# @return [CATimedelta::Element]
|
|
589
|
-
def -(other) =
|
|
701
|
+
def -(other) = combined_with_duration(other, -1)
|
|
590
702
|
|
|
591
703
|
# @overload *(n)
|
|
592
704
|
# Returns this duration scaled by an Integer.
|
|
@@ -598,17 +710,20 @@ class CATimedelta
|
|
|
598
710
|
end
|
|
599
711
|
|
|
600
712
|
# @overload /(other)
|
|
601
|
-
# By an Integer -> a scaled {CATimedelta::Element}
|
|
713
|
+
# By an Integer -> a scaled {CATimedelta::Element}, the quotient
|
|
714
|
+
# truncated toward zero (a duration is a magnitude, so it shrinks
|
|
715
|
+
# toward zero -- the same direction {CATimedelta#/} and #to_unit
|
|
716
|
+
# take, not Ruby Integer division's floor); by another
|
|
602
717
|
# {CATimedelta::Element} -> their dimensionless ratio as a `Rational`
|
|
603
718
|
# (both brought to the finer unit; cross-group raises).
|
|
604
719
|
# @param other [Integer, CATimedelta::Element]
|
|
605
720
|
# @return [CATimedelta::Element, Rational]
|
|
606
721
|
def /(other)
|
|
607
722
|
case other
|
|
608
|
-
when Integer then Element.new(value
|
|
723
|
+
when Integer then Element.new(Rational(value, other).truncate, unit)
|
|
609
724
|
when Element
|
|
610
725
|
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
611
|
-
Rational(
|
|
726
|
+
Rational(duration_in(u), other.duration_in(u))
|
|
612
727
|
else
|
|
613
728
|
raise TypeError, "CATimedelta::Element / #{other.class} is not allowed"
|
|
614
729
|
end
|
|
@@ -618,20 +733,20 @@ class CATimedelta
|
|
|
618
733
|
|
|
619
734
|
# This duration's value expressed in `to` unit (exact scale; raises on a
|
|
620
735
|
# cross-group or non-whole conversion).
|
|
621
|
-
def
|
|
736
|
+
def duration_in(to)
|
|
622
737
|
return value if to == unit
|
|
623
738
|
CATimeUnitAlgebra.convert_scale!(CA_INT64([value]), unit, to)[0]
|
|
624
739
|
end
|
|
625
740
|
|
|
626
741
|
private
|
|
627
742
|
|
|
628
|
-
def
|
|
743
|
+
def combined_with_duration(other, sign)
|
|
629
744
|
unless other.is_a?(Element)
|
|
630
745
|
raise TypeError,
|
|
631
746
|
"CATimedelta::Element #{sign > 0 ? '+' : '-'} #{other.class} is not allowed"
|
|
632
747
|
end
|
|
633
748
|
u = CATimeUnitAlgebra.finer(unit, other.unit)
|
|
634
|
-
Element.new(
|
|
749
|
+
Element.new(duration_in(u) + sign * other.duration_in(u), u)
|
|
635
750
|
end
|
|
636
751
|
end
|
|
637
752
|
end
|
|
@@ -665,23 +780,33 @@ class CATime
|
|
|
665
780
|
end
|
|
666
781
|
|
|
667
782
|
# @overload to_unit(unit)
|
|
668
|
-
# Returns the same instants re-expressed on
|
|
669
|
-
# {CATime} whose storage is `
|
|
670
|
-
#
|
|
671
|
-
#
|
|
672
|
-
#
|
|
673
|
-
#
|
|
674
|
-
#
|
|
783
|
+
# Returns the same instants re-expressed on the `unit` grid: a new
|
|
784
|
+
# {CATime} whose storage resolution is `unit`. Changing the resolution
|
|
785
|
+
# is a cast, not an assertion -- the same relation `CArray.time` has to
|
|
786
|
+
# its input:
|
|
787
|
+
#
|
|
788
|
+
# - **Finer target** (`:D` -> `:h`, `:Y` -> `:M`, `:M` -> `:s`): exact.
|
|
789
|
+
# Every instant lands on the new grid unchanged.
|
|
790
|
+
# - **Coarser target** (`:h` -> `:D`, `:D` -> `:M`): each instant floors
|
|
791
|
+
# to the head of the `unit` tick it falls in -- toward the past, so a
|
|
792
|
+
# pre-epoch instant floors the same way a post-epoch one does. Use
|
|
793
|
+
# {#ceil} / {#round} first to land on a different boundary.
|
|
794
|
+
#
|
|
795
|
+
# The calendar / fixed-length boundary is crossed by civil-date algebra,
|
|
796
|
+
# not by a ratio: a `:M` value is a real instant (the month's first
|
|
797
|
+
# midnight), so `:M` -> `:D` widens exactly and `:D` -> `:M` floors to
|
|
798
|
+
# the containing month. `:Y` / `:M` -> `:W` is the one refusal, since a
|
|
799
|
+
# month head is not week-aligned and widening would move the instant.
|
|
800
|
+
#
|
|
801
|
+
# A *duration* has no such conversion ({CATimedelta#to_unit} truncates
|
|
802
|
+
# toward zero and refuses to cross the boundary at all).
|
|
675
803
|
# @param unit [Resolution, Symbol, String] target resolution.
|
|
676
804
|
# @return [CATime]
|
|
677
|
-
# @raise [ArgumentError]
|
|
678
|
-
# `unit`'s (including any calendar / fixed-length pair, where no fixed
|
|
679
|
-
# ratio exists).
|
|
805
|
+
# @raise [ArgumentError] on `:Y` / `:M` -> `:W`.
|
|
680
806
|
# @raise [RangeError] when the widened ticks overflow int64.
|
|
681
807
|
def to_unit(unit)
|
|
682
808
|
to = CATime::Resolution.parse(unit)
|
|
683
|
-
CATimeUnitAlgebra.
|
|
684
|
-
CATimeUnitAlgebra.multiple_factor(self.unit, to)).time(unit: to)
|
|
809
|
+
CATimeUnitAlgebra.convert_instant_floor(parent, self.unit, to).time(unit: to)
|
|
685
810
|
end
|
|
686
811
|
|
|
687
812
|
# @overload +(other)
|
|
@@ -754,17 +879,17 @@ class CATime
|
|
|
754
879
|
# @return [Array(Element, Element), Array(CATime, CATime)]
|
|
755
880
|
def minmax(*args, **opts)
|
|
756
881
|
lo, hi = parent.minmax(*args, **opts)
|
|
757
|
-
[
|
|
882
|
+
[relift_extremum(lo), relift_extremum(hi)]
|
|
758
883
|
end
|
|
759
884
|
|
|
760
|
-
def
|
|
885
|
+
def relift_extremum(r)
|
|
761
886
|
case r
|
|
762
887
|
when Integer then Element.new(r, unit)
|
|
763
888
|
when CArray then r.time(unit: unit)
|
|
764
889
|
else r
|
|
765
890
|
end
|
|
766
891
|
end
|
|
767
|
-
private :
|
|
892
|
+
private :relift_extremum
|
|
768
893
|
|
|
769
894
|
# The centroid / spread / order reductions report on the array's own grid:
|
|
770
895
|
# they run on the storage ticks and round the result back to the nearest
|
|
@@ -778,7 +903,7 @@ class CATime
|
|
|
778
903
|
# nearest tick.
|
|
779
904
|
# @return [Element, CATime]
|
|
780
905
|
def mean(*args, **opts)
|
|
781
|
-
|
|
906
|
+
reduce_on_own_grid(:mean, :time, args, opts)
|
|
782
907
|
end
|
|
783
908
|
|
|
784
909
|
# @overload median(axis: nil, **opts)
|
|
@@ -787,7 +912,7 @@ class CATime
|
|
|
787
912
|
# cases interpolate and round to the nearest tick.
|
|
788
913
|
# @return [Element, CATime]
|
|
789
914
|
def median(*args, **opts)
|
|
790
|
-
|
|
915
|
+
reduce_on_own_grid(:median, :time, args, opts)
|
|
791
916
|
end
|
|
792
917
|
|
|
793
918
|
# @overload percentile(*p, axis: nil, **opts)
|
|
@@ -797,7 +922,7 @@ class CATime
|
|
|
797
922
|
# Array of those.
|
|
798
923
|
# @return [Element, CATime, Array<Element>, Array<CATime>]
|
|
799
924
|
def percentile(*args, **opts)
|
|
800
|
-
|
|
925
|
+
reduce_on_own_grid(:percentile, :time, args, opts)
|
|
801
926
|
end
|
|
802
927
|
|
|
803
928
|
# @overload quantile(axis: nil, **opts)
|
|
@@ -805,7 +930,7 @@ class CATime
|
|
|
805
930
|
# `self`'s unit (shorthand for `percentile(0, 25, 50, 75, 100)`).
|
|
806
931
|
# @return [Array<Element>, Array<CATime>]
|
|
807
932
|
def quantile(*args, **opts)
|
|
808
|
-
|
|
933
|
+
reduce_on_own_grid(:quantile, :time, args, opts)
|
|
809
934
|
end
|
|
810
935
|
|
|
811
936
|
# @overload stddev(axis: nil, **opts)
|
|
@@ -815,7 +940,7 @@ class CATime
|
|
|
815
940
|
# `t.to_unit(:h).stddev` when the precision matters (§8).
|
|
816
941
|
# @return [CATimedelta::Element, CATimedelta]
|
|
817
942
|
def stddev(*args, **opts)
|
|
818
|
-
|
|
943
|
+
reduce_on_own_grid(:stddev, :timedelta, args, opts)
|
|
819
944
|
end
|
|
820
945
|
|
|
821
946
|
# @overload stddevp(axis: nil, **opts)
|
|
@@ -823,7 +948,7 @@ class CATime
|
|
|
823
948
|
# as {#stddev}.
|
|
824
949
|
# @return [CATimedelta::Element, CATimedelta]
|
|
825
950
|
def stddevp(*args, **opts)
|
|
826
|
-
|
|
951
|
+
reduce_on_own_grid(:stddevp, :timedelta, args, opts)
|
|
827
952
|
end
|
|
828
953
|
|
|
829
954
|
# @overload sum(*)
|
|
@@ -989,7 +1114,7 @@ class CATime
|
|
|
989
1114
|
f = unit.tick_ratio # exact seconds / tick (Rational)
|
|
990
1115
|
parent.convert(:object) {|v| Time.at(v * f, in: 'UTC')}
|
|
991
1116
|
else # calendar: exact granule midnight
|
|
992
|
-
(
|
|
1117
|
+
(days_since_epoch * 86400).convert(:object) {|v| Time.at(v, in: 'UTC')}
|
|
993
1118
|
end
|
|
994
1119
|
end
|
|
995
1120
|
|
|
@@ -1001,7 +1126,7 @@ class CATime
|
|
|
1001
1126
|
def to_date
|
|
1002
1127
|
require 'date'
|
|
1003
1128
|
# 2440588 = JD of 1970-01-01; proleptic Gregorian to match to_time and the field accessors.
|
|
1004
|
-
|
|
1129
|
+
days_since_epoch.convert(:object) {|d| Date.jd(2440588 + d, Date::GREGORIAN)}
|
|
1005
1130
|
end
|
|
1006
1131
|
|
|
1007
1132
|
# @overload to_datetime
|
|
@@ -1024,8 +1149,8 @@ class CATime
|
|
|
1024
1149
|
def year
|
|
1025
1150
|
case unit.base
|
|
1026
1151
|
when :Y then parent * unit.count + 1970
|
|
1027
|
-
when :M then
|
|
1028
|
-
else
|
|
1152
|
+
when :M then (parent * unit.count + 1970 * 12) / 12
|
|
1153
|
+
else CATimeCivil.civil_from_days(days_since_epoch)[0]
|
|
1029
1154
|
end
|
|
1030
1155
|
end
|
|
1031
1156
|
|
|
@@ -1037,8 +1162,8 @@ class CATime
|
|
|
1037
1162
|
when :Y then parent * 0 + 1
|
|
1038
1163
|
when :M
|
|
1039
1164
|
mo = parent * unit.count + 1970 * 12
|
|
1040
|
-
mo
|
|
1041
|
-
else
|
|
1165
|
+
mo % 12 + 1
|
|
1166
|
+
else CATimeCivil.civil_from_days(days_since_epoch)[1]
|
|
1042
1167
|
end
|
|
1043
1168
|
end
|
|
1044
1169
|
|
|
@@ -1048,35 +1173,35 @@ class CATime
|
|
|
1048
1173
|
def day
|
|
1049
1174
|
case unit.base
|
|
1050
1175
|
when :Y, :M then parent * 0 + 1
|
|
1051
|
-
else
|
|
1176
|
+
else CATimeCivil.civil_from_days(days_since_epoch)[2]
|
|
1052
1177
|
end
|
|
1053
1178
|
end
|
|
1054
1179
|
|
|
1055
1180
|
# Hour of the day, 0..23; 0 when the storage unit is coarser than an hour.
|
|
1056
1181
|
# @return [CArray] integer.
|
|
1057
|
-
def hour;
|
|
1182
|
+
def hour; clock_field(:h); end
|
|
1058
1183
|
# Minute of the hour, 0..59; 0 when the storage unit is coarser than a minute.
|
|
1059
1184
|
# @return [CArray] integer.
|
|
1060
|
-
def minute;
|
|
1185
|
+
def minute; clock_field(:m); end
|
|
1061
1186
|
# Second of the minute, 0..59; 0 when the storage unit is coarser than a second.
|
|
1062
1187
|
# @return [CArray] integer.
|
|
1063
|
-
def second;
|
|
1188
|
+
def second; clock_field(:s); end
|
|
1064
1189
|
|
|
1065
1190
|
# Day of the week, Sunday = 0 .. Saturday = 6.
|
|
1066
1191
|
# @return [CArray] integer.
|
|
1067
1192
|
def weekday
|
|
1068
1193
|
# 1970-01-01 is a Thursday (wday 4); Sun=0..Sat=6.
|
|
1069
|
-
n =
|
|
1070
|
-
n
|
|
1194
|
+
n = days_since_epoch + 4
|
|
1195
|
+
n % 7
|
|
1071
1196
|
end
|
|
1072
1197
|
|
|
1073
1198
|
# Day of the year, 1..366.
|
|
1074
1199
|
# @return [CArray] integer.
|
|
1075
1200
|
def yday
|
|
1076
|
-
d =
|
|
1077
|
-
y =
|
|
1201
|
+
d = days_since_epoch
|
|
1202
|
+
y = CATimeCivil.civil_from_days(d)[0]
|
|
1078
1203
|
ones = CArray.int64(*shape) { 1 }
|
|
1079
|
-
d -
|
|
1204
|
+
d - CATimeCivil.days_from_civil(y, ones, ones) + 1
|
|
1080
1205
|
end
|
|
1081
1206
|
# @!endgroup
|
|
1082
1207
|
|
|
@@ -1122,14 +1247,14 @@ class CATime
|
|
|
1122
1247
|
# calendar unit that means the month / year ordinal, so a :M centroid is
|
|
1123
1248
|
# the centroid of month numbers (a caller wanting the day-space answer
|
|
1124
1249
|
# writes to_unit(:D) first) -- and the result is rounded back to a tick.
|
|
1125
|
-
def
|
|
1126
|
-
|
|
1250
|
+
def reduce_on_own_grid(op, kind, args, opts)
|
|
1251
|
+
round_and_relift(parent.public_send(op, *args, **opts), kind)
|
|
1127
1252
|
end
|
|
1128
1253
|
|
|
1129
1254
|
# Round-to-nearest onto the storage grid, then wear the Face again. An
|
|
1130
1255
|
# Array arrives from the multi-p percentile / quantile shapes; UNDEF and
|
|
1131
1256
|
# nil (empty or all-masked) pass through untouched.
|
|
1132
|
-
def
|
|
1257
|
+
def round_and_relift(r, kind)
|
|
1133
1258
|
case r
|
|
1134
1259
|
when Numeric
|
|
1135
1260
|
v = r.round
|
|
@@ -1142,7 +1267,7 @@ class CATime
|
|
|
1142
1267
|
iv = r.round.mask_invalid.int64
|
|
1143
1268
|
kind == :timedelta ? iv.timedelta(unit: unit) : iv.time(unit: unit)
|
|
1144
1269
|
when Array
|
|
1145
|
-
r.map {|x|
|
|
1270
|
+
r.map {|x| round_and_relift(x, kind)}
|
|
1146
1271
|
else
|
|
1147
1272
|
r # UNDEF / nil passthrough (empty or all-masked reduction)
|
|
1148
1273
|
end
|
|
@@ -1168,21 +1293,25 @@ class CATimedelta
|
|
|
1168
1293
|
end
|
|
1169
1294
|
|
|
1170
1295
|
# @overload to_unit(unit)
|
|
1171
|
-
# Returns the same durations re-expressed on
|
|
1172
|
-
# {CATimedelta} whose storage is `
|
|
1173
|
-
#
|
|
1174
|
-
# (
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1296
|
+
# Returns the same durations re-expressed on the `unit` grid: a new
|
|
1297
|
+
# {CATimedelta} whose storage resolution is `unit`. A finer target is
|
|
1298
|
+
# exact (`:D` -> `:h`); a coarser one drops the sub-`unit` remainder
|
|
1299
|
+
# **toward zero** (`+30 h` -> `+1 D`, `-30 h` -> `-1 D`), the same
|
|
1300
|
+
# truncation `dt + td` already applies.
|
|
1301
|
+
#
|
|
1302
|
+
# The direction differs from {CATime#to_unit} on purpose: a time is a
|
|
1303
|
+
# point on an axis, so it floors toward the past; a duration is a
|
|
1304
|
+
# magnitude, so it shrinks toward zero.
|
|
1305
|
+
#
|
|
1306
|
+
# A calendar / fixed-length pair always raises -- unlike an instant, a
|
|
1307
|
+
# duration of one month has no length in days.
|
|
1177
1308
|
# @param unit [Resolution, Symbol, String] target resolution.
|
|
1178
1309
|
# @return [CATimedelta]
|
|
1179
|
-
# @raise [ArgumentError]
|
|
1180
|
-
# `unit`'s.
|
|
1310
|
+
# @raise [ArgumentError] on a calendar / fixed-length pair.
|
|
1181
1311
|
# @raise [RangeError] when the widened ticks overflow int64.
|
|
1182
1312
|
def to_unit(unit)
|
|
1183
1313
|
to = CATime::Resolution.parse(unit)
|
|
1184
|
-
CATimeUnitAlgebra.
|
|
1185
|
-
CATimeUnitAlgebra.multiple_factor(self.unit, to)).timedelta(unit: to)
|
|
1314
|
+
CATimeUnitAlgebra.convert_scale_trunc(parent, self.unit, to).timedelta(unit: to)
|
|
1186
1315
|
end
|
|
1187
1316
|
|
|
1188
1317
|
# @overload +(other)
|
|
@@ -1195,7 +1324,7 @@ class CATimedelta
|
|
|
1195
1324
|
def +(other)
|
|
1196
1325
|
case other
|
|
1197
1326
|
when CATimedelta
|
|
1198
|
-
u =
|
|
1327
|
+
u = common_duration_unit(other.unit)
|
|
1199
1328
|
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1200
1329
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1201
1330
|
(a + b).timedelta(unit: u)
|
|
@@ -1215,7 +1344,7 @@ class CATimedelta
|
|
|
1215
1344
|
def -(other)
|
|
1216
1345
|
case other
|
|
1217
1346
|
when CATimedelta
|
|
1218
|
-
u =
|
|
1347
|
+
u = common_duration_unit(other.unit)
|
|
1219
1348
|
a = CATimeUnitAlgebra.convert_scale!(parent, unit, u)
|
|
1220
1349
|
b = CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u)
|
|
1221
1350
|
(a - b).timedelta(unit: u)
|
|
@@ -1260,11 +1389,13 @@ class CATimedelta
|
|
|
1260
1389
|
# @raise [TypeError] on incompatible operands.
|
|
1261
1390
|
def /(other)
|
|
1262
1391
|
case other
|
|
1263
|
-
when Integer
|
|
1392
|
+
when Integer
|
|
1393
|
+
CATimeUnitAlgebra.trunc_divide(parent, other).timedelta(unit: unit)
|
|
1264
1394
|
when CATimedelta
|
|
1265
|
-
u =
|
|
1266
|
-
CATimeUnitAlgebra.
|
|
1267
|
-
CATimeUnitAlgebra.convert_scale!(
|
|
1395
|
+
u = common_duration_unit(other.unit)
|
|
1396
|
+
CATimeUnitAlgebra.trunc_divide(
|
|
1397
|
+
CATimeUnitAlgebra.convert_scale!(parent, unit, u),
|
|
1398
|
+
CATimeUnitAlgebra.convert_scale!(other.parent, other.unit, u))
|
|
1268
1399
|
else raise TypeError, "CATimedelta / #{other.class} is not allowed"
|
|
1269
1400
|
end
|
|
1270
1401
|
end
|
|
@@ -1281,14 +1412,14 @@ class CATimedelta
|
|
|
1281
1412
|
# or as a {CATimedelta} view for per-axis reduction.
|
|
1282
1413
|
# @return [Element, CATimedelta]
|
|
1283
1414
|
def sum(*args, **opts)
|
|
1284
|
-
|
|
1415
|
+
round_and_relift(parent.sum(*args, **opts))
|
|
1285
1416
|
end
|
|
1286
1417
|
|
|
1287
1418
|
# @overload mean(axis: nil, **opts)
|
|
1288
1419
|
# Returns the mean duration rounded to the nearest unit count.
|
|
1289
1420
|
# @return [Element, CATimedelta]
|
|
1290
1421
|
def mean(*args, **opts)
|
|
1291
|
-
|
|
1422
|
+
round_and_relift(parent.mean(*args, **opts))
|
|
1292
1423
|
end
|
|
1293
1424
|
|
|
1294
1425
|
# median / percentile / quantile / stddev / stddevp report on this array's
|
|
@@ -1300,7 +1431,7 @@ class CATimedelta
|
|
|
1300
1431
|
# Returns the median duration on `self`'s unit.
|
|
1301
1432
|
# @return [Element, CATimedelta]
|
|
1302
1433
|
def median(*args, **opts)
|
|
1303
|
-
|
|
1434
|
+
round_and_relift(parent.median(*args, **opts))
|
|
1304
1435
|
end
|
|
1305
1436
|
|
|
1306
1437
|
# @overload percentile(*p, axis: nil, **opts)
|
|
@@ -1309,7 +1440,7 @@ class CATimedelta
|
|
|
1309
1440
|
# two or more give an Array).
|
|
1310
1441
|
# @return [Element, CATimedelta, Array<Element>, Array<CATimedelta>]
|
|
1311
1442
|
def percentile(*args, **opts)
|
|
1312
|
-
|
|
1443
|
+
round_and_relift(parent.percentile(*args, **opts))
|
|
1313
1444
|
end
|
|
1314
1445
|
|
|
1315
1446
|
# @overload quantile(axis: nil, **opts)
|
|
@@ -1317,7 +1448,7 @@ class CATimedelta
|
|
|
1317
1448
|
# `self`'s unit.
|
|
1318
1449
|
# @return [Array<Element>, Array<CATimedelta>]
|
|
1319
1450
|
def quantile(*args, **opts)
|
|
1320
|
-
|
|
1451
|
+
round_and_relift(parent.quantile(*args, **opts))
|
|
1321
1452
|
end
|
|
1322
1453
|
|
|
1323
1454
|
# @overload stddev(axis: nil, **opts)
|
|
@@ -1326,7 +1457,7 @@ class CATimedelta
|
|
|
1326
1457
|
# the precision matters.
|
|
1327
1458
|
# @return [Element, CATimedelta]
|
|
1328
1459
|
def stddev(*args, **opts)
|
|
1329
|
-
|
|
1460
|
+
round_and_relift(parent.stddev(*args, **opts))
|
|
1330
1461
|
end
|
|
1331
1462
|
|
|
1332
1463
|
# @overload stddevp(axis: nil, **opts)
|
|
@@ -1334,29 +1465,19 @@ class CATimedelta
|
|
|
1334
1465
|
# {#stddev}.
|
|
1335
1466
|
# @return [Element, CATimedelta]
|
|
1336
1467
|
def stddevp(*args, **opts)
|
|
1337
|
-
|
|
1468
|
+
round_and_relift(parent.stddevp(*args, **opts))
|
|
1338
1469
|
end
|
|
1339
1470
|
|
|
1340
|
-
|
|
1341
|
-
# unit: their value is in squared ticks, and no Face represents squared
|
|
1342
|
-
# time. They are left as the inherited plain Float (unlike
|
|
1343
|
-
# {CATime#variance}, which raises -- a squared instant is meaningless,
|
|
1344
|
-
# while a squared duration is a real quantity, s^2). Read the result as
|
|
1345
|
-
# "in units of `self.unit` squared", or take {#stddev} for a typed answer.
|
|
1346
|
-
|
|
1347
|
-
def _lift_reduced(r)
|
|
1471
|
+
def round_and_relift(r)
|
|
1348
1472
|
case r
|
|
1349
1473
|
when Numeric then Element.new(r.round, unit)
|
|
1350
1474
|
when CArray then r.round.mask_invalid.int64.timedelta(unit: unit)
|
|
1351
|
-
when Array then r.map {|x|
|
|
1475
|
+
when Array then r.map {|x| round_and_relift(x)}
|
|
1352
1476
|
else r
|
|
1353
1477
|
end
|
|
1354
1478
|
end
|
|
1355
|
-
private :
|
|
1479
|
+
private :round_and_relift
|
|
1356
1480
|
|
|
1357
|
-
# min / max ride the core reduce Face gate (ORDERABLE storage descent +
|
|
1358
|
-
# output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
|
|
1359
|
-
# CArray#min / #max return the Element / CATimedelta shapes directly.
|
|
1360
1481
|
# @overload variance(*)
|
|
1361
1482
|
# Not supported: the variance of durations has squared-time units, which
|
|
1362
1483
|
# no type represents -- the same reason {CATime#variance} refuses. Use
|
|
@@ -1373,6 +1494,9 @@ class CATimedelta
|
|
|
1373
1494
|
raise TypeError, "CATimedelta#variancep is ill-defined (squared-time units); use stddevp"
|
|
1374
1495
|
end
|
|
1375
1496
|
|
|
1497
|
+
# min / max ride the core reduce Face gate (ORDERABLE storage descent +
|
|
1498
|
+
# output re-lift, see ext/mkkernel.rb face_gate: :relift); the inherited
|
|
1499
|
+
# CArray#min / #max return the Element / CATimedelta shapes directly.
|
|
1376
1500
|
# minmax has no core gate, so re-lift both extremes here.
|
|
1377
1501
|
|
|
1378
1502
|
# @overload minmax(*axes, **opts)
|
|
@@ -1380,17 +1504,17 @@ class CATimedelta
|
|
|
1380
1504
|
# @return [Array(Element, Element), Array(CATimedelta, CATimedelta)]
|
|
1381
1505
|
def minmax(*args, **opts)
|
|
1382
1506
|
lo, hi = parent.minmax(*args, **opts)
|
|
1383
|
-
[
|
|
1507
|
+
[relift_extremum(lo), relift_extremum(hi)]
|
|
1384
1508
|
end
|
|
1385
1509
|
|
|
1386
|
-
def
|
|
1510
|
+
def relift_extremum(r)
|
|
1387
1511
|
case r
|
|
1388
1512
|
when Integer then Element.new(r, unit)
|
|
1389
1513
|
when CArray then r.timedelta(unit: unit)
|
|
1390
1514
|
else r
|
|
1391
1515
|
end
|
|
1392
1516
|
end
|
|
1393
|
-
private :
|
|
1517
|
+
private :relift_extremum
|
|
1394
1518
|
# @!endgroup
|
|
1395
1519
|
|
|
1396
1520
|
# sort / partition ride the core sort Face gate (ORDERABLE storage
|
|
@@ -1477,7 +1601,7 @@ class CATimedelta
|
|
|
1477
1601
|
# The finer of `self`'s unit and `ou` for a duration + duration / ratio. A
|
|
1478
1602
|
# cross-group pair (a month vs a second) has no common duration grid, so it
|
|
1479
1603
|
# raises rather than mixing calendar and fixed durations.
|
|
1480
|
-
def
|
|
1604
|
+
def common_duration_unit(ou)
|
|
1481
1605
|
unless CATimeUnitAlgebra.same_group?(unit, ou)
|
|
1482
1606
|
raise ArgumentError,
|
|
1483
1607
|
"cannot combine a #{unit} duration with a #{ou} duration " \
|
|
@@ -1487,30 +1611,67 @@ class CATimedelta
|
|
|
1487
1611
|
end
|
|
1488
1612
|
end
|
|
1489
1613
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1614
|
+
# Reads a time literal (Time / DateTime / Integer unix-seconds / String) into
|
|
1615
|
+
# the pieces the CATime constructors need. All parsing is UTC (an explicit
|
|
1616
|
+
# offset is honoured; otherwise UTC) and DateTime-independent (Date._parse +
|
|
1617
|
+
# a civil-date kernel). Internal to the time surface: nothing here is part
|
|
1618
|
+
# of the public API.
|
|
1619
|
+
module CATimeLiteral
|
|
1620
|
+
module_function
|
|
1621
|
+
|
|
1622
|
+
# Date fields of a String literal, UTC. Ruby's Date._parse does not read
|
|
1623
|
+
# the calendar-grid forms CATime#to_s prints -- "2019-09" comes back as a
|
|
1624
|
+
# month of 20 with a zone, and a bare "2019" as a month and a day -- so
|
|
1625
|
+
# those two are read here and everything else is left to Date._parse.
|
|
1626
|
+
#
|
|
1627
|
+
# What Date._parse returns is then checked, because an out-of-range field
|
|
1628
|
+
# used to flow into the civil kernel and normalise into a different date:
|
|
1629
|
+
# "201909" is a valid YYMMDD to Ruby (2020-19-09, which landed on 2021-07),
|
|
1630
|
+
# and "2019-02-31" landed on 2019-03-03. A missing finer field is not an
|
|
1631
|
+
# error -- a year or a year-month names the head of that period.
|
|
1632
|
+
def parse_date_fields(spec, format)
|
|
1633
|
+
h =
|
|
1634
|
+
if format then Date._strptime(spec, format)
|
|
1635
|
+
elsif spec =~ /\A(\d{4})-(\d{1,2})\z/ then { year: $1.to_i, mon: $2.to_i }
|
|
1636
|
+
elsif spec =~ /\A(\d{4})\z/ then { year: $1.to_i }
|
|
1637
|
+
else Date._parse(spec)
|
|
1638
|
+
end
|
|
1639
|
+
unless h && h[:year]
|
|
1640
|
+
raise ArgumentError, "cannot parse time #{spec.inspect}"
|
|
1641
|
+
end
|
|
1642
|
+
y, m, d = h[:year], h[:mon] || 1, h[:mday] || 1
|
|
1643
|
+
unless (1..12).cover?(m) && Date.valid_date?(y, m, d, Date::GREGORIAN)
|
|
1644
|
+
raise ArgumentError,
|
|
1645
|
+
"cannot parse time #{spec.inspect}: it reads as year #{y}, " \
|
|
1646
|
+
"month #{m}, day #{d}, which is not a date"
|
|
1647
|
+
end
|
|
1648
|
+
h
|
|
1649
|
+
end
|
|
1650
|
+
private_class_method :parse_date_fields
|
|
1497
1651
|
|
|
1498
1652
|
# Exact Rational seconds since the Unix epoch for a start literal (Time /
|
|
1499
1653
|
# DateTime / Integer unix-seconds / String). UTC default.
|
|
1500
|
-
def
|
|
1654
|
+
def epoch_seconds(spec, format = nil)
|
|
1501
1655
|
require 'date'
|
|
1502
1656
|
require 'time'
|
|
1503
1657
|
case spec
|
|
1504
1658
|
when Time then spec.to_r
|
|
1505
1659
|
when Integer then Rational(spec)
|
|
1506
|
-
when
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1660
|
+
when CATime::Element
|
|
1661
|
+
# A time element is already an instant, so it needs no parsing: a
|
|
1662
|
+
# fixed unit counts seconds directly, and a calendar one names the
|
|
1663
|
+
# first midnight of its granule (the same instant convert_instant!
|
|
1664
|
+
# gives it). This is what lets floor / ceil / to_unit output feed
|
|
1665
|
+
# straight back into time_range / time_series / .time.
|
|
1666
|
+
if CATimeUnitAlgebra::FIXED.key?(spec.unit.base)
|
|
1667
|
+
Rational(spec.value) * spec.unit.tick_ratio
|
|
1668
|
+
else
|
|
1669
|
+
Rational(CATimeUnitAlgebra.calendar_days_since_epoch(CA_INT64([spec.value]),
|
|
1670
|
+
spec.unit)[0] * 86400)
|
|
1510
1671
|
end
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1672
|
+
when String
|
|
1673
|
+
h = parse_date_fields(spec, format)
|
|
1674
|
+
days = CATimeCivil.days_from_civil_1(h[:year], h[:mon] || 1, h[:mday] || 1)
|
|
1514
1675
|
sec = Rational(days * 86400 + (h[:hour] || 0) * 3600 +
|
|
1515
1676
|
(h[:min] || 0) * 60 + (h[:sec] || 0))
|
|
1516
1677
|
sec += h[:sec_fraction] if h[:sec_fraction]
|
|
@@ -1526,17 +1687,25 @@ class CArray
|
|
|
1526
1687
|
end
|
|
1527
1688
|
|
|
1528
1689
|
# [year, month] (UTC) of a start literal, for a calendar-resolution grid.
|
|
1529
|
-
def
|
|
1690
|
+
def year_month(spec, format = nil)
|
|
1530
1691
|
require 'date'
|
|
1531
1692
|
require 'time'
|
|
1532
1693
|
case spec
|
|
1533
1694
|
when Time then t = spec.utc; [t.year, t.month]
|
|
1534
1695
|
when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
|
|
1535
|
-
when
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1696
|
+
when CATime::Element
|
|
1697
|
+
# A calendar element is a month ordinal already, so read it as one
|
|
1698
|
+
# rather than going out through Time (which a :Y / :M element would
|
|
1699
|
+
# have to re-derive). Integer / and % floor, so a pre-epoch value
|
|
1700
|
+
# lands in the right month.
|
|
1701
|
+
case spec.unit.base
|
|
1702
|
+
when :M then mo = spec.value * spec.unit.count + 1970 * 12
|
|
1703
|
+
[mo / 12, mo % 12 + 1]
|
|
1704
|
+
when :Y then [spec.value * spec.unit.count + 1970, 1]
|
|
1705
|
+
else t = spec.to_time.utc; [t.year, t.month]
|
|
1539
1706
|
end
|
|
1707
|
+
when String
|
|
1708
|
+
h = parse_date_fields(spec, format)
|
|
1540
1709
|
[h[:year], h[:mon] || 1]
|
|
1541
1710
|
else
|
|
1542
1711
|
if defined?(DateTime) && spec.is_a?(DateTime)
|
|
@@ -1548,16 +1717,37 @@ class CArray
|
|
|
1548
1717
|
end
|
|
1549
1718
|
|
|
1550
1719
|
# Tick index of `spec`'s instant on the `res` grid (floor toward the past).
|
|
1551
|
-
def
|
|
1720
|
+
def tick_index(spec, res, format = nil)
|
|
1552
1721
|
if CATimeUnitAlgebra::FIXED.key?(res.base)
|
|
1553
|
-
(
|
|
1722
|
+
(epoch_seconds(spec, format) / res.tick_ratio).floor
|
|
1554
1723
|
else
|
|
1555
|
-
y, m =
|
|
1724
|
+
y, m = year_month(spec, format)
|
|
1556
1725
|
months = (y - 1970) * 12 + (m - 1)
|
|
1557
1726
|
(Rational(months) / res.tick_ratio).floor
|
|
1558
1727
|
end
|
|
1559
1728
|
end
|
|
1560
1729
|
|
|
1730
|
+
# Single-literal build for {.time}: a 1-element CATime, honouring
|
|
1731
|
+
# the on_error policy (raise, or a masked cell).
|
|
1732
|
+
def to_time_array(literal, res, format, on_error)
|
|
1733
|
+
raw = CArray.int64(1)
|
|
1734
|
+
begin
|
|
1735
|
+
raw[0] = tick_index(literal, res, format)
|
|
1736
|
+
rescue ArgumentError, TypeError
|
|
1737
|
+
raise if on_error == :raise
|
|
1738
|
+
raw[0] = UNDEF
|
|
1739
|
+
end
|
|
1740
|
+
raw.time(unit: res)
|
|
1741
|
+
end
|
|
1742
|
+
end
|
|
1743
|
+
|
|
1744
|
+
class CArray
|
|
1745
|
+
# Time is stored as an int64 tick index on a grid whose resolution is
|
|
1746
|
+
# `unit` (= a {CATime::Resolution}, tick = count * base). The value
|
|
1747
|
+
# is the k-th tick since the Unix epoch (1970-01-01 UTC): e.g. unit
|
|
1748
|
+
# `"10 minutes"` value 3 = 1970-01-01T00:30:00Z. Literals are read by
|
|
1749
|
+
# {CATimeLiteral} (UTC).
|
|
1750
|
+
|
|
1561
1751
|
# @overload time_range(start, last, unit:, step: nil, format: nil)
|
|
1562
1752
|
# Returns a {CATime} from `start` to `last` inclusive on the `unit`
|
|
1563
1753
|
# grid, spaced `step` apart. `unit` is the resolution the result is
|
|
@@ -1567,8 +1757,10 @@ class CArray
|
|
|
1567
1757
|
# bucket head (toward the past); the phase is anchored at `start`, and
|
|
1568
1758
|
# `last` is a bound rather than a member -- the series stops at the last
|
|
1569
1759
|
# step at or before it.
|
|
1570
|
-
# @param start [Time, String, Integer, DateTime] first
|
|
1571
|
-
#
|
|
1760
|
+
# @param start [Time, String, Integer, DateTime, CATime::Element] first
|
|
1761
|
+
# instant.
|
|
1762
|
+
# @param last [Time, String, Integer, DateTime, CATime::Element] last
|
|
1763
|
+
# instant (inclusive).
|
|
1572
1764
|
# @param unit [Resolution, Symbol, String] grid resolution (tick).
|
|
1573
1765
|
# @param step [Resolution, Symbol, String, nil] spacing between elements
|
|
1574
1766
|
# (default: one `unit` tick). Must be a whole multiple of `unit`.
|
|
@@ -1581,8 +1773,8 @@ class CArray
|
|
|
1581
1773
|
res = CATime::Resolution.parse(unit)
|
|
1582
1774
|
stride = step.nil? ? 1 :
|
|
1583
1775
|
CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
|
|
1584
|
-
s =
|
|
1585
|
-
e =
|
|
1776
|
+
s = CATimeLiteral.tick_index(start, res, format)
|
|
1777
|
+
e = CATimeLiteral.tick_index(last, res, format)
|
|
1586
1778
|
n = e < s ? 0 : (e - s) / stride + 1
|
|
1587
1779
|
CArray.int64(n) {|i| s + i * stride }.time(unit: res)
|
|
1588
1780
|
end
|
|
@@ -1593,7 +1785,8 @@ class CArray
|
|
|
1593
1785
|
# is stored on and `step` is the spacing, so an hourly grid sampled once
|
|
1594
1786
|
# a day is `unit: :h, step: "1 day"`. With no `step` the spacing is one
|
|
1595
1787
|
# `unit` tick (consecutive ticks, as before).
|
|
1596
|
-
# @param start [Time, String, Integer, DateTime] first
|
|
1788
|
+
# @param start [Time, String, Integer, DateTime, CATime::Element] first
|
|
1789
|
+
# instant.
|
|
1597
1790
|
# @param count [Integer] number of elements.
|
|
1598
1791
|
# @param unit [Resolution, Symbol, String] grid resolution (tick).
|
|
1599
1792
|
# @param step [Resolution, Symbol, String, nil] spacing between elements
|
|
@@ -1607,7 +1800,7 @@ class CArray
|
|
|
1607
1800
|
res = CATime::Resolution.parse(unit)
|
|
1608
1801
|
stride = step.nil? ? 1 :
|
|
1609
1802
|
CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
|
|
1610
|
-
s =
|
|
1803
|
+
s = CATimeLiteral.tick_index(start, res, format)
|
|
1611
1804
|
CArray.int64(count) {|i| s + i * stride }.time(unit: res)
|
|
1612
1805
|
end
|
|
1613
1806
|
|
|
@@ -1622,7 +1815,7 @@ class CArray
|
|
|
1622
1815
|
# pass `on_error: :mask` to make it an UNDEF cell instead. A masked /
|
|
1623
1816
|
# `nil` input cell is a *missing* value (not a parse failure) and always
|
|
1624
1817
|
# becomes UNDEF, regardless of `on_error`.
|
|
1625
|
-
# @param x [Time, String, Integer, DateTime, Array, CArray] a literal, a
|
|
1818
|
+
# @param x [Time, String, Integer, DateTime, CATime::Element, Array, CArray] a literal, a
|
|
1626
1819
|
# Ruby Array of literals, or a CArray of literals.
|
|
1627
1820
|
# @param unit [Resolution, Symbol, String] target grid resolution.
|
|
1628
1821
|
# @param format [String, nil] optional strptime format for String input.
|
|
@@ -1641,7 +1834,7 @@ class CArray
|
|
|
1641
1834
|
end
|
|
1642
1835
|
x = CA_OBJECT(x) if x.is_a?(Array) # Ruby Array of literals -> object CArray
|
|
1643
1836
|
unless x.is_a?(CArray)
|
|
1644
|
-
return
|
|
1837
|
+
return CATimeLiteral.to_time_array(x, res, format, on_error)
|
|
1645
1838
|
end
|
|
1646
1839
|
raw = CArray.int64(*x.shape)
|
|
1647
1840
|
x.each_index do |*idx|
|
|
@@ -1651,7 +1844,7 @@ class CArray
|
|
|
1651
1844
|
next
|
|
1652
1845
|
end
|
|
1653
1846
|
begin
|
|
1654
|
-
raw[*idx] =
|
|
1847
|
+
raw[*idx] = CATimeLiteral.tick_index(s, res, format)
|
|
1655
1848
|
rescue ArgumentError, TypeError
|
|
1656
1849
|
raise if on_error == :raise
|
|
1657
1850
|
raw[*idx] = UNDEF # opt-in parse-mask
|
|
@@ -1660,19 +1853,6 @@ class CArray
|
|
|
1660
1853
|
raw.time(unit: res)
|
|
1661
1854
|
end
|
|
1662
1855
|
|
|
1663
|
-
# Single-literal build for {.time}: a 1-element CATime, honouring
|
|
1664
|
-
# the on_error policy (raise, or a masked cell).
|
|
1665
|
-
def self._time_cell(literal, res, format, on_error)
|
|
1666
|
-
raw = CArray.int64(1)
|
|
1667
|
-
begin
|
|
1668
|
-
raw[0] = _epoch_tick_index(literal, res, format)
|
|
1669
|
-
rescue ArgumentError, TypeError
|
|
1670
|
-
raise if on_error == :raise
|
|
1671
|
-
raw[0] = UNDEF
|
|
1672
|
-
end
|
|
1673
|
-
raw.time(unit: res)
|
|
1674
|
-
end
|
|
1675
|
-
|
|
1676
1856
|
# @overload time(unit: :ns, origin: nil)
|
|
1677
1857
|
# Returns `self` as a {CATime} on the `unit` grid. With `origin`
|
|
1678
1858
|
# nil, `self`'s int64 values are taken as tick indices already anchored
|
|
@@ -1680,7 +1860,7 @@ class CArray
|
|
|
1680
1860
|
# indices are relative to `origin` and are rebased to the epoch (a new
|
|
1681
1861
|
# int64 array is built; the origin is not stored).
|
|
1682
1862
|
# @param unit [Resolution, Symbol, String] grid resolution.
|
|
1683
|
-
# @param origin [Time, String, Integer, DateTime, nil] base instant that
|
|
1863
|
+
# @param origin [Time, String, Integer, DateTime, CATime::Element, nil] base instant that
|
|
1684
1864
|
# `self`'s indices are counted from (default: the Unix epoch).
|
|
1685
1865
|
# @return [CATime]
|
|
1686
1866
|
# @note An `int64` receiver is wrapped zero-copy. A narrower integer type
|
|
@@ -1688,11 +1868,11 @@ class CArray
|
|
|
1688
1868
|
# raises (cast it explicitly if the truncation is intended).
|
|
1689
1869
|
def time(unit: :ns, origin: nil)
|
|
1690
1870
|
res = CATime::Resolution.parse(unit)
|
|
1691
|
-
src =
|
|
1871
|
+
src = as_int64_time_storage
|
|
1692
1872
|
if origin.nil?
|
|
1693
1873
|
CATime.wrap(src, unit: res)
|
|
1694
1874
|
else
|
|
1695
|
-
o =
|
|
1875
|
+
o = CATimeLiteral.tick_index(origin, res)
|
|
1696
1876
|
(src + o).time(unit: res)
|
|
1697
1877
|
end
|
|
1698
1878
|
end
|
|
@@ -1705,13 +1885,13 @@ class CArray
|
|
|
1705
1885
|
# @note An `int64` receiver is wrapped zero-copy; a narrower integer type
|
|
1706
1886
|
# is widened to `int64` first (a copy); a `Float` / non-integer raises.
|
|
1707
1887
|
def timedelta(unit: :ns)
|
|
1708
|
-
CATimedelta.wrap(
|
|
1888
|
+
CATimedelta.wrap(as_int64_time_storage, unit: unit)
|
|
1709
1889
|
end
|
|
1710
1890
|
|
|
1711
1891
|
# Coerce the receiver to the int64 storage a time / timedelta Face needs:
|
|
1712
1892
|
# int64 passes through (zero-copy), a narrower integer type widens losslessly
|
|
1713
1893
|
# to int64 (a copy), and any non-integer (Float, boolean, object, ...) raises.
|
|
1714
|
-
def
|
|
1894
|
+
def as_int64_time_storage
|
|
1715
1895
|
return self if data_type == CA_INT64
|
|
1716
1896
|
unless integer?
|
|
1717
1897
|
raise TypeError,
|
|
@@ -1721,7 +1901,7 @@ class CArray
|
|
|
1721
1901
|
end
|
|
1722
1902
|
int64
|
|
1723
1903
|
end
|
|
1724
|
-
private :
|
|
1904
|
+
private :as_int64_time_storage
|
|
1725
1905
|
end
|
|
1726
1906
|
|
|
1727
1907
|
# ============================================================================
|
|
@@ -1740,6 +1920,198 @@ end
|
|
|
1740
1920
|
# See devel/PROPOSAL_DATETIME64_STEP_SYSTEM.md.
|
|
1741
1921
|
# ============================================================================
|
|
1742
1922
|
|
|
1923
|
+
# Places a bucket grid on a storage resolution: resolves a timestep into whole
|
|
1924
|
+
# storage ticks, resolves an origin into a tick count (or a month ordinal on
|
|
1925
|
+
# the calendar path), and guards both against int64 overflow. Internal to the
|
|
1926
|
+
# time surface: nothing here is part of the public API.
|
|
1927
|
+
module CATimeGrid
|
|
1928
|
+
module_function
|
|
1929
|
+
|
|
1930
|
+
# Base units that are day-or-finer (a period head is representable exactly;
|
|
1931
|
+
# the civil path targets these for a calendar bucket).
|
|
1932
|
+
SU_LE_DAY = %i[D h m s ms us ns ps fs as].freeze
|
|
1933
|
+
|
|
1934
|
+
# Smallest value an int64 storage tick can hold.
|
|
1935
|
+
INT64_MIN = -(2**63)
|
|
1936
|
+
# Largest value an int64 storage tick can hold.
|
|
1937
|
+
INT64_MAX = 2**63 - 1
|
|
1938
|
+
|
|
1939
|
+
# Raise (rather than let an int64 CArray operand silently wrap) if a
|
|
1940
|
+
# Ruby-domain quantity is out of int64 range. Returns the value on success
|
|
1941
|
+
# so it composes inline.
|
|
1942
|
+
def check_int64_range(v, what)
|
|
1943
|
+
if v < INT64_MIN || v > INT64_MAX
|
|
1944
|
+
raise RangeError,
|
|
1945
|
+
"time step: #{what} = #{v} overflows int64 " \
|
|
1946
|
+
"(the storage unit is too fine for this step / origin / span; " \
|
|
1947
|
+
"use a coarser unit)"
|
|
1948
|
+
end
|
|
1949
|
+
v
|
|
1950
|
+
end
|
|
1951
|
+
|
|
1952
|
+
# Return [mul, step_ticks, origin_ticks] (all Integer) for the integer
|
|
1953
|
+
# path -- step_ticks and origin_ticks in numerator ticks, the finer of
|
|
1954
|
+
# the storage and step grids (see #resolve_timestep_grid). The calendar path is
|
|
1955
|
+
# handled by the caller.
|
|
1956
|
+
def resolve_integer_grid(step_res, storage_res, origin)
|
|
1957
|
+
mul, step_ticks = resolve_step_scale(step_res, storage_res)
|
|
1958
|
+
[mul, step_ticks, resolve_origin_ticks(origin, step_res,
|
|
1959
|
+
mul == 1 ? storage_res : step_res)]
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
# Storage ticks per day of `storage_res` (must be a whole number, else the
|
|
1963
|
+
# storage grid does not tile a day -- a calendar bucket is unrepresentable).
|
|
1964
|
+
def ticks_per_day(storage_res)
|
|
1965
|
+
r = CATimeUnitAlgebra.ratio(CATime::Resolution.new(1, :D), storage_res)
|
|
1966
|
+
unless r.denominator == 1
|
|
1967
|
+
raise ArgumentError,
|
|
1968
|
+
"cannot place a calendar bucket on storage resolution " \
|
|
1969
|
+
"#{storage_res} (its tick does not tile a day)"
|
|
1970
|
+
end
|
|
1971
|
+
check_int64_range(r.numerator, "ticks per day for #{storage_res}")
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
# Storage ticks of the bucket head at timestep `k` (int64 CArray) for a
|
|
1975
|
+
# calendar bucket: ym0 + k*count months -> day 1 of that month -> ticks.
|
|
1976
|
+
def calendar_bucket_head_ticks(k, st, storage_res, origin)
|
|
1977
|
+
tpd = ticks_per_day(storage_res)
|
|
1978
|
+
count = st.count * (st.base == :Y ? 12 : 1)
|
|
1979
|
+
ym0 = origin_month_ordinal(origin)
|
|
1980
|
+
ymk = ym0 + k * count
|
|
1981
|
+
yy = ymk / 12
|
|
1982
|
+
mm = ymk - yy * 12 + 1
|
|
1983
|
+
CATimeCivil.days_from_civil(yy, mm, CArray.int64(*k.shape) { 1 }) * tpd
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
# Month ordinal (year*12 + month-1) of origin; day / time ignored.
|
|
1987
|
+
# Default (nil) is the epoch month 1970-01.
|
|
1988
|
+
def origin_month_ordinal(origin)
|
|
1989
|
+
return 1970 * 12 if origin.nil?
|
|
1990
|
+
y, m = check_calendar_origin(origin, nil)
|
|
1991
|
+
y * 12 + (m - 1)
|
|
1992
|
+
end
|
|
1993
|
+
|
|
1994
|
+
# [mul, step_ticks] for the integer path, or :civil (calendar bucket on
|
|
1995
|
+
# day-or-finer storage), or raise. Bucket arithmetic runs in the finer of
|
|
1996
|
+
# the two grids, so exactly one of the pair is > 1:
|
|
1997
|
+
# - bucket at or coarser than the storage tick (a :h bucket on :s
|
|
1998
|
+
# storage) -> [1, N]: N storage ticks per bucket.
|
|
1999
|
+
# - bucket finer than the storage tick (a :h bucket on :D storage) ->
|
|
2000
|
+
# [N, 1]: one storage tick spans N buckets exactly, so a timestep is a
|
|
2001
|
+
# plain widening. This needs the storage tick to be a *whole* multiple
|
|
2002
|
+
# of the bucket tick; a partial multiple ("90 minutes" storage against
|
|
2003
|
+
# an :h bucket) has no integer timestep and raises.
|
|
2004
|
+
def resolve_step_scale(step_res, storage_res)
|
|
2005
|
+
r = CATimeUnitAlgebra.ratio(step_res, storage_res) # storage ticks / step tick
|
|
2006
|
+
if r
|
|
2007
|
+
what = "bucket #{step_res} in ticks of #{storage_res}"
|
|
2008
|
+
return [1, check_int64_range(r.numerator, what)] if r.denominator == 1
|
|
2009
|
+
return [check_int64_range(r.denominator, what), 1] if r.numerator == 1
|
|
2010
|
+
end
|
|
2011
|
+
if CATimeUnitAlgebra::CALENDAR.key?(step_res.base) && SU_LE_DAY.include?(storage_res.base)
|
|
2012
|
+
return :civil
|
|
2013
|
+
end
|
|
2014
|
+
raise ArgumentError,
|
|
2015
|
+
"cannot express bucket #{step_res} on storage resolution #{storage_res} " \
|
|
2016
|
+
"(neither is a whole multiple of the other)"
|
|
2017
|
+
end
|
|
2018
|
+
|
|
2019
|
+
# origin -> integer tick count in storage ticks. Default (nil) is the
|
|
2020
|
+
# epoch, except a week bucket defaults to ISO Monday (1970-01-05). A lossy
|
|
2021
|
+
# conversion (origin not landing exactly on the storage grid) raises; a
|
|
2022
|
+
# bare Integer is rejected (epoch-dependent, ambiguous).
|
|
2023
|
+
def resolve_origin_ticks(origin, step_res, storage_res)
|
|
2024
|
+
if origin.nil?
|
|
2025
|
+
if step_res.base == :W && SU_LE_DAY.include?(storage_res.base)
|
|
2026
|
+
return check_int64_range(ticks_per_day(storage_res) * 4,
|
|
2027
|
+
"ISO-Monday week origin in ticks of #{storage_res}")
|
|
2028
|
+
end
|
|
2029
|
+
return 0
|
|
2030
|
+
end
|
|
2031
|
+
if CATimeUnitAlgebra::FIXED.key?(storage_res.base)
|
|
2032
|
+
secs = origin_seconds_exact(origin)
|
|
2033
|
+
tick = secs / storage_res.tick_ratio # Rational
|
|
2034
|
+
unless tick.denominator == 1
|
|
2035
|
+
raise ArgumentError,
|
|
2036
|
+
"origin #{origin.inspect} is not representable losslessly " \
|
|
2037
|
+
"in storage resolution #{storage_res} (would truncate the grid phase)"
|
|
2038
|
+
end
|
|
2039
|
+
check_int64_range(tick.numerator, "origin #{origin.inspect} in ticks of #{storage_res}")
|
|
2040
|
+
else # :Y / :M storage
|
|
2041
|
+
y, m = check_calendar_origin(origin, storage_res)
|
|
2042
|
+
ord = storage_res.base == :Y ? (y - 1970) : (y * 12 + (m - 1) - 1970 * 12)
|
|
2043
|
+
if storage_res.count > 1
|
|
2044
|
+
unless (ord % storage_res.count).zero?
|
|
2045
|
+
raise ArgumentError,
|
|
2046
|
+
"origin #{origin.inspect} is not on the #{storage_res} grid"
|
|
2047
|
+
end
|
|
2048
|
+
ord /= storage_res.count
|
|
2049
|
+
end
|
|
2050
|
+
ord
|
|
2051
|
+
end
|
|
2052
|
+
end
|
|
2053
|
+
|
|
2054
|
+
# Exact Rational seconds since the Unix epoch for a fixed-storage origin.
|
|
2055
|
+
# Everything but a bare Integer goes through the shared entry point, so
|
|
2056
|
+
# an origin reads the same here as it does for a start literal.
|
|
2057
|
+
def origin_seconds_exact(origin)
|
|
2058
|
+
case origin
|
|
2059
|
+
when Integer
|
|
2060
|
+
raise ArgumentError,
|
|
2061
|
+
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2062
|
+
"Time / String / CATime scalar"
|
|
2063
|
+
else
|
|
2064
|
+
CATimeLiteral.epoch_seconds(origin) # Time / String / DateTime / Element
|
|
2065
|
+
end
|
|
2066
|
+
end
|
|
2067
|
+
|
|
2068
|
+
# Whether `origin` sits exactly on the head of its month (the 1st at
|
|
2069
|
+
# 00:00 UTC). A calendar grid is addressed by month ordinal, so its
|
|
2070
|
+
# bucket heads are month heads and nothing else; an origin anywhere in
|
|
2071
|
+
# between names a bucket that does not exist.
|
|
2072
|
+
def origin_on_month_head?(origin)
|
|
2073
|
+
# A calendar element is a month head by construction, and the shared
|
|
2074
|
+
# entry point says so by handing back that midnight -- no branch here
|
|
2075
|
+
# has to know it.
|
|
2076
|
+
secs = CATimeLiteral.epoch_seconds(origin)
|
|
2077
|
+
y, m = origin_year_month(origin)
|
|
2078
|
+
head = CATimeCivil.days_from_civil(CA_INT64([y]), CA_INT64([m]), CA_INT64([1]))[0]
|
|
2079
|
+
secs == head * 86400
|
|
2080
|
+
end
|
|
2081
|
+
|
|
2082
|
+
# Raise unless `origin` can be the head of bucket 0 on a calendar grid:
|
|
2083
|
+
# it has to be a month head, and a :Y tick starts in January. The day
|
|
2084
|
+
# and time would otherwise be dropped silently -- the same loss a fixed
|
|
2085
|
+
# unit already refuses (see resolve_origin_ticks).
|
|
2086
|
+
def check_calendar_origin(origin, storage_res)
|
|
2087
|
+
return if origin.nil?
|
|
2088
|
+
y, m = origin_year_month(origin)
|
|
2089
|
+
unless origin_on_month_head?(origin)
|
|
2090
|
+
raise ArgumentError,
|
|
2091
|
+
"origin #{origin.inspect} is not a month head: a calendar grid " \
|
|
2092
|
+
"is addressed by month, so its origin must be the 1st at 00:00"
|
|
2093
|
+
end
|
|
2094
|
+
if storage_res && storage_res.base == :Y && m != 1
|
|
2095
|
+
raise ArgumentError,
|
|
2096
|
+
"origin #{origin.inspect} is not on the #{storage_res} grid " \
|
|
2097
|
+
"(a #{storage_res} tick starts in January)"
|
|
2098
|
+
end
|
|
2099
|
+
[y, m]
|
|
2100
|
+
end
|
|
2101
|
+
|
|
2102
|
+
# [year, month] of a calendar-storage origin (finer fields ignored).
|
|
2103
|
+
def origin_year_month(origin)
|
|
2104
|
+
case origin
|
|
2105
|
+
when Integer
|
|
2106
|
+
raise ArgumentError,
|
|
2107
|
+
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2108
|
+
"Time / String / CATime scalar"
|
|
2109
|
+
else
|
|
2110
|
+
CATimeLiteral.year_month(origin) # Time / String / DateTime / Element
|
|
2111
|
+
end
|
|
2112
|
+
end
|
|
2113
|
+
end
|
|
2114
|
+
|
|
1743
2115
|
class CATime
|
|
1744
2116
|
# A tick resolution: `count` ticks of a `base` unit. The human surface is
|
|
1745
2117
|
# a String ("3 hours" / "1 month"); a bare Symbol (:h) is the count-1
|
|
@@ -1802,9 +2174,10 @@ class CATime
|
|
|
1802
2174
|
|
|
1803
2175
|
def initialize(count, base)
|
|
1804
2176
|
raise ArgumentError, "unit count must be >= 1 (got #{count})" if count < 1
|
|
1805
|
-
#
|
|
1806
|
-
#
|
|
1807
|
-
#
|
|
2177
|
+
# Only integer multiples are well-defined (calendar Y/M bases are
|
|
2178
|
+
# month-ordinal linear, so a fractional multiple has no meaning).
|
|
2179
|
+
# Resolution.parse always hands in an Integer; a fractional count via
|
|
2180
|
+
# a direct `new` is not guarded here.
|
|
1808
2181
|
@count = count
|
|
1809
2182
|
@base = base
|
|
1810
2183
|
freeze
|
|
@@ -1826,9 +2199,180 @@ class CATime
|
|
|
1826
2199
|
def inspect = "#<CATime::Resolution #{count} #{base}>"
|
|
1827
2200
|
end
|
|
1828
2201
|
|
|
1829
|
-
#
|
|
1830
|
-
#
|
|
1831
|
-
|
|
2202
|
+
# A tick resolution *and where its ticks are anchored*: the `(unit:,
|
|
2203
|
+
# origin:)` pair that {#timesteps}, {#snap} and {.from_timesteps} already
|
|
2204
|
+
# take, given a name and a value.
|
|
2205
|
+
#
|
|
2206
|
+
# {Resolution} says how wide a tick is; Grid says that and where tick 0
|
|
2207
|
+
# starts. A CATime is stored as epoch-anchored int64 either way, so this
|
|
2208
|
+
# reifies a calling convention -- it does not add a second time
|
|
2209
|
+
# representation, and the reference-time epoch stays out of storage.
|
|
2210
|
+
#
|
|
2211
|
+
# g = CATime::Grid.parse("12 hours since 2017-11-30 09:00")
|
|
2212
|
+
# t.snap(g, direction: :floor) # the pair, passed once
|
|
2213
|
+
# t.timesteps(g) # which tick each element falls in
|
|
2214
|
+
# g.at(CA_INT64([0, 1, 2])) # and back
|
|
2215
|
+
#
|
|
2216
|
+
# Proleptic Gregorian, UTC. There is no calendar seam here on purpose: a
|
|
2217
|
+
# swappable calendar taxes every operation that touches a time, and the
|
|
2218
|
+
# benefit does not follow.
|
|
2219
|
+
class Grid
|
|
2220
|
+
|
|
2221
|
+
# udunits' origin-shift operators, all equivalent: the words `since`,
|
|
2222
|
+
# `after`, `from`, `ref` (whitespace-separated) and the `@` sign, which
|
|
2223
|
+
# needs none ("seconds@1970-01-01"). Generous on input because a
|
|
2224
|
+
# `units` attribute is written by whoever wrote the file; {#to_s} only
|
|
2225
|
+
# ever emits `since`, which is what CF-conforming files use.
|
|
2226
|
+
ORIGIN_SHIFT = /\A(.+?)(?:\s+(?:since|after|from|ref)\s+|\s*@\s*)(.+)\z/i
|
|
2227
|
+
|
|
2228
|
+
# Plural unit words, the spelling {Resolution.parse} reads back. (Bare
|
|
2229
|
+
# Resolution#to_s does not round-trip: it prints "h" / "12 h", and
|
|
2230
|
+
# Resolution.parse takes neither.)
|
|
2231
|
+
WORD = { Y: "years", M: "months", W: "weeks", D: "days", h: "hours",
|
|
2232
|
+
m: "minutes", s: "seconds", ms: "milliseconds",
|
|
2233
|
+
us: "microseconds", ns: "nanoseconds", ps: "picoseconds",
|
|
2234
|
+
fs: "femtoseconds", as: "attoseconds" }.freeze
|
|
2235
|
+
|
|
2236
|
+
# Storage resolutions to fall back on when the unit's own tick cannot
|
|
2237
|
+
# hold the origin exactly (a day grid anchored at 12:00 needs seconds).
|
|
2238
|
+
FALLBACK_BASES = %i[s ms us ns].freeze
|
|
2239
|
+
|
|
2240
|
+
# @param spec ["<unit> since <instant>", unit spec, Grid]
|
|
2241
|
+
# @param origin [Time, String, DateTime, Element, nil] when `spec`
|
|
2242
|
+
# carries no origin-shift clause.
|
|
2243
|
+
# @return [Grid]
|
|
2244
|
+
def self.parse(spec, origin: nil)
|
|
2245
|
+
return spec if spec.is_a?(Grid)
|
|
2246
|
+
if spec.is_a?(String) and (fields = ORIGIN_SHIFT.match(spec))
|
|
2247
|
+
new(fields[1], fields[2])
|
|
2248
|
+
else
|
|
2249
|
+
new(spec, origin)
|
|
2250
|
+
end
|
|
2251
|
+
end
|
|
2252
|
+
|
|
2253
|
+
# Normalizes the three ways a grid reaches a method -- positionally, as
|
|
2254
|
+
# `unit:`, or as the loose `(unit:, origin:)` pair -- into that pair.
|
|
2255
|
+
# One call at the head of a method is the whole Grid arm.
|
|
2256
|
+
# @return [Array(Object, Object)] (unit, origin).
|
|
2257
|
+
def self.resolve(grid, unit, origin)
|
|
2258
|
+
return [grid.unit, grid.origin] if grid.is_a?(Grid)
|
|
2259
|
+
return [unit.unit, unit.origin] if unit.is_a?(Grid)
|
|
2260
|
+
unless grid.nil?
|
|
2261
|
+
raise ArgumentError,
|
|
2262
|
+
"the positional argument must be a CATime::Grid (got #{grid.class})"
|
|
2263
|
+
end
|
|
2264
|
+
[unit, origin]
|
|
2265
|
+
end
|
|
2266
|
+
|
|
2267
|
+
def initialize(unit, origin = nil)
|
|
2268
|
+
@unit = Resolution.parse(unit)
|
|
2269
|
+
@origin = origin
|
|
2270
|
+
@storage = resolve_storage
|
|
2271
|
+
@origin = CArray.time(origin, unit: @storage)[0] if origin
|
|
2272
|
+
freeze
|
|
2273
|
+
end
|
|
2274
|
+
|
|
2275
|
+
# @return [Resolution] the tick this grid counts in.
|
|
2276
|
+
attr_reader :unit
|
|
2277
|
+
|
|
2278
|
+
# @return [Element, nil] the instant tick 0 starts at. nil is the epoch,
|
|
2279
|
+
# which is what every `origin:` keyword already defaults to.
|
|
2280
|
+
attr_reader :origin
|
|
2281
|
+
|
|
2282
|
+
# @return [Resolution] the resolution an array on this grid is stored on:
|
|
2283
|
+
# the unit itself, or finer when the origin's phase needs it. Derived
|
|
2284
|
+
# from (unit, origin) and memoized here, so a grid's identity stays the
|
|
2285
|
+
# (count, unit, origin) triple.
|
|
2286
|
+
attr_reader :storage
|
|
2287
|
+
|
|
2288
|
+
# @return [Boolean] whether the unit is a calendar one (:Y / :M), whose
|
|
2289
|
+
# ticks are month ordinals rather than a fixed number of seconds.
|
|
2290
|
+
def calendar? = %i[Y M].include?(@unit.base)
|
|
2291
|
+
|
|
2292
|
+
# @return [Integer] storage ticks per unit tick.
|
|
2293
|
+
def step_ticks = Integer(@unit.tick_ratio / @storage.tick_ratio)
|
|
2294
|
+
|
|
2295
|
+
# @return [Integer] storage ticks from the epoch to the origin.
|
|
2296
|
+
def origin_ticks
|
|
2297
|
+
@origin ? Integer(CATimeLiteral.epoch_seconds(@origin) / @storage.tick_ratio) : 0
|
|
2298
|
+
end
|
|
2299
|
+
|
|
2300
|
+
# Tick indices -> instants: the {CATime.from_timesteps} direction, but
|
|
2301
|
+
# answering on {#storage} so a phased origin survives.
|
|
2302
|
+
# @param k [CArray, Integer]
|
|
2303
|
+
# @return [CATime, Element]
|
|
2304
|
+
def at(k)
|
|
2305
|
+
return at(CArray.int64(1) { Integer(k) })[0] unless k.is_a?(CArray)
|
|
2306
|
+
return CATime.from_timesteps(k.int64, unit: @unit, origin: @origin) if calendar?
|
|
2307
|
+
(origin_ticks + k.int64 * step_ticks).time(unit: @storage)
|
|
2308
|
+
end
|
|
2309
|
+
|
|
2310
|
+
# Instants -> tick indices: the {CATime#timesteps} direction. Off-grid
|
|
2311
|
+
# elements floor toward the past; ask {#on} when that would be a lie.
|
|
2312
|
+
# @return [CArray] int64.
|
|
2313
|
+
def index(time) = time.timesteps(self)
|
|
2314
|
+
|
|
2315
|
+
# @return [CArray] boolean, flagging elements that land on a tick.
|
|
2316
|
+
def on(time) = time.is_righttime(self)
|
|
2317
|
+
|
|
2318
|
+
# @return [Boolean] whether every element lands on a tick.
|
|
2319
|
+
def on?(time) = on(time).all
|
|
2320
|
+
|
|
2321
|
+
# A regular series on this grid.
|
|
2322
|
+
def range(start, last) = CArray.time_range(start, last, unit: @storage, step: @unit)
|
|
2323
|
+
# A regular series on this grid: +count+ elements from +start+, one per
|
|
2324
|
+
# tick.
|
|
2325
|
+
def series(start, count:) = CArray.time_series(start, count: count, unit: @storage, step: @unit)
|
|
2326
|
+
|
|
2327
|
+
# @return [String] "12 hours since 2017-11-30 09:00:00", which {.parse}
|
|
2328
|
+
# reads back into an equal grid.
|
|
2329
|
+
def to_s
|
|
2330
|
+
word = WORD.fetch(@unit.base)
|
|
2331
|
+
spec = @unit.count == 1 ? word : "#{@unit.count} #{word}"
|
|
2332
|
+
@origin ? "#{spec} since #{@origin.to_time.strftime('%Y-%m-%d %H:%M:%S')}" : spec
|
|
2333
|
+
end
|
|
2334
|
+
|
|
2335
|
+
# @return [String] the {#to_s} spec, wrapped for the console.
|
|
2336
|
+
def inspect = "#<CATime::Grid #{self}>"
|
|
2337
|
+
|
|
2338
|
+
# Two grids are equal when they place ticks in the same places: same
|
|
2339
|
+
# timestep, same origin.
|
|
2340
|
+
# @return [Boolean]
|
|
2341
|
+
def ==(other) = other.is_a?(Grid) && other.unit == @unit && other.to_s == to_s
|
|
2342
|
+
alias eql? ==
|
|
2343
|
+
|
|
2344
|
+
# Hashes with {#==}, so a grid works as a Hash key.
|
|
2345
|
+
# @return [Integer]
|
|
2346
|
+
def hash = [@unit, to_s].hash
|
|
2347
|
+
|
|
2348
|
+
private
|
|
2349
|
+
|
|
2350
|
+
# The coarsest storage holding both the unit's tick and the origin's
|
|
2351
|
+
# phase without truncation. A calendar grid is addressed by month, so
|
|
2352
|
+
# its origin is already pinned to a month head and the unit serves.
|
|
2353
|
+
def resolve_storage
|
|
2354
|
+
return @unit if calendar?
|
|
2355
|
+
candidates = [@unit, *FALLBACK_BASES.map {|base| Resolution.new(1, base) }]
|
|
2356
|
+
candidates.each do |res|
|
|
2357
|
+
next unless (@unit.tick_ratio / res.tick_ratio).denominator == 1
|
|
2358
|
+
next unless @origin.nil? ||
|
|
2359
|
+
(CATimeLiteral.epoch_seconds(@origin) / res.tick_ratio).denominator == 1
|
|
2360
|
+
return res
|
|
2361
|
+
end
|
|
2362
|
+
raise ArgumentError,
|
|
2363
|
+
"origin #{@origin.inspect} cannot be held exactly on any storage " \
|
|
2364
|
+
"resolution for unit #{@unit}"
|
|
2365
|
+
end
|
|
2366
|
+
end
|
|
2367
|
+
|
|
2368
|
+
# Rounding rules {#snap} accepts, the same three CArray#snap takes.
|
|
2369
|
+
SNAP_DIRECTIONS = %i[round floor ceil].freeze
|
|
2370
|
+
|
|
2371
|
+
# The grid this array is stored on: its own tick, anchored at the epoch.
|
|
2372
|
+
# `t.timesteps` is `t.timesteps(t.grid)`, which is what the `unit:` default
|
|
2373
|
+
# has always meant.
|
|
2374
|
+
# @return [Grid]
|
|
2375
|
+
def grid = Grid.new(unit)
|
|
1832
2376
|
|
|
1833
2377
|
# @overload timesteps(unit: self.unit, origin: nil)
|
|
1834
2378
|
# Returns the integer timestep of every element: the k-th `unit`-wide
|
|
@@ -1840,17 +2384,23 @@ class CATime
|
|
|
1840
2384
|
# {#ticks}, but a fresh array rather than the live storage).
|
|
1841
2385
|
# @param unit [String, Symbol, Resolution] bucket resolution
|
|
1842
2386
|
# (default: this array's own storage resolution).
|
|
1843
|
-
# @param origin [Time, String, CATime::Element, DateTime, nil]
|
|
1844
|
-
#
|
|
2387
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] head of
|
|
2388
|
+
# bucket 0 (default: the Unix epoch, or ISO Monday for a week bucket
|
|
2389
|
+
# on day-or-finer storage -- a week grid counts from the epoch
|
|
2390
|
+
# Thursday and cannot hold a Monday, so it keeps its own ticks).
|
|
2391
|
+
# It has to be a bucket head itself: on a calendar grid, the 1st at
|
|
2392
|
+
# 00:00.
|
|
1845
2393
|
# @return [CArray] int64 timesteps.
|
|
1846
2394
|
# @raise [ArgumentError] on a sub-resolution / unrepresentable (unit,
|
|
1847
2395
|
# storage-resolution) pair or a lossy origin.
|
|
1848
|
-
def timesteps(unit:
|
|
1849
|
-
|
|
1850
|
-
|
|
2396
|
+
def timesteps(grid = nil, unit: nil, origin: nil)
|
|
2397
|
+
unit, origin = Grid.resolve(grid, unit, origin)
|
|
2398
|
+
unit = self.unit if unit.nil?
|
|
2399
|
+
g = resolve_timestep_grid(unit, origin)
|
|
2400
|
+
return calendar_bucket(:index, g[1], origin) if g[0] == :civil
|
|
1851
2401
|
su, mul, step_ticks, o = g
|
|
1852
|
-
|
|
1853
|
-
d =
|
|
2402
|
+
raise_if_int64_overflow(su, mul, o)
|
|
2403
|
+
d = storage_ticks_in_numerator_grid(mul) - o
|
|
1854
2404
|
q = d / step_ticks
|
|
1855
2405
|
q - (d - q * step_ticks).lt(0) # floor-div correction (%-independent)
|
|
1856
2406
|
end
|
|
@@ -1859,54 +2409,83 @@ class CATime
|
|
|
1859
2409
|
# Returns each element floored to its bucket head (toward the past), as a
|
|
1860
2410
|
# {CATime} in the same storage resolution.
|
|
1861
2411
|
# @param unit [String, Symbol, Resolution] bucket resolution.
|
|
1862
|
-
# @param origin [Time, String, CATime::Element, DateTime, nil]
|
|
2412
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] head of
|
|
2413
|
+
# bucket 0 (default: the Unix epoch, or ISO Monday for a week bucket
|
|
2414
|
+
# on day-or-finer storage -- a week grid counts from the epoch
|
|
2415
|
+
# Thursday and cannot hold a Monday, so it keeps its own ticks).
|
|
2416
|
+
# It has to be a bucket head itself: on a calendar grid, the 1st at
|
|
2417
|
+
# 00:00.
|
|
2418
|
+
# @return [CATime]
|
|
2419
|
+
# @overload snap(grid = nil, unit:, origin: nil, direction: :round)
|
|
2420
|
+
# Returns each element snapped to a point on the bucket grid, as a
|
|
2421
|
+
# {CATime} in the same storage resolution. This is the time-domain twin
|
|
2422
|
+
# of {CArray#snap}: there, `(step, offset:, direction:)`; here a {Grid}
|
|
2423
|
+
# (= tick + origin), or the same pair spelled as `unit:` / `origin:`,
|
|
2424
|
+
# and the same three directions.
|
|
2425
|
+
#
|
|
2426
|
+
# `:round` picks the nearest bucket head, ties toward the future;
|
|
2427
|
+
# `:floor` the head at or before; `:ceil` the head at or after.
|
|
2428
|
+
# {#floor}, {#ceil} and {#round} are this method with `direction:` fixed.
|
|
2429
|
+
# @param grid [Grid, nil] the bucket grid, passed as one value.
|
|
2430
|
+
# @param unit [String, Symbol, Resolution, Grid] bucket resolution.
|
|
2431
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] head of
|
|
2432
|
+
# bucket 0 (default: the Unix epoch, or ISO Monday for a week bucket
|
|
2433
|
+
# on day-or-finer storage -- a week grid counts from the epoch
|
|
2434
|
+
# Thursday and cannot hold a Monday, so it keeps its own ticks).
|
|
2435
|
+
# It has to be a bucket head itself: on a calendar grid, the 1st at
|
|
2436
|
+
# 00:00.
|
|
2437
|
+
# @param direction [:round, :floor, :ceil] rounding rule.
|
|
1863
2438
|
# @return [CATime]
|
|
1864
|
-
def
|
|
1865
|
-
|
|
1866
|
-
|
|
2439
|
+
def snap(grid = nil, unit: nil, origin: nil, direction: :round)
|
|
2440
|
+
unless SNAP_DIRECTIONS.include?(direction)
|
|
2441
|
+
raise ArgumentError,
|
|
2442
|
+
"snap: direction must be :round / :floor / :ceil " \
|
|
2443
|
+
"(got #{direction.inspect})"
|
|
2444
|
+
end
|
|
2445
|
+
unit, origin = Grid.resolve(grid, unit, origin)
|
|
2446
|
+
g = resolve_timestep_grid(unit, origin)
|
|
2447
|
+
return calendar_bucket(direction, g[1], origin) if g[0] == :civil
|
|
1867
2448
|
su, mul, step_ticks, o = g
|
|
1868
|
-
|
|
1869
|
-
|
|
2449
|
+
raise_if_int64_overflow(su, mul, o, step_ticks, direction)
|
|
2450
|
+
n = storage_ticks_in_numerator_grid(mul)
|
|
2451
|
+
d = n - o
|
|
1870
2452
|
q = d / step_ticks
|
|
1871
|
-
q = q - (d - q * step_ticks).lt(0)
|
|
1872
|
-
|
|
2453
|
+
q = q - (d - q * step_ticks).lt(0) # floor bucket
|
|
2454
|
+
case direction
|
|
2455
|
+
when :ceil
|
|
2456
|
+
fl = o + q * step_ticks
|
|
2457
|
+
return bucket_head_as_time(fl + step_ticks * n.ne(fl), mul, su)
|
|
2458
|
+
when :round
|
|
2459
|
+
r = d - q * step_ticks # 0 <= r < step_ticks
|
|
2460
|
+
q = q + r.ge((step_ticks + 1) / 2) # ties -> future; no 2*d / 2*r
|
|
2461
|
+
end
|
|
2462
|
+
bucket_head_as_time(o + q * step_ticks, mul, su)
|
|
1873
2463
|
end
|
|
1874
2464
|
|
|
1875
|
-
# @overload
|
|
1876
|
-
#
|
|
1877
|
-
#
|
|
2465
|
+
# @overload floor(grid = nil, unit:, origin: nil)
|
|
2466
|
+
# {#snap} with `direction: :floor` -- each element at its bucket head
|
|
2467
|
+
# (toward the past), as a {CATime} in the same storage resolution.
|
|
1878
2468
|
# @return [CATime]
|
|
1879
|
-
def
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
q = d / step_ticks
|
|
1887
|
-
q = q - (d - q * step_ticks).lt(0)
|
|
1888
|
-
fl = o + q * step_ticks
|
|
1889
|
-
_head_time(fl + step_ticks * n.ne(fl), mul, su)
|
|
1890
|
-
end
|
|
1891
|
-
|
|
1892
|
-
# @overload round(unit:, origin: nil)
|
|
1893
|
-
# Returns each element rounded to its nearest bucket head (ties toward
|
|
1894
|
-
# the future, matching `snap` :round), as a {CATime}. Exact for
|
|
1895
|
-
# odd `step_ticks` (no half-tick loss). For a calendar bucket the nearest
|
|
1896
|
-
# head is by absolute tick distance (month lengths vary), ties toward the
|
|
1897
|
-
# future.
|
|
2469
|
+
def floor(grid = nil, unit: nil, origin: nil)
|
|
2470
|
+
snap(grid, unit: unit, origin: origin, direction: :floor)
|
|
2471
|
+
end
|
|
2472
|
+
|
|
2473
|
+
# @overload ceil(grid = nil, unit:, origin: nil)
|
|
2474
|
+
# {#snap} with `direction: :ceil` -- each element at the bucket head at
|
|
2475
|
+
# or after it (an element already on a boundary maps to itself).
|
|
1898
2476
|
# @return [CATime]
|
|
1899
|
-
def
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2477
|
+
def ceil(grid = nil, unit: nil, origin: nil)
|
|
2478
|
+
snap(grid, unit: unit, origin: origin, direction: :ceil)
|
|
2479
|
+
end
|
|
2480
|
+
|
|
2481
|
+
# @overload round(grid = nil, unit:, origin: nil)
|
|
2482
|
+
# {#snap} with `direction: :round` -- each element at its nearest bucket
|
|
2483
|
+
# head, ties toward the future. Exact for odd `step_ticks` (no half-tick
|
|
2484
|
+
# loss). For a calendar bucket the nearest head is by absolute tick
|
|
2485
|
+
# distance (month lengths vary), ties toward the future.
|
|
2486
|
+
# @return [CATime]
|
|
2487
|
+
def round(grid = nil, unit: nil, origin: nil)
|
|
2488
|
+
snap(grid, unit: unit, origin: origin, direction: :round)
|
|
1910
2489
|
end
|
|
1911
2490
|
|
|
1912
2491
|
# @overload is_righttime(unit:, origin: nil)
|
|
@@ -1914,33 +2493,57 @@ class CATime
|
|
|
1914
2493
|
# head. Use as an assertion before matching to catch off-grid series (a
|
|
1915
2494
|
# timesteps match is "same bucket", not "same instant").
|
|
1916
2495
|
# @param unit [String, Symbol, Resolution] bucket resolution.
|
|
1917
|
-
# @param origin [Time, String, CATime::Element, DateTime, nil]
|
|
2496
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] head of
|
|
2497
|
+
# bucket 0 (default: the Unix epoch, or ISO Monday for a week bucket
|
|
2498
|
+
# on day-or-finer storage -- a week grid counts from the epoch
|
|
2499
|
+
# Thursday and cannot hold a Monday, so it keeps its own ticks).
|
|
2500
|
+
# It has to be a bucket head itself: on a calendar grid, the 1st at
|
|
2501
|
+
# 00:00.
|
|
1918
2502
|
# @return [CArray] boolean.
|
|
1919
|
-
def is_righttime(unit
|
|
1920
|
-
|
|
1921
|
-
|
|
2503
|
+
def is_righttime(grid = nil, unit: nil, origin: nil)
|
|
2504
|
+
unit, origin = Grid.resolve(grid, unit, origin)
|
|
2505
|
+
g = resolve_timestep_grid(unit, origin)
|
|
2506
|
+
return calendar_bucket(:on, g[1], origin) if g[0] == :civil
|
|
1922
2507
|
su, mul, step_ticks, o = g
|
|
1923
|
-
|
|
1924
|
-
d =
|
|
2508
|
+
raise_if_int64_overflow(su, mul, o)
|
|
2509
|
+
d = storage_ticks_in_numerator_grid(mul) - o
|
|
1925
2510
|
q = d / step_ticks
|
|
1926
2511
|
(d - q * step_ticks).eq(0)
|
|
1927
2512
|
end
|
|
1928
2513
|
|
|
1929
2514
|
# @overload from_timesteps(k, unit:, origin: nil)
|
|
1930
2515
|
# Inverse of {#timesteps}: returns the bucket-head time for timestep
|
|
1931
|
-
# `k
|
|
1932
|
-
#
|
|
2516
|
+
# `k`. Use to relabel a `group_by(timesteps)` result, generate a
|
|
2517
|
+
# regular grid, or as a timesteps round-trip oracle.
|
|
1933
2518
|
# A scalar `k` returns a {Element}; a CArray `k` returns a {CATime}.
|
|
2519
|
+
#
|
|
2520
|
+
# The result is stored on the `unit` grid, except a week bucket, which
|
|
2521
|
+
# is stored on `:D`. A week grid counts from the epoch (a Thursday) and
|
|
2522
|
+
# so cannot hold its own bucket head: the head is the ISO Monday, four
|
|
2523
|
+
# days off every week tick. Days hold it exactly, so a `:W` bucket
|
|
2524
|
+
# answers on the day grid and the round trip against a day-or-finer
|
|
2525
|
+
# series is exact. `unit:` names the bucket here, the way it does for
|
|
2526
|
+
# {#floor} / {#ceil} -- not the storage the answer lands on.
|
|
1934
2527
|
# @param k [Integer, CArray] timestep / timesteps.
|
|
1935
|
-
# @param unit [String, Symbol, Resolution]
|
|
1936
|
-
# @param origin [Time, String, CATime::Element, DateTime, nil]
|
|
1937
|
-
#
|
|
1938
|
-
|
|
2528
|
+
# @param unit [String, Symbol, Resolution] bucket resolution.
|
|
2529
|
+
# @param origin [Time, String, CATime::Element, DateTime, nil] head of
|
|
2530
|
+
# bucket 0 (default: the Unix epoch, or ISO Monday for a week bucket).
|
|
2531
|
+
# It has to be a bucket head itself: on a calendar grid, the 1st at
|
|
2532
|
+
# 00:00.
|
|
2533
|
+
# @return [Element, CATime] on the `unit` grid (`:D` for a week bucket).
|
|
2534
|
+
def self.from_timesteps(k, grid = nil, unit: nil, origin: nil)
|
|
2535
|
+
grid = unit if unit.is_a?(Grid)
|
|
2536
|
+
# A Grid answers on its own storage, so an origin off the unit grid
|
|
2537
|
+
# ("12 hours since ... 09:00") survives; the keyword form keeps the
|
|
2538
|
+
# narrower rule it always had, where the origin sits on the unit itself.
|
|
2539
|
+
return grid.at(k) if grid.is_a?(Grid)
|
|
2540
|
+
unit, origin = Grid.resolve(grid, unit, origin)
|
|
1939
2541
|
res = Resolution.parse(unit)
|
|
2542
|
+
out = res.base == :W ? Resolution.new(1, :D) : res
|
|
1940
2543
|
kk = k.is_a?(CArray) ? k.int64 : CArray.int64(1) { Integer(k) }
|
|
1941
|
-
_mul, step_ticks, o =
|
|
2544
|
+
_mul, step_ticks, o = CATimeGrid.resolve_integer_grid(res, out, origin)
|
|
1942
2545
|
raw = o + kk * step_ticks
|
|
1943
|
-
k.is_a?(CArray) ? raw.time(unit:
|
|
2546
|
+
k.is_a?(CArray) ? raw.time(unit: out) : Element.new(raw[0], out)
|
|
1944
2547
|
end
|
|
1945
2548
|
|
|
1946
2549
|
private
|
|
@@ -1952,19 +2555,19 @@ class CATime
|
|
|
1952
2555
|
# per numerator tick) is 1 for a bucket at or coarser than the storage
|
|
1953
2556
|
# tick, and the widening factor for a finer bucket. `origin_ticks` is
|
|
1954
2557
|
# likewise in numerator ticks.
|
|
1955
|
-
def
|
|
2558
|
+
def resolve_timestep_grid(step, origin)
|
|
1956
2559
|
storage_res = unit
|
|
1957
2560
|
step_res = Resolution.parse(step)
|
|
1958
|
-
scale =
|
|
2561
|
+
scale = CATimeGrid.resolve_step_scale(step_res, storage_res)
|
|
1959
2562
|
return [:civil, step_res] if scale == :civil
|
|
1960
2563
|
mul, step_ticks = scale
|
|
1961
2564
|
num_res = mul == 1 ? storage_res : step_res
|
|
1962
|
-
o =
|
|
2565
|
+
o = CATimeGrid.resolve_origin_ticks(origin, step_res, num_res)
|
|
1963
2566
|
[storage_res, mul, step_ticks, o]
|
|
1964
2567
|
end
|
|
1965
2568
|
|
|
1966
|
-
# Storage ticks lifted into the numerator grid (see {#
|
|
1967
|
-
def
|
|
2569
|
+
# Storage ticks lifted into the numerator grid (see {#resolve_timestep_grid}).
|
|
2570
|
+
def storage_ticks_in_numerator_grid(mul)
|
|
1968
2571
|
mul == 1 ? parent : CATimeUnitAlgebra.widen(parent, mul)
|
|
1969
2572
|
end
|
|
1970
2573
|
|
|
@@ -1972,7 +2575,7 @@ class CATime
|
|
|
1972
2575
|
# storage resolution. The division is exact: a numerator grid finer than
|
|
1973
2576
|
# the storage tick only arises when every element already sits on it, so
|
|
1974
2577
|
# each head is a whole number of storage ticks.
|
|
1975
|
-
def
|
|
2578
|
+
def bucket_head_as_time(head, mul, su)
|
|
1976
2579
|
(mul == 1 ? head : head / mul).time(unit: su)
|
|
1977
2580
|
end
|
|
1978
2581
|
|
|
@@ -1993,20 +2596,24 @@ class CATime
|
|
|
1993
2596
|
# widened (`mul` > 1) grid, where a coarse storage unit is lifted into a
|
|
1994
2597
|
# much finer one; a pathological raw value wrapped in a coarse-unit Face is
|
|
1995
2598
|
# not guarded (the gate is a value-range heuristic, not a proof).
|
|
1996
|
-
|
|
2599
|
+
#
|
|
2600
|
+
# min / max skip masked cells, so a masked cell does not decide the range
|
|
2601
|
+
# (it carries no time), and answer UNDEF when there is nothing to bound --
|
|
2602
|
+
# an empty array, or one whose every cell is masked.
|
|
2603
|
+
def raise_if_int64_overflow(su, mul, o, step_ticks = nil, kind = nil)
|
|
1997
2604
|
return unless mul > 1 or FINE_UNITS.include?(su.base)
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
lo = Integer(
|
|
2001
|
-
hi = Integer(
|
|
2002
|
-
chk = ->(v, w) {
|
|
2605
|
+
min = parent.min
|
|
2606
|
+
return if min == UNDEF
|
|
2607
|
+
lo = Integer(min) * mul
|
|
2608
|
+
hi = Integer(parent.max) * mul
|
|
2609
|
+
chk = ->(v, w) { CATimeGrid.check_int64_range(v, w) }
|
|
2003
2610
|
chk.(lo, "ticks in bucket resolution")
|
|
2004
2611
|
chk.(hi, "ticks in bucket resolution")
|
|
2005
2612
|
chk.(lo - o, "parent - origin")
|
|
2006
2613
|
chk.(hi - o, "parent - origin")
|
|
2007
2614
|
if kind
|
|
2008
|
-
chk.(
|
|
2009
|
-
chk.(
|
|
2615
|
+
chk.(bucket_head_ticks(lo, o, step_ticks, kind), "bucket head")
|
|
2616
|
+
chk.(bucket_head_ticks(hi, o, step_ticks, kind), "bucket head")
|
|
2010
2617
|
end
|
|
2011
2618
|
end
|
|
2012
2619
|
|
|
@@ -2014,7 +2621,7 @@ class CATime
|
|
|
2014
2621
|
# scalar mirror of the array floor / ceil / round, used only by the overflow
|
|
2015
2622
|
# guard. Ruby Integer `/` and `%` floor, so `(v - o) / s` is the floor
|
|
2016
2623
|
# bucket directly.
|
|
2017
|
-
def
|
|
2624
|
+
def bucket_head_ticks(v, o, s, kind)
|
|
2018
2625
|
fq = (v - o) / s
|
|
2019
2626
|
case kind
|
|
2020
2627
|
when :floor then o + fq * s
|
|
@@ -2026,27 +2633,27 @@ class CATime
|
|
|
2026
2633
|
# Days since the Unix epoch of the date each element represents (mask
|
|
2027
2634
|
# propagates). Calendar units (:Y / :M) resolve to day 1 of their year /
|
|
2028
2635
|
# month; a week is 7 days; day-or-finer units floor to the day.
|
|
2029
|
-
def
|
|
2636
|
+
def days_since_epoch
|
|
2030
2637
|
res = unit
|
|
2031
2638
|
case res.base
|
|
2032
2639
|
when :Y
|
|
2033
2640
|
ones = CArray.int64(*shape) { 1 }
|
|
2034
|
-
|
|
2641
|
+
CATimeCivil.days_from_civil(parent * res.count + 1970, ones, ones)
|
|
2035
2642
|
when :M
|
|
2036
2643
|
mo = parent * res.count + 1970 * 12
|
|
2037
|
-
y =
|
|
2644
|
+
y = mo / 12
|
|
2038
2645
|
m = mo - y * 12 + 1
|
|
2039
|
-
|
|
2646
|
+
CATimeCivil.days_from_civil(y, m, CArray.int64(*shape) { 1 })
|
|
2040
2647
|
when :W then parent * res.count * 7
|
|
2041
2648
|
when :D then parent * res.count
|
|
2042
2649
|
else # fixed sub-day storage
|
|
2043
2650
|
day = CATime::Resolution.new(1, :D)
|
|
2044
2651
|
r = CATimeUnitAlgebra.ratio(day, res) # storage ticks per day
|
|
2045
2652
|
if r.denominator == 1
|
|
2046
|
-
|
|
2653
|
+
parent / r.numerator
|
|
2047
2654
|
else # non-day-aligned tick
|
|
2048
2655
|
tr = res.tick_ratio # seconds / tick
|
|
2049
|
-
|
|
2656
|
+
parent * tr.numerator / (86400 * tr.denominator)
|
|
2050
2657
|
end
|
|
2051
2658
|
end
|
|
2052
2659
|
end
|
|
@@ -2057,13 +2664,13 @@ class CATime
|
|
|
2057
2664
|
# coarser than `fu` both resolve exactly (a 10-minute grid gives minute 0 /
|
|
2058
2665
|
# 10 / 20 / ...; a day grid collapses hour / minute / second to 0). A
|
|
2059
2666
|
# calendar-storage element has no intra-day field, so it collapses to 0.
|
|
2060
|
-
def
|
|
2667
|
+
def clock_field(fu)
|
|
2061
2668
|
r = CATimeUnitAlgebra.ratio(unit, CATime::Resolution.new(1, fu)) # fu ticks / storage tick
|
|
2062
2669
|
return parent * 0 if r.nil? # calendar storage: no clock field
|
|
2063
2670
|
fi = r.denominator == 1 ? parent * r.numerator # epoch-relative fu index
|
|
2064
|
-
:
|
|
2671
|
+
: parent * r.numerator / r.denominator
|
|
2065
2672
|
cycle = fu == :h ? 24 : 60 # hour 0..23; min/sec 0..59
|
|
2066
|
-
fi
|
|
2673
|
+
fi % cycle
|
|
2067
2674
|
end
|
|
2068
2675
|
|
|
2069
2676
|
# Calendar path (Y/M step on day-or-finer storage) via civil-date integer
|
|
@@ -2071,19 +2678,19 @@ class CATime
|
|
|
2071
2678
|
# min-max, O(N) branch-free, origin-absolute k (negative pre-origin). The
|
|
2072
2679
|
# origin's day / time is ignored (month ordinal only, per the design).
|
|
2073
2680
|
# `kind`: :index / :floor / :ceil / :round / :on.
|
|
2074
|
-
def
|
|
2681
|
+
def calendar_bucket(kind, st, origin)
|
|
2075
2682
|
su = unit # storage Resolution
|
|
2076
2683
|
count = st.count * (st.base == :Y ? 12 : 1) # bucket in months
|
|
2077
|
-
ym0 =
|
|
2078
|
-
days =
|
|
2079
|
-
y, m =
|
|
2684
|
+
ym0 = CATimeGrid.origin_month_ordinal(origin)
|
|
2685
|
+
days = days_since_epoch # count-folded day index
|
|
2686
|
+
y, m = CATimeCivil.civil_from_days(days)
|
|
2080
2687
|
ym = y * 12 + (m - 1)
|
|
2081
|
-
k =
|
|
2688
|
+
k = (ym - ym0) / count
|
|
2082
2689
|
return k if kind == :index
|
|
2083
|
-
head =
|
|
2690
|
+
head = CATimeGrid.calendar_bucket_head_ticks(k, st, su, origin)
|
|
2084
2691
|
return head.time(unit: su) if kind == :floor
|
|
2085
2692
|
return parent.eq(head) if kind == :on
|
|
2086
|
-
head_next =
|
|
2693
|
+
head_next = CATimeGrid.calendar_bucket_head_ticks(k + 1, st, su, origin)
|
|
2087
2694
|
sel = # boolean 0/1: pick head_next?
|
|
2088
2695
|
case kind
|
|
2089
2696
|
when :ceil then parent.ne(head) # on-boundary keeps head
|
|
@@ -2092,198 +2699,6 @@ class CATime
|
|
|
2092
2699
|
(head + (head_next - head) * sel).time(unit: su)
|
|
2093
2700
|
end
|
|
2094
2701
|
|
|
2095
|
-
class << self
|
|
2096
|
-
private
|
|
2097
|
-
|
|
2098
|
-
INT64_MIN = -(2**63)
|
|
2099
|
-
INT64_MAX = 2**63 - 1
|
|
2100
|
-
|
|
2101
|
-
# Raise (rather than let an int64 CArray operand silently wrap) if a
|
|
2102
|
-
# Ruby-domain quantity is out of int64 range. Returns the value on success
|
|
2103
|
-
# so it composes inline.
|
|
2104
|
-
def _chk64(v, what)
|
|
2105
|
-
if v < INT64_MIN || v > INT64_MAX
|
|
2106
|
-
raise RangeError,
|
|
2107
|
-
"time step: #{what} = #{v} overflows int64 " \
|
|
2108
|
-
"(the storage unit is too fine for this step / origin / span; " \
|
|
2109
|
-
"use a coarser unit)"
|
|
2110
|
-
end
|
|
2111
|
-
v
|
|
2112
|
-
end
|
|
2113
|
-
|
|
2114
|
-
# Return [step_ticks, origin_ticks] (both Integer, in storage ticks) for
|
|
2115
|
-
# the integer path. The calendar path is handled by the caller.
|
|
2116
|
-
def _resolve_grid(step_res, storage_res, origin)
|
|
2117
|
-
mul, step_ticks = _resolve_step_scale(step_res, storage_res)
|
|
2118
|
-
[mul, step_ticks, _resolve_origin_ticks(origin, step_res,
|
|
2119
|
-
mul == 1 ? storage_res : step_res)]
|
|
2120
|
-
end
|
|
2121
|
-
|
|
2122
|
-
# Storage ticks per day of `storage_res` (must be a whole number, else the
|
|
2123
|
-
# storage grid does not tile a day -- a calendar bucket is unrepresentable).
|
|
2124
|
-
def _ticks_per_day(storage_res)
|
|
2125
|
-
r = CATimeUnitAlgebra.ratio(Resolution.new(1, :D), storage_res)
|
|
2126
|
-
unless r.denominator == 1
|
|
2127
|
-
raise ArgumentError,
|
|
2128
|
-
"cannot place a calendar bucket on storage resolution " \
|
|
2129
|
-
"#{storage_res} (its tick does not tile a day)"
|
|
2130
|
-
end
|
|
2131
|
-
_chk64(r.numerator, "ticks per day for #{storage_res}")
|
|
2132
|
-
end
|
|
2133
|
-
|
|
2134
|
-
# Storage ticks of the bucket head at timestep `k` (int64 CArray) for a
|
|
2135
|
-
# calendar bucket: ym0 + k*count months -> day 1 of that month -> ticks.
|
|
2136
|
-
def _civil_head_ticks(k, st, storage_res, origin)
|
|
2137
|
-
tpd = _ticks_per_day(storage_res)
|
|
2138
|
-
count = st.count * (st.base == :Y ? 12 : 1)
|
|
2139
|
-
ym0 = _origin_month_ordinal(origin)
|
|
2140
|
-
ymk = ym0 + k * count
|
|
2141
|
-
yy = _floordiv_i(ymk, 12)
|
|
2142
|
-
mm = ymk - yy * 12 + 1
|
|
2143
|
-
_days_from_civil(yy, mm, CArray.int64(*k.shape) { 1 }) * tpd
|
|
2144
|
-
end
|
|
2145
|
-
|
|
2146
|
-
# Month ordinal (year*12 + month-1) of origin; day / time ignored.
|
|
2147
|
-
# Default (nil) is the epoch month 1970-01.
|
|
2148
|
-
def _origin_month_ordinal(origin)
|
|
2149
|
-
return 1970 * 12 if origin.nil?
|
|
2150
|
-
y, m = _origin_year_month(origin)
|
|
2151
|
-
y * 12 + (m - 1)
|
|
2152
|
-
end
|
|
2153
|
-
|
|
2154
|
-
# Floored integer division of an int64 CArray by a positive Integer
|
|
2155
|
-
# (toward -inf; CArray `/` truncates toward zero).
|
|
2156
|
-
def _floordiv_i(a, b)
|
|
2157
|
-
q = a / b
|
|
2158
|
-
q - (a - q * b).lt(0)
|
|
2159
|
-
end
|
|
2160
|
-
|
|
2161
|
-
# Vectorized Howard Hinnant civil-date algebra (proleptic Gregorian,
|
|
2162
|
-
# negative days supported). days since the Unix epoch -> [year, month,
|
|
2163
|
-
# day] int64 CArrays. Truncating division is what the algorithm assumes;
|
|
2164
|
-
# the only negative operand (the 400-year era) is pre-adjusted so
|
|
2165
|
-
# truncation behaves like floor.
|
|
2166
|
-
def _civil_from_days(z)
|
|
2167
|
-
z = z + 719468
|
|
2168
|
-
era = (z - 146096 * z.lt(0)) / 146097
|
|
2169
|
-
doe = z - era * 146097
|
|
2170
|
-
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
|
|
2171
|
-
y = yoe + era * 400
|
|
2172
|
-
doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
|
|
2173
|
-
mp = (5 * doy + 2) / 153
|
|
2174
|
-
d = doy - (153 * mp + 2) / 5 + 1 # day of month, 1..31
|
|
2175
|
-
m = mp + (3 - 12 * mp.ge(10)) # mp<10 ? +3 : -9
|
|
2176
|
-
[y + m.le(2), m, d]
|
|
2177
|
-
end
|
|
2178
|
-
|
|
2179
|
-
# [year, month, day] int64 CArrays -> days since the Unix epoch.
|
|
2180
|
-
def _days_from_civil(y, m, d)
|
|
2181
|
-
y = y - m.le(2)
|
|
2182
|
-
era = (y - 399 * y.lt(0)) / 400
|
|
2183
|
-
yoe = y - era * 400
|
|
2184
|
-
doy = (153 * (m + (9 - 12 * m.gt(2))) + 2) / 5 + (d - 1) # m>2 ? -3 : +9
|
|
2185
|
-
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
|
|
2186
|
-
era * 146097 + doe - 719468
|
|
2187
|
-
end
|
|
2188
|
-
|
|
2189
|
-
# [mul, step_ticks] for the integer path, or :civil (calendar bucket on
|
|
2190
|
-
# day-or-finer storage), or raise. Bucket arithmetic runs in the finer of
|
|
2191
|
-
# the two grids, so exactly one of the pair is > 1:
|
|
2192
|
-
# - bucket at or coarser than the storage tick (a :h bucket on :s
|
|
2193
|
-
# storage) -> [1, N]: N storage ticks per bucket.
|
|
2194
|
-
# - bucket finer than the storage tick (a :h bucket on :D storage) ->
|
|
2195
|
-
# [N, 1]: one storage tick spans N buckets exactly, so a timestep is a
|
|
2196
|
-
# plain widening. This needs the storage tick to be a *whole* multiple
|
|
2197
|
-
# of the bucket tick; a partial multiple ("90 minutes" storage against
|
|
2198
|
-
# an :h bucket) has no integer timestep and raises.
|
|
2199
|
-
def _resolve_step_scale(step_res, storage_res)
|
|
2200
|
-
r = CATimeUnitAlgebra.ratio(step_res, storage_res) # storage ticks / step tick
|
|
2201
|
-
if r
|
|
2202
|
-
what = "bucket #{step_res} in ticks of #{storage_res}"
|
|
2203
|
-
return [1, _chk64(r.numerator, what)] if r.denominator == 1
|
|
2204
|
-
return [_chk64(r.denominator, what), 1] if r.numerator == 1
|
|
2205
|
-
end
|
|
2206
|
-
if CATimeUnitAlgebra::CALENDAR.key?(step_res.base) && SU_LE_DAY.include?(storage_res.base)
|
|
2207
|
-
return :civil
|
|
2208
|
-
end
|
|
2209
|
-
raise ArgumentError,
|
|
2210
|
-
"cannot express bucket #{step_res} on storage resolution #{storage_res} " \
|
|
2211
|
-
"(neither is a whole multiple of the other)"
|
|
2212
|
-
end
|
|
2213
|
-
|
|
2214
|
-
# origin -> integer tick count in storage ticks. Default (nil) is the
|
|
2215
|
-
# epoch, except a week bucket defaults to ISO Monday (1970-01-05). A lossy
|
|
2216
|
-
# conversion (origin not landing exactly on the storage grid) raises; a
|
|
2217
|
-
# bare Integer is rejected (epoch-dependent, ambiguous).
|
|
2218
|
-
def _resolve_origin_ticks(origin, step_res, storage_res)
|
|
2219
|
-
if origin.nil?
|
|
2220
|
-
if step_res.base == :W && SU_LE_DAY.include?(storage_res.base)
|
|
2221
|
-
return _chk64(_ticks_per_day(storage_res) * 4,
|
|
2222
|
-
"ISO-Monday week origin in ticks of #{storage_res}")
|
|
2223
|
-
end
|
|
2224
|
-
return 0
|
|
2225
|
-
end
|
|
2226
|
-
if CATimeUnitAlgebra::FIXED.key?(storage_res.base)
|
|
2227
|
-
secs = _origin_seconds_exact(origin)
|
|
2228
|
-
tick = secs / storage_res.tick_ratio # Rational
|
|
2229
|
-
unless tick.denominator == 1
|
|
2230
|
-
raise ArgumentError,
|
|
2231
|
-
"origin #{origin.inspect} is not representable losslessly " \
|
|
2232
|
-
"in storage resolution #{storage_res} (would truncate the grid phase)"
|
|
2233
|
-
end
|
|
2234
|
-
_chk64(tick.numerator, "origin #{origin.inspect} in ticks of #{storage_res}")
|
|
2235
|
-
else # :Y / :M storage
|
|
2236
|
-
y, m = _origin_year_month(origin) # day / time ignored
|
|
2237
|
-
ord = storage_res.base == :Y ? (y - 1970) : (y * 12 + (m - 1) - 1970 * 12)
|
|
2238
|
-
if storage_res.count > 1
|
|
2239
|
-
unless (ord % storage_res.count).zero?
|
|
2240
|
-
raise ArgumentError,
|
|
2241
|
-
"origin #{origin.inspect} is not on the #{storage_res} grid"
|
|
2242
|
-
end
|
|
2243
|
-
ord /= storage_res.count
|
|
2244
|
-
end
|
|
2245
|
-
ord
|
|
2246
|
-
end
|
|
2247
|
-
end
|
|
2248
|
-
|
|
2249
|
-
# Exact Rational seconds since the Unix epoch for a fixed-storage origin.
|
|
2250
|
-
# Time / String / DateTime go through the DateTime-independent parser.
|
|
2251
|
-
def _origin_seconds_exact(origin)
|
|
2252
|
-
case origin
|
|
2253
|
-
when CATime::Element
|
|
2254
|
-
unless CATimeUnitAlgebra::FIXED.key?(origin.unit.base)
|
|
2255
|
-
raise ArgumentError,
|
|
2256
|
-
"origin scalar in calendar unit #{origin.unit} has no exact " \
|
|
2257
|
-
"seconds; use a fixed-unit scalar / Time / String"
|
|
2258
|
-
end
|
|
2259
|
-
Rational(origin.value) * origin.unit.tick_ratio
|
|
2260
|
-
when Integer
|
|
2261
|
-
raise ArgumentError,
|
|
2262
|
-
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2263
|
-
"Time / String / CATime scalar"
|
|
2264
|
-
else
|
|
2265
|
-
CArray._epoch_seconds_exact(origin) # Time / String / DateTime
|
|
2266
|
-
end
|
|
2267
|
-
end
|
|
2268
|
-
|
|
2269
|
-
# [year, month] of a calendar-storage origin (finer fields ignored).
|
|
2270
|
-
def _origin_year_month(origin)
|
|
2271
|
-
case origin
|
|
2272
|
-
when CATime::Element
|
|
2273
|
-
case origin.unit.base
|
|
2274
|
-
when :M then mo = origin.value * origin.unit.count + 1970 * 12; [mo / 12, mo % 12 + 1]
|
|
2275
|
-
when :Y then [origin.value * origin.unit.count + 1970, 1]
|
|
2276
|
-
else t = origin.to_time.utc; [t.year, t.month]
|
|
2277
|
-
end
|
|
2278
|
-
when Integer
|
|
2279
|
-
raise ArgumentError,
|
|
2280
|
-
"origin: a bare Integer is ambiguous (epoch-dependent); pass a " \
|
|
2281
|
-
"Time / String / CATime scalar"
|
|
2282
|
-
else
|
|
2283
|
-
CArray._epoch_year_month(origin) # Time / String / DateTime
|
|
2284
|
-
end
|
|
2285
|
-
end
|
|
2286
|
-
end
|
|
2287
2702
|
end
|
|
2288
2703
|
|
|
2289
2704
|
# The view-creating method lift hook lives in the C layer (= subclass-
|