trifle-stats 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +21 -2
- data/lib/trifle/stats/configuration.rb +1 -0
- data/lib/trifle/stats/driver/api.rb +131 -0
- data/lib/trifle/stats/driver/mysql.rb +33 -15
- data/lib/trifle/stats/driver/postgres.rb +66 -37
- data/lib/trifle/stats/driver/redis.rb +37 -30
- data/lib/trifle/stats/driver/sqlite.rb +66 -35
- data/lib/trifle/stats/nocturnal.rb +2 -2
- data/lib/trifle/stats/operations/timeseries/increment.rb +10 -0
- data/lib/trifle/stats/operations/timeseries/set.rb +10 -0
- data/lib/trifle/stats/transponder/expression.rb +117 -0
- data/lib/trifle/stats/transponder/expression_engine.rb +356 -0
- data/lib/trifle/stats/version.rb +1 -1
- data/lib/trifle/stats.rb +4 -10
- metadata +5 -13
- data/lib/trifle/stats/nocturnal_legacy.rb +0 -161
- data/lib/trifle/stats/transponder/add.rb +0 -30
- data/lib/trifle/stats/transponder/divide.rb +0 -30
- data/lib/trifle/stats/transponder/max.rb +0 -31
- data/lib/trifle/stats/transponder/mean.rb +0 -31
- data/lib/trifle/stats/transponder/min.rb +0 -31
- data/lib/trifle/stats/transponder/multiply.rb +0 -30
- data/lib/trifle/stats/transponder/ratio.rb +0 -30
- data/lib/trifle/stats/transponder/standard_deviation.rb +0 -34
- data/lib/trifle/stats/transponder/subtract.rb +0 -30
- data/lib/trifle/stats/transponder/sum.rb +0 -28
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module Trifle
|
|
6
|
+
module Stats
|
|
7
|
+
class Transponder
|
|
8
|
+
class ExpressionEngine
|
|
9
|
+
LETTERS = ('a'..'z').to_a.freeze
|
|
10
|
+
TOKEN_OPERATORS = %w[+ - * / ( ) ,].freeze
|
|
11
|
+
NUMBER_PATTERN = /\A\d+(?:\.\d+)?/.freeze
|
|
12
|
+
IDENT_PATTERN = /\A[A-Za-z_][A-Za-z0-9_]*/.freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def max_vars
|
|
16
|
+
LETTERS.length
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def allowed_vars(count)
|
|
20
|
+
LETTERS.take(count)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate(paths:, expression:)
|
|
24
|
+
normalized_paths = normalize_paths(paths)
|
|
25
|
+
parse(expression: expression, paths: normalized_paths)
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse(expression:, paths:)
|
|
30
|
+
normalized_paths = normalize_paths(paths)
|
|
31
|
+
ensure_within_var_limit(normalized_paths)
|
|
32
|
+
tokens = tokenize(expression.to_s)
|
|
33
|
+
parser = Parser.new(tokens, allowed_vars(normalized_paths.length))
|
|
34
|
+
ast = parser.parse
|
|
35
|
+
raise ArgumentError, 'Unexpected token sequence.' unless parser.done?
|
|
36
|
+
|
|
37
|
+
ast
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def evaluate(ast:, env:)
|
|
41
|
+
Evaluator.new(env).evaluate(ast)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize_paths(paths)
|
|
47
|
+
raise ArgumentError, 'Paths must be an array.' unless paths.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
cleaned = paths.map { |path| path.to_s.strip }.reject(&:empty?)
|
|
50
|
+
raise ArgumentError, 'At least one path is required.' if cleaned.empty?
|
|
51
|
+
|
|
52
|
+
cleaned
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ensure_within_var_limit(paths)
|
|
56
|
+
return if paths.length <= max_vars
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "Too many paths. Maximum supported is #{max_vars}."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tokenize(expression)
|
|
62
|
+
trimmed = normalize_expression_text(expression)
|
|
63
|
+
tokens = []
|
|
64
|
+
index = 0
|
|
65
|
+
|
|
66
|
+
while index < trimmed.length
|
|
67
|
+
token, consumed = read_token(trimmed, index)
|
|
68
|
+
tokens << token unless token.nil?
|
|
69
|
+
index += consumed
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
tokens
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalize_expression_text(expression)
|
|
76
|
+
trimmed = expression.to_s.strip
|
|
77
|
+
raise ArgumentError, 'Expression must be text.' if trimmed.empty?
|
|
78
|
+
|
|
79
|
+
trimmed
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def read_token(source, index)
|
|
83
|
+
char = source[index]
|
|
84
|
+
|
|
85
|
+
return [nil, 1] if whitespace?(char)
|
|
86
|
+
return [char, 1] if operator?(char)
|
|
87
|
+
return number_token(source, index) if digit?(char)
|
|
88
|
+
return ident_token(source, index) if identifier_start?(char)
|
|
89
|
+
|
|
90
|
+
raise_invalid_token(index)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def whitespace?(char)
|
|
94
|
+
char.match?(/\s/)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def operator?(char)
|
|
98
|
+
TOKEN_OPERATORS.include?(char)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def digit?(char)
|
|
102
|
+
char.match?(/\d/)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def identifier_start?(char)
|
|
106
|
+
char.match?(/[A-Za-z_]/)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def number_token(source, index)
|
|
110
|
+
match = source[index..].match(NUMBER_PATTERN)
|
|
111
|
+
raise_invalid_token(index) unless match
|
|
112
|
+
|
|
113
|
+
[[:number, BigDecimal(match[0])], match[0].length]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ident_token(source, index)
|
|
117
|
+
match = source[index..].match(IDENT_PATTERN)
|
|
118
|
+
raise_invalid_token(index) unless match
|
|
119
|
+
|
|
120
|
+
[[:ident, match[0]], match[0].length]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def raise_invalid_token(index)
|
|
124
|
+
raise ArgumentError, "Invalid token at position #{index}."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Parser
|
|
129
|
+
def initialize(tokens, vars)
|
|
130
|
+
@tokens = tokens
|
|
131
|
+
@vars = vars
|
|
132
|
+
@index = 0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def parse
|
|
136
|
+
parse_expression
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def done?
|
|
140
|
+
current.nil?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def parse_expression
|
|
146
|
+
node = parse_term
|
|
147
|
+
|
|
148
|
+
while %w[+ -].include?(current)
|
|
149
|
+
op = consume
|
|
150
|
+
node = [op.to_sym, node, parse_term]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
node
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_term
|
|
157
|
+
node = parse_factor
|
|
158
|
+
|
|
159
|
+
while %w[* /].include?(current)
|
|
160
|
+
op = consume
|
|
161
|
+
node = [op.to_sym, node, parse_factor]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
node
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_factor
|
|
168
|
+
return parse_unary_factor if %w[+ -].include?(current)
|
|
169
|
+
return parse_grouped_expression if current == '('
|
|
170
|
+
|
|
171
|
+
parse_primary
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_primary
|
|
175
|
+
token = current
|
|
176
|
+
raise ArgumentError, 'Unexpected end of expression.' if token.nil?
|
|
177
|
+
|
|
178
|
+
return parse_number(token) if number_token?(token)
|
|
179
|
+
return parse_identifier(token) if ident_token?(token)
|
|
180
|
+
|
|
181
|
+
raise ArgumentError, "Unexpected token #{token.inspect}."
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def parse_args
|
|
185
|
+
return [] if current == ')'
|
|
186
|
+
|
|
187
|
+
args = [parse_expression]
|
|
188
|
+
|
|
189
|
+
while current == ','
|
|
190
|
+
consume
|
|
191
|
+
args << parse_expression
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
args
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def current
|
|
198
|
+
@tokens[@index]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def consume
|
|
202
|
+
token = current
|
|
203
|
+
@index += 1
|
|
204
|
+
token
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def parse_unary_factor
|
|
208
|
+
operator = consume
|
|
209
|
+
node = parse_factor
|
|
210
|
+
operator == '-' ? [:neg, node] : node
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parse_grouped_expression
|
|
214
|
+
consume
|
|
215
|
+
node = parse_expression
|
|
216
|
+
expect_token!(')', 'Missing closing parenthesis.')
|
|
217
|
+
consume
|
|
218
|
+
node
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def parse_number(token)
|
|
222
|
+
consume
|
|
223
|
+
[:number, token[1]]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def parse_identifier(token)
|
|
227
|
+
consume
|
|
228
|
+
ident = token[1]
|
|
229
|
+
return parse_function_call(ident) if current == '('
|
|
230
|
+
|
|
231
|
+
validate_variable!(ident)
|
|
232
|
+
[:var, ident]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parse_function_call(ident)
|
|
236
|
+
consume
|
|
237
|
+
args = parse_args
|
|
238
|
+
expect_token!(')', 'Missing closing parenthesis.')
|
|
239
|
+
consume
|
|
240
|
+
[:func, ident, args]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def validate_variable!(ident)
|
|
244
|
+
raise ArgumentError, "Unknown variable #{ident}." unless @vars.include?(ident)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def number_token?(token)
|
|
248
|
+
token.is_a?(Array) && token[0] == :number
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def ident_token?(token)
|
|
252
|
+
token.is_a?(Array) && token[0] == :ident
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def expect_token!(expected, message)
|
|
256
|
+
raise ArgumentError, message unless current == expected
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
class Evaluator
|
|
261
|
+
ZERO = BigDecimal('0')
|
|
262
|
+
AVERAGE = lambda do |args|
|
|
263
|
+
args.reduce(ZERO, :+) / BigDecimal(args.length.to_s)
|
|
264
|
+
end
|
|
265
|
+
BINARY_OPERATIONS = {
|
|
266
|
+
:+ => ->(left, right) { left + right },
|
|
267
|
+
:- => ->(left, right) { left - right },
|
|
268
|
+
:* => ->(left, right) { left * right },
|
|
269
|
+
:/ => ->(left, right) { right.zero? ? nil : left / right }
|
|
270
|
+
}.freeze
|
|
271
|
+
FUNCTIONS = {
|
|
272
|
+
'sum' => ->(args) { args.reduce(ZERO, :+) },
|
|
273
|
+
'mean' => AVERAGE,
|
|
274
|
+
'avg' => AVERAGE,
|
|
275
|
+
'min' => :min.to_proc,
|
|
276
|
+
'max' => :max.to_proc
|
|
277
|
+
}.freeze
|
|
278
|
+
|
|
279
|
+
def initialize(env)
|
|
280
|
+
@env = env || {}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def evaluate(node)
|
|
284
|
+
type, *rest = node
|
|
285
|
+
return rest[0] if type == :number
|
|
286
|
+
return evaluate_variable(rest[0]) if type == :var
|
|
287
|
+
return evaluate_negation(rest[0]) if type == :neg
|
|
288
|
+
return evaluate_binary(type, rest[0], rest[1]) if BINARY_OPERATIONS.key?(type)
|
|
289
|
+
return evaluate_function(rest[0], rest[1]) if type == :func
|
|
290
|
+
|
|
291
|
+
raise ArgumentError, "Unknown AST node #{type.inspect}."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private
|
|
295
|
+
|
|
296
|
+
def normalize_value(value)
|
|
297
|
+
return nil if value.nil?
|
|
298
|
+
return value if value.is_a?(BigDecimal)
|
|
299
|
+
return BigDecimal(value.to_s) if value.is_a?(Numeric)
|
|
300
|
+
|
|
301
|
+
nil
|
|
302
|
+
rescue ArgumentError
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def evaluate_variable(name)
|
|
307
|
+
normalize_value(@env[name])
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def evaluate_negation(node)
|
|
311
|
+
value = evaluate(node)
|
|
312
|
+
value.nil? ? nil : -value
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def evaluate_binary(operator, left_node, right_node)
|
|
316
|
+
left = evaluate(left_node)
|
|
317
|
+
right = evaluate(right_node)
|
|
318
|
+
apply_binary(operator, left, right)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def evaluate_function(name, arg_nodes)
|
|
322
|
+
args = arg_nodes.map { |arg| evaluate(arg) }
|
|
323
|
+
apply_function(name, args)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def apply_binary(operator, left, right)
|
|
327
|
+
return nil if left.nil? || right.nil?
|
|
328
|
+
|
|
329
|
+
operation = BINARY_OPERATIONS[operator]
|
|
330
|
+
raise ArgumentError, "Unknown binary operator #{operator.inspect}." unless operation
|
|
331
|
+
|
|
332
|
+
operation.call(left, right)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def apply_function(name, args)
|
|
336
|
+
return nil if args.empty? || args.any?(&:nil?)
|
|
337
|
+
|
|
338
|
+
return apply_sqrt(args) if name == 'sqrt'
|
|
339
|
+
|
|
340
|
+
function = FUNCTIONS[name]
|
|
341
|
+
raise ArgumentError, "Unknown function #{name}." unless function
|
|
342
|
+
|
|
343
|
+
function.call(args)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def apply_sqrt(args)
|
|
347
|
+
raise ArgumentError, 'Function sqrt expects 1 argument.' unless args.length == 1
|
|
348
|
+
return nil if args[0].negative?
|
|
349
|
+
|
|
350
|
+
BigDecimal(Math.sqrt(args[0].to_f).to_s)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
data/lib/trifle/stats/version.rb
CHANGED
data/lib/trifle/stats.rb
CHANGED
|
@@ -28,16 +28,8 @@ require 'trifle/stats/operations/timeseries/set'
|
|
|
28
28
|
require 'trifle/stats/operations/timeseries/values'
|
|
29
29
|
require 'trifle/stats/operations/status/beam'
|
|
30
30
|
require 'trifle/stats/operations/status/scan'
|
|
31
|
-
require 'trifle/stats/transponder/
|
|
32
|
-
require 'trifle/stats/transponder/
|
|
33
|
-
require 'trifle/stats/transponder/max'
|
|
34
|
-
require 'trifle/stats/transponder/mean'
|
|
35
|
-
require 'trifle/stats/transponder/min'
|
|
36
|
-
require 'trifle/stats/transponder/multiply'
|
|
37
|
-
require 'trifle/stats/transponder/ratio'
|
|
38
|
-
require 'trifle/stats/transponder/standard_deviation'
|
|
39
|
-
require 'trifle/stats/transponder/subtract'
|
|
40
|
-
require 'trifle/stats/transponder/sum'
|
|
31
|
+
require 'trifle/stats/transponder/expression_engine'
|
|
32
|
+
require 'trifle/stats/transponder/expression'
|
|
41
33
|
require 'trifle/stats/version'
|
|
42
34
|
|
|
43
35
|
module Trifle
|
|
@@ -107,3 +99,5 @@ module Trifle
|
|
|
107
99
|
end
|
|
108
100
|
end
|
|
109
101
|
end
|
|
102
|
+
|
|
103
|
+
require 'trifle/stats/driver/api'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trifle-stats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jozef Vaclavik
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -225,6 +225,7 @@ files:
|
|
|
225
225
|
- lib/trifle/stats/designator/geometric.rb
|
|
226
226
|
- lib/trifle/stats/designator/linear.rb
|
|
227
227
|
- lib/trifle/stats/driver/README.md
|
|
228
|
+
- lib/trifle/stats/driver/api.rb
|
|
228
229
|
- lib/trifle/stats/driver/mongo.rb
|
|
229
230
|
- lib/trifle/stats/driver/mysql.rb
|
|
230
231
|
- lib/trifle/stats/driver/postgres.rb
|
|
@@ -238,23 +239,14 @@ files:
|
|
|
238
239
|
- lib/trifle/stats/nocturnal.rb
|
|
239
240
|
- lib/trifle/stats/nocturnal/key.rb
|
|
240
241
|
- lib/trifle/stats/nocturnal/parser.rb
|
|
241
|
-
- lib/trifle/stats/nocturnal_legacy.rb
|
|
242
242
|
- lib/trifle/stats/operations/status/beam.rb
|
|
243
243
|
- lib/trifle/stats/operations/status/scan.rb
|
|
244
244
|
- lib/trifle/stats/operations/timeseries/increment.rb
|
|
245
245
|
- lib/trifle/stats/operations/timeseries/set.rb
|
|
246
246
|
- lib/trifle/stats/operations/timeseries/values.rb
|
|
247
247
|
- lib/trifle/stats/series.rb
|
|
248
|
-
- lib/trifle/stats/transponder/
|
|
249
|
-
- lib/trifle/stats/transponder/
|
|
250
|
-
- lib/trifle/stats/transponder/max.rb
|
|
251
|
-
- lib/trifle/stats/transponder/mean.rb
|
|
252
|
-
- lib/trifle/stats/transponder/min.rb
|
|
253
|
-
- lib/trifle/stats/transponder/multiply.rb
|
|
254
|
-
- lib/trifle/stats/transponder/ratio.rb
|
|
255
|
-
- lib/trifle/stats/transponder/standard_deviation.rb
|
|
256
|
-
- lib/trifle/stats/transponder/subtract.rb
|
|
257
|
-
- lib/trifle/stats/transponder/sum.rb
|
|
248
|
+
- lib/trifle/stats/transponder/expression.rb
|
|
249
|
+
- lib/trifle/stats/transponder/expression_engine.rb
|
|
258
250
|
- lib/trifle/stats/version.rb
|
|
259
251
|
- trifle-stats.gemspec
|
|
260
252
|
homepage: https://trifle.io
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Trifle
|
|
4
|
-
module Stats
|
|
5
|
-
class Nocturnal # rubocop:disable Metrics/ClassLength
|
|
6
|
-
UNIT_MAP = {
|
|
7
|
-
's' => :second,
|
|
8
|
-
'm' => :minute,
|
|
9
|
-
'h' => :hour,
|
|
10
|
-
'd' => :day,
|
|
11
|
-
'w' => :week,
|
|
12
|
-
'mo' => :month,
|
|
13
|
-
'q' => :quarter,
|
|
14
|
-
'y' => :year
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
DAYS_INTO_WEEK = {
|
|
18
|
-
sunday: 0, monday: 1, tuesday: 2, wednesday: 3,
|
|
19
|
-
thursday: 4, friday: 5, saturday: 6
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
def self.timeline(from:, to:, granularity:, config: nil)
|
|
23
|
-
list = []
|
|
24
|
-
from = new(from, config: config).send(granularity)
|
|
25
|
-
to = new(to, config: config).send(granularity)
|
|
26
|
-
item = from.dup
|
|
27
|
-
while item <= to
|
|
28
|
-
list << item
|
|
29
|
-
item = Nocturnal.new(item, config: config).send("next_#{granularity}")
|
|
30
|
-
end
|
|
31
|
-
list
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def initialize(at, config: nil)
|
|
35
|
-
@at = at
|
|
36
|
-
@config = config
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def config
|
|
40
|
-
@config || Trifle::Stats.default
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def change(**fractions)
|
|
44
|
-
Time.new(
|
|
45
|
-
fractions.fetch(:year, @at.year),
|
|
46
|
-
fractions.fetch(:month, @at.month),
|
|
47
|
-
fractions.fetch(:day, @at.day),
|
|
48
|
-
fractions.fetch(:hour, @at.hour),
|
|
49
|
-
fractions.fetch(:minute, @at.min),
|
|
50
|
-
fractions.fetch(:second, 0),
|
|
51
|
-
config.tz.utc_offset
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def second
|
|
56
|
-
change(second: @at.sec)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def next_second
|
|
60
|
-
Nocturnal.new(
|
|
61
|
-
second + 1,
|
|
62
|
-
config: config
|
|
63
|
-
).second
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def minute
|
|
67
|
-
change(second: 0)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def next_minute
|
|
71
|
-
Nocturnal.new(
|
|
72
|
-
minute + 60,
|
|
73
|
-
config: config
|
|
74
|
-
).minute
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def hour
|
|
78
|
-
change(minute: 0)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def next_hour
|
|
82
|
-
Nocturnal.new(
|
|
83
|
-
hour + 60 * 60,
|
|
84
|
-
config: config
|
|
85
|
-
).hour
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def day
|
|
89
|
-
change(hour: 0, minute: 0)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def next_day
|
|
93
|
-
Nocturnal.new(
|
|
94
|
-
day + 60 * 60 * 24,
|
|
95
|
-
config: config
|
|
96
|
-
).day
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def week
|
|
100
|
-
today = day
|
|
101
|
-
|
|
102
|
-
(today.to_date - days_to_week_start).to_time
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def next_week
|
|
106
|
-
Nocturnal.new(
|
|
107
|
-
week + 60 * 60 * 24 * 7,
|
|
108
|
-
config: config
|
|
109
|
-
).week
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def days_to_week_start
|
|
113
|
-
start_day_number = DAYS_INTO_WEEK.fetch(
|
|
114
|
-
config.beginning_of_week
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
(@at.wday - start_day_number) % 7
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def month
|
|
121
|
-
change(day: 1, hour: 0, minute: 0)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def next_month
|
|
125
|
-
Nocturnal.new(
|
|
126
|
-
month + 60 * 60 * 24 * 31,
|
|
127
|
-
config: config
|
|
128
|
-
).month
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def quarter
|
|
132
|
-
first_quarter_month = @at.month - (2 + @at.month) % 3
|
|
133
|
-
|
|
134
|
-
change(
|
|
135
|
-
month: first_quarter_month,
|
|
136
|
-
day: 1,
|
|
137
|
-
hour: 0,
|
|
138
|
-
minute: 0
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def next_quarter
|
|
143
|
-
Nocturnal.new(
|
|
144
|
-
quarter + 60 * 60 * 24 * 31 * 3,
|
|
145
|
-
config: config
|
|
146
|
-
).quarter
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def year
|
|
150
|
-
change(month: 1, day: 1, hour: 0, minute: 0)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def next_year
|
|
154
|
-
Nocturnal.new(
|
|
155
|
-
year + 60 * 60 * 24 * 31 * 12,
|
|
156
|
-
config: config
|
|
157
|
-
).year
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Trifle
|
|
4
|
-
module Stats
|
|
5
|
-
class Transponder
|
|
6
|
-
class Add
|
|
7
|
-
include Trifle::Stats::Mixins::Packer
|
|
8
|
-
Trifle::Stats::Series.register_transponder(:add, self)
|
|
9
|
-
|
|
10
|
-
def transpond(series:, left:, right:, response: 'add') # rubocop:disable Metrics/MethodLength
|
|
11
|
-
left_keys = left.to_s.split('.')
|
|
12
|
-
right_keys = right.to_s.split('.')
|
|
13
|
-
|
|
14
|
-
series[:values] = series[:values].map do |data|
|
|
15
|
-
dleft = data.dig(*left_keys)
|
|
16
|
-
dright = data.dig(*right_keys)
|
|
17
|
-
next data unless dleft && dright
|
|
18
|
-
|
|
19
|
-
dres = dleft + dright
|
|
20
|
-
signal = {
|
|
21
|
-
response => dres
|
|
22
|
-
}
|
|
23
|
-
self.class.deep_merge(data, self.class.unpack(hash: signal))
|
|
24
|
-
end
|
|
25
|
-
series
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Trifle
|
|
4
|
-
module Stats
|
|
5
|
-
class Transponder
|
|
6
|
-
class Divide
|
|
7
|
-
include Trifle::Stats::Mixins::Packer
|
|
8
|
-
Trifle::Stats::Series.register_transponder(:divide, self)
|
|
9
|
-
|
|
10
|
-
def transpond(series:, left:, right:, response: 'divide') # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
11
|
-
left_keys = left.to_s.split('.')
|
|
12
|
-
right_keys = right.to_s.split('.')
|
|
13
|
-
|
|
14
|
-
series[:values] = series[:values].map do |data|
|
|
15
|
-
dleft = data.dig(*left_keys)
|
|
16
|
-
dright = data.dig(*right_keys)
|
|
17
|
-
next data unless dleft && dright
|
|
18
|
-
|
|
19
|
-
dres = (dleft / dright)
|
|
20
|
-
signal = {
|
|
21
|
-
response => dres.nan? ? BigDecimal(0) : dres
|
|
22
|
-
}
|
|
23
|
-
self.class.deep_merge(data, self.class.unpack(hash: signal))
|
|
24
|
-
end
|
|
25
|
-
series
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Trifle
|
|
4
|
-
module Stats
|
|
5
|
-
class Transponder
|
|
6
|
-
class Max
|
|
7
|
-
include Trifle::Stats::Mixins::Packer
|
|
8
|
-
Trifle::Stats::Series.register_transponder(:max, self)
|
|
9
|
-
|
|
10
|
-
def transpond(series:, paths:, response: 'max') # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
11
|
-
path_keys = paths.map { |path| path.to_s.split('.') }
|
|
12
|
-
|
|
13
|
-
series[:values] = series[:values].map do |data|
|
|
14
|
-
dvalues = path_keys.map { |path_key| data.dig(*path_key) }
|
|
15
|
-
next data if dvalues.any?(&:nil?)
|
|
16
|
-
|
|
17
|
-
numeric_values = dvalues.select { |v| v.is_a?(Numeric) }
|
|
18
|
-
next data if numeric_values.empty?
|
|
19
|
-
|
|
20
|
-
dres = numeric_values.max
|
|
21
|
-
signal = {
|
|
22
|
-
response => dres
|
|
23
|
-
}
|
|
24
|
-
self.class.deep_merge(data, self.class.unpack(hash: signal))
|
|
25
|
-
end
|
|
26
|
-
series
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|