diff --git a/.rvmrc b/.rvmrc index ee3b09c..ca9e207 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1,2 +1,3 @@ rvm_gemset_create_on_use_flag=1 -rvm gemset use monetico +rvm gemset use monetico +rvm use 1.9.3@monetico --create diff --git a/Gemfile b/Gemfile index 4a8b71e..b6cc049 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,6 @@ source 'http://rubygems.org' # Specify your gem's dependencies in monetico.gemspec gemspec + +group :development, :test do +end diff --git a/Rakefile b/Rakefile index f57ae68..0d17dde 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,38 @@ +# encoding: utf-8 + #!/usr/bin/env rake require "bundler/gem_tasks" + +require 'rubygems' +require 'bundler' +begin + Bundler.setup(:default, :development) +rescue Bundler::BundlerError => e + $stderr.puts e.message + $stderr.puts "Run `bundle install` to install missing gems" + exit e.status_code +end +require 'rake' + +require 'rspec/core' +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] +end + +RSpec::Core::RakeTask.new(:rcov) do |spec| + spec.pattern = 'spec/**/*_spec.rb' + spec.rcov = true +end + +task :default => :spec + +require 'rake/rdoctask' +Rake::RDocTask.new do |rdoc| + version = File.exist?('VERSION') ? File.read('VERSION') : "" + + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "simple_metar_parser #{version}" + rdoc.rdoc_files.include('README*') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/lib/excel.rb b/lib/excel.rb new file mode 100644 index 0000000..1bf6758 --- /dev/null +++ b/lib/excel.rb @@ -0,0 +1,35 @@ +module Excel + def pmt(rate, nper, pv, fv=0, type=0) + ((-pv * pvif(rate, nper) - fv ) / ((1.0 + rate * type) * fvifa(rate, nper))) + end + + def ipmt(rate, per, nper, pv, fv=0, type=0) + p = pmt(rate, nper, pv, fv, 0); + ip = -(pv * pow1p(rate, per - 1) * rate + p * pow1pm1(rate, per - 1)) + (type == 0) ? ip : ip / (1 + rate) + end + + def ppmt(rate, per, nper, pv, fv=0, type=0) + p = pmt(rate, nper, pv, fv, type) + ip = ipmt(rate, per, nper, pv, fv, type) + p - ip + end + + protected + + def pow1pm1(x, y) + (x <= -1) ? ((1 + x) ** y) - 1 : Math.exp(y * Math.log(1.0 + x)) - 1 + end + + def pow1p(x, y) + (x.abs > 0.5) ? ((1 + x) ** y) : Math.exp(y * Math.log(1.0 + x)) + end + + def pvif(rate, nper) + pow1p(rate, nper) + end + + def fvifa(rate, nper) + (rate == 0) ? nper : pow1pm1(rate, nper) / rate + end +end diff --git a/lib/monetico.rb b/lib/monetico.rb index 04cbbc5..24e19c5 100644 --- a/lib/monetico.rb +++ b/lib/monetico.rb @@ -1,23 +1,22 @@ require "bigdecimal" +require "excel" +require "monetico/money_array" +require "monetico/calculable" require "monetico/loan" require "monetico/version" class Float - def big; BigDecimal(self.to_s); end + include Monetico::Calculable +end - def round_to(x) - (self * 10**x).ceil.to_f / 10**x - end +class BigDecimal + include Monetico::Calculable +end - def round_down(x) - if self >= 0 - (self * 10**x).floor.to_f / 10**x - else - -((-self * 10**x).floor.to_f / 10**x) - end - end +class Fixnum + # usable for #big + include Monetico::Calculable end module Monetico - # Your code goes here... end diff --git a/lib/monetico/calculable.rb b/lib/monetico/calculable.rb new file mode 100644 index 0000000..2ae9eb2 --- /dev/null +++ b/lib/monetico/calculable.rb @@ -0,0 +1,25 @@ +module Monetico + module Calculable + DEF_PREC = 2 + + def big + BigDecimal(self.to_s); + end + + alias_method :to_big, :big + + def round_to(x = DEF_PREC) + (self * 10**x).ceil.to_f / 10**x + end + + alias_method :round_up, :round_to + + def round_down(x = DEF_PREC) + if self >= 0 + (self * 10**x).floor.to_f / 10**x + else + -((-self * 10**x).floor.to_f / 10**x) + end + end + end +end \ No newline at end of file diff --git a/lib/monetico/loan.rb b/lib/monetico/loan.rb index 8b26154..2c3778f 100644 --- a/lib/monetico/loan.rb +++ b/lib/monetico/loan.rb @@ -1,57 +1,215 @@ module Monetico class Loan + include Excel + CADENCE = { monthly: 12, weekly: 52, } - def initialize(amount, interest_rate, no_installments, cadence=:monthly, kind=:desc) + # methods + # - rounded + # - float, used in internal calculations + # - calculate for the first time, float + + def initialize(amount, interest_rate, no_installments, cadence = :monthly, kind = :desc) @amount = amount.big - @interest_rate = interest_rate.big / CADENCE[cadence] + @interest_rate = interest_rate.big / CADENCE[cadence] @no_installments = no_installments @kind = kind end - def capital + private + + # Capital - amount / number of installments + def capital + round capital_real + end + + public :capital + + def capital_real + @capital = calculate_capital if @capital.nil? + @capital + end + + def calculate_capital @amount / @no_installments end + # Total amount of interests def total_interests + round total_interests_real + end + + public :total_interests + + def total_interests_real + @total_interests = calculate_total_interests if @total_interests.nil? + @total_interests + end + + def calculate_total_interests if const? par = (1 + @interest_rate) ** @no_installments - payback_amount = @amount * @interest_rate * par / (par - 1) - - payback_amount * @no_installments - @amount + payback_amount = @amount * @interest_rate * par / (par - 1) + payback_amount * @no_installments - @amount else - 0.5.big * @interest_rate * @no_installments * (@amount + capital) + 0.5.big * @interest_rate * @no_installments * (@amount + capital_real) end end + # Interests for period/installment def interests(idx) - (@amount - (idx - 1) * capital) * @interest_rate + round interests_real(idx) + end + + public :interests + + def interests_real(idx) + @interests = Array.new if @interests.nil? + @interests[idx] = calculate_interests(idx) if @interests[idx].nil? + @interests[idx] + end + + # little refactoring + alias_method :interests_for_period, :interests + + def calculate_interests(idx) + if const? + ipmt(@interest_rate, idx, @no_installments, @amount).abs + else + (@amount - (idx - 1) * capital_real) * @interest_rate + end + end + + # Monthly payment + def monthly_payment + round monthly_payment_real + end + + public :monthly_payment + + def monthly_payment_real + @monthly_payment = calculate_monthly_payment if @monthly_payment_float.nil? + @monthly_payment + end + + def calculate_monthly_payment + pmt(@interest_rate, @no_installments, @amount).abs + end + + # Capital for period + def capital_for_period(idx) + round capital_for_period_real(idx) + end + + public :capital_for_period + + def capital_for_period_real(idx) + # only for desc + return capital_real if not const? + + # only for const + @capitals = Array.new if @capitals.nil? + @capitals[idx] = calculate_capital_for_period(idx) if @capitals[idx].nil? + @capitals[idx] + end + + def calculate_capital_for_period(idx) + ppmt(@interest_rate, idx, @no_installments, @amount).abs + end + + # Amount for period + def amounts(idx) + capital_for_period(idx) + interests(idx) + end + + public :amounts + + def const? + @kind == :const end - def payback(range) - from = range.begin - to = range.end + public 'const?' + + # round money value + def round(v) + MoneyArray.money_round_value(v) + end + + # Loan debug + def to_s + s = "Loan\n" + table = payback_all + table.each_with_index do |t, i| + s += "#{t[:no]}: #{t[:amount].to_f};\t#{t[:balance].to_f};\t#{t[:capital].to_f};\t#{t[:interests].to_f}\n" + end + return s + end + + public :to_s + + # Paybacks items + # all payback items for all + def calculate_payback + from = 1 + to = @no_installments + range = (from..to) + + current_amount = 0.0 if const? par = (1 + @interest_rate) ** @no_installments - range.map do |n| - { no: n, interests: interests(n), amount: @amount * @interest_rate * par / (par - 1) } + res = range.map do |n| + current_amount += monthly_payment_real + { no: n, interests: interests(n), amount: monthly_payment_real, capital: capital_for_period(n), balance: @amount + total_interests_real - current_amount } end else - range.map do |n| - { no: n, interests: interests(n), amount: capital + interests(n) } + res = range.map do |n| + amount = capital_real + interests(n) + current_amount += amount + { no: n, interests: interests(n), amount: amount, capital: capital_real, balance: @amount + total_interests_real - current_amount } end - end + end + @paybacks = res + @paybacks_round = round_paybacks(res) end - def const? - @kind == :const + def payback_real(range) + calculate_payback if @paybacks.nil? + @paybacks[range] + end + + def payback(range) + calculate_payback if @paybacks.nil? + @paybacks_round[range] end - private :const? + + def payback_all + calculate_payback if @paybacks.nil? + @paybacks_round + end + + public :payback_all + + public :payback + + + def round_paybacks(res) + [:interests, :amount, :capital, :balance].each do |k| + tmp = MoneyArray.factory(res.collect { |r| r[k] }) + tmp = tmp.money_round + + res.each_with_index do |r, i| + r[k] = tmp[i] + end + + end + return res + end + end -end \ No newline at end of file +end diff --git a/lib/monetico/money_array.rb b/lib/monetico/money_array.rb new file mode 100644 index 0000000..878f504 --- /dev/null +++ b/lib/monetico/money_array.rb @@ -0,0 +1,55 @@ +module Monetico + class MoneyArray < Array + # round money values to proper precision, ... + def money_round + return self.class.money_round(self) + end + + def array_sum + self.inject(nil) { |sum, x| sum ? sum+x : x } + end + + def self.factory(_from) + obj = self.new + _from.each do |f| + obj << f + end + return obj + end + + # round up currency value + def self.money_round_value(v) + return ((v * 100).round / 100.0).to_big + # TODO delete it + #return ((v * 100).floor / 100.0) + #return ((v * 100).ceil / 100.0) + end + + # round series of values to achieve proper sum + def self.money_round(a) + sum = a.array_sum + rest = 0.0 + result = MoneyArray.new + + a.each do |b| + rounded = money_round_value(b) + real = b + + # compensation + if rest >= 1.0 + rounded -= 1.0 + rest -= 1.0 + elsif rest <= -1.0 + rounded -= -1.0 + rest -= -1.0 + end + + result << rounded.to_big + rest += real - rounded + end + + return result + + end + end +end \ No newline at end of file diff --git a/monetico.gemspec b/monetico.gemspec index 29c704c..86014d7 100644 --- a/monetico.gemspec +++ b/monetico.gemspec @@ -18,4 +18,11 @@ Gem::Specification.new do |gem| gem.name = "monetico" gem.require_paths = ["lib"] gem.version = Monetico::VERSION + + gem.add_development_dependency "rspec" + gem.add_development_dependency "rake" + + gem.add_development_dependency "rdoc" + gem.add_development_dependency "shoulda" + gem.add_development_dependency "simplecov" end diff --git a/spec/loan_spec.rb b/spec/loan_spec.rb new file mode 100644 index 0000000..13d0adf --- /dev/null +++ b/spec/loan_spec.rb @@ -0,0 +1,106 @@ +require "spec_helper" + +describe Monetico::Loan do + describe "const loan" do + before :all do + # tests were updated using + # http://www.money.pl/banki/kalkulatory/kredytowy/ + @loan = Monetico::Loan.new(200000.0, 0.065, 360, :monthly, :const) + end + + it "first monthly_payment" do + @loan.monthly_payment.to_f.should == 1264.14 + @loan.monthly_payment.to_f.should == 1264.14 + end + + it "total_interests returns sum of interests" do + @loan.total_interests.to_f.should == 255088.98 + end + + it "monthly_capital(1) returns capital for first payment" do + @loan.capital_for_period(1).to_f.abs.should == 180.80 + end + + it "monthly_interests(1) returns capital for first payment" do + @loan.interests_for_period(1).to_f.abs.should == 1083.33 + end + + it "monthly_capital(117) returns capital for payment" do + @loan.capital_for_period(117).to_f.abs.should == 338.34 + end + + it "monthly_intersts(117) returns capital for payment" do + @loan.interests_for_period(117).to_f.abs.should == 925.80 + end + + it "monthly_capital(231) returns capital for payment" do + @loan.capital_for_period(231).to_f.abs.should == 626.33 + end + + it "monthly_intersts(231) returns capital for payment" do + @loan.interests_for_period(231).to_f.abs.should == 637.81 + end + + # TODO + it "payback_all returns payback table and last item balance should be 0.0" do + #table = @loan.payback(1..360) # 1 or 0 should be the first payback item? + table = @loan.payback_all + + #table.size.should == 360 + + table[0][:capital].to_f.abs.should == 180.80 + table[0][:interests].to_f.abs.should == 1083.33 + #table[0][:amount].to_f.abs.should == 180.80 + #table[0][:balance].to_f.abs.should == 180.80 + + + #table.last[:capital].to_f.abs.should == 180.80 + #table.last[:interests].to_f.abs.should == 1083.33 + #table.last[:amount].to_f.abs.should == 180.80 + table.last[:balance].to_f.should == 0.0 + end + + end + + describe "desc loan" do + before :all do + @loan = Monetico::Loan.new(200000.0, 0.065, 360, :monthly, :desc) + end + + it "capital returns amount/no_installments" do + @loan.capital.to_f.abs.should == 555.56 + end + + it "total_interests returns sum of interests" do + @loan.total_interests.to_f.should == 195541.67 + end + + it "interests(1) returns intersts for 1 payment" do + @loan.interests(1).to_f.should == 1083.33 + end + + it "amount(1) returns amount for 1 payment" do + @loan.amounts(1).to_f.should == 1638.89 + end + + it "interests(123) returns intersts for 1 payment" do + @loan.interests(123).to_f.should == 716.20 + end + + it "amount(123) returns amount for 1 payment" do + @loan.amounts(123).to_f.should == 1271.76 + end + + it "payback_all returns payback table and last item balance should be 0.0" do + table = @loan.payback_all + table[359][:balance].to_f.should == 0.0 + end + + it "can return debug string using to_s" do + @loan.to_s.should be_kind_of(String) + end + + end + +end + diff --git a/spec/monetico_spec.rb b/spec/monetico_spec.rb new file mode 100644 index 0000000..40554ea --- /dev/null +++ b/spec/monetico_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'rspec' + +describe Monetico do + before :each do + end + + it "should round up/down BigDecimal" do + b = BigDecimal.new('1.0155') + b.round_down(2).should == 1.01 + b.round_up(2).should == 1.02 + b.round_to(2).should == 1.02 + b.round_down.should == 1.01 + b.round_up.should == 1.02 + + b.round_down(1).should == 1.0 + b.round_up(1).should == 1.1 + + b.round_down(0).should == 1.0 + b.round_up(0).should == 2.0 + end + + it "should round up/down Float" do + b = 1.0155 + b.round_down(2).should == 1.01 + b.round_up(2).should == 1.02 + b.round_to(2).should == 1.02 + b.round_down.should == 1.01 + b.round_up.should == 1.02 + + b.round_down(1).should == 1.0 + b.round_up(1).should == 1.1 + + b.round_down(0).should == 1.0 + b.round_up(0).should == 2.0 + end + +end diff --git a/spec/money_round_spec.rb b/spec/money_round_spec.rb new file mode 100644 index 0000000..902ebfa --- /dev/null +++ b/spec/money_round_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe Monetico::MoneyArray do + describe "round simple array" do + before :all do + end + + it "should round simple array" do + ma = Monetico::MoneyArray.new + ma << 10.001 + ma << 9.999 + + ma.money_round.each do |m| + # precision check + rest = (m * 100.00) % 1 + #puts "#{m} - rest #{rest}" + rest.should == 0.0 + end + + end + + it "should round simple loan results" do + ma = Monetico::MoneyArray.new + # also testes using floats + ma << BigDecimal.new("327.8389946270847") + ma << BigDecimal.new("333.3029778708694") + ma << BigDecimal.new("338.85802750205056") + + ma.money_round.each do |m| + # precision check + rest = (m * 100.00) % 1 + #puts "#{m} - rest #{rest}" + rest.should == 0.0 + end + + # sum after rounding + ma.money_round.array_sum.should == 1000.0 + end + + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2df4502 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,6 @@ +require "bundler" +Bundler.setup + +require "rspec" + +Dir[File.expand_path(File.dirname(__FILE__) + "/../lib/**/*.rb")].each { |f| require f }