From 21e05a06c2a632386b4920f65a7ab511baeec643 Mon Sep 17 00:00:00 2001 From: Jozef Vaclavik Date: Thu, 25 Apr 2024 20:52:48 +0400 Subject: [PATCH] feat: Introduce Series and refactor aggregators, formatters and transponders --- lib/trifle/stats.rb | 7 +- lib/trifle/stats/aggregator/max.rb | 17 +---- lib/trifle/stats/aggregator/min.rb | 17 +---- lib/trifle/stats/aggregator/sum.rb | 17 +---- lib/trifle/stats/driver/redis.rb | 2 +- lib/trifle/stats/formatter/category.rb | 21 ++++++ lib/trifle/stats/formatter/timeline.rb | 11 +--- lib/trifle/stats/mixins/packer.rb | 4 +- lib/trifle/stats/series.rb | 64 +++++++++++++++++++ lib/trifle/stats/transponder.rb | 25 -------- lib/trifle/stats/transponder/average.rb | 18 +++--- .../stats/transponder/standard_deviation.rb | 28 ++++---- 12 files changed, 125 insertions(+), 106 deletions(-) create mode 100644 lib/trifle/stats/formatter/category.rb create mode 100644 lib/trifle/stats/series.rb delete mode 100644 lib/trifle/stats/transponder.rb diff --git a/lib/trifle/stats.rb b/lib/trifle/stats.rb index 7b2bdcd..73c1558 100644 --- a/lib/trifle/stats.rb +++ b/lib/trifle/stats.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'trifle/stats/mixins/packer' +require 'trifle/stats/series' require 'trifle/stats/aggregator/max' require 'trifle/stats/aggregator/min' require 'trifle/stats/aggregator/sum' @@ -12,6 +13,7 @@ require 'trifle/stats/driver/process' require 'trifle/stats/driver/redis' require 'trifle/stats/driver/sqlite' +require 'trifle/stats/formatter/category' require 'trifle/stats/formatter/timeline' require 'trifle/stats/nocturnal' require 'trifle/stats/configuration' @@ -19,7 +21,6 @@ require 'trifle/stats/operations/timeseries/increment' require 'trifle/stats/operations/timeseries/set' require 'trifle/stats/operations/timeseries/values' -require 'trifle/stats/transponder' require 'trifle/stats/transponder/average' require 'trifle/stats/transponder/standard_deviation' require 'trifle/stats/version' @@ -75,5 +76,9 @@ def self.values(key:, from:, to:, range:, config: nil) config: config ).perform end + + def self.series(**params) + Trifle::Stats::Series.new(values(**params)) + end end end diff --git a/lib/trifle/stats/aggregator/max.rb b/lib/trifle/stats/aggregator/max.rb index 47da0cb..820bec1 100644 --- a/lib/trifle/stats/aggregator/max.rb +++ b/lib/trifle/stats/aggregator/max.rb @@ -4,21 +4,10 @@ module Trifle module Stats class Aggregator class Max - include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_aggregator(:max, self) - attr_reader :series, :path - - def initialize(series:, path:) - @series = series - @series[:values] = self.class.normalize(@series[:values]) - @path = path - end - - def keys - @keys ||= path.split('.') - end - - def aggregate + def aggregate(series:, path:) + keys = path.split('.') result = series[:values].map do |data| data.dig(*keys).to_f end diff --git a/lib/trifle/stats/aggregator/min.rb b/lib/trifle/stats/aggregator/min.rb index 8202d77..5331e4f 100644 --- a/lib/trifle/stats/aggregator/min.rb +++ b/lib/trifle/stats/aggregator/min.rb @@ -4,21 +4,10 @@ module Trifle module Stats class Aggregator class Min - include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_aggregator(:min, self) - attr_reader :series, :path - - def initialize(series:, path:) - @series = series - @series[:values] = self.class.normalize(@series[:values]) - @path = path - end - - def keys - @keys ||= path.split('.') - end - - def aggregate + def aggregate(series:, path:) + keys = path.split('.') result = series[:values].map do |data| data.dig(*keys).to_f end diff --git a/lib/trifle/stats/aggregator/sum.rb b/lib/trifle/stats/aggregator/sum.rb index a1113e1..4746ad4 100644 --- a/lib/trifle/stats/aggregator/sum.rb +++ b/lib/trifle/stats/aggregator/sum.rb @@ -4,21 +4,10 @@ module Trifle module Stats class Aggregator class Sum - include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_aggregator(:sum, self) - attr_reader :series, :path - - def initialize(series:, path:) - @series = series - @series[:values] = self.class.normalize(@series[:values]) - @path = path - end - - def keys - @keys ||= path.split('.') - end - - def aggregate + def aggregate(series:, path:) + keys = path.split('.') result = series[:values].map do |data| data.dig(*keys).to_f end diff --git a/lib/trifle/stats/driver/redis.rb b/lib/trifle/stats/driver/redis.rb index 7ba15a8..464bf89 100644 --- a/lib/trifle/stats/driver/redis.rb +++ b/lib/trifle/stats/driver/redis.rb @@ -9,7 +9,7 @@ class Redis include Mixins::Packer attr_accessor :client, :prefix, :separator - def initialize(client = ::Redis.current, prefix: 'trfl') + def initialize(client, prefix: 'trfl') @client = client @prefix = prefix @separator = '::' diff --git a/lib/trifle/stats/formatter/category.rb b/lib/trifle/stats/formatter/category.rb new file mode 100644 index 0000000..473cb4b --- /dev/null +++ b/lib/trifle/stats/formatter/category.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Trifle + module Stats + class Formatter + class Category + Trifle::Stats::Series.register_formatter(:category, self) + + def format(series:, path:) + keys = path.split('.') + series[:at].each_with_object(Hash.new(0)).with_index do |(_at, map), i| + series[:values][i].dig(*keys).each do |key, value| + k, v = block_given? ? yield(key, value) : [key.to_s, value.to_f] + map[k] += v + end + end + end + end + end + end +end diff --git a/lib/trifle/stats/formatter/timeline.rb b/lib/trifle/stats/formatter/timeline.rb index c0ec4ef..35d76e8 100644 --- a/lib/trifle/stats/formatter/timeline.rb +++ b/lib/trifle/stats/formatter/timeline.rb @@ -4,16 +4,9 @@ module Trifle module Stats class Formatter class Timeline - include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_formatter(:timeline, self) - attr_reader :series - - def initialize(series:) - @series = series - @series[:values] = self.class.normalize(@series[:values]) - end - - def format(path:) + def format(series:, path:) keys = path.split('.') series[:at].map.with_index do |at, i| value = series[:values][i].dig(*keys) diff --git a/lib/trifle/stats/mixins/packer.rb b/lib/trifle/stats/mixins/packer.rb index de11b34..1ab1d59 100644 --- a/lib/trifle/stats/mixins/packer.rb +++ b/lib/trifle/stats/mixins/packer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'bigdecimal' + module Trifle module Stats module Mixins @@ -56,7 +58,7 @@ def normalize(object) when Array object.map { |v| normalize(v) } else - object + BigDecimal(object) end end end diff --git a/lib/trifle/stats/series.rb b/lib/trifle/stats/series.rb new file mode 100644 index 0000000..8cd78b1 --- /dev/null +++ b/lib/trifle/stats/series.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Trifle + module Stats + class Series + include Trifle::Stats::Mixins::Packer + + attr_accessor :series + + def initialize(series) + @series = series + @series[:values] = self.class.normalize(@series[:values]) + end + + class Aggregator + def initialize(series) + @series = series + end + end + + def aggregate + @aggregate ||= Aggregator.new(self) + end + + def self.register_aggregator(name, klass) + Aggregator.define_method(name) do |params| + klass.new.aggregate(series: @series.series, **params) + end + end + + class Formatter + def initialize(series) + @series = series + end + end + + def format + @format ||= Formatter.new(self) + end + + def self.register_formatter(name, klass) + Formatter.define_method(name) do |params, &block| + klass.new.format(series: @series.series, **params, &block) + end + end + + class Transponder + def initialize(series) + @series = series + end + end + + def transpond + @transpond ||= Transponder.new(self) + end + + def self.register_transponder(name, klass) + Transponder.define_method(name) do |params| + @series.series = klass.new.transpond(series: @series.series, **params) + end + end + end + end +end diff --git a/lib/trifle/stats/transponder.rb b/lib/trifle/stats/transponder.rb deleted file mode 100644 index 1130062..0000000 --- a/lib/trifle/stats/transponder.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Trifle - module Stats - class Transponder - include Trifle::Stats::Mixins::Packer - - attr_reader :series, :transponders - - def initialize(series:, transponders: []) - @series = series - @series[:values] = self.class.normalize(@series[:values]) - @transponders = transponders - end - - def transpond - transponders.inject(series) do |ser, transponder| - transponder.each.inject(ser) do |s, (p, t)| - t.transpond(series: s, path: p) - end - end - end - end - end -end diff --git a/lib/trifle/stats/transponder/average.rb b/lib/trifle/stats/transponder/average.rb index 2b9d7c2..bcc4575 100644 --- a/lib/trifle/stats/transponder/average.rb +++ b/lib/trifle/stats/transponder/average.rb @@ -5,19 +5,17 @@ module Stats class Transponder class Average include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_transponder(:average, self) - attr_reader :sum, :count - - def initialize(sum: 'sum', count: 'count') - @sum = sum - @count = count - end - - def transpond(series:, path:) - keys = path.split('.') + def transpond(series:, path:, sum: 'sum', count: 'count') # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + keys = path.to_s.split('.') + key = [path, 'average'].compact.join('.') series[:values] = series[:values].map do |data| + dsum = data.dig(*keys, sum) || BigDecimal(0) + dcount = data.dig(*keys, count) || BigDecimal(0) + dres = (dsum / dcount) signal = { - "#{path}.average" => data.dig(*keys, sum) / data.dig(*keys, count) + key => dres.nan? ? BigDecimal(0) : dres } self.class.deep_merge(data, self.class.unpack(hash: signal)) end diff --git a/lib/trifle/stats/transponder/standard_deviation.rb b/lib/trifle/stats/transponder/standard_deviation.rb index ebe5f67..b079c8f 100644 --- a/lib/trifle/stats/transponder/standard_deviation.rb +++ b/lib/trifle/stats/transponder/standard_deviation.rb @@ -5,26 +5,20 @@ module Stats class Transponder class StandardDeviation include Trifle::Stats::Mixins::Packer + Trifle::Stats::Series.register_transponder(:standard_deviation, self) - attr_reader :sum, :count, :square - - def initialize(sum: 'sum', count: 'count', square: 'square') - @sum = sum - @count = count - @square = square - end - - def transpond(series:, path:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - keys = path.split('.') - + def transpond(series:, path:, sum: 'sum', count: 'count', square: 'square') # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + keys = path.to_s.split('.') + key = [path, 'sd'].compact.join('.') series[:values] = series[:values].map do |data| - dcount = data.dig(*keys, count) - dsquare = data.dig(*keys, square) - dsum = data.dig(*keys, sum) + dcount = data.dig(*keys, count) || BigDecimal(0) + dsquare = data.dig(*keys, square) || BigDecimal(0) + dsum = data.dig(*keys, sum) || BigDecimal(0) + dres = Math.sqrt( + (dcount * dsquare - dsum * dsum) / (dcount * (dcount - 1)) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + ) signal = { - "#{path}.sd" => Math.sqrt( - (dcount * dsquare - dsum * dsum) / (dcount * (dcount - 1)) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands - ) + key => dres.nan? ? BigDecimal(0) : dres } self.class.deep_merge(data, self.class.unpack(hash: signal)) end