trifle-stats 2.4.1 → 2.5.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 +1 -1
- data/README.md +20 -2
- 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/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 +2 -10
- metadata +4 -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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trifle
|
|
4
|
+
module Stats
|
|
5
|
+
class Transponder
|
|
6
|
+
class Expression
|
|
7
|
+
include Trifle::Stats::Mixins::Packer
|
|
8
|
+
Trifle::Stats::Series.register_transponder(:expression, self)
|
|
9
|
+
|
|
10
|
+
def transpond(series:, paths:, expression:, response:)
|
|
11
|
+
normalized_paths, normalized_response = normalized_config(paths, response)
|
|
12
|
+
ast = ExpressionEngine.parse(expression: expression, paths: normalized_paths)
|
|
13
|
+
series[:values] = transformed_values(series[:values], normalized_paths, normalized_response, ast)
|
|
14
|
+
series
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate(paths:, expression:, response:)
|
|
18
|
+
normalized_paths, = normalized_config(paths, response)
|
|
19
|
+
ExpressionEngine.validate(paths: normalized_paths, expression: expression)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalized_config(paths, response)
|
|
25
|
+
normalized_paths = normalize_paths(paths)
|
|
26
|
+
normalized_response = normalize_response(response)
|
|
27
|
+
|
|
28
|
+
ensure_no_wildcards!(normalized_paths, normalized_response)
|
|
29
|
+
[normalized_paths, normalized_response]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def normalize_paths(paths)
|
|
33
|
+
raise ArgumentError, 'Paths must be an array.' unless paths.is_a?(Array)
|
|
34
|
+
|
|
35
|
+
cleaned = paths.map { |path| path.to_s.strip }.reject(&:empty?)
|
|
36
|
+
raise ArgumentError, 'At least one path is required.' if cleaned.empty?
|
|
37
|
+
|
|
38
|
+
cleaned
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def normalize_response(response)
|
|
42
|
+
normalized_response = response.to_s.strip
|
|
43
|
+
ensure_response!(normalized_response)
|
|
44
|
+
normalized_response
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ensure_response!(response)
|
|
48
|
+
raise ArgumentError, 'Response path is required.' if response.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ensure_no_wildcards!(paths, response)
|
|
52
|
+
wildcard = paths.any? { |path| path.include?('*') } || response.include?('*')
|
|
53
|
+
raise ArgumentError, 'Wildcard paths are not supported yet.' if wildcard
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_env(data, paths)
|
|
57
|
+
ExpressionEngine.allowed_vars(paths.length).zip(paths).to_h do |var, path|
|
|
58
|
+
[var, data.dig(*path.split('.'))]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transformed_values(values, paths, response, ast)
|
|
63
|
+
values.map { |data| transform_row(data, paths, response, ast) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def transform_row(data, paths, response, ast)
|
|
67
|
+
env = build_env(data, paths)
|
|
68
|
+
value = ExpressionEngine.evaluate(ast: ast, env: env)
|
|
69
|
+
put_response(data, response, value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def put_response(data, response, value)
|
|
73
|
+
keys = response.split('.')
|
|
74
|
+
raise ArgumentError, "Cannot write to response path #{response}." unless can_create_path?(data, keys)
|
|
75
|
+
|
|
76
|
+
updated = deep_dup(data)
|
|
77
|
+
target = updated
|
|
78
|
+
|
|
79
|
+
keys[0..-2].each do |key|
|
|
80
|
+
target[key] ||= {}
|
|
81
|
+
target = target[key]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
target[keys[-1]] = value
|
|
85
|
+
updated
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def can_create_path?(data, keys)
|
|
89
|
+
return true if keys.length <= 1
|
|
90
|
+
|
|
91
|
+
current = data
|
|
92
|
+
|
|
93
|
+
keys[0..-2].each do |key|
|
|
94
|
+
value = current[key]
|
|
95
|
+
return true if value.nil?
|
|
96
|
+
return false unless value.is_a?(Hash)
|
|
97
|
+
|
|
98
|
+
current = value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def deep_dup(value)
|
|
105
|
+
case value
|
|
106
|
+
when Hash
|
|
107
|
+
value.transform_values { |inner| deep_dup(inner) }
|
|
108
|
+
when Array
|
|
109
|
+
value.map { |inner| deep_dup(inner) }
|
|
110
|
+
else
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -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
|
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.5.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-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -238,23 +238,14 @@ files:
|
|
|
238
238
|
- lib/trifle/stats/nocturnal.rb
|
|
239
239
|
- lib/trifle/stats/nocturnal/key.rb
|
|
240
240
|
- lib/trifle/stats/nocturnal/parser.rb
|
|
241
|
-
- lib/trifle/stats/nocturnal_legacy.rb
|
|
242
241
|
- lib/trifle/stats/operations/status/beam.rb
|
|
243
242
|
- lib/trifle/stats/operations/status/scan.rb
|
|
244
243
|
- lib/trifle/stats/operations/timeseries/increment.rb
|
|
245
244
|
- lib/trifle/stats/operations/timeseries/set.rb
|
|
246
245
|
- lib/trifle/stats/operations/timeseries/values.rb
|
|
247
246
|
- 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
|
|
247
|
+
- lib/trifle/stats/transponder/expression.rb
|
|
248
|
+
- lib/trifle/stats/transponder/expression_engine.rb
|
|
258
249
|
- lib/trifle/stats/version.rb
|
|
259
250
|
- trifle-stats.gemspec
|
|
260
251
|
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
|