mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-24 23:59:40 +02:00
Add Money and Money Series (#505)
* Add Money class * Standardize creation of money series * Formatting * Fix test
This commit is contained in:
parent
89ea12e9a1
commit
0fe9b6d34a
16 changed files with 228 additions and 161 deletions
|
@ -28,29 +28,17 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def balance_series(period)
|
||||
filtered_balances = balances.in_period(period).order(:date)
|
||||
return nil if filtered_balances.empty?
|
||||
|
||||
series_data = [ nil, *filtered_balances ].each_cons(2).map do |previous, current|
|
||||
trend = current&.trend(previous)
|
||||
{ data: current, trend: { amount: trend&.amount, direction: trend&.direction, percent: trend&.percent } }
|
||||
end
|
||||
|
||||
last_balance = series_data.last[:data]
|
||||
|
||||
{
|
||||
series_data: series_data,
|
||||
last_balance: last_balance.balance,
|
||||
trend: last_balance.trend(series_data.first[:data])
|
||||
}
|
||||
MoneySeries.new(
|
||||
balances.in_period(period).order(:date),
|
||||
{ trend_type: classification }
|
||||
)
|
||||
end
|
||||
|
||||
def valuation_series
|
||||
series_data = [ nil, *valuations.order(:date) ].each_cons(2).map do |previous, current|
|
||||
{ value: current, trend: current&.trend(previous) }
|
||||
end
|
||||
|
||||
series_data.reverse_each
|
||||
MoneySeries.new(
|
||||
valuations.order(:date),
|
||||
{ trend_type: classification, amount_accessor: :value }
|
||||
)
|
||||
end
|
||||
|
||||
def check_currency
|
||||
|
|
32
app/models/money.rb
Normal file
32
app/models/money.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
class Money
|
||||
attr_reader :amount, :currency
|
||||
|
||||
def self.from_amount(amount, currency = "USD")
|
||||
Money.new(amount, currency)
|
||||
end
|
||||
|
||||
def initialize(amount, currency = :USD)
|
||||
@amount = amount
|
||||
@currency = currency
|
||||
end
|
||||
|
||||
def cents(precision: nil)
|
||||
_precision = precision || CURRENCY_OPTIONS[@currency.to_sym][:precision]
|
||||
return "" unless _precision.positive?
|
||||
|
||||
fractional_part = @amount.to_s.split(".")[1] || ""
|
||||
fractional_part = fractional_part[0, _precision].ljust(_precision, "0")
|
||||
end
|
||||
|
||||
def symbol
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:symbol]
|
||||
end
|
||||
|
||||
def separator
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:separator]
|
||||
end
|
||||
|
||||
def precision
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:precision]
|
||||
end
|
||||
end
|
60
app/models/money_series.rb
Normal file
60
app/models/money_series.rb
Normal file
|
@ -0,0 +1,60 @@
|
|||
class MoneySeries
|
||||
def initialize(series, options = {})
|
||||
@trend_type = options[:trend_type] || :asset # Defines whether a positive trend is good or bad
|
||||
@accessor = options[:amount_accessor] || :balance
|
||||
@series = series
|
||||
end
|
||||
|
||||
def valid?
|
||||
@series.length > 1
|
||||
end
|
||||
|
||||
def data
|
||||
[ nil, *@series ].each_cons(2).map do |previous, current|
|
||||
{
|
||||
raw: current,
|
||||
date: current.date,
|
||||
value: Money.from_amount(current.send(@accessor), current.currency),
|
||||
trend: Trend.new(
|
||||
current: current.send(@accessor),
|
||||
previous: previous&.send(@accessor),
|
||||
type: @trend_type
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def trend
|
||||
return Trend.new(current: 0, type: @trend_type) unless valid?
|
||||
|
||||
Trend.new(
|
||||
current: @series.last.send(@accessor),
|
||||
previous: @series.first&.send(@accessor),
|
||||
type: @trend_type
|
||||
)
|
||||
end
|
||||
|
||||
def serialize_for_d3_chart
|
||||
{
|
||||
data: data.map do |datum|
|
||||
{
|
||||
date: datum[:date],
|
||||
amount: datum[:value].amount,
|
||||
currency: datum[:value].currency,
|
||||
trend: {
|
||||
amount: datum[:trend].amount,
|
||||
percent: datum[:trend].percent,
|
||||
direction: datum[:trend].direction,
|
||||
type: datum[:trend].type
|
||||
}
|
||||
}
|
||||
end,
|
||||
trend: {
|
||||
amount: trend.amount,
|
||||
percent: trend.percent,
|
||||
direction: trend.direction,
|
||||
type: trend.type
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
end
|
|
@ -1,26 +1,27 @@
|
|||
class Trend
|
||||
attr_reader :current, :previous
|
||||
attr_reader :current, :previous, :type
|
||||
|
||||
def initialize(current, previous)
|
||||
@current = current
|
||||
@previous = previous
|
||||
end
|
||||
def initialize(current:, previous: nil, type: :asset)
|
||||
@current = current
|
||||
@previous = previous
|
||||
@type = type # :asset means positive trend is good, :liability means negative trend is good
|
||||
end
|
||||
|
||||
def direction
|
||||
return "flat" unless @previous
|
||||
return "up" if @current > @previous
|
||||
return "down" if @current < @previous
|
||||
"flat"
|
||||
end
|
||||
def direction
|
||||
return "flat" unless @previous
|
||||
return "up" if @current > @previous
|
||||
return "down" if @current < @previous
|
||||
"flat"
|
||||
end
|
||||
|
||||
def amount
|
||||
return 0 if @previous.nil?
|
||||
@current - @previous
|
||||
end
|
||||
def amount
|
||||
return 0 if @previous.nil?
|
||||
@current - @previous
|
||||
end
|
||||
|
||||
def percent
|
||||
return 0 if @previous.nil?
|
||||
return Float::INFINITY if @previous == 0
|
||||
((@current - @previous).abs / @previous.to_f * 100).round(1)
|
||||
end
|
||||
def percent
|
||||
return 0 if @previous.nil?
|
||||
return Float::INFINITY if @previous == 0
|
||||
((@current - @previous).abs / @previous.to_f * 100).round(1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class Valuation < ApplicationRecord
|
|||
after_commit :sync_account
|
||||
|
||||
def trend(previous)
|
||||
Trend.new(value, previous&.value)
|
||||
Trend.new(current: value, previous: previous&.value, type: account.classification)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue