1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 12:49:38 +02:00

Rubocop updates (#1118)

* Minimal code style enforcement

* Formatting and lint code updates (no change in functionality)
This commit is contained in:
Zach Gollwitzer 2024-08-23 10:06:24 -04:00 committed by GitHub
parent 359bceb58e
commit eef4c2643b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 529 additions and 519 deletions

7
.editorconfig Normal file
View file

@ -0,0 +1,7 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2

View file

@ -1,12 +1,15 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
inherit_gem:
rubocop-rails-omakase: rubocop.yml
Layout/IndentationWidth:
Enabled: true
# Overwrite or add rules to create your own house style
#
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
# Layout/SpaceInsideArrayLiteralBrackets:
# Enabled: false
Layout/ElseAlignment:
Enabled: false
Layout/EndAlignment:
Enabled: false
Layout/IndentationStyle:
EnforcedStyle: spaces
IndentationWidth: 2
Layout/IndentationConsistency:
Enabled: true
Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: true

View file

@ -42,12 +42,12 @@ gem "inline_svg"
gem "octokit"
gem "pagy"
gem "rails-settings-cached"
gem "tzinfo-data", platforms: %i[ windows jruby ]
gem "tzinfo-data", platforms: %i[windows jruby]
gem "csv"
gem "redcarpet"
group :development, :test do
gem "debug", platforms: %i[ mri windows ]
gem "debug", platforms: %i[mri windows]
gem "brakeman", require: false
gem "rubocop-rails-omakase", require: false
gem "i18n-tasks"

View file

@ -2,7 +2,7 @@ class Account::EntriesController < ApplicationController
layout :with_sidebar
before_action :set_account
before_action :set_entry, only: %i[ edit update show destroy ]
before_action :set_entry, only: %i[edit update show destroy]
def edit
render entryable_view_path(:edit)

View file

@ -8,7 +8,7 @@ class Account::TradesController < ApplicationController
end
def index
@entries = @account.entries.reverse_chronological.where(entryable_type: %w[ Account::Trade Account::Transaction ])
@entries = @account.entries.reverse_chronological.where(entryable_type: %w[Account::Trade Account::Transaction])
end
def create

View file

@ -2,7 +2,7 @@ class AccountsController < ApplicationController
layout :with_sidebar
include Filterable
before_action :set_account, only: %i[ edit show destroy sync update ]
before_action :set_account, only: %i[edit show destroy sync update]
def index
@institutions = Current.family.institutions

View file

@ -1,7 +1,7 @@
class CategoriesController < ApplicationController
layout :with_sidebar
before_action :set_category, only: %i[ edit update ]
before_action :set_category, only: %i[edit update]
before_action :set_transaction, only: :create
def index

View file

@ -6,17 +6,17 @@ class Category::DropdownsController < ApplicationController
end
private
def set_from_params
if params[:category_id]
@selected_category = categories_scope.find(params[:category_id])
def set_from_params
if params[:category_id]
@selected_category = categories_scope.find(params[:category_id])
end
if params[:transaction_id]
@transaction = Current.family.transactions.find(params[:transaction_id])
end
end
if params[:transaction_id]
@transaction = Current.family.transactions.find(params[:transaction_id])
def categories_scope
Current.family.categories.alphabetically
end
end
def categories_scope
Current.family.categories.alphabetically
end
end

View file

@ -13,27 +13,27 @@ module Authentication
private
def authenticate_user!
if user = User.find_by(id: session[:user_id])
Current.user = user
else
redirect_to new_session_url
def authenticate_user!
if user = User.find_by(id: session[:user_id])
Current.user = user
else
redirect_to new_session_url
end
end
end
def login(user)
Current.user = user
reset_session
session[:user_id] = user.id
set_last_login_at
end
def login(user)
Current.user = user
reset_session
session[:user_id] = user.id
set_last_login_at
end
def logout
Current.user = nil
reset_session
end
def logout
Current.user = nil
reset_session
end
def set_last_login_at
Current.user.update(last_login_at: DateTime.now)
end
def set_last_login_at
Current.user.update(last_login_at: DateTime.now)
end
end

View file

@ -1,9 +1,9 @@
module Filterable
extend ActiveSupport::Concern
extend ActiveSupport::Concern
included do
before_action :set_period
end
included do
before_action :set_period
end
private

View file

@ -1,7 +1,7 @@
require "ostruct"
class ImportsController < ApplicationController
before_action :set_import, except: %i[ index new create ]
before_action :set_import, except: %i[index new create]
def index
@imports = Current.family.imports

View file

@ -1,5 +1,5 @@
class InstitutionsController < ApplicationController
before_action :set_institution, except: %i[ new create ]
before_action :set_institution, except: %i[new create]
def new
@institution = Institution.new

View file

@ -1,7 +1,7 @@
class MerchantsController < ApplicationController
layout :with_sidebar
before_action :set_merchant, only: %i[ edit update destroy ]
before_action :set_merchant, only: %i[edit update destroy]
def index
@merchants = Current.family.merchants.alphabetically
@ -31,11 +31,11 @@ class MerchantsController < ApplicationController
private
def set_merchant
@merchant = Current.family.merchants.find(params[:id])
end
def set_merchant
@merchant = Current.family.merchants.find(params[:id])
end
def merchant_params
params.require(:merchant).permit(:name, :color)
end
def merchant_params
params.require(:merchant).permit(:name, :color)
end
end

View file

@ -3,7 +3,7 @@ class PasswordResetsController < ApplicationController
layout "auth"
before_action :set_user_by_token, only: %i[ edit update ]
before_action :set_user_by_token, only: %i[edit update]
def new
end
@ -33,12 +33,12 @@ class PasswordResetsController < ApplicationController
private
def set_user_by_token
@user = User.find_by_token_for(:password_reset, params[:token])
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
end
def set_user_by_token
@user = User.find_by_token_for(:password_reset, params[:token])
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
end
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
end

View file

@ -12,7 +12,7 @@ class PasswordsController < ApplicationController
private
def password_params
params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
end
def password_params
params.require(:user).permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
end
end

View file

@ -28,17 +28,17 @@ class RegistrationsController < ApplicationController
private
def set_user
@user = User.new user_params.except(:invite_code)
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code)
end
def claim_invite_code
unless InviteCode.claim! params[:user][:invite_code]
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
def set_user
@user = User.new user_params.except(:invite_code)
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :invite_code)
end
def claim_invite_code
unless InviteCode.claim! params[:user][:invite_code]
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
end
end
end
end

View file

@ -32,8 +32,8 @@ class Settings::ProfilesController < SettingsController
private
def user_params
params.require(:user).permit(:first_name, :last_name, :profile_image,
family_attributes: [ :name, :id ])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :profile_image,
family_attributes: [ :name, :id ])
end
end

View file

@ -1,7 +1,7 @@
class TagsController < ApplicationController
layout :with_sidebar
before_action :set_tag, only: %i[ edit update ]
before_action :set_tag, only: %i[edit update]
def index
@tags = Current.family.tags.alphabetically

View file

@ -5,12 +5,12 @@ class ApplicationMailer < ActionMailer::Base
private
def set_self_host_settings
mail.from = Setting.email_sender
mail.delivery_method.settings.merge!({ address: Setting.smtp_host,
port: Setting.smtp_port,
user_name: Setting.smtp_username,
password: Setting.smtp_password,
tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" })
end
def set_self_host_settings
mail.from = Setting.email_sender
mail.delivery_method.settings.merge!({ address: Setting.smtp_host,
port: Setting.smtp_port,
user_name: Setting.smtp_username,
password: Setting.smtp_password,
tls: ENV.fetch("SMTP_TLS_ENABLED", "true") == "true" })
end
end

View file

@ -1,9 +1,9 @@
class Account::Balance < ApplicationRecord
include Monetizable
include Monetizable
belongs_to :account
validates :account, :date, :balance, presence: true
monetize :balance
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
scope :chronological, -> { order(:date) }
belongs_to :account
validates :account, :date, :balance, presence: true
monetize :balance
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
scope :chronological, -> { order(:date) }
end

View file

@ -1,7 +1,7 @@
class Account::EntryBuilder
include ActiveModel::Model
TYPES = %w[ income expense buy sell interest transfer_in transfer_out ].freeze
TYPES = %w[income expense buy sell interest transfer_in transfer_out].freeze
attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id

View file

@ -1,7 +1,7 @@
module Account::Entryable
extend ActiveSupport::Concern
TYPES = %w[ Account::Valuation Account::Transaction Account::Trade ]
TYPES = %w[Account::Valuation Account::Transaction Account::Trade]
def self.from_type(entryable_type)
entryable_type.presence_in(TYPES).constantize

View file

@ -1,7 +1,7 @@
class Account::TradeBuilder < Account::EntryBuilder
include ActiveModel::Model
TYPES = %w[ buy sell ].freeze
TYPES = %w[buy sell].freeze
attr_accessor :type, :qty, :price, :ticker, :date, :account

View file

@ -25,7 +25,7 @@ class Account::Transaction < ApplicationRecord
private
def searchable_keys
%i[ categories merchants ]
%i[categories merchants]
end
end

View file

@ -1,7 +1,7 @@
class Account::TransactionBuilder
include ActiveModel::Model
TYPES = %w[ income expense interest transfer_in transfer_out ].freeze
TYPES = %w[income expense interest transfer_in transfer_out].freeze
attr_accessor :type, :amount, :date, :account, :transfer_account_id

View file

@ -47,7 +47,7 @@ class Category < ApplicationRecord
private
def clear_internal_category
self.internal_category = nil
end
def clear_internal_category
self.internal_category = nil
end
end

View file

@ -1,8 +1,8 @@
module Accountable
extend ActiveSupport::Concern
ASSET_TYPES = %w[ Depository Investment Crypto Property Vehicle OtherAsset ]
LIABILITY_TYPES = %w[ CreditCard Loan OtherLiability ]
ASSET_TYPES = %w[Depository Investment Crypto Property Vehicle OtherAsset]
LIABILITY_TYPES = %w[CreditCard Loan OtherLiability]
TYPES = ASSET_TYPES + LIABILITY_TYPES
def self.from_type(type)

View file

@ -1,14 +1,14 @@
module Monetizable
extend ActiveSupport::Concern
extend ActiveSupport::Concern
class_methods do
def monetize(*fields)
fields.each do |field|
define_method("#{field}_money") do
value = self.send(field)
value.nil? ? nil : Money.new(value, currency || Money.default_currency)
end
class_methods do
def monetize(*fields)
fields.each do |field|
define_method("#{field}_money") do
value = self.send(field)
value.nil? ? nil : Money.new(value, currency || Money.default_currency)
end
end
end
end
end

View file

@ -16,10 +16,10 @@ class InviteCode < ApplicationRecord
private
def generate_token
loop do
self.token = SecureRandom.hex(4)
break token unless self.class.exists?(token: token)
def generate_token
loop do
self.token = SecureRandom.hex(4)
break token unless self.class.exists?(token: token)
end
end
end
end

View file

@ -1,35 +1,35 @@
class Period
attr_reader :name, :date_range
attr_reader :name, :date_range
def self.find_by_name(name)
INDEX[name]
end
def self.names
INDEX.keys.sort
end
def initialize(name: "custom", date_range:)
@name = name
@date_range = date_range
end
def extend_backward(duration)
Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last)
end
BUILTIN = [
new(name: "all", date_range: nil..Date.current),
new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current),
new(name: "last_30_days", date_range: 30.days.ago.to_date..Date.current),
new(name: "last_365_days", date_range: 365.days.ago.to_date..Date.current)
]
INDEX = BUILTIN.index_by(&:name)
BUILTIN.each do |period|
define_singleton_method(period.name) do
period
end
def self.find_by_name(name)
INDEX[name]
end
def self.names
INDEX.keys.sort
end
def initialize(name: "custom", date_range:)
@name = name
@date_range = date_range
end
def extend_backward(duration)
Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last)
end
BUILTIN = [
new(name: "all", date_range: nil..Date.current),
new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current),
new(name: "last_30_days", date_range: 30.days.ago.to_date..Date.current),
new(name: "last_365_days", date_range: 365.days.ago.to_date..Date.current)
]
INDEX = BUILTIN.index_by(&:name)
BUILTIN.each do |period|
define_singleton_method(period.name) do
period
end
end
end

View file

@ -16,11 +16,11 @@ class Provider::Github
latest_commit = Octokit.branch(repo, branch)
release_info = if latest_release
{
version: latest_version,
url: latest_release.html_url,
commit_sha: Octokit.commit(repo, latest_release.tag_name).sha
}
{
version: latest_version,
url: latest_release.html_url,
commit_sha: Octokit.commit(repo, latest_release.tag_name).sha
}
end
commit_info = {

View file

@ -1,5 +1,5 @@
class TimeSeries
DIRECTIONS = %w[ up down ].freeze
DIRECTIONS = %w[up down].freeze
attr_reader :values, :favorable_direction

View file

@ -74,18 +74,18 @@ class User < ApplicationRecord
private
def last_user_in_family?
family.users.count == 1
end
def deactivated_email
email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
def profile_image_size
if profile_image.attached? && profile_image.byte_size > 5.megabytes
# i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size')
errors.add(:profile_image, :invalid_file_size, max_megabytes: 5)
def last_user_in_family?
family.users.count == 1
end
def deactivated_email
email.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
def profile_image_size
if profile_image.attached? && profile_image.byte_size > 5.megabytes
# i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size')
errors.add(:profile_image, :invalid_file_size, max_megabytes: 5)
end
end
end
end

View file

@ -3,100 +3,100 @@
attr_reader :name, :children, :value, :currency
def initialize(name, currency = Money.default_currency)
@name = name
@currency = Money::Currency.new(currency)
@children = []
@name = name
@currency = Money::Currency.new(currency)
@children = []
end
def sum
return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil?
children.sum(&:sum)
return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil?
children.sum(&:sum)
end
def avg
return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil?
leaf_values = value_nodes.map(&:value)
leaf_values.compact.sum / leaf_values.compact.size
return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil?
leaf_values = value_nodes.map(&:value)
leaf_values.compact.sum / leaf_values.compact.size
end
def series
return @series if is_value_node?
return @series if is_value_node?
summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc|
child.series.values.each do |series_value|
acc[series_value.date] += series_value.value
end
summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc|
child.series.values.each do |series_value|
acc[series_value.date] += series_value.value
end
end
summed_series = summed_by_date.map { |date, value| { date: date, value: value } }
summed_series = summed_by_date.map { |date, value| { date: date, value: value } }
TimeSeries.new(summed_series)
TimeSeries.new(summed_series)
end
def series=(series)
raise "Cannot set series on a non-leaf node" unless is_value_node?
raise "Cannot set series on a non-leaf node" unless is_value_node?
_series = series || TimeSeries.new([])
_series = series || TimeSeries.new([])
raise "Series must be an instance of TimeSeries" unless _series.is_a?(TimeSeries)
raise "Series must contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency }
@series = _series
raise "Series must be an instance of TimeSeries" unless _series.is_a?(TimeSeries)
raise "Series must contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency }
@series = _series
end
def value_nodes
return [ self ] unless value.nil?
children.flat_map { |child| child.value_nodes }
return [ self ] unless value.nil?
children.flat_map { |child| child.value_nodes }
end
def empty?
value_nodes.empty?
value_nodes.empty?
end
def percent_of_total
return 100 if parent.nil? || parent.sum.zero?
return 100 if parent.nil? || parent.sum.zero?
((sum / parent.sum) * 100).round(1)
((sum / parent.sum) * 100).round(1)
end
def add_child_group(name, currency = Money.default_currency)
raise "Cannot add subgroup to node with a value" if is_value_node?
child = self.class.new(name, currency)
child.parent = self
@children << child
child
raise "Cannot add subgroup to node with a value" if is_value_node?
child = self.class.new(name, currency)
child.parent = self
@children << child
child
end
def add_value_node(original, value, series = nil)
raise "Cannot add value node to a non-leaf node" unless can_add_value_node?
child = self.class.new(original.name)
child.original = original
child.value = value
child.series = series
child.parent = self
@children << child
child
raise "Cannot add value node to a non-leaf node" unless can_add_value_node?
child = self.class.new(original.name)
child.original = original
child.value = value
child.series = series
child.parent = self
@children << child
child
end
def value=(value)
raise "Cannot set value on a non-leaf node" unless is_leaf_node?
raise "Value must be an instance of Money" unless value.is_a?(Money)
@value = value
@currency = value.currency
raise "Cannot set value on a non-leaf node" unless is_leaf_node?
raise "Value must be an instance of Money" unless value.is_a?(Money)
@value = value
@currency = value.currency
end
def is_leaf_node?
children.empty?
children.empty?
end
def is_value_node?
value.present?
value.present?
end
private
def can_add_value_node?
return false if is_value_node?
children.empty? || children.all?(&:is_value_node?)
end
def can_add_value_node?
return false if is_value_node?
children.empty? || children.all?(&:is_value_node?)
end
end

View file

@ -63,5 +63,5 @@ Rails.application.configure do
# Raise error when a before_action's only/except options reference missing actions
config.action_controller.raise_on_missing_callback_actions = true
config.autoload_paths += %w[ test/support ]
config.autoload_paths += %w[test/support]
end

View file

@ -42,8 +42,8 @@ Rails.application.routes.draw do
end
end
resources :tags, except: %i[ show destroy ] do
resources :deletions, only: %i[ new create ], module: :tag
resources :tags, except: %i[show destroy] do
resources :deletions, only: %i[new create], module: :tag
end
namespace :category do
@ -51,16 +51,16 @@ Rails.application.routes.draw do
end
resources :categories do
resources :deletions, only: %i[ new create ], module: :category
resources :deletions, only: %i[new create], module: :category
end
resources :merchants, only: %i[ index new create edit update destroy ]
resources :merchants, only: %i[index new create edit update destroy]
namespace :account do
resources :transfers, only: %i[ new create destroy ]
resources :transfers, only: %i[new create destroy]
namespace :transaction do
resources :rules, only: %i[ index ]
resources :rules, only: %i[index]
end
end
@ -78,21 +78,21 @@ Rails.application.routes.draw do
scope module: :account do
resource :logo, only: :show
resources :holdings, only: %i[ index new show ]
resources :holdings, only: %i[index new show]
resources :cashes, only: :index
resources :transactions, only: %i[ index update ]
resources :valuations, only: %i[ index new create ]
resources :trades, only: %i[ index new create ]
resources :transactions, only: %i[index update]
resources :valuations, only: %i[index new create]
resources :trades, only: %i[index new create]
resources :entries, only: %i[ edit update show destroy ]
resources :entries, only: %i[edit update show destroy]
end
end
resources :properties, only: %i[ create update ]
resources :vehicles, only: %i[ create update ]
resources :properties, only: %i[create update]
resources :vehicles, only: %i[create update]
resources :transactions, only: %i[ index new create ] do
resources :transactions, only: %i[index new create] do
collection do
post "bulk_delete"
get "bulk_edit"
@ -103,7 +103,7 @@ Rails.application.routes.draw do
end
end
resources :institutions, except: %i[ index show ]
resources :institutions, except: %i[index show]
resources :issues, only: :show

View file

@ -1,62 +1,62 @@
module Money::Arithmetic
CoercedNumeric = Struct.new(:value)
CoercedNumeric = Struct.new(:value)
def +(other)
if other.is_a?(Money)
self.class.new(amount + other.amount, currency)
else
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount + value, currency)
end
def +(other)
if other.is_a?(Money)
self.class.new(amount + other.amount, currency)
else
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount + value, currency)
end
end
def -(other)
if other.is_a?(Money)
self.class.new(amount - other.amount, currency)
else
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount - value, currency)
end
def -(other)
if other.is_a?(Money)
self.class.new(amount - other.amount, currency)
else
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount - value, currency)
end
end
def -@
self.class.new(-amount, currency)
end
def -@
self.class.new(-amount, currency)
end
def *(other)
raise TypeError, "Can't multiply Money by Money, use Numeric instead" if other.is_a?(self.class)
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount * value, currency)
end
def *(other)
raise TypeError, "Can't multiply Money by Money, use Numeric instead" if other.is_a?(self.class)
value = other.is_a?(CoercedNumeric) ? other.value : other
self.class.new(amount * value, currency)
end
def /(other)
if other.is_a?(self.class)
amount / other.amount
else
raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric)
self.class.new(amount / other, currency)
end
def /(other)
if other.is_a?(self.class)
amount / other.amount
else
raise TypeError, "can't divide Numeric by Money" if other.is_a?(CoercedNumeric)
self.class.new(amount / other, currency)
end
end
def abs
self.class.new(amount.abs, currency)
end
def abs
self.class.new(amount.abs, currency)
end
def zero?
amount.zero?
end
def zero?
amount.zero?
end
def negative?
amount.negative?
end
def negative?
amount.negative?
end
def positive?
amount.positive?
end
def positive?
amount.positive?
end
# Override Ruby's coerce method so the order of operands doesn't matter
# Wrap in Coerced so we can distinguish between Money and other types
def coerce(other)
[ self, CoercedNumeric.new(other) ]
end
# Override Ruby's coerce method so the order of operands doesn't matter
# Wrap in Coerced so we can distinguish between Money and other types
def coerce(other)
[ self, CoercedNumeric.new(other) ]
end
end

View file

@ -1,69 +1,69 @@
class Money::Currency
include Comparable
include Comparable
class UnknownCurrencyError < ArgumentError; end
class UnknownCurrencyError < ArgumentError; end
CURRENCIES_FILE_PATH = Rails.root.join("config", "currencies.yml")
CURRENCIES_FILE_PATH = Rails.root.join("config", "currencies.yml")
# Cached instances by iso code
@@instances = {}
# Cached instances by iso code
@@instances = {}
class << self
def new(object)
iso_code = case object
when String, Symbol
object.to_s.downcase
when Money::Currency
object.iso_code.downcase
else
raise ArgumentError, "Invalid argument type"
end
class << self
def new(object)
iso_code = case object
when String, Symbol
object.to_s.downcase
when Money::Currency
object.iso_code.downcase
else
raise ArgumentError, "Invalid argument type"
end
@@instances[iso_code] ||= super(iso_code)
end
def all
@all ||= YAML.load_file(CURRENCIES_FILE_PATH)
end
def all_instances
all.values.map { |currency_data| new(currency_data["iso_code"]) }
end
def popular
all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) }
end
@@instances[iso_code] ||= super(iso_code)
end
attr_reader :name, :priority, :iso_code, :iso_numeric, :html_code,
:symbol, :minor_unit, :minor_unit_conversion, :smallest_denomination,
:separator, :delimiter, :default_format, :default_precision
def initialize(iso_code)
currency_data = self.class.all[iso_code]
raise UnknownCurrencyError if currency_data.nil?
@name = currency_data["name"]
@priority = currency_data["priority"]
@iso_code = currency_data["iso_code"]
@iso_numeric = currency_data["iso_numeric"]
@html_code = currency_data["html_code"]
@symbol = currency_data["symbol"]
@minor_unit = currency_data["minor_unit"]
@minor_unit_conversion = currency_data["minor_unit_conversion"]
@smallest_denomination = currency_data["smallest_denomination"]
@separator = currency_data["separator"]
@delimiter = currency_data["delimiter"]
@default_format = currency_data["default_format"]
@default_precision = currency_data["default_precision"]
def all
@all ||= YAML.load_file(CURRENCIES_FILE_PATH)
end
def step
(1.0/10**default_precision)
def all_instances
all.values.map { |currency_data| new(currency_data["iso_code"]) }
end
def <=>(other)
return nil unless other.is_a?(Money::Currency)
@iso_code <=> other.iso_code
def popular
all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) }
end
end
attr_reader :name, :priority, :iso_code, :iso_numeric, :html_code,
:symbol, :minor_unit, :minor_unit_conversion, :smallest_denomination,
:separator, :delimiter, :default_format, :default_precision
def initialize(iso_code)
currency_data = self.class.all[iso_code]
raise UnknownCurrencyError if currency_data.nil?
@name = currency_data["name"]
@priority = currency_data["priority"]
@iso_code = currency_data["iso_code"]
@iso_numeric = currency_data["iso_numeric"]
@html_code = currency_data["html_code"]
@symbol = currency_data["symbol"]
@minor_unit = currency_data["minor_unit"]
@minor_unit_conversion = currency_data["minor_unit_conversion"]
@smallest_denomination = currency_data["smallest_denomination"]
@separator = currency_data["separator"]
@delimiter = currency_data["delimiter"]
@default_format = currency_data["default_format"]
@default_precision = currency_data["default_precision"]
end
def step
(1.0/10**default_precision)
end
def <=>(other)
return nil unless other.is_a?(Money::Currency)
@iso_code <=> other.iso_code
end
end

View file

@ -1,53 +1,53 @@
require "test_helper"
class Money::CurrencyTest < ActiveSupport::TestCase
setup do
@currency = Money::Currency.new(:usd)
end
setup do
@currency = Money::Currency.new(:usd)
end
test "has many currencies" do
assert_operator Money::Currency.all.count, :>, 100
end
test "has many currencies" do
assert_operator Money::Currency.all.count, :>, 100
end
test "can test equality of currencies" do
assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd)
assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur)
end
test "can test equality of currencies" do
assert_equal Money::Currency.new(:usd), Money::Currency.new(:usd)
assert_not_equal Money::Currency.new(:usd), Money::Currency.new(:eur)
end
test "can get metadata about a currency" do
assert_equal "USD", @currency.iso_code
assert_equal "United States Dollar", @currency.name
assert_equal "$", @currency.symbol
assert_equal 1, @currency.priority
assert_equal "Cent", @currency.minor_unit
assert_equal 100, @currency.minor_unit_conversion
assert_equal 1, @currency.smallest_denomination
assert_equal ".", @currency.separator
assert_equal ",", @currency.delimiter
assert_equal "%u%n", @currency.default_format
assert_equal 2, @currency.default_precision
end
test "can get metadata about a currency" do
assert_equal "USD", @currency.iso_code
assert_equal "United States Dollar", @currency.name
assert_equal "$", @currency.symbol
assert_equal 1, @currency.priority
assert_equal "Cent", @currency.minor_unit
assert_equal 100, @currency.minor_unit_conversion
assert_equal 1, @currency.smallest_denomination
assert_equal ".", @currency.separator
assert_equal ",", @currency.delimiter
assert_equal "%u%n", @currency.default_format
assert_equal 2, @currency.default_precision
end
test "can extract cents string from amount" do
value1 = Money.new(100)
value2 = Money.new(100.1)
value3 = Money.new(100.12)
value4 = Money.new(100.123)
value5 = Money.new(200, :jpy)
test "can extract cents string from amount" do
value1 = Money.new(100)
value2 = Money.new(100.1)
value3 = Money.new(100.12)
value4 = Money.new(100.123)
value5 = Money.new(200, :jpy)
assert_equal "00", value1.cents_str
assert_equal "10", value2.cents_str
assert_equal "12", value3.cents_str
assert_equal "12", value4.cents_str
assert_equal "", value5.cents_str
assert_equal "00", value1.cents_str
assert_equal "10", value2.cents_str
assert_equal "12", value3.cents_str
assert_equal "12", value4.cents_str
assert_equal "", value5.cents_str
assert_equal "", value4.cents_str(0)
assert_equal "1", value4.cents_str(1)
assert_equal "12", value4.cents_str(2)
assert_equal "123", value4.cents_str(3)
end
assert_equal "", value4.cents_str(0)
assert_equal "1", value4.cents_str(1)
assert_equal "12", value4.cents_str(2)
assert_equal "123", value4.cents_str(3)
end
test "step returns the smallest value of the currency" do
assert_equal 0.01, @currency.step
end
test "step returns the smallest value of the currency" do
assert_equal 0.01, @currency.step
end
end

View file

@ -39,7 +39,7 @@ class Import::CsvTest < ActiveSupport::TestCase
test "CSV with semicolon column separator" do
csv = Import::Csv.new(valid_csv_str_with_semicolon_separator, col_sep: ";")
assert_equal %w[ date name category tags amount ], csv.table.headers
assert_equal %w[date name category tags amount], csv.table.headers
assert_equal 4, csv.table.size
assert_equal "Paycheck", csv.table[3][1]
end
@ -84,7 +84,7 @@ class Import::CsvTest < ActiveSupport::TestCase
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings)
assert_equal %w[ date name ], csv.table.headers
assert_equal %w[date name], csv.table.headers
assert_equal 2, csv.table.size
assert_equal "Amazon stuff", csv.table[1][1]
end
@ -113,7 +113,7 @@ class Import::CsvTest < ActiveSupport::TestCase
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings, ";")
assert_equal %w[ date name ], csv.table.headers
assert_equal %w[date name], csv.table.headers
assert_equal 2, csv.table.size
assert_equal "Amazon stuff", csv.table[1][1]
end

View file

@ -1,141 +1,141 @@
require "test_helper"
require "ostruct"
class ValueGroupTest < ActiveSupport::TestCase
setup do
# Level 1
@assets = ValueGroup.new("Assets", :usd)
setup do
# Level 1
@assets = ValueGroup.new("Assets", :usd)
# Level 2
@depositories = @assets.add_child_group("Depositories", :usd)
@other_assets = @assets.add_child_group("Other Assets", :usd)
# Level 2
@depositories = @assets.add_child_group("Depositories", :usd)
@other_assets = @assets.add_child_group("Other Assets", :usd)
# Level 3 (leaf/value nodes)
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000))
@savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000))
@collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550))
# Level 3 (leaf/value nodes)
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000))
@savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000))
@collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550))
end
test "empty group works" do
group = ValueGroup.new("Root", :usd)
assert_equal "Root", group.name
assert_equal [], group.children
assert_equal 0, group.sum
assert_equal 0, group.avg
assert_equal 100, group.percent_of_total
assert_nil group.parent
end
test "group without value nodes has no value" do
assets = ValueGroup.new("Assets")
depositories = assets.add_child_group("Depositories")
assert_equal 0, assets.sum
assert_equal 0, depositories.sum
end
test "sum equals value at leaf level" do
assert_equal @checking_node.value, @checking_node.sum
assert_equal @savings_node.value, @savings_node.sum
assert_equal @collectable_node.value, @collectable_node.sum
end
test "value is nil at rollup levels" do
assert_not_equal @depositories.value, @depositories.sum
assert_nil @depositories.value
assert_nil @other_assets.value
end
test "generates list of value nodes regardless of level in hierarchy" do
assert_equal [ @checking_node, @savings_node, @collectable_node ], @assets.value_nodes
assert_equal [ @checking_node, @savings_node ], @depositories.value_nodes
assert_equal [ @collectable_node ], @other_assets.value_nodes
end
test "group with value nodes aggregates totals correctly" do
assert_equal Money.new(5000), @checking_node.sum
assert_equal Money.new(20000), @savings_node.sum
assert_equal Money.new(550), @collectable_node.sum
assert_equal Money.new(25000), @depositories.sum
assert_equal Money.new(550), @other_assets.sum
assert_equal Money.new(25550), @assets.sum
end
test "group averages leaf nodes" do
assert_equal Money.new(5000), @checking_node.avg
assert_equal Money.new(20000), @savings_node.avg
assert_equal Money.new(550), @collectable_node.avg
assert_in_delta 12500, @depositories.avg.amount, 0.01
assert_in_delta 550, @other_assets.avg.amount, 0.01
assert_in_delta 8516.67, @assets.avg.amount, 0.01
end
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
test "group calculates percent of parent total" do
assert_equal 100, @assets.percent_of_total
assert_in_delta 97.85, @depositories.percent_of_total, 0.1
assert_in_delta 2.15, @other_assets.percent_of_total, 0.1
assert_in_delta 80.0, @savings_node.percent_of_total, 0.1
assert_in_delta 20.0, @checking_node.percent_of_total, 0.1
assert_equal 100, @collectable_node.percent_of_total
end
test "handles unbalanced tree" do
vehicles = @assets.add_child_group("Vehicles")
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
assert_equal Money.new(25550), @assets.sum
end
test "can attach and aggregate time series" do
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
@checking_node.series = checking_series
@savings_node.series = savings_series
assert_not_nil @checking_node.series
assert_not_nil @savings_node.series
assert_equal @checking_node.sum, @checking_node.series.last.value
assert_equal @savings_node.sum, @savings_node.series.last.value
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
assert_equal aggregated_depository_series.values, @depositories.series.values
assert_equal aggregated_assets_series.values, @assets.series.values
end
test "attached series must be a TimeSeries" do
assert_raises(RuntimeError) do
@checking_node.series = []
end
end
test "cannot add time series to non-leaf node" do
assert_raises(RuntimeError) do
@assets.series = TimeSeries.new([])
end
end
test "can only add value node at leaf level of tree" do
root = ValueGroup.new("Root Level")
grandparent = root.add_child_group("Grandparent")
parent = grandparent.add_child_group("Parent")
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
assert_raises(RuntimeError) do
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
test "empty group works" do
group = ValueGroup.new("Root", :usd)
assert_equal "Root", group.name
assert_equal [], group.children
assert_equal 0, group.sum
assert_equal 0, group.avg
assert_equal 100, group.percent_of_total
assert_nil group.parent
end
test "group without value nodes has no value" do
assets = ValueGroup.new("Assets")
depositories = assets.add_child_group("Depositories")
assert_equal 0, assets.sum
assert_equal 0, depositories.sum
end
test "sum equals value at leaf level" do
assert_equal @checking_node.value, @checking_node.sum
assert_equal @savings_node.value, @savings_node.sum
assert_equal @collectable_node.value, @collectable_node.sum
end
test "value is nil at rollup levels" do
assert_not_equal @depositories.value, @depositories.sum
assert_nil @depositories.value
assert_nil @other_assets.value
end
test "generates list of value nodes regardless of level in hierarchy" do
assert_equal [ @checking_node, @savings_node, @collectable_node ], @assets.value_nodes
assert_equal [ @checking_node, @savings_node ], @depositories.value_nodes
assert_equal [ @collectable_node ], @other_assets.value_nodes
end
test "group with value nodes aggregates totals correctly" do
assert_equal Money.new(5000), @checking_node.sum
assert_equal Money.new(20000), @savings_node.sum
assert_equal Money.new(550), @collectable_node.sum
assert_equal Money.new(25000), @depositories.sum
assert_equal Money.new(550), @other_assets.sum
assert_equal Money.new(25550), @assets.sum
end
test "group averages leaf nodes" do
assert_equal Money.new(5000), @checking_node.avg
assert_equal Money.new(20000), @savings_node.avg
assert_equal Money.new(550), @collectable_node.avg
assert_in_delta 12500, @depositories.avg.amount, 0.01
assert_in_delta 550, @other_assets.avg.amount, 0.01
assert_in_delta 8516.67, @assets.avg.amount, 0.01
end
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
test "group calculates percent of parent total" do
assert_equal 100, @assets.percent_of_total
assert_in_delta 97.85, @depositories.percent_of_total, 0.1
assert_in_delta 2.15, @other_assets.percent_of_total, 0.1
assert_in_delta 80.0, @savings_node.percent_of_total, 0.1
assert_in_delta 20.0, @checking_node.percent_of_total, 0.1
assert_equal 100, @collectable_node.percent_of_total
end
test "handles unbalanced tree" do
vehicles = @assets.add_child_group("Vehicles")
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
assert_equal Money.new(25550), @assets.sum
end
test "can attach and aggregate time series" do
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
@checking_node.series = checking_series
@savings_node.series = savings_series
assert_not_nil @checking_node.series
assert_not_nil @savings_node.series
assert_equal @checking_node.sum, @checking_node.series.last.value
assert_equal @savings_node.sum, @savings_node.series.last.value
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
assert_equal aggregated_depository_series.values, @depositories.series.values
assert_equal aggregated_assets_series.values, @assets.series.values
end
test "attached series must be a TimeSeries" do
assert_raises(RuntimeError) do
@checking_node.series = []
end
end
test "cannot add time series to non-leaf node" do
assert_raises(RuntimeError) do
@assets.series = TimeSeries.new([])
end
end
test "can only add value node at leaf level of tree" do
root = ValueGroup.new("Root Level")
grandparent = root.add_child_group("Grandparent")
parent = grandparent.add_child_group("Parent")
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
assert_raises(RuntimeError) do
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
assert_raises(RuntimeError) do
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
assert_raises(RuntimeError) do
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
end
end
end