bean_sprout 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4733d40d0fe67374824dead281510cda6053ec71
4
- data.tar.gz: e8865c4596897622f821e6c379c3d766732def86
3
+ metadata.gz: 8f8703dcac93814b06f78ef5c7f6081d41e3809c
4
+ data.tar.gz: 3d50d400de7ff74305f88db06a0feef6c6816658
5
5
  SHA512:
6
- metadata.gz: d13adb40a9589069865f7c72e217fc92017fc1db491040dc87fba2f30e8a6fa73dff009cb10a63b6a5956243e1a9a7885b5549ac87ff1f1652ba45602d201b39
7
- data.tar.gz: 8a608a9d8d6822c9c04f4b0ae26493eedef5b9ce3c5ceb2ba39a05a35f4532e8295cc92a19486ea94cd8b12bf3192374917969428b04177ef59edfad2f08e87c
6
+ metadata.gz: 73181700e7e1a3e20b552bc4010cdd4820718fc19c7990968183fad0e8f3e4aa99a49b043099e261e5a60556b06a7d5e56de2744516f54babbb0776dcba93c41
7
+ data.tar.gz: 85fb4800498552ed1a3a5f3b491c07456467044f048682ea3198387bef22f1ff407634449f8723ca9991d5646939242f7c58fd9b9a9c7c0edc24559b76e32757
@@ -1,31 +1,46 @@
1
- require 'bean_sprout/struct_from_hash_mixin'
2
- require 'bean_sprout/struct_archive_mixin'
1
+ require 'bean_sprout/package_private'
3
2
 
4
3
  module BeanSprout
5
- class Account < Struct.new(:currency, :external_id, :other_data)
6
- include StructFromHashMixin
7
- include StructArchiveMixin
4
+ # TODO: abstract :id?
5
+ class Bean
6
+ include PackagePrivate::InternalClass
8
7
 
9
- class BalanceHolder < Struct.new(:value)
10
- end
8
+ attr_reader :id, :balance, :currency, :sprouts
9
+
10
+ define_public_interface :Account
11
11
 
12
- def initialize *fields
13
- super *fields
14
- @entries = []
15
- @balance = BalanceHolder.new(0)
12
+ def initialize id, currency
13
+ @id = id
14
+ @currency = currency
15
+
16
+ @sprouts = Set.new
17
+ @balance = 0
16
18
  end
17
19
 
18
- def append_entry entry
19
- @entries.push entry
20
- @balance.value += entry.accurate_amount
20
+ def grow sprout
21
+ @sprouts.add sprout
22
+ @balance += sprout.amount
21
23
  end
22
24
 
23
- def entries
24
- @entries.clone
25
+ def pick sprout
26
+ @sprouts.delete sprout
27
+ @balance -= sprout.amount
25
28
  end
29
+ end
30
+
31
+ # Public interface.
32
+ class Account < PackagePrivate::PublicInterfaceBase
33
+ def_default_delegators :balance, :currency
34
+ def_private_default_delegators :sprouts
26
35
 
27
- def balance
28
- @balance.value
36
+ def entries
37
+ sprouts.map do |sprout|
38
+ if block_given?
39
+ yield sprout.to_entry
40
+ else
41
+ sprout.to_entry
42
+ end
43
+ end
29
44
  end
30
45
  end
31
46
  end
@@ -1,5 +1,4 @@
1
- require 'bean_sprout/struct_from_hash_mixin'
2
- require 'bean_sprout/struct_archive_mixin'
1
+ require 'bean_sprout/package_private'
3
2
  require 'bigdecimal'
4
3
  require 'bigdecimal/util'
5
4
 
@@ -10,16 +9,31 @@ module BeanSprout
10
9
  # 2. The amount to be added to the account balance, in local currency;
11
10
  # 3. Convention rate from local currency to the base currency;
12
11
  # 4. Other arbitrary data.
13
- class Entry < Struct.new(:account, :amount, :rate, :other_data)
14
- include StructFromHashMixin
15
- include StructArchiveMixin
12
+ class Sprout
13
+ include PackagePrivate::InternalClass
14
+ attr_reader :id, :bean, :amount, :rate
16
15
 
17
- def rate_or_one
18
- rate or 1
16
+ define_public_interface :Entry
17
+
18
+ def initialize id, bean, amount, rate = 1
19
+ @id = id
20
+ @bean = bean
21
+ @amount = amount.to_d
22
+ @rate = rate
23
+ end
24
+
25
+ def unified_amount
26
+ amount * rate
19
27
  end
28
+ end
29
+
30
+ # Public Interface.
31
+ class Entry < PackagePrivate::PublicInterfaceBase
32
+ def_default_delegators :amount, :unified_amount, :rate
33
+ def_private_default_delegators :bean
20
34
 
21
- def accurate_amount
22
- @accurate_amount ||= amount.to_d
35
+ def account
36
+ bean.to_account
23
37
  end
24
38
  end
25
39
  end
@@ -0,0 +1,24 @@
1
+ require 'forwardable'
2
+
3
+ module BeanSprout
4
+ class ForwardableDelegate
5
+ extend Forwardable
6
+
7
+ def initialize obj
8
+ @target = obj
9
+ end
10
+
11
+ class << self
12
+ def def_default_delegators *args
13
+ def_delegators :@target, *args
14
+ end
15
+
16
+ def def_private_default_delegators *args
17
+ def_default_delegators *args
18
+ private *args
19
+ end
20
+
21
+ private :def_default_delegators, :def_private_default_delegators
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,129 @@
1
+ require 'bean_sprout/sparse_array'
2
+ require 'bean_sprout/account'
3
+ require 'bean_sprout/entry'
4
+ require 'bean_sprout/transaction'
5
+
6
+ module BeanSprout
7
+ class Ledger
8
+ attr_reader :base_currency
9
+
10
+ def initialize base_currency
11
+ @base_currency = base_currency
12
+
13
+ @beans = SparseArray.new
14
+ @sprout_bunches = SparseArray.new
15
+ @sprouts = SparseArray.new
16
+ end
17
+
18
+ def create_account currency, other_data: nil
19
+ bean = @beans.store do |next_id|
20
+ Bean.new(next_id, currency)
21
+ end
22
+
23
+ Account.new(bean, other_data)
24
+ end
25
+
26
+ def create_entry account, amount, rate = nil, other_data: nil
27
+ bean = get_target account
28
+ if not @beans.has_key? bean.id
29
+ raise "Unkown account #{bean.to_account} refered."
30
+ end
31
+
32
+ if not (rate or bean.currency == base_currency)
33
+ raise "Rate must be specified if account is not in base currency " +
34
+ "#{base_currency}."
35
+ end
36
+ rate ||= 1
37
+
38
+
39
+ sprout = @sprouts.store do |next_id|
40
+ Sprout.new(next_id, bean, amount, rate)
41
+ end
42
+
43
+ Entry.new(sprout, other_data)
44
+ end
45
+
46
+ def create_transaction entries, other_data: nil
47
+ sprouts = entries.map do |entry| get_target entry end
48
+ sprout_bunch = @sprout_bunches.store do |next_id|
49
+ SproutBunch.new(next_id, sprouts)
50
+ end
51
+
52
+ Transaction.new(sprout_bunch, other_data)
53
+ end
54
+
55
+ def transfer from_acc, to_acc, amount
56
+ if from_acc.currency != @base_currency || to_acc.currency != @base_currency
57
+ raise "Cannot transfer between two forex accounts."
58
+ end
59
+
60
+ entry0 = create_entry from_acc, -amount
61
+ entry1 = create_entry to_acc, amount
62
+ commit_entries [entry0, entry1]
63
+ end
64
+
65
+ def base_currency_forex_transfer from_acc, to_acc, from_amount, to_amount
66
+ raise "Amount can't be 0." unless from_amount != 0 && to_amount != 0
67
+
68
+ rate0 = rate1 = nil
69
+ if from_acc.currency == @base_currency
70
+ rate1 = from_amount / to_amount
71
+ elsif to_acc.currency == @base_currency
72
+ rate0 = to_amount / from_amount
73
+ else
74
+ raise "Forex transfer must be to or from an account of base currency."
75
+ end
76
+
77
+ entry0 = create_entry from_acc, -from_amount, rate0
78
+ entry1 = create_entry to_acc, to_amount, rate1
79
+ commit_entries [entry0, entry1]
80
+ end
81
+
82
+ # TODO: clients can't access ID.
83
+ def account id
84
+ @beans.fetch(id).to_account
85
+ end
86
+
87
+ # TODO: clients can't access ID.
88
+ def transaction id
89
+ @sprout_bunches.fetch(id).to_transaction
90
+ end
91
+
92
+ # TODO: test
93
+ def accounts
94
+ @beans.values.map do |bean|
95
+ if block_given?
96
+ yield bean.to_account
97
+ else
98
+ bean.to_account
99
+ end
100
+ end
101
+ end
102
+
103
+ # TODO: test
104
+ def transactions
105
+ @sprout_bunches.values.map do |sprout_bunch|
106
+ if block_given?
107
+ yield sprout_bunch.to_transaction
108
+ else
109
+ sprout_bunch.to_transaction
110
+ end
111
+ end
112
+ end
113
+
114
+ def dummy_account
115
+ @dummy_account ||= create_account @base_currency, other_data: "This is a dummy account."
116
+ end
117
+
118
+ private
119
+ def get_target obj
120
+ obj.instance_variable_get :@target
121
+ end
122
+
123
+ def commit_entries entries
124
+ trans = create_transaction entries
125
+ trans.commit
126
+ trans
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,41 @@
1
+ require 'bean_sprout/forwardable_delegate'
2
+
3
+ module BeanSprout
4
+ # Implements the concept of package private methods.
5
+ module PackagePrivate
6
+ # A module to be included by the delegatee.
7
+ module InternalClass
8
+ # The public interface, set by PublicInterfaceBase.
9
+ attr_reader :public_interface
10
+
11
+ def self.included klass
12
+ klass.extend ClassMethods
13
+ end
14
+
15
+ def bind_public_interface public_interface
16
+ raise "Cannot bind public interface to null." if public_interface.nil?
17
+ raise "Cannot bind public interface twice." unless @public_interface.nil?
18
+ @public_interface = public_interface
19
+ end
20
+
21
+ module ClassMethods
22
+ def define_public_interface klass_name
23
+ define_method "to_#{(klass_name.to_s.split "::").last.downcase}" do
24
+ @public_interface
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # A base class for delegator classes.
31
+ class PublicInterfaceBase < ForwardableDelegate
32
+ def initialize obj, other_data = nil
33
+ super(obj)
34
+ obj.bind_public_interface self
35
+ @other_data = other_data
36
+ end
37
+
38
+ attr_accessor :other_data
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ require 'forwardable'
2
+
3
+ module BeanSprout
4
+ class SparseArray
5
+ extend Forwardable
6
+
7
+ def_delegators :@entities, :each, :has_key?, :each_value, :values
8
+
9
+ def initialize index_offset = 0
10
+ @entities = {}
11
+ @index = index_offset
12
+ end
13
+
14
+ def store
15
+ index = next_index
16
+ @entities[index] = yield index
17
+ end
18
+
19
+ def fetch index
20
+ @entities[index]
21
+ end
22
+
23
+ def fetch! index
24
+ raise "Unkown index #{index}." unless @entities.has_key? index
25
+ fetch index
26
+ end
27
+
28
+ private
29
+ def next_index
30
+ @index += 1
31
+ end
32
+ end
33
+ end
@@ -1,29 +1,87 @@
1
- require 'bean_sprout/struct_archive_mixin'
1
+ require 'bean_sprout/package_private'
2
2
 
3
3
  module BeanSprout
4
- class Transaction < Struct.new(:entries, :other_data)
5
- include StructFromHashMixin
6
- include StructArchiveMixin
4
+ class SproutBunch
5
+ include PackagePrivate::InternalClass
7
6
 
8
- alias entries_data entries
9
- private :entries_data
7
+ attr_reader :sprouts
8
+
9
+ define_public_interface :Transaction
10
+
11
+ class NotBalancedError < StandardError
12
+ end
13
+
14
+ class IllegalStateError < StandardError
15
+ end
16
+
17
+ def initialize id, sprouts
18
+ @id = id
19
+ @sprouts = sprouts
20
+ end
10
21
 
11
- public
12
22
  def balanced?
13
23
  balance = 0
14
- entries_data.each do |entry|
15
- balance += entry.accurate_amount / entry.rate_or_one
24
+ @sprouts.each do |sprout|
25
+ balance += sprout.unified_amount
16
26
  end
17
27
  balance == 0
18
28
  end
19
29
 
20
30
  def balanced!
21
- raise "#{entries_data} is not balanced." unless balanced?
31
+ raise NotBalancedError.new("#{@sprouts} not balanced.") unless balanced?
32
+ end
33
+
34
+ def plant
35
+ balanced!
36
+ raise IllegalStateError, "Can't plant twice." if @in_place
37
+ sprouts.each do |sprout|
38
+ sprout.bean.grow sprout
39
+ end
40
+ @in_place = true
41
+ end
42
+
43
+ def remove
44
+ balanced!
45
+ raise IllegalStateError, "Must plant before remove." unless @in_place
46
+ sprouts.each do |sprout|
47
+ sprout.bean.pick sprout
48
+ end
49
+ @in_place = false
50
+ end
51
+ end
52
+
53
+ class Transaction < PackagePrivate::PublicInterfaceBase
54
+ def_default_delegators :balanced?
55
+ def_private_default_delegators :sprouts, :plant, :remove
56
+
57
+ def commit
58
+ begin
59
+ plant
60
+ rescue SproutBunch::NotBalancedError
61
+ raise "Cannot commit an imbalance transaction."
62
+ rescue SproutBunch::IllegalStateError
63
+ raise "Cannot commit a transaction more than once."
64
+ end
65
+ end
66
+
67
+ def revert
68
+ begin
69
+ remove
70
+ rescue SproutBunch::NotBalancedError
71
+ raise "Cannot revert an imbalance transaction."
72
+ rescue SproutBunch::IllegalStateError
73
+ raise "Cannot revert a transaction more than once."
74
+ end
22
75
  end
23
76
 
24
77
  def entries
25
- entries_data.clone
78
+ sprouts.map do |sprout|
79
+ if block_given?
80
+ yield sprout.to_entry
81
+ else
82
+ sprout.to_entry
83
+ end
84
+ end
26
85
  end
27
86
  end
28
87
  end
29
-
@@ -1,3 +1,3 @@
1
1
  module BeanSprout
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
data/lib/bean_sprout.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  require 'bean_sprout/account'
2
2
  require 'bean_sprout/entry'
3
- require 'bean_sprout/glass_jar'
4
- require 'bean_sprout/struct_archive_mixin'
5
- require 'bean_sprout/struct_from_hash_mixin'
3
+ require 'bean_sprout/ledger'
6
4
  require 'bean_sprout/transaction'
7
5
  require 'bean_sprout/version'
8
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bean_sprout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liqing Muyi
@@ -64,9 +64,10 @@ files:
64
64
  - lib/bean_sprout.rb
65
65
  - lib/bean_sprout/account.rb
66
66
  - lib/bean_sprout/entry.rb
67
- - lib/bean_sprout/glass_jar.rb
68
- - lib/bean_sprout/struct_archive_mixin.rb
69
- - lib/bean_sprout/struct_from_hash_mixin.rb
67
+ - lib/bean_sprout/forwardable_delegate.rb
68
+ - lib/bean_sprout/ledger.rb
69
+ - lib/bean_sprout/package_private.rb
70
+ - lib/bean_sprout/sparse_array.rb
70
71
  - lib/bean_sprout/transaction.rb
71
72
  - lib/bean_sprout/version.rb
72
73
  homepage: http://github.com/muyiliqing/bean_sprout
@@ -1,123 +0,0 @@
1
- module BeanSprout
2
- class GlassJar
3
- # TODO: maybe each transaction can have its own
4
- # currency.
5
- attr_reader :base_currency
6
-
7
- def initialize base_currency
8
- @base_currency = base_currency
9
-
10
- @accounts = {}
11
- @account_id = 0
12
- @account_external_ids = {}
13
-
14
- @transactions = {}
15
- @transaction_id = 0
16
-
17
- @entries = {}
18
- end
19
-
20
- def open_account account
21
- account.archive_in_glass_jar self, next_account_id
22
- @accounts[account.id] = account
23
- @account_external_ids[account.external_id] = account
24
- end
25
-
26
- def create_account currency, external_id = nil, other_data: nil
27
- account = Account.new currency, external_id, other_data
28
- open_account account
29
- end
30
-
31
- def accounts
32
- @accounts.values
33
- end
34
-
35
- def account id
36
- @accounts[id]
37
- end
38
-
39
- def account_for external_id
40
- @account_external_ids[external_id]
41
- end
42
-
43
- def transactions
44
- @transactions.values
45
- end
46
-
47
- def transaction id
48
- @transactions[id]
49
- end
50
-
51
- def entries
52
- @entries.values
53
- end
54
-
55
- def entry id
56
- @entries[id]
57
- end
58
-
59
- AccountEntitySizeBits = 30
60
- def commit_transaction trans
61
- raise "Creating transaction with no entries." if trans.entries.empty?
62
- if trans.entries.size >= (1 << AccountEntitySizeBits)
63
- raise "Creating transaction with too many entries."
64
- end
65
-
66
- # Validate trans status.
67
- trans.entries.each do |entry|
68
- valid_account! entry.account
69
- valid_rate! entry
70
- end
71
- trans.balanced!
72
-
73
- trans.archive_in_glass_jar self, next_transaction_id
74
- trans.entries.each_with_index do |entry, index|
75
- entry_id = (trans.id << AccountEntitySizeBits) + index
76
- entry.archive_in_glass_jar self, entry_id
77
-
78
- @entries[entry_id] = entry
79
- account(entry.account).append_entry(entry)
80
- end
81
- @transactions[trans.id] = trans
82
- end
83
-
84
- def create_transaction *entries, other_data: nil
85
- trans = Transaction.new(entries, other_data)
86
- commit_transaction(trans)
87
- end
88
-
89
- def new_account *args
90
- Account.new args
91
- end
92
-
93
- def new_transaction *args
94
- Transaction.new args
95
- end
96
-
97
- def new_entry *args
98
- Entry.new args
99
- end
100
-
101
- private
102
- def next_account_id
103
- @account_id += 1
104
- end
105
-
106
- def next_transaction_id
107
- @transaction_id += 1
108
- end
109
-
110
- def valid_account! account
111
- if not @accounts.has_key? account
112
- raise "Unkown account #{account} refered."
113
- end
114
- end
115
-
116
- def valid_rate! entry
117
- if not (entry.rate or account(entry.account).currency == base_currency)
118
- raise "Rate must be specified if entry is not in base currency " +
119
- "#{base_currency}.\n Entry is #{entry}"
120
- end
121
- end
122
- end
123
- end
@@ -1,16 +0,0 @@
1
- module BeanSprout
2
- module StructArchiveMixin
3
- def self.included klass
4
- klass.class_eval do
5
- attr_reader :id
6
- attr_reader :glass_jar
7
- end
8
- end
9
-
10
- def archive_in_glass_jar glass_jar, id
11
- @glass_jar = glass_jar
12
- @id = id
13
- freeze
14
- end
15
- end
16
- end
@@ -1,14 +0,0 @@
1
- module BeanSprout
2
- module StructFromHashMixin
3
- def self.included klass
4
- klass.extend ClassMethods
5
- end
6
-
7
- module ClassMethods
8
- def from_hash hash
9
- new(*hash.values_at(*members))
10
- end
11
- end
12
- end
13
- end
14
-