1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +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:
inherit_gem: { rubocop-rails-omakase: rubocop.yml } rubocop-rails-omakase: rubocop.yml
Layout/IndentationWidth:
Enabled: true
# Overwrite or add rules to create your own house style Layout/IndentationStyle:
# EnforcedStyle: spaces
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` IndentationWidth: 2
# Layout/SpaceInsideArrayLiteralBrackets:
# Enabled: false Layout/IndentationConsistency:
Layout/ElseAlignment: Enabled: true
Enabled: false
Layout/EndAlignment: Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: false Enabled: true

View file

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

View file

@ -2,7 +2,7 @@ class Account::EntriesController < ApplicationController
layout :with_sidebar layout :with_sidebar
before_action :set_account 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 def edit
render entryable_view_path(:edit) render entryable_view_path(:edit)

View file

@ -8,7 +8,7 @@ class Account::TradesController < ApplicationController
end end
def index 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 end
def create def create

View file

@ -2,7 +2,7 @@ class AccountsController < ApplicationController
layout :with_sidebar layout :with_sidebar
include Filterable 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 def index
@institutions = Current.family.institutions @institutions = Current.family.institutions

View file

@ -1,7 +1,7 @@
class CategoriesController < ApplicationController class CategoriesController < ApplicationController
layout :with_sidebar 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 before_action :set_transaction, only: :create
def index def index

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
class Account::EntryBuilder class Account::EntryBuilder
include ActiveModel::Model 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 attr_accessor :type, :date, :qty, :ticker, :price, :amount, :currency, :account, :transfer_account_id

View file

@ -1,7 +1,7 @@
module Account::Entryable module Account::Entryable
extend ActiveSupport::Concern 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) def self.from_type(entryable_type)
entryable_type.presence_in(TYPES).constantize entryable_type.presence_in(TYPES).constantize

View file

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

View file

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

View file

@ -1,7 +1,7 @@
class Account::TransactionBuilder class Account::TransactionBuilder
include ActiveModel::Model 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 attr_accessor :type, :amount, :date, :account, :transfer_account_id

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,100 +3,100 @@
attr_reader :name, :children, :value, :currency attr_reader :name, :children, :value, :currency
def initialize(name, currency = Money.default_currency) def initialize(name, currency = Money.default_currency)
@name = name @name = name
@currency = Money::Currency.new(currency) @currency = Money::Currency.new(currency)
@children = [] @children = []
end end
def sum def sum
return value if is_value_node? return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil? return Money.new(0, currency) if children.empty? && value.nil?
children.sum(&:sum) children.sum(&:sum)
end end
def avg def avg
return value if is_value_node? return value if is_value_node?
return Money.new(0, currency) if children.empty? && value.nil? return Money.new(0, currency) if children.empty? && value.nil?
leaf_values = value_nodes.map(&:value) leaf_values = value_nodes.map(&:value)
leaf_values.compact.sum / leaf_values.compact.size leaf_values.compact.sum / leaf_values.compact.size
end end
def series 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| summed_by_date = children.each_with_object(Hash.new(0)) do |child, acc|
child.series.values.each do |series_value| child.series.values.each do |series_value|
acc[series_value.date] += series_value.value acc[series_value.date] += series_value.value
end
end 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 end
def series=(series) 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 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 } raise "Series must contain money values in the node's currency" unless _series.values.all? { |v| v.value.currency == currency }
@series = _series @series = _series
end end
def value_nodes def value_nodes
return [ self ] unless value.nil? return [ self ] unless value.nil?
children.flat_map { |child| child.value_nodes } children.flat_map { |child| child.value_nodes }
end end
def empty? def empty?
value_nodes.empty? value_nodes.empty?
end end
def percent_of_total 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 end
def add_child_group(name, currency = Money.default_currency) def add_child_group(name, currency = Money.default_currency)
raise "Cannot add subgroup to node with a value" if is_value_node? raise "Cannot add subgroup to node with a value" if is_value_node?
child = self.class.new(name, currency) child = self.class.new(name, currency)
child.parent = self child.parent = self
@children << child @children << child
child child
end end
def add_value_node(original, value, series = nil) def add_value_node(original, value, series = nil)
raise "Cannot add value node to a non-leaf node" unless can_add_value_node? raise "Cannot add value node to a non-leaf node" unless can_add_value_node?
child = self.class.new(original.name) child = self.class.new(original.name)
child.original = original child.original = original
child.value = value child.value = value
child.series = series child.series = series
child.parent = self child.parent = self
@children << child @children << child
child child
end end
def value=(value) def value=(value)
raise "Cannot set value on a non-leaf node" unless is_leaf_node? 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) raise "Value must be an instance of Money" unless value.is_a?(Money)
@value = value @value = value
@currency = value.currency @currency = value.currency
end end
def is_leaf_node? def is_leaf_node?
children.empty? children.empty?
end end
def is_value_node? def is_value_node?
value.present? value.present?
end end
private private
def can_add_value_node? def can_add_value_node?
return false if is_value_node? return false if is_value_node?
children.empty? || children.all?(&:is_value_node?) children.empty? || children.all?(&:is_value_node?)
end end
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 # Raise error when a before_action's only/except options reference missing actions
config.action_controller.raise_on_missing_callback_actions = true config.action_controller.raise_on_missing_callback_actions = true
config.autoload_paths += %w[ test/support ] config.autoload_paths += %w[test/support]
end end

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ class Import::CsvTest < ActiveSupport::TestCase
test "CSV with semicolon column separator" do test "CSV with semicolon column separator" do
csv = Import::Csv.new(valid_csv_str_with_semicolon_separator, col_sep: ";") 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 4, csv.table.size
assert_equal "Paycheck", csv.table[3][1] assert_equal "Paycheck", csv.table[3][1]
end end
@ -84,7 +84,7 @@ class Import::CsvTest < ActiveSupport::TestCase
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings) 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 2, csv.table.size
assert_equal "Amazon stuff", csv.table[1][1] assert_equal "Amazon stuff", csv.table[1][1]
end end
@ -113,7 +113,7 @@ class Import::CsvTest < ActiveSupport::TestCase
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings, ";") 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 2, csv.table.size
assert_equal "Amazon stuff", csv.table[1][1] assert_equal "Amazon stuff", csv.table[1][1]
end end

View file

@ -1,141 +1,141 @@
require "test_helper" require "test_helper"
require "ostruct" require "ostruct"
class ValueGroupTest < ActiveSupport::TestCase class ValueGroupTest < ActiveSupport::TestCase
setup do setup do
# Level 1 # Level 1
@assets = ValueGroup.new("Assets", :usd) @assets = ValueGroup.new("Assets", :usd)
# Level 2 # Level 2
@depositories = @assets.add_child_group("Depositories", :usd) @depositories = @assets.add_child_group("Depositories", :usd)
@other_assets = @assets.add_child_group("Other Assets", :usd) @other_assets = @assets.add_child_group("Other Assets", :usd)
# Level 3 (leaf/value nodes) # Level 3 (leaf/value nodes)
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000)) @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)) @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)) @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 end
test "empty group works" do assert_raises(RuntimeError) do
group = ValueGroup.new("Root", :usd) grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
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
end end
end
end end