1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 13:35:21 +02:00

Refactor TimeSeries artifacts (#651)

* Reindent TimeSeries classes

* Fix spacing in time series tests

* Remove trend tests where current is nil

I think if we've gotten this far with a nil value for current, there's a data integrity problem.

If we allow this, we'll have to be very defensive in our code. Best to raise and fix early.

* Reindent Money class

* Refactor TimeSeries artifacts

* Use as_json in TimeSeries

* Bring back tests for trends where current is nil

* Bring back trend test

* Correctly enumerate trend test

* Use favorable_direction for trend_styles helper

* Make trend public in TimeSeries::Value

* Allow nil current values in trends

I think I might've gotten it wrong before, nils might appear in trends if values are unavailable for snapshots

* Clean up TimeSeries::Trend

* Skip trend values same class validations if any values are nil

* Refactor Money

* Remove object parsing in TimeSeries::Value

We're only every passing hashes
This commit is contained in:
Jose Farias 2024-04-22 06:30:42 -06:00 committed by GitHub
parent fe2a2ac3f9
commit fc3ade392a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 258 additions and 206 deletions

View file

@ -1,48 +1,93 @@
class TimeSeries::Trend
attr_reader :type
include ActiveModel::Validations
# Tells us whether an increasing/decreasing trend is good or bad (i.e. a liability decreasing is good)
TYPES = %i[normal inverse].freeze
attr_reader :current, :previous
def initialize(current: nil, previous: nil, type: :normal)
validate_data_types(current, previous)
validate_type(type)
delegate :favorable_direction, to: :series
validate :values_must_be_of_same_type, :values_must_be_of_known_type
def initialize(current:, previous:, series: nil)
@current = current
@previous = previous
@type = type
@series = series
validate!
end
def direction
return "flat" if @previous.nil? || @current == @previous
return "up" if @current && @current > @previous
"down"
if previous.nil? || current == previous
"flat"
elsif current && current > previous
"up"
else
"down"
end.inquiry
end
def value
return Money.new(0) if @previous.nil? && @current.is_a?(Money)
return 0 if @previous.nil?
@current - @previous
if previous.nil?
current.is_a?(Money) ? Money.new(0) : 0
else
current - previous
end
end
def percent
return 0.0 if @previous.nil?
return Float::INFINITY if @previous == 0
((extract_numeric(@current) - extract_numeric(@previous)).abs / extract_numeric(@previous).abs.to_f * 100).round(1).to_f
if previous.nil?
0.0
elsif previous.zero?
Float::INFINITY
else
change = (current_amount - previous_amount).abs
base = previous_amount.abs.to_f
(change / base * 100).round(1).to_f
end
end
def as_json
{
favorable_direction: favorable_direction,
direction: direction,
value: value,
percent: percent
}.as_json
end
private
def validate_type(type)
raise ArgumentError, "Invalid type" unless TYPES.include?(type)
attr_reader :series
def values_must_be_of_same_type
unless current.class == previous.class || [ previous, current ].any?(&:nil?)
errors.add :current, "must be of the same type as previous"
errors.add :previous, "must be of the same type as current"
end
end
def validate_data_types(current, previous)
return if previous.nil? || current.nil?
raise ArgumentError, "Current and previous values must be of the same type" unless current.class == previous.class
raise ArgumentError, "Current and previous values must be of type Money or Numeric" unless current.is_a?(Money) || current.is_a?(Numeric)
def values_must_be_of_known_type
unless current.is_a?(Money) || current.is_a?(Numeric) || current.nil?
errors.add :current, "must be of type Money, Numeric, or nil"
end
unless previous.is_a?(Money) || previous.is_a?(Numeric) || previous.nil?
errors.add :previous, "must be of type Money, Numeric, or nil"
end
end
def current_amount
extract_numeric current
end
def previous_amount
extract_numeric previous
end
def extract_numeric(obj)
return obj.amount if obj.is_a? Money
obj
if obj.is_a? Money
obj.amount
else
obj
end
end
end

View file

@ -1,32 +1,46 @@
class TimeSeries::Value
include Comparable
include Comparable
include ActiveModel::Validations
attr_accessor :trend
attr_reader :value, :date, :original
attr_reader :value, :date, :original, :trend
def initialize(obj)
@original = obj.fetch(:original, obj)
validates :date, presence: true
validate :value_must_be_of_known_type
if obj.is_a?(Hash)
@date = obj[:date]
@value = obj[:value]
else
@date = obj.date
@value = obj.value
end
def initialize(date:, value:, original: nil, series: nil, previous_value: nil)
@date, @value, @original, @series = date, value, original, series
@trend = create_trend previous_value
validate_input
validate!
end
def <=>(other)
result = date <=> other.date
result = value <=> other.value if result == 0
result
end
def as_json
{
date: date,
value: value.as_json,
trend: trend.as_json
}
end
private
attr_reader :series
def create_trend(previous_value)
TimeSeries::Trend.new \
current: value,
previous: previous_value,
series: series
end
def <=>(other)
result = date <=> other.date
result = value <=> other.value if result == 0
result
def value_must_be_of_known_type
unless value.is_a?(Money) || value.is_a?(Numeric)
errors.add :value, "must be a Money or Numeric"
end
end
private
def validate_input
raise ArgumentError, "Date is required" unless @date.is_a?(Date)
raise ArgumentError, "Money or Numeric value is required" unless @value.is_a?(Money) || @value.is_a?(Numeric)
end
end