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 +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +4 -0
- data/ledgerous.gemspec +26 -0
- data/lib/ledgerous.rb +116 -0
- data/lib/ledgerous/version.rb +3 -0
- data/lib/transaction.rb +27 -0
- data/spec/ledgerous_spec.rb +79 -0
- data/spec/spec_helper.rb +1 -0
- data/tasks/rspec.rake +3 -0
- metadata +115 -0
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
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.5
|
data/Gemfile
ADDED
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
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
|
data/lib/transaction.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ledgerous'
|
data/tasks/rspec.rake
ADDED
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
|