ledgerous 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a673d0fc9f70df27fe19f549ece32fc75b304932
4
+ data.tar.gz: 7aa46a23ef8d9a0507729de8daee805d1515dcb2
5
+ SHA512:
6
+ metadata.gz: da961eed6499572265d9bc054a1b315b46c1f27ed44bcb1b42e6795504bb59c933b2d95cc984d06ff46652373a01bc32ad12046cb606dbe46e7ba3e82198b575
7
+ data.tar.gz: 3e599c3cc975ae0906d4ca4bd01dfea0cc74d206f439f30c4b07fcfc79a54814b01e869715060df394dc607ced9441d6e5d2f09548dc2338bb7df7473bdfc52b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ledgerous.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Varun Srinivasan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Ledgerous
2
+
3
+ Ledgerous is an accounting helper that calculates the smallest set of
4
+ payments that will settle open accounts.
5
+
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'ledgerous'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ledgerous
20
+
21
+ ## Usage
22
+
23
+ To use ledgerous, just require it in your project:
24
+
25
+ require 'ledgerous'
26
+
27
+ Let's say that Jane paid for Bob's breakfast, Bob paid for Sue's lunch and Sue
28
+ paid for Jane's dinner. You'd set the transactions up as follows:
29
+
30
+ transactions = []
31
+ transactions << Transaction.new('Jane', Bob, 20) # breakfast
32
+ transactions << Transaction.new('Bob', 'Sue', 50) # lunch
33
+ transactions << Transaction.new('Sue', 'Jane', 35) # dinner
34
+
35
+ Next, create a new ledger, and add each transaction into the ledger:
36
+
37
+ accounts = Ledgerous.new
38
+ transactions.each { |t| accounts.reconcile(t) }
39
+
40
+ You can also set a minimum threshold for debts. Any debts equal to or lower
41
+ than this amount will be discarded:
42
+
43
+ accounts.threshold = 2
44
+
45
+ You can keep adding transactions as they happen. When you're ready to settle accounts, just call:
46
+
47
+ puts accounts.settle
48
+
49
+
50
+
51
+ ## Contributing
52
+
53
+ 1. Fork it
54
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
55
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
56
+ 4. Push to the branch (`git push origin my-new-feature`)
57
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ Dir.glob('tasks/**/*.rake').each(&method(:import))
4
+
data/ledgerous.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ledgerous/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "ledgerous"
8
+ s.version = Ledgerous::VERSION
9
+ s.authors = ["Varun Srinivasan"]
10
+ s.email = ["varunsrin@gmail.com"]
11
+ s.summary = %q{Accounting module to settle account balances}
12
+ s.homepage = "https://github.com/varunsrin/ledgerous"
13
+ s.license = "MIT"
14
+
15
+ s.required_ruby_version = ">= 1.9.2"
16
+
17
+ s.files = `git ls-files`.split($/)
18
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "bundler", "~> 1.3"
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "rspec"
25
+ s.add_development_dependency "pry"
26
+ end
data/lib/ledgerous.rb ADDED
@@ -0,0 +1,116 @@
1
+ require "ledgerous/version"
2
+ require "transaction"
3
+
4
+ class Ledgerous
5
+
6
+ attr_reader :accounts, :payments
7
+
8
+ attr_accessor :threshold
9
+
10
+ def initialize
11
+ @accounts = Hash.new(0)
12
+ @threshold = 0
13
+ end
14
+
15
+ # Returns true if there are no open accounts above or below the threshold
16
+ def empty?
17
+ @accounts.each do |k, v|
18
+ return false if v.abs > @threshold
19
+ end
20
+ return true
21
+ end
22
+
23
+ # Returns all accounts with negative balances
24
+ def debtors
25
+ @accounts.select {|_, amt| amt < 0}
26
+ end
27
+
28
+ # Returns all accounts wiht positive balances
29
+ def creditors
30
+ @accounts.select {|_, amt| amt > 0}
31
+ end
32
+
33
+ # Reconciles the ledgerous with the transaction
34
+ def reconcile(t)
35
+ update_or_create_account(t.creditor, t.amount)
36
+ update_or_create_account(t.debtor, -t.amount)
37
+ end
38
+
39
+ # Returns list of transactions that will settle open accounts
40
+ def settle
41
+ s = Marshal.load(Marshal.dump(self))
42
+ s.settle!
43
+ end
44
+
45
+ # Returns a list of transactions that will settle open accounts, and applies
46
+ # them to clear accounts
47
+ def settle!
48
+ payments = []
49
+ next_payment do |n|
50
+ reconcile(n)
51
+ payments << n.reverse
52
+ end
53
+ payments
54
+ end
55
+
56
+ protected
57
+
58
+ # Returns the first payment that can clear two accounts
59
+ def double_clear_payment
60
+ debtors.each do |d|
61
+ creditors.each do |c|
62
+ return Transaction.new(c[0], d[0], c[1]) if c[1] == -d[1]
63
+ end
64
+ end
65
+ return false
66
+ end
67
+
68
+ # Returns the first payment that clears one account, but will result in two
69
+ # accounts being cleared next
70
+ def double_clear_lookahead
71
+ s = Marshal.load(Marshal.dump(self))
72
+ s.single_clears do |p|
73
+ s.reconcile(p)
74
+ s.double_clear_payment ? (return p) : s.reconcile(p.reverse)
75
+ end
76
+ return false
77
+ end
78
+
79
+ # Yields payments that will clear one account
80
+ def single_clears
81
+ debtors.each do |d|
82
+ creditors.each do |c|
83
+ yield Transaction.new(c[0], d[0], [c[1], -d[1]].min)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Returns the first payment that will clear one account
89
+ def single_clear
90
+ single_clears { |n| return n }
91
+ end
92
+
93
+ private
94
+
95
+ # Updates the account, or creates it if it does not exist
96
+ def update_or_create_account(name, amount)
97
+ @accounts[name.to_sym] += amount
98
+ end
99
+
100
+ # Finds the next best payment to settle the account
101
+ #
102
+ # * Cancel a payment that removes two accounts
103
+ # * Cancel a payment that removes one account, but will result in two being
104
+ # removed on the next payment
105
+ # * Cancel a payment that removes any account
106
+ def next_payment
107
+ until empty?
108
+ if double_clear_payment
109
+ yield double_clear_payment
110
+ next
111
+ end
112
+ yield double_clear_lookahead ? double_clear_lookahead : single_clear
113
+ end
114
+ end
115
+
116
+ end
@@ -0,0 +1,3 @@
1
+ class Ledgerous
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,27 @@
1
+ # Transaction
2
+ # A single transaction
3
+
4
+ class Transaction
5
+
6
+ attr_reader :debtor, :creditor, :amount
7
+
8
+ def initialize(debtor, creditor, amount)
9
+ @debtor = debtor
10
+ @creditor = creditor
11
+ @amount = amount
12
+ end
13
+
14
+ # Returns a transaction that is the reverse of the current one
15
+ def reverse
16
+ Transaction.new(@creditor, @debtor, @amount)
17
+ end
18
+
19
+ def ==(obj)
20
+ (
21
+ obj.amount == self.amount &&
22
+ obj.creditor == self.creditor &&
23
+ obj.debtor == self.debtor
24
+ )
25
+ end
26
+
27
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ledgerous do
4
+ subject { Ledgerous.new }
5
+
6
+ describe '#empty?' do
7
+
8
+ it 'returns true if the ledger is empty' do
9
+ expect(subject.empty?).to eq(true)
10
+ end
11
+
12
+ it 'returns false if the ledger has any accounts' do
13
+ subject.reconcile(Transaction.new('A', 'B', 5))
14
+ expect(subject.empty?).to eq(false)
15
+ end
16
+
17
+ it 'returns true if no accounts are greater than the threshold' do
18
+ subject.reconcile(Transaction.new('A', 'B', 5))
19
+ subject.threshold = 5
20
+ expect(subject.empty?).to eq(true)
21
+ end
22
+
23
+ end
24
+
25
+ describe '#reconcile' do
26
+
27
+ before :each do
28
+ subject.reconcile(Transaction.new('A', 'B', 20))
29
+ end
30
+
31
+ it 'adds two accounts to the ledger' do
32
+ expect(subject.accounts.length).to eq(2)
33
+ expect(subject.accounts[:A]).to eq(-20)
34
+ expect(subject.accounts[:B]).to eq(20)
35
+ end
36
+
37
+ it 'updates accounts if they already exist' do
38
+ subject.reconcile(Transaction.new('A', 'B', 20))
39
+ expect(subject.accounts.length).to eq(2)
40
+ expect(subject.accounts[:A]).to eq(-40)
41
+ expect(subject.accounts[:B]).to eq(40)
42
+ end
43
+
44
+ it 'eliminates transitive debts' do
45
+ subject.reconcile(Transaction.new('B', 'C', 20))
46
+ expect(subject.debtors.length && subject.creditors.length).to eq(1)
47
+ end
48
+ end
49
+
50
+
51
+ describe '#settle' do
52
+
53
+ before do
54
+ # A: 10, B: -2, C: -12, D:4, E: 25, F: 50, G: -75, H: 7, I: -7
55
+ subject.reconcile(Transaction.new('B', 'A', 2))
56
+ subject.reconcile(Transaction.new('C', 'A', 8))
57
+ subject.reconcile(Transaction.new('G', 'E', 25))
58
+ subject.reconcile(Transaction.new('G', 'F', 50))
59
+ subject.reconcile(Transaction.new('C', 'D', 4))
60
+ subject.reconcile(Transaction.new('I', 'H', 7))
61
+ @payments = subject.settle
62
+ end
63
+
64
+ it 'chooses one payment that cancels two accounts first' do
65
+ expect(@payments[0]).to eq(Transaction.new(:I, :H, 7))
66
+ end
67
+
68
+ it 'chooses two payments that cancel three accounts second' do
69
+ expect(@payments[1]).to eq(Transaction.new(:G, :E, 25))
70
+ expect(@payments[2]).to eq(Transaction.new(:G, :F, 50))
71
+ end
72
+
73
+ it 'chooses one payment that cancels one account next' do
74
+ expect(@payments[3]).to eq(Transaction.new(:B, :A, 2))
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1 @@
1
+ require 'ledgerous'
data/tasks/rspec.rake ADDED
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ledgerous
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Varun Srinivasan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - varunsrin@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".ruby-version"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - ledgerous.gemspec
83
+ - lib/ledgerous.rb
84
+ - lib/ledgerous/version.rb
85
+ - lib/transaction.rb
86
+ - spec/ledgerous_spec.rb
87
+ - spec/spec_helper.rb
88
+ - tasks/rspec.rake
89
+ homepage: https://github.com/varunsrin/ledgerous
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 1.9.2
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.4.3
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Accounting module to settle account balances
113
+ test_files:
114
+ - spec/ledgerous_spec.rb
115
+ - spec/spec_helper.rb