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